From d15ff45a8fa39cc46d1b5b280b61a7fbbf8aa4b7 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:56:29 +0800 Subject: [PATCH] fix: schedule weekly advisory after the closed UTC cutoff --- .github/workflows/publish_advisory_site.yml | 2 +- .github/workflows/weekly_advisory_review.yml | 2 +- docs/data_factor_roadmap.md | 33 +++++++++++++ docs/data_factor_roadmap.zh-CN.md | 18 +++++++ docs/system_design.md | 45 +++++++++++++++++ docs/system_design.zh-CN.md | 29 +++++++++++ tests/test_build_pipeline.py | 51 ++++++++++++++++++++ tests/test_workflow_defaults.py | 18 +++++++ 8 files changed, 196 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish_advisory_site.yml b/.github/workflows/publish_advisory_site.yml index 5e5bf01..cc81344 100644 --- a/.github/workflows/publish_advisory_site.yml +++ b/.github/workflows/publish_advisory_site.yml @@ -48,7 +48,7 @@ on: default: "" type: string schedule: - - cron: "0 13 * * 6" + - cron: "0 13 * * 0" permissions: contents: read diff --git a/.github/workflows/weekly_advisory_review.yml b/.github/workflows/weekly_advisory_review.yml index 416823a..27a0676 100644 --- a/.github/workflows/weekly_advisory_review.yml +++ b/.github/workflows/weekly_advisory_review.yml @@ -43,7 +43,7 @@ on: default: "" type: string schedule: - - cron: "30 12 * * 6" + - cron: "30 12 * * 0" permissions: contents: read diff --git a/docs/data_factor_roadmap.md b/docs/data_factor_roadmap.md index 76a2404..7f95354 100644 --- a/docs/data_factor_roadmap.md +++ b/docs/data_factor_roadmap.md @@ -80,6 +80,39 @@ Priority order: 6. Macro/risk regime: VIX, rates, dollar, credit spreads, oil, yield curve, and sector beta. +## 2026-10-06 input publication recovery (planned) + +Read-only evidence shows that the Advisor still builds and publishes, but its +checked-out upstream inputs do not reflect the latest generated artifacts. +The [October 6 report build](https://github.com/QuantStrategyLab/QuantAdvisorResearch/actions/runs/37460712408) +has `as_of=2026-10-03`, 42 market-confirmation rows, and zero final picks. The +rejections are `ai_signal_provenance_untrusted` and +`theme_momentum_stale_as_of`; these gates must remain unchanged. + +- [x] Separate generation, protected publication, and consumer acceptance: + RSCP's [fresh theme artifact](https://github.com/QuantStrategyLab/ResearchSignalContextPipelines/actions/runs/37132550227) + was retained after a protected-main push rejection; PETR produced a manual + publication handoff; the latest AI request failed without pushing files +- [x] Prepare and locally test the two weekly Advisor schedules for Sunday + 12:30/13:00 UTC, retaining their clock times, closed-period helper, and freshness + contract; remote publication remains pending +- [x] Record the RSCP generation-versus-publication distinction and fixed-artifact + review requirements; workflow identity changes remain deferred +- [ ] Review the exact existing theme artifact and publish it through a normal + PR only after its producer metadata, bytes, time bounds, and policy pass + verification; verify the actual PR head's required `test` check +- [ ] Resolve event-entity evidence and AI signal/manifest recovery in their + owning workstreams, without broadening this bounded change +- [ ] After separately authorized publication, verify the upstream commit or + artifact digest actually consumed, report freshness, and deployed report hash + +A successful generation run is not completed publication. A one-time reviewed +PR restores one snapshot only; it does not establish unattended weekly delivery. +Zero final picks remain an acceptable result when valid evidence is insufficient. +See [the recovery constraints](system_design.md#input-publication-recovery-2026-10-06) +for the verified identity and timing limits. This checklist records planned work, +not a completed repair. + ## Low-Risk Implementation Order 1. Keep public output focused on final recommendations. Preserve diff --git a/docs/data_factor_roadmap.zh-CN.md b/docs/data_factor_roadmap.zh-CN.md index 96231b1..373ae86 100644 --- a/docs/data_factor_roadmap.zh-CN.md +++ b/docs/data_factor_roadmap.zh-CN.md @@ -1,5 +1,23 @@ # 数据源与因子完善路线 +## 2026-10-06 输入发布恢复(待实施) + +只读核查确认投顾仍在生成和发布,但 checkout 的上游 main 文件没有跟上新产物。 +[10/06 构建](https://github.com/QuantStrategyLab/QuantAdvisorResearch/actions/runs/37460712408) +报告截止日为 10/03,有 42 条市场确认,最终推荐和观察均为 0;质量告警为 +`ai_signal_provenance_untrusted` 和 `theme_momentum_stale_as_of`,保留这些门槛。 + +- [x] 区分生成、受保护发布和消费者接受:RSCP 新主题因保护规则拒绝推 main 后只留 artifact;PETR 只产人工交接;最新 AI 请求失败且未推文件 +- [x] 已准备并本地测试两条周任务的 UTC 周日 12:30/13:00 候选,保留原有时分、周期算法和时间合同;远端发布待处理 +- [x] 已记录 RSCP 生成与发布的区别及固定 artifact 审核要求;持续发布身份改动暂缓 +- [ ] 对固定主题 artifact 验证生产者元数据、原始字节、时间和政策后,另行审核正常 PR;核实该 PR 实际 head 的必需 test +- [ ] 事件实体证据与 AI signal/manifest 恢复由各自负责工作线处理,不扩大本次有限写集 +- [ ] 发布另获授权后,核验真正消费的上游 commit 或 artifact digest、报告新鲜度及部署报告 hash + +一次正常 PR 只恢复一个快照,不代表每周自动交付闭环。有效证据不足时,最终数量为 0 +仍是合法结果;不得降低门槛或改写历史生成时间来制造非零结果。身份和截止时点约束见 +[系统设计恢复说明](system_design.zh-CN.md#2026-10-06-输入发布恢复)。当前清单表示计划,并非修复已完成。 + [English](data_factor_roadmap.md) | [简体中文](data_factor_roadmap.zh-CN.md) ## 当前结论 diff --git a/docs/system_design.md b/docs/system_design.md index e048a5a..ad9d091 100644 --- a/docs/system_design.md +++ b/docs/system_design.md @@ -168,6 +168,51 @@ monthly, and publish workflows generate it automatically. Short-horizon gates require market confirmation, medium-horizon gates are led by theme and symbol momentum, and long-horizon gates require durable AI shadow or context strength. +## Input Publication Recovery (2026-10-06) + +The bounded recovery changes scheduling and documents publication handoff. It does +not change renderers, score thresholds, account boundaries, or source contracts. +The [recovery checklist](data_factor_roadmap.md#2026-10-06-input-publication-recovery-planned) +tracks implementation and acceptance separately. + +At incident capture, the weekly jobs ran on Saturday while `default_weekly_as_of()` selects +the last *fully closed* Saturday. For example, a 2026-10-03 Saturday run selects +2026-09-26, whose `reference_time` is 2026-09-27 00:00 UTC. A theme generated on +2026-10-03 must not pass that earlier cutoff. The local candidate schedules weekly +review at 12:30 UTC Sunday and publication at 13:00 UTC Sunday, preserving the +existing times. Both use +the same helper after 00:00 UTC, so the new report's cutoff includes Saturday's +already-generated context. A delayed run must still use authentic source dates; +changing `generated_at`, expiry, or freshness limits is not a recovery mechanism. + +`ResearchSignalContextPipelines` is public, and current workflows check out its +public Git content. That is not proof that the Advisor's job token can download +cross-repository Actions artifacts. The documented artifact download API needs +Actions read access to the producer repository. The current workflow exposes no +verified publisher identity or token with that target-repository permission. +Connector read access during investigation is not a substitute for CI access. +No cross-repository artifact reader is enabled by this plan. + +The current theme job has `contents: write` only. It cannot be assumed to create +PRs, and required checks cannot be assumed to run automatically. GitHub's current +[`GITHUB_TOKEN` documentation](https://docs.github.com/en/actions/concepts/security/github_token) +says PR opened/synchronize/reopened events created with that token produce +approval-required runs. A normal reviewed PR must still have its actual head's +required checks completed. Do not change branch protection or grant new access +as part of this recovery. + +An exact retained artifact may be considered for a separately reviewed one-time +PR. Before import, verify repository ID, approved workflow path and source SHA, +same-repository `main` run, event, conclusion, run attempt, artifact ID/name, +expiry, archive digest, exact member path and file hash, and the snapshot's +schema/policy/coverage/time contract. Extraction alone proves none of these. +Record generated, review-required, published, and consumer-accepted as distinct +states. A reviewed PR does not solve the remaining unattended CI identity gap. + +References: [artifact API](https://docs.github.com/en/rest/actions/artifacts#download-an-artifact), +[fixed producer run](https://github.com/QuantStrategyLab/ResearchSignalContextPipelines/actions/runs/37132550227), +[consumer build](https://github.com/QuantStrategyLab/QuantAdvisorResearch/actions/runs/37460712408). + ## Fixture vs Live Inputs Reports built from `examples/` are `source_mode=fixture` and are suitable for diff --git a/docs/system_design.zh-CN.md b/docs/system_design.zh-CN.md index a1af71a..d69996d 100644 --- a/docs/system_design.zh-CN.md +++ b/docs/system_design.zh-CN.md @@ -2,6 +2,35 @@ [English](system_design.md) | [简体中文](system_design.zh-CN.md) +## 2026-10-06 输入发布恢复 + +本次计划只修改周任务截止时点并记录主题发布交接,不改网页、评分门槛、账户边界或可信输入合同。 +进度见[恢复清单](data_factor_roadmap.zh-CN.md#2026-10-06-输入发布恢复待实施)。 + +事件核查时,周六运行却选“最近完全闭合的周六”:10/03 的运行选 09/26,参考截止时间为 +09/27 00:00 UTC,因此 10/03 新生成的主题不能合法用于这一旧截止日。本地候选将两条周任务 +移到 UTC 周日 12:30/13:00,保留原有时分和周期函数,使当周六生成的上下文处于报告截止前。 +延迟执行仍按真实来源时间验证,不补假日期、不延长 freshness 或 expiry 来消除告警。 + +RSCP 是公开仓库;当前 QAR 能 checkout 公开 Git 内容,不代表其 job token 能读取 +RSCP 的 Actions artifact 下载。下载 API 要求生产者仓库的 Actions read 权限,当前 +工作流没有已验证的跨仓库读取身份。诊断连接器读取成功不能替代 CI 身份证明,本方案 +暂不新增消费者 artifact 下载器。 + +主题任务当前只有 contents:write,不能假定可开 PR。按 GitHub 当前官方文档, +GITHUB_TOKEN 创建或更新 PR 会产生 approval-required 的 opened/synchronize/reopened +运行,仍须人工批准并核验实际 PR head 的必需检查;不以修改分支保护或扩大权限解决。 + +固定既有 artifact 的一次正常 PR 导入须另行审核:核实仓库 ID、批准的 workflow 路径与 +源码 SHA、同仓 main、event、run attempt、成功结论、artifact ID/name/有效期、压缩包 +digest、唯一文件路径和 hash,以及快照 schema/policy/覆盖率/时间合同。解压复制不等于 +可信通过。生成、待审、发布、被消费者接受是不同状态;一次 PR 不代表持续自动化已恢复。 + +依据:[固定生产运行](https://github.com/QuantStrategyLab/ResearchSignalContextPipelines/actions/runs/37132550227)、 +[GITHUB_TOKEN 触发规则](https://docs.github.com/en/actions/concepts/security/github_token)、 +[artifact 下载规则](https://docs.github.com/en/rest/actions/artifacts#download-an-artifact)。 + + ## 当前架构理解 QuantStrategyLab 现有仓库已经天然分层: diff --git a/tests/test_build_pipeline.py b/tests/test_build_pipeline.py index 43fe8e1..2d9c780 100644 --- a/tests/test_build_pipeline.py +++ b/tests/test_build_pipeline.py @@ -15,6 +15,7 @@ default_weekly_as_of, ) from quant_advisor_research.cross_repo_smoke import run_cross_repo_smoke +from quant_advisor_research.time_contract import assess_context_freshness, report_time_bounds ROOT = Path(__file__).resolve().parents[1] @@ -57,6 +58,56 @@ def test_default_as_of_values_use_only_closed_periods() -> None: assert default_monthly_as_of(dt.date(2026, 6, 20)) == dt.date(2026, 5, 31) +@pytest.mark.parametrize( + ("run_date", "expected_as_of"), + [ + (dt.date(2026, 10, 3), dt.date(2026, 9, 26)), + (dt.date(2026, 10, 4), dt.date(2026, 10, 3)), + (dt.date(2026, 1, 3), dt.date(2025, 12, 27)), + (dt.date(2026, 1, 4), dt.date(2026, 1, 3)), + (dt.date(2026, 2, 1), dt.date(2026, 1, 31)), + (dt.date(2026, 11, 1), dt.date(2026, 10, 31)), + ], +) +def test_weekly_cutoff_keeps_saturday_closed_and_rolls_on_sunday( + run_date: dt.date, expected_as_of: dt.date +) -> None: + assert default_weekly_as_of(run_date) == expected_as_of + generated_at = dt.datetime.combine(run_date, dt.time(12, 30), tzinfo=dt.UTC) + bounds = report_time_bounds(expected_as_of, generated_at) + assert bounds.reference_time == dt.datetime.combine( + expected_as_of + dt.timedelta(days=1), dt.time.min, tzinfo=dt.UTC + ) + assert bounds.reference_time <= generated_at + + +def test_sunday_weekly_cutoff_accepts_saturday_context_without_rewriting_its_time() -> None: + context = { + "as_of": "2026-10-02", + "generated_at": "2026-10-03T11:45:00Z", + "expires_at": "2026-10-09T23:59:59Z", + } + for run_date, expected_reason in ( + (dt.date(2026, 10, 3), "as_of_in_future"), + (dt.date(2026, 10, 4), "fresh"), + ): + as_of = default_weekly_as_of(run_date) + bounds = report_time_bounds( + as_of, dt.datetime.combine(run_date, dt.time(13), tzinfo=dt.UTC) + ) + freshness = assess_context_freshness( + context, + report_as_of=as_of, + reference_time=bounds.reference_time, + report_generated_at=bounds.generated_at, + max_age_days=7, + ) + assert freshness.reason == expected_reason + assert freshness.valid is (expected_reason == "fresh") + + assert context["generated_at"] == "2026-10-03T11:45:00Z" + + def test_build_advisory_artifacts_builds_market_report_and_site(tmp_path: Path) -> None: result = build_advisory_artifacts( as_of=dt.date(2026, 5, 31), diff --git a/tests/test_workflow_defaults.py b/tests/test_workflow_defaults.py index 9d219c4..ed06d9e 100644 --- a/tests/test_workflow_defaults.py +++ b/tests/test_workflow_defaults.py @@ -1,11 +1,29 @@ from __future__ import annotations from pathlib import Path +import re + +import pytest ROOT = Path(__file__).resolve().parents[1] +@pytest.mark.parametrize( + ("workflow", "expected_cron"), + [ + ("weekly_advisory_review.yml", "30 12 * * 0"), + ("publish_advisory_site.yml", "0 13 * * 0"), + ], +) +def test_weekly_workflows_run_after_saturday_closes_without_changing_utc_time( + workflow: str, expected_cron: str +) -> None: + text = (ROOT / ".github" / "workflows" / workflow).read_text(encoding="utf-8") + assert re.findall(r'^\s+- cron: "([^"]+)"$', text, flags=re.MULTILINE) == [expected_cron] + assert "default_weekly_as_of" in text + + def test_public_workflows_default_to_live_event_inputs() -> None: for workflow in ("weekly_advisory_review.yml", "publish_advisory_site.yml", "monthly_advisory_review.yml"): text = (ROOT / ".github" / "workflows" / workflow).read_text(encoding="utf-8")