Skip to content

Commit 80c4a69

Browse files
Pigbibicodex
andcommitted
fix: share exact preview bundle validation
Co-Authored-By: Codex <noreply@openai.com>
1 parent 4ea245e commit 80c4a69

5 files changed

Lines changed: 435 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: QAR D3 daily preview artifact
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
paths:
7+
- ".github/workflows/qar_d3_daily_preview_artifact.yml"
8+
- "pyproject.toml"
9+
- "uv.lock"
10+
- "scripts/d3_*.py"
11+
- "src/quant_advisor_research/**"
12+
- "tests/test_d3_exact_bundle.py"
13+
- "examples/political_events.example.csv"
14+
- "examples/political_watchlist.example.csv"
15+
workflow_dispatch:
16+
inputs:
17+
as_of:
18+
description: "Daily representative fixture as_of"
19+
required: false
20+
default: "2026-06-20"
21+
frozen_generated_at:
22+
description: "Harness-only deterministic generated_at"
23+
required: false
24+
default: "2026-07-15T00:00:00Z"
25+
26+
permissions:
27+
contents: read
28+
29+
jobs:
30+
build-and-verify:
31+
runs-on: ubuntu-latest
32+
env:
33+
INPUT_AS_OF: ${{ inputs.as_of || '2026-06-20' }}
34+
FROZEN_GENERATED_AT: ${{ inputs.frozen_generated_at || '2026-07-15T00:00:00Z' }}
35+
steps:
36+
- name: Checkout repository
37+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
38+
- name: Set up Python
39+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
40+
with:
41+
python-version: "3.11"
42+
- name: Set up uv
43+
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
44+
with:
45+
version: "0.11.19"
46+
- name: Sync locked test environment
47+
run: uv sync --locked --extra test
48+
- name: Build representative previews
49+
id: build
50+
env:
51+
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}
52+
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
53+
BUILD_ROOT: ${{ runner.temp }}/qar-d3
54+
run: |
55+
mkdir -p "$BUILD_ROOT"
56+
uv run --no-sync python scripts/d3_build_daily_preview.py \
57+
--as-of "$INPUT_AS_OF" --political-events examples/political_events.example.csv \
58+
--political-watchlist examples/political_watchlist.example.csv \
59+
--frozen-generated-at "$FROZEN_GENERATED_AT" --base-sha "$BASE_SHA" --head-sha "$HEAD_SHA" \
60+
--uv-version "$(uv --version)" --lock-path uv.lock --repo-root "$GITHUB_WORKSPACE" \
61+
--temp-root "$BUILD_ROOT" --evidence-path "$BUILD_ROOT/build-evidence.json" \
62+
--workspace-path-file "$BUILD_ROOT/workspace-path.txt"
63+
echo "workspace=$(cat "$BUILD_ROOT/workspace-path.txt")" >> "$GITHUB_OUTPUT"
64+
echo "evidence=$BUILD_ROOT/build-evidence.json" >> "$GITHUB_OUTPUT"
65+
- name: Upload exact preview members
66+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
67+
with:
68+
name: qar-d3-preview-bundle
69+
path: |
70+
${{ steps.build.outputs.workspace }}/report.json
71+
${{ steps.build.outputs.workspace }}/report.html
72+
${{ steps.build.outputs.workspace }}/manifest.json
73+
include-hidden-files: true
74+
if-no-files-found: error
75+
- name: Upload build evidence
76+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
77+
with:
78+
name: qar-d3-build-evidence
79+
path: ${{ steps.build.outputs.evidence }}
80+
if-no-files-found: error
81+
- name: Download preview members
82+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
83+
with:
84+
name: qar-d3-preview-bundle
85+
path: ${{ runner.temp }}/qar-d3-download/bundle
86+
- name: Download build evidence
87+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
88+
with:
89+
name: qar-d3-build-evidence
90+
path: ${{ runner.temp }}/qar-d3-download/evidence
91+
- name: Verify downloaded artifact
92+
run: |
93+
uv run --no-sync python scripts/d3_verify_daily_preview.py \
94+
--workspace "${{ runner.temp }}/qar-d3-download/bundle" \
95+
--evidence-path "${{ runner.temp }}/qar-d3-download/evidence/build-evidence.json" \
96+
--base-sha "${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}" \
97+
--head-sha "${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}" \
98+
--uv-version "$(uv --version)" --lock-path uv.lock --repo-root "$GITHUB_WORKSPACE" \
99+
--as-of "$INPUT_AS_OF" --political-events examples/political_events.example.csv \
100+
--political-watchlist examples/political_watchlist.example.csv --frozen-generated-at "$FROZEN_GENERATED_AT"
101+
- name: Cleanup private preview workspaces
102+
if: always()
103+
run: rm -rf "${{ runner.temp }}/qar-d3"

scripts/d3_build_daily_preview.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python3
2+
"""Build two frozen-clock representative daily previews and bind evidence."""
3+
from __future__ import annotations
4+
5+
import argparse
6+
import hashlib
7+
import importlib.metadata
8+
import json
9+
import platform
10+
import re
11+
import tempfile
12+
from pathlib import Path
13+
from unittest.mock import patch
14+
15+
from d3_evidence import DEPENDENCY_INVENTORY, EVIDENCE_VERSION, EvidenceContractError, locked_environment_evidence, repository_file_hashes, validate_exact_bundle
16+
from quant_advisor_research.advisory_report import build_advisory_report
17+
from quant_advisor_research.preview_bundle import read_preview_bundle
18+
from quant_advisor_research.preview_workspace import build_preview_workspace
19+
20+
21+
def _distributions() -> list[str]:
22+
return sorted({f"{name}=={dist.version}" for dist in importlib.metadata.distributions() if (name := dist.metadata.get("Name"))})
23+
24+
25+
def _build_report(args: argparse.Namespace) -> dict[str, object]:
26+
with patch("quant_advisor_research.advisory_report.utc_now_iso", return_value=args.frozen_generated_at):
27+
return build_advisory_report(as_of=args.as_of, cadence="daily", political_events_path=Path(args.political_events), political_watchlist_path=Path(args.political_watchlist))
28+
29+
30+
def _bundle_hashes(workspace: Path) -> dict[str, dict[str, object]]:
31+
members = validate_exact_bundle(workspace)
32+
result = {}
33+
for name, path in members.items():
34+
content = path.read_bytes()
35+
result[name] = {"sha256": hashlib.sha256(content).hexdigest(), "size": len(content)}
36+
return result
37+
38+
39+
def build_evidence(args: argparse.Namespace) -> None:
40+
try:
41+
dependency_files = repository_file_hashes(args.repo_root, DEPENDENCY_INVENTORY)
42+
lock_sha = hashlib.sha256(Path(args.lock_path).read_bytes()).hexdigest()
43+
environment = locked_environment_evidence(lock_sha256=lock_sha, uv_version=args.uv_version, python_version=platform.python_version(), distributions=_distributions())
44+
if not re.fullmatch(r"[0-9a-f]{40}", args.base_sha) or not re.fullmatch(r"[0-9a-f]{40}", args.head_sha):
45+
raise EvidenceContractError("provenance_sha_invalid")
46+
parent = Path(tempfile.mkdtemp(prefix="qar-d3-parent-", dir=args.temp_root))
47+
first = build_preview_workspace(_build_report(args), parent)
48+
second = build_preview_workspace(_build_report(args), parent)
49+
first_members = validate_exact_bundle(first)
50+
second_members = validate_exact_bundle(second)
51+
read_preview_bundle(first); read_preview_bundle(second)
52+
first_files, second_files = _bundle_hashes(first), _bundle_hashes(second)
53+
if first_files != second_files or any(first_members[name].read_bytes() != second_members[name].read_bytes() for name in first_members):
54+
raise EvidenceContractError("repeat_build_not_equal")
55+
manifest = json.loads(first_members["manifest.json"].read_text(encoding="utf-8"))
56+
source = {"schema_version": "5", "contract_version": "model_recommendations.v5", "cadence": "daily", "as_of": args.as_of, "generated_at": args.frozen_generated_at}
57+
if manifest.get("bundle_contract") != "qar.preview_bundle.v1" or manifest.get("source") != source:
58+
raise EvidenceContractError("source_contract_mismatch")
59+
evidence = {
60+
"evidence_version": EVIDENCE_VERSION, "base_sha": args.base_sha, "head_sha": args.head_sha,
61+
"source": {"fixture_paths": [Path(args.political_events).as_posix(), Path(args.political_watchlist).as_posix()], "provenance": "repository_representative_fixture"},
62+
"deterministic_clock": {"frozen_generated_at": args.frozen_generated_at, "producer_invocations": 2},
63+
"workflow_dependency_inventory": DEPENDENCY_INVENTORY, "dependency_files": dependency_files,
64+
"locked_environment": environment,
65+
"bundle": {"contract": "qar.preview_bundle.v1", "source": source, "files": first_files},
66+
"repeat_build": {"independent_invocations": 2, "bytes_equal": True, "files": second_files},
67+
}
68+
evidence_path = Path(args.evidence_path)
69+
evidence_path.parent.mkdir(parents=True, exist_ok=True)
70+
evidence_path.write_text(json.dumps(evidence, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
71+
Path(args.workspace_path_file).write_text(str(first) + "\n", encoding="utf-8")
72+
print(json.dumps({"workspace": str(first), "evidence": str(evidence_path), "base_sha": args.base_sha, "head_sha": args.head_sha}, sort_keys=True))
73+
except EvidenceContractError as exc:
74+
raise RuntimeError(exc.code) from None
75+
76+
77+
def parse_args() -> argparse.Namespace:
78+
p = argparse.ArgumentParser()
79+
for name in ("as-of", "political-events", "political-watchlist", "frozen-generated-at", "base-sha", "head-sha", "uv-version", "lock-path", "repo-root", "temp-root", "evidence-path", "workspace-path-file"):
80+
p.add_argument(f"--{name}", required=True)
81+
return p.parse_args()
82+
83+
84+
if __name__ == "__main__":
85+
build_evidence(parse_args())

scripts/d3_evidence.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Pure D3 evidence helpers, including the shared exact-bundle validator."""
2+
from __future__ import annotations
3+
4+
import hashlib
5+
import os
6+
import re
7+
import stat
8+
from collections.abc import Iterable, Mapping
9+
from pathlib import Path
10+
11+
FIXED_FILES = ("manifest.json", "report.html", "report.json")
12+
EVIDENCE_VERSION = "qar.d3.build_evidence.v4"
13+
DIST_RE = re.compile(r"^([A-Za-z0-9_.-]+)==([^\s=]+)$")
14+
DEPENDENCY_INVENTORY = [
15+
".github/workflows/qar_d3_daily_preview_artifact.yml", "pyproject.toml", "uv.lock",
16+
"scripts/d3_build_daily_preview.py", "scripts/d3_verify_daily_preview.py", "scripts/d3_evidence.py",
17+
"src/quant_advisor_research/advisory_report.py", "src/quant_advisor_research/artifact_integrity.py",
18+
"src/quant_advisor_research/artifacts.py", "src/quant_advisor_research/contracts.py",
19+
"src/quant_advisor_research/csv_utils.py", "src/quant_advisor_research/period_contract.py",
20+
"src/quant_advisor_research/preview_bundle.py", "src/quant_advisor_research/preview_workspace.py",
21+
"src/quant_advisor_research/time_contract.py", "tests/test_d3_exact_bundle.py",
22+
"examples/political_events.example.csv", "examples/political_watchlist.example.csv",
23+
]
24+
25+
26+
class EvidenceContractError(ValueError):
27+
def __init__(self, code: str) -> None:
28+
self.code = code
29+
super().__init__(code)
30+
31+
32+
def validate_exact_bundle(workspace: str | Path) -> dict[str, Path]:
33+
"""Validate all directory members before any caller reads or hashes them."""
34+
root = Path(workspace)
35+
try:
36+
root_info = root.lstat()
37+
if not stat.S_ISDIR(root_info.st_mode) or stat.S_ISLNK(root_info.st_mode):
38+
raise EvidenceContractError("bundle_directory_invalid")
39+
entries = list(os.scandir(root))
40+
if {entry.name for entry in entries} != set(FIXED_FILES) or len(entries) != len(FIXED_FILES):
41+
raise EvidenceContractError("bundle_member_set_invalid")
42+
result: dict[str, Path] = {}
43+
for entry in entries:
44+
info = entry.stat(follow_symlinks=False)
45+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
46+
raise EvidenceContractError("bundle_member_invalid")
47+
result[entry.name] = root / entry.name
48+
return {name: result[name] for name in FIXED_FILES}
49+
except EvidenceContractError:
50+
raise
51+
except (OSError, TypeError, ValueError):
52+
raise EvidenceContractError("bundle_member_invalid") from None
53+
54+
55+
def canonical_distribution_snapshot(values: Iterable[str]) -> tuple[list[str], str]:
56+
try:
57+
raw = list(values)
58+
normalized: dict[str, str] = {}
59+
for value in raw:
60+
if type(value) is not str or not (match := DIST_RE.fullmatch(value)):
61+
raise EvidenceContractError("distribution_snapshot_invalid")
62+
name = re.sub(r"[-_.]+", "-", match.group(1)).lower()
63+
item = f"{name}=={match.group(2)}"
64+
if name in normalized and normalized[name] != item:
65+
raise EvidenceContractError("distribution_snapshot_conflict")
66+
normalized[name] = item
67+
snapshot = sorted(normalized.values())
68+
digest = hashlib.sha256("\n".join(snapshot).encode()).hexdigest()
69+
return snapshot, digest
70+
except EvidenceContractError:
71+
raise
72+
except (TypeError, ValueError, UnicodeError):
73+
raise EvidenceContractError("distribution_snapshot_invalid") from None
74+
75+
76+
def locked_environment_evidence(*, lock_sha256: str, uv_version: str, python_version: str, distributions: Iterable[str]) -> dict[str, object]:
77+
if type(lock_sha256) is not str or not re.fullmatch(r"[0-9a-f]{64}", lock_sha256):
78+
raise EvidenceContractError("lock_digest_invalid")
79+
if type(uv_version) is not str or not uv_version.startswith("uv "):
80+
raise EvidenceContractError("uv_version_invalid")
81+
if type(python_version) is not str or not re.fullmatch(r"3\.11(?:\.\d+)?", python_version):
82+
raise EvidenceContractError("python_version_invalid")
83+
snapshot, digest = canonical_distribution_snapshot(distributions)
84+
return {"lock_sha256": lock_sha256, "uv_version": uv_version, "python_version": python_version, "installed_distributions": snapshot, "installed_distributions_sha256": digest}
85+
86+
87+
def repository_file_hashes(repo_root: str | Path, paths: Iterable[str]) -> dict[str, str]:
88+
root = Path(repo_root)
89+
try:
90+
if not stat.S_ISDIR(root.lstat().st_mode):
91+
raise EvidenceContractError("dependency_root_invalid")
92+
result: dict[str, str] = {}
93+
for raw in paths:
94+
relative = Path(raw)
95+
if type(raw) is not str or not raw or relative.is_absolute() or ".." in relative.parts or raw in result:
96+
raise EvidenceContractError("dependency_path_invalid")
97+
target = root / relative
98+
info = target.lstat()
99+
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_nlink != 1:
100+
raise EvidenceContractError("dependency_file_invalid")
101+
result[raw] = hashlib.sha256(target.read_bytes()).hexdigest()
102+
return dict(sorted(result.items()))
103+
except EvidenceContractError:
104+
raise
105+
except (OSError, TypeError, ValueError, UnicodeError):
106+
raise EvidenceContractError("dependency_file_invalid") from None
107+
108+
109+
def require_exact_locked_environment(value: object, expected: Mapping[str, object]) -> None:
110+
if not isinstance(value, Mapping) or dict(value) != dict(expected):
111+
raise EvidenceContractError("locked_environment_mismatch")

scripts/d3_verify_daily_preview.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env python3
2+
"""Verify downloaded D3 bundle and complete evidence binding."""
3+
from __future__ import annotations
4+
5+
import argparse
6+
import hashlib
7+
import importlib.metadata
8+
import json
9+
import platform
10+
from pathlib import Path
11+
12+
from d3_evidence import DEPENDENCY_INVENTORY, EVIDENCE_VERSION, EvidenceContractError, locked_environment_evidence, repository_file_hashes, validate_exact_bundle
13+
from quant_advisor_research.preview_bundle import read_preview_bundle
14+
15+
EXPECTED_KEYS = frozenset({"evidence_version", "base_sha", "head_sha", "source", "deterministic_clock", "workflow_dependency_inventory", "dependency_files", "locked_environment", "bundle", "repeat_build"})
16+
17+
18+
def _distributions() -> list[str]:
19+
return sorted({f"{name}=={dist.version}" for dist in importlib.metadata.distributions() if (name := dist.metadata.get("Name"))})
20+
21+
22+
def _hashes(workspace: Path) -> dict[str, dict[str, object]]:
23+
members = validate_exact_bundle(workspace)
24+
return {name: {"sha256": hashlib.sha256(path.read_bytes()).hexdigest(), "size": path.stat().st_size} for name, path in members.items()}
25+
26+
27+
def verify(args: argparse.Namespace) -> None:
28+
try:
29+
evidence = json.loads(Path(args.evidence_path).read_text(encoding="utf-8"))
30+
if not isinstance(evidence, dict) or set(evidence) != EXPECTED_KEYS:
31+
raise EvidenceContractError("build_evidence_shape_invalid")
32+
dependency_files = repository_file_hashes(args.repo_root, DEPENDENCY_INVENTORY)
33+
if evidence["workflow_dependency_inventory"] != DEPENDENCY_INVENTORY or evidence["dependency_files"] != dependency_files:
34+
raise EvidenceContractError("dependency_evidence_mismatch")
35+
environment = locked_environment_evidence(lock_sha256=hashlib.sha256(Path(args.lock_path).read_bytes()).hexdigest(), uv_version=args.uv_version, python_version=platform.python_version(), distributions=_distributions())
36+
expected_source = {"schema_version": "5", "contract_version": "model_recommendations.v5", "cadence": "daily", "as_of": args.as_of, "generated_at": args.frozen_generated_at}
37+
workspace = Path(args.workspace)
38+
members = validate_exact_bundle(workspace)
39+
read_preview_bundle(workspace)
40+
manifest = json.loads(members["manifest.json"].read_text(encoding="utf-8"))
41+
if manifest.get("bundle_contract") != "qar.preview_bundle.v1" or manifest.get("source") != expected_source:
42+
raise EvidenceContractError("source_contract_mismatch")
43+
files = _hashes(workspace)
44+
expected = {
45+
"evidence_version": EVIDENCE_VERSION, "base_sha": args.base_sha, "head_sha": args.head_sha,
46+
"source": {"fixture_paths": [Path(args.political_events).as_posix(), Path(args.political_watchlist).as_posix()], "provenance": "repository_representative_fixture"},
47+
"deterministic_clock": {"frozen_generated_at": args.frozen_generated_at, "producer_invocations": 2},
48+
"workflow_dependency_inventory": DEPENDENCY_INVENTORY, "dependency_files": dependency_files,
49+
"locked_environment": environment,
50+
"bundle": {"contract": "qar.preview_bundle.v1", "source": expected_source, "files": files},
51+
"repeat_build": {"independent_invocations": 2, "bytes_equal": True, "files": files},
52+
}
53+
if evidence != expected:
54+
raise EvidenceContractError("build_evidence_mismatch")
55+
print(json.dumps({"status": "passed", "evidence_binding": "exact_full_payload_and_bundle_members", "base_sha": args.base_sha, "head_sha": args.head_sha, "files": files}, sort_keys=True))
56+
except EvidenceContractError as exc:
57+
raise RuntimeError(exc.code) from None
58+
59+
60+
def parse_args() -> argparse.Namespace:
61+
p = argparse.ArgumentParser()
62+
for name in ("workspace", "evidence-path", "base-sha", "head-sha", "uv-version", "lock-path", "repo-root", "as-of", "political-events", "political-watchlist", "frozen-generated-at"):
63+
p.add_argument(f"--{name}", required=True)
64+
return p.parse_args()
65+
66+
67+
if __name__ == "__main__":
68+
verify(parse_args())

0 commit comments

Comments
 (0)