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-Litesensenova-6.7-flash-lite每5小时1500次面向真实工作流的轻量多模态智能体模型,支持文本对话与图像输入理解
SenseNova U1 Fastsensenova-u1-fast每5小时1500次基于 SenseNova U1 的加速版本,专供信息图(Infographics)生成
DeepSeek V4 Flashdeepseek-v4-flash每5小时500次DeepSeek 高性能对话模型,支持思考/非思考模式、256K 上下文、工具调用
+

图像输入理解

上下文长度 256K tokens

+

POST https://token.sensenova.cn/v1/images/generations

+

256K 上下文

` + + 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 := ` +
X2/X1.5模型
2 元/百万tokens
+
Ultra模型
0.8 元/百万tokens
+
Pro模型
5 元/百万tokens
+
Lite模型
0 元/百万tokens
+ ` + + 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