// Command dualrun 是 AC-15 双跑 harness 的 CLI 入口。 // // 用法: // // dualrun stage1 --case-set real100 # 真实基线 100 条 // dualrun stage1 --case-set synthetic # 合成边界 6 类 × 5 // dualrun stage1 --all --out report.json --md report.md // dualrun stage1 --dry-run # 只列 case 不执行 // dualrun stage1 --all --idempotency # 额外做幂等验证 // // 环境变量(口令只经环境变量,不落盘): // // AC15_DB_HOST / AC15_DB_PORT / AC15_DB_NAME / AC15_DB_USER / AC15_DB_PASS // AC15_LEGACY_REPO legacy 应用根目录(默认 E:/study/php/ysw-finance/yuansiwei-finance) // AC15_PHP_BIN php 可执行(默认 php) // // 退出码:0 = 跑完(无论结论如何);2 = 参数错误;3 = 环境层面失败(守卫/断言/连接)。 package main import ( "context" "flag" "fmt" "os" "yuansiwei/unified-backend/internal/academic/dualrun" ) func main() { if len(os.Args) < 2 { usage() os.Exit(2) } switch os.Args[1] { case "stage1": os.Exit(runStage1(os.Args[2:])) case "-h", "--help", "help": usage() os.Exit(0) default: fmt.Fprintf(os.Stderr, "未知子命令:%s\n\n", os.Args[1]) usage() os.Exit(2) } } func usage() { fmt.Fprint(os.Stderr, `dualrun —— AC-15 双跑 harness(阶段 1:课酬链) 子命令: stage1 跑课酬链比对 stage1 参数: --case-set string real100 | synthetic | all(默认 all) --all 等价于 --case-set all --dry-run 只列 case 不执行 --out string JSON 报告路径(默认 dualrun-stage1.json) --md string Markdown 报告路径(默认 dualrun-stage1.md) --idempotency 额外做幂等验证(同 case 连跑两次) --real-count int 真实基线条数(默认 100) --per-cat int 合成每类条数(默认 5) `) } func runStage1(args []string) int { fs := flag.NewFlagSet("stage1", flag.ContinueOnError) fs.SetOutput(os.Stderr) caseSet := fs.String("case-set", "all", "real100 | synthetic | all") all := fs.Bool("all", false, "等价于 --case-set all") dryRun := fs.Bool("dry-run", false, "只列 case 不执行") out := fs.String("out", "dualrun-stage1.json", "JSON 报告路径") md := fs.String("md", "dualrun-stage1.md", "Markdown 报告路径") idem := fs.Bool("idempotency", false, "幂等验证") realCount := fs.Int("real-count", 100, "真实基线条数") perCat := fs.Int("per-cat", 5, "合成每类条数") if err := fs.Parse(args); err != nil { return 2 } if *all { *caseSet = "all" } cfg := dualrun.LoadConfigFromEnv() // 守卫违反会 panic(验收要求),这里转成清晰的退出码与提示 exit := 0 func() { defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", r) exit = 3 } }() rep, err := dualrun.RunStage1(context.Background(), cfg, dualrun.Options{ CaseSet: *caseSet, DryRun: *dryRun, OutJSON: *out, OutMD: *md, Idempotency: *idem, RealCount: *realCount, PerCat: *perCat, }) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: %v\n", err) exit = 3 return } fmt.Printf("cases=%d dryRun=%v\n", len(rep.Cases)+len(rep.DryRunCases), rep.DryRun) for _, a := range dualrun.AllAttributions { if n := rep.Summary[a]; n > 0 { fmt.Printf(" %-24s %d\n", a, n) } } if rep.DryRun { fmt.Printf("dry-run 清单已写入 %s / %s\n", *out, *md) } else { fmt.Printf("报告已写入 %s / %s\n", *out, *md) } }() return exit }