fix: harden review and verifier governance

This commit is contained in:
phamnazage-jpg
2026-05-29 18:48:48 +08:00
parent 88833fac8b
commit e999d31b25
133 changed files with 2538 additions and 159 deletions

1
.gitignore vendored
View File

@@ -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

View File

@@ -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+ 的目标不是再声明“可发布”,而是防止已经恢复绿色的主链路因为治理退化再次失真。
### 当前运行真相
当前可直接引用的事实是:

View File

@@ -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. 发布前备份

View File

@@ -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

View File

@@ -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 failureredeclared 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 guardreview 不能再无提示传播旧 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 guardreview 已宣告移除的问题不得继续留在 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:10afternoon-review cron
> **前置说明**:距上一次 review05-28 15:10约 **24 小时**。距最后一次 commit88833fa05-27 22:01约 17 小时,无新 commit。工作区在 05-28 review 之后重新积累 75 modified + 46 untracked 共 121 个文件变更。Phase 1/2/3/4/5 全 PASSECharts FAIL 已消失Phase 6 FAILxfyun-live smoke FAILwindow_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 FAILstale current truth snapshotBACKLOG table guard FAILresolved rows=13
- **xfyun-live smoke FAIL 替换 sensenova-live**:本轮 Phase 6 FAIL 根因从 sensenova 切换为 xfyunchromium 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 untrackedBACKLOG 本身也在 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:01cron 失败与最终日报生成时间存在矛盾
- **影响**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 → freshtable guard → resolved_rows=0
### 2026-05-27 15:10afternoon-review cron
> **前置说明**:距上一次 review05-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=<issue>:<priority>:<impact>,...`
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:10afternoon-review cron
> **前置说明**:距上一次 review05-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 FAILverify_phase4 ECharts 断言失败。go test 全 PASS。

View File

@@ -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 ''))

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -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

View File

@@ -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'

68
scripts/cron_status_report.sh Executable file
View File

@@ -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}"

View File

@@ -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"

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
// fetch_multi_source.go - 多源 LLM 定价采集器
// 支持: OpenRouter, Moonshot, DeepSeek, OpenAI 等

View File

@@ -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

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -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),

View File

@@ -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", "")

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -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}"

View File

@@ -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'

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -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)<h4[^>]*>([^<]+)</h4>.*?调用次数限制</p><p[^>]*>每5小时([0-9]+)次</p>.*?MODEL ID</p><code[^>]*>([^<]+)</code>`)
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)<tr[^>]*>\s*<td[^>]*>([^<]+)</td>\s*<td[^>]*>.*?<code[^>]*>([^<]+)</code>.*?</td>\s*<td[^>]*>每5小时([0-9]+)次</td>\s*<td[^>]*>([^<]+)</td>\s*</tr>`)
)
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"))
}

View File

@@ -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 = `<p>GET https://token.sensenova.cn/v1/models</p>
<pre>{"pricing":{"prompt":"0","completion":"0","image":"0","request":"0"}}</pre>
<section id="models-overview"><table><tbody>
<tr><td>SenseNova 6.7 Flash-Lite</td><td><code>sensenova-6.7-flash-lite</code></td><td>每5小时1500次</td><td>面向真实工作流的轻量多模态智能体模型,支持文本对话与图像输入理解</td></tr>
<tr><td>SenseNova U1 Fast</td><td><code>sensenova-u1-fast</code></td><td>每5小时1500次</td><td>基于 SenseNova U1 的加速版本专供信息图Infographics生成</td></tr>
<tr><td>DeepSeek V4 Flash</td><td><code>deepseek-v4-flash</code></td><td>每5小时500次</td><td>DeepSeek 高性能对话模型,支持思考/非思考模式、256K 上下文、工具调用</td></tr>
</tbody></table></section>
<section id="model-flash"><p>图像输入理解</p><p>上下文长度 256K tokens</p></section>
<section id="model-u1"><p>POST https://token.sensenova.cn/v1/images/generations</p></section>
<section id="model-deepseek-v4-flash"><p>256K 上下文</p></section>`
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{

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -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)<div class="apiprice_cardTitle__[^"]+">([^<]+)</div><div class="apiprice_cardPrice__[^"]+"><span>([0-9]+(?:\.[0-9]+)?)</span><span class="[^"]+">元/百万tokens</span>`)
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(
"<br>", "\n",
"<br/>", "\n",
"<br />", "\n",
"</div>", "\n",
"</span>", "\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模型":

View File

@@ -37,6 +37,26 @@ func TestParseXfyunPricingCatalogBuildsRecords(t *testing.T) {
}
}
func TestParseXfyunPricingCatalogSupportsRenderedTextFallback(t *testing.T) {
raw := `
<div class="price-card"><div>X2/X1.5模型</div><div>2 元/百万tokens</div></div>
<div class="price-card"><div>Ultra模型</div><div>0.8 元/百万tokens</div></div>
<div class="price-card"><div>Pro模型</div><div>5 元/百万tokens</div></div>
<div class="price-card"><div>Lite模型</div><div>0 元/百万tokens</div></div>
`
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{

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -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

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

5
scripts/package_stub.go Normal file
View File

@@ -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.

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -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

View File

@@ -1,4 +1,4 @@
//go:build llm_script
//go:build llm_script && !scripts_pkg
package main

View File

@@ -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" ]]

View File

@@ -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:-}")"

View File

@@ -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

View File

@@ -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:10afternoon-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:10afternoon-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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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:10afternoon-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:10afternoon-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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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' ]]

View File

@@ -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"

View File

@@ -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

View File

@@ -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' ]]

View File

@@ -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"

View File

@@ -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"

View File

@@ -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_onlystability_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_onlystability_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

View File

@@ -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"

View File

@@ -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

View File

@@ -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'

View File

@@ -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'

View File

@@ -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"

View File

@@ -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'

View File

@@ -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"

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More