ruvector/scripts/research-gate/verify_promotion.py
rUv a2326c0449
feat: ADR-280/281/282 — durable RVF metadata, role-aware embeddings, nightly research quality gate (#774)
Three ADRs implemented and hardened across five rounds of adversarial review, plus the fixes that review surfaced.

**ADR-280 — durable RVF metadata.** Delta-encoded generations with a snapshot every 32. The first implementation wrote a full snapshot per commit and replayed every one at open: 600 commits produced a 725 MiB file that could no longer be opened, with no repair path. Now 241 KB of META payload for the same workload, opening in ~4 ms. Review also closed: derive-children that could not be reopened, an 80-byte file driving a 512 MiB allocation, delete() rollback leaving in-memory tombstones that bricked the artifact, ten BufWriter sites discarding flush errors before sync_all, corrupt mid-chain deltas made unopenable (now recovers the longest valid prefix), and an ordering bug where recovery pruning committed without its re-anchoring snapshot so `rvf ingest` printed a repair warning and then destroyed the file.

**ADR-281 — role-aware embeddings.** Query/passage routing with an attested embedding-space identity. Review found the space id hashed CARGO_PKG_VERSION, so a routine version bump would have rejected every persisted corpus and invalidated every cache key — with the test suite structurally blind to it. Now keyed on a dedicated format revision with a golden-id test. Also: three constructors that failed unconditionally with ten unmigrated callers, prompt templates applied from the attested identity rather than hardcoded strings, and ApiEmbedding no longer bypassing templating.

**ADR-282 — nightly research quality gate.** Review found the gate had never completed a single run: the candidate checkout was shallow so its git diff always failed, and a jq quoting bug made the override path dead code. Check-run queries were unpaginated — on a real main commit 8 of 22 failures were invisible, so a red base could be certified green. Schemas are now load-bearing with a hashed dependency closure.

**CI note.** The two red checks are both pre-existing on main, not regressions from this branch: `Tests (core-and-rest)` routinely exceeds its 4-hour window, and `Hooks CI` has failed on main since 2026-08-02 (and in May) on `cp -r node_modules $GITHUB_WORKSPACE/npm/packages/cli/` in hooks-ci.yml — this branch's one-line version sync merely re-triggered its path filter. 72 checks pass.

Follow-ups filed and not blocking: #770, #771, #772.

🤖 Generated with [claude-flow](https://github.com/ruvnet/claude-flow)
2026-08-03 14:13:37 -03:00

78 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Verify downloaded trusted evidence without importing candidate code."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from research_gate import (
FULL_SHA_RE,
PROMOTION_UNINDEXED,
GateError,
load_json,
sha256_file,
validate_artifact_index,
validate_base_gate,
validate_manifest,
validate_override,
)
REF_RE = re.compile(r"^research/(?:candidate|nightly)/[A-Za-z0-9._/-]+$")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--evidence", required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()
root = Path(args.evidence).resolve()
subject = load_json(root / "attestation-subject.json")
index = load_json(root / "artifact-index.json")
manifest = load_json(root / "research-manifest.json")
commit = subject.get("candidate_commit", "")
ref = subject.get("candidate_ref", "")
if not FULL_SHA_RE.fullmatch(commit):
raise GateError("attested candidate commit is invalid")
if not REF_RE.fullmatch(ref):
raise GateError("attested candidate ref is outside research namespaces")
if index.get("candidate_commit") != commit or index.get("candidate_ref") != ref:
raise GateError("attestation and artifact index candidate mismatch")
if manifest.get("commit") != commit:
raise GateError("attested manifest commit mismatch")
validate_manifest(manifest)
if sha256_file(root / "artifact-index.json") != subject.get("artifact_index_sha256"):
raise GateError("attested artifact-index digest mismatch")
if sha256_file(root / "research-manifest.json") != subject.get("manifest_sha256"):
raise GateError("attested manifest digest mismatch")
base_gate = load_json(root / "base-gate.json")
validate_base_gate(base_gate, subject.get("base_sha", ""), commit)
if base_gate.get("state") != subject.get("base_gate_state"):
raise GateError("attested base gate state mismatch")
if base_gate.get("override_sha256") != subject.get("override_sha256"):
raise GateError("attested override digest mismatch")
if base_gate["state"] == "authorized-red":
override_path = root / "override.json"
if not override_path.is_file() or sha256_file(override_path) != base_gate["override_sha256"]:
raise GateError("authorized red base is missing its exact override")
validate_override(
load_json(override_path),
base_gate["base_sha"],
commit,
base_gate["failed_checks"],
)
checks = subject.get("required_checks", {})
if not checks or any(value != "success" for value in checks.values()):
raise GateError("attestation does not bind a complete green check set")
# The downloaded attested bundle also carries the signed attestation subject.
validate_artifact_index(index, root, allow_unindexed=PROMOTION_UNINDEXED)
Path(args.output).write_text(
f"candidate_sha={commit}\ncandidate_ref={ref}\n", encoding="utf-8"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())