From 46d2727828d20680cdc6d3a1ff1b419c3ec2bca4 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:04:12 +0100 Subject: [PATCH] Validate final browser evidence without rewriting reviewed changes Governance run 37016171545 rejected the original malformed timestamp even though a later reviewed receipt corrected it for identical runtime bytes. Use the existing fail-closed integration-range guard in CI while retaining per-commit canonical checks. Exercise the workflow shell with valid additive correction and invalid missing-base or unverified-content fixtures. Change-source: pulse-maintainer --- .github/workflows/canonical-governance.yml | 23 +++++++--- .../CANONICAL_DEVELOPMENT_PROTOCOL.md | 10 +++-- .../browser_verification_guard_test.py | 42 +++++++++++++++++++ 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/.github/workflows/canonical-governance.yml b/.github/workflows/canonical-governance.yml index fe6db7666..c8982d77e 100644 --- a/.github/workflows/canonical-governance.yml +++ b/.github/workflows/canonical-governance.yml @@ -130,16 +130,27 @@ jobs: status=1 fi fi - if ! git diff-tree --no-commit-id --name-only -r "${commit}" \ - | python3 scripts/release_control/browser_verification_guard.py \ - --files-from-stdin --commit "${commit}"; then - echo "Browser verification guard failed for commit ${commit}." - status=1 - fi echo "::endgroup::" done exit ${status} + - name: Validate final frontend browser evidence + shell: bash + env: + WORKFLOW_OUTPUT_1: ${{ steps.diff.outputs.range }} + run: | + set -euo pipefail + range="${WORKFLOW_OUTPUT_1}" + if [ -z "${range}" ]; then + printf '' | python3 scripts/release_control/browser_verification_guard.py --files-from-stdin + exit 0 + fi + # Preserve reviewed commits: a later non-merge receipt can correct + # metadata for the same verified bytes. Require coverage of every + # final frontend path, including merge resolutions; never waive proof. + python3 scripts/release_control/browser_verification_guard.py \ + --base "${range%%...*}" --commit "${range##*...}" + - name: Validate Pulse Intelligence release-gate schema run: python3 scripts/release_control/pulse_intelligence_gate.py --validate-only --matrix docs/release-control/v6/internal/pulse-intelligence-release-gate.json diff --git a/docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md b/docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md index 73761c66b..8b27e3619 100644 --- a/docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md +++ b/docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md @@ -246,8 +246,8 @@ must update that receipt with: 7. a UTC verification timestamp and `result: "passed"` `scripts/release_control/browser_verification_guard.py` enforces the receipt -against the staged index locally and against each changed commit in canonical -governance CI. A stale receipt, later source edit, incomplete path coverage, +against the staged index locally and against the final integration range in +canonical governance CI. Canonical contract completion remains checked per commit. A stale receipt, later source edit, incomplete path coverage, missing responsive viewport, or omitted state/interaction evidence is a hard failure. Agents may print a non-passing receipt skeleton with `python3 scripts/release_control/browser_verification_guard.py --print-template` @@ -265,7 +265,11 @@ bound to that commit's parent and to that commit's tree. Receipts written by merge commits are never evidence, because a conflict resolution has no browser run behind it. Content that changed after its browser pass, including a correction commit, a conflict resolution, or two changes to one file, needs a -fresh receipt for the final content in its own non-merge commit. +fresh receipt for the final content in its own non-merge commit. CI uses this +same range mode so an additive receipt correction can retain reviewed commit +identities. An invalid historical receipt contributes no coverage; a later +valid receipt must still bind its own parent and the exact final content. An +unavailable range base fails closed rather than narrowing browser coverage. A source diff that is byte-for-byte the locked Prettier output of its parent has no rendered behavior or visual delta and does not require a new browser diff --git a/scripts/release_control/browser_verification_guard_test.py b/scripts/release_control/browser_verification_guard_test.py index fde2d6a28..b0076a672 100644 --- a/scripts/release_control/browser_verification_guard_test.py +++ b/scripts/release_control/browser_verification_guard_test.py @@ -9,6 +9,7 @@ import os from pathlib import Path import subprocess import tempfile +import textwrap import unittest from unittest.mock import patch @@ -292,6 +293,47 @@ class IntegrationRangeTest(unittest.TestCase): self.commit("record proof") self.assertEqual(self.run_range()[0], 0) + def run_workflow_browser_step(self, *, base: str | None = None) -> subprocess.CompletedProcess: + workflow = (REPO_ROOT / ".github/workflows/canonical-governance.yml").read_text() + step = workflow.split(" - name: Validate final frontend browser evidence\n", 1)[1] + step = step.split("\n - name:", 1)[0] + command = textwrap.dedent(step.split(" run: |\n", 1)[1]) + for name in ("browser_verification_guard.py", "format_staged_frontend.py"): + self.write("scripts/release_control/" + name, + (REPO_ROOT / "scripts/release_control" / name).read_text()) + return subprocess.run( + ["bash", "-c", command], cwd=self.repo_root, capture_output=True, text=True, + env={**self.env, "WORKFLOW_OUTPUT_1": + f"{base or self.base}...{self.git('rev-parse', 'HEAD')}"}, + ) + + def test_workflow_accepts_additive_receipt_correction_but_not_unverified_edit(self) -> None: + self.write(CHANGED_PATH, "export const a = 1;\n") + self.write_receipt([CHANGED_PATH]) + payload = json.loads((self.repo_root / RECEIPT_PATH).read_text()) + payload["verified_at"] = "2026-10-02T13:00:00+00:00" + self.write(RECEIPT_PATH, json.dumps(payload)) + original = self.commit("frontend with malformed timestamp") + self.assertNotEqual(self.run_workflow_browser_step().returncode, 0) + + self.write_receipt([CHANGED_PATH]) + self.commit("correct receipt without rewriting source history") + corrected = self.run_workflow_browser_step() + self.assertEqual(corrected.returncode, 0, corrected.stdout + corrected.stderr) + self.git("merge-base", "--is-ancestor", original, "HEAD") + + self.write(CHANGED_PATH, "export const a = 2;\n") + self.commit("unverified later edit") + self.assertNotEqual(self.run_workflow_browser_step().returncode, 0) + + def test_workflow_checks_merge_resolution_and_rejects_missing_range_base(self) -> None: + self.merge_advanced_main_into_candidate() + self.assertEqual(self.run_workflow_browser_step().returncode, 0) + self.assertNotEqual(self.run_workflow_browser_step(base="b" * 40).returncode, 0) + self.write(CHANGED_PATH, "export const a = 3;\n") + self.commit("unverified integration edit") + self.assertNotEqual(self.run_workflow_browser_step().returncode, 0) + def test_blocks_frontend_edit_after_merge_without_fresh_proof(self) -> None: self.merge_advanced_main_into_candidate() self.write(CHANGED_PATH, "export const a = 2;\n")