Skip to content
Merged
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
8 changes: 6 additions & 2 deletions .github/workflows/cross_repo_smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
as_of:
description: "Smoke date. Defaults to current UTC date when empty."
description: "Smoke date. Defaults to the most recently closed UTC date when empty."
required: false
type: string
schedule:
Expand Down Expand Up @@ -48,7 +48,11 @@ jobs:
INPUT_AS_OF: ${{ github.event.inputs.as_of || '' }}
run: |
set -euo pipefail
AS_OF="${INPUT_AS_OF:-$(date -u +%F)}"
if [ -n "${INPUT_AS_OF}" ]; then
AS_OF="${INPUT_AS_OF}"
else
AS_OF="$(python -c 'from quant_advisor_research.build_pipeline import default_daily_as_of; print(default_daily_as_of().isoformat())')"
fi
python scripts/run_cross_repo_smoke.py \
--as-of "${AS_OF}" \
--political-events ../political-events/data/live/political_events.csv \
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/monthly_advisory_review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
as_of:
description: "Report date. Defaults to current UTC date when empty."
description: "Report date. Defaults to the last day of the most recently closed UTC month when empty."
required: false
type: string
previous_report_path:
Expand Down Expand Up @@ -104,7 +104,11 @@ jobs:
MARKET_DATA_PROXY_POOL_URL: ${{ github.event.inputs.market_data_proxy_pool_url || vars.MARKET_DATA_PROXY_POOL_URL || '' }}
run: |
set -euo pipefail
AS_OF="${INPUT_AS_OF:-$(date -u +%F)}"
if [ -n "${INPUT_AS_OF}" ]; then
AS_OF="${INPUT_AS_OF}"
else
AS_OF="$(python -c 'from quant_advisor_research.build_pipeline import default_monthly_as_of; print(default_monthly_as_of().isoformat())')"
fi
mkdir -p .cache/market-data
ARGS=(
--as-of "${AS_OF}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish_advisory_site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
as_of:
description: "Report date. Defaults to the most recent Saturday when empty."
description: "Report date. Defaults to the most recent fully closed Saturday when empty."
required: false
type: string
site_url:
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/weekly_advisory_review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
as_of:
description: "Report date. Defaults to current UTC date when empty."
description: "Report date. Defaults to the most recent fully closed Saturday when empty."
required: false
type: string
political_events_path:
Expand Down Expand Up @@ -98,7 +98,11 @@ jobs:
MARKET_DATA_PROXY_POOL_URL: ${{ github.event.inputs.market_data_proxy_pool_url || vars.MARKET_DATA_PROXY_POOL_URL || '' }}
run: |
set -euo pipefail
AS_OF="${INPUT_AS_OF:-$(date -u +%F)}"
if [ -n "${INPUT_AS_OF}" ]; then
AS_OF="${INPUT_AS_OF}"
else
AS_OF="$(python -c 'from quant_advisor_research.build_pipeline import default_weekly_as_of; print(default_weekly_as_of().isoformat())')"
fi
mkdir -p .cache/market-data
ARGS=(
--as-of "${AS_OF}"
Expand Down
27 changes: 16 additions & 11 deletions docs/advisory_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
Required top-level fields:

```text
schema_version: "5"
schema_version: "6"
contract_version: "model_recommendations.v6"
as_of: ISO date
reference_time: exclusive start of the UTC day after as_of
generated_at: ISO datetime
expires_at: generated_at + 7 days
input_digest: 64 lowercase hexadecimal SHA-256 characters
freshness.ai_signal
freshness.theme_momentum
mode: "model_recommendations"
cadence: "daily" | "weekly" | "monthly"
audience_scope: "non_personalized_model_research"
Expand All @@ -19,12 +25,10 @@ final_decisions: object, optional
policy: object
```

The current CLI and scheduled workflows remain explicit v5 producers. Readers
accept both v5 and v6, but a producer must opt in to v6 only after it can emit
all v6 provenance fields. No v6 field is silently added to, or accepted on, a
v5 report.
The current CLI and scheduled workflows produce v6. Readers continue to accept
legacy v5 reports, but no v6 field is silently accepted on a v5 report.

### V6 dual-read migration contract
### V6 default contract

A v6 report uses `contract_version = model_recommendations.v6` and adds these
required top-level fields:
Expand Down Expand Up @@ -307,9 +311,9 @@ Required manifest fields:
```text
manifest_type = model_recommendation_report
artifact_type = model_recommendations
contract_version = model_recommendations.v5
schema_version = 5
version = <as_of>-<cadence>-schema-5-<run-or-sha>
contract_version = model_recommendations.v6
schema_version = 6
version = <as_of>-<cadence>-schema-6-<run-or-sha>
source_project = QuantAdvisorResearch
producer.repository
producer.git_sha
Expand All @@ -320,10 +324,11 @@ artifacts.json.sha256
artifacts.markdown.sha256
policy
generated_at
input_digest
```

For a v6 report, the manifest additionally carries the exact top-level
`input_digest`. V5 manifests remain unchanged and omit this field.
The v6 manifest carries the exact top-level `input_digest`. Legacy v5 manifests
remain readable and omit this field.

## Source Mode

Expand Down
6 changes: 4 additions & 2 deletions examples/research_signal_context.example.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"schema_version": "1",
"schema_version": "2",
"model_version": "shadow-v2",
"scoring_version": "rules-v2",
"as_of": "2026-05-28",
"generated_at": "2026-05-28T14:38:47.474891Z",
"mode": "shadow",
Expand All @@ -19,7 +21,7 @@
"semiconductor_and_leveraged_etf_high_volatility",
"no_point_in_time_replay_evidence"
],
"research_bias": {
"candidate_bias": {
"IDX1": "positive",
"IDX2": "positive",
"SECT1": "watch",
Expand Down
9 changes: 5 additions & 4 deletions scripts/d3_build_daily_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from pathlib import Path
from unittest.mock import patch

from d3_evidence import DEPENDENCY_INVENTORY, EVIDENCE_VERSION, BundleSnapshot, EvidenceContractError, locked_environment_evidence, repository_file_hashes, validate_exact_bundle, validate_preview_snapshot
from d3_evidence import DEPENDENCY_INVENTORY, EVIDENCE_VERSION, BundleSnapshot, EvidenceContractError, locked_environment_evidence, preview_source_contract, repository_file_hashes, validate_exact_bundle, validate_preview_snapshot
from quant_advisor_research.advisory_report import build_advisory_report
from quant_advisor_research.preview_workspace import build_preview_workspace

Expand Down Expand Up @@ -45,13 +45,14 @@ def build_evidence(args: argparse.Namespace) -> None:
raise EvidenceContractError("repeat_workspace_not_distinct")
first_snapshot = validate_exact_bundle(first)
second_snapshot = validate_exact_bundle(second)
validate_preview_snapshot(first_snapshot); validate_preview_snapshot(second_snapshot)
validate_preview_snapshot(first_snapshot)
validate_preview_snapshot(second_snapshot)
first_files, second_files = _bundle_hashes(first_snapshot), _bundle_hashes(second_snapshot)
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")):
raise EvidenceContractError("repeat_build_not_equal")
manifest = json.loads(first_snapshot.member("manifest.json").content.decode("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("bundle_contract") != "qar.preview_bundle.v1" or manifest.get("source") != source:
source = preview_source_contract(manifest, as_of=args.as_of, generated_at=args.frozen_generated_at)
if manifest.get("bundle_contract") != "qar.preview_bundle.v1":
raise EvidenceContractError("source_contract_mismatch")
evidence = {
"evidence_version": EVIDENCE_VERSION, "base_sha": args.base_sha, "head_sha": args.head_sha,
Expand Down
28 changes: 28 additions & 0 deletions scripts/d3_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"src/quant_advisor_research/time_contract.py", "tests/test_d3_exact_bundle.py",
"examples/political_events.example.csv", "examples/political_watchlist.example.csv",
]
SOURCE_CONTRACTS = {"5": "model_recommendations.v5", "6": "model_recommendations.v6"}


class EvidenceContractError(ValueError):
Expand All @@ -49,6 +50,33 @@ def member(self, name: str) -> BundleMemberSnapshot:
raise EvidenceContractError("bundle_member_set_invalid")


def preview_source_contract(
manifest: Mapping[str, object],
*,
as_of: str,
generated_at: str,
) -> dict[str, object]:
source = manifest.get("source")
if not isinstance(source, Mapping) or set(source) != {
"schema_version",
"contract_version",
"cadence",
"as_of",
"generated_at",
}:
raise EvidenceContractError("source_contract_mismatch")
schema_version = source.get("schema_version")
if (
type(schema_version) is not str
or source.get("contract_version") != SOURCE_CONTRACTS.get(schema_version)
or source.get("cadence") != "daily"
or source.get("as_of") != as_of
or source.get("generated_at") != generated_at
):
raise EvidenceContractError("source_contract_mismatch")
return dict(source)


def _directory_flags() -> int:
required = ("O_DIRECTORY", "O_CLOEXEC", "O_NOFOLLOW")
if any(not hasattr(os, name) for name in required):
Expand Down
10 changes: 7 additions & 3 deletions scripts/d3_verify_daily_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import platform
from pathlib import Path

from d3_evidence import DEPENDENCY_INVENTORY, EVIDENCE_VERSION, BundleSnapshot, EvidenceContractError, locked_environment_evidence, repository_file_hashes, validate_exact_bundle, validate_preview_snapshot
from d3_evidence import DEPENDENCY_INVENTORY, EVIDENCE_VERSION, BundleSnapshot, EvidenceContractError, locked_environment_evidence, preview_source_contract, repository_file_hashes, validate_exact_bundle, validate_preview_snapshot

EXPECTED_KEYS = frozenset({"evidence_version", "base_sha", "head_sha", "source", "deterministic_clock", "workflow_dependency_inventory", "dependency_files", "locked_environment", "bundle", "repeat_build"})

Expand All @@ -31,10 +31,14 @@ def verify(args: argparse.Namespace) -> None:
if evidence["workflow_dependency_inventory"] != DEPENDENCY_INVENTORY or evidence["dependency_files"] != dependency_files:
raise EvidenceContractError("dependency_evidence_mismatch")
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}
snapshot = validate_exact_bundle(args.workspace)
_, manifest = validate_preview_snapshot(snapshot)
if manifest.get("bundle_contract") != "qar.preview_bundle.v1" or manifest.get("source") != expected_source:
expected_source = preview_source_contract(
manifest,
as_of=args.as_of,
generated_at=args.frozen_generated_at,
)
if manifest.get("bundle_contract") != "qar.preview_bundle.v1":
raise EvidenceContractError("source_contract_mismatch")
files = _hashes(snapshot)
expected = {
Expand Down
Loading
Loading