fix: harden review and verifier governance
This commit is contained in:
69
scripts/apply_script_test_tags.py
Normal file
69
scripts/apply_script_test_tags.py
Normal 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 ''))
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
68
scripts/cron_status_report.sh
Executable 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}"
|
||||
32
scripts/cron_status_report_test.sh
Normal file
32
scripts/cron_status_report_test.sh
Normal 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"
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
// fetch_multi_source.go - 多源 LLM 定价采集器
|
||||
// 支持: OpenRouter, Moonshot, DeepSeek, OpenAI 等
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
21
scripts/git_commit_status_report.sh
Executable file
21
scripts/git_commit_status_report.sh
Executable 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}"
|
||||
36
scripts/git_commit_status_test.sh
Normal file
36
scripts/git_commit_status_test.sh
Normal 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'
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -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模型":
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
29
scripts/list_testable_script_entries.sh
Executable file
29
scripts/list_testable_script_entries.sh
Executable 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
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
5
scripts/package_stub.go
Normal file
5
scripts/package_stub.go
Normal 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.
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
22
scripts/pipeline_script_includes_signature_query_test.sh
Normal file
22
scripts/pipeline_script_includes_signature_query_test.sh
Normal 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
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build llm_script
|
||||
//go:build llm_script && !scripts_pkg
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -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" ]]
|
||||
|
||||
@@ -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:-}")"
|
||||
|
||||
37
scripts/review/backlog_blocker_freshness_guard.sh
Executable file
37
scripts/review/backlog_blocker_freshness_guard.sh
Executable 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
|
||||
41
scripts/review/backlog_blocker_freshness_guard_test.sh
Normal file
41
scripts/review/backlog_blocker_freshness_guard_test.sh
Normal 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:10(afternoon-review cron)
|
||||
- freshness_hint=same-day-blocker-switch old=sensenova-live new=xfyun-live
|
||||
EOF
|
||||
|
||||
set +e
|
||||
bash scripts/review/backlog_blocker_freshness_guard.sh "$BAD_BACKLOG" >/tmp/backlog_blocker_bad.out 2>&1
|
||||
BAD_RC=$?
|
||||
set -e
|
||||
[[ "$BAD_RC" -ne 0 ]]
|
||||
grep -q 'stale blocker still present in current table' /tmp/backlog_blocker_bad.out
|
||||
|
||||
GOOD_BACKLOG="$TMP_DIR/backlog-good.md"
|
||||
cat > "$GOOD_BACKLOG" <<'EOF'
|
||||
## 当前未修复问题速查表(截至 2026-05-29 15:10)
|
||||
| # | 问题 | 优先级 | 首次暴露 | 修复状态 | 影响次数 |
|
||||
|---|------|--------|----------|----------|----------|
|
||||
| 48 | xfyun-live smoke FAIL 导致 live_run SKIP 传导链 | P1 | 05-29 15:10 | ❌ 未修复 | 首次暴露 |
|
||||
---
|
||||
### 2026-05-29 15:10(afternoon-review cron)
|
||||
- freshness_hint=same-day-blocker-switch old=sensenova-live new=xfyun-live
|
||||
EOF
|
||||
|
||||
bash scripts/review/backlog_blocker_freshness_guard.sh "$GOOD_BACKLOG" >/tmp/backlog_blocker_good.out 2>&1
|
||||
grep -q 'BACKLOG_BLOCKER_FRESHNESS_GUARD: PASS' /tmp/backlog_blocker_good.out
|
||||
25
scripts/review/backlog_current_freshness_guard.sh
Executable file
25
scripts/review/backlog_current_freshness_guard.sh
Executable 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
|
||||
36
scripts/review/backlog_current_freshness_guard_test.sh
Normal file
36
scripts/review/backlog_current_freshness_guard_test.sh
Normal 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
|
||||
28
scripts/review/backlog_current_table_guard.sh
Executable file
28
scripts/review/backlog_current_table_guard.sh
Executable 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
|
||||
38
scripts/review/backlog_current_table_guard_test.sh
Normal file
38
scripts/review/backlog_current_table_guard_test.sh
Normal 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
|
||||
31
scripts/review/backlog_revocation_guard.sh
Executable file
31
scripts/review/backlog_revocation_guard.sh
Executable 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
|
||||
40
scripts/review/backlog_revocation_guard_test.sh
Normal file
40
scripts/review/backlog_revocation_guard_test.sh
Normal 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
|
||||
14
scripts/review/blocker_switch_guard.sh
Executable file
14
scripts/review/blocker_switch_guard.sh
Executable 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"
|
||||
31
scripts/review/blocker_switch_guard_test.sh
Normal file
31
scripts/review/blocker_switch_guard_test.sh
Normal 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:10(afternoon-review cron)
|
||||
- xfyun-live smoke FAIL 替换 sensenova-live
|
||||
EOF
|
||||
|
||||
set +e
|
||||
bash scripts/review/blocker_switch_guard.sh "$BAD_FILE" >/tmp/blocker_switch_bad.out 2>&1
|
||||
BAD_RC=$?
|
||||
set -e
|
||||
[[ "$BAD_RC" -ne 0 ]]
|
||||
grep -q 'missing blocker switch freshness hint' /tmp/blocker_switch_bad.out
|
||||
|
||||
GOOD_FILE="$TMP_DIR/review-switch-good.txt"
|
||||
cat > "$GOOD_FILE" <<'EOF'
|
||||
### 2026-05-29 15:10(afternoon-review cron)
|
||||
- xfyun-live smoke FAIL 替换 sensenova-live
|
||||
- freshness_hint=same-day-blocker-switch old=sensenova-live new=xfyun-live
|
||||
EOF
|
||||
|
||||
bash scripts/review/blocker_switch_guard.sh "$GOOD_FILE" >/tmp/blocker_switch_good.out 2>&1
|
||||
grep -q 'BLOCKER_SWITCH_GUARD: PASS' /tmp/blocker_switch_good.out
|
||||
12
scripts/review/current_row_revocation_guard.sh
Executable file
12
scripts/review/current_row_revocation_guard.sh
Executable 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"
|
||||
12
scripts/review/global_blocker_switch_capture_test.sh
Normal file
12
scripts/review/global_blocker_switch_capture_test.sh
Normal 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"
|
||||
20
scripts/review/global_blocker_switch_guard.sh
Executable file
20
scripts/review/global_blocker_switch_guard.sh
Executable 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"
|
||||
33
scripts/review/global_blocker_switch_guard_test.sh
Normal file
33
scripts/review/global_blocker_switch_guard_test.sh
Normal 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
|
||||
23
scripts/review/live_run_classification_test.sh
Normal file
23
scripts/review/live_run_classification_test.sh
Normal 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' ]]
|
||||
12
scripts/review/partial_output_guard.sh
Executable file
12
scripts/review/partial_output_guard.sh
Executable 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"
|
||||
30
scripts/review/partial_output_guard_test.sh
Normal file
30
scripts/review/partial_output_guard_test.sh
Normal 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
|
||||
20
scripts/review/provider_root_cause_test.sh
Normal file
20
scripts/review/provider_root_cause_test.sh
Normal 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' ]]
|
||||
12
scripts/review/release_semantics_capture_test.sh
Normal file
12
scripts/review/release_semantics_capture_test.sh
Normal 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"
|
||||
11
scripts/review/release_semantics_guard.sh
Executable file
11
scripts/review/release_semantics_guard.sh
Executable 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"
|
||||
29
scripts/review/release_semantics_guard_test.sh
Normal file
29
scripts/review/release_semantics_guard_test.sh
Normal 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_only;stability_label=recovered-external-incident)
|
||||
EOF
|
||||
|
||||
set +e
|
||||
bash scripts/review/release_semantics_guard.sh "$BAD_FILE" >/tmp/release_semantics_bad.out 2>&1
|
||||
BAD_RC=$?
|
||||
set -e
|
||||
[[ "$BAD_RC" -ne 0 ]]
|
||||
grep -q 'missing release semantics summary' /tmp/release_semantics_bad.out
|
||||
|
||||
GOOD_FILE="$TMP_DIR/good-phase6.txt"
|
||||
cat > "$GOOD_FILE" <<'EOF'
|
||||
[WARN] window_gate_result=WARN 最近 7 次采集成功率未达 95%(仅外部文档站失败:external_provider_failure_only;stability_label=recovered-external-incident)
|
||||
RELEASE_SEMANTICS class=degraded-external-provider gate=window_gate policy=release-allowed-with-warning
|
||||
EOF
|
||||
|
||||
bash scripts/review/release_semantics_guard.sh "$GOOD_FILE" >/tmp/release_semantics_good.out 2>&1
|
||||
grep -q 'RELEASE_SEMANTICS_GUARD: PASS' /tmp/release_semantics_good.out
|
||||
35
scripts/review/review_action_guard.sh
Executable file
35
scripts/review/review_action_guard.sh
Executable 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"
|
||||
47
scripts/review/review_action_guard_test.sh
Normal file
47
scripts/review/review_action_guard_test.sh
Normal 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
|
||||
20
scripts/review/review_aging_priority_test.sh
Normal file
20
scripts/review/review_aging_priority_test.sh
Normal 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'
|
||||
22
scripts/review/review_same_day_no_decision_test.sh
Normal file
22
scripts/review/review_same_day_no_decision_test.sh
Normal 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'
|
||||
69
scripts/review/review_status_summary.sh
Executable file
69
scripts/review/review_status_summary.sh
Executable 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"
|
||||
22
scripts/review/review_status_summary_test.sh
Normal file
22
scripts/review/review_status_summary_test.sh
Normal 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'
|
||||
24
scripts/review/review_truth_guard.sh
Executable file
24
scripts/review/review_truth_guard.sh
Executable 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"
|
||||
62
scripts/review/review_truth_guard_test.sh
Normal file
62
scripts/review/review_truth_guard_test.sh
Normal 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
|
||||
14
scripts/review/review_worktree_status_guard.sh
Executable file
14
scripts/review/review_worktree_status_guard.sh
Executable 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"
|
||||
23
scripts/review/review_worktree_status_guard_test.sh
Normal file
23
scripts/review/review_worktree_status_guard_test.sh
Normal 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
|
||||
11
scripts/review/root_cause_summary_guard.sh
Executable file
11
scripts/review/root_cause_summary_guard.sh
Executable 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"
|
||||
34
scripts/review/root_cause_summary_test.sh
Normal file
34
scripts/review/root_cause_summary_test.sh
Normal 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
|
||||
14
scripts/review/stability_status_guard.sh
Executable file
14
scripts/review/stability_status_guard.sh
Executable 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
Reference in New Issue
Block a user