From 874e0e6a5888501d6a75cae426410f8c841f0db9 Mon Sep 17 00:00:00 2001 From: vntrevx <20063774+vntrevx@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:31:57 +0900 Subject: [PATCH 1/2] fix(automation): stop evidence-only PR accumulation --- .github/ci-contract.json | 6 +- .github/scripts/test_ci_contract.py | 11 +- .github/workflows/ci.yml | 16 +- .github/workflows/nfi-compatibility.yml | 63 ---- docs/ci-policy.md | 11 +- docs/future-nfi-compatibility.md | 13 +- planning/acceptance-commands.json | 2 +- .../compatibility_automation.py | 11 +- scripts/compatibility_review_pr.py | 306 ------------------ tests/test_compatibility_automation.py | 12 +- tests/test_compatibility_review_pr.py | 115 ------- tests/test_nfi_compatibility_workflow.py | 19 +- 12 files changed, 57 insertions(+), 528 deletions(-) delete mode 100644 scripts/compatibility_review_pr.py delete mode 100644 tests/test_compatibility_review_pr.py diff --git a/.github/ci-contract.json b/.github/ci-contract.json index 3499ea6b..56cd4097 100644 --- a/.github/ci-contract.json +++ b/.github/ci-contract.json @@ -42,8 +42,10 @@ "prefixes": [], "files": [ ".github/workflows/nfi-compatibility.yml", - "scripts/compatibility_review_pr.py", - "tests/test_compatibility_review_pr.py", + "python/nfi_backtest_engine/compatibility_automation.py", + "scripts/compatibility_issue.py", + "tests/test_compatibility_automation.py", + "tests/test_compatibility_issue.py", "tests/test_nfi_compatibility_workflow.py" ] }, diff --git a/.github/scripts/test_ci_contract.py b/.github/scripts/test_ci_contract.py index f6e98af9..182705ce 100644 --- a/.github/scripts/test_ci_contract.py +++ b/.github/scripts/test_ci_contract.py @@ -47,8 +47,10 @@ def test_policy_and_documentation_use_policy_lane(self) -> None: def test_compatibility_automation_uses_focused_lane(self) -> None: paths = [ ".github/workflows/nfi-compatibility.yml", - "scripts/compatibility_review_pr.py", - "tests/test_compatibility_review_pr.py", + "python/nfi_backtest_engine/compatibility_automation.py", + "scripts/compatibility_issue.py", + "tests/test_compatibility_automation.py", + "tests/test_compatibility_issue.py", "tests/test_nfi_compatibility_workflow.py", ".github/workflows/ci.yml", ] @@ -65,7 +67,7 @@ def test_runtime_or_unknown_paths_fail_closed_to_code(self) -> None: ["planning/futures-discovery-policy.json"], [".github/workflows/release.yml"], [ - "scripts/compatibility_review_pr.py", + "scripts/compatibility_issue.py", "rust/crates/nfi-sim-core/Cargo.toml", ], [], @@ -117,8 +119,7 @@ def test_workflow_exposes_stable_aggregate_and_lane_conditions(self) -> None: self.assertIn("needs.changes.outputs.code_changes == 'true'", workflow) self.assertIn("Run focused compatibility automation tests", workflow) self.assertNotIn("pull_request_target:", workflow) - self.assertIn('paths-ignore:', workflow) - self.assertIn('- "planning/compatibility-reviews/**"', workflow) + self.assertNotIn('paths-ignore:', workflow) for path in self.contract["push"]["release_paths"]: self.assertIn(f'- "{path}"', workflow) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 840c76a8..b4bcbbbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,6 @@ on: - ".github/product-release-contract.json" - ".github/release-candidate-contract.json" pull_request: - paths-ignore: - # Evidence-only semantic review PRs do not need CI until implementation code - # is added. Ignoring their PR event prevents a redundant action_required run. - - "planning/compatibility-reviews/**" workflow_dispatch: permissions: @@ -109,18 +105,22 @@ jobs: if: needs.changes.outputs.automation_changes == 'true' run: >- uv run pytest -q - tests/test_compatibility_review_pr.py + tests/test_compatibility_automation.py + tests/test_compatibility_issue.py tests/test_nfi_compatibility_workflow.py tests/test_release_contract.py - name: Lint and type-check the focused automation surface if: needs.changes.outputs.automation_changes == 'true' run: | uv run ruff check \ - scripts/compatibility_review_pr.py \ - tests/test_compatibility_review_pr.py \ + python/nfi_backtest_engine/compatibility_automation.py \ + scripts/compatibility_issue.py \ + tests/test_compatibility_automation.py \ + tests/test_compatibility_issue.py \ tests/test_nfi_compatibility_workflow.py uv run basedpyright --level error \ - scripts/compatibility_review_pr.py + python/nfi_backtest_engine/compatibility_automation.py \ + scripts/compatibility_issue.py python: name: Python ${{ matrix.os }} / ${{ matrix.python-version }} diff --git a/.github/workflows/nfi-compatibility.yml b/.github/workflows/nfi-compatibility.yml index 686824ff..1664d03b 100644 --- a/.github/workflows/nfi-compatibility.yml +++ b/.github/workflows/nfi-compatibility.yml @@ -472,67 +472,6 @@ jobs: if-no-files-found: error retention-days: 30 - semantic-review: - name: Open ${{ matrix.trading_mode }} generic semantic review Draft PR - needs: - - discover - - targeted - - canary - if: >- - always() - && needs.discover.outputs.changed == 'true' - && needs.targeted.result == 'success' - && needs.canary.result == 'success' - runs-on: ubuntu-latest - strategy: - fail-fast: false - max-parallel: 1 - matrix: - trading_mode: - - spot - - futures - permissions: - contents: write - pull-requests: write - steps: - - name: Check out checked engine identity - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ needs.discover.outputs.engine_sha }} - persist-credentials: true - - name: Download mode automation decision - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - name: latest-nfi-x7-${{ matrix.trading_mode }}-targeted - path: .compatibility - - name: Select semantic review route - id: review - run: | - if [ "$(jq -r .automation_route \ - .compatibility/automation-decision-${{ matrix.trading_mode }}.json)" \ - = "semantic_review_draft_pr" ]; then - echo "required=true" >> "${GITHUB_OUTPUT}" - else - echo "required=false" >> "${GITHUB_OUTPUT}" - fi - - name: Install locked Python environment - if: steps.review.outputs.required == 'true' - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - with: - python-version: "3.12" - - name: Open evidence-only Draft PR - if: steps.review.outputs.required == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - uv sync --extra dev --frozen - uv run python scripts/compatibility_review_pr.py \ - --decision \ - .compatibility/automation-decision-${{ matrix.trading_mode }}.json \ - --repo-root "${GITHUB_WORKSPACE}" \ - --repository "${GITHUB_REPOSITORY}" \ - --base main - publish: name: Preserve ledger and reconcile compatibility issue needs: @@ -678,7 +617,6 @@ jobs: - x7 - targeted - canary - - semantic-review - publish if: always() runs-on: ubuntu-latest @@ -706,5 +644,4 @@ jobs: --stage "x7=${{ needs.x7.result }}" \ --stage "targeted=${{ needs.targeted.result }}" \ --stage "canary=${{ needs.canary.result }}" \ - --stage "semantic-review=${{ needs.semantic-review.result }}" \ --stage "publish=${{ needs.publish.result }}" diff --git a/docs/ci-policy.md b/docs/ci-policy.md index 7e1fec1c..51654542 100644 --- a/docs/ci-policy.md +++ b/docs/ci-policy.md @@ -13,8 +13,9 @@ The classifier selects the cheapest lane that covers every changed path: - `policy-only`: documentation plus explicitly listed repository metadata and the required-CI implementation itself; a dependency-free policy self-test runs on Ubuntu; -- `automation-only`: the allowlisted NFI semantic-review workflow, publisher, and - focused tests; one Ubuntu job runs only their tests, lint, and type check; +- `automation-only`: the allowlisted NFI compatibility routing, issue reconciler, + workflow, and focused tests; one Ubuntu job runs only their tests, lint, and type + check; - `code`: every unlisted path, runtime source, tests, schemas, fixtures, build inputs, and installers; the full Python matrix on Linux, Windows, and macOS, static/Rust checks, and native full-parity fixtures run. @@ -35,6 +36,12 @@ or downloading data. Unknown infrastructure failures still fail and remain visib Non-candidate artifacts expire after one day; raw candles, caches, container layers, and traces are never uploaded. +Blocked generic semantics are retained in the append-only compatibility ledger and +one automatically reconciled `nfi-compatibility` issue. They do not create evidence- +only Draft PRs. Pull requests are reserved for a compact independently exact fixture +candidate or an implementation change, so a scheduled watcher cannot grow the PR +queue. + Pull requests are the normal required-check surface. A protected merge is not tested a second time on `main`; push-triggered CI is limited to version and product-release contract paths that require a same-commit release check. Manual dispatch remains diff --git a/docs/future-nfi-compatibility.md b/docs/future-nfi-compatibility.md index e1075ecf..de1fd567 100644 --- a/docs/future-nfi-compatibility.md +++ b/docs/future-nfi-compatibility.md @@ -97,17 +97,18 @@ trade surface exact, full-state exact가 모두 필요하다. 원본 fixture는 Signal/Grind 번호, pair, timerange 또는 기대 결과를 분기값으로 사용하지 않는다. - `native_exact`: 변경 branch 도달과 trade surface/full-state exact가 모두 참일 때만 허용 -- `semantic_review_draft_pr`: 새 opcode 또는 generic lowering 검토가 필요하며 실행은 official-only +- `semantic_review_issue`: 새 opcode 또는 generic lowering 검토가 필요하며 실행은 official-only - `bounded_discovery`: 정적 lowering은 가능하지만 exact branch fixture가 부족함 - `exact_fixture_draft_pr`: discovery가 독립 exact 후보를 만들었으나 병합 전 검토가 필요함 - `external_data_deferred`: 외부 데이터 재시도 보류이며 exact 증거가 아님 - `official_only`: 탐색이 끝났거나 현재 Native exact를 증명할 수 없음 -semantic review Draft PR은 compact 결정문과 검토 요구사항만 추가한다. 런타임 -의미론을 추측 생성하지 않으며 자동 승인·병합하지 않는다. maintainer가 범용 -opcode/lowerer, 단위 테스트, 공식 fixture를 추가하고 Required CI와 exact 검증을 -통과해야 Native로 승격된다. 외부 데이터 보류를 재사용할 때도 저장된 결정문의 -`execution_route=official_only`, `exact=false`를 다시 검사한다. +semantic review는 append-only ledger와 자동 조정되는 단일 compatibility issue에 +기록한다. evidence-only Draft PR은 만들지 않는다. maintainer가 범용 opcode/lowerer, +단위 테스트, 공식 fixture를 추가하고 Required CI와 exact 검증을 통과해야 Native로 +승격된다. PR은 독립 exact fixture 후보 또는 실제 구현 변경에만 사용한다. 외부 데이터 +보류를 재사용할 때도 저장된 결정문의 `execution_route=official_only`, `exact=false`를 +다시 검사한다. ## Managed exit의 단계적 Native 전환 diff --git a/planning/acceptance-commands.json b/planning/acceptance-commands.json index d495b0bb..151f168e 100644 --- a/planning/acceptance-commands.json +++ b/planning/acceptance-commands.json @@ -97,7 +97,7 @@ "mutates_external_state": false }, "future_nfi_compatibility_tests": { - "command": "uv run pytest -q tests/test_official_fallback.py tests/test_strategy_diff.py tests/test_targeted_verification.py tests/test_compatibility_identity.py tests/test_compatibility_canary.py tests/test_compatibility_automation.py tests/test_compatibility_review_pr.py tests/test_resolve_upstream_source.py tests/test_compatibility_fixture_registry.py tests/test_compatibility_issue.py tests/test_workflow_health_issue.py tests/test_nfi_compatibility_workflow.py tests/test_compatibility_qualification.py tests/test_state_machine_ir.py tests/test_state_machine_shadow.py tests/test_dynamic_grind_levels.py tests/test_stateful_coverage.py tests/test_strategy_compatibility.py tests/test_generic_adapter.py tests/test_probe_capture.py tests/test_research_reference.py", + "command": "uv run pytest -q tests/test_official_fallback.py tests/test_strategy_diff.py tests/test_targeted_verification.py tests/test_compatibility_identity.py tests/test_compatibility_canary.py tests/test_compatibility_automation.py tests/test_resolve_upstream_source.py tests/test_compatibility_fixture_registry.py tests/test_compatibility_issue.py tests/test_workflow_health_issue.py tests/test_nfi_compatibility_workflow.py tests/test_compatibility_qualification.py tests/test_state_machine_ir.py tests/test_state_machine_shadow.py tests/test_dynamic_grind_levels.py tests/test_stateful_coverage.py tests/test_strategy_compatibility.py tests/test_generic_adapter.py tests/test_probe_capture.py tests/test_research_reference.py", "working_directory": ".", "platform": "any", "timeout_seconds": 1200, diff --git a/python/nfi_backtest_engine/compatibility_automation.py b/python/nfi_backtest_engine/compatibility_automation.py index 13cd8739..c98e7deb 100644 --- a/python/nfi_backtest_engine/compatibility_automation.py +++ b/python/nfi_backtest_engine/compatibility_automation.py @@ -88,7 +88,7 @@ def classify_compatibility_automation( if exact: route = "native_exact" elif not static_compatible: - route = "semantic_review_draft_pr" + route = "semantic_review_issue" review_kind = "new_opcode" if added_opcodes else "generic_lowering" elif discovery_status is None or discovery_status == "budget_exhausted": route = "bounded_discovery" @@ -103,9 +103,7 @@ def classify_compatibility_automation( native_allowed = route == "native_exact" draft_kind = ( - review_kind - if route == "semantic_review_draft_pr" - else "exact_fixture" + "exact_fixture" if route == "exact_fixture_draft_pr" else None ) @@ -154,6 +152,7 @@ def classify_compatibility_automation( "bounded_discovery_required": route == "bounded_discovery", "draft_pr_allowed": draft_kind is not None, "draft_pr_kind": draft_kind, + "issue_required": route == "semantic_review_issue", "official_fallback_available": True, "automatic_semantic_merge_allowed": False, "external_data_deferred_is_exact": False, @@ -348,9 +347,9 @@ def _message(route: str, *, review_kind: str | None) -> str: ), "official_only": "Native exactness is unproven; use the announced official fallback.", } - if route == "semantic_review_draft_pr": + if route == "semantic_review_issue": subject = "new generic opcode" if review_kind == "new_opcode" else "generic lowering" - return f"Static lowering is blocked; open a {subject} review Draft PR." + return f"Static lowering is blocked; track the {subject} in the compatibility issue." return messages[route] diff --git a/scripts/compatibility_review_pr.py b/scripts/compatibility_review_pr.py deleted file mode 100644 index 77b3087a..00000000 --- a/scripts/compatibility_review_pr.py +++ /dev/null @@ -1,306 +0,0 @@ -#!/usr/bin/env python3 -"""Open one evidence-only Draft PR for a blocked generic semantic review.""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -from collections.abc import Mapping -from pathlib import Path -from typing import Any - -from nfi_backtest_engine.canonical import read_json, write_json - -_FINGERPRINT = re.compile(r"[0-9a-f]{64}") -_SHA = re.compile(r"[0-9a-f]{40}") -_MODES = {"spot", "futures"} -_REVIEW_KINDS = {"new_opcode", "generic_lowering"} - - -def build_review_plan( - decision: Mapping[str, Any], - repository_root: str | Path, -) -> dict[str, Any]: - """Validate one fail-closed decision and choose its allowlisted PR path.""" - - root = Path(repository_root).resolve() - if decision.get("schema_version") != "1.0.0": - raise ValueError("compatibility automation decision schema is unsupported") - mode = decision.get("trading_mode") - review_kind = decision.get("review_kind") - action_fingerprint = decision.get("action_fingerprint") - identity = decision.get("identity") - action = decision.get("action") - verification = decision.get("verification") - if ( - mode not in _MODES - or review_kind not in _REVIEW_KINDS - or _FINGERPRINT.fullmatch(str(action_fingerprint)) is None - or not isinstance(identity, Mapping) - or not isinstance(action, Mapping) - or not isinstance(verification, Mapping) - ): - raise ValueError("semantic review decision identity is invalid") - if ( - decision.get("automation_route") != "semantic_review_draft_pr" - or decision.get("execution_route") != "official_only" - or verification.get("exact") is not False - or action.get("native_promotion_allowed") is not False - or action.get("draft_pr_allowed") is not True - or action.get("draft_pr_kind") != review_kind - or action.get("automatic_semantic_merge_allowed") is not False - ): - raise ValueError("semantic review decision is not fail-closed") - upstream_sha = identity.get("upstream_sha") - engine_sha = identity.get("engine_sha") - if ( - _SHA.fullmatch(str(upstream_sha)) is None - or _SHA.fullmatch(str(engine_sha)) is None - ): - raise ValueError("semantic review commit identity is invalid") - suffix = str(action_fingerprint)[:16] - destination = Path("planning") / "compatibility-reviews" / f"{mode}-{suffix}.json" - if (root / destination).exists(): - raise ValueError("semantic review destination already exists on the base branch") - return { - "branch": f"automation/{mode}-semantic-review-{suffix}", - "destination": destination.as_posix(), - "trading_mode": mode, - "review_kind": review_kind, - "action_fingerprint": action_fingerprint, - "upstream_sha": upstream_sha, - "engine_sha": engine_sha, - "document": { - "schema_version": "compatibility-semantic-review-v1", - "claim_boundary": ( - "Automation evidence for generic IR review only. Native support remains " - "blocked until independent trade-surface and full-state exact proof." - ), - "decision": dict(decision), - "review_requirements": [ - "implement only a source-structural generic opcode or lowerer", - "add a focused unit test and an official captured fixture", - "prove changed-branch, trade-surface, and full-state exactness", - "obtain human review and Required CI; never merge automatically", - ], - }, - } - - -def publish_review( - plan: Mapping[str, Any], - *, - repository_root: str | Path, - repository: str, - base: str, -) -> dict[str, Any]: - """Push an evidence-only branch and open a Draft PR without approving it.""" - - root = Path(repository_root).resolve() - branch = str(plan["branch"]) - pending = _existing_pending_review(repository, plan) - if pending is not None: - return { - "branch": pending["headRefName"], - "pull_request_url": pending["url"], - "created": False, - "state": pending["state"], - "ci_dispatched": False, - "deduplicated": True, - } - existing = _existing_pr(repository, branch) - if existing is not None: - return { - "branch": branch, - "pull_request_url": existing["url"], - "created": False, - "state": existing["state"], - "ci_dispatched": False, - } - remote_exists = bool( - _run( - ["git", "ls-remote", "--heads", "origin", f"refs/heads/{branch}"], - cwd=root, - ).strip() - ) - if not remote_exists: - if _run(["git", "status", "--porcelain"], cwd=root).strip(): - raise ValueError("semantic review publisher requires a clean worktree") - _run(["git", "switch", "--create", branch], cwd=root) - destination = root / str(plan["destination"]) - write_json(destination, plan["document"]) - _run(["git", "add", "--", str(plan["destination"])], cwd=root) - staged = _run(["git", "diff", "--cached", "--name-only"], cwd=root).splitlines() - if staged != [str(plan["destination"])]: - raise ValueError("semantic review publisher staged a path outside its allowlist") - _run( - [ - "git", - "-c", - "user.name=github-actions", - "-c", - "user.email=41898282+github-actions@users.noreply.github.com", - "commit", - "-m", - f"chore({plan['trading_mode']}): propose generic semantic review", - ], - cwd=root, - ) - _run(["git", "push", "origin", f"HEAD:{branch}"], cwd=root) - title = ( - f"chore({plan['trading_mode']}): review {plan['review_kind']} compatibility" - ) - marker = ( - "" - ) - body = ( - f"{marker}\n\n" - "Automated evidence-only review for a blocked generic Native lowering.\n\n" - f"- Upstream: `{plan['upstream_sha']}`\n" - f"- Engine: `{plan['engine_sha']}`\n" - f"- Review kind: `{plan['review_kind']}`\n" - f"- Fingerprint: `{plan['action_fingerprint']}`\n\n" - "This PR does not add Native semantics and must never be approved or merged " - "automatically. A maintainer must implement the generic behavior and attach " - "independent exact evidence." - ) - url = _run( - [ - "gh", - "pr", - "create", - "--repo", - repository, - "--base", - base, - "--head", - branch, - "--title", - title, - "--body", - body, - "--draft", - ], - cwd=root, - ).strip() - return { - "branch": branch, - "pull_request_url": url, - "created": True, - "state": "OPEN", - "ci_dispatched": False, - "deduplicated": False, - } - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--decision", type=Path, required=True) - parser.add_argument("--repo-root", type=Path, required=True) - parser.add_argument("--repository", required=True) - parser.add_argument("--base", default="main") - args = parser.parse_args() - decision = read_json(args.decision) - if not isinstance(decision, dict): - raise ValueError("compatibility automation decision must be an object") - plan = build_review_plan(decision, args.repo_root) - result = publish_review( - plan, - repository_root=args.repo_root, - repository=args.repository, - base=args.base, - ) - print(json.dumps(result, sort_keys=True)) - return 0 - - -def _existing_pr(repository: str, branch: str) -> dict[str, Any] | None: - records = json.loads( - _run( - [ - "gh", - "pr", - "list", - "--repo", - repository, - "--head", - branch, - "--state", - "all", - "--json", - "number,state,url", - ] - ) - ) - if not isinstance(records, list): - raise ValueError("GitHub returned an invalid semantic review PR list") - record = records[0] if records else None - return dict(record) if isinstance(record, Mapping) else None - - -def _existing_pending_review( - repository: str, - plan: Mapping[str, Any], -) -> dict[str, Any] | None: - records = json.loads( - _run( - [ - "gh", - "pr", - "list", - "--repo", - repository, - "--state", - "open", - "--limit", - "100", - "--json", - "body,headRefName,state,url", - ] - ) - ) - if not isinstance(records, list): - raise ValueError("GitHub returned an invalid open semantic review PR list") - return find_pending_review(records, plan) - - -def find_pending_review( - records: list[object], - plan: Mapping[str, Any], -) -> dict[str, Any] | None: - """Find an already-open review for the same upstream semantic gap.""" - - marker = f"\n\n" - "- Upstream: `" + "a" * 40 + "`\n" - "- Engine: `" + "2" * 40 + "`\n" - "- Review kind: `new_opcode`\n" - ) - record = { - "body": body, - "headRefName": "automation/futures-semantic-review-existing", - "state": "OPEN", - "url": "https://example.invalid/pull/1", - } - - assert MODULE.find_pending_review([record], plan) == record - - changed_upstream = dict(plan) - changed_upstream["upstream_sha"] = "9" * 40 - assert MODULE.find_pending_review([record], changed_upstream) is None diff --git a/tests/test_nfi_compatibility_workflow.py b/tests/test_nfi_compatibility_workflow.py index b63ae5e6..d9718b23 100644 --- a/tests/test_nfi_compatibility_workflow.py +++ b/tests/test_nfi_compatibility_workflow.py @@ -116,21 +116,20 @@ def test_workflow_health_is_separate_from_compatibility_blockers() -> None: assert "nfi-automation-health" in text assert "scripts/workflow_health_issue.py" in text assert '--stage "canary=${{ needs.canary.result }}"' in text - assert '--stage "semantic-review=${{ needs.semantic-review.result }}"' in text + assert "semantic-review" not in text assert "--stage \"publish=${{ needs.publish.result }}\"" in text -def test_workflow_routes_blocked_generic_semantics_to_evidence_only_draft_pr() -> None: +def test_workflow_routes_blocked_semantics_to_one_issue_and_never_opens_review_prs() -> None: text = WORKFLOW.read_text(encoding="utf-8") targeted = text[text.index(" targeted:") : text.index(" canary:")] - review = text[text.index(" semantic-review:") : text.index(" publish:")] + publish = text[text.index(" publish:") : text.index(" health:")] assert "scripts/compatibility_automation.py" in targeted assert "automation-decision-${{ matrix.trading_mode }}.json" in targeted - assert "semantic_review_draft_pr" in review - assert "scripts/compatibility_review_pr.py" in review - assert "actions: write" not in review - assert "pull-requests: write" in review - assert "gh pr merge" not in review - assert "gh pr review" not in review - assert "auto-merge" not in review + assert "semantic_review_issue" not in text # Routing stays inside the decision JSON. + assert "scripts/compatibility_issue.py" in publish + assert "Reconcile deduplicated compatibility issue" in publish + assert "compatibility_review_pr.py" not in text + assert "pull-requests: write" not in text + assert "gh pr create" not in text From c4890dd5fce15c9afe35940d565851dd015a88e5 Mon Sep 17 00:00:00 2001 From: vntrevx <20063774+vntrevx@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:33:21 +0900 Subject: [PATCH 2/2] fix(ci): classify retired PR publisher paths --- .github/ci-contract.json | 2 ++ .github/scripts/test_ci_contract.py | 2 ++ docs/ci-policy.md | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/ci-contract.json b/.github/ci-contract.json index 56cd4097..7be0eaba 100644 --- a/.github/ci-contract.json +++ b/.github/ci-contract.json @@ -44,8 +44,10 @@ ".github/workflows/nfi-compatibility.yml", "python/nfi_backtest_engine/compatibility_automation.py", "scripts/compatibility_issue.py", + "scripts/compatibility_review_pr.py", "tests/test_compatibility_automation.py", "tests/test_compatibility_issue.py", + "tests/test_compatibility_review_pr.py", "tests/test_nfi_compatibility_workflow.py" ] }, diff --git a/.github/scripts/test_ci_contract.py b/.github/scripts/test_ci_contract.py index 182705ce..cc0a1726 100644 --- a/.github/scripts/test_ci_contract.py +++ b/.github/scripts/test_ci_contract.py @@ -49,8 +49,10 @@ def test_compatibility_automation_uses_focused_lane(self) -> None: ".github/workflows/nfi-compatibility.yml", "python/nfi_backtest_engine/compatibility_automation.py", "scripts/compatibility_issue.py", + "scripts/compatibility_review_pr.py", "tests/test_compatibility_automation.py", "tests/test_compatibility_issue.py", + "tests/test_compatibility_review_pr.py", "tests/test_nfi_compatibility_workflow.py", ".github/workflows/ci.yml", ] diff --git a/docs/ci-policy.md b/docs/ci-policy.md index 51654542..41271101 100644 --- a/docs/ci-policy.md +++ b/docs/ci-policy.md @@ -40,7 +40,8 @@ Blocked generic semantics are retained in the append-only compatibility ledger a one automatically reconciled `nfi-compatibility` issue. They do not create evidence- only Draft PRs. Pull requests are reserved for a compact independently exact fixture candidate or an implementation change, so a scheduled watcher cannot grow the PR -queue. +queue. The removed publisher and test paths remain explicit classification tombstones +so the deletion commit itself also stays on this focused automation lane. Pull requests are the normal required-check surface. A protected merge is not tested a second time on `main`; push-triggered CI is limited to version and product-release