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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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