mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-02 20:29:43 +00:00
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
This commit is contained in:
parent
3b42a3da67
commit
46d2727828
3 changed files with 66 additions and 9 deletions
23
.github/workflows/canonical-governance.yml
vendored
23
.github/workflows/canonical-governance.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue