Skip to content

Commit ff915ae

Browse files
Pigbibicodex
andauthored
fix: validate exact D3 preview bundle members (#58)
* fix: share exact preview bundle validation Co-Authored-By: Codex <noreply@openai.com> * fix: bind preview evidence to descriptor snapshots Co-Authored-By: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
1 parent 4ea245e commit ff915ae

5 files changed

Lines changed: 534 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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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, BundleSnapshot, EvidenceContractError, locked_environment_evidence, repository_file_hashes, validate_exact_bundle, validate_preview_snapshot
16+
from quant_advisor_research.advisory_report import build_advisory_report
17+
from quant_advisor_research.preview_workspace import build_preview_workspace
18+
19+
20+
def _distributions() -> list[str]:
21+
return sorted({f"{name}=={dist.version}" for dist in importlib.metadata.distributions() if (name := dist.metadata.get("Name"))})
22+
23+
24+
def _build_report(args: argparse.Namespace) -> dict[str, object]:
25+
with patch("quant_advisor_research.advisory_report.utc_now_iso", return_value=args.frozen_generated_at):
26+
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))
27+
28+
29+
def _bundle_hashes(snapshot: BundleSnapshot) -> dict[str, dict[str, object]]:
30+
return {item.name: {"sha256": item.sha256, "size": len(item.content)} for item in snapshot.members}
31+
32+
33+
def build_evidence(args: argparse.Namespace) -> None:
34+
try:
35+
dependency_files = repository_file_hashes(args.repo_root, DEPENDENCY_INVENTORY)
36+
lock_sha = hashlib.sha256(Path(args.lock_path).read_bytes()).hexdigest()
37+
environment = locked_environment_evidence(lock_sha256=lock_sha, uv_version=args.uv_version, python_version=platform.python_version(), distributions=_distributions())
38+
if not re.fullmatch(r"[0-9a-f]{40}", args.base_sha) or not re.fullmatch(r"[0-9a-f]{40}", args.head_sha):
39+
raise EvidenceContractError("provenance_sha_invalid")
40+
first_parent = Path(tempfile.mkdtemp(prefix="qar-d3-parent-a-", dir=args.temp_root))
41+
second_parent = Path(tempfile.mkdtemp(prefix="qar-d3-parent-b-", dir=args.temp_root))
42+
first = build_preview_workspace(_build_report(args), first_parent)
43+
second = build_preview_workspace(_build_report(args), second_parent)
44+
if first == second or first.stat().st_ino == second.stat().st_ino:
45+
raise EvidenceContractError("repeat_workspace_not_distinct")
46+
first_snapshot = validate_exact_bundle(first)
47+
second_snapshot = validate_exact_bundle(second)
48+
validate_preview_snapshot(first_snapshot); validate_preview_snapshot(second_snapshot)
49+
first_files, second_files = _bundle_hashes(first_snapshot), _bundle_hashes(second_snapshot)
50+
if first_files != second_files or any(first_snapshot.member(name).content != second_snapshot.member(name).content for name in ("manifest.json", "report.html", "report.json")):
51+
raise EvidenceContractError("repeat_build_not_equal")
52+
manifest = json.loads(first_snapshot.member("manifest.json").content.decode("utf-8"))
53+
source = {"schema_version": "5", "contract_version": "model_recommendations.v5", "cadence": "daily", "as_of": args.as_of, "generated_at": args.frozen_generated_at}
54+
if manifest.get("bundle_contract") != "qar.preview_bundle.v1" or manifest.get("source") != source:
55+
raise EvidenceContractError("source_contract_mismatch")
56+
evidence = {
57+
"evidence_version": EVIDENCE_VERSION, "base_sha": args.base_sha, "head_sha": args.head_sha,
58+
"source": {"fixture_paths": [Path(args.political_events).as_posix(), Path(args.political_watchlist).as_posix()], "provenance": "repository_representative_fixture"},
59+
"deterministic_clock": {"frozen_generated_at": args.frozen_generated_at, "producer_invocations": 2},
60+
"workflow_dependency_inventory": DEPENDENCY_INVENTORY, "dependency_files": dependency_files,
61+
"locked_environment": environment,
62+
"bundle": {"contract": "qar.preview_bundle.v1", "source": source, "files": first_files},
63+
"repeat_build": {"independent_invocations": 2, "bytes_equal": True, "files": second_files},
64+
}
65+
evidence_path = Path(args.evidence_path)
66+
evidence_path.parent.mkdir(parents=True, exist_ok=True)
67+
evidence_path.write_text(json.dumps(evidence, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
68+
Path(args.workspace_path_file).write_text(str(first) + "\n", encoding="utf-8")
69+
print(json.dumps({"workspace": str(first), "evidence": str(evidence_path), "base_sha": args.base_sha, "head_sha": args.head_sha}, sort_keys=True))
70+
except EvidenceContractError as exc:
71+
raise RuntimeError(exc.code) from None
72+
73+
74+
def parse_args() -> argparse.Namespace:
75+
p = argparse.ArgumentParser()
76+
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"):
77+
p.add_argument(f"--{name}", required=True)
78+
return p.parse_args()
79+
80+
81+
if __name__ == "__main__":
82+
build_evidence(parse_args())

scripts/d3_evidence.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""Pure D3 evidence helpers, including the shared exact-bundle validator."""
2+
from __future__ import annotations
3+
4+
import hashlib
5+
import json
6+
import os
7+
import re
8+
import stat
9+
from collections.abc import Iterable, Mapping
10+
from dataclasses import dataclass
11+
from pathlib import Path
12+
13+
FIXED_FILES = ("manifest.json", "report.html", "report.json")
14+
EVIDENCE_VERSION = "qar.d3.build_evidence.v4"
15+
DIST_RE = re.compile(r"^([A-Za-z0-9_.-]+)==([^\s=]+)$")
16+
DEPENDENCY_INVENTORY = [
17+
".github/workflows/qar_d3_daily_preview_artifact.yml", "pyproject.toml", "uv.lock",
18+
"scripts/d3_build_daily_preview.py", "scripts/d3_verify_daily_preview.py", "scripts/d3_evidence.py",
19+
"src/quant_advisor_research/advisory_report.py", "src/quant_advisor_research/artifact_integrity.py",
20+
"src/quant_advisor_research/artifacts.py", "src/quant_advisor_research/contracts.py",
21+
"src/quant_advisor_research/csv_utils.py", "src/quant_advisor_research/period_contract.py",
22+
"src/quant_advisor_research/preview_bundle.py", "src/quant_advisor_research/preview_workspace.py",
23+
"src/quant_advisor_research/time_contract.py", "tests/test_d3_exact_bundle.py",
24+
"examples/political_events.example.csv", "examples/political_watchlist.example.csv",
25+
]
26+
27+
28+
class EvidenceContractError(ValueError):
29+
def __init__(self, code: str) -> None:
30+
self.code = code
31+
super().__init__(code)
32+
33+
34+
@dataclass(frozen=True, slots=True)
35+
class BundleMemberSnapshot:
36+
name: str
37+
content: bytes
38+
sha256: str
39+
40+
41+
@dataclass(frozen=True, slots=True)
42+
class BundleSnapshot:
43+
members: tuple[BundleMemberSnapshot, ...]
44+
45+
def member(self, name: str) -> BundleMemberSnapshot:
46+
for item in self.members:
47+
if item.name == name:
48+
return item
49+
raise EvidenceContractError("bundle_member_set_invalid")
50+
51+
52+
def _directory_flags() -> int:
53+
required = ("O_DIRECTORY", "O_CLOEXEC", "O_NOFOLLOW")
54+
if any(not hasattr(os, name) for name in required):
55+
raise EvidenceContractError("filesystem_unsupported")
56+
return os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW
57+
58+
59+
def _member_flags() -> int:
60+
if any(not hasattr(os, name) for name in ("O_CLOEXEC", "O_NOFOLLOW")):
61+
raise EvidenceContractError("filesystem_unsupported")
62+
return os.O_RDONLY | os.O_NONBLOCK | os.O_CLOEXEC | os.O_NOFOLLOW
63+
64+
65+
def validate_exact_bundle(workspace: str | Path) -> BundleSnapshot:
66+
"""Return an immutable FD-bound snapshot after validating all members."""
67+
root_fd = -1
68+
try:
69+
root_fd = os.open(workspace, _directory_flags())
70+
root_info = os.fstat(root_fd)
71+
if not stat.S_ISDIR(root_info.st_mode) or stat.S_ISLNK(root_info.st_mode):
72+
raise EvidenceContractError("bundle_directory_invalid")
73+
names = os.listdir(root_fd)
74+
if set(names) != set(FIXED_FILES) or len(names) != len(FIXED_FILES):
75+
raise EvidenceContractError("bundle_member_set_invalid")
76+
result: list[BundleMemberSnapshot] = []
77+
for name in FIXED_FILES:
78+
fd = -1
79+
try:
80+
fd = os.open(name, _member_flags(), dir_fd=root_fd)
81+
info = os.fstat(fd)
82+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
83+
raise EvidenceContractError("bundle_member_invalid")
84+
chunks: list[bytes] = []
85+
while chunk := os.read(fd, 1024 * 1024):
86+
chunks.append(chunk)
87+
content = b"".join(chunks)
88+
result.append(BundleMemberSnapshot(name, content, hashlib.sha256(content).hexdigest()))
89+
finally:
90+
if fd >= 0:
91+
os.close(fd)
92+
if set(os.listdir(root_fd)) != set(FIXED_FILES):
93+
raise EvidenceContractError("bundle_member_set_invalid")
94+
return BundleSnapshot(tuple(result))
95+
except EvidenceContractError:
96+
raise
97+
except (OSError, TypeError, ValueError):
98+
raise EvidenceContractError("bundle_member_invalid") from None
99+
finally:
100+
if root_fd >= 0:
101+
os.close(root_fd)
102+
103+
104+
def validate_preview_snapshot(snapshot: BundleSnapshot) -> tuple[Mapping[str, object], Mapping[str, object]]:
105+
"""Readback validation using only the immutable member bytes."""
106+
try:
107+
from quant_advisor_research.preview_bundle import _canonical_json, _manifest, _render_html, _validated_source
108+
report_bytes = snapshot.member("report.json").content
109+
html_bytes = snapshot.member("report.html").content
110+
manifest_bytes = snapshot.member("manifest.json").content
111+
report = json.loads(report_bytes.decode("utf-8"))
112+
if not isinstance(report, Mapping):
113+
raise EvidenceContractError("readback_invalid")
114+
validated = _validated_source(report)
115+
if _canonical_json(validated) != report_bytes:
116+
raise EvidenceContractError("report_bytes_noncanonical")
117+
manifest = json.loads(manifest_bytes.decode("utf-8"))
118+
if not isinstance(manifest, Mapping) or manifest != _manifest(validated, report_bytes, html_bytes):
119+
raise EvidenceContractError("manifest_mismatch")
120+
if html_bytes != _render_html(validated, report_bytes) or html_bytes.count(b'href="report.json"') != 1 or html_bytes.count(b'href="manifest.json"') != 1:
121+
raise EvidenceContractError("html_links_invalid")
122+
return validated, manifest
123+
except EvidenceContractError:
124+
raise
125+
except (UnicodeError, json.JSONDecodeError, TypeError, ValueError, OverflowError, RecursionError):
126+
raise EvidenceContractError("readback_invalid") from None
127+
128+
129+
def canonical_distribution_snapshot(values: Iterable[str]) -> tuple[list[str], str]:
130+
try:
131+
raw = list(values)
132+
normalized: dict[str, str] = {}
133+
for value in raw:
134+
if type(value) is not str or not (match := DIST_RE.fullmatch(value)):
135+
raise EvidenceContractError("distribution_snapshot_invalid")
136+
name = re.sub(r"[-_.]+", "-", match.group(1)).lower()
137+
item = f"{name}=={match.group(2)}"
138+
if name in normalized and normalized[name] != item:
139+
raise EvidenceContractError("distribution_snapshot_conflict")
140+
normalized[name] = item
141+
snapshot = sorted(normalized.values())
142+
digest = hashlib.sha256("\n".join(snapshot).encode()).hexdigest()
143+
return snapshot, digest
144+
except EvidenceContractError:
145+
raise
146+
except (TypeError, ValueError, UnicodeError):
147+
raise EvidenceContractError("distribution_snapshot_invalid") from None
148+
149+
150+
def locked_environment_evidence(*, lock_sha256: str, uv_version: str, python_version: str, distributions: Iterable[str]) -> dict[str, object]:
151+
if type(lock_sha256) is not str or not re.fullmatch(r"[0-9a-f]{64}", lock_sha256):
152+
raise EvidenceContractError("lock_digest_invalid")
153+
if type(uv_version) is not str or not uv_version.startswith("uv "):
154+
raise EvidenceContractError("uv_version_invalid")
155+
if type(python_version) is not str or not re.fullmatch(r"3\.11(?:\.\d+)?", python_version):
156+
raise EvidenceContractError("python_version_invalid")
157+
snapshot, digest = canonical_distribution_snapshot(distributions)
158+
return {"lock_sha256": lock_sha256, "uv_version": uv_version, "python_version": python_version, "installed_distributions": snapshot, "installed_distributions_sha256": digest}
159+
160+
161+
def repository_file_hashes(repo_root: str | Path, paths: Iterable[str]) -> dict[str, str]:
162+
root_fd = -1
163+
opened: list[int] = []
164+
try:
165+
root_fd = os.open(repo_root, _directory_flags())
166+
if not stat.S_ISDIR(os.fstat(root_fd).st_mode):
167+
raise EvidenceContractError("dependency_root_invalid")
168+
result: dict[str, str] = {}
169+
for raw in paths:
170+
relative = Path(raw)
171+
if type(raw) is not str or not raw or relative.is_absolute() or any(part in ("", ".", "..") for part in relative.parts) or raw in result:
172+
raise EvidenceContractError("dependency_path_invalid")
173+
parent_fd = root_fd
174+
traversed: list[int] = []
175+
try:
176+
for part in relative.parts[:-1]:
177+
next_fd = os.open(part, _directory_flags(), dir_fd=parent_fd)
178+
traversed.append(next_fd)
179+
parent_fd = next_fd
180+
file_fd = os.open(relative.parts[-1], _member_flags(), dir_fd=parent_fd)
181+
opened.append(file_fd)
182+
info = os.fstat(file_fd)
183+
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
184+
raise EvidenceContractError("dependency_file_invalid")
185+
digest = hashlib.sha256()
186+
while chunk := os.read(file_fd, 1024 * 1024):
187+
digest.update(chunk)
188+
result[raw] = digest.hexdigest()
189+
finally:
190+
for fd in reversed(traversed):
191+
os.close(fd)
192+
return dict(sorted(result.items()))
193+
except EvidenceContractError:
194+
raise
195+
except (OSError, TypeError, ValueError, UnicodeError):
196+
raise EvidenceContractError("dependency_file_invalid") from None
197+
finally:
198+
for fd in opened:
199+
os.close(fd)
200+
if root_fd >= 0:
201+
os.close(root_fd)
202+
203+
204+
def require_exact_locked_environment(value: object, expected: Mapping[str, object]) -> None:
205+
if not isinstance(value, Mapping) or dict(value) != dict(expected):
206+
raise EvidenceContractError("locked_environment_mismatch")

0 commit comments

Comments
 (0)