| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- // 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"
- "strings"
- "yuansiwei/unified-backend/internal/academic/dualrun"
- "yuansiwei/unified-backend/internal/academic/dualrun/stage2"
- )
- func main() {
- if len(os.Args) < 2 {
- usage()
- os.Exit(2)
- }
- switch os.Args[1] {
- case "stage1":
- os.Exit(runStage1(os.Args[2:]))
- case "stage2":
- os.Exit(runStage2(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
- 子命令:
- stage1 跑课酬链比对(课酬,纯函数)
- stage2 跑写 SP 双跑(消费:sp_user_expense_school_expenditure ↔ FundService.Expenditure)
- 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
- }
- // runStage2 跑写 SP 双跑(第一支:消费)。
- func runStage2(args []string) int {
- fs := flag.NewFlagSet("stage2", flag.ContinueOnError)
- fs.SetOutput(os.Stderr)
- only := fs.String("case", "", "只跑指定 case(按名字子串匹配)")
- if err := fs.Parse(args); err != nil {
- return 2
- }
- repoRoot, err := os.Getwd()
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: 取工作目录失败:%v\n", err)
- return 3
- }
- cfg, err := stage2.LoadConfig(repoRoot)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- if err := cfg.Guard(); err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
- return 3
- }
- fmt.Printf("%s\n\n", cfg.Masked())
- ctx := context.Background()
- var results []*stage2.CaseResult
- for _, c := range stage2.DefaultCases("") {
- if *only != "" && !strings.Contains(c.Name, *only) {
- continue
- }
- r, err := stage2.RunOne(ctx, cfg, c)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: case %q 执行失败:%v\n", c.Name, err)
- return 3
- }
- stage2.PrintResult(os.Stdout, r)
- results = append(results, r)
- }
- if ok := stage2.Summarize(os.Stdout, results); !ok {
- return 1
- }
- return 0
- }
|