diff --git a/CHANGELOG.md b/CHANGELOG.md index bf192f6a9e..e7c3af134f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### Noema review publication revalidates exact-head uniqueness + +- `inspect_and_review` now re-fetches the live pull request and repeats the trusted Noema receipt check immediately before review submission. A concurrent exact-head review published during model work can no longer be duplicated. The same repair removes the unconsumed `statusCheckRollup` query field. Proposed in ContextualWisdomLab/.github#1482. + ### Failed-check finding names the Strix sandbox instead of the gateway - `opencode-review-dispatch.yml`'s `emit_strix_provider_failure_finding` rendered one fixed finding for every `STRIX_PROVIDER_UNAVAILABLE` line, whose Root cause read "The contextual-orchestrator gateway or its discovered provider pool was unavailable for this run". `#1953` had just given the Strix sandbox bootstrap failure its own second verdict token (`STRIX_SANDBOX_UNAVAILABLE`) precisely because that attribution is wrong for it -- the sandbox container never reaches its Caido proxy, so the run dies before the gateway serves anything -- and this consumer re-applied the wrong attribution one step downstream, into the review findings and the failure census. The emitter now branches on the second token: a sandbox verdict gets a finding that names Strix's sandbox, says the verdict does not name the gateway, and tells the reader not to change gateway or provider configuration on its strength. A `STRIX_PROVIDER_UNAVAILABLE` line without the token keeps its existing text verbatim, so the gateway class has no regression surface. No test covered this finding text at all before (`gateway or its discovered provider pool` matched nothing under `tests/`); `tests/test_opencode_dispatch_strix_sandbox_finding.py` now runs the production emitter from the published run block and pins both directions plus the no-signal case. Refs #1953, #1935. diff --git a/docs/doctoring/noema-final-submission-revalidation.md b/docs/doctoring/noema-final-submission-revalidation.md new file mode 100644 index 0000000000..73fc0165aa --- /dev/null +++ b/docs/doctoring/noema-final-submission-revalidation.md @@ -0,0 +1,39 @@ +# Noema exact-head review submission is revalidated at the write boundary + +검토 기준일: **2026-09-07** + +## Problem + +GitHub Actions concurrency cancellation is best-effort. A run already executing +model work can reach the review POST after another run has published a valid +Noema review for the same head. Head equality alone does not prove that the +write is still unique. + +## Decision + +The Noema gate keeps the pre-model duplicate check, then re-fetches the live +pull request after model work. Immediately before `submit_review`, it repeats +the trusted exact-head Noema receipt check. A concurrent receipt returns +successfully without publishing a duplicate. Closed or moved heads continue to +fail closed before this check. + +The unused `statusCheckRollup` selection is removed because review publication +does not consume check contexts. + +## Verification contract + +`test_inspect_and_review_rechecks_for_a_concurrent_submission_before_posting` +models two live reads: the first has no receipt and the second contains a +trusted receipt for the same head. The POST recorder must remain empty. Hosted +exact-head checks remain mandatory. + +## Status + +**Proposed** in ContextualWisdomLab/.github#1482. Protected `main` remains the +release authority. + +## Reference + +GitHub. (n.d.). *Control the concurrency of workflows and jobs*. GitHub Docs. +Retrieved September 7, 2026, from +https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1cc9e20313..59078d2d05 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -7,6 +7,12 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +### 2026-09-07 Noema final-submission concurrency amendment + +- **Gap:** best-effort Actions cancellation cannot stop a run already inside model work, so another run can publish an exact-head Noema review before the first run reaches its POST. +- **Action:** ContextualWisdomLab/.github#1482 re-fetches the live PR after model work and re-checks the independent Noema receipt immediately before submission; the unused status-check query is removed. +- **Status:** Proposed; exact-head hosted Checks, independent review, ordinary protected integration, and post-merge current-main verification remain required. + ## 1. 근거와 범위 ### 1.1 우선순위가 높은 근거 diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 5ab7e830f3..0530299db3 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -329,27 +329,6 @@ def graphql(query: str, **fields: str | int) -> dict[str, Any]: commit { oid } } } - statusCheckRollup { - contexts(first: 100) { - nodes { - __typename - ... on CheckRun { - name - status - conclusion - checkSuite { - workflowRun { - workflow { name } - } - } - } - ... on StatusContext { - context - state - } - } - } - } } } } @@ -1795,6 +1774,12 @@ def inspect_and_review(repo: str, number: int, expected_head: str) -> int: except RuntimeError: print("Pull request closed or its head changed during review; stale verdict was not published.") return 0 + if existing_noema_review(current_pr, actor): + print( + "Current head already has a Noema review immediately before submission; " + "duplicate verdict was not published." + ) + return 0 submit_review(repo, number, current_pr, actor, verdict) return 0 diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index e8a0dd6f59..20312a5a8a 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -1933,6 +1933,61 @@ def test_inspect_and_review_does_not_wait_for_other_reviews_or_checks(monkeypatc assert calls +def test_inspect_and_review_rechecks_for_a_concurrent_submission_before_posting( + monkeypatch, +): + """Do not publish when another run reviewed the same head during model work.""" + head = "a" * 40 + first_pr = make_pr(headRefOid=head) + marker = "\n".join( + [ + noema.NOEMA_REVIEW_FOOTER_MARKER, + "- Result: APPROVE", + f"- Head SHA: `{head}`", + "- Reviewer credential: `test`", + "- Actor: `noema`", + "", + f"", + ] + ) + reviewed_pr = make_pr( + headRefOid=head, + reviews={"nodes": [review(commit=head, login="noema", body=marker)]}, + ) + pull_requests = iter((first_pr, reviewed_pr)) + submissions = [] + monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: next(pull_requests)) + monkeypatch.setattr(noema, "current_actor", lambda: "noema") + monkeypatch.setattr(noema, "fetch_diff", lambda repo, number: ("diff", False)) + monkeypatch.setattr( + noema, + "fetch_changed_files", + lambda repo, number: [("tool.py", "modified")], + ) + monkeypatch.setattr( + noema, + "build_review_context", + lambda repo, number, pr, changed_files=None: "context", + ) + monkeypatch.setattr( + noema, + "call_llm", + lambda *args, **kwargs: { + "decision": "approve", + "summary": "ok", + "findings": [], + }, + ) + monkeypatch.setattr( + noema, + "submit_review", + lambda *args, **kwargs: submissions.append(args), + ) + + assert noema.inspect_and_review("owner/repo", 7, head) == 0 + assert submissions == [] + + def test_stale_trigger_stops_before_identity_or_model_work(monkeypatch): monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: make_pr(headRefOid="b" * 40)) monkeypatch.setattr(