Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions .github/workflows/qar_d3_daily_preview_artifact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: QAR D3 daily preview artifact

on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- ".github/workflows/qar_d3_daily_preview_artifact.yml"
- "pyproject.toml"
- "uv.lock"
- "scripts/d3_*.py"
- "src/quant_advisor_research/**"
- "tests/test_d3_tool_agnostic.py"
- "examples/political_events.example.csv"
- "examples/political_watchlist.example.csv"
workflow_dispatch:
inputs:
as_of:
description: "Daily representative fixture as_of"
required: false
default: "2026-06-20"
frozen_generated_at:
description: "Harness-only deterministic generated_at"
required: false
default: "2026-07-15T00:00:00Z"

permissions:
contents: read

jobs:
build-and-verify:
runs-on: ubuntu-latest
env:
INPUT_AS_OF: ${{ inputs.as_of || '2026-06-20' }}
FROZEN_GENERATED_AT: ${{ inputs.frozen_generated_at || '2026-07-15T00:00:00Z' }}
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"
- name: Set up uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
with:
version: "0.11.19"
- name: Sync locked test environment
run: uv sync --locked --extra test
- name: Build representative previews
id: build
env:
# PRs bind the real target/source commits; dispatch binds both to the documented workflow SHA.
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
BUILD_ROOT: ${{ runner.temp }}/qar-d3
run: |
mkdir -p "$BUILD_ROOT"
uv run --no-sync python scripts/d3_build_daily_preview.py \
--as-of "$INPUT_AS_OF" \
--political-events examples/political_events.example.csv \
--political-watchlist examples/political_watchlist.example.csv \
--frozen-generated-at "$FROZEN_GENERATED_AT" \
--base-sha "$BASE_SHA" \
--head-sha "$HEAD_SHA" \
--uv-version "$(uv --version)" \
--lock-path uv.lock \
--repo-root "$GITHUB_WORKSPACE" \
--temp-root "$BUILD_ROOT" \
--evidence-path "$BUILD_ROOT/build-evidence.json" \
--workspace-path-file "$BUILD_ROOT/workspace-path.txt"
echo "workspace=$(cat "$BUILD_ROOT/workspace-path.txt")" >> "$GITHUB_OUTPUT"
echo "evidence=$BUILD_ROOT/build-evidence.json" >> "$GITHUB_OUTPUT"
- name: Upload exact preview members
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qar-d3-preview-bundle
path: |
${{ steps.build.outputs.workspace }}/report.json
${{ steps.build.outputs.workspace }}/report.html
${{ steps.build.outputs.workspace }}/manifest.json
include-hidden-files: true
if-no-files-found: error
- name: Upload build evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qar-d3-build-evidence
path: ${{ steps.build.outputs.evidence }}
if-no-files-found: error
- name: Download preview members
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: qar-d3-preview-bundle
path: ${{ runner.temp }}/qar-d3-download/bundle
- name: Download build evidence
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: qar-d3-build-evidence
path: ${{ runner.temp }}/qar-d3-download/evidence
- name: Verify downloaded artifact and full evidence binding
run: |
uv run --no-sync python scripts/d3_verify_daily_preview.py \
--workspace "${{ runner.temp }}/qar-d3-download/bundle" \
--evidence-path "${{ runner.temp }}/qar-d3-download/evidence/build-evidence.json" \
--base-sha "${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}" \
--head-sha "${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}" \
--uv-version "$(uv --version)" \
--lock-path uv.lock \
--repo-root "$GITHUB_WORKSPACE" \
--as-of "$INPUT_AS_OF" \
--political-events examples/political_events.example.csv \
--political-watchlist examples/political_watchlist.example.csv \
--frozen-generated-at "$FROZEN_GENERATED_AT"
- name: Cleanup private preview workspaces
if: always()
run: rm -rf "${{ runner.temp }}/qar-d3"
100 changes: 100 additions & 0 deletions scripts/d3_build_daily_preview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Workflow-only D3 builder; uv discovery is never imported by ordinary tests."""
from __future__ import annotations

import argparse
import hashlib
import importlib.metadata
import json
import os
import platform
import re
import stat
import tempfile
from pathlib import Path
from unittest.mock import patch

from d3_evidence import DEPENDENCY_INVENTORY, EvidenceContractError, locked_environment_evidence, repository_file_hashes
from quant_advisor_research.advisory_report import build_advisory_report
from quant_advisor_research.preview_bundle import read_preview_bundle
from quant_advisor_research.preview_workspace import build_preview_workspace

EVIDENCE_VERSION = "qar.d3.build_evidence.v3"
FIXED_FILES = ("manifest.json", "report.html", "report.json")


def _file_hashes(workspace: Path) -> dict[str, dict[str, object]]:
result: dict[str, dict[str, object]] = {}
for name in FIXED_FILES:
path = workspace / name
info = path.lstat()
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
raise RuntimeError("bundle_member_invalid")
content = path.read_bytes()
result[name] = {"sha256": hashlib.sha256(content).hexdigest(), "size": len(content)}
return result


def _distributions() -> list[str]:
return sorted({f"{name}=={dist.version}" for dist in importlib.metadata.distributions() if (name := dist.metadata.get("Name"))})


def _report(as_of: str, events: Path, watchlist: Path, generated_at: str) -> dict[str, object]:
with patch("quant_advisor_research.advisory_report.utc_now_iso", return_value=generated_at):
return build_advisory_report(as_of=as_of, cadence="daily", political_events_path=events, political_watchlist_path=watchlist)


def build_evidence(args: argparse.Namespace) -> Path:
try:
dependency_files = repository_file_hashes(args.repo_root, DEPENDENCY_INVENTORY)
except EvidenceContractError as exc:
raise RuntimeError(exc.code) from None
lock_path = Path(args.lock_path)
environment = locked_environment_evidence(
lock_sha256=hashlib.sha256(lock_path.read_bytes()).hexdigest(),
uv_version=args.uv_version,
python_version=platform.python_version(),
distributions=_distributions(),
)
parent = Path(tempfile.mkdtemp(prefix="qar-d3-parent-", dir=args.temp_root))
succeeded = False
try:
first = build_preview_workspace(_report(args.as_of, Path(args.political_events), Path(args.political_watchlist), args.frozen_generated_at), parent)
second = build_preview_workspace(_report(args.as_of, Path(args.political_events), Path(args.political_watchlist), args.frozen_generated_at), parent)
read_preview_bundle(first); read_preview_bundle(second)
first_files, second_files = _file_hashes(first), _file_hashes(second)
if first_files != second_files or any((first / n).read_bytes() != (second / n).read_bytes() for n in FIXED_FILES):
raise RuntimeError("repeat_build_not_equal")
manifest = json.loads((first / "manifest.json").read_text(encoding="utf-8"))
source = {"schema_version": "5", "contract_version": "model_recommendations.v5", "cadence": "daily", "as_of": args.as_of, "generated_at": args.frozen_generated_at}
if manifest.get("source") != source or manifest.get("bundle_contract") != "qar.preview_bundle.v1":
raise RuntimeError("source_contract_mismatch")
if not re.fullmatch(r"[0-9a-f]{40}", args.base_sha) or not re.fullmatch(r"[0-9a-f]{40}", args.head_sha):
raise RuntimeError("provenance_sha_invalid")
evidence = {
"evidence_version": EVIDENCE_VERSION, "base_sha": args.base_sha, "head_sha": args.head_sha,
"source": {"fixture_paths": [Path(args.political_events).as_posix(), Path(args.political_watchlist).as_posix()], "provenance": "repository_representative_fixture"},
"deterministic_clock": {"frozen_generated_at": args.frozen_generated_at, "producer_invocations": 2},
"workflow_dependency_inventory": DEPENDENCY_INVENTORY,
"dependency_files": dependency_files,
"locked_environment": environment,
"bundle": {"contract": manifest["bundle_contract"], "source": source, "files": first_files},
"repeat_build": {"independent_invocations": 2, "bytes_equal": True, "files": second_files},
}
evidence_path = Path(args.evidence_path); evidence_path.parent.mkdir(parents=True, exist_ok=True)
evidence_path.write_text(json.dumps(evidence, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
Path(args.workspace_path_file).write_text(str(first) + "\n", encoding="utf-8")
succeeded = True
print(json.dumps({"workspace": str(first), "evidence": str(evidence_path), "base_sha": args.base_sha}, sort_keys=True))
return first
finally:
if not succeeded:
try: parent.rmdir()
except OSError: pass


def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(); p.add_argument("--as-of", required=True); p.add_argument("--political-events", required=True); p.add_argument("--political-watchlist", required=True); p.add_argument("--frozen-generated-at", required=True); p.add_argument("--base-sha", required=True); p.add_argument("--head-sha", required=True); p.add_argument("--uv-version", required=True); p.add_argument("--lock-path", required=True); p.add_argument("--repo-root", required=True); p.add_argument("--temp-root", required=True); p.add_argument("--evidence-path", required=True); p.add_argument("--workspace-path-file", required=True); return p.parse_args()


if __name__ == "__main__": build_evidence(parse_args())
98 changes: 98 additions & 0 deletions scripts/d3_evidence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Tool-agnostic D3 evidence contracts; no uv/process/filesystem discovery."""
from __future__ import annotations

import hashlib
import re
from collections.abc import Iterable, Mapping
from pathlib import Path
import stat

DIST_RE = re.compile(r"^([A-Za-z0-9_.-]+)==([^\s=]+)$")

DEPENDENCY_INVENTORY = [
".github/workflows/qar_d3_daily_preview_artifact.yml", "pyproject.toml", "uv.lock",
"scripts/d3_build_daily_preview.py", "scripts/d3_verify_daily_preview.py", "scripts/d3_evidence.py",
"src/quant_advisor_research/advisory_report.py", "src/quant_advisor_research/artifact_integrity.py",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind the package initializer in evidence

When a PR changes src/quant_advisor_research/__init__.py, the D3 producer still executes that file before loading quant_advisor_research.advisory_report/preview_bundle, but this inventory never hashes it. That lets the build and verifier pass with evidence that omits a repository file that can affect imports and report generation, so the claimed dependency binding is incomplete; add the package initializer to DEPENDENCY_INVENTORY.

Useful? React with 👍 / 👎.

"src/quant_advisor_research/artifacts.py", "src/quant_advisor_research/contracts.py",
"src/quant_advisor_research/csv_utils.py", "src/quant_advisor_research/period_contract.py",
"src/quant_advisor_research/preview_bundle.py", "src/quant_advisor_research/preview_workspace.py",
"src/quant_advisor_research/time_contract.py", "tests/test_d3_tool_agnostic.py",
"examples/political_events.example.csv", "examples/political_watchlist.example.csv",
]


class EvidenceContractError(ValueError):
def __init__(self, code: str) -> None:
self.code = code
super().__init__(code)


def canonical_distribution_snapshot(values: Iterable[str]) -> tuple[list[str], str]:
try:
snapshot = list(values)
except (TypeError, ValueError):
raise EvidenceContractError("distribution_snapshot_invalid") from None
normalized: dict[str, str] = {}
for value in snapshot:
if type(value) is not str or not (match := DIST_RE.fullmatch(value)):
raise EvidenceContractError("distribution_snapshot_invalid")
name = re.sub(r"[-_.]+", "-", match.group(1)).lower()
normalized_value = f"{name}=={match.group(2)}"
if name in normalized and normalized[name] != normalized_value:
raise EvidenceContractError("distribution_snapshot_conflict")
normalized[name] = normalized_value
snapshot = sorted(normalized.values())
if len(snapshot) != len(normalized):
raise EvidenceContractError("distribution_snapshot_invalid")
digest = hashlib.sha256("\n".join(snapshot).encode("utf-8")).hexdigest()
return snapshot, digest


def locked_environment_evidence(*, lock_sha256: str, uv_version: str, python_version: str, distributions: Iterable[str]) -> dict[str, object]:
if type(lock_sha256) is not str or not re.fullmatch(r"[0-9a-f]{64}", lock_sha256):
raise EvidenceContractError("lock_digest_invalid")
if type(uv_version) is not str or not uv_version.startswith("uv "):
raise EvidenceContractError("uv_version_invalid")
if type(python_version) is not str or not re.fullmatch(r"3\.11(?:\.\d+)?", python_version):
raise EvidenceContractError("python_version_invalid")
snapshot, snapshot_sha256 = canonical_distribution_snapshot(distributions)
return {
"lock_sha256": lock_sha256,
"uv_version": uv_version,
"python_version": python_version,
"installed_distributions": snapshot,
"installed_distributions_sha256": snapshot_sha256,
}


def require_exact_locked_environment(value: object, expected: Mapping[str, object]) -> None:
if not isinstance(value, Mapping) or dict(value) != dict(expected):
raise EvidenceContractError("locked_environment_mismatch")


def repository_file_hashes(repo_root: str | Path, relative_paths: Iterable[str]) -> dict[str, str]:
root = Path(repo_root)
try:
root_info = root.stat()
if not stat.S_ISDIR(root_info.st_mode):
raise EvidenceContractError("dependency_root_invalid")
result: dict[str, str] = {}
for raw_path in relative_paths:
if type(raw_path) is not str or not raw_path or Path(raw_path).is_absolute():
raise EvidenceContractError("dependency_path_invalid")
relative = Path(raw_path)
if ".." in relative.parts or raw_path in result:
raise EvidenceContractError("dependency_path_invalid")
target = root / relative
info = target.lstat()
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
raise EvidenceContractError("dependency_file_invalid")
resolved = target.resolve(strict=True)
if resolved != target.absolute():
raise EvidenceContractError("dependency_file_invalid")
result[raw_path] = hashlib.sha256(target.read_bytes()).hexdigest()
return dict(sorted(result.items()))
except EvidenceContractError:
raise
except (OSError, TypeError, ValueError, UnicodeError):
raise EvidenceContractError("dependency_file_invalid") from None
58 changes: 58 additions & 0 deletions scripts/d3_verify_daily_preview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Workflow-only downloaded artifact verifier with exact environment binding."""
from __future__ import annotations

import argparse
import hashlib
import importlib.metadata
import json
import platform
from pathlib import Path

from d3_evidence import DEPENDENCY_INVENTORY, EvidenceContractError, locked_environment_evidence, repository_file_hashes
from quant_advisor_research.preview_bundle import read_preview_bundle

EVIDENCE_VERSION = "qar.d3.build_evidence.v3"
FIXED_FILES = ("manifest.json", "report.html", "report.json")
EXPECTED_EVIDENCE_KEYS = frozenset({
"evidence_version", "base_sha", "head_sha", "source", "deterministic_clock", "workflow_dependency_inventory",
"dependency_files",
"locked_environment", "bundle", "repeat_build",
})


def _file_hashes(workspace: Path) -> dict[str, dict[str, object]]:
return {name: {"sha256": hashlib.sha256((workspace / name).read_bytes()).hexdigest(), "size": (workspace / name).stat().st_size} for name in FIXED_FILES}


def _distributions() -> list[str]:
return sorted({f"{name}=={dist.version}" for dist in importlib.metadata.distributions() if (name := dist.metadata.get("Name"))})


def verify(args: argparse.Namespace) -> None:
evidence = json.loads(Path(args.evidence_path).read_text(encoding="utf-8"))
if not isinstance(evidence, dict) or set(evidence) != EXPECTED_EVIDENCE_KEYS:
raise ValueError("build_evidence_shape_invalid")
if evidence.get("workflow_dependency_inventory") != DEPENDENCY_INVENTORY:
raise ValueError("dependency_inventory_mismatch")
try:
dependency_files = repository_file_hashes(args.repo_root, DEPENDENCY_INVENTORY)
except EvidenceContractError as exc:
raise ValueError(exc.code) from None
expected_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())
expected_source = {"schema_version": "5", "contract_version": "model_recommendations.v5", "cadence": "daily", "as_of": args.as_of, "generated_at": args.frozen_generated_at}
workspace = Path(args.workspace); read_preview_bundle(workspace)
if {p.name for p in workspace.iterdir()} != set(FIXED_FILES): raise ValueError("file_set_invalid")
manifest = json.loads((workspace / "manifest.json").read_text(encoding="utf-8"))
if manifest.get("source") != expected_source or manifest.get("bundle_contract") != "qar.preview_bundle.v1": raise ValueError("source_contract_mismatch")
files = _file_hashes(workspace)
expected = {"evidence_version": EVIDENCE_VERSION, "base_sha": args.base_sha, "head_sha": args.head_sha, "source": {"fixture_paths": [Path(args.political_events).as_posix(), Path(args.political_watchlist).as_posix()], "provenance": "repository_representative_fixture"}, "deterministic_clock": {"frozen_generated_at": args.frozen_generated_at, "producer_invocations": 2}, "workflow_dependency_inventory": DEPENDENCY_INVENTORY, "dependency_files": dependency_files, "locked_environment": expected_environment, "bundle": {"contract": "qar.preview_bundle.v1", "source": expected_source, "files": files}, "repeat_build": {"independent_invocations": 2, "bytes_equal": True, "files": files}}
if evidence != expected: raise ValueError("build_evidence_mismatch")
print(json.dumps({"status": "passed", "evidence_binding": "exact_full_payload", "base_sha": args.base_sha, "files": files}, sort_keys=True))


def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(); p.add_argument("--workspace", required=True); p.add_argument("--evidence-path", required=True); p.add_argument("--base-sha", required=True); p.add_argument("--head-sha", required=True); p.add_argument("--uv-version", required=True); p.add_argument("--lock-path", required=True); p.add_argument("--repo-root", required=True); p.add_argument("--as-of", required=True); p.add_argument("--political-events", required=True); p.add_argument("--political-watchlist", required=True); p.add_argument("--frozen-generated-at", required=True); return p.parse_args()


if __name__ == "__main__": verify(parse_args())
Loading
Loading