test: require human review for M1 acceptance

This commit is contained in:
Codex 2026-10-21 18:37:50 +02:00
parent 3c26e1bdab
commit a5a8e4086d
6 changed files with 80 additions and 11 deletions

View file

@ -23,6 +23,8 @@ The repository now provides:
non-executing artifact-verification report, including the Rayforge pin and
bundle, installer, addon-verification and source checkpoint-manifest fields;
- a fail-closed manual desktop-acceptance report and validator;
complete operator evidence is only ready for review until a named,
timezone-stamped human review is recorded;
- pinned upstream contract/license manifests and a drift checker;
- a fail-closed API-23 adapter isolating Rayforge's internal driver registry;
- a proposed MIT project-license decision in ADR 0003; Kaz has not accepted

View file

@ -137,11 +137,12 @@ python scripts/validate_m1_manual_acceptance.py `
```
The validator verifies the kit manifest, repository commit, artifact report,
evidence hashes and relative paths. It reports
`incomplete` for any unrun or blocked scenario and `failed` for a failed step,
connected hardware, non-isolated app data, changed evidence or any stop
condition. Even a complete `passed` report still requires human review and
does not authorize hardware access.
evidence hashes and relative paths. It reports `incomplete` for any unrun or
blocked scenario, `ready_for_review` when every operator step passes but the
review record is still empty, and `failed` for a failed step, connected
hardware, non-isolated app data, changed evidence, malformed review or any
stop condition. Final `passed` requires a non-empty reviewer name and an
ISO-8601 review timestamp with timezone. No status authorizes hardware access.
## Remaining external gates

View file

@ -328,8 +328,10 @@ packaged addon source before the manual GUI checklist may begin. It also
rejects omitted manifest entries and extra files hidden in the packaged addon
directory.
The manual checklist now also has a versioned, fail-closed report template and
validator. Missing steps remain incomplete, stop conditions fail the run, and
hashed screenshots stay outside the public repository pending sanitization.
validator. Missing steps remain incomplete, a complete operator run remains
ready for review until a named, timezone-stamped human review is recorded,
stop conditions fail the run, and hashed screenshots stay outside the public
repository pending sanitization.
A reproducible kit generator copies only allowlisted synthetic inputs into the
ignored build area, records their hashes and refuses to overwrite an existing
test run. Its schema-2 provenance chain binds the editable manual report to the

View file

@ -81,7 +81,8 @@ evidence for:
archive safety, sizes, hashes, the pinned Rayforge revision and every
packaged addon source while retaining the manual GUI gate;
- a versioned manual desktop-acceptance template and validator that cannot
pass missing steps, connected hardware, changed evidence or stop conditions;
pass missing steps, connected hardware, changed evidence, stop conditions or
an absent/malformed human review record;
- a deterministic acceptance-kit generator that copies only four allowlisted
public template/input sources, hashes the three immutable inputs, refuses to
overwrite an existing run directory, and binds the report to the exact kit,

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import argparse
from datetime import datetime
import hashlib
import json
import re
@ -171,6 +172,26 @@ def _validate_artifact_report(report_root: Path, binding: dict) -> list[str]:
return errors
def _review_state(review: dict) -> tuple[bool, list[str]]:
reviewer = review.get("reviewer")
reviewed_at = review.get("reviewed_at")
if reviewer is None and reviewed_at is None:
return False, []
errors = []
if not isinstance(reviewer, str) or not reviewer.strip():
errors.append("reviewer must be a non-empty name")
if not isinstance(reviewed_at, str):
errors.append("reviewed_at must be an ISO-8601 timestamp with timezone")
else:
try:
timestamp = datetime.fromisoformat(reviewed_at.replace("Z", "+00:00"))
if timestamp.tzinfo is None:
raise ValueError
except ValueError:
errors.append("reviewed_at must be an ISO-8601 timestamp with timezone")
return not errors, errors
def validate(report_path: Path) -> dict:
report_path = report_path.resolve()
report = json.loads(report_path.read_text(encoding="utf-8-sig"))
@ -201,6 +222,8 @@ def validate(report_path: Path) -> dict:
artifact_errors = _validate_artifact_report(report_path.parent, artifact)
errors.extend(artifact_errors)
errors.extend(_validate_evidence(report_path.parent, report.get("evidence", [])))
reviewed, review_errors = _review_state(report.get("review", {}))
errors.extend(review_errors)
explicit_failure = (
any(step.get("status") == "failed" for step in steps)
@ -209,7 +232,7 @@ def validate(report_path: Path) -> dict:
or environment.get("fresh_app_data") is False
or artifact.get("status") == "failed"
)
complete = (
operator_complete = (
not errors
and step_ids == list(STEP_IDS)
and all(step.get("status") == "passed" for step in steps)
@ -220,7 +243,14 @@ def validate(report_path: Path) -> dict:
and artifact.get("status") == "passed"
and not artifact_errors
)
status = "failed" if explicit_failure or errors else "passed" if complete else "incomplete"
if explicit_failure or errors:
status = "failed"
elif operator_complete and reviewed:
status = "passed"
elif operator_complete:
status = "ready_for_review"
else:
status = "incomplete"
return {
"status": status,
"schema_valid": not errors,
@ -228,7 +258,8 @@ def validate(report_path: Path) -> dict:
"passed_steps": sum(step.get("status") == "passed" for step in steps),
"required_steps": len(STEP_IDS),
"hardware_access_authorized": False,
"manual_review_required": True,
"review_recorded": reviewed,
"manual_review_required": status != "passed",
}

View file

@ -99,6 +99,11 @@ class M1ManualAcceptanceTests(unittest.TestCase):
"path": "evidence/offline-state.png",
"sha256": hashlib.sha256(screenshot.read_bytes()).hexdigest(),
}]
report["review"] = {
"reviewer": "Synthetic Test Reviewer",
"reviewed_at": "2026-10-21T12:00:00+02:00",
"notes": "Synthetic unit-test review only.",
}
return report
def test_template_is_valid_but_incomplete(self):
@ -118,7 +123,34 @@ class M1ManualAcceptanceTests(unittest.TestCase):
result = validate(self._write(root, self._passing(root)))
self.assertEqual(result["status"], "passed")
self.assertFalse(result["hardware_access_authorized"])
self.assertFalse(result["manual_review_required"])
self.assertTrue(result["review_recorded"])
def test_complete_operator_report_waits_for_human_review(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory)
report = self._passing(root)
report["review"] = {"reviewer": None, "reviewed_at": None, "notes": ""}
result = validate(self._write(root, report))
self.assertEqual("ready_for_review", result["status"])
self.assertTrue(result["manual_review_required"])
self.assertFalse(result["review_recorded"])
def test_partial_or_timezone_free_review_is_rejected(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory)
report = self._passing(root)
report["review"] = {
"reviewer": "Synthetic Test Reviewer",
"reviewed_at": "2026-10-21T12:00:00",
"notes": "",
}
result = validate(self._write(root, report))
self.assertEqual("failed", result["status"])
self.assertIn(
"reviewed_at must be an ISO-8601 timestamp with timezone",
result["errors"],
)
def test_any_stop_condition_fails_checkpoint(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory: