Skip to content

Commit 108cc8f

Browse files
authored
Merge pull request #69 from QuantStrategyLab/codex/ai-g3-strict-ingestion-20260902
Enforce strict AI signal ingestion and advisory v6
2 parents a24a190 + 1183293 commit 108cc8f

29 files changed

Lines changed: 901 additions & 96 deletions

.github/workflows/cross_repo_smoke.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
workflow_dispatch:
55
inputs:
66
as_of:
7-
description: "Smoke date. Defaults to current UTC date when empty."
7+
description: "Smoke date. Defaults to the most recently closed UTC date when empty."
88
required: false
99
type: string
1010
schedule:
@@ -48,7 +48,11 @@ jobs:
4848
INPUT_AS_OF: ${{ github.event.inputs.as_of || '' }}
4949
run: |
5050
set -euo pipefail
51-
AS_OF="${INPUT_AS_OF:-$(date -u +%F)}"
51+
if [ -n "${INPUT_AS_OF}" ]; then
52+
AS_OF="${INPUT_AS_OF}"
53+
else
54+
AS_OF="$(python -c 'from quant_advisor_research.build_pipeline import default_daily_as_of; print(default_daily_as_of().isoformat())')"
55+
fi
5256
python scripts/run_cross_repo_smoke.py \
5357
--as-of "${AS_OF}" \
5458
--political-events ../political-events/data/live/political_events.csv \

.github/workflows/monthly_advisory_review.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
workflow_dispatch:
55
inputs:
66
as_of:
7-
description: "Report date. Defaults to current UTC date when empty."
7+
description: "Report date. Defaults to the last day of the most recently closed UTC month when empty."
88
required: false
99
type: string
1010
previous_report_path:
@@ -104,7 +104,11 @@ jobs:
104104
MARKET_DATA_PROXY_POOL_URL: ${{ github.event.inputs.market_data_proxy_pool_url || vars.MARKET_DATA_PROXY_POOL_URL || '' }}
105105
run: |
106106
set -euo pipefail
107-
AS_OF="${INPUT_AS_OF:-$(date -u +%F)}"
107+
if [ -n "${INPUT_AS_OF}" ]; then
108+
AS_OF="${INPUT_AS_OF}"
109+
else
110+
AS_OF="$(python -c 'from quant_advisor_research.build_pipeline import default_monthly_as_of; print(default_monthly_as_of().isoformat())')"
111+
fi
108112
mkdir -p .cache/market-data
109113
ARGS=(
110114
--as-of "${AS_OF}"

.github/workflows/publish_advisory_site.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
workflow_dispatch:
55
inputs:
66
as_of:
7-
description: "Report date. Defaults to the most recent Saturday when empty."
7+
description: "Report date. Defaults to the most recent fully closed Saturday when empty."
88
required: false
99
type: string
1010
site_url:

.github/workflows/weekly_advisory_review.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
workflow_dispatch:
55
inputs:
66
as_of:
7-
description: "Report date. Defaults to current UTC date when empty."
7+
description: "Report date. Defaults to the most recent fully closed Saturday when empty."
88
required: false
99
type: string
1010
political_events_path:
@@ -98,7 +98,11 @@ jobs:
9898
MARKET_DATA_PROXY_POOL_URL: ${{ github.event.inputs.market_data_proxy_pool_url || vars.MARKET_DATA_PROXY_POOL_URL || '' }}
9999
run: |
100100
set -euo pipefail
101-
AS_OF="${INPUT_AS_OF:-$(date -u +%F)}"
101+
if [ -n "${INPUT_AS_OF}" ]; then
102+
AS_OF="${INPUT_AS_OF}"
103+
else
104+
AS_OF="$(python -c 'from quant_advisor_research.build_pipeline import default_weekly_as_of; print(default_weekly_as_of().isoformat())')"
105+
fi
102106
mkdir -p .cache/market-data
103107
ARGS=(
104108
--as-of "${AS_OF}"

docs/advisory_contract.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
55
Required top-level fields:
66

77
```text
8-
schema_version: "5"
8+
schema_version: "6"
9+
contract_version: "model_recommendations.v6"
910
as_of: ISO date
11+
reference_time: exclusive start of the UTC day after as_of
1012
generated_at: ISO datetime
13+
expires_at: generated_at + 7 days
14+
input_digest: 64 lowercase hexadecimal SHA-256 characters
15+
freshness.ai_signal
16+
freshness.theme_momentum
1117
mode: "model_recommendations"
1218
cadence: "daily" | "weekly" | "monthly"
1319
audience_scope: "non_personalized_model_research"
@@ -19,12 +25,10 @@ final_decisions: object, optional
1925
policy: object
2026
```
2127

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

27-
### V6 dual-read migration contract
31+
### V6 default contract
2832

2933
A v6 report uses `contract_version = model_recommendations.v6` and adds these
3034
required top-level fields:
@@ -307,9 +311,9 @@ Required manifest fields:
307311
```text
308312
manifest_type = model_recommendation_report
309313
artifact_type = model_recommendations
310-
contract_version = model_recommendations.v5
311-
schema_version = 5
312-
version = <as_of>-<cadence>-schema-5-<run-or-sha>
314+
contract_version = model_recommendations.v6
315+
schema_version = 6
316+
version = <as_of>-<cadence>-schema-6-<run-or-sha>
313317
source_project = QuantAdvisorResearch
314318
producer.repository
315319
producer.git_sha
@@ -320,10 +324,11 @@ artifacts.json.sha256
320324
artifacts.markdown.sha256
321325
policy
322326
generated_at
327+
input_digest
323328
```
324329

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

328333
## Source Mode
329334

examples/research_signal_context.example.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
2-
"schema_version": "1",
2+
"schema_version": "2",
3+
"model_version": "shadow-v2",
4+
"scoring_version": "rules-v2",
35
"as_of": "2026-05-28",
46
"generated_at": "2026-05-28T14:38:47.474891Z",
57
"mode": "shadow",
@@ -19,7 +21,7 @@
1921
"semiconductor_and_leveraged_etf_high_volatility",
2022
"no_point_in_time_replay_evidence"
2123
],
22-
"research_bias": {
24+
"candidate_bias": {
2325
"IDX1": "positive",
2426
"IDX2": "positive",
2527
"SECT1": "watch",

scripts/d3_build_daily_preview.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from pathlib import Path
1313
from unittest.mock import patch
1414

15-
from d3_evidence import DEPENDENCY_INVENTORY, EVIDENCE_VERSION, BundleSnapshot, EvidenceContractError, locked_environment_evidence, repository_file_hashes, validate_exact_bundle, validate_preview_snapshot
15+
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
1616
from quant_advisor_research.advisory_report import build_advisory_report
1717
from quant_advisor_research.preview_workspace import build_preview_workspace
1818

@@ -45,13 +45,14 @@ def build_evidence(args: argparse.Namespace) -> None:
4545
raise EvidenceContractError("repeat_workspace_not_distinct")
4646
first_snapshot = validate_exact_bundle(first)
4747
second_snapshot = validate_exact_bundle(second)
48-
validate_preview_snapshot(first_snapshot); validate_preview_snapshot(second_snapshot)
48+
validate_preview_snapshot(first_snapshot)
49+
validate_preview_snapshot(second_snapshot)
4950
first_files, second_files = _bundle_hashes(first_snapshot), _bundle_hashes(second_snapshot)
5051
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")):
5152
raise EvidenceContractError("repeat_build_not_equal")
5253
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:
54+
source = preview_source_contract(manifest, as_of=args.as_of, generated_at=args.frozen_generated_at)
55+
if manifest.get("bundle_contract") != "qar.preview_bundle.v1":
5556
raise EvidenceContractError("source_contract_mismatch")
5657
evidence = {
5758
"evidence_version": EVIDENCE_VERSION, "base_sha": args.base_sha, "head_sha": args.head_sha,

scripts/d3_evidence.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"src/quant_advisor_research/time_contract.py", "tests/test_d3_exact_bundle.py",
2424
"examples/political_events.example.csv", "examples/political_watchlist.example.csv",
2525
]
26+
SOURCE_CONTRACTS = {"5": "model_recommendations.v5", "6": "model_recommendations.v6"}
2627

2728

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

5152

53+
def preview_source_contract(
54+
manifest: Mapping[str, object],
55+
*,
56+
as_of: str,
57+
generated_at: str,
58+
) -> dict[str, object]:
59+
source = manifest.get("source")
60+
if not isinstance(source, Mapping) or set(source) != {
61+
"schema_version",
62+
"contract_version",
63+
"cadence",
64+
"as_of",
65+
"generated_at",
66+
}:
67+
raise EvidenceContractError("source_contract_mismatch")
68+
schema_version = source.get("schema_version")
69+
if (
70+
type(schema_version) is not str
71+
or source.get("contract_version") != SOURCE_CONTRACTS.get(schema_version)
72+
or source.get("cadence") != "daily"
73+
or source.get("as_of") != as_of
74+
or source.get("generated_at") != generated_at
75+
):
76+
raise EvidenceContractError("source_contract_mismatch")
77+
return dict(source)
78+
79+
5280
def _directory_flags() -> int:
5381
required = ("O_DIRECTORY", "O_CLOEXEC", "O_NOFOLLOW")
5482
if any(not hasattr(os, name) for name in required):

scripts/d3_verify_daily_preview.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import platform
1010
from pathlib import Path
1111

12-
from d3_evidence import DEPENDENCY_INVENTORY, EVIDENCE_VERSION, BundleSnapshot, EvidenceContractError, locked_environment_evidence, repository_file_hashes, validate_exact_bundle, validate_preview_snapshot
12+
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
1313

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

@@ -31,10 +31,14 @@ def verify(args: argparse.Namespace) -> None:
3131
if evidence["workflow_dependency_inventory"] != DEPENDENCY_INVENTORY or evidence["dependency_files"] != dependency_files:
3232
raise EvidenceContractError("dependency_evidence_mismatch")
3333
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())
34-
expected_source = {"schema_version": "5", "contract_version": "model_recommendations.v5", "cadence": "daily", "as_of": args.as_of, "generated_at": args.frozen_generated_at}
3534
snapshot = validate_exact_bundle(args.workspace)
3635
_, manifest = validate_preview_snapshot(snapshot)
37-
if manifest.get("bundle_contract") != "qar.preview_bundle.v1" or manifest.get("source") != expected_source:
36+
expected_source = preview_source_contract(
37+
manifest,
38+
as_of=args.as_of,
39+
generated_at=args.frozen_generated_at,
40+
)
41+
if manifest.get("bundle_contract") != "qar.preview_bundle.v1":
3842
raise EvidenceContractError("source_contract_mismatch")
3943
files = _hashes(snapshot)
4044
expected = {

0 commit comments

Comments
 (0)