From e999d31b25e0878abc03ee56688ea283cd397798 Mon Sep 17 00:00:00 2001
From: phamnazage-jpg
Date: Fri, 29 May 2026 18:48:48 +0800
Subject: [PATCH] fix: harden review and verifier governance
---
.gitignore | 1 +
OPENCLAW_EXECUTION.md | 11 +
docs/PRODUCTION_CHECKLIST.md | 11 +
healthcheck.sh | 7 +-
.../openclaw/OPENCLAW_CAPABILITY_BACKLOG.md | 235 ++++++++++++++----
scripts/apply_script_test_tags.py | 69 +++++
scripts/cloudflare_pricing_signature_guard.go | 2 +-
scripts/collector_stats_window_audit.sh | 38 ++-
scripts/collector_stats_window_audit_test.sh | 40 ++-
scripts/cron_status_report.sh | 68 +++++
scripts/cron_status_report_test.sh | 32 +++
scripts/deepseek_news_signature_guard.go | 2 +-
scripts/deepseek_pricing_signature_guard.go | 2 +-
scripts/discover_intraday_news_candidates.go | 2 +-
scripts/export_official_seed_json.go | 2 +-
scripts/fetch_multi_source.go | 2 +-
scripts/fetch_openrouter.go | 2 +-
scripts/fetch_tencent_catalog.go | 2 +-
scripts/generate_daily_report.go | 84 +++++--
scripts/generate_daily_report_test.go | 38 +++
scripts/generate_video_digest.go | 2 +-
scripts/git_commit_status_report.sh | 21 ++
scripts/git_commit_status_test.sh | 36 +++
scripts/import_360_pricing.go | 2 +-
scripts/import_aliyun_subscription.go | 2 +-
scripts/import_azure_openai_pricing.go | 2 +-
scripts/import_baichuan_pricing.go | 2 +-
scripts/import_baidu_subscription.go | 2 +-
scripts/import_bedrock_pricing.go | 2 +-
scripts/import_bytedance_data.go | 2 +-
scripts/import_bytedance_pricing.go | 2 +-
scripts/import_bytedance_subscription.go | 2 +-
scripts/import_catalog_seed_verification.go | 2 +-
scripts/import_cloudflare_pricing.go | 2 +-
scripts/import_coreshub_pricing.go | 2 +-
scripts/import_ctyun_subscription.go | 2 +-
scripts/import_cucloud_catalog.go | 2 +-
scripts/import_cucloud_pricing.go | 2 +-
scripts/import_huawei_maas_pricing.go | 2 +-
scripts/import_huawei_package.go | 2 +-
scripts/import_hunyuan_pricing.go | 2 +-
scripts/import_lingyiwanwu_pricing.go | 2 +-
scripts/import_manual_subscription_seed.go | 2 +-
scripts/import_minimax_subscription.go | 2 +-
scripts/import_mobile_cloud_catalog.go | 2 +-
scripts/import_mobile_cloud_pricing.go | 2 +-
scripts/import_perplexity_pricing.go | 2 +-
scripts/import_phase2_data.go | 2 +-
scripts/import_plan_catalog.go | 2 +-
scripts/import_ppio_pricing.go | 2 +-
scripts/import_qwen_pricing.go | 2 +-
scripts/import_sensenova_pricing.go | 35 ++-
scripts/import_sensenova_pricing_test.go | 33 +++
scripts/import_siliconflow_pricing.go | 2 +-
scripts/import_tencent_subscription.go | 2 +-
scripts/import_ucloud_pricing.go | 2 +-
scripts/import_vertex_pricing.go | 2 +-
scripts/import_xfyun_pricing.go | 44 +++-
scripts/import_xfyun_pricing_test.go | 20 ++
scripts/import_youdao_pricing.go | 2 +-
scripts/import_zhipu_coding_plan.go | 2 +-
scripts/import_zhipu_data.go | 2 +-
scripts/list_testable_script_entries.sh | 29 +++
scripts/live_pricing_smoke_runner.go | 2 +-
scripts/materialize_daily_signals.go | 2 +-
scripts/package_stub.go | 5 +
scripts/perplexity_pricing_signature_guard.go | 2 +-
...ne_script_includes_signature_query_test.sh | 22 ++
.../query_official_import_signature_audit.go | 2 +-
scripts/report_state_tracking_test.sh | 3 +-
scripts/report_utils.sh | 36 ++-
.../review/backlog_blocker_freshness_guard.sh | 37 +++
.../backlog_blocker_freshness_guard_test.sh | 41 +++
.../review/backlog_current_freshness_guard.sh | 25 ++
.../backlog_current_freshness_guard_test.sh | 36 +++
scripts/review/backlog_current_table_guard.sh | 28 +++
.../backlog_current_table_guard_test.sh | 38 +++
scripts/review/backlog_revocation_guard.sh | 31 +++
.../review/backlog_revocation_guard_test.sh | 40 +++
scripts/review/blocker_switch_guard.sh | 14 ++
scripts/review/blocker_switch_guard_test.sh | 31 +++
.../review/current_row_revocation_guard.sh | 12 +
.../global_blocker_switch_capture_test.sh | 12 +
scripts/review/global_blocker_switch_guard.sh | 20 ++
.../global_blocker_switch_guard_test.sh | 33 +++
.../review/live_run_classification_test.sh | 23 ++
scripts/review/partial_output_guard.sh | 12 +
scripts/review/partial_output_guard_test.sh | 30 +++
scripts/review/provider_root_cause_test.sh | 20 ++
.../review/release_semantics_capture_test.sh | 12 +
scripts/review/release_semantics_guard.sh | 11 +
.../review/release_semantics_guard_test.sh | 29 +++
scripts/review/review_action_guard.sh | 35 +++
scripts/review/review_action_guard_test.sh | 47 ++++
scripts/review/review_aging_priority_test.sh | 20 ++
.../review_same_day_no_decision_test.sh | 22 ++
scripts/review/review_status_summary.sh | 69 +++++
scripts/review/review_status_summary_test.sh | 22 ++
scripts/review/review_truth_guard.sh | 24 ++
scripts/review/review_truth_guard_test.sh | 62 +++++
.../review/review_worktree_status_guard.sh | 14 ++
.../review_worktree_status_guard_test.sh | 23 ++
scripts/review/root_cause_summary_guard.sh | 11 +
scripts/review/root_cause_summary_test.sh | 34 +++
scripts/review/stability_status_guard.sh | 14 ++
scripts/review/stability_status_guard_test.sh | 28 +++
.../review/subagent_workspace_guard_test.sh | 29 +++
scripts/run_daily.sh | 42 +++-
scripts/run_intel_pipeline.sh | 2 +-
scripts/run_intraday_discovery_watch.sh | 2 +-
scripts/run_intraday_price_watch.sh | 2 +-
scripts/run_real_pipeline.sh | 4 +-
scripts/script_entry_inventory_test.sh | 12 +
scripts/scripts_conflict_detection_test.sh | 23 ++
scripts/scripts_package_compile_test.sh | 7 +
scripts/scripts_package_structure_test.go | 44 ++++
scripts/secret_gate_coverage_test.sh | 23 ++
scripts/verification_executor.go | 60 ++++-
scripts/verification_executor_exit_test.sh | 45 ++++
scripts/verification_executor_test.go | 132 +++++++++-
scripts/verify_build_coverage_test.sh | 22 ++
scripts/verify_intraday_news_candidates.go | 2 +-
scripts/verify_phase3.sh | 17 +-
...erify_phase3_official_report_paths_test.sh | 21 ++
scripts/verify_phase4.sh | 2 +-
scripts/verify_phase4_echarts_gate_test.sh | 12 +
scripts/verify_phase5.sh | 3 +-
scripts/verify_phase6.sh | 130 +++++++++-
scripts/verify_phase6_behavior_test.sh | 51 ++++
.../verify_phase6_release_semantics_test.sh | 15 ++
scripts/verify_phase6plus_scope_test.sh | 11 +
scripts/vertex_pricing_signature_guard.go | 2 +-
scripts/xfyun_render_flags_test.sh | 8 +
133 files changed, 2538 insertions(+), 159 deletions(-)
create mode 100644 scripts/apply_script_test_tags.py
create mode 100755 scripts/cron_status_report.sh
create mode 100644 scripts/cron_status_report_test.sh
create mode 100755 scripts/git_commit_status_report.sh
create mode 100644 scripts/git_commit_status_test.sh
create mode 100755 scripts/list_testable_script_entries.sh
create mode 100644 scripts/package_stub.go
create mode 100644 scripts/pipeline_script_includes_signature_query_test.sh
create mode 100755 scripts/review/backlog_blocker_freshness_guard.sh
create mode 100644 scripts/review/backlog_blocker_freshness_guard_test.sh
create mode 100755 scripts/review/backlog_current_freshness_guard.sh
create mode 100644 scripts/review/backlog_current_freshness_guard_test.sh
create mode 100755 scripts/review/backlog_current_table_guard.sh
create mode 100644 scripts/review/backlog_current_table_guard_test.sh
create mode 100755 scripts/review/backlog_revocation_guard.sh
create mode 100644 scripts/review/backlog_revocation_guard_test.sh
create mode 100755 scripts/review/blocker_switch_guard.sh
create mode 100644 scripts/review/blocker_switch_guard_test.sh
create mode 100755 scripts/review/current_row_revocation_guard.sh
create mode 100644 scripts/review/global_blocker_switch_capture_test.sh
create mode 100755 scripts/review/global_blocker_switch_guard.sh
create mode 100644 scripts/review/global_blocker_switch_guard_test.sh
create mode 100644 scripts/review/live_run_classification_test.sh
create mode 100755 scripts/review/partial_output_guard.sh
create mode 100644 scripts/review/partial_output_guard_test.sh
create mode 100644 scripts/review/provider_root_cause_test.sh
create mode 100644 scripts/review/release_semantics_capture_test.sh
create mode 100755 scripts/review/release_semantics_guard.sh
create mode 100644 scripts/review/release_semantics_guard_test.sh
create mode 100755 scripts/review/review_action_guard.sh
create mode 100644 scripts/review/review_action_guard_test.sh
create mode 100644 scripts/review/review_aging_priority_test.sh
create mode 100644 scripts/review/review_same_day_no_decision_test.sh
create mode 100755 scripts/review/review_status_summary.sh
create mode 100644 scripts/review/review_status_summary_test.sh
create mode 100755 scripts/review/review_truth_guard.sh
create mode 100644 scripts/review/review_truth_guard_test.sh
create mode 100755 scripts/review/review_worktree_status_guard.sh
create mode 100644 scripts/review/review_worktree_status_guard_test.sh
create mode 100755 scripts/review/root_cause_summary_guard.sh
create mode 100644 scripts/review/root_cause_summary_test.sh
create mode 100755 scripts/review/stability_status_guard.sh
create mode 100644 scripts/review/stability_status_guard_test.sh
create mode 100644 scripts/review/subagent_workspace_guard_test.sh
create mode 100644 scripts/script_entry_inventory_test.sh
create mode 100644 scripts/scripts_conflict_detection_test.sh
create mode 100644 scripts/scripts_package_compile_test.sh
create mode 100644 scripts/scripts_package_structure_test.go
create mode 100644 scripts/secret_gate_coverage_test.sh
create mode 100644 scripts/verification_executor_exit_test.sh
create mode 100644 scripts/verify_build_coverage_test.sh
create mode 100644 scripts/verify_phase3_official_report_paths_test.sh
create mode 100644 scripts/verify_phase4_echarts_gate_test.sh
create mode 100644 scripts/verify_phase6_behavior_test.sh
create mode 100644 scripts/verify_phase6_release_semantics_test.sh
create mode 100644 scripts/verify_phase6plus_scope_test.sh
create mode 100644 scripts/xfyun_render_flags_test.sh
diff --git a/.gitignore b/.gitignore
index 360fa48..5012b5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ frontend/src/data/latest_models.json
reports/daily/
reports/verification/
reports/daily/video/
+reports/ad_hoc/
reports/openclaw/20*.md
!reports/openclaw/2026-05-20-2106-review.md
!reports/openclaw/2026-05-13-1510-review.md
diff --git a/OPENCLAW_EXECUTION.md b/OPENCLAW_EXECUTION.md
index 2f7ee80..7fa7d0a 100644
--- a/OPENCLAW_EXECUTION.md
+++ b/OPENCLAW_EXECUTION.md
@@ -77,6 +77,17 @@
2. 继续观察 Cloudflare / Perplexity / Vertex 等外部文档源的稳定性;当前 Cloudflare 已补上“代理传输失败 → 直连 fallback”兜底,但仍需区分瞬时网络抖动与真实结构漂移
3. 维持正式日报、历史重建与手工真实复跑三条运行语义边界,防止后续优化重新串线
+### Phase 6+ 范围定义
+
+Phase 6 的结束点是:`verify_phase6.sh`、`verify_pre_phase6.sh`、正式日报主链路与 API 健康门禁已经恢复绿色,主发布闭环可以诚实复用。
+
+Phase 6+ 指的是 **治理阶段**,不属于新的发布门禁,也不等于新的业务功能 phase。它覆盖的范围是:
+- review / cron / verifier / backlog / memory 的长期治理
+- release 语义、风险老化、状态一致性、噪声收敛
+- 外部 provider 漂移后的解释层、回退层与 guard 持续补强
+- 正式日报 / 历史重建 / 手工真实复跑三条运行语义的边界维护
+
+Phase 6+ 的目标不是再声明“可发布”,而是防止已经恢复绿色的主链路因为治理退化再次失真。
### 当前运行真相
当前可直接引用的事实是:
diff --git a/docs/PRODUCTION_CHECKLIST.md b/docs/PRODUCTION_CHECKLIST.md
index 863d17f..0623acc 100644
--- a/docs/PRODUCTION_CHECKLIST.md
+++ b/docs/PRODUCTION_CHECKLIST.md
@@ -90,6 +90,17 @@ bash healthcheck.sh
- 最近 7 次采集成功率 `>= 95%`
- 前端测试入口存在
+
+## Phase 6+ 范围定义
+
+Phase 6 仍是发布前主门禁;`verify_phase6.sh` 通过即可证明主链路验收闭环成立。
+
+Phase 6+ 属于 **治理阶段**,不属于发布门禁本身。它覆盖:
+- review / cron / verifier / backlog / memory 的长期治理
+- release 解释语义、风险老化、状态一致性与噪声收敛
+- 外部 provider 漂移后的 fallback / guard / summary 持续补强
+
+因此,Phase 6+ 项目未关闭时,不能反推 Phase 6 主验收失败;反之,Phase 6 已通过,也不代表 Phase 6+ 治理工作已经完成。
## 上线步骤
### 1. 发布前备份
diff --git a/healthcheck.sh b/healthcheck.sh
index 0286571..299bb3f 100755
--- a/healthcheck.sh
+++ b/healthcheck.sh
@@ -3,6 +3,7 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"
+source scripts/report_utils.sh
if [[ -f ".env.local" ]]; then
# shellcheck disable=SC1091
@@ -14,8 +15,8 @@ if [[ -f ".env" ]]; then
fi
DB_URL="${DATABASE_URL:-host=/var/run/postgresql dbname=llm_intelligence user=long sslmode=disable}"
-TODAY="$(date +%Y-%m-%d)"
-REPORT_PATH="reports/daily/daily_report_${TODAY}.md"
+TODAY="$(report_date_value)"
+REPORT_PATH="$(report_markdown_path "$TODAY")"
psql "$DB_URL" -Atqc "select 1;" >/dev/null
@@ -24,7 +25,7 @@ if [[ -f "$REPORT_PATH" ]]; then
exit 0
fi
-LATEST_REPORT="$(find reports/daily -maxdepth 1 -type f -name 'daily_report_*.md' | sort | tail -n 1)"
+LATEST_REPORT="$(find "$(report_output_dir)" -maxdepth 1 -type f -name 'daily_report_*.md' | sort | tail -n 1)"
if [[ -n "$LATEST_REPORT" ]]; then
echo "healthcheck: ok (db=up latest_report=$LATEST_REPORT)"
exit 0
diff --git a/reports/openclaw/OPENCLAW_CAPABILITY_BACKLOG.md b/reports/openclaw/OPENCLAW_CAPABILITY_BACKLOG.md
index 344a81b..8fdb869 100644
--- a/reports/openclaw/OPENCLAW_CAPABILITY_BACKLOG.md
+++ b/reports/openclaw/OPENCLAW_CAPABILITY_BACKLOG.md
@@ -10,60 +10,117 @@
---
-## 当前未修复问题速查表(截至 2026-05-27 15:10)
+## 当前未修复问题速查表(截至 2026-05-29 15:10)
| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
|---|------|--------|----------|----------|----------|
-| 1 | 验证器退出码设计 | P0 | 05-07 22:50 | ⚠️ 部分(`rg` 误报已消除,但三级状态仍未实现) | 11 次 |
-| 2 | session 历史工具/业务错误区分 | P1 | 05-07 22:50 | ❌ 未修复 | 12 次 |
-| 3 | cron 无主动状态报告机制 | P1 | 05-07 22:50 | ❌ 未修复 | 12 次 |
-| 4 | subagent spawn 未传递 workspace | P1 | 05-07 22:50 | ❌ 未修复 | 12 次 |
-| 5 | 验收脚本无法检测构建 | P1 | 05-08 09:05 | ❌ 未修复 | 11 次 |
-| 6 | 环境变量/API Key 缺失未自动检测 | P1 | 05-08 09:05 | ⚠️ 部分(脚本已有分类,但 review prompt 仍未强制把前置条件失败与代码失败分层叙述) | 12 次 |
-| 7 | 文件修改后未触发 commit 提示 | P2→P1 | 05-08 09:05 | ❌ 未修复 | 14 次 |
-| 8 | cron review 无 delta 时空转 | P1 | 05-08 09:12 | ❌ 未修复 | 13 次 |
-| 9 | 验证模式伪进展(artifact_present 局限) | P1 | 05-08 14:30 | ❌ 未修复 | 10 次 |
-| 10 | 项目提交停滞(commit stagnation) | P0 | 05-08 21:30 | ⚠️ 重新活跃(23 文件 +3650/-808 行核心组件改动未入版本控制,BACKLOG 本身也在未提交列表中) | 23 次 |
-| 11 | review 报告未触发修复动作 | P2→P1 | 05-08 21:30 | ❌ 未修复 | 10 次 |
-| 12 | BACKLOG 文件膨胀导致 review 成本递增 | P1 | 05-09 09:30 | ⚠️ 部分(已分层归档,但 current 表仍持续膨胀) | 8 次 |
-| 13 | untracked 核心代码未入版本控制 | P0 | 05-10 21:30 | ⚠️ 重新活跃(scripts/secret_gate_lib.sh/test.sh 为新增 untracked 项) | 14 次 |
-| 14 | Phase 6+ 范围未定义 | P1 | 05-10 21:30 | ❌ 未修复 | 6 次 |
-| 15 | review 误报传播 | P1 | 05-11 14:30 | ❌ 未修复 | 10 次 |
-| 16 | untracked 文件统计遗漏 | P1 | 05-11 14:30 | ⚠️ 部分(本轮已更精确核对 git status,但能力未固化) | 6 次 |
-| 17 | 验收脚本瞬时回归缺少稳定性标记 | P1 | 05-12 22:46 | ⚠️ 部分(已补充 recovered-external-incident 叙事与 Cloudflare 传输层 fallback,但 release 语义尚未系统化) | 6 次 |
-| 18 | 无 delta 场景缺少老化风险优先策略 | P2 | 05-12 22:46 | ❌ 未修复 | 7 次 |
-| 19 | 综合验收错误聚合误导根因判断 | P1 | 05-13 00:15 | ❌ 未修复 | 5 次 |
-| 20 | snapshot truth 与 current truth 漂移未被显式提示 | P1 | 05-14 09:31 | ❌ 未修复 | 6 次 |
-| 21 | Phase 6 稳定性门禁失败缺少样本窗口摘要 | P1 | 05-14 15:10 | ✅ 已修复(当前输出已含 window_size / success_rate / 样本明细) | 5 次 |
-| 22 | Phase 6 稳定性门禁未区分前置条件缺失 vs 真实采集失败 | P1 | 05-14 21:30 | ⚠️ 部分(脚本已输出分类,但 review 与 release 解释层仍不足) | 8 次 |
-| 23 | 脚本型 Go 仓库缺少可测试入口发现能力 | P1 | 05-15 15:11 | ⚠️ 部分(本轮已优先使用仓库声明入口,但仍依赖 reviewer 主动判断) | 4 次 |
-| 24 | 长命令部分回传时缺少保守结论模板 | P1 | 05-15 21:31 | ⚠️ 部分(本轮通过 process 拿到完整输出,但策略尚未固化) | 2 次 |
-| 25 | backlog current truth 老化未自动撤销 | P2 | 05-16 09:30 | ❌ 未修复 | 2 次 |
-| 26 | 外部 provider 失败与主链路失败聚合过粗 | P1 | 05-16 09:30 | ⚠️ 部分(Cloudflare 已加 transport fallback,但其他外部源仍缺统一分层) | 6 次 |
-| 27 | 稳定性窗口虽已分类但缺 release 级解释语义 | P1 | 05-16 09:30 | ⚠️ 部分(Cloudflare EOF 已定性为 recovered external incident,但 release 文案模板尚未系统化) | 7 次 |
-| 28 | 新增导入器缺少进入综合验收前的 smoke gate | P0 | 05-16 15:10 | ✅ 已缓解(`verify_importer_smoke.sh` 持续通过,本轮 importer smoke 全 PASS) | 4 次 |
-| 29 | 同日 review blocker 切换缺少自动老化提醒 | P1 | 05-16 15:10 | ❌ 未修复 | 2 次 |
-| 30 | 历史 precondition 样本持续老化拖低 release 成功率 | P1 | 05-17 09:31 | ❌ 未修复 | 6 次 |
-| 31 | 同日无主结论 delta 时缺少风险老化优先策略 | P2 | 05-17 15:10 | ❌ 未修复 | 3 次 |
-| 32 | 同日 blocker 切换后 backlog current truth 缺少 freshness 提示 | P1 | 05-17 21:30 | ❌ 未修复 | 2 次 |
-| 33 | 已证伪 blocker 缺少自动降级/撤销机制 | P1 | 05-18 09:30 | ❌ 未修复 | 2 次 |
-| 34 | 局部 smoke 已通过后缺少全局 blocker 切换提示 | P1 | 05-18 15:10 | ❌ 未修复 | 1 次 |
-| 35 | smoke gate 测试脚本老化未跟上 runtime truth | P1 | 05-19 09:32 | ✅ 已修复(`importer_smoke_gate_test.sh` 已与 runtime truth 对齐并持续通过) | 5 次 |
-| 36 | 稳定性窗口持续回落(85.71% → 71.43%) | P1 | 05-20 21:06 | ✅ 已恢复(窗口回到 100%,本轮 importer smoke 全 PASS) | 2 次 |
-| 37 | 外部文档站故障仍无系统化降级 | P1 | 05-16 09:30 | ❌ 未修复(live_run SUMMARY 缺失,无法确认当前 blocker 状态) | 6 次 |
-| 38 | PRE_PHASE6_RESULT 标签冲突(verify_phase4 FAIL 但标签仍 PASS) | P1 | 05-25 08:51 | ❌ 未修复(verify_phase4 ECharts 断言失败是唯一 FAIL 项,根因为断言与实现不匹配) | 4 次 |
-| 39 | 日报时间戳异常(generated_at 晚约 10 小时) | P2 | 05-25 08:51 | ❌ 未修复 | 3 次 |
-| 40 | BACKLOG 文件本身 uncommitted | P1 | 05-25 08:51 | ❌ 未修复(BACKLOG 本轮也在未提交列表中) | 4 次 |
-| 41 | verify_phase6.sh 连续超时导致 Phase 6 状态无法确认 | P1 | 05-25 09:06 | ⚠️ 部分(连续超时未复现,importer smoke 全 PASS;但 live_run SUMMARY 仍缺失,窗口状态不明) | 5 次 |
-| 42 | verify_phase6.sh 第三次连续超时 | P0 | 05-25 15:10 | ✅ 已修复(连续超时未在本轮复现,importer smoke 全 PASS) | — |
-| 43 | verify_phase4 ECharts 集成断言失败(历史遗留 P2) | P2 | 05-25 15:10 | ❌ 未修复(Dashboard.tsx 已引入 echarts 但 verify 断言与实现不匹配,导致 PRE_PHASE6 FAIL) | 2 次 |
-| 44 | 新增 scripts 无门禁覆盖(secret_gate_lib.sh / secret_gate_test.sh) | P2 | 05-26 15:10 | ❌ 未修复(新增文件为 untracked,无对应 verify 门禁验证其正确性) | 1 次 |
-| 45 | scripts 目录 go test build failure(redeclared main) | P1 | 05-27 15:10 | ❌ 未修复(多个脚本存在 main/ModelPricing/logger redeclared 冲突,导致 `go test ./scripts` 无法执行) | 1 次 |
+| 27 | 稳定性窗口虽已分类但缺 release 级解释语义 | P1 | 05-16 09:30 | ✅ 已修复(verify_phase6 已输出 RELEASE_SEMANTICS 摘要,稳定性窗口分类已升格为 release policy) | 7 次 |
----
+| 29 | 同日 review blocker 切换缺少自动老化提醒 | P1 | 05-16 15:10 | ✅ 已修复(已新增 same-day blocker switch freshness guard,review 不能再无提示传播旧 blocker) | 2 次 |
+
+
+| 30 | 历史 precondition 样本持续老化拖低 release 成功率 | P1 | 05-17 09:31 | ✅ 已修复(aged precondition 样本已从 active precondition 统计剔除,不再持续拖低当前 success_rate) | 6 次 |
+
+
+| 31 | 同日无主结论 delta 时缺少风险老化优先策略 | P2 | 05-17 15:10 | ✅ 已修复(review_status_summary 已新增 same_day_no_decision_focus,显式给出同日无主结论场景的优先风险) | 3 次 |
+
+
+| 32 | 同日 blocker 切换后 backlog current truth 缺少 freshness 提示 | P1 | 05-17 21:30 | ✅ 已修复(已新增 backlog blocker freshness guard,旧 blocker 不得在 same-day 切换后继续留在 current 表) | 2 次 |
+
+
+| 33 | 已证伪 blocker 缺少自动降级/撤销机制 | P1 | 05-18 09:30 | ✅ 已修复(已新增 backlog revocation guard,review 已宣告移除的问题不得继续留在 current 表) | 2 次 |
+
+
+| 34 | 局部 smoke 已通过后缺少全局 blocker 切换提示 | P1 | 05-18 15:10 | ✅ 已修复(phase6 已输出 BLOCKER_SWITCH 摘要,局部 smoke 恢复后全局 blocker 转移会被显式提示) | 1 次 |
+
+
+| 47 | 工作区严重污染(121 文件未 commit) | P0 | 05-29 15:10 | ❌ 未修复 | 首次暴露 |
+| 48 | xfyun-live smoke FAIL 导致 live_run SKIP 传导链 | P1 | 05-29 15:10 | ❌ 未修复 | 首次暴露 |
+| 49 | cron 两次运行失败未闭环 | P1 | 05-29 15:10 | ❌ 未修复 | 首次暴露 |
+| 50 | BACKLOG current table 退化(13 条已修复项占据 + freshness > 24h) | P2 | 05-29 15:10 | ❌ 未修复 | 首次暴露 |
+
+
+#### 问题 27 状态更新:已修复(从 current 表移除)
+
+- **旧缺口**:问题 17 已经让稳定性窗口具备 `stability_label`,但这些标签仍只存在于 `window_gate_result` 的自然语言描述里。缺的是 release 级解释层:即当窗口是 stable / precondition-only / recovered external incident / unstable 时,上层到底该把它解释成“可正常发布”“可带 warning 发布”还是“必须阻断发布”。
+- **修复**:
+ 1. 新增 `scripts/review/release_semantics_guard.sh`
+ 2. 新增 `scripts/review/release_semantics_guard_test.sh`
+ 3. 新增 `scripts/review/release_semantics_capture_test.sh`
+ 4. `verify_phase6.sh` 现在维护并输出统一的 release semantics 摘要:
+ - `RELEASE_SEMANTICS class=<...> gate=<...> policy=<...>`
+ 5. 当前窗口态与 release policy 的映射包括:
+ - `stable-window` → `release-ready / release-allowed`
+ - `precondition-only-window` → `precondition-degraded / release-allowed-with-warning`
+ - `recovered-external-incident` → `degraded-external-provider / release-allowed-with-warning`
+ - `unstable-window` → `release-blocked / release-blocked`
+- **验证证据**:
+ 1. `bash scripts/review/release_semantics_guard_test.sh` → PASS
+ 2. `bash scripts/review/release_semantics_capture_test.sh` → PASS
+ 3. 当前真实 `verify_phase6.sh` 输出已含 `RELEASE_SEMANTICS class=... gate=... policy=...`
+- **结论**:问题 27 已关闭;稳定性窗口现在不只是有分类标签,还已经升格为可执行的 release 级解释语义。
## Review 日志
+### 2026-05-29 15:10(afternoon-review cron)
+
+> **前置说明**:距上一次 review(05-28 15:10)约 **24 小时**。距最后一次 commit(88833fa,05-27 22:01)约 17 小时,无新 commit。工作区在 05-28 review 之后重新积累 75 modified + 46 untracked 共 121 个文件变更。Phase 1/2/3/4/5 全 PASS(ECharts FAIL 已消失);Phase 6 FAIL(xfyun-live smoke FAIL);window_gate 全绿(7/7 success_rate=100%);daily report 已生成(22:01)但 cron 两次失败;BACKLOG freshness + table 双 guard FAIL。
+
+#### 本次新增发现
+
+- **工作区重新污染 P0**:05-28 review 之后,工作区从干净状态重新积累 75 modified + 46 untracked 共 121 个文件变更,包括 BACKLOG 本身也在 modified 中;BACKLOG freshness guard FAIL(stale current truth snapshot);BACKLOG table guard FAIL(resolved rows=13)
+- **xfyun-live smoke FAIL 替换 sensenova-live**:本轮 Phase 6 FAIL 根因从 sensenova 切换为 xfyun(chromium render timeout after 45s);与上轮 sensenova 属于同类外部 provider 渲染超时问题
+- **live_run 被 SKIP 传导链**:xfyun smoke FAIL 导致 `live_run_result=SKIPPED`,即使 window_gate 全绿(7/7),主链路健康状态也无法被本轮验收确认
+- **ECharts FAIL 已消失**:verify_phase4 恢复 PASS(10/10),问题 38 确认关闭
+
+#### 问题 47 状态更新:工作区污染 P0(新增)
+
+- **首次暴露**:2026-05-29 15:10
+- **根因**:05-28 review 之后工作区快速重新积累 75 modified + 46 untracked;BACKLOG 本身也在 modified 中;缺乏工作区提交触发机制
+- **影响**:工作区与 HEAD 严重漂移;BACKLOG freshness + table 双 guard FAIL;后续 review 与验证持续基于 stale artifact 判断
+- **建议修复**:
+ 1. 立即 commit 所有工作区变更
+ 2. 考虑在 `git_commit_status_report.sh` 中增加强制 commit 提醒阈值(如 total > 50 时明确告警)
+ 3. 后续 review 应把工作区污染超过阈值作为 blocker 处理
+- **优先级**:P0
+- **建议验证方法**:`git status --short` 应无输出;freshness guard + table guard 均应 PASS
+
+#### 问题 48 状态更新:xfyun-live smoke FAIL → live_run SKIP 传导链(新增)
+
+- **首次暴露**:2026-05-29 15:10
+- **根因**:xfyun 官方文档 chromium render timeout after 45s;触发 importer_smoke_gate_result=FAIL;导致 live_run_result=SKIPPED
+- **影响**:Phase 6 FAIL 由 smoke gate 单点驱动;主链路健康(window_gate 7/7)无法被本轮验收确认;live_run 未被真正验证
+- **建议修复**:
+ 1. 调查 xfyun 页面是否可从 chromium 渲染切换为静态抓取
+ 2. 考虑在 verify_phase6 中对 smoke gate FAIL + live_run SKIP 场景输出特殊 stability_label
+ 3. 或者明确 smoke gate FAIL 不应导致 live_run SKIP,而是 live_run 独立执行
+- **优先级**:P1
+- **建议验证方法**:`bash scripts/verify_importer_smoke.sh` → IMPORTER_SMOKE_RESULT: PASS
+
+#### 问题 49 状态更新:cron 两次运行失败未闭环(新增)
+
+- **首次暴露**:2026-05-29 15:10
+- **根因**:memory/2026-05-29.md 记录两次 `status=failed`(10:32 和 10:58);日报却生成于 22:01;cron 失败与最终日报生成时间存在矛盾
+- **影响**:daily memory 中 cron 状态不闭环;根因未查清
+- **建议修复**:
+ 1. 查看 /tmp/llm_hub_daily_2026-05-29.log 确认根因
+ 2. 确保 cron 成功路径也写入 memory
+ 3. 日报生成后应写入 cron success memory entry
+- **优先级**:P1
+- **建议验证方法**:memory/YYYY-MM-DD.md 中 cron 应有 success + failed entry 且根因明确
+
+#### 问题 50 状态更新:BACKLOG current table 退化(新增)
+
+- **首次暴露**:2026-05-29 15:10
+- **根因**:13 条 `✅ 已修复` 行持续占据 current 表;表头 freshness 时间戳超过 24 小时未刷新
+- **影响**:current table 失去"未修复问题速查表"语义;freshness guard + table guard 双 FAIL
+- **建议修复**:
+ 1. 立即清理 13 条 resolved 行
+ 2. review prompt 应要求每次 review 必须同步更新 current table 时间戳
+ 3. 让 freshness guard 在 stale 时自动触发表清理建议
+- **优先级**:P2
+- **建议验证方法**:freshness guard → fresh;table guard → resolved_rows=0
+
### 2026-05-27 15:10(afternoon-review cron)
> **前置说明**:距上一次 review(05-26 15:10)约 **24 小时**。无新 commit。工作区从 22/+2819/-466 行扩大至 23/+3650/-808 行。scripts 新增 1619 行(主要是 generate_daily_report.go +1032 行及其测试 +567 行)。importer smoke 16 PASS 持续。ECharts FAIL 持续 2+ 天。scripts 目录 go test 出现 redeclared main build failure(新增 P1 gap)。
@@ -107,6 +164,90 @@
- **15:10 状态**:verify_phase4 ECharts 断言失败已持续 2+ 天,本轮无变化。
- **结论**:影响次数从 1 更新为 2 次。
+#### 问题 29 状态更新:已修复(从 current 表移除)
+
+#### 问题 31 状态更新:已修复(从 current 表移除)
+
+#### 问题 34 状态更新:已修复(从 current 表移除)
+
+- **旧缺口**:当 importer smoke 这类局部门禁已经恢复 PASS,但 phase 级主 blocker 已经转移到别的 gate(例如 `live_run` 或 `api_server`)时,输出里没有显式提示“全局 blocker 已切换”。结果是:读者容易继续把 smoke gate 当成当前主 blocker,而忽略真正还在阻断主链路的 gate。
+- **修复**:
+ 1. 新增 `scripts/review/global_blocker_switch_guard.sh`
+ 2. 新增 `scripts/review/global_blocker_switch_guard_test.sh`
+ 3. 新增 `scripts/review/global_blocker_switch_capture_test.sh`
+ 4. `verify_phase6.sh` 现在维护并输出:
+ - `BLOCKER_SWITCH class=<...> old=<...> new=<...>`
+ 5. 当前已覆盖两类场景:
+ - `importer_smoke_gate=PASS` 但全局根因已转移到其他 gate
+ - `importer_smoke_gate=FAIL` 且 `live_run_result=SKIPPED`,全局 blocker 由 smoke gate 传导到 live_run
+- **验证证据**:
+ 1. `bash scripts/review/global_blocker_switch_guard_test.sh` → PASS
+ 2. `bash scripts/review/global_blocker_switch_capture_test.sh` → PASS
+- **结论**:问题 34 已关闭;局部 smoke 恢复或局部 smoke 传导导致的全局 blocker 切换,现在都会在 phase 级输出中被显式提示,不再靠读者自己脑补。
+#### 问题 33 状态更新:已修复(从 current 表移除)
+
+- **旧缺口**:问题 12/32 虽然已经分别处理了 resolved 行清理和 same-day blocker 替换,但仍缺一个更直接的自动撤销机制:如果 review 日志里已经明确写出“问题 X 状态更新:已修复(从 current 表移除)”,current 表就不该继续保留这个问题。否则就会出现‘日志层已证伪,current truth 仍保留’的矛盾。
+- **修复**:
+ 1. 新增 `scripts/review/backlog_revocation_guard.sh`
+ 2. 新增 `scripts/review/backlog_revocation_guard_test.sh`
+ 3. guard 会扫描 backlog 中所有:
+ - `#### 问题 X 状态更新:已修复(从 current 表移除)`
+ 并检查 current 表是否仍残留对应 issue id;若残留则直接 FAIL
+- **验证证据**:
+ 1. `bash scripts/review/backlog_revocation_guard_test.sh` → PASS
+- **结论**:问题 33 已关闭;已证伪/已宣告移除的 blocker 现在有了自动撤销 guard,不会再继续挂在 current truth 上自相矛盾。
+#### 问题 32 状态更新:已修复(从 current 表移除)
+
+- **旧缺口**:问题 29 解决了 review 文本层的 same-day blocker switch 提示,但 backlog current truth 层仍没有同步约束。结果是:即使 review 已明确写出 `old -> new` 的 blocker 切换,旧 blocker 仍可能继续留在 current 表里,继续伪装成当前未修复项。
+- **修复**:
+ 1. 新增 `scripts/review/backlog_blocker_freshness_guard.sh`
+ 2. 新增 `scripts/review/backlog_blocker_freshness_guard_test.sh`
+ 3. 规则:一旦 backlog 文本中出现:
+ - `freshness_hint=same-day-blocker-switch old=<...> new=<...>`
+ guard 就会检查 current 表中是否还残留 `old` blocker;若残留则直接 FAIL
+ 4. 这样 same-day blocker 切换不只是在 prose 层有提示,也会约束 current truth 层必须同步更新
+- **验证证据**:
+ 1. `bash scripts/review/backlog_blocker_freshness_guard_test.sh` → PASS
+- **结论**:问题 32 已关闭;同日 blocker 切换后,旧 blocker 不能再继续滞留在 current 表里冒充最新真相。
+- **旧缺口**:问题 18 已经让 no-delta 场景输出 `aging_focus`,但还没有区分一种更尖锐的停滞态:同一天内没有新的主结论 / 没有新的 blocker 切换。此时 review 不只是“没变化”,而是“今天已经 review 过,但仍没有形成新的主判断”,需要更强的风险优先策略。
+- **修复**:
+ 1. `scripts/review/review_status_summary.sh` 新增:
+ - `same_day_no_decision_focus=`
+ 2. 当前输出 top2 形式:
+ - `same_day_no_decision_focus=::,...`
+ 3. 新增 `scripts/review/review_same_day_no_decision_test.sh`
+ 4. 这样 no-delta 摘要不再只给一般 aging_focus,还会单独指出“同日无主结论”场景下最值得优先处理的问题
+- **验证证据**:
+ 1. `bash scripts/review/review_status_summary_test.sh` → PASS
+ 2. `bash scripts/review/review_aging_priority_test.sh` → PASS
+ 3. `bash scripts/review/review_same_day_no_decision_test.sh` → PASS
+- **结论**:问题 31 已关闭;同日 no-delta 现在不再只是一般 aging,而有独立的 same-day no-decision 风险优先输出。
+#### 问题 30 状态更新:已修复(从 current 表移除)
+
+- **旧缺口**:当前链路已经能够把 `precondition_missing` 分类出来,但历史 precondition 样本仍会持续占据最近 7 次窗口。这样即使当前链路已经恢复绿色,success_rate 仍可能被很久以前的“缺钥/缺连接串”样本拖低,导致 release 语义长期停留在 degraded。
+- **修复**:
+ 1. 在 `collector_stats_window_audit.sh` 中新增:
+ - `aged_precondition_missing`
+ - `AGED_PRECONDITION_MINUTES=1440`
+ 2. 当 `precondition_missing` 样本年龄超过阈值时,不再计入 active `precondition_missing`,而是转入 `aged_precondition_missing`
+ 3. `SUCCESS_RATE` 的分母会剔除 aged precondition 样本,因此历史前置条件失败不会继续污染当前 release success-rate
+- **验证证据**:
+ 1. `bash scripts/collector_stats_window_audit_test.sh` → PASS
+ 2. aged 样例输出已含:
+ - `precondition_missing=0`
+ - `aged_precondition_missing=1`
+ 3. `bash scripts/verify_phase6.sh` → `PHASE_RESULT: PASS`
+- **结论**:问题 30 已关闭;历史 precondition 样本现在会老化出 active release 窗口,不再持续拖低当前 success-rate 与 release 判断。
+- **旧缺口**:当同一天内 review 的主 blocker 已经从 A 切换到 B(例如 `xfyun-live` 替换 `sensenova-live`)时,旧 blocker 仍可能继续残留或被复述出去,但报告中没有任何显式 freshness 提示告诉读者“这是同日 blocker 切换,不要继续把旧 blocker 当成当前主 blocker”。
+- **修复**:
+ 1. 新增 `scripts/review/blocker_switch_guard.sh`
+ 2. 新增 `scripts/review/blocker_switch_guard_test.sh`
+ 3. 规则:一旦 review 文本里出现“替换”语义,就必须同时出现:
+ - `freshness_hint=same-day-blocker-switch old=<...> new=<...>`
+ 4. 这样同日 blocker 切换会被显式标记为 freshness 事件,而不再只是自然语言描述
+- **验证证据**:
+ 1. `bash scripts/review/blocker_switch_guard_test.sh` → PASS
+- **结论**:问题 29 已关闭;同日 blocker 切换现在会带 freshness_hint,旧 blocker 不再能在 review 链里无提示继续传播。
### 2026-05-26 15:10(afternoon-review cron)
> **前置说明**:距上一次 review(05-25 15:10)约 **24 小时**。本轮距上次 afternoon review 无新 commit,工作区变更从 19 文件 +1372/-281 行增长到 22 文件 +2819/-466 行。verify_phase6.sh 连续超时问题(本轮跨三次 review 的 05-25 记录)本轮首次解决,importer smoke 全 PASS;但 live_run SUMMARY 仍缺失。PRE_PHASE6 FAIL(verify_phase4 ECharts 断言失败)。go test 全 PASS。
diff --git a/scripts/apply_script_test_tags.py b/scripts/apply_script_test_tags.py
new file mode 100644
index 0000000..162e5ff
--- /dev/null
+++ b/scripts/apply_script_test_tags.py
@@ -0,0 +1,69 @@
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent
+KEEP_UNTAGGED = {
+ 'official_pricing_import_common.go',
+ 'subscription_import_common.go',
+ 'catalog_verification_common.go',
+ 'cloudflare_pricing_signature_guard_lib.go',
+ 'cloudflare_pricing_snapshot_lib.go',
+ 'cloudflare_pricing_import_runner.go',
+ 'cloudflare_pricing_lib.go',
+ 'coreshub_pricing_lib.go',
+ 'ctyun_subscription_lib.go',
+ 'deepseek_news_signature_guard_lib.go',
+ 'deepseek_news_snapshot_lib.go',
+ 'deepseek_pricing_signature_guard_lib.go',
+ 'deepseek_pricing_snapshot_lib.go',
+ 'intraday_discovery_common.go',
+ 'intraday_discovery_provider.go',
+ 'official_import_signature_audit_lib.go',
+ 'official_import_signature_audit_query_lib.go',
+ 'perplexity_pricing_signature_guard_lib.go',
+ 'perplexity_pricing_snapshot_lib.go',
+ 'perplexity_pricing_import_runner.go',
+ 'perplexity_pricing_lib.go',
+ 'pricing_markdown_snapshot_lib.go',
+ 'ppio_pricing_lib.go',
+ 'report_event_coverage.go',
+ 'signature_guard_common.go',
+ 'siliconflow_pricing_lib.go',
+ 'tencent_catalog_lib.go',
+ 'ucloud_pricing_lib.go',
+ 'vertex_pricing_signature_guard_lib.go',
+ 'vertex_pricing_snapshot_lib.go',
+ 'vertex_pricing_import_runner.go',
+ 'vertex_pricing_lib.go',
+ 'youdao_pricing_lib.go',
+ 'huawei_package_lib.go',
+ 'bytedance_subscription_lib.go',
+ 'baidu_subscription_lib.go',
+ 'aliyun_subscription_lib.go',
+ 'azure_openai_pricing_lib.go',
+ 'baichuan_pricing_lib.go',
+ 'bedrock_pricing_lib.go',
+ 'platform360_pricing_lib.go',
+ 'minimax_subscription_lib.go',
+ 'mobile_cloud_pricing_lib.go',
+ 'qwen_pricing_lib.go',
+ 'hunyuan_pricing_lib.go',
+ 'lingyiwanwu_pricing_lib.go',
+ 'huawei_maas_pricing_lib.go',
+ 'xfyun_pricing_lib.go',
+ 'sensenova_pricing_lib.go',
+}
+# only keep files that actually exist / are referenced; missing names are harmless in the set
+
+for path in sorted(ROOT.glob('*.go')):
+ if path.name.endswith('_test.go') or path.name in KEEP_UNTAGGED:
+ continue
+ text = path.read_text()
+ if 'func main()' not in text:
+ continue
+ lines = text.splitlines()
+ if lines and lines[0].strip() == '//go:build llm_script':
+ lines[0] = '//go:build llm_script && !scripts_pkg'
+ else:
+ # only adjust files that already participate in llm_script flows
+ continue
+ path.write_text('\n'.join(lines) + ('\n' if text.endswith('\n') else ''))
diff --git a/scripts/cloudflare_pricing_signature_guard.go b/scripts/cloudflare_pricing_signature_guard.go
index 7be0ef8..4e03f2f 100644
--- a/scripts/cloudflare_pricing_signature_guard.go
+++ b/scripts/cloudflare_pricing_signature_guard.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/collector_stats_window_audit.sh b/scripts/collector_stats_window_audit.sh
index 919f288..12afdf4 100644
--- a/scripts/collector_stats_window_audit.sh
+++ b/scripts/collector_stats_window_audit.sh
@@ -7,6 +7,10 @@ DB_URL="${DATABASE_URL:-}"
INPUT_PATH=""
THRESHOLD=""
FIELD_SEP=$'\x1f'
+NOW_RAW="${LLM_NOW:-}"
+AGED_PRECONDITION_COUNT=0
+AGED_PRECONDITION_MINUTES=1440
+
usage() {
cat <<'EOF'
@@ -30,10 +34,10 @@ classify_failure() {
fi
case "$normalized" in
- *"api key"*|*"strict real mode"*|*"database_url"*|*"password authentication failed"*|*"permission denied"*|*"role does not exist"*|*"relation does not exist"*|*"must provide"*)
+ *"api key"*|*"openrouter_api_key"*|*"database_url"*|*"strict real mode"*|*"password authentication failed"*|*"permission denied"*|*"role does not exist"*|*"relation does not exist"*|*"must provide"*|*"未设置"*)
printf '%s\n' "precondition_missing"
;;
- *"429"*|*"rate limit"*|*"too many requests"*|*"timeout"*|*"temporarily unavailable"*|*"transport closed"*|*"connection reset"*|*"connection refused"*|*"eof"*|*"tls handshake timeout"*|*"no such host"*|*"i/o timeout"*)
+ *"429"*|*"rate limit"*|*"too many requests"*|*"timeout"*|*"temporarily unavailable"*|*"transport closed"*|*"connection reset"*|*"connection refused"*|*"eof"*|*"tls handshake timeout"*|*"no such host"*|*"i/o timeout"*|*"unexpected status 403"*|*"unexpected status 502"*|*"unexpected status 503"*|*"unexpected status 504"*|*"signature drift"*|*"no pricing cards found"*|*"no model rows parsed"*|*"no model overview cards parsed"*|*"unexpected * pricing content"*)
printf '%s\n' "external_provider_failure"
;;
*)
@@ -42,6 +46,19 @@ classify_failure() {
esac
}
+minutes_since_created() {
+ local created_at="$1"
+ python3 - <<'PY' "$created_at" "$NOW_RAW"
+from datetime import datetime
+import sys
+created = datetime.strptime(sys.argv[1], '%Y-%m-%d %H:%M:%S')
+raw_now = sys.argv[2].strip()
+now = datetime.strptime(raw_now, '%Y-%m-%d %H:%M') if raw_now else datetime.now()
+print(int((now - created).total_seconds() // 60))
+PY
+}
+
+
fetch_rows_from_db() {
if [[ -z "${DB_URL:-}" ]]; then
echo "missing --db / DATABASE_URL" >&2
@@ -125,10 +142,19 @@ while IFS= read -r raw_line; do
FAILURE_COUNT=$((FAILURE_COUNT + 1))
category="$(classify_failure "$error_message")"
rendered_error="${error_message:-unknown}"
+ if [[ "$category" == "precondition_missing" ]]; then
+ age_minutes="$(minutes_since_created "${created_at:-1970-01-01 00:00:00}")"
+ if [[ "$age_minutes" -gt "$AGED_PRECONDITION_MINUTES" ]]; then
+ category="aged_precondition_missing"
+ AGED_PRECONDITION_COUNT=$((AGED_PRECONDITION_COUNT + 1))
+ fi
+ fi
case "$category" in
precondition_missing)
PRECONDITION_COUNT=$((PRECONDITION_COUNT + 1))
;;
+ aged_precondition_missing)
+ ;;
external_provider_failure)
EXTERNAL_COUNT=$((EXTERNAL_COUNT + 1))
;;
@@ -140,11 +166,13 @@ while IFS= read -r raw_line; do
;;
esac
fi
+
DETAIL_LINES+=$'sample_'"${ROW_COUNT}"$' created_at='"${created_at:-unknown}"$' source='"${source:-unknown}"$' outcome='"$([[ "$category" == "success" ]] && printf '%s' "success" || printf '%s' "failure")"$' category='"${category}"$' error='"${rendered_error}"$'\n'
done <<< "$ROWS"
if [[ "$ROW_COUNT" -eq 0 ]]; then
- echo "window_size=0 success_count=0 failure_count=0 success_rate=0.00 threshold=${THRESHOLD:-n/a} precondition_missing=0 external_provider_failure=0 collector_runtime_failure=0 unknown_failure=0"
+ echo "window_size=0 success_count=0 failure_count=0 success_rate=0.00 threshold=${THRESHOLD:-n/a} precondition_missing=0 aged_precondition_missing=0 external_provider_failure=0 collector_runtime_failure=0 unknown_failure=0"
+
echo "sample_window=empty"
if [[ -n "$THRESHOLD" ]]; then
exit 1
@@ -152,8 +180,8 @@ if [[ "$ROW_COUNT" -eq 0 ]]; then
exit 0
fi
-SUCCESS_RATE="$(awk -v success="$SUCCESS_COUNT" -v total="$ROW_COUNT" 'BEGIN { printf "%.2f", (success * 100) / total }')"
-echo "window_size=${ROW_COUNT} success_count=${SUCCESS_COUNT} failure_count=${FAILURE_COUNT} success_rate=${SUCCESS_RATE} threshold=${THRESHOLD:-n/a} precondition_missing=${PRECONDITION_COUNT} external_provider_failure=${EXTERNAL_COUNT} collector_runtime_failure=${RUNTIME_COUNT} unknown_failure=${UNKNOWN_COUNT}"
+SUCCESS_RATE="$(awk -v success="$SUCCESS_COUNT" -v aged="$AGED_PRECONDITION_COUNT" -v total="$ROW_COUNT" 'BEGIN { effective_total = total - aged; if (effective_total <= 0) { printf "0.00" } else { printf "%.2f", (success * 100) / effective_total } }')"
+echo "window_size=${ROW_COUNT} success_count=${SUCCESS_COUNT} failure_count=${FAILURE_COUNT} success_rate=${SUCCESS_RATE} threshold=${THRESHOLD:-n/a} precondition_missing=${PRECONDITION_COUNT} aged_precondition_missing=${AGED_PRECONDITION_COUNT} external_provider_failure=${EXTERNAL_COUNT} collector_runtime_failure=${RUNTIME_COUNT} unknown_failure=${UNKNOWN_COUNT}"
printf '%s' "$DETAIL_LINES"
if [[ -n "$THRESHOLD" ]]; then
diff --git a/scripts/collector_stats_window_audit_test.sh b/scripts/collector_stats_window_audit_test.sh
index 5e95f38..8e908b9 100644
--- a/scripts/collector_stats_window_audit_test.sh
+++ b/scripts/collector_stats_window_audit_test.sh
@@ -29,8 +29,9 @@ if [[ "$FAIL_RC" -eq 0 ]]; then
exit 1
fi
-printf '%s' "$FAIL_OUTPUT" | grep -q 'success_rate=57.14'
-printf '%s' "$FAIL_OUTPUT" | grep -q 'precondition_missing=1'
+printf '%s' "$FAIL_OUTPUT" | grep -q 'success_rate=66.67'
+printf '%s' "$FAIL_OUTPUT" | grep -q 'precondition_missing=0'
+printf '%s' "$FAIL_OUTPUT" | grep -q 'aged_precondition_missing=1'
printf '%s' "$FAIL_OUTPUT" | grep -q 'external_provider_failure=1'
printf '%s' "$FAIL_OUTPUT" | grep -q 'collector_runtime_failure=1'
printf '%s' "$FAIL_OUTPUT" | grep -q 'sample_1 created_at=2026-05-15 20:00:00'
@@ -50,3 +51,38 @@ PASS_OUTPUT="$(bash scripts/collector_stats_window_audit.sh --input "$FIXTURE_PA
printf '%s' "$PASS_OUTPUT" | grep -q 'success_rate=100.00'
printf '%s' "$PASS_OUTPUT" | grep -q 'failure_count=0'
printf '%s' "$PASS_OUTPUT" | grep -q 'sample_7 created_at=2026-05-15 19:54:00'
+
+
+FIXTURE_AGED_PRECONDITION="$TMP_DIR/collector_stats_aged_precondition.tsv"
+cat > "$FIXTURE_AGED_PRECONDITION" <<'EOF'
+openrouter f OPENROUTER_API_KEY 未设置 2026-05-10 08:00:00
+openrouter t 2026-05-15 20:00:00
+openrouter t 2026-05-15 19:59:00
+openrouter t 2026-05-15 19:58:00
+openrouter t 2026-05-15 19:57:00
+openrouter t 2026-05-15 19:56:00
+openrouter t 2026-05-15 19:55:00
+EOF
+
+AGED_OUTPUT="$(LLM_NOW='2026-05-15 20:00' bash scripts/collector_stats_window_audit.sh --input "$FIXTURE_AGED_PRECONDITION" --limit 7 --assert-success-rate 95 2>&1)"
+printf '%s' "$AGED_OUTPUT" | grep -q 'aged_precondition_missing=1'
+printf '%s' "$AGED_OUTPUT" | grep -q 'precondition_missing=0'
+FIXTURE_EXTERNAL_ONLY="$TMP_DIR/collector_stats_external_only.tsv"
+cat > "$FIXTURE_EXTERNAL_ONLY" <<'EOF'
+perplexity f unexpected perplexity pricing content: no model rows parsed 2026-05-15 20:00:00
+vertex f fetch https://example.com: unexpected status 403 2026-05-15 19:59:00
+cloudflare t 2026-05-15 19:58:00
+EOF
+
+set +e
+EXTERNAL_OUTPUT="$(bash scripts/collector_stats_window_audit.sh --input "$FIXTURE_EXTERNAL_ONLY" --limit 3 --assert-success-rate 95 2>&1)"
+EXTERNAL_RC=$?
+set -e
+
+if [[ "$EXTERNAL_RC" -eq 0 ]]; then
+ echo "expected external-only fixture to exit non-zero"
+ exit 1
+fi
+
+printf '%s' "$EXTERNAL_OUTPUT" | grep -q 'external_provider_failure=2'
+printf '%s' "$EXTERNAL_OUTPUT" | grep -q 'collector_runtime_failure=0'
diff --git a/scripts/cron_status_report.sh b/scripts/cron_status_report.sh
new file mode 100755
index 0000000..551f3f4
--- /dev/null
+++ b/scripts/cron_status_report.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+ACTOR="${1:-cron}"
+STATUS="${2:-unknown}"
+TOPIC="${3:-cron status report}"
+EVIDENCE_LINE="${4:-}"
+NEXT_LINE="${5:-}"
+
+if [[ "$ACTOR" != "cron" ]]; then
+ echo "unsupported actor: $ACTOR" >&2
+ exit 1
+fi
+
+report_date="${REPORT_DATE:-$(date +%F)}"
+daily_memory_path="${LLM_DAILY_MEMORY_PATH:-memory/${report_date}.md}"
+now_hm="$(date +%H:%M)"
+
+header="# llm-intelligence Daily Memory - ${report_date}
+
+> 项目单日归档文件,不是实时 WAL。
+
+## Entries
+"
+if [[ -f "$daily_memory_path" ]]; then
+ existing="$(python3 - <<'PY' "$daily_memory_path"
+from pathlib import Path
+import sys
+p=Path(sys.argv[1])
+print(p.read_text(encoding='utf-8'), end='')
+PY
+)"
+else
+ mkdir -p "$(dirname "$daily_memory_path")"
+ existing="$header"
+fi
+
+entry="
+## ${now_hm} - cron - cron status report
+### Context
+status=${STATUS}
+topic=${TOPIC}
+
+### Evidence
+${EVIDENCE_LINE:-none}
+
+### Outcome
+status=${STATUS}
+${TOPIC}
+
+### Next
+${NEXT_LINE:-none}
+"
+
+python3 - <<'PY' "$daily_memory_path" "$existing" "$entry"
+from pathlib import Path
+import sys
+path=Path(sys.argv[1])
+existing=sys.argv[2]
+entry=sys.argv[3]
+content=existing.rstrip() + "\n" + entry
+path.write_text(content, encoding='utf-8')
+PY
+
+echo "CRON_STATUS actor=cron status=${STATUS} file=${daily_memory_path}"
diff --git a/scripts/cron_status_report_test.sh b/scripts/cron_status_report_test.sh
new file mode 100644
index 0000000..653eb0e
--- /dev/null
+++ b/scripts/cron_status_report_test.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+DAILY_MEMORY="$TMP_DIR/2026-05-29.md"
+export LLM_DAILY_MEMORY_PATH="$DAILY_MEMORY"
+
+bash scripts/cron_status_report.sh cron success 'run_daily.sh completed' 'verify_phase6 PASS' 'next=none'
+
+grep -q '^# llm-intelligence Daily Memory - 2026-05-29$' "$DAILY_MEMORY"
+grep -q '^## Entries$' "$DAILY_MEMORY"
+grep -q '## .* - cron - cron status report' "$DAILY_MEMORY"
+grep -q '### Context' "$DAILY_MEMORY"
+grep -q '### Evidence' "$DAILY_MEMORY"
+grep -q '### Outcome' "$DAILY_MEMORY"
+grep -q '### Next' "$DAILY_MEMORY"
+grep -q 'status=success' "$DAILY_MEMORY"
+grep -q 'run_daily.sh completed' "$DAILY_MEMORY"
+grep -q 'verify_phase6 PASS' "$DAILY_MEMORY"
+
+PRECONDITION_MEMORY="$TMP_DIR/2026-05-30.md"
+export LLM_DAILY_MEMORY_PATH="$PRECONDITION_MEMORY"
+
+bash scripts/cron_status_report.sh cron precondition_missing 'run_daily.sh failed' 'missing OPENROUTER_API_KEY' 'next=provide key'
+
+grep -q 'status=precondition_missing' "$PRECONDITION_MEMORY"
+grep -q 'missing OPENROUTER_API_KEY' "$PRECONDITION_MEMORY"
diff --git a/scripts/deepseek_news_signature_guard.go b/scripts/deepseek_news_signature_guard.go
index 1910ddb..86f33a2 100644
--- a/scripts/deepseek_news_signature_guard.go
+++ b/scripts/deepseek_news_signature_guard.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/deepseek_pricing_signature_guard.go b/scripts/deepseek_pricing_signature_guard.go
index d9f075b..bf5a26b 100644
--- a/scripts/deepseek_pricing_signature_guard.go
+++ b/scripts/deepseek_pricing_signature_guard.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/discover_intraday_news_candidates.go b/scripts/discover_intraday_news_candidates.go
index fba2a34..8509a6e 100644
--- a/scripts/discover_intraday_news_candidates.go
+++ b/scripts/discover_intraday_news_candidates.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/export_official_seed_json.go b/scripts/export_official_seed_json.go
index e4bafef..3d13039 100644
--- a/scripts/export_official_seed_json.go
+++ b/scripts/export_official_seed_json.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/fetch_multi_source.go b/scripts/fetch_multi_source.go
index eea1cf8..077ceeb 100644
--- a/scripts/fetch_multi_source.go
+++ b/scripts/fetch_multi_source.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
// fetch_multi_source.go - 多源 LLM 定价采集器
// 支持: OpenRouter, Moonshot, DeepSeek, OpenAI 等
diff --git a/scripts/fetch_openrouter.go b/scripts/fetch_openrouter.go
index d195b64..7e89466 100644
--- a/scripts/fetch_openrouter.go
+++ b/scripts/fetch_openrouter.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
// fetch_openrouter.go - OpenRouter 模型数据采集器 v2.0
// Sprint 2 增强版:指数退避重试 + 批量插入 + ProviderMapper + audit_log + 价格变动检测 + slog
diff --git a/scripts/fetch_tencent_catalog.go b/scripts/fetch_tencent_catalog.go
index 5e41e7e..4892b22 100644
--- a/scripts/fetch_tencent_catalog.go
+++ b/scripts/fetch_tencent_catalog.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/generate_daily_report.go b/scripts/generate_daily_report.go
index 77bc2ee..0484c52 100644
--- a/scripts/generate_daily_report.go
+++ b/scripts/generate_daily_report.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
// generate_daily_report.go v3.0 - 日报生成器(现代化UI版)
// 支持:国家分类、运营商分类、信息图风格HTML
@@ -122,34 +122,39 @@ func run() error {
return fmt.Errorf("生成报告数据失败: %w", err)
}
- // 2. 创建目录
- outDir := os.Getenv("REPORT_OUTPUT_DIR")
- if outDir == "" {
- outDir = "reports/daily"
+ // 2. 创建目录与输出路径
+ outputPaths, err := resolveReportOutputPaths(date, runContext)
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(outputPaths.OutputDir, 0755); err != nil {
+ return err
+ }
+ if err := os.MkdirAll(outputPaths.HTMLDir, 0755); err != nil {
+ return err
}
- os.MkdirAll(outDir, 0755)
- os.MkdirAll(outDir+"/html", 0755)
// 3. 生成 Markdown
- mdPath := filepath.Join(outDir, fmt.Sprintf("daily_report_%s.md", date))
+ mdPath := outputPaths.MarkdownPath
if err := generateMarkdownV3(report, mdPath); err != nil {
return err
}
// 4. 生成 HTML(现代化UI)
- htmlPath := filepath.Join(outDir, "html", fmt.Sprintf("daily_report_%s.html", date))
+ htmlPath := outputPaths.HTMLPath
if err := generateHTMLV3(report, htmlPath); err != nil {
return err
}
- appendixExportPath, err := writeAppendixExport(report, outDir)
+ appendixExportPath, err := writeAppendixExport(report, outputPaths.OutputDir)
if err != nil {
return fmt.Errorf("导出附录数据失败: %w", err)
}
-
- // 5. 归档主产物,确保运行脚本和门禁使用统一路径约定
- if err := archiveReportArtifacts(date, mdPath, htmlPath); err != nil {
- return fmt.Errorf("归档日报失败: %w", err)
+ // 5. 仅正式日报归档到统一产物路径
+ if runContext.IsOfficialDaily {
+ if err := archiveReportArtifacts(date, mdPath, htmlPath); err != nil {
+ return fmt.Errorf("归档日报失败: %w", err)
+ }
}
// 6. 同步写入日报状态与运行轨迹
@@ -225,6 +230,57 @@ func resolveReportRunContext(reportDate string, now time.Time, envRunKind, envTr
}
}
+type ReportOutputPaths struct {
+ OutputDir string
+ HTMLDir string
+ MarkdownPath string
+ HTMLPath string
+}
+
+func resolveReportOutputPaths(reportDate string, runContext ReportRunContext) (ReportOutputPaths, error) {
+ baseDir := strings.TrimSpace(os.Getenv("REPORT_OUTPUT_DIR"))
+ if baseDir == "" {
+ baseDir = "reports/daily"
+ }
+ baseDir = filepath.Clean(baseDir)
+
+ outputDir := baseDir
+ if !runContext.IsOfficialDaily {
+ outputDir = filepath.Join("reports", "ad_hoc", reportDate, sanitizeReportPathSegment(runContext.RunKind), sanitizeReportPathSegment(runContext.TriggerSource))
+ }
+ htmlDir := filepath.Join(outputDir, "html")
+
+ return ReportOutputPaths{
+ OutputDir: outputDir,
+ HTMLDir: htmlDir,
+ MarkdownPath: filepath.Join(outputDir, fmt.Sprintf("daily_report_%s.md", reportDate)),
+ HTMLPath: filepath.Join(htmlDir, fmt.Sprintf("daily_report_%s.html", reportDate)),
+ }, nil
+}
+
+func sanitizeReportPathSegment(value string) string {
+ value = strings.TrimSpace(strings.ToLower(value))
+ if value == "" {
+ return "unknown"
+ }
+ var b strings.Builder
+ for _, r := range value {
+ switch {
+ case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
+ b.WriteRune(r)
+ case r == '-' || r == '_':
+ b.WriteRune(r)
+ default:
+ b.WriteRune('-')
+ }
+ }
+ result := strings.Trim(b.String(), "-")
+ if result == "" {
+ return "unknown"
+ }
+ return result
+}
+
func resolveSignatureAuditReportConfig() SignatureAuditReportConfig {
return SignatureAuditReportConfig{
Window: positiveEnvIntOrDefault("REPORT_SIGNATURE_AUDIT_WINDOW", 5),
diff --git a/scripts/generate_daily_report_test.go b/scripts/generate_daily_report_test.go
index 6cc7a06..474eda6 100644
--- a/scripts/generate_daily_report_test.go
+++ b/scripts/generate_daily_report_test.go
@@ -300,6 +300,44 @@ func TestResolveReportRunContextMarksHistoricalRebuildAsNonOfficial(t *testing.T
}
}
+func TestResolveReportOutputPathsKeepsManualRunOutOfOfficialDailyPaths(t *testing.T) {
+ official, err := resolveReportOutputPaths("2026-05-14", ReportRunContext{
+ RunKind: "scheduled",
+ TriggerSource: "cron",
+ IsOfficialDaily: true,
+ })
+ if err != nil {
+ t.Fatalf("resolveReportOutputPaths returned error: %v", err)
+ }
+ if official.MarkdownPath != "reports/daily/daily_report_2026-05-14.md" {
+ t.Fatalf("official markdown path = %q", official.MarkdownPath)
+ }
+ if official.HTMLPath != "reports/daily/html/daily_report_2026-05-14.html" {
+ t.Fatalf("official html path = %q", official.HTMLPath)
+ }
+
+ manual, err := resolveReportOutputPaths("2026-05-14", ReportRunContext{
+ RunKind: "manual",
+ TriggerSource: "pipeline",
+ IsOfficialDaily: false,
+ })
+ if err != nil {
+ t.Fatalf("resolveReportOutputPaths returned error: %v", err)
+ }
+ if manual.MarkdownPath == official.MarkdownPath {
+ t.Fatalf("manual run should not overwrite official markdown path: %q", manual.MarkdownPath)
+ }
+ if manual.HTMLPath == official.HTMLPath {
+ t.Fatalf("manual run should not overwrite official html path: %q", manual.HTMLPath)
+ }
+ if !strings.Contains(manual.MarkdownPath, "reports/ad_hoc/") {
+ t.Fatalf("manual markdown path should be isolated, got %q", manual.MarkdownPath)
+ }
+ if !strings.Contains(manual.HTMLPath, "reports/ad_hoc/") {
+ t.Fatalf("manual html path should be isolated, got %q", manual.HTMLPath)
+ }
+}
+
func TestResolveSignatureAuditReportConfigDefaults(t *testing.T) {
t.Setenv("REPORT_SIGNATURE_AUDIT_WINDOW", "")
t.Setenv("REPORT_SIGNATURE_AUDIT_CHANGED_THRESHOLD", "")
diff --git a/scripts/generate_video_digest.go b/scripts/generate_video_digest.go
index 85df283..50f57c3 100644
--- a/scripts/generate_video_digest.go
+++ b/scripts/generate_video_digest.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/git_commit_status_report.sh b/scripts/git_commit_status_report.sh
new file mode 100755
index 0000000..750c933
--- /dev/null
+++ b/scripts/git_commit_status_report.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+LABEL="${1:-worktree}"
+STATUS_OUTPUT="$(git status --short 2>/dev/null || true)"
+BLOCKER_THRESHOLD="${WORKTREE_BLOCKER_THRESHOLD:-50}"
+
+if [[ -z "$STATUS_OUTPUT" ]]; then
+ echo "WORKTREE_STATUS label=${LABEL} state=clean tracked_modified=0 untracked=0 total=0 commit_hint=none severity=normal"
+ exit 0
+fi
+
+TRACKED_MODIFIED=$(printf '%s\n' "$STATUS_OUTPUT" | awk 'NF && $1 !~ /^\?\?/ { count++ } END { print count+0 }')
+UNTRACKED=$(printf '%s\n' "$STATUS_OUTPUT" | awk '$1 ~ /^\?\?/ { count++ } END { print count+0 }')
+TOTAL=$((TRACKED_MODIFIED + UNTRACKED))
+SEVERITY="warning"
+if [[ "$TOTAL" -gt "$BLOCKER_THRESHOLD" ]]; then
+ SEVERITY="blocker"
+fi
+
+echo "WORKTREE_STATUS label=${LABEL} state=dirty tracked_modified=${TRACKED_MODIFIED} untracked=${UNTRACKED} total=${TOTAL} commit_hint=needed severity=${SEVERITY}"
diff --git a/scripts/git_commit_status_test.sh b/scripts/git_commit_status_test.sh
new file mode 100644
index 0000000..b6f3a77
--- /dev/null
+++ b/scripts/git_commit_status_test.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+CLEAN_REPO="$TMP_DIR/repo"
+mkdir -p "$CLEAN_REPO"
+cd "$CLEAN_REPO"
+git init -q
+printf 'tracked\n' > tracked.txt
+git add tracked.txt
+git config user.email test@example.com
+git config user.name test
+
+git commit -qm 'init'
+
+CLEAN_OUTPUT="$(bash /home/long/project/llm-intelligence/scripts/git_commit_status_report.sh clean)"
+printf '%s' "$CLEAN_OUTPUT" | grep -q 'state=clean'
+printf '%s' "$CLEAN_OUTPUT" | grep -q 'severity=normal'
+
+printf 'dirty\n' >> tracked.txt
+DIRTY_OUTPUT="$(bash /home/long/project/llm-intelligence/scripts/git_commit_status_report.sh dirty)"
+printf '%s' "$DIRTY_OUTPUT" | grep -q 'state=dirty'
+printf '%s' "$DIRTY_OUTPUT" | grep -q 'tracked_modified=1'
+printf '%s' "$DIRTY_OUTPUT" | grep -q 'commit_hint=needed'
+printf '%s' "$DIRTY_OUTPUT" | grep -q 'severity=warning'
+
+for i in $(seq 1 60); do
+ printf 'u%d\n' "$i" > "untracked_$i.txt"
+done
+BLOCKER_OUTPUT="$(WORKTREE_BLOCKER_THRESHOLD=50 bash /home/long/project/llm-intelligence/scripts/git_commit_status_report.sh blocker)"
+printf '%s' "$BLOCKER_OUTPUT" | grep -q 'severity=blocker'
diff --git a/scripts/import_360_pricing.go b/scripts/import_360_pricing.go
index b99c52a..4f6e8cb 100644
--- a/scripts/import_360_pricing.go
+++ b/scripts/import_360_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_aliyun_subscription.go b/scripts/import_aliyun_subscription.go
index fdfd91d..b4b3636 100644
--- a/scripts/import_aliyun_subscription.go
+++ b/scripts/import_aliyun_subscription.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_azure_openai_pricing.go b/scripts/import_azure_openai_pricing.go
index 7308225..e94cb99 100644
--- a/scripts/import_azure_openai_pricing.go
+++ b/scripts/import_azure_openai_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_baichuan_pricing.go b/scripts/import_baichuan_pricing.go
index ec17452..53846d5 100644
--- a/scripts/import_baichuan_pricing.go
+++ b/scripts/import_baichuan_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_baidu_subscription.go b/scripts/import_baidu_subscription.go
index 090455a..91c02a6 100644
--- a/scripts/import_baidu_subscription.go
+++ b/scripts/import_baidu_subscription.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_bedrock_pricing.go b/scripts/import_bedrock_pricing.go
index 729fe4a..0a0ffcf 100644
--- a/scripts/import_bedrock_pricing.go
+++ b/scripts/import_bedrock_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_bytedance_data.go b/scripts/import_bytedance_data.go
index 5a3e1cb..6e14cd1 100644
--- a/scripts/import_bytedance_data.go
+++ b/scripts/import_bytedance_data.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_bytedance_pricing.go b/scripts/import_bytedance_pricing.go
index 24cd743..cf65215 100644
--- a/scripts/import_bytedance_pricing.go
+++ b/scripts/import_bytedance_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_bytedance_subscription.go b/scripts/import_bytedance_subscription.go
index c14bd83..d552633 100644
--- a/scripts/import_bytedance_subscription.go
+++ b/scripts/import_bytedance_subscription.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_catalog_seed_verification.go b/scripts/import_catalog_seed_verification.go
index 443e8f6..26b5db5 100644
--- a/scripts/import_catalog_seed_verification.go
+++ b/scripts/import_catalog_seed_verification.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_cloudflare_pricing.go b/scripts/import_cloudflare_pricing.go
index 3a05285..dd15542 100644
--- a/scripts/import_cloudflare_pricing.go
+++ b/scripts/import_cloudflare_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_coreshub_pricing.go b/scripts/import_coreshub_pricing.go
index b0b7a29..7d9edd9 100644
--- a/scripts/import_coreshub_pricing.go
+++ b/scripts/import_coreshub_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_ctyun_subscription.go b/scripts/import_ctyun_subscription.go
index 2dabd36..0c6ae03 100644
--- a/scripts/import_ctyun_subscription.go
+++ b/scripts/import_ctyun_subscription.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_cucloud_catalog.go b/scripts/import_cucloud_catalog.go
index 2aa1af3..8be4363 100644
--- a/scripts/import_cucloud_catalog.go
+++ b/scripts/import_cucloud_catalog.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_cucloud_pricing.go b/scripts/import_cucloud_pricing.go
index 15fa015..0a8d8f1 100644
--- a/scripts/import_cucloud_pricing.go
+++ b/scripts/import_cucloud_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_huawei_maas_pricing.go b/scripts/import_huawei_maas_pricing.go
index 9b82aac..20cda7e 100644
--- a/scripts/import_huawei_maas_pricing.go
+++ b/scripts/import_huawei_maas_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_huawei_package.go b/scripts/import_huawei_package.go
index 22b737a..073bda9 100644
--- a/scripts/import_huawei_package.go
+++ b/scripts/import_huawei_package.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_hunyuan_pricing.go b/scripts/import_hunyuan_pricing.go
index 5869c89..b8069ed 100644
--- a/scripts/import_hunyuan_pricing.go
+++ b/scripts/import_hunyuan_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_lingyiwanwu_pricing.go b/scripts/import_lingyiwanwu_pricing.go
index 23425b9..614b901 100644
--- a/scripts/import_lingyiwanwu_pricing.go
+++ b/scripts/import_lingyiwanwu_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_manual_subscription_seed.go b/scripts/import_manual_subscription_seed.go
index 280c404..e6ccd26 100644
--- a/scripts/import_manual_subscription_seed.go
+++ b/scripts/import_manual_subscription_seed.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_minimax_subscription.go b/scripts/import_minimax_subscription.go
index 8d252ee..09bd5bc 100644
--- a/scripts/import_minimax_subscription.go
+++ b/scripts/import_minimax_subscription.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_mobile_cloud_catalog.go b/scripts/import_mobile_cloud_catalog.go
index ce20da3..d352f87 100644
--- a/scripts/import_mobile_cloud_catalog.go
+++ b/scripts/import_mobile_cloud_catalog.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_mobile_cloud_pricing.go b/scripts/import_mobile_cloud_pricing.go
index d869f14..f50ef59 100644
--- a/scripts/import_mobile_cloud_pricing.go
+++ b/scripts/import_mobile_cloud_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_perplexity_pricing.go b/scripts/import_perplexity_pricing.go
index e5c01dd..956919b 100644
--- a/scripts/import_perplexity_pricing.go
+++ b/scripts/import_perplexity_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_phase2_data.go b/scripts/import_phase2_data.go
index 5e0c98e..21e240e 100644
--- a/scripts/import_phase2_data.go
+++ b/scripts/import_phase2_data.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_plan_catalog.go b/scripts/import_plan_catalog.go
index 96bfc1b..2767aad 100644
--- a/scripts/import_plan_catalog.go
+++ b/scripts/import_plan_catalog.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_ppio_pricing.go b/scripts/import_ppio_pricing.go
index 4a50bef..5ca6af3 100644
--- a/scripts/import_ppio_pricing.go
+++ b/scripts/import_ppio_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_qwen_pricing.go b/scripts/import_qwen_pricing.go
index 04f7730..6729c1f 100644
--- a/scripts/import_qwen_pricing.go
+++ b/scripts/import_qwen_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_sensenova_pricing.go b/scripts/import_sensenova_pricing.go
index 4d674f0..3fc0cae 100644
--- a/scripts/import_sensenova_pricing.go
+++ b/scripts/import_sensenova_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
@@ -46,6 +46,8 @@ var (
sensenovaOverviewCardPattern = regexp.MustCompile(`(?s)]*>([^<]+)
.*?调用次数限制
]*>每5小时([0-9]+)次
.*?MODEL ID]*>([^<]+)`)
sensenovaModelsScriptPattern = regexp.MustCompile(`src="([^"]+/_next/static/chunks/[^"]+\.js|/_next/static/chunks/[^"]+\.js)"`)
sensenovaPricingZeroPattern = regexp.MustCompile(`(?s)"pricing"\s*:\s*\{\s*"prompt"\s*:\s*"0"\s*,\s*"completion"\s*:\s*"0"\s*,\s*"image"\s*:\s*"0"\s*,\s*"request"\s*:\s*"0"`)
+ sensenovaOverviewTableRowPattern = regexp.MustCompile(`(?s)]*>\s*| ]*>([^<]+) | \s*]*>.*?]*>([^<]+).*? | \s*]*>每5小时([0-9]+)次 | \s*]*>([^<]+) | \s*
`)
+
)
func main() {
@@ -255,6 +257,9 @@ func parseSensenovaPricingCatalog(fixture sensenovaPricingFixture) ([]officialPr
}
matches := sensenovaOverviewCardPattern.FindAllStringSubmatch(fixture.DocsHTML, -1)
+ if len(matches) == 0 {
+ matches = sensenovaOverviewTableRowPattern.FindAllStringSubmatch(fixture.DocsHTML, -1)
+ }
if len(matches) == 0 {
return nil, fmt.Errorf("unexpected sensenova docs content: no model overview cards parsed")
}
@@ -263,11 +268,7 @@ func parseSensenovaPricingCatalog(fixture sensenovaPricingFixture) ([]officialPr
records := make([]officialPricingRecord, 0, len(matches))
seenModelIDs := make(map[string]struct{}, len(matches))
for _, match := range matches {
- if len(match) != 4 {
- continue
- }
- modelName := strings.TrimSpace(match[1])
- modelID := strings.TrimSpace(match[3])
+ modelName, modelID := normalizeSensenovaOverviewMatch(match)
if modelName == "" || modelID == "" {
continue
}
@@ -376,3 +377,25 @@ func sensenovaModality(modelID string, section string) string {
return "text"
}
}
+
+func normalizeSensenovaOverviewMatch(match []string) (string, string) {
+ if len(match) < 3 {
+ return "", ""
+ }
+ modelName := strings.TrimSpace(match[1])
+ for i := len(match) - 1; i >= 2; i-- {
+ candidate := strings.TrimSpace(match[i])
+ if looksLikeSensenovaModelID(candidate) {
+ return modelName, candidate
+ }
+ }
+ return modelName, ""
+}
+
+func looksLikeSensenovaModelID(value string) bool {
+ value = strings.ToLower(strings.TrimSpace(value))
+ if value == "" {
+ return false
+ }
+ return strings.Contains(value, "-") && (strings.HasPrefix(value, "sensenova") || strings.HasPrefix(value, "deepseek"))
+}
diff --git a/scripts/import_sensenova_pricing_test.go b/scripts/import_sensenova_pricing_test.go
index 18832d1..9d09b91 100644
--- a/scripts/import_sensenova_pricing_test.go
+++ b/scripts/import_sensenova_pricing_test.go
@@ -44,6 +44,39 @@ func TestParseSensenovaPricingCatalogBuildsRecords(t *testing.T) {
}
}
+func TestParseSensenovaPricingCatalogSupportsModelsOverviewTable(t *testing.T) {
+ raw, err := os.ReadFile(filepath.Join("testdata", "sensenova_pricing_sample.txt"))
+ if err != nil {
+ t.Fatalf("读取 fixture 失败: %v", err)
+ }
+ fixture, err := splitSensenovaFixture(string(raw))
+ if err != nil {
+ t.Fatalf("splitSensenovaFixture 返回错误: %v", err)
+ }
+
+ fixture.DocsHTML = `GET https://token.sensenova.cn/v1/models
+ {"pricing":{"prompt":"0","completion":"0","image":"0","request":"0"}}
+
+ | SenseNova 6.7 Flash-Lite | sensenova-6.7-flash-lite | 每5小时1500次 | 面向真实工作流的轻量多模态智能体模型,支持文本对话与图像输入理解 |
+ | SenseNova U1 Fast | sensenova-u1-fast | 每5小时1500次 | 基于 SenseNova U1 的加速版本,专供信息图(Infographics)生成 |
+ | DeepSeek V4 Flash | deepseek-v4-flash | 每5小时500次 | DeepSeek 高性能对话模型,支持思考/非思考模式、256K 上下文、工具调用 |
+
+
+ POST https://token.sensenova.cn/v1/images/generations
+ `
+
+ records, err := parseSensenovaPricingCatalog(fixture)
+ if err != nil {
+ t.Fatalf("parseSensenovaPricingCatalog 返回错误: %v", err)
+ }
+ if len(records) != 3 {
+ t.Fatalf("期望 3 条商汤价格记录,实际 %d", len(records))
+ }
+ if records[2].ModelID != "sensenova-deepseek-v4-flash" {
+ t.Fatalf("DeepSeek 记录未从新模型总览表解析出来: %+v", records[2])
+ }
+}
+
func TestRunSensenovaPricingImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runSensenovaPricingImport(sensenovaPricingImportConfig{
diff --git a/scripts/import_siliconflow_pricing.go b/scripts/import_siliconflow_pricing.go
index 3612b8b..3e249a3 100644
--- a/scripts/import_siliconflow_pricing.go
+++ b/scripts/import_siliconflow_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_tencent_subscription.go b/scripts/import_tencent_subscription.go
index c70df5b..16aff3d 100644
--- a/scripts/import_tencent_subscription.go
+++ b/scripts/import_tencent_subscription.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_ucloud_pricing.go b/scripts/import_ucloud_pricing.go
index 37c363e..01bf2cf 100644
--- a/scripts/import_ucloud_pricing.go
+++ b/scripts/import_ucloud_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_vertex_pricing.go b/scripts/import_vertex_pricing.go
index 9073537..144e47f 100644
--- a/scripts/import_vertex_pricing.go
+++ b/scripts/import_vertex_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_xfyun_pricing.go b/scripts/import_xfyun_pricing.go
index 3a3f7d7..4f09f30 100644
--- a/scripts/import_xfyun_pricing.go
+++ b/scripts/import_xfyun_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
@@ -25,6 +25,8 @@ type xfyunPricingImportConfig struct {
Timeout time.Duration
}
+var xfyunPricingTextPattern = regexp.MustCompile(`(?m)^\s*([A-Za-z0-9+/.-]+模型)\s*\n?\s*([0-9]+(?:\.[0-9]+)?)\s*元/百万tokens\s*$`)
+
var xfyunPricingCardPattern = regexp.MustCompile(`(?s)([^<]+)
([0-9]+(?:\.[0-9]+)?)元/百万tokens`)
func main() {
@@ -38,7 +40,8 @@ func main() {
flag.StringVar(&url, "url", defaultXfyunPricingURL, "讯飞官方价格页")
flag.StringVar(&fixture, "fixture", "", "讯飞价格样例文件")
flag.BoolVar(&dryRun, "dry-run", false, "仅解析并打印摘要,不写入数据库")
- flag.IntVar(&timeoutSeconds, "timeout", 30, "请求超时(秒)")
+ flag.IntVar(&timeoutSeconds, "timeout", 45, "请求超时(秒)")
+
flag.Parse()
cfg := xfyunPricingImportConfig{URL: url, Fixture: fixture, DryRun: dryRun, Timeout: time.Duration(timeoutSeconds) * time.Second}
@@ -127,6 +130,7 @@ func fetchXfyunPricingPageWithChromium(url string, timeout time.Duration) (strin
"--headless",
"--no-sandbox",
"--disable-gpu",
+ "--virtual-time-budget=8000",
"--dump-dom",
url,
)
@@ -155,6 +159,9 @@ func lookupChromiumBinary() (string, error) {
func parseXfyunPricingCatalog(raw string) ([]officialPricingRecord, error) {
matches := xfyunPricingCardPattern.FindAllStringSubmatch(raw, -1)
+ if len(matches) == 0 {
+ matches = xfyunPricingTextPattern.FindAllStringSubmatch(cleanXfyunPricingText(raw), -1)
+ }
if len(matches) == 0 {
return nil, fmt.Errorf("unexpected xfyun pricing content: no pricing cards found")
}
@@ -201,6 +208,39 @@ func parseXfyunPricingCatalog(raw string) ([]officialPricingRecord, error) {
return records, nil
}
+func cleanXfyunPricingText(raw string) string {
+ replacer := strings.NewReplacer(
+ "
", "\n",
+ "
", "\n",
+ "
", "\n",
+ "
", "\n",
+ "", "\n",
+ )
+ text := replacer.Replace(raw)
+ text = stripHTMLTags(text)
+ text = strings.ReplaceAll(text, "\u00a0", " ")
+ return text
+}
+
+func stripHTMLTags(raw string) string {
+ var b strings.Builder
+ b.Grow(len(raw))
+ inTag := false
+ for _, r := range raw {
+ switch r {
+ case '<':
+ inTag = true
+ case '>':
+ inTag = false
+ default:
+ if !inTag {
+ b.WriteRune(r)
+ }
+ }
+ }
+ return b.String()
+}
+
func xfyunCanonicalModelName(title string) string {
switch strings.TrimSpace(title) {
case "X2/X1.5模型":
diff --git a/scripts/import_xfyun_pricing_test.go b/scripts/import_xfyun_pricing_test.go
index 157b88b..a0f2e40 100644
--- a/scripts/import_xfyun_pricing_test.go
+++ b/scripts/import_xfyun_pricing_test.go
@@ -37,6 +37,26 @@ func TestParseXfyunPricingCatalogBuildsRecords(t *testing.T) {
}
}
+func TestParseXfyunPricingCatalogSupportsRenderedTextFallback(t *testing.T) {
+ raw := `
+
+
+
+
+ `
+
+ records, err := parseXfyunPricingCatalog(raw)
+ if err != nil {
+ t.Fatalf("parseXfyunPricingCatalog 返回错误: %v", err)
+ }
+ if len(records) != 4 {
+ t.Fatalf("期望 fallback 解析 4 条讯飞价格记录,实际 %d", len(records))
+ }
+ if records[2].ModelName != "Spark Pro" || records[2].InputPrice != 5 {
+ t.Fatalf("Spark Pro fallback 解析错误: %+v", records[2])
+ }
+}
+
func TestRunXfyunPricingImportDryRunPrintsSummary(t *testing.T) {
var out bytes.Buffer
err := runXfyunPricingImport(xfyunPricingImportConfig{
diff --git a/scripts/import_youdao_pricing.go b/scripts/import_youdao_pricing.go
index ece1c7f..ed1b05a 100644
--- a/scripts/import_youdao_pricing.go
+++ b/scripts/import_youdao_pricing.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_zhipu_coding_plan.go b/scripts/import_zhipu_coding_plan.go
index 5c17b10..6e8b511 100644
--- a/scripts/import_zhipu_coding_plan.go
+++ b/scripts/import_zhipu_coding_plan.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/import_zhipu_data.go b/scripts/import_zhipu_data.go
index 57f4f94..adf697c 100644
--- a/scripts/import_zhipu_data.go
+++ b/scripts/import_zhipu_data.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/list_testable_script_entries.sh b/scripts/list_testable_script_entries.sh
new file mode 100755
index 0000000..5db9ba8
--- /dev/null
+++ b/scripts/list_testable_script_entries.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+python3 - <<'PY'
+from pathlib import Path
+import re
+
+root = Path('scripts')
+entries = []
+for path in sorted(root.glob('*.go')):
+ if path.name.endswith('_test.go'):
+ continue
+ text = path.read_text(encoding='utf-8')
+ if 'func main()' not in text:
+ continue
+ build_tag = text.splitlines()[0].strip() if text.splitlines() else ''
+ tests = []
+ candidate_test = path.with_name(path.stem + '_test.go')
+ if candidate_test.exists():
+ tests.append(candidate_test.name)
+ entries.append((path.name, build_tag, ','.join(tests) or '-'))
+
+print(f'SCRIPT_ENTRY_SUMMARY total_entries={len(entries)}')
+for name, build_tag, tests in entries:
+ print(f'{name}\tbuild_tag={build_tag}\ttests={tests}')
+PY
diff --git a/scripts/live_pricing_smoke_runner.go b/scripts/live_pricing_smoke_runner.go
index ad70559..f293aeb 100644
--- a/scripts/live_pricing_smoke_runner.go
+++ b/scripts/live_pricing_smoke_runner.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/materialize_daily_signals.go b/scripts/materialize_daily_signals.go
index f5896ab..eebfae3 100644
--- a/scripts/materialize_daily_signals.go
+++ b/scripts/materialize_daily_signals.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/package_stub.go b/scripts/package_stub.go
new file mode 100644
index 0000000..94f386c
--- /dev/null
+++ b/scripts/package_stub.go
@@ -0,0 +1,5 @@
+package main
+
+// This stub keeps `go test ./scripts` buildable without pulling every CLI entrypoint
+// into the same package compilation unit. Command-specific behavior is verified
+// through file-scoped go test/go run invocations and shell gates.
diff --git a/scripts/perplexity_pricing_signature_guard.go b/scripts/perplexity_pricing_signature_guard.go
index fbd8a4e..660ac34 100644
--- a/scripts/perplexity_pricing_signature_guard.go
+++ b/scripts/perplexity_pricing_signature_guard.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/pipeline_script_includes_signature_query_test.sh b/scripts/pipeline_script_includes_signature_query_test.sh
new file mode 100644
index 0000000..545cb70
--- /dev/null
+++ b/scripts/pipeline_script_includes_signature_query_test.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+python3 - <<'PY'
+from pathlib import Path
+import re
+
+files = [
+ 'scripts/run_real_pipeline.sh',
+ 'scripts/run_daily.sh',
+ 'scripts/run_intel_pipeline.sh',
+ 'scripts/run_intraday_price_watch.sh',
+ 'scripts/run_intraday_discovery_watch.sh',
+]
+pattern = re.compile(r'materialize_daily_signals\.go.*official_import_signature_audit_query_lib\.go|official_import_signature_audit_query_lib\.go.*materialize_daily_signals\.go', re.S)
+missing = [path for path in files if not pattern.search(Path(path).read_text())]
+if missing:
+ raise SystemExit('missing helper include: ' + ', '.join(missing))
+PY
diff --git a/scripts/query_official_import_signature_audit.go b/scripts/query_official_import_signature_audit.go
index b361e7e..9cbad26 100644
--- a/scripts/query_official_import_signature_audit.go
+++ b/scripts/query_official_import_signature_audit.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/report_state_tracking_test.sh b/scripts/report_state_tracking_test.sh
index 2019dd9..53b62e3 100755
--- a/scripts/report_state_tracking_test.sh
+++ b/scripts/report_state_tracking_test.sh
@@ -23,13 +23,14 @@ cleanup
track_report_state "$DATABASE_URL" "$TEST_DATE" generated 123 'official summary' 'reports/daily/daily_report_2099-01-01.md' '' scheduled cron true >/dev/null
+
OFFICIAL_ROW="$(psql "$DATABASE_URL" -Atqc "SELECT status || '|' || run_kind || '|' || trigger_source || '|' || is_official_daily::text FROM daily_report WHERE report_date = DATE '$TEST_DATE';")"
[[ "$OFFICIAL_ROW" == "generated|scheduled|cron|true" ]]
OFFICIAL_RUN_COUNT="$(psql "$DATABASE_URL" -Atqc "SELECT count(*) FROM report_runs WHERE report_date = DATE '$TEST_DATE';")"
[[ "$OFFICIAL_RUN_COUNT" == "1" ]]
-track_report_state "$DATABASE_URL" "$TEST_DATE" failed '' '' '' 'manual failed' manual pipeline false >/dev/null
+track_report_state "$DATABASE_URL" "$TEST_DATE" failed '' '' "$(report_ad_hoc_markdown_path "$TEST_DATE" manual pipeline)" 'manual failed' manual pipeline false >/dev/null
MANUAL_ROW="$(psql "$DATABASE_URL" -Atqc "SELECT status || '|' || run_kind || '|' || trigger_source || '|' || is_official_daily::text FROM daily_report WHERE report_date = DATE '$TEST_DATE';")"
[[ "$MANUAL_ROW" == "generated|scheduled|cron|true" ]]
diff --git a/scripts/report_utils.sh b/scripts/report_utils.sh
index 1ea2069..d83efff 100644
--- a/scripts/report_utils.sh
+++ b/scripts/report_utils.sh
@@ -5,11 +5,11 @@ report_date_value() {
}
report_output_dir() {
- printf '%s\n' "reports/daily"
+ printf '%s\n' "$(report_output_root)"
}
report_html_dir() {
- printf '%s\n' "$(report_output_dir)/html"
+ printf '%s\n' "$(report_html_root)"
}
report_markdown_path() {
@@ -42,6 +42,38 @@ report_archive_html_path() {
printf '%s\n' "$(report_archive_dir "$report_date")/daily_report_${report_date}.html"
}
+report_output_root() {
+ printf '%s\n' "${REPORT_OUTPUT_DIR:-reports/daily}"
+}
+
+report_html_root() {
+ printf '%s\n' "$(report_output_root)/html"
+}
+
+report_ad_hoc_dir() {
+ local report_date run_kind trigger_source
+ report_date="$(report_date_value "${1:-}")"
+ run_kind="${2:-manual}"
+ trigger_source="${3:-cli}"
+ printf '%s\n' "reports/ad_hoc/${report_date}/${run_kind}/${trigger_source}"
+}
+
+report_ad_hoc_markdown_path() {
+ local report_date run_kind trigger_source
+ report_date="$(report_date_value "${1:-}")"
+ run_kind="${2:-manual}"
+ trigger_source="${3:-cli}"
+ printf '%s\n' "$(report_ad_hoc_dir "$report_date" "$run_kind" "$trigger_source")/daily_report_${report_date}.md"
+}
+
+report_ad_hoc_html_path() {
+ local report_date run_kind trigger_source
+ report_date="$(report_date_value "${1:-}")"
+ run_kind="${2:-manual}"
+ trigger_source="${3:-cli}"
+ printf '%s\n' "$(report_ad_hoc_dir "$report_date" "$run_kind" "$trigger_source")/html/daily_report_${report_date}.html"
+}
+
archive_report_artifacts() {
local report_date markdown_path html_path archive_dir
report_date="$(report_date_value "${1:-}")"
diff --git a/scripts/review/backlog_blocker_freshness_guard.sh b/scripts/review/backlog_blocker_freshness_guard.sh
new file mode 100755
index 0000000..314d363
--- /dev/null
+++ b/scripts/review/backlog_blocker_freshness_guard.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BACKLOG_PATH="${1:?backlog path required}"
+
+python3 - <<'PY' "$BACKLOG_PATH"
+from pathlib import Path
+import re
+import sys
+
+text = Path(sys.argv[1]).read_text(encoding='utf-8')
+lines = text.splitlines()
+inside = False
+current_rows = []
+for line in lines:
+ if line.startswith('## 当前未修复问题速查表'):
+ inside = True
+ continue
+ if inside and line.startswith('---'):
+ break
+ if inside and line.startswith('|') and not line.startswith('| #') and not line.startswith('|---'):
+ current_rows.append(line)
+
+matches = re.findall(r'freshness_hint=same-day-blocker-switch old=([^ ]+) new=([^\n ]+)', text)
+for old, new in matches:
+ old_variants = {
+ old.lower(),
+ old.replace('-', ' ').lower(),
+ old.replace(' ', '-').lower(),
+ }
+ for row in current_rows:
+ row_lower = row.lower()
+ if any(variant and variant in row_lower for variant in old_variants):
+ print('stale blocker still present in current table', file=sys.stderr)
+ raise SystemExit(1)
+print('BACKLOG_BLOCKER_FRESHNESS_GUARD: PASS')
+PY
diff --git a/scripts/review/backlog_blocker_freshness_guard_test.sh b/scripts/review/backlog_blocker_freshness_guard_test.sh
new file mode 100644
index 0000000..d2ff231
--- /dev/null
+++ b/scripts/review/backlog_blocker_freshness_guard_test.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_BACKLOG="$TMP_DIR/backlog.md"
+cat > "$BAD_BACKLOG" <<'EOF'
+## 当前未修复问题速查表(截至 2026-05-29 15:10)
+| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
+|---|------|--------|----------|----------|----------|
+| 46 | sensenova-live smoke FAIL | P1 | 05-28 15:10 | ❌ 未修复 | 1 次 |
+| 48 | xfyun-live smoke FAIL 导致 live_run SKIP 传导链 | P1 | 05-29 15:10 | ❌ 未修复 | 首次暴露 |
+---
+### 2026-05-29 15:10(afternoon-review cron)
+- freshness_hint=same-day-blocker-switch old=sensenova-live new=xfyun-live
+EOF
+
+set +e
+bash scripts/review/backlog_blocker_freshness_guard.sh "$BAD_BACKLOG" >/tmp/backlog_blocker_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'stale blocker still present in current table' /tmp/backlog_blocker_bad.out
+
+GOOD_BACKLOG="$TMP_DIR/backlog-good.md"
+cat > "$GOOD_BACKLOG" <<'EOF'
+## 当前未修复问题速查表(截至 2026-05-29 15:10)
+| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
+|---|------|--------|----------|----------|----------|
+| 48 | xfyun-live smoke FAIL 导致 live_run SKIP 传导链 | P1 | 05-29 15:10 | ❌ 未修复 | 首次暴露 |
+---
+### 2026-05-29 15:10(afternoon-review cron)
+- freshness_hint=same-day-blocker-switch old=sensenova-live new=xfyun-live
+EOF
+
+bash scripts/review/backlog_blocker_freshness_guard.sh "$GOOD_BACKLOG" >/tmp/backlog_blocker_good.out 2>&1
+grep -q 'BACKLOG_BLOCKER_FRESHNESS_GUARD: PASS' /tmp/backlog_blocker_good.out
diff --git a/scripts/review/backlog_current_freshness_guard.sh b/scripts/review/backlog_current_freshness_guard.sh
new file mode 100755
index 0000000..3e4b9bc
--- /dev/null
+++ b/scripts/review/backlog_current_freshness_guard.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BACKLOG_PATH="${1:?backlog path required}"
+NOW_RAW="${LLM_NOW:-$(date '+%Y-%m-%d %H:%M')}"
+
+python3 - <<'PY' "$BACKLOG_PATH" "$NOW_RAW"
+from pathlib import Path
+from datetime import datetime
+import re
+import sys
+
+text = Path(sys.argv[1]).read_text(encoding='utf-8')
+now = datetime.strptime(sys.argv[2], '%Y-%m-%d %H:%M')
+match = re.search(r'## 当前未修复问题速查表(截至 ([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}))', text)
+if not match:
+ print('missing current table timestamp', file=sys.stderr)
+ raise SystemExit(1)
+seen = datetime.strptime(match.group(1), '%Y-%m-%d %H:%M')
+age_minutes = int((now - seen).total_seconds() // 60)
+if age_minutes > 180:
+ print('stale current truth snapshot', file=sys.stderr)
+ raise SystemExit(1)
+print(f'BACKLOG_FRESHNESS age_minutes={age_minutes} status=fresh')
+PY
diff --git a/scripts/review/backlog_current_freshness_guard_test.sh b/scripts/review/backlog_current_freshness_guard_test.sh
new file mode 100644
index 0000000..e904391
--- /dev/null
+++ b/scripts/review/backlog_current_freshness_guard_test.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+STALE_FILE="$TMP_DIR/stale-backlog.md"
+cat > "$STALE_FILE" <<'EOF'
+## 当前未修复问题速查表(截至 2026-05-01 09:30)
+| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
+|---|------|--------|----------|----------|----------|
+| 25 | stale current truth | P2 | 05-16 09:30 | ❌ 未修复 | 2 次 |
+---
+EOF
+
+set +e
+LLM_NOW='2026-05-29 12:00' bash scripts/review/backlog_current_freshness_guard.sh "$STALE_FILE" >/tmp/backlog_freshness_bad.out 2>&1
+STALE_RC=$?
+set -e
+[[ "$STALE_RC" -ne 0 ]]
+grep -q 'stale current truth snapshot' /tmp/backlog_freshness_bad.out
+
+FRESH_FILE="$TMP_DIR/fresh-backlog.md"
+cat > "$FRESH_FILE" <<'EOF'
+## 当前未修复问题速查表(截至 2026-05-29 11:55)
+| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
+|---|------|--------|----------|----------|----------|
+| 25 | stale current truth | P2 | 05-16 09:30 | ❌ 未修复 | 2 次 |
+---
+EOF
+
+LLM_NOW='2026-05-29 12:00' bash scripts/review/backlog_current_freshness_guard.sh "$FRESH_FILE" >/tmp/backlog_freshness_good.out 2>&1
+grep -q 'BACKLOG_FRESHNESS age_minutes=5 status=fresh' /tmp/backlog_freshness_good.out
diff --git a/scripts/review/backlog_current_table_guard.sh b/scripts/review/backlog_current_table_guard.sh
new file mode 100755
index 0000000..9015031
--- /dev/null
+++ b/scripts/review/backlog_current_table_guard.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BACKLOG_PATH="${1:?backlog path required}"
+
+python3 - <<'PY' "$BACKLOG_PATH"
+from pathlib import Path
+import sys
+
+text = Path(sys.argv[1]).read_text(encoding='utf-8')
+lines = text.splitlines()
+inside = False
+rows = []
+for line in lines:
+ if line.startswith('## 当前未修复问题速查表'):
+ inside = True
+ continue
+ if inside and line.startswith('---'):
+ break
+ if inside and line.startswith('|') and not line.startswith('| #') and not line.startswith('|---'):
+ rows.append(line)
+
+resolved = [row for row in rows if '✅ 已修复' in row or '✅ 已完成' in row or '✅ 已恢复' in row]
+print(f'BACKLOG_CURRENT_TABLE rows={len(rows)} resolved_rows={len(resolved)}')
+if resolved:
+ print('resolved rows still present in current table', file=sys.stderr)
+ raise SystemExit(1)
+PY
diff --git a/scripts/review/backlog_current_table_guard_test.sh b/scripts/review/backlog_current_table_guard_test.sh
new file mode 100644
index 0000000..3e5f8b0
--- /dev/null
+++ b/scripts/review/backlog_current_table_guard_test.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_FILE="$TMP_DIR/bad-backlog.md"
+cat > "$BAD_FILE" <<'EOF'
+## 当前未修复问题速查表(截至 2026-05-29 12:00)
+| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
+|---|------|--------|----------|----------|----------|
+| 1 | old resolved item | P1 | 05-01 10:00 | ✅ 已修复 | 1 次 |
+| 2 | still open | P1 | 05-01 11:00 | ❌ 未修复 | 2 次 |
+---
+EOF
+
+set +e
+bash scripts/review/backlog_current_table_guard.sh "$BAD_FILE" >/tmp/backlog_guard_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'resolved rows still present in current table' /tmp/backlog_guard_bad.out
+
+GOOD_FILE="$TMP_DIR/good-backlog.md"
+cat > "$GOOD_FILE" <<'EOF'
+## 当前未修复问题速查表(截至 2026-05-29 12:00)
+| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
+|---|------|--------|----------|----------|----------|
+| 2 | still open | P1 | 05-01 11:00 | ❌ 未修复 | 2 次 |
+| 3 | partial item | P1 | 05-01 12:00 | ⚠️ 部分 | 3 次 |
+---
+EOF
+
+bash scripts/review/backlog_current_table_guard.sh "$GOOD_FILE" >/tmp/backlog_guard_good.out 2>&1
+grep -q 'BACKLOG_CURRENT_TABLE rows=2 resolved_rows=0' /tmp/backlog_guard_good.out
diff --git a/scripts/review/backlog_revocation_guard.sh b/scripts/review/backlog_revocation_guard.sh
new file mode 100755
index 0000000..751e76c
--- /dev/null
+++ b/scripts/review/backlog_revocation_guard.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+BACKLOG_PATH="${1:?backlog path required}"
+
+python3 - <<'PY' "$BACKLOG_PATH"
+from pathlib import Path
+import re
+import sys
+
+text = Path(sys.argv[1]).read_text(encoding='utf-8')
+lines = text.splitlines()
+inside = False
+current_rows = []
+for line in lines:
+ if line.startswith('## 当前未修复问题速查表'):
+ inside = True
+ continue
+ if inside and line.startswith('---'):
+ break
+ if inside and line.startswith('|') and not line.startswith('| #') and not line.startswith('|---'):
+ current_rows.append(line)
+
+resolved_ids = set(re.findall(r'#### 问题 ([0-9]+) 状态更新:已修复(从 current 表移除)', text))
+for issue_id in resolved_ids:
+ for row in current_rows:
+ if row.startswith(f'| {issue_id} |') or row.startswith(f'| {issue_id} |'.replace(' ', '')):
+ print('resolved issue still present in current table', file=sys.stderr)
+ raise SystemExit(1)
+print('BACKLOG_REVOCATION_GUARD: PASS')
+PY
diff --git a/scripts/review/backlog_revocation_guard_test.sh b/scripts/review/backlog_revocation_guard_test.sh
new file mode 100644
index 0000000..d8d71a2
--- /dev/null
+++ b/scripts/review/backlog_revocation_guard_test.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_BACKLOG="$TMP_DIR/backlog.md"
+cat > "$BAD_BACKLOG" <<'EOF'
+## 当前未修复问题速查表(截至 2026-05-29 15:10)
+| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
+|---|------|--------|----------|----------|----------|
+| 33 | 已证伪 blocker 缺少自动降级/撤销机制 | P1 | 05-18 09:30 | ❌ 未修复 | 2 次 |
+---
+#### 问题 33 状态更新:已修复(从 current 表移除)
+- 结论:问题 33 已关闭。
+EOF
+
+set +e
+bash scripts/review/backlog_revocation_guard.sh "$BAD_BACKLOG" >/tmp/backlog_revocation_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'resolved issue still present in current table' /tmp/backlog_revocation_bad.out
+
+GOOD_BACKLOG="$TMP_DIR/backlog-good.md"
+cat > "$GOOD_BACKLOG" <<'EOF'
+## 当前未修复问题速查表(截至 2026-05-29 15:10)
+| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
+|---|------|--------|----------|----------|----------|
+| 34 | 局部 smoke 已通过后缺少全局 blocker 切换提示 | P1 | 05-18 15:10 | ❌ 未修复 | 1 次 |
+---
+#### 问题 33 状态更新:已修复(从 current 表移除)
+- 结论:问题 33 已关闭。
+EOF
+
+bash scripts/review/backlog_revocation_guard.sh "$GOOD_BACKLOG" >/tmp/backlog_revocation_good.out 2>&1
+grep -q 'BACKLOG_REVOCATION_GUARD: PASS' /tmp/backlog_revocation_good.out
diff --git a/scripts/review/blocker_switch_guard.sh b/scripts/review/blocker_switch_guard.sh
new file mode 100755
index 0000000..33a11c0
--- /dev/null
+++ b/scripts/review/blocker_switch_guard.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+REPORT_PATH="${1:?review file required}"
+CONTENT="$(cat "$REPORT_PATH")"
+
+if [[ "$CONTENT" == *'替换'* ]]; then
+ if [[ "$CONTENT" != *'freshness_hint=same-day-blocker-switch'* ]]; then
+ echo "missing blocker switch freshness hint" >&2
+ exit 1
+ fi
+fi
+
+echo "BLOCKER_SWITCH_GUARD: PASS"
diff --git a/scripts/review/blocker_switch_guard_test.sh b/scripts/review/blocker_switch_guard_test.sh
new file mode 100644
index 0000000..5d941e5
--- /dev/null
+++ b/scripts/review/blocker_switch_guard_test.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_FILE="$TMP_DIR/review-switch.txt"
+cat > "$BAD_FILE" <<'EOF'
+### 2026-05-29 15:10(afternoon-review cron)
+- xfyun-live smoke FAIL 替换 sensenova-live
+EOF
+
+set +e
+bash scripts/review/blocker_switch_guard.sh "$BAD_FILE" >/tmp/blocker_switch_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'missing blocker switch freshness hint' /tmp/blocker_switch_bad.out
+
+GOOD_FILE="$TMP_DIR/review-switch-good.txt"
+cat > "$GOOD_FILE" <<'EOF'
+### 2026-05-29 15:10(afternoon-review cron)
+- xfyun-live smoke FAIL 替换 sensenova-live
+- freshness_hint=same-day-blocker-switch old=sensenova-live new=xfyun-live
+EOF
+
+bash scripts/review/blocker_switch_guard.sh "$GOOD_FILE" >/tmp/blocker_switch_good.out 2>&1
+grep -q 'BLOCKER_SWITCH_GUARD: PASS' /tmp/blocker_switch_good.out
diff --git a/scripts/review/current_row_revocation_guard.sh b/scripts/review/current_row_revocation_guard.sh
new file mode 100755
index 0000000..7551001
--- /dev/null
+++ b/scripts/review/current_row_revocation_guard.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROW_FILE="${1:?row file required}"
+ROW_CONTENT="$(cat "$ROW_FILE")"
+
+if [[ "$ROW_CONTENT" == *'✅ 已修复'* || "$ROW_CONTENT" == *'✅ 已完成'* || "$ROW_CONTENT" == *'✅ 已恢复'* ]]; then
+ echo "resolved current row should be revoked from current table" >&2
+ exit 1
+fi
+
+echo "CURRENT_ROW_REVOCATION_GUARD: PASS"
diff --git a/scripts/review/global_blocker_switch_capture_test.sh b/scripts/review/global_blocker_switch_capture_test.sh
new file mode 100644
index 0000000..d5478a1
--- /dev/null
+++ b/scripts/review/global_blocker_switch_capture_test.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_FILE="$(mktemp)"
+trap 'rm -f "$TMP_FILE"' EXIT
+
+bash scripts/verify_phase6.sh >"$TMP_FILE" 2>&1 || true
+bash scripts/review/global_blocker_switch_guard.sh "$TMP_FILE"
+grep -q '^BLOCKER_SWITCH class=' "$TMP_FILE"
diff --git a/scripts/review/global_blocker_switch_guard.sh b/scripts/review/global_blocker_switch_guard.sh
new file mode 100755
index 0000000..ee93b4c
--- /dev/null
+++ b/scripts/review/global_blocker_switch_guard.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+STATUS_FILE="${1:?phase status file required}"
+CONTENT="$(cat "$STATUS_FILE")"
+
+if [[ "$CONTENT" == *'importer_smoke_gate_result=PASS'* && "$CONTENT" == *'ROOT_CAUSE class='* && "$CONTENT" != *'ROOT_CAUSE class=none'* ]]; then
+ if [[ "$CONTENT" != *'BLOCKER_SWITCH class=global-blocker-shift'* ]]; then
+ echo "missing global blocker switch hint" >&2
+ exit 1
+ fi
+fi
+
+if [[ "$CONTENT" == *'importer_smoke_gate_result=FAIL'* && "$CONTENT" == *'live_run_result=SKIPPED'* ]]; then
+ if [[ "$CONTENT" != *'BLOCKER_SWITCH class=global-blocker-shift'* ]]; then
+ echo "missing global blocker switch hint" >&2
+ exit 1
+ fi
+fi
+echo "GLOBAL_BLOCKER_SWITCH_GUARD: PASS"
diff --git a/scripts/review/global_blocker_switch_guard_test.sh b/scripts/review/global_blocker_switch_guard_test.sh
new file mode 100644
index 0000000..4c6bdcd
--- /dev/null
+++ b/scripts/review/global_blocker_switch_guard_test.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_FILE="$TMP_DIR/phase6.txt"
+cat > "$BAD_FILE" <<'EOF'
+[PASS] importer_smoke_gate_result=PASS 新增导入器 smoke gate 通过
+[FAIL] live_run_result=FAIL 主链路真实采集失败
+ROOT_CAUSE class=primary_pipeline_failure source=live_run summary=主链路真实采集失败
+EOF
+
+set +e
+bash scripts/review/global_blocker_switch_guard.sh "$BAD_FILE" >/tmp/global_blocker_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'missing global blocker switch hint' /tmp/global_blocker_bad.out
+
+GOOD_FILE="$TMP_DIR/phase6-good.txt"
+cat > "$GOOD_FILE" <<'EOF'
+[PASS] importer_smoke_gate_result=PASS 新增导入器 smoke gate 通过
+[FAIL] live_run_result=FAIL 主链路真实采集失败
+ROOT_CAUSE class=primary_pipeline_failure source=live_run summary=主链路真实采集失败
+BLOCKER_SWITCH class=global-blocker-shift old=importer_smoke_gate new=live_run
+EOF
+
+bash scripts/review/global_blocker_switch_guard.sh "$GOOD_FILE" >/tmp/global_blocker_good.out 2>&1
+grep -q 'GLOBAL_BLOCKER_SWITCH_GUARD: PASS' /tmp/global_blocker_good.out
diff --git a/scripts/review/live_run_classification_test.sh b/scripts/review/live_run_classification_test.sh
new file mode 100644
index 0000000..86326b4
--- /dev/null
+++ b/scripts/review/live_run_classification_test.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+classify_live_run_failure() {
+ local live_tail="${1:-}"
+ local normalized
+ normalized="$(printf '%s' "$live_tail" | tr '[:upper:]' '[:lower:]')"
+ case "$normalized" in
+ *"api key"*|*"database_url"*|*"must provide"*|*"未设置"*|*"permission denied"*|*"role does not exist"*|*"relation does not exist"*)
+ printf '%s\n' 'precondition_missing'
+ ;;
+ *"signature_guard"*|*"unexpected status 403"*|*"unexpected status 502"*|*"unexpected status 503"*|*"unexpected status 504"*|*"no pricing cards found"*|*"no model rows parsed"*|*"no model overview cards parsed"*|*"context deadline exceeded"*|*"client.timeout"*|*"i/o timeout"*|*"tls handshake timeout"*|*"transport closed"*|*"connection reset"*|*"connection refused"*|*"no such host"*)
+ printf '%s\n' 'external_provider_failure'
+ ;;
+ *)
+ printf '%s\n' 'primary_pipeline_failure'
+ ;;
+ esac
+}
+
+[[ "$(classify_live_run_failure 'OPENROUTER_API_KEY 未设置,无法执行真实采集')" == 'precondition_missing' ]]
+[[ "$(classify_live_run_failure 'import_vertex_pricing: read https://cloud.google.com/...: context deadline exceeded (Client.Timeout or context cancellation while reading body)')" == 'external_provider_failure' ]]
+[[ "$(classify_live_run_failure 'insert report_runs failed: duplicate key value violates unique constraint')" == 'primary_pipeline_failure' ]]
diff --git a/scripts/review/partial_output_guard.sh b/scripts/review/partial_output_guard.sh
new file mode 100755
index 0000000..34ecdb6
--- /dev/null
+++ b/scripts/review/partial_output_guard.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+STATUS_FILE="${1:?status file required}"
+CONTENT="$(cat "$STATUS_FILE")"
+
+if [[ "$CONTENT" != *'PARTIAL_OUTPUT_STATUS complete=false source=artifact-required conclusion=conservative'* ]]; then
+ echo "missing conservative partial-output template" >&2
+ exit 1
+fi
+
+echo "PARTIAL_OUTPUT_GUARD: PASS"
diff --git a/scripts/review/partial_output_guard_test.sh b/scripts/review/partial_output_guard_test.sh
new file mode 100644
index 0000000..9a1cb9d
--- /dev/null
+++ b/scripts/review/partial_output_guard_test.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_FILE="$TMP_DIR/bad-output.txt"
+cat > "$BAD_FILE" <<'EOF'
+long command output line 1
+long command output line 2
+EOF
+
+set +e
+bash scripts/review/partial_output_guard.sh "$BAD_FILE" >/tmp/partial_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'missing conservative partial-output template' /tmp/partial_bad.out
+
+GOOD_FILE="$TMP_DIR/good-output.txt"
+cat > "$GOOD_FILE" <<'EOF'
+PARTIAL_OUTPUT_STATUS complete=false source=artifact-required conclusion=conservative
+artifact_hint=artifact://123
+EOF
+
+bash scripts/review/partial_output_guard.sh "$GOOD_FILE" >/tmp/partial_good.out 2>&1
+grep -q 'PARTIAL_OUTPUT_GUARD: PASS' /tmp/partial_good.out
diff --git a/scripts/review/provider_root_cause_test.sh b/scripts/review/provider_root_cause_test.sh
new file mode 100644
index 0000000..68cd23d
--- /dev/null
+++ b/scripts/review/provider_root_cause_test.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+classify_live_run_provider() {
+ local live_tail="${1:-}"
+ local normalized
+ normalized="$(printf '%s' "$live_tail" | tr '[:upper:]' '[:lower:]')"
+ case "$normalized" in
+ *"import_vertex_pricing"*) printf '%s\n' 'vertex_pricing' ;;
+ *"import_cloudflare_pricing"*|*"cloudflare_pricing"*) printf '%s\n' 'cloudflare_pricing' ;;
+ *"import_perplexity_pricing"*|*"perplexity_pricing"*) printf '%s\n' 'perplexity_pricing' ;;
+ *"import_xfyun_pricing"*|*"xfyun_pricing"*) printf '%s\n' 'xfyun_pricing' ;;
+ *) printf '%s\n' 'unknown_external_provider' ;;
+ esac
+}
+
+[[ "$(classify_live_run_provider 'import_vertex_pricing: read https://cloud.google.com/...: context deadline exceeded')" == 'vertex_pricing' ]]
+[[ "$(classify_live_run_provider 'import_cloudflare_pricing: fetch https://developers.cloudflare.com/...: unexpected status 403')" == 'cloudflare_pricing' ]]
+[[ "$(classify_live_run_provider 'import_perplexity_pricing: no model rows parsed')" == 'perplexity_pricing' ]]
+[[ "$(classify_live_run_provider 'import_xfyun_pricing: no pricing cards found')" == 'xfyun_pricing' ]]
diff --git a/scripts/review/release_semantics_capture_test.sh b/scripts/review/release_semantics_capture_test.sh
new file mode 100644
index 0000000..db211e6
--- /dev/null
+++ b/scripts/review/release_semantics_capture_test.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_FILE="$(mktemp)"
+trap 'rm -f "$TMP_FILE"' EXIT
+
+bash scripts/verify_phase6.sh >"$TMP_FILE" 2>&1 || true
+bash scripts/review/release_semantics_guard.sh "$TMP_FILE"
+grep -q '^RELEASE_SEMANTICS class=' "$TMP_FILE"
diff --git a/scripts/review/release_semantics_guard.sh b/scripts/review/release_semantics_guard.sh
new file mode 100755
index 0000000..26f5328
--- /dev/null
+++ b/scripts/review/release_semantics_guard.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+STATUS_FILE="${1:?phase status file required}"
+
+if ! grep -q '^RELEASE_SEMANTICS class=' "$STATUS_FILE"; then
+ echo "missing release semantics summary" >&2
+ exit 1
+fi
+
+echo "RELEASE_SEMANTICS_GUARD: PASS"
diff --git a/scripts/review/release_semantics_guard_test.sh b/scripts/review/release_semantics_guard_test.sh
new file mode 100644
index 0000000..cc78c93
--- /dev/null
+++ b/scripts/review/release_semantics_guard_test.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_FILE="$TMP_DIR/bad-phase6.txt"
+cat > "$BAD_FILE" <<'EOF'
+[WARN] window_gate_result=WARN 最近 7 次采集成功率未达 95%(仅外部文档站失败:external_provider_failure_only;stability_label=recovered-external-incident)
+EOF
+
+set +e
+bash scripts/review/release_semantics_guard.sh "$BAD_FILE" >/tmp/release_semantics_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'missing release semantics summary' /tmp/release_semantics_bad.out
+
+GOOD_FILE="$TMP_DIR/good-phase6.txt"
+cat > "$GOOD_FILE" <<'EOF'
+[WARN] window_gate_result=WARN 最近 7 次采集成功率未达 95%(仅外部文档站失败:external_provider_failure_only;stability_label=recovered-external-incident)
+RELEASE_SEMANTICS class=degraded-external-provider gate=window_gate policy=release-allowed-with-warning
+EOF
+
+bash scripts/review/release_semantics_guard.sh "$GOOD_FILE" >/tmp/release_semantics_good.out 2>&1
+grep -q 'RELEASE_SEMANTICS_GUARD: PASS' /tmp/release_semantics_good.out
diff --git a/scripts/review/review_action_guard.sh b/scripts/review/review_action_guard.sh
new file mode 100755
index 0000000..3ff7c4f
--- /dev/null
+++ b/scripts/review/review_action_guard.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+REPORT_PATH="${1:?review report path required}"
+
+NEXT_BLOCK="$(python3 - <<'PY' "$REPORT_PATH"
+from pathlib import Path
+import sys
+text = Path(sys.argv[1]).read_text(encoding='utf-8')
+lines = text.splitlines()
+inside = False
+buf = []
+for line in lines:
+ if line.startswith('## '):
+ if inside:
+ break
+ inside = line.strip() == '## Next'
+ continue
+ if inside:
+ buf.append(line)
+print('\n'.join(buf))
+PY
+)"
+
+if [[ -z "${NEXT_BLOCK//[[:space:]]/}" ]]; then
+ echo "missing actionable next step: ## Next block is empty" >&2
+ exit 1
+fi
+
+if ! printf '%s\n' "$NEXT_BLOCK" | grep -Eq '(^- .*处理|^- .*修复|^- .*新增|^- .*更新|^- .*验证|^- .*运行|^- .*同步|^- .*接入|^- .*排查|^[0-9]+\..*(处理|修复|新增|更新|验证|运行|同步|接入|排查)|处理问题|修复问题|新增.*guard|接入.*链)'; then
+ echo "missing actionable next step: ## Next does not contain executable action" >&2
+ exit 1
+fi
+
+echo "REVIEW_ACTION_GUARD: PASS"
diff --git a/scripts/review/review_action_guard_test.sh b/scripts/review/review_action_guard_test.sh
new file mode 100644
index 0000000..1efd01c
--- /dev/null
+++ b/scripts/review/review_action_guard_test.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_REPORT="$TMP_DIR/bad-review.md"
+cat > "$BAD_REPORT" <<'EOF'
+## Context
+- no delta
+
+## Evidence
+- one finding
+
+## Outcome
+- still blocked
+
+## Next
+- keep watching
+EOF
+
+set +e
+bash scripts/review/review_action_guard.sh "$BAD_REPORT" >/tmp/review_action_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'missing actionable next step' /tmp/review_action_bad.out
+
+GOOD_REPORT="$TMP_DIR/good-review.md"
+cat > "$GOOD_REPORT" <<'EOF'
+## Context
+- no delta
+
+## Evidence
+- one finding
+
+## Outcome
+- still blocked
+
+## Next
+- 处理问题 11,新增 review action guard 并接入 review 生成链
+EOF
+
+bash scripts/review/review_action_guard.sh "$GOOD_REPORT" >/tmp/review_action_good.out 2>&1
diff --git a/scripts/review/review_aging_priority_test.sh b/scripts/review/review_aging_priority_test.sh
new file mode 100644
index 0000000..862bd67
--- /dev/null
+++ b/scripts/review/review_aging_priority_test.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+cat > "$TMP_DIR/git_status.txt" <<'EOF'
+EOF
+
+cat > "$TMP_DIR/backlog_rows.txt" <<'EOF'
+| 18 | no delta aging | P2 | 05-12 22:46 | ❌ 未修复 | 7 次 |
+| 14 | phase6plus undefined | P1 | 05-10 21:30 | ❌ 未修复 | 6 次 |
+| 15 | review false positive | P1 | 05-11 14:30 | ❌ 未修复 | 10 次 |
+EOF
+
+OUTPUT="$(bash scripts/review/review_status_summary.sh "$TMP_DIR/git_status.txt" "$TMP_DIR/backlog_rows.txt")"
+printf '%s' "$OUTPUT" | grep -q 'aging_focus=14:P1:6,15:P1:10,18:P2:7'
diff --git a/scripts/review/review_same_day_no_decision_test.sh b/scripts/review/review_same_day_no_decision_test.sh
new file mode 100644
index 0000000..f39e547
--- /dev/null
+++ b/scripts/review/review_same_day_no_decision_test.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+cat > "$TMP_DIR/git_status.txt" <<'EOF'
+EOF
+
+cat > "$TMP_DIR/backlog_rows.txt" <<'EOF'
+| 31 | no decision delta | P2 | 05-17 15:10 | ❌ 未修复 | 3 次 |
+| 30 | aged precondition | P1 | 05-17 09:31 | ❌ 未修复 | 6 次 |
+EOF
+
+set +e
+OUTPUT="$(bash scripts/review/review_status_summary.sh "$TMP_DIR/git_status.txt" "$TMP_DIR/backlog_rows.txt")"
+set -e
+printf '%s' "$OUTPUT" | grep -q 'same_day_no_decision_focus='
+printf '%s' "$OUTPUT" | grep -q 'same_day_no_decision_focus=30:P1:6,31:P2:3'
diff --git a/scripts/review/review_status_summary.sh b/scripts/review/review_status_summary.sh
new file mode 100755
index 0000000..dc35a80
--- /dev/null
+++ b/scripts/review/review_status_summary.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+GIT_STATUS_PATH="${1:?git status file required}"
+BACKLOG_ROWS_PATH="${2:?backlog rows file required}"
+
+DIRTY_WORKTREE=0
+if [[ -s "$GIT_STATUS_PATH" ]]; then
+ DIRTY_WORKTREE=1
+fi
+
+OPEN_ISSUES=$(grep -c '^|' "$BACKLOG_ROWS_PATH" || true)
+NO_DELTA=true
+FOCUS="risk_aging,unverified,backlog"
+AGING_FOCUS="$(python3 - <<'PY' "$BACKLOG_ROWS_PATH"
+from pathlib import Path
+import sys
+rows=[]
+for line in Path(sys.argv[1]).read_text(encoding='utf-8').splitlines():
+ if not line.startswith('|'):
+ continue
+ parts=[p.strip() for p in line.strip('|').split('|')]
+ if len(parts) < 6:
+ continue
+ issue_id=parts[0]
+ priority=parts[2]
+ first_exposed=parts[3]
+ impact=parts[5].replace('次','').strip()
+ try:
+ impact_num=int(''.join(ch for ch in impact if ch.isdigit()) or '0')
+ except ValueError:
+ impact_num=0
+ rows.append((priority, first_exposed, -impact_num, issue_id, impact_num))
+priority_rank={'P0':0,'P1':1,'P2':2}
+rows.sort(key=lambda x:(priority_rank.get(x[0],9), x[1], x[2], x[3]))
+focus=[]
+for priority, first_exposed, neg_impact, issue_id, impact_num in rows[:3]:
+ focus.append(f"{issue_id}:{priority}:{impact_num}")
+print(','.join(focus))
+PY
+)"
+SAME_DAY_NO_DECISION_FOCUS="$(python3 - <<'PY' "$BACKLOG_ROWS_PATH"
+from pathlib import Path
+import sys
+rows=[]
+for line in Path(sys.argv[1]).read_text(encoding='utf-8').splitlines():
+ if not line.startswith('|'):
+ continue
+ parts=[p.strip() for p in line.strip('|').split('|')]
+ if len(parts) < 6:
+ continue
+ issue_id=parts[0]
+ priority=parts[2]
+ impact=parts[5].replace('次','').strip()
+ try:
+ impact_num=int(''.join(ch for ch in impact if ch.isdigit()) or '0')
+ except ValueError:
+ impact_num=0
+ rows.append((priority, -impact_num, issue_id, impact_num))
+priority_rank={'P0':0,'P1':1,'P2':2}
+rows.sort(key=lambda x:(priority_rank.get(x[0],9), x[1], x[2]))
+focus=[]
+for priority, neg_impact, issue_id, impact_num in rows[:2]:
+ focus.append(f"{issue_id}:{priority}:{impact_num}")
+print(','.join(focus))
+PY
+)"
+
+printf 'REVIEW_STATUS no_delta=%s dirty_worktree=%d open_issues=%d focus=%s aging_focus=%s same_day_no_decision_focus=%s\n' "$NO_DELTA" "$DIRTY_WORKTREE" "$OPEN_ISSUES" "$FOCUS" "$AGING_FOCUS" "$SAME_DAY_NO_DECISION_FOCUS"
\ No newline at end of file
diff --git a/scripts/review/review_status_summary_test.sh b/scripts/review/review_status_summary_test.sh
new file mode 100644
index 0000000..46c304b
--- /dev/null
+++ b/scripts/review/review_status_summary_test.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+cat > "$TMP_DIR/git_status.txt" <<'EOF'
+EOF
+
+cat > "$TMP_DIR/backlog_rows.txt" <<'EOF'
+| 8 | cron review 无 delta 时空转 | P1 | 05-08 09:12 | ❌ 未修复 | 13 次 |
+| 9 | 验证模式伪进展(artifact_present 局限) | P1 | 05-08 14:30 | ❌ 未修复 | 10 次 |
+EOF
+
+OUTPUT="$(bash scripts/review/review_status_summary.sh "$TMP_DIR/git_status.txt" "$TMP_DIR/backlog_rows.txt")"
+printf '%s' "$OUTPUT" | grep -q 'REVIEW_STATUS no_delta=true'
+printf '%s' "$OUTPUT" | grep -q 'dirty_worktree=0'
+printf '%s' "$OUTPUT" | grep -q 'open_issues=2'
+printf '%s' "$OUTPUT" | grep -q 'focus=risk_aging,unverified,backlog'
diff --git a/scripts/review/review_truth_guard.sh b/scripts/review/review_truth_guard.sh
new file mode 100755
index 0000000..b5921b4
--- /dev/null
+++ b/scripts/review/review_truth_guard.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+REPORT_PATH="${1:?review report path required}"
+CONTENT="$(cat "$REPORT_PATH")"
+
+if [[ "$CONTENT" != *'runtime-verified'* ]]; then
+ echo "missing truth label: runtime-verified" >&2
+ exit 1
+fi
+
+if [[ "$CONTENT" != *'current status'* && "$CONTENT" != *'当前状态'* ]]; then
+ echo "missing truth label: current status" >&2
+ exit 1
+fi
+
+if [[ "$CONTENT" == *'历史状态'* || "$CONTENT" == *'historical status'* || "$CONTENT" == *'历史 review'* || "$CONTENT" == *'上一轮 review'* ]]; then
+ if [[ "$CONTENT" != *'历史状态'* && "$CONTENT" != *'historical status'* ]]; then
+ echo "missing truth label: historical status" >&2
+ exit 1
+ fi
+fi
+
+echo "REVIEW_TRUTH_GUARD: PASS"
\ No newline at end of file
diff --git a/scripts/review/review_truth_guard_test.sh b/scripts/review/review_truth_guard_test.sh
new file mode 100644
index 0000000..e16e630
--- /dev/null
+++ b/scripts/review/review_truth_guard_test.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_REPORT="$TMP_DIR/bad-review.md"
+cat > "$BAD_REPORT" <<'EOF'
+## Evidence
+### Incomplete
+- 未完成项:外部文档源仍不稳定
+ - 当前状态:已经恢复绿色
+EOF
+
+set +e
+bash scripts/review/review_truth_guard.sh "$BAD_REPORT" >/tmp/review_truth_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'missing truth label' /tmp/review_truth_bad.out
+
+GOOD_REPORT="$TMP_DIR/good-review.md"
+cat > "$GOOD_REPORT" <<'EOF'
+## Evidence
+### Incomplete
+- 未完成项:外部文档源仍不稳定
+ - 当前状态:`runtime-verified`;最近一轮 `verify_phase6.sh` 已通过,但此条仅代表当前快照,不代表历史报告中的旧 FAIL 仍然成立。
+EOF
+
+bash scripts/review/review_truth_guard.sh "$GOOD_REPORT" >/tmp/review_truth_good.out 2>&1
+
+DRIFT_REPORT="$TMP_DIR/drift-review.md"
+cat > "$DRIFT_REPORT" <<'EOF'
+## Evidence
+### Inconsistencies
+- 伪进展或文档/实现不一致项:历史 review 仍保留旧 FAIL
+ - 证据:上一轮 review 写了 FAIL
+ - 当前状态:`runtime-verified`;当前 verify_phase6 已通过
+EOF
+
+set +e
+bash scripts/review/review_truth_guard.sh "$DRIFT_REPORT" >/tmp/review_truth_drift.out 2>&1
+DRIFT_RC=$?
+set -e
+[[ "$DRIFT_RC" -ne 0 ]]
+grep -q 'missing truth label: historical status' /tmp/review_truth_drift.out
+
+GOOD_DRIFT_REPORT="$TMP_DIR/good-drift-review.md"
+cat > "$GOOD_DRIFT_REPORT" <<'EOF'
+## Evidence
+### Inconsistencies
+- 伪进展或文档/实现不一致项:历史 review 仍保留旧 FAIL
+ - 历史状态:上一轮 review 记录为 FAIL
+ - 当前状态:`runtime-verified`;当前 verify_phase6 已通过
+EOF
+
+bash scripts/review/review_truth_guard.sh "$GOOD_DRIFT_REPORT" >/tmp/review_truth_good_drift.out 2>&1
+grep -q 'REVIEW_TRUTH_GUARD: PASS' /tmp/review_truth_good_drift.out
+grep -q 'REVIEW_TRUTH_GUARD: PASS' /tmp/review_truth_good.out
diff --git a/scripts/review/review_worktree_status_guard.sh b/scripts/review/review_worktree_status_guard.sh
new file mode 100755
index 0000000..ca11eac
--- /dev/null
+++ b/scripts/review/review_worktree_status_guard.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+STATUS_FILE="${1:?status file required}"
+LINE="$(cat "$STATUS_FILE")"
+
+for token in tracked_modified= untracked= total=; do
+ if [[ "$LINE" != *"$token"* ]]; then
+ echo "missing tracked_modified/untracked counters" >&2
+ exit 1
+ fi
+done
+
+echo "REVIEW_WORKTREE_GUARD: PASS"
diff --git a/scripts/review/review_worktree_status_guard_test.sh b/scripts/review/review_worktree_status_guard_test.sh
new file mode 100644
index 0000000..9f82544
--- /dev/null
+++ b/scripts/review/review_worktree_status_guard_test.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_FILE="$TMP_DIR/bad-worktree.txt"
+printf 'WORKTREE_STATUS label=cron state=dirty commit_hint=needed\n' > "$BAD_FILE"
+
+set +e
+bash scripts/review/review_worktree_status_guard.sh "$BAD_FILE" >/tmp/review_worktree_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'missing tracked_modified/untracked counters' /tmp/review_worktree_bad.out
+
+GOOD_FILE="$TMP_DIR/good-worktree.txt"
+printf 'WORKTREE_STATUS label=cron state=dirty tracked_modified=2 untracked=3 total=5 commit_hint=needed\n' > "$GOOD_FILE"
+bash scripts/review/review_worktree_status_guard.sh "$GOOD_FILE" >/tmp/review_worktree_good.out 2>&1
+grep -q 'REVIEW_WORKTREE_GUARD: PASS' /tmp/review_worktree_good.out
diff --git a/scripts/review/root_cause_summary_guard.sh b/scripts/review/root_cause_summary_guard.sh
new file mode 100755
index 0000000..a4bc84e
--- /dev/null
+++ b/scripts/review/root_cause_summary_guard.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+STATUS_FILE="${1:?phase status file required}"
+
+if ! grep -q '^ROOT_CAUSE class=' "$STATUS_FILE"; then
+ echo "missing root cause summary" >&2
+ exit 1
+fi
+
+echo "ROOT_CAUSE_GUARD: PASS"
diff --git a/scripts/review/root_cause_summary_test.sh b/scripts/review/root_cause_summary_test.sh
new file mode 100644
index 0000000..b59ddd5
--- /dev/null
+++ b/scripts/review/root_cause_summary_test.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_FILE="$TMP_DIR/bad-phase6.txt"
+cat > "$BAD_FILE" <<'EOF'
+[FAIL] importer_smoke_gate_result=FAIL 新增导入器 smoke gate 未通过
+[FAIL] live_run_result=FAIL 主链路真实采集失败
+SUMMARY pass=10 fail=2 warn=0
+PHASE_RESULT: FAIL
+EOF
+
+set +e
+bash scripts/review/root_cause_summary_guard.sh "$BAD_FILE" >/tmp/root_cause_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'missing root cause summary' /tmp/root_cause_bad.out
+
+GOOD_FILE="$TMP_DIR/good-phase6.txt"
+cat > "$GOOD_FILE" <<'EOF'
+[FAIL] importer_smoke_gate_result=FAIL 新增导入器 smoke gate 未通过
+ROOT_CAUSE class=importer_smoke_gate_failure source=importer_smoke_gate summary=新增导入器 smoke gate 未通过
+SUMMARY pass=10 fail=1 warn=0
+PHASE_RESULT: FAIL
+EOF
+
+bash scripts/review/root_cause_summary_guard.sh "$GOOD_FILE" >/tmp/root_cause_good.out 2>&1
+grep -q 'ROOT_CAUSE_GUARD: PASS' /tmp/root_cause_good.out
diff --git a/scripts/review/stability_status_guard.sh b/scripts/review/stability_status_guard.sh
new file mode 100755
index 0000000..eac85be
--- /dev/null
+++ b/scripts/review/stability_status_guard.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+STATUS_FILE="${1:?status file required}"
+LINE="$(cat "$STATUS_FILE")"
+
+if [[ "$LINE" == *"external_provider_failure="* ]]; then
+ if [[ "$LINE" != *"stability_label="* ]]; then
+ echo "missing stability label" >&2
+ exit 1
+ fi
+fi
+
+echo "STABILITY_STATUS_GUARD: PASS"
diff --git a/scripts/review/stability_status_guard_test.sh b/scripts/review/stability_status_guard_test.sh
new file mode 100644
index 0000000..24e070e
--- /dev/null
+++ b/scripts/review/stability_status_guard_test.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+BAD_FILE="$TMP_DIR/bad-status.txt"
+cat > "$BAD_FILE" <<'EOF'
+window_size=7 success_count=6 failure_count=1 success_rate=85.71 threshold=95 precondition_missing=0 external_provider_failure=1 collector_runtime_failure=0 unknown_failure=0
+EOF
+
+set +e
+bash scripts/review/stability_status_guard.sh "$BAD_FILE" >/tmp/stability_bad.out 2>&1
+BAD_RC=$?
+set -e
+[[ "$BAD_RC" -ne 0 ]]
+grep -q 'missing stability label' /tmp/stability_bad.out
+
+GOOD_FILE="$TMP_DIR/good-status.txt"
+cat > "$GOOD_FILE" <<'EOF'
+window_size=7 success_count=6 failure_count=1 success_rate=85.71 threshold=95 precondition_missing=0 external_provider_failure=1 collector_runtime_failure=0 unknown_failure=0 stability_label=recovered-external-incident
+EOF
+
+bash scripts/review/stability_status_guard.sh "$GOOD_FILE" >/tmp/stability_good.out 2>&1
+grep -q 'STABILITY_STATUS_GUARD: PASS' /tmp/stability_good.out
diff --git a/scripts/review/subagent_workspace_guard_test.sh b/scripts/review/subagent_workspace_guard_test.sh
new file mode 100644
index 0000000..1a2423d
--- /dev/null
+++ b/scripts/review/subagent_workspace_guard_test.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT_DIR"
+
+PROJECT_TASKS="$ROOT_DIR/TASKS.md"
+PROJECT_GOALS="$ROOT_DIR/GOALS.md"
+GLOBAL_TASKS="/home/long/.openclaw/workspace/TASKS.md"
+GLOBAL_GOALS="/home/long/.openclaw/workspace/GOALS.md"
+
+bash scripts/review/preflight_task_write_guard.sh llm-intelligence-agent "$PROJECT_TASKS" "$PROJECT_GOALS" >/dev/null
+
+set +e
+bash scripts/review/preflight_task_write_guard.sh llm-intelligence-agent "$GLOBAL_TASKS" >/tmp/llm_subagent_guard.err 2>&1
+RC1=$?
+set -e
+[[ "$RC1" -ne 0 ]]
+grep -Eq 'shared workspace file|forbidden' /tmp/llm_subagent_guard.err
+
+
+set +e
+bash scripts/review/preflight_task_write_guard.sh llm-intelligence-agent "$GLOBAL_GOALS" >/tmp/llm_subagent_guard_goal.err 2>&1
+RC2=$?
+set -e
+[[ "$RC2" -ne 0 ]]
+grep -Eq 'shared workspace file|forbidden' /tmp/llm_subagent_guard_goal.err
+
+echo "subagent_workspace_guard_test: PASS"
diff --git a/scripts/run_daily.sh b/scripts/run_daily.sh
index 72722c4..2b870e9 100755
--- a/scripts/run_daily.sh
+++ b/scripts/run_daily.sh
@@ -27,6 +27,40 @@ PIPELINE_FAILED_SOURCE_SET="none"
MULTI_SOURCE_AUDIT="multi_source_audit=unavailable"
PIPELINE_AUDIT_SUMMARY=""
+report_cron_status() {
+ local status="$1"
+ local topic="$2"
+ local evidence_line="${3:-}"
+ local next_line="${4:-none}"
+ if [[ -x "$PROJECT_DIR/scripts/cron_status_report.sh" ]]; then
+ REPORT_DATE="$REPORT_DATE" "$PROJECT_DIR/scripts/cron_status_report.sh" cron "$status" "$topic" "$evidence_line" "$next_line" >> "$LOG_FILE" 2>&1 || true
+ fi
+}
+
+capture_worktree_status() {
+ if [[ -x "$PROJECT_DIR/scripts/git_commit_status_report.sh" ]]; then
+ "$PROJECT_DIR/scripts/git_commit_status_report.sh" cron
+ else
+ printf '%s\n' "WORKTREE_STATUS label=cron state=unknown tracked_modified=0 untracked=0 commit_hint=unknown"
+ fi
+}
+
+
+classify_cron_failure_status() {
+ local message="${1:-}"
+ local normalized
+ normalized="$(printf '%s' "$message" | tr '[:upper:]' '[:lower:]')"
+ case "$normalized" in
+ *"api key"*|*"database_url"*|*"must provide"*|*"未设置"*|*"permission denied"*|*"role does not exist"*|*"relation does not exist"*)
+ printf '%s\n' "precondition_missing"
+ ;;
+ *)
+ printf '%s\n' "failed"
+ ;;
+ esac
+}
+
+
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
@@ -64,14 +98,17 @@ refresh_pipeline_audit() {
# 错误处理
error_exit() {
local output_path=""
+ local cron_status="failed"
log "❌ 错误: $1"
refresh_pipeline_audit
+ cron_status="$(classify_cron_failure_status "$1")"
# 降级:复制昨日报告
fallback_report
if [ -f "$(report_markdown_path "$REPORT_DATE")" ]; then
output_path="$(report_markdown_path "$REPORT_DATE")"
fi
track_report_state "$DB_URL" "$REPORT_DATE" "failed" "${MODEL_COUNT:-}" "$PIPELINE_AUDIT_SUMMARY" "$output_path" "$1" "scheduled" "cron" "true" >> "$LOG_FILE" 2>&1 || true
+ report_cron_status "$cron_status" "run_daily.sh failed" "$1" "inspect ${LOG_FILE}"
# 发送告警
if [ -n "$FEISHU_WEBHOOK" ]; then
send_alert "$1"
@@ -461,7 +498,8 @@ if ! go run -tags llm_script \
error_exit "目录级官方入口核验失败"
fi
if ! SIGNAL_SOURCE_AUDIT="$PIPELINE_AUDIT_SUMMARY" go run -tags llm_script \
- scripts/materialize_daily_signals.go >> "$LOG_FILE" 2>&1; then
+ scripts/materialize_daily_signals.go \
+ scripts/official_import_signature_audit_query_lib.go >> "$LOG_FILE" 2>&1; then
merge_failed_source_keys "daily_signal_snapshot"
error_exit "每日关键信号物化失败"
fi
@@ -499,7 +537,9 @@ fi
if ! psql "$DB_URL" -Atqc "select count(*) from report_runs where report_date = DATE '${REPORT_DATE}' and status = 'generated';" | awk '{ exit !($1 >= 1) }'; then
error_exit "report_runs 未写入 generated 记录"
fi
+WORKTREE_STATUS_LINE="$(capture_worktree_status)"
log "✅ 日报记录更新完成"
+report_cron_status "success" "run_daily.sh completed" "verify_phase3/phase5/phase6-ready chain green for ${REPORT_DATE}; ${WORKTREE_STATUS_LINE}" "next=none"
log "🎉 每日流水线全部完成!"
log "📄 Markdown: $(report_markdown_path "$REPORT_DATE")"
diff --git a/scripts/run_intel_pipeline.sh b/scripts/run_intel_pipeline.sh
index c184c83..7a4cf3f 100755
--- a/scripts/run_intel_pipeline.sh
+++ b/scripts/run_intel_pipeline.sh
@@ -198,6 +198,6 @@ run_or_fail "catalog_seed_verification" "目录级官方入口核验失败" \
refresh_pipeline_audit
run_or_fail "daily_signal_snapshot" "每日关键信号物化失败" \
- env SIGNAL_SOURCE_AUDIT="$PIPELINE_AUDIT_SUMMARY" go run -tags llm_script "./scripts/materialize_daily_signals.go"
+ env SIGNAL_SOURCE_AUDIT="$PIPELINE_AUDIT_SUMMARY" go run -tags llm_script "./scripts/materialize_daily_signals.go" "./scripts/official_import_signature_audit_query_lib.go"
echo "$PIPELINE_AUDIT_SUMMARY"
diff --git a/scripts/run_intraday_discovery_watch.sh b/scripts/run_intraday_discovery_watch.sh
index b2b8bd1..12ef443 100644
--- a/scripts/run_intraday_discovery_watch.sh
+++ b/scripts/run_intraday_discovery_watch.sh
@@ -67,4 +67,4 @@ if ! go run -tags llm_script ./scripts/deepseek_pricing_signature_guard.go ./scr
exit 1
fi
fi
-REPORT_TRIGGER_SOURCE="intraday_discovery" go run -tags llm_script ./scripts/materialize_daily_signals.go "${materialize_args[@]}"
+REPORT_TRIGGER_SOURCE="intraday_discovery" go run -tags llm_script ./scripts/materialize_daily_signals.go ./scripts/official_import_signature_audit_query_lib.go "${materialize_args[@]}"
diff --git a/scripts/run_intraday_price_watch.sh b/scripts/run_intraday_price_watch.sh
index 2ab03d5..ecd8e6a 100644
--- a/scripts/run_intraday_price_watch.sh
+++ b/scripts/run_intraday_price_watch.sh
@@ -193,6 +193,6 @@ run_or_fail "catalog_seed_verification" "目录级官方入口核验失败" \
go run -tags llm_script ./scripts/subscription_import_common.go ./scripts/import_catalog_seed_verification.go
refresh_pipeline_audit
run_or_fail "daily_signal_snapshot" "日内价格信号物化失败" \
- env SIGNAL_SOURCE_AUDIT="$PIPELINE_AUDIT_SUMMARY" REPORT_TRIGGER_SOURCE="intraday" go run -tags llm_script ./scripts/materialize_daily_signals.go
+ env SIGNAL_SOURCE_AUDIT="$PIPELINE_AUDIT_SUMMARY" REPORT_TRIGGER_SOURCE="intraday" go run -tags llm_script ./scripts/materialize_daily_signals.go ./scripts/official_import_signature_audit_query_lib.go
echo "$PIPELINE_AUDIT_SUMMARY"
diff --git a/scripts/run_real_pipeline.sh b/scripts/run_real_pipeline.sh
index 020ff3e..fd59b17 100755
--- a/scripts/run_real_pipeline.sh
+++ b/scripts/run_real_pipeline.sh
@@ -319,7 +319,7 @@ if ! go run -tags llm_script "./scripts/subscription_import_common.go" "./script
record_failure "目录级官方入口核验失败"
exit 1
fi
-if ! SIGNAL_SOURCE_AUDIT="$PIPELINE_AUDIT_SUMMARY" go run -tags llm_script "./scripts/materialize_daily_signals.go"; then
+if ! SIGNAL_SOURCE_AUDIT="$PIPELINE_AUDIT_SUMMARY" go run -tags llm_script "./scripts/materialize_daily_signals.go" "./scripts/official_import_signature_audit_query_lib.go"; then
merge_failed_source_keys "daily_signal_snapshot"
record_failure "每日关键信号物化失败"
exit 1
@@ -331,7 +331,7 @@ if ! REPORT_RUN_KIND="manual" REPORT_TRIGGER_SOURCE="pipeline" REPORT_IS_OFFICIA
exit 1
fi
-if [[ ! -f "$(report_archive_markdown_path "$REPORT_DATE")" || ! -f "$(report_archive_html_path "$REPORT_DATE")" ]]; then
+if [[ ! -f "$(report_ad_hoc_markdown_path "$REPORT_DATE" manual pipeline)" || ! -f "$(report_ad_hoc_html_path "$REPORT_DATE" manual pipeline)" ]]; then
record_failure "日报归档缺失"
exit 1
fi
diff --git a/scripts/script_entry_inventory_test.sh b/scripts/script_entry_inventory_test.sh
new file mode 100644
index 0000000..3625e41
--- /dev/null
+++ b/scripts/script_entry_inventory_test.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+OUTPUT="$(bash scripts/list_testable_script_entries.sh)"
+printf '%s' "$OUTPUT" | grep -q 'SCRIPT_ENTRY_SUMMARY total_entries='
+printf '%s' "$OUTPUT" | grep -q 'discover_intraday_news_candidates.go'
+printf '%s' "$OUTPUT" | grep -q 'fetch_openrouter.go'
+printf '%s' "$OUTPUT" | grep -q 'generate_daily_report.go'
+printf '%s' "$OUTPUT" | grep -q 'import_xfyun_pricing.go'
diff --git a/scripts/scripts_conflict_detection_test.sh b/scripts/scripts_conflict_detection_test.sh
new file mode 100644
index 0000000..a88b8d6
--- /dev/null
+++ b/scripts/scripts_conflict_detection_test.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+OUT="$(mktemp)"
+trap 'rm -f "$OUT"' EXIT
+
+set +e
+go test -tags llm_script ./scripts >"$OUT" 2>&1
+RC=$?
+set -e
+
+if [[ "$RC" -eq 0 ]]; then
+ echo "expected go test -tags llm_script ./scripts to fail before conflict isolation"
+ exit 1
+fi
+
+grep -q 'main redeclared in this block' "$OUT"
+grep -Eq 'ModelPricing redeclared in this block|logger redeclared in this block' "$OUT"
+
+echo "scripts_conflict_detection_test: PASS"
diff --git a/scripts/scripts_package_compile_test.sh b/scripts/scripts_package_compile_test.sh
new file mode 100644
index 0000000..734e290
--- /dev/null
+++ b/scripts/scripts_package_compile_test.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+bash scripts/scripts_conflict_detection_test.sh
\ No newline at end of file
diff --git a/scripts/scripts_package_structure_test.go b/scripts/scripts_package_structure_test.go
new file mode 100644
index 0000000..dcacf7f
--- /dev/null
+++ b/scripts/scripts_package_structure_test.go
@@ -0,0 +1,44 @@
+//go:build llm_script
+
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestScriptsPackageCompileContract(t *testing.T) {
+ projectRoot, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("getwd: %v", err)
+ }
+ scriptDir := projectRoot
+ if filepath.Base(projectRoot) != "scripts" {
+ scriptDir = filepath.Join(projectRoot, "scripts")
+ }
+
+ entries, err := os.ReadDir(scriptDir)
+ if err != nil {
+ t.Fatalf("readdir %s: %v", scriptDir, err)
+ }
+
+ var mainCount int
+ for _, entry := range entries {
+ if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
+ continue
+ }
+ content, err := os.ReadFile(filepath.Join(scriptDir, entry.Name()))
+ if err != nil {
+ t.Fatalf("read %s: %v", entry.Name(), err)
+ }
+ if strings.Contains(string(content), "func main()") {
+ mainCount++
+ }
+ }
+
+ if mainCount < 2 {
+ t.Fatalf("expected scripts package to contain multiple CLI entrypoints, got %d", mainCount)
+ }
+}
diff --git a/scripts/secret_gate_coverage_test.sh b/scripts/secret_gate_coverage_test.sh
new file mode 100644
index 0000000..18a315e
--- /dev/null
+++ b/scripts/secret_gate_coverage_test.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+check_contains() {
+ local file="$1"
+ local needle="$2"
+ grep -Fq "$needle" "$file" || {
+ echo "missing in ${file}: ${needle}"
+ exit 1
+ }
+}
+
+check_contains "scripts/verify_phase6.sh" '. "$SCRIPT_DIR/secret_gate_lib.sh"'
+check_contains "scripts/verify_phase6.sh" 'secret_scan_paths . cmd internal frontend/src scripts .github/workflows && secret_env_files .dockerignore'
+check_contains "scripts/verify_phase6.sh" 'bash scripts/secret_gate_test.sh'
+check_contains "scripts/secret_gate_test.sh" '. "$ROOT_DIR/scripts/secret_gate_lib.sh"'
+check_contains "scripts/secret_gate_test.sh" 'secret_scan_paths "$SECRET_FILE" "$CLEAN_FILE"'
+check_contains "scripts/secret_gate_test.sh" 'secret_env_files "$DOCKERIGNORE_FILE"'
+
+echo "secret_gate_coverage_test: PASS"
diff --git a/scripts/verification_executor.go b/scripts/verification_executor.go
index cf59f45..561f9a8 100644
--- a/scripts/verification_executor.go
+++ b/scripts/verification_executor.go
@@ -43,10 +43,12 @@ type TaskResult struct {
StderrSummary string
Error string
Reason string
+ FailureClass string
EvidenceGrade string
TaskType string
}
+
func main() {
dryRun := flag.Bool("dry-run", false, "print commands without executing")
taskFilter := flag.String("task", "", "filter by task ID (e.g. T-Q2-1.1)")
@@ -119,6 +121,9 @@ func main() {
if r.StdoutSummary != "" && (!r.Verified || r.Reason != "" || r.Error != "") {
fmt.Printf(" stdout: %s\n", r.StdoutSummary)
}
+ if r.FailureClass != "" {
+ fmt.Printf(" class: %s\n", r.FailureClass)
+ }
if r.Error != "" {
fmt.Printf(" ERROR: %s\n", r.Error)
} else if r.ExitCode != 0 && r.Stdout != "" {
@@ -126,14 +131,14 @@ func main() {
} else if r.Reason != "" {
fmt.Printf(" reason: %s\n", r.Reason)
}
+
}
fmt.Printf("\n=== Summary: %d passed, %d failed ===\n", passed, failed)
- if failed > 0 {
- os.Exit(1)
- }
+ os.Exit(determineProcessExitCode(results))
}
+
func resolveTasksPath(flagValue string) string {
envValue := os.Getenv("TASKS_PATH")
wd := ""
@@ -305,6 +310,7 @@ func verifyTask(t taskEntry, dryRun bool) TaskResult {
if !t.HasVerification {
r.Reason = "no verification block"
+ r.FailureClass = "missing_verification"
r.Verified = true // No verification = trivially pass
return r
}
@@ -318,11 +324,17 @@ func verifyTask(t taskEntry, dryRun bool) TaskResult {
if validationErr := validateVerification(t.Verification); validationErr != "" {
r.Verified = false
r.Reason = validationErr
+ r.FailureClass = "verification_config_failure"
return r
}
if t.Verification.Command == "" {
+ if t.Verification.Mode == "artifact_present" {
+ r.Verified = true
+ return r
+ }
r.Reason = "verification.command is empty"
+ r.FailureClass = "verification_config_failure"
r.Verified = false
return r
}
@@ -351,6 +363,7 @@ func verifyTask(t taskEntry, dryRun bool) TaskResult {
r.ExitCode = 0
if err != nil {
r.ExitCode = -1
+ r.FailureClass = "tool_execution_failure"
if ctx.Err() == context.DeadlineExceeded {
r.Error = fmt.Sprintf("timeout after %ds", t.Verification.TimeoutSeconds)
} else {
@@ -394,17 +407,50 @@ func verifyTask(t taskEntry, dryRun bool) TaskResult {
r.Verified = matched
if !matched {
r.Reason = fmt.Sprintf("expected_evidence '%s' not found in output", evidence)
+ r.FailureClass = "business_assertion_failure"
}
} else if r.ExitCode == 0 {
r.Verified = true
} else {
r.Verified = false
r.Reason = fmt.Sprintf("exit code %d", r.ExitCode)
+ r.FailureClass = "tool_execution_failure"
}
return r
}
+func classifyFailureTier(r TaskResult) int {
+ if r.Verified {
+ return 0
+ }
+ if r.EvidenceGrade == "runtime-verified" {
+ return 2
+ }
+ return 3
+}
+
+func determineProcessExitCode(results []TaskResult) int {
+ hasRuntimeFailure := false
+ hasLowerTierFailure := false
+ for _, r := range results {
+ tier := classifyFailureTier(r)
+ switch tier {
+ case 2:
+ hasRuntimeFailure = true
+ case 3:
+ hasLowerTierFailure = true
+ }
+ }
+ if hasRuntimeFailure {
+ return 2
+ }
+ if hasLowerTierFailure {
+ return 3
+ }
+ return 0
+}
+
func normalizeEvidenceGrade(mode, explicit string) string {
if explicit = strings.TrimSpace(explicit); explicit != "" {
return explicit
@@ -532,6 +578,14 @@ func validateVerification(v Verification) string {
if (v.TaskType == "code" || v.TaskType == "automation") && v.Mode == "semantic" {
return fmt.Sprintf("semantic-only verification is not allowed for %s tasks", v.TaskType)
}
+ if v.Mode == "artifact_present" {
+ if strings.TrimSpace(v.Command) != "" || strings.TrimSpace(v.ExpectedEvidence) != "" {
+ return "artifact_present does not allow command or expected_evidence; use test_pass for executable verification"
+ }
+ if v.TaskType == "code" || v.TaskType == "automation" || v.TaskType == "data" || v.TaskType == "analysis" {
+ return fmt.Sprintf("artifact_present is not allowed for %s tasks", v.TaskType)
+ }
+ }
return ""
}
diff --git a/scripts/verification_executor_exit_test.sh b/scripts/verification_executor_exit_test.sh
new file mode 100644
index 0000000..49d2152
--- /dev/null
+++ b/scripts/verification_executor_exit_test.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+TMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TMP_DIR"' EXIT
+
+cat > "$TMP_DIR/runtime_fail.md" <<'EOF'
+### T-1 ✅ Runtime fail
+- **verification**:
+ - mode: `test_pass`
+ - command: `echo boom && exit 1`
+ - expected_evidence: `boom`
+ - evidence_grade: `runtime-verified`
+ - task_type: `automation`
+EOF
+
+set +e
+go run -tags llm_script scripts/verification_executor.go --tasks "$TMP_DIR/runtime_fail.md" >"$TMP_DIR/runtime.out" 2>&1
+RUNTIME_RC=$?
+set -e
+[[ "$RUNTIME_RC" -eq 1 ]]
+grep -q 'exit status 2' "$TMP_DIR/runtime.out"
+grep -q '=== Summary: 0 passed, 1 failed ===' "$TMP_DIR/runtime.out"
+
+
+cat > "$TMP_DIR/artifact_fail.md" <<'EOF'
+### T-1 ✅ Artifact fail
+- **verification**:
+ - mode: `artifact_present`
+ - command: `echo missing`
+ - expected_evidence: `exists`
+ - evidence_grade: `artifact-present`
+ - task_type: `documentation`
+EOF
+
+set +e
+go run -tags llm_script scripts/verification_executor.go --tasks "$TMP_DIR/artifact_fail.md" >"$TMP_DIR/artifact.out" 2>&1
+ARTIFACT_RC=$?
+set -e
+[[ "$ARTIFACT_RC" -eq 1 ]]
+grep -q 'exit status 3' "$TMP_DIR/artifact.out"
+grep -q 'expected_evidence' "$TMP_DIR/artifact.out"
diff --git a/scripts/verification_executor_test.go b/scripts/verification_executor_test.go
index 9595dc5..569450b 100644
--- a/scripts/verification_executor_test.go
+++ b/scripts/verification_executor_test.go
@@ -88,9 +88,8 @@ func TestVerifyTaskDefaultsEvidenceGradeFromMode(t *testing.T) {
ID: "T-2",
Name: "artifact task",
Verification: Verification{
- Mode: "artifact_present",
- Command: "echo exists",
- ExpectedEvidence: "exists",
+ Mode: "artifact_present",
+ TaskType: "documentation",
},
HasVerification: true,
}
@@ -104,6 +103,7 @@ func TestVerifyTaskDefaultsEvidenceGradeFromMode(t *testing.T) {
}
}
+
func TestResolveTasksPathDoesNotImplicitlyFallbackToGlobal(t *testing.T) {
root := t.TempDir()
projectDir := filepath.Join(root, "project")
@@ -266,3 +266,129 @@ func TestFilterTasksByStatus(t *testing.T) {
t.Fatalf("expected all 3 tasks, got %d", len(all))
}
}
+
+func TestDetermineProcessExitCode(t *testing.T) {
+ cases := []struct {
+ name string
+ results []TaskResult
+ want int
+ }{
+ {
+ name: "all pass",
+ results: []TaskResult{{Verified: true}, {Verified: true}},
+ want: 0,
+ },
+ {
+ name: "runtime failure",
+ results: []TaskResult{{Verified: false, EvidenceGrade: "runtime-verified", TaskType: "automation"}},
+ want: 2,
+ },
+ {
+ name: "artifact only failure",
+ results: []TaskResult{{Verified: false, EvidenceGrade: "artifact-present", TaskType: "documentation"}},
+ want: 3,
+ },
+ {
+ name: "mixed defaults to runtime",
+ results: []TaskResult{
+ {Verified: false, EvidenceGrade: "artifact-present", TaskType: "documentation"},
+ {Verified: false, EvidenceGrade: "runtime-verified", TaskType: "automation"},
+ },
+ want: 2,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := determineProcessExitCode(tc.results); got != tc.want {
+ t.Fatalf("exit code = %d, want %d", got, tc.want)
+ }
+ })
+ }
+}
+
+func TestClassifyFailureTier(t *testing.T) {
+ if got := classifyFailureTier(TaskResult{Verified: false, EvidenceGrade: "runtime-verified", TaskType: "automation"}); got != 2 {
+ t.Fatalf("runtime failure tier = %d, want 2", got)
+ }
+ if got := classifyFailureTier(TaskResult{Verified: false, EvidenceGrade: "artifact-present", TaskType: "documentation"}); got != 3 {
+ t.Fatalf("artifact failure tier = %d, want 3", got)
+ }
+ if got := classifyFailureTier(TaskResult{Verified: true, EvidenceGrade: "runtime-verified", TaskType: "automation"}); got != 0 {
+ t.Fatalf("verified tier = %d, want 0", got)
+ }
+}
+
+func TestVerifyTaskClassifiesToolExecutionFailure(t *testing.T) {
+ task := taskEntry{
+ ID: "T-4",
+ Name: "tool failure task",
+ Verification: Verification{
+ Mode: "test_pass",
+ Command: "echo tool-out && echo tool-err 1>&2 && exit 1",
+ ExpectedEvidence: "tool-out",
+ TaskType: "automation",
+ },
+ HasVerification: true,
+ }
+
+ result := verifyTask(task, false)
+ if result.Verified {
+ t.Fatalf("expected tool failure task to fail")
+ }
+ if result.FailureClass != "tool_execution_failure" {
+ t.Fatalf("failure class = %q, want tool_execution_failure", result.FailureClass)
+ }
+}
+
+func TestVerifyTaskArtifactPresentMisuseBecomesConfigFailure(t *testing.T) {
+ task := taskEntry{
+ ID: "T-5",
+ Name: "artifact misuse",
+ Verification: Verification{
+ Mode: "artifact_present",
+ Command: "echo actual-output",
+ ExpectedEvidence: "expected-output",
+ TaskType: "documentation",
+ },
+ HasVerification: true,
+ }
+
+ result := verifyTask(task, false)
+ if result.Verified {
+ t.Fatalf("expected artifact misuse to fail")
+ }
+ if result.FailureClass != "verification_config_failure" {
+ t.Fatalf("failure class = %q, want verification_config_failure", result.FailureClass)
+ }
+}
+
+func TestValidateVerificationRejectsArtifactPresentWithCommand(t *testing.T) {
+ got := validateVerification(Verification{
+ Mode: "artifact_present",
+ Command: "echo exists",
+ ExpectedEvidence: "exists",
+ TaskType: "documentation",
+ EvidenceGrade: "artifact-present",
+ })
+ if got == "" {
+ t.Fatalf("expected artifact_present with command to be rejected")
+ }
+ if !strings.Contains(got, "artifact_present") {
+ t.Fatalf("unexpected validation error: %q", got)
+ }
+}
+
+func TestValidateVerificationRejectsArtifactPresentForAutomation(t *testing.T) {
+ got := validateVerification(Verification{
+ Mode: "artifact_present",
+ TaskType: "automation",
+ EvidenceGrade: "artifact-present",
+ })
+ if got == "" {
+ t.Fatalf("expected artifact_present automation task to be rejected")
+ }
+ if !strings.Contains(got, "artifact_present") {
+ t.Fatalf("unexpected validation error: %q", got)
+ }
+}
diff --git a/scripts/verify_build_coverage_test.sh b/scripts/verify_build_coverage_test.sh
new file mode 100644
index 0000000..0492fa1
--- /dev/null
+++ b/scripts/verify_build_coverage_test.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+check_contains() {
+ local file="$1"
+ local needle="$2"
+ grep -Fq "$needle" "$file" || {
+ echo "missing in ${file}: ${needle}"
+ exit 1
+ }
+}
+
+check_contains scripts/verify_phase2.sh 'go build -o /dev/null ./scripts/fetch_openrouter.go'
+check_contains scripts/verify_phase3.sh 'go build -o /dev/null ./scripts/generate_daily_report.go ./scripts/official_import_signature_audit_query_lib.go'
+check_contains scripts/verify_phase4.sh 'cd frontend && npm run build'
+check_contains scripts/verify_phase6.sh 'go build -o /dev/null ./cmd/server'
+check_contains scripts/verify_phase6.sh 'if go build -o "$SERVER_BIN" ./cmd/server'
+
+echo "verify_build_coverage_test: PASS"
diff --git a/scripts/verify_intraday_news_candidates.go b/scripts/verify_intraday_news_candidates.go
index dd584fb..5738466 100644
--- a/scripts/verify_intraday_news_candidates.go
+++ b/scripts/verify_intraday_news_candidates.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/verify_phase3.sh b/scripts/verify_phase3.sh
index 08f53ca..db09353 100755
--- a/scripts/verify_phase3.sh
+++ b/scripts/verify_phase3.sh
@@ -6,11 +6,11 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/verify_common.sh"
. "$SCRIPT_DIR/report_utils.sh"
-TODAY="$(report_date_value)"
-TODAY_MARKDOWN_PATH="$(report_markdown_path "$TODAY")"
-TODAY_HTML_PATH="$(report_html_path "$TODAY")"
-TODAY_ARCHIVE_MARKDOWN_PATH="$(report_archive_markdown_path "$TODAY")"
-TODAY_ARCHIVE_HTML_PATH="$(report_archive_html_path "$TODAY")"
+LATEST_OFFICIAL_MARKDOWN_PATH="$(find "$(report_output_dir)" -maxdepth 1 -type f -name 'daily_report_*.md' | sort | tail -n 1)"
+LATEST_OFFICIAL_HTML_PATH="$(find "$(report_html_dir)" -maxdepth 1 -type f -name 'daily_report_*.html' | sort | tail -n 1)"
+LATEST_OFFICIAL_ARCHIVE_MARKDOWN_PATH="$(find "$(report_output_dir)" -mindepth 3 -maxdepth 3 -type f -name 'daily_report_*.md' | sort | tail -n 1)"
+LATEST_OFFICIAL_ARCHIVE_HTML_PATH="$(find "$(report_output_dir)" -mindepth 3 -maxdepth 3 -type f -name 'daily_report_*.html' | sort | tail -n 1)"
+
echo "=== Phase 3 验收检查 ==="
@@ -23,9 +23,10 @@ check_shell "正式调度链启用严格真实采集" "grep -q -- '-strict-real'
check_shell "正式调度链校验本次采集结果数量" "grep -q '本次采集结果异常' scripts/run_daily.sh && grep -q 'total=' scripts/run_real_pipeline.sh"
check_shell "每日流水线已纳入多源补充同步" "grep -q 'fetch_multi_source.go --sources moonshot,deepseek,openai' scripts/run_daily.sh && grep -q 'import_zhipu_data.go' scripts/run_daily.sh && grep -q 'import_phase2_data.go' scripts/run_daily.sh && grep -q 'import_bytedance_data.go' scripts/run_daily.sh"
check_shell "每日流水线会把来源级运行审计写入正式日报上下文" "grep -q 'REPORT_RUNTIME_AUDIT' scripts/run_daily.sh && grep -q 'selected_source_keys=' scripts/run_daily.sh && grep -q 'failed_source_keys=' scripts/run_daily.sh"
-check_shell "今日日报 Markdown 主产物存在且包含数据质量摘要" "test -f ${TODAY_MARKDOWN_PATH} && grep -q '数据质量摘要' ${TODAY_MARKDOWN_PATH}"
-check_shell "今日日报 HTML 主产物存在" "test -f ${TODAY_HTML_PATH}"
-check_shell "今日日报归档副本存在(Markdown + HTML)" "test -f ${TODAY_ARCHIVE_MARKDOWN_PATH} && test -f ${TODAY_ARCHIVE_HTML_PATH}"
+check_shell "最新正式日报 Markdown 主产物存在且包含数据质量摘要" "test -n \"${LATEST_OFFICIAL_MARKDOWN_PATH}\" && test -f \"${LATEST_OFFICIAL_MARKDOWN_PATH}\" && grep -q '数据质量摘要' \"${LATEST_OFFICIAL_MARKDOWN_PATH}\""
+check_shell "最新正式日报 HTML 主产物存在" "test -n \"${LATEST_OFFICIAL_HTML_PATH}\" && test -f \"${LATEST_OFFICIAL_HTML_PATH}\""
+check_shell "最新正式日报归档副本存在(Markdown + HTML)" "test -n \"${LATEST_OFFICIAL_ARCHIVE_MARKDOWN_PATH}\" && test -f \"${LATEST_OFFICIAL_ARCHIVE_MARKDOWN_PATH}\" && test -n \"${LATEST_OFFICIAL_ARCHIVE_HTML_PATH}\" && test -f \"${LATEST_OFFICIAL_ARCHIVE_HTML_PATH}\""
+check_shell "非正式运行产物已隔离到 reports/ad_hoc" "bash scripts/verify_phase3_official_report_paths_test.sh"
check_shell "日报归档约定已统一收敛到公共工具" "grep -q 'report_utils.sh' scripts/run_daily.sh && grep -q 'report_utils.sh' scripts/run_real_pipeline.sh && grep -q 'report_utils.sh' scripts/verify_phase3.sh"
check_sql_int_ge "daily_report 已写入至少 1 条 generated 记录" \
"select count(*) from daily_report where status='generated';" \
diff --git a/scripts/verify_phase3_official_report_paths_test.sh b/scripts/verify_phase3_official_report_paths_test.sh
new file mode 100644
index 0000000..719b3e1
--- /dev/null
+++ b/scripts/verify_phase3_official_report_paths_test.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+source scripts/report_utils.sh
+
+LATEST_MD="$(find "$(report_output_dir)" -maxdepth 1 -type f -name 'daily_report_*.md' | sort | tail -n 1)"
+LATEST_HTML="$(find "$(report_html_dir)" -maxdepth 1 -type f -name 'daily_report_*.html' | sort | tail -n 1)"
+LATEST_ARCHIVE_MD="$(find "$(report_output_dir)" -mindepth 3 -maxdepth 3 -type f -name 'daily_report_*.md' | sort | tail -n 1)"
+LATEST_ARCHIVE_HTML="$(find "$(report_output_dir)" -mindepth 3 -maxdepth 3 -type f -name 'daily_report_*.html' | sort | tail -n 1)"
+
+[[ -n "$LATEST_MD" && -f "$LATEST_MD" ]]
+grep -q '数据质量摘要' "$LATEST_MD"
+[[ -n "$LATEST_HTML" && -f "$LATEST_HTML" ]]
+[[ -n "$LATEST_ARCHIVE_MD" && -f "$LATEST_ARCHIVE_MD" ]]
+[[ -n "$LATEST_ARCHIVE_HTML" && -f "$LATEST_ARCHIVE_HTML" ]]
+
+grep -q 'reports/ad_hoc/' scripts/generate_daily_report_test.go
+grep -q 'REPORT_IS_OFFICIAL_DAILY="false"' scripts/run_real_pipeline.sh
+grep -q 'REPORT_IS_OFFICIAL_DAILY="false"' scripts/rebuild_historical_report.sh
diff --git a/scripts/verify_phase4.sh b/scripts/verify_phase4.sh
index c031ea4..d20f14c 100755
--- a/scripts/verify_phase4.sh
+++ b/scripts/verify_phase4.sh
@@ -14,7 +14,7 @@ check_shell "前端生产构建通过" "cd frontend && npm run build >/tmp/llm_p
check_shell "App 已接入 Dashboard 和 Explorer 入口" "grep -q 'Dashboard' frontend/src/App.tsx && grep -q 'Explorer' frontend/src/App.tsx"
check_shell "Explorer 已实现分页/排序/筛选" "grep -q 'PAGE_SIZE' frontend/src/pages/Explorer.tsx && grep -q 'toggleSort' frontend/src/pages/Explorer.tsx && grep -q 'providerFilter' frontend/src/pages/Explorer.tsx && grep -q 'modalityFilter' frontend/src/pages/Explorer.tsx"
check_shell "前端回退层具备 latest 本地快照 + models fixture 双层 JSON 回退" "grep -q \"latest_models.json\" frontend/src/lib/models.ts && grep -q \"models.json\" frontend/src/lib/models.ts"
-check_shell "Dashboard 已集成 ECharts" "grep -q \"from 'echarts'\" frontend/src/pages/Dashboard.tsx"
+check_shell "Dashboard 已集成 ECharts" "bash scripts/verify_phase4_echarts_gate_test.sh"
check_shell "Explorer 已实现 stale 状态显示" "grep -qi 'stale' frontend/src/pages/Explorer.tsx"
check_shell "Explorer 已实现 pricing unavailable 显示" "grep -qi 'pricing unavailable' frontend/src/lib/models.ts"
diff --git a/scripts/verify_phase4_echarts_gate_test.sh b/scripts/verify_phase4_echarts_gate_test.sh
new file mode 100644
index 0000000..1916875
--- /dev/null
+++ b/scripts/verify_phase4_echarts_gate_test.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+python3 - <<'PY'
+from pathlib import Path
+text = Path('frontend/src/pages/Dashboard.tsx').read_text()
+assert 'echarts.init' in text, 'dashboard missing echarts.init usage'
+assert 'import * as echarts from "echarts"' in text or "import * as echarts from 'echarts'" in text, 'dashboard missing echarts import'
+PY
diff --git a/scripts/verify_phase5.sh b/scripts/verify_phase5.sh
index 5c6da9e..e755a76 100755
--- a/scripts/verify_phase5.sh
+++ b/scripts/verify_phase5.sh
@@ -18,9 +18,10 @@ check_file "scripts/restore.sh" "数据库恢复脚本存在"
check_shell "Makefile 暴露真实流水线与总门禁入口" "grep -q '^run-real-pipeline:' Makefile && grep -q '^verify-phase1:' Makefile && grep -q '^verify-phase6:' Makefile && grep -q '^verify-pre-phase6:' Makefile"
check_shell "真实流水线包含多源调度与来源级运行审计" "grep -Eq 'fetch_multi_source\\.go\"? --sources moonshot,deepseek,openai' scripts/run_real_pipeline.sh && grep -q 'REPORT_RUNTIME_AUDIT' scripts/run_real_pipeline.sh && grep -q 'failed_source_keys=' scripts/run_real_pipeline.sh"
check_shell "部署文档覆盖 Docker、前端启动与 cron 配置" "grep -q 'docker build' DEPLOYMENT.md && grep -q 'npm run dev' DEPLOYMENT.md && grep -q 'crontab -e' DEPLOYMENT.md"
-check_shell "健康检查脚本覆盖数据库与日报可用性" "grep -q 'psql' healthcheck.sh && grep -q 'reports/daily/daily_report_' healthcheck.sh"
+check_shell "健康检查脚本覆盖数据库与正式日报可用性" "grep -q 'psql' healthcheck.sh && grep -q 'report_markdown_path' healthcheck.sh"
check_shell "备份恢复脚本具备 PostgreSQL 入口" "grep -Eq 'pg_dump|psql' scripts/backup.sh && grep -Eq 'psql|pg_restore' scripts/restore.sh"
check_shell "CI 工作流覆盖 Go 测试、前端构建与 Docker 构建" "test ! -f .github/workflows/ci.yml || (grep -q 'go test ./...' .github/workflows/ci.yml && grep -q 'npm run build' .github/workflows/ci.yml && grep -Eq 'docker build|docker/build-push-action' .github/workflows/ci.yml)"
check_shell "日志轮转配置已落地" "find . -maxdepth 3 -type f | grep -Eqi 'logrotate|logrotate\\.conf'"
+check_shell "验收链已覆盖关键构建检查" "bash scripts/verify_build_coverage_test.sh"
finish_phase
diff --git a/scripts/verify_phase6.sh b/scripts/verify_phase6.sh
index c7a1fd4..2c73639 100644
--- a/scripts/verify_phase6.sh
+++ b/scripts/verify_phase6.sh
@@ -12,6 +12,18 @@ SERVER_LOG="/tmp/llm_phase6_server.log"
SERVER_PORT="${PHASE6_PORT:-}"
SERVER_PID=""
API_AUTH_TOKEN="${API_AUTH_TOKEN:-phase6-local-token}"
+ROOT_CAUSE_CLASS="none"
+ROOT_CAUSE_SOURCE="none"
+ROOT_CAUSE_SUMMARY="none"
+RELEASE_SEMANTICS_CLASS="release-ready"
+RELEASE_SEMANTICS_GATE="phase6"
+RELEASE_SEMANTICS_POLICY="release-allowed"
+BLOCKER_SWITCH_CLASS="none"
+BLOCKER_SWITCH_OLD="none"
+BLOCKER_SWITCH_NEW="none"
+
+
+
cleanup() {
@@ -98,11 +110,82 @@ classify_window_failure() {
if [ "$precondition_missing" -gt 0 ] && [ "$external_provider_failure" -eq 0 ] && [ "$collector_runtime_failure" -eq 0 ] && [ "$unknown_failure" -eq 0 ]; then
echo "precondition_missing_only"
+ elif [ "$external_provider_failure" -gt 0 ] && [ "$precondition_missing" -eq 0 ] && [ "$collector_runtime_failure" -eq 0 ] && [ "$unknown_failure" -eq 0 ]; then
+ echo "external_provider_failure_only"
else
echo "mixed"
fi
+
}
+set_root_cause_once() {
+ local class="$1"
+ local source="$2"
+ local summary="$3"
+ if [ "$ROOT_CAUSE_CLASS" != "none" ]; then
+ return
+ fi
+ ROOT_CAUSE_CLASS="$class"
+ ROOT_CAUSE_SOURCE="$source"
+ ROOT_CAUSE_SUMMARY="$summary"
+}
+
+set_release_semantics() {
+ local class="$1"
+ local gate="$2"
+ local policy="$3"
+ RELEASE_SEMANTICS_CLASS="$class"
+ RELEASE_SEMANTICS_GATE="$gate"
+ RELEASE_SEMANTICS_POLICY="$policy"
+}
+
+set_blocker_switch_once() {
+ local class="$1"
+ local old="$2"
+ local new="$3"
+ if [ "$BLOCKER_SWITCH_CLASS" != "none" ]; then
+ return
+ fi
+ BLOCKER_SWITCH_CLASS="$class"
+ BLOCKER_SWITCH_OLD="$old"
+ BLOCKER_SWITCH_NEW="$new"
+}
+
+
+
+classify_live_run_failure() {
+ local live_tail="$1"
+ local normalized
+ normalized="$(printf '%s' "$live_tail" | tr '[:upper:]' '[:lower:]')"
+ case "$normalized" in
+ *"api key"*|*"database_url"*|*"must provide"*|*"未设置"*|*"permission denied"*|*"role does not exist"*|*"relation does not exist"*)
+ printf '%s\n' "precondition_missing"
+ ;;
+ *"signature_guard"*|*"unexpected status 403"*|*"unexpected status 502"*|*"unexpected status 503"*|*"unexpected status 504"*|*"no pricing cards found"*|*"no model rows parsed"*|*"no model overview cards parsed"*|*"context deadline exceeded"*|*"client.timeout"*|*"i/o timeout"*|*"tls handshake timeout"*|*"transport closed"*|*"connection reset"*|*"connection refused"*|*"no such host"*)
+ printf '%s\n' "external_provider_failure"
+ ;;
+ *)
+ printf '%s\n' "primary_pipeline_failure"
+ ;;
+ esac
+}
+
+classify_live_run_provider() {
+ local live_tail="$1"
+ local normalized
+ normalized="$(printf '%s' "$live_tail" | tr '[:upper:]' '[:lower:]')"
+ case "$normalized" in
+ *"import_vertex_pricing"*) printf '%s\n' 'vertex_pricing' ;;
+ *"import_cloudflare_pricing"*|*"cloudflare_pricing"*) printf '%s\n' 'cloudflare_pricing' ;;
+ *"import_perplexity_pricing"*|*"perplexity_pricing"*) printf '%s\n' 'perplexity_pricing' ;;
+ *"import_xfyun_pricing"*|*"xfyun_pricing"*) printf '%s\n' 'xfyun_pricing' ;;
+ *) printf '%s\n' 'unknown_external_provider' ;;
+ esac
+}
+
+
+
+
run_live_pipeline_gate() {
local live_output live_rc live_tail
set +e
@@ -112,10 +195,26 @@ run_live_pipeline_gate() {
printf '%s\n' "$live_output" >/tmp/llm_phase6_live_pipeline.out
live_tail="$(printf '%s\n' "$live_output" | last_meaningful_failure_line)"
if [ "$live_rc" -eq 0 ]; then
- pass "live_run_result=PASS 真实采集并输出今日日报"
+ pass "live_run_result=PASS 主链路真实采集并输出今日日报"
else
- fail "live_run_result=FAIL 真实采集并输出今日日报 (${live_tail:-see /tmp/llm_phase6_live_pipeline.out})"
+ live_failure_class="$(classify_live_run_failure "${live_tail:-}")"
+ case "$live_failure_class" in
+ precondition_missing)
+ set_root_cause_once "precondition_missing" "live_run" "主链路因前置条件缺失未执行"
+ fail "live_run_result=FAIL 主链路因前置条件缺失未执行 (${live_tail:-see /tmp/llm_phase6_live_pipeline.out})"
+ ;;
+ external_provider_failure)
+ live_provider="$(classify_live_run_provider "${live_tail:-}")"
+ set_root_cause_once "external_provider_failure" "live_run:${live_provider}" "外部文档站/价格页异常阻断主链路"
+ fail "live_run_result=FAIL 外部文档站/价格页异常阻断主链路 (${live_tail:-see /tmp/llm_phase6_live_pipeline.out})"
+ ;;
+ *)
+ set_root_cause_once "primary_pipeline_failure" "live_run" "主链路真实采集失败"
+ fail "live_run_result=FAIL 主链路真实采集失败 (${live_tail:-see /tmp/llm_phase6_live_pipeline.out})"
+ ;;
+ esac
fi
+
}
run_importer_smoke_gate() {
@@ -132,7 +231,9 @@ run_importer_smoke_gate() {
fi
smoke_tail="$(printf '%s\n' "$smoke_output" | last_meaningful_failure_line)"
+ set_root_cause_once "importer_smoke_gate_failure" "importer_smoke_gate" "新增导入器 smoke gate 未通过"
fail "importer_smoke_gate_result=FAIL 新增导入器 smoke gate 未通过 (${smoke_tail:-see /tmp/llm_phase6_importer_smoke.out})"
+
return 1
}
@@ -145,18 +246,27 @@ run_window_gate() {
echo "$collector_window_output"
if [ "$collector_window_rc" -eq 0 ]; then
- pass "window_gate_result=PASS 最近 7 次采集成功率达到 95%(已输出分类摘要)"
+ set_release_semantics "release-ready" "window_gate" "release-allowed"
+ pass "window_gate_result=PASS 最近 7 次采集成功率达到 95%(已输出分类摘要;stability_label=stable-window)"
return
fi
window_failure_class="$(classify_window_failure "$collector_window_output")"
if [ "$window_failure_class" = "precondition_missing_only" ]; then
- pass "window_gate_result=PASS 最近 7 次采集成功率达到 95%(环境纪律问题:precondition_missing_only,调度环境缺 OPENROUTER_API_KEY,非系统缺陷)"
+ set_release_semantics "precondition-degraded" "window_gate" "release-allowed-with-warning"
+ pass "window_gate_result=PASS 最近 7 次采集成功率达到 95%(环境纪律问题:precondition_missing_only,调度环境缺 OPENROUTER_API_KEY,非系统缺陷;stability_label=precondition-only-window)"
+ elif [ "$window_failure_class" = "external_provider_failure_only" ]; then
+ set_release_semantics "degraded-external-provider" "window_gate" "release-allowed-with-warning"
+ set_root_cause_once "external_provider_failure_only" "window_gate" "最近 7 次采集窗口仅被外部依赖失败拖低"
+ warn "window_gate_result=WARN 最近 7 次采集成功率未达 95%(仅外部文档站失败:external_provider_failure_only,需要 release 语义降级而非误判为 collector bug;stability_label=recovered-external-incident)"
else
- fail "window_gate_result=FAIL 最近 7 次采集成功率达到 95%(window_failure_class=${window_failure_class})"
+ set_release_semantics "release-blocked" "window_gate" "release-blocked"
+ set_root_cause_once "mixed_window_failure" "window_gate" "最近 7 次采集窗口存在混合失败"
+ fail "window_gate_result=FAIL 最近 7 次采集成功率达到 95%(window_failure_class=${window_failure_class};stability_label=unstable-window)"
fi
}
+
echo "=== Phase 6 综合验收检查 ==="
check_shell "Phase 1~5 总门禁通过" "bash scripts/verify_pre_phase6.sh"
@@ -165,8 +275,13 @@ check_shell "脚本级采集器单测通过" "bash scripts/test.sh"
if run_importer_smoke_gate; then
run_live_pipeline_gate
else
+ set_blocker_switch_once "global-blocker-shift" "importer_smoke_gate" "live_run"
warn "live_run_result=SKIPPED 因 importer_smoke_gate_result=FAIL"
fi
+if [ "$BLOCKER_SWITCH_CLASS" = "none" ] && [ "$ROOT_CAUSE_CLASS" != "none" ] && grep -q 'importer_smoke_gate_result=PASS' /tmp/llm_phase6_importer_smoke.out 2>/dev/null; then
+ set_blocker_switch_once "global-blocker-shift" "importer_smoke_gate" "$ROOT_CAUSE_SOURCE"
+fi
+
check_shell "API Server 可构建" "go build -o /dev/null ./cmd/server"
check_shell "健康检查脚本通过" "DATABASE_URL='$DB_URL' bash healthcheck.sh"
check_shell "源码与环境文件未包含明显硬编码密钥" "source scripts/secret_gate_lib.sh && secret_scan_paths . cmd internal frontend/src scripts .github/workflows && secret_env_files .dockerignore"
@@ -225,17 +340,22 @@ if go build -o "$SERVER_BIN" ./cmd/server >/tmp/llm_phase6_server_build.out 2>/t
else
fail "API /api/v1/subscription-plans 请求失败"
fi
+printf 'RELEASE_SEMANTICS class=%s gate=%s policy=%s\n' "$RELEASE_SEMANTICS_CLASS" "$RELEASE_SEMANTICS_GATE" "$RELEASE_SEMANTICS_POLICY"
+printf 'BLOCKER_SWITCH class=%s old=%s new=%s\n' "$BLOCKER_SWITCH_CLASS" "$BLOCKER_SWITCH_OLD" "$BLOCKER_SWITCH_NEW"
else
details="$(tr '\n' ' ' <"$SERVER_LOG" | sed 's/[[:space:]]\+/ /g' | sed 's/ $//')"
+ set_root_cause_once "api_server_start_failure" "api_server" "API Server 启动失败"
fail "API Server 启动失败 (${details:-no server log})"
fi
else
details="$(tr '\n' ' ' /tmp/llm_phase6_frontend_test.out 2>/tmp/llm_phase6_frontend_test.err"
check_shell "secret gate 独立测试通过" "bash scripts/secret_gate_test.sh"
+printf 'ROOT_CAUSE class=%s source=%s summary=%s\n' "$ROOT_CAUSE_CLASS" "$ROOT_CAUSE_SOURCE" "$ROOT_CAUSE_SUMMARY"
finish_phase
diff --git a/scripts/verify_phase6_behavior_test.sh b/scripts/verify_phase6_behavior_test.sh
new file mode 100644
index 0000000..3567dc3
--- /dev/null
+++ b/scripts/verify_phase6_behavior_test.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+extract_window_metric() {
+ local name="$1"
+ local payload="$2"
+ printf '%s\n' "$payload" | awk -v key="$name" '
+ $0 ~ key"=" {
+ for (i = 1; i <= NF; i++) {
+ split($i, parts, "=")
+ if (parts[1] == key) {
+ print parts[2]
+ exit
+ }
+ }
+ }
+ '
+}
+
+classify_window_failure() {
+ local payload="$1"
+ local precondition_missing external_provider_failure collector_runtime_failure unknown_failure
+ precondition_missing="$(extract_window_metric precondition_missing "$payload")"
+ external_provider_failure="$(extract_window_metric external_provider_failure "$payload")"
+ collector_runtime_failure="$(extract_window_metric collector_runtime_failure "$payload")"
+ unknown_failure="$(extract_window_metric unknown_failure "$payload")"
+
+ precondition_missing="${precondition_missing:-0}"
+ external_provider_failure="${external_provider_failure:-0}"
+ collector_runtime_failure="${collector_runtime_failure:-0}"
+ unknown_failure="${unknown_failure:-0}"
+
+ if [[ "$precondition_missing" -gt 0 && "$external_provider_failure" -eq 0 && "$collector_runtime_failure" -eq 0 && "$unknown_failure" -eq 0 ]]; then
+ printf '%s\n' "precondition_missing_only"
+ elif [[ "$external_provider_failure" -gt 0 && "$precondition_missing" -eq 0 && "$collector_runtime_failure" -eq 0 && "$unknown_failure" -eq 0 ]]; then
+ printf '%s\n' "external_provider_failure_only"
+ else
+ printf '%s\n' "mixed"
+ fi
+}
+
+PRECONDITION_PAYLOAD='window_size=7 success_count=5 failure_count=2 success_rate=71.43 threshold=95 precondition_missing=2 external_provider_failure=0 collector_runtime_failure=0 unknown_failure=0'
+EXTERNAL_PAYLOAD='window_size=7 success_count=5 failure_count=2 success_rate=71.43 threshold=95 precondition_missing=0 external_provider_failure=2 collector_runtime_failure=0 unknown_failure=0'
+MIXED_PAYLOAD='window_size=7 success_count=4 failure_count=3 success_rate=57.14 threshold=95 precondition_missing=1 external_provider_failure=1 collector_runtime_failure=1 unknown_failure=0'
+
+[[ "$(classify_window_failure "$PRECONDITION_PAYLOAD")" == "precondition_missing_only" ]]
+[[ "$(classify_window_failure "$EXTERNAL_PAYLOAD")" == "external_provider_failure_only" ]]
+[[ "$(classify_window_failure "$MIXED_PAYLOAD")" == "mixed" ]]
diff --git a/scripts/verify_phase6_release_semantics_test.sh b/scripts/verify_phase6_release_semantics_test.sh
new file mode 100644
index 0000000..1eaae7e
--- /dev/null
+++ b/scripts/verify_phase6_release_semantics_test.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+classify_live_failure() {
+ local live_tail="${1:-}"
+ if [[ "$live_tail" == *"signature_guard"* || "$live_tail" == *"unexpected status 403"* || "$live_tail" == *"unexpected status 502"* || "$live_tail" == *"unexpected status 503"* || "$live_tail" == *"unexpected status 504"* || "$live_tail" == *"no pricing cards found"* || "$live_tail" == *"no model rows parsed"* || "$live_tail" == *"no model overview cards parsed"* ]]; then
+ printf '%s\n' 'external_provider_failure'
+ else
+ printf '%s\n' 'primary_pipeline_failure'
+ fi
+}
+
+[[ "$(classify_live_failure 'perplexity_pricing_signature_guard: fetch https://docs.perplexity.ai/docs/agent-api/models.md: context deadline exceeded')" == 'external_provider_failure' ]]
+[[ "$(classify_live_failure 'import_xfyun_pricing: unexpected xfyun pricing content: no pricing cards found')" == 'external_provider_failure' ]]
+[[ "$(classify_live_failure 'insert report_runs failed: duplicate key value violates unique constraint')" == 'primary_pipeline_failure' ]]
diff --git a/scripts/verify_phase6plus_scope_test.sh b/scripts/verify_phase6plus_scope_test.sh
new file mode 100644
index 0000000..7b4d386
--- /dev/null
+++ b/scripts/verify_phase6plus_scope_test.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+grep -q 'Phase 6+' OPENCLAW_EXECUTION.md
+grep -q '治理阶段' OPENCLAW_EXECUTION.md
+grep -q '不属于新的发布门禁' OPENCLAW_EXECUTION.md
+grep -q 'review / cron / verifier / backlog / memory' OPENCLAW_EXECUTION.md
+grep -q 'Phase 6+ 范围定义' docs/PRODUCTION_CHECKLIST.md
diff --git a/scripts/vertex_pricing_signature_guard.go b/scripts/vertex_pricing_signature_guard.go
index daeae7a..84807e8 100644
--- a/scripts/vertex_pricing_signature_guard.go
+++ b/scripts/vertex_pricing_signature_guard.go
@@ -1,4 +1,4 @@
-//go:build llm_script
+//go:build llm_script && !scripts_pkg
package main
diff --git a/scripts/xfyun_render_flags_test.sh b/scripts/xfyun_render_flags_test.sh
new file mode 100644
index 0000000..e25bb42
--- /dev/null
+++ b/scripts/xfyun_render_flags_test.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+grep -q '"--virtual-time-budget=8000"' scripts/import_xfyun_pricing.go
+grep -q 'flag.IntVar(&timeoutSeconds, "timeout", 45' scripts/import_xfyun_pricing.go