From 834ab22198b377cea91abf8a8b20f7e08ecf2a55 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:49:24 +0800 Subject: [PATCH 1/3] fix: Preserve execution evidence after heartbeat failures Co-Authored-By: Codex --- .../workflows/execution-report-heartbeat.yml | 1 + docs/runtime-daily-heartbeat-sync.zh-CN.md | 5 +++++ tests/test_execution_evidence_failure_path.py | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 tests/test_execution_evidence_failure_path.py diff --git a/.github/workflows/execution-report-heartbeat.yml b/.github/workflows/execution-report-heartbeat.yml index 9212d561..7fd098be 100644 --- a/.github/workflows/execution-report-heartbeat.yml +++ b/.github/workflows/execution-report-heartbeat.yml @@ -126,6 +126,7 @@ jobs: uv run --no-sync python scripts/with_heartbeat_digest_binding.py candidates - name: Publish read-only runtime execution evidence + if: ${{ !cancelled() && steps.install_runtime_deps.outcome == 'success' && (steps.gcp_auth_primary.outcome == 'success' || steps.gcp_auth_retry.outcome == 'success') }} uses: QuantStrategyLab/QuantRuntimeSettings/actions/publish-runtime-execution-evidence@c97752202670fac58db797c01596d11dd105a94b with: source-id: runtime-reports-schwab diff --git a/docs/runtime-daily-heartbeat-sync.zh-CN.md b/docs/runtime-daily-heartbeat-sync.zh-CN.md index 62f02412..9509097b 100644 --- a/docs/runtime-daily-heartbeat-sync.zh-CN.md +++ b/docs/runtime-daily-heartbeat-sync.zh-CN.md @@ -9,6 +9,11 @@ daily publisher step,在既有 22:20 UTC 调度内复用同一 WIF/uv 流程 或证据同步失败仍可记录日报;认证、依赖安装失败或运行取消不发布。日报的失败 不会跳过已经执行的原健康通知,原步骤顺序与行为不变。 +`Publish read-only runtime execution evidence` 同样独立于健康检查和候选发布的 +成功状态:只要运行未取消、依赖安装成功且本次认证成功,就继续导出已有执行证据。 +因此缺少可接受回执或候选冲突不会额外造成管理站的执行证据缺口。原检查仍保留失败 +结论;导出步骤继续使用原有 action、来源和步骤级凭据,不调用策略或券商。 + GitHub默认条件语义:[官方表达式说明](https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#status-check-functions)。 ## 启用 diff --git a/tests/test_execution_evidence_failure_path.py b/tests/test_execution_evidence_failure_path.py new file mode 100644 index 00000000..9e690e07 --- /dev/null +++ b/tests/test_execution_evidence_failure_path.py @@ -0,0 +1,20 @@ +"""Evidence export remains independent of failed heartbeat/candidate checks.""" +from test_runtime_daily_heartbeat_workflow import ( + _condition, _evaluate, _step_block, _workflow_text, +) + + +def test_evidence_export_after_attention_uses_authenticated_installed_reader(): + step = _step_block(_workflow_text(), "Publish read-only runtime execution evidence") + condition = _condition(step) + # A status function overrides implicit success(), including failed candidate publication. + assert "!cancelled()" in condition + assert _evaluate(condition, "", "success", "skipped") + assert _evaluate(condition, "", "failure", "success") + for primary, retry in (("failure", "failure"), ("failure", "skipped")): + assert not _evaluate(condition, "", primary, retry) + for installed in ("failure", "skipped", "cancelled"): + assert not _evaluate(condition, "", "success", "skipped", installed=installed) + assert not _evaluate(condition, "", "success", "skipped", cancelled=True) + assert "actions/publish-runtime-execution-evidence@" in step + assert "EXECUTION_EVIDENCE_SYNC_TOKEN:" in step From 13d27bdfd919dc54efa98b25a624ca6cdf1ae54d Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:57:03 +0800 Subject: [PATCH 2/3] feat: unify digest candidate confirmation warnings Co-Authored-By: Codex --- docs/unified-daily-notification.md | 15 ++++ .../digest_snapshot/presentation-source.json | 6 ++ notifications/digest_snapshot/presentation.py | 80 +++++++++++++++++++ scripts/digest_coverage_warning.py | 34 +++++++- tests/test_digest_coverage_warning.py | 31 +++++++ 5 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 notifications/digest_snapshot/presentation-source.json create mode 100644 notifications/digest_snapshot/presentation.py diff --git a/docs/unified-daily-notification.md b/docs/unified-daily-notification.md index 585cc830..fa87052d 100644 --- a/docs/unified-daily-notification.md +++ b/docs/unified-daily-notification.md @@ -276,3 +276,18 @@ means the plugin observation completed with no routine signals. These states remain distinct in canonical comparison. Existing contradictory candidates are not rewritten or normalized away. Source identity, factual conflict rejection and generation-conditional writes continue to apply on replay and restart. + +## Candidate warning display + +The existing candidate-persistence warning uses the shared five sections: +Result, Scope, Evidence, Coverage and Next step. Its QPK presentation snapshot +has a separate byte/source manifest under `notifications/digest_snapshot`; +other snapshots and runtime dependency pins are unchanged. + +The warning states that inclusion is unconfirmed, with unknown orders/fills and +an unknown business window when none was supplied. A run link uses only valid +current Actions context; absent context remains unavailable. It does not copy +private storage paths or raw exceptions. Existing attempt-marker identity, +atomic publication, sender callback, strict boolean acknowledgement and +failure behavior are unchanged. This copy change does not resolve an actual +candidate conflict or prove central delivery. diff --git a/notifications/digest_snapshot/presentation-source.json b/notifications/digest_snapshot/presentation-source.json new file mode 100644 index 00000000..654b7be9 --- /dev/null +++ b/notifications/digest_snapshot/presentation-source.json @@ -0,0 +1,6 @@ +{ + "repository": "QuantStrategyLab/QuantPlatformKit", + "commit": "fb8e687fb9a5d61694d5ad21591c52247bf8dfa1", + "path": "src/quant_platform_kit/notifications/presentation.py", + "sha256": "e3cfdadb0c138daa2e174ae1a94f43a81abacdd70b2c66b502443da4bc08e999" +} diff --git a/notifications/digest_snapshot/presentation.py b/notifications/digest_snapshot/presentation.py new file mode 100644 index 00000000..d1aac250 --- /dev/null +++ b/notifications/digest_snapshot/presentation.py @@ -0,0 +1,80 @@ +"""Shared display sections; no execution, routing, receipt or transport policy.""" +from __future__ import annotations + +from collections.abc import Sequence +from urllib.parse import urlsplit, urlunsplit + +SECTION_NAMES = { + "zh": ("结果", "范围", "证据", "覆盖", "下一步"), + "en": ("Result", "Scope", "Evidence", "Coverage", "Next step"), +} + + +def safe_run_url(value: object) -> str | None: + """Use only caller-supplied HTTPS links, dropping query and fragment.""" + if not isinstance(value, str) or any(c.isspace() or ord(c) < 32 for c in value): + return None + try: + parsed = urlsplit(value) + if parsed.scheme == "https" and parsed.hostname and not parsed.username and not parsed.password: + return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", "")) + except ValueError: + pass + return None + + +def next_step_text(locale: str, *, needs_review: bool) -> str: + if locale == "zh": + return ("核对该次运行、回执和缺失来源;保持现有执行限制。" if needs_review else + "无需操作;等待下一统计窗口。") + return ("Review the source run, receipt and missing sources; retain existing execution restrictions." + if needs_review else "No action required; await the next reporting window.") + + +def render_operator_message(*, title: str, locale: str, result: Sequence[str], + scope: Sequence[str], evidence: Sequence[str], coverage: Sequence[str], + next_step: Sequence[str], max_units: int | None = None, + required_evidence: Sequence[str] = ()) -> str: + """Keep result, scope, coverage and next step before optional evidence. + + Truncation is explicit and cannot remove a section or imply full coverage. + Callers put urgent evidence before optional observations. Financial facts + must already have been validated by the producer; this helper infers none. + """ + names = SECTION_NAMES["zh" if locale == "zh" else "en"] + unknown = "未知" if locale == "zh" else "Unknown" + groups = [[str(line) for line in group if line] or [unknown] + for group in (result, scope, evidence, coverage, next_step)] + + def assemble(): + lines = [title] + for name, group in zip(names, groups): + lines.extend([f"【{name}】" if locale == "zh" else f"[{name}]", *group]) + return "\n".join(lines) + + if max_units is None: + return assemble() + if max_units < 256: + raise ValueError("notification_budget_too_small") + omitted = 0 + original = [line for line in groups[2] if line not in required_evidence] + required = list(dict.fromkeys(required_evidence)) + groups[2] = required.copy() + suffix = "部分证据已省略;请查看运行记录。" if locale == "zh" else "Some evidence omitted; inspect the source run." + groups[2] = required + [suffix] + if len(assemble().encode("utf-16-le")) // 2 > max_units: + raise ValueError("notification_required_fields_exceed_budget") + groups[2] = required.copy() + for line in original: + groups[2].append(line) + groups[2].append(suffix) + fits = len(assemble().encode("utf-16-le")) // 2 <= max_units + groups[2].pop() + if not fits: + groups[2].pop() + omitted += 1 + if omitted: + groups[2].append(suffix) + if not groups[2]: + groups[2] = [unknown] + return assemble() diff --git a/scripts/digest_coverage_warning.py b/scripts/digest_coverage_warning.py index 890ebb82..c3b93a43 100644 --- a/scripts/digest_coverage_warning.py +++ b/scripts/digest_coverage_warning.py @@ -4,6 +4,7 @@ import hashlib import json import os +import re import sys import tempfile @@ -47,12 +48,37 @@ def warn_coverage_unconfirmed(send, locale): pass elif os.environ.get("GITHUB_ACTIONS") == "true": print("digest_coverage_warning_marker_unavailable", file=sys.stderr) - message = ("⚠️ Schwab 日报候选记录未确认,日报可能缺少本次记录;请检查运行记录和摘要存储。" - if str(locale).lower().startswith("zh") else - "⚠️ Schwab daily digest candidate record is unconfirmed; the digest may omit this record. " - "Check the run and digest storage.") + message = _coverage_message(locale) try: return send(message) is True except Exception: # noqa: BLE001 -- The attempt remains recorded when delivery is uncertain. print("digest_coverage_warning_send_unconfirmed", file=sys.stderr) return False + + +def _coverage_message(locale): + """Display only; preserve the existing attempted-send marker and callback.""" + from notifications.digest_snapshot.presentation import next_step_text, render_operator_message + + lang = "zh" if str(locale).lower().startswith("zh") else "en" + zh = lang == "zh" + link = None + # Only the active Actions run is a valid source for this warning's run link. + repository = os.environ.get("GITHUB_REPOSITORY", "") + run_id = os.environ.get("GITHUB_RUN_ID", "") + if (os.environ.get("GITHUB_ACTIONS") == "true" + and os.environ.get("GITHUB_SERVER_URL") == "https://github.com" + and re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository) + and re.fullmatch(r"[0-9]{1,20}", run_id)): + link = f"https://github.com/{repository}/actions/runs/{run_id}" + return render_operator_message( + title="⚠️ Schwab 日报候选确认" if zh else "⚠️ Schwab digest candidate confirmation", + locale=lang, + result=["日报候选记录未确认" if zh else "Daily digest candidate record is unconfirmed"], + scope=["本次候选保存尝试;统计窗口未知" if zh else "This candidate persistence attempt; business window unknown"], + evidence=["reason_code: candidate_publication_unconfirmed", + ("运行记录:" if zh else "Run record: ") + (link or ("未提供运行链接" if zh else "run link not provided"))], + coverage=["日报可能缺少本次记录;订单:未知;成交:未知" if zh else + "The digest may omit this record; orders: unknown; fills: unknown"], + next_step=[next_step_text(lang, needs_review=True)], + ) diff --git a/tests/test_digest_coverage_warning.py b/tests/test_digest_coverage_warning.py index 434aaf02..3812de1c 100644 --- a/tests/test_digest_coverage_warning.py +++ b/tests/test_digest_coverage_warning.py @@ -179,6 +179,37 @@ def test_languages_are_bounded_and_private(self): self.assertNotIn('private exception', sent[0]) self.assertNotIn('http', sent[0].lower()) + def test_unified_sections_and_unknown_counts_do_not_change_send_ack(self): + for locale in ("zh", "en"): + with self.subTest(locale=locale), patch.dict(os.environ, {}, clear=True): + sent = [] + self.assertFalse(warn_coverage_unconfirmed(lambda text: sent.append(text), locale)) + self.assertEqual(len(sent), 1) + sections = ("结果", "范围", "证据", "覆盖", "下一步") if locale == "zh" else ("Result", "Scope", "Evidence", "Coverage", "Next step") + for section in sections: + self.assertIn(f"【{section}】" if locale == "zh" else f"[{section}]", sent[0]) + self.assertIn("成交:未知" if locale == "zh" else "fills: unknown", sent[0]) + self.assertIn("candidate_publication_unconfirmed", sent[0]) + self.assertNotIn("成交:0", sent[0]) + + def test_run_link_requires_valid_current_actions_context(self): + environment = dict(GITHUB_ACTIONS="true", GITHUB_SERVER_URL="https://github.com", + GITHUB_REPOSITORY="example/synthetic", GITHUB_RUN_ID="123") + for run_id in ("123", "123?private=hidden"): + with self.subTest(valid=run_id == "123"), patch.dict(os.environ, {**environment, "GITHUB_RUN_ID": run_id}, clear=True): + sent = [] + self.assertTrue(warn_coverage_unconfirmed(lambda text: sent.append(text) or True, "en")) + self.assertEqual("https://github.com/example/synthetic/actions/runs/123" in sent[0], run_id == "123") + self.assertNotIn("private=hidden", sent[0]) + + def test_presentation_snapshot_matches_recorded_source_bytes(self): + import hashlib + import json + root = ROOT / "notifications/digest_snapshot" + manifest = json.loads((root / "presentation-source.json").read_text()) + self.assertRegex(manifest["commit"], r"^[a-f0-9]{40}$") + self.assertEqual(hashlib.sha256((root / "presentation.py").read_bytes()).hexdigest(), manifest["sha256"]) + def test_workflow_uses_same_job_path_in_both_steps(self): workflow = (ROOT / '.github/workflows/execution-report-heartbeat.yml').read_text() marker = ('DIGEST_COVERAGE_ALERT_ATTEMPT_PATH: ${{ runner.temp }}/schwab-digest-coverage-' From 208488fab3751ba995784eda2d043c9bfe2e79fe Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:04:12 +0800 Subject: [PATCH 3/3] test: verify candidate warning facts across unified sections Co-Authored-By: Codex --- tests/test_daily_notification_contract.py | 10 ++++++++-- tests/test_unified_notification_entrypoints.py | 6 +++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/test_daily_notification_contract.py b/tests/test_daily_notification_contract.py index b2433773..da400a42 100644 --- a/tests/test_daily_notification_contract.py +++ b/tests/test_daily_notification_contract.py @@ -271,8 +271,14 @@ def test_healthy_heartbeat_coverage_failure_is_urgent_and_private(locale, monkey source_reports=[{"execution_receipt": {"synthetic": True}}]) assert result == 1 assert len(sent) == 1 - assert ("Schwab 日报候选记录未确认,日报可能缺少本次记录" if locale == "zh" else - "Schwab daily digest candidate record is unconfirmed; the digest may omit this record") in sent[0] + assert "Schwab" in sent[0] + assert ("日报候选记录未确认" if locale == "zh" else + "Daily digest candidate record is unconfirmed") in sent[0] + assert ("日报可能缺少本次记录" if locale == "zh" else "The digest may omit this record") in sent[0] + assert ("成交:未知" if locale == "zh" else "fills: unknown") in sent[0] + for section in (("结果", "范围", "证据", "覆盖", "下一步") if locale == "zh" else + ("Result", "Scope", "Evidence", "Coverage", "Next step")): + assert (f"【{section}】" if locale == "zh" else f"[{section}]") in sent[0] assert "private order detail" not in sent[0] diff --git a/tests/test_unified_notification_entrypoints.py b/tests/test_unified_notification_entrypoints.py index 9bfa3abd..d7a5fe8f 100644 --- a/tests/test_unified_notification_entrypoints.py +++ b/tests/test_unified_notification_entrypoints.py @@ -105,7 +105,11 @@ def test_heartbeat_failed_producer_sends_coverage_error_and_reports_unknown(monk monkeypatch.setattr(unified, "emit_completed_candidates", lambda env, **kwargs: {"uploaded": False}) assert heartbeat.main(NOW) == 1 assert len(sent) == 1 - assert "Schwab 日报候选记录未确认,日报可能缺少本次记录" in sent[0] + assert "Schwab" in sent[0] + assert "日报候选记录未确认" in sent[0] + assert "日报可能缺少本次记录" in sent[0] + assert "成交:未知" in sent[0] + assert "【覆盖】" in sent[0] and "【下一步】" in sent[0] assert "coverage=unknown" in capsys.readouterr().out