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
2 changes: 1 addition & 1 deletion .github/workflows/publish_advisory_site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ on:
default: ""
type: string
schedule:
- cron: "0 13 * * 6"
- cron: "0 13 * * 0"

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/weekly_advisory_review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ on:
default: ""
type: string
schedule:
- cron: "30 12 * * 6"
- cron: "30 12 * * 0"

permissions:
contents: read
Expand Down
33 changes: 33 additions & 0 deletions docs/data_factor_roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions docs/data_factor_roadmap.zh-CN.md
Original file line number Diff line number Diff line change
@@ -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)

## 当前结论
Expand Down
45 changes: 45 additions & 0 deletions docs/system_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions docs/system_design.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 现有仓库已经天然分层:
Expand Down
51 changes: 51 additions & 0 deletions tests/test_build_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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),
Expand Down
18 changes: 18 additions & 0 deletions tests/test_workflow_defaults.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
Loading