main.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. // Command dualrun 是 AC-15 双跑 harness 的 CLI 入口。
  2. //
  3. // 用法:
  4. //
  5. // dualrun stage1 --case-set real100 # 真实基线 100 条
  6. // dualrun stage1 --case-set synthetic # 合成边界 6 类 × 5
  7. // dualrun stage1 --all --out report.json --md report.md
  8. // dualrun stage1 --dry-run # 只列 case 不执行
  9. // dualrun stage1 --all --idempotency # 额外做幂等验证
  10. //
  11. // 环境变量(口令只经环境变量,不落盘):
  12. //
  13. // AC15_DB_HOST / AC15_DB_PORT / AC15_DB_NAME / AC15_DB_USER / AC15_DB_PASS
  14. // AC15_LEGACY_REPO legacy 应用根目录(默认 E:/study/php/ysw-finance/yuansiwei-finance)
  15. // AC15_PHP_BIN php 可执行(默认 php)
  16. //
  17. // 退出码:0 = 跑完(无论结论如何);2 = 参数错误;3 = 环境层面失败(守卫/断言/连接)。
  18. package main
  19. import (
  20. "context"
  21. "flag"
  22. "fmt"
  23. "os"
  24. "yuansiwei/unified-backend/internal/academic/dualrun"
  25. )
  26. func main() {
  27. if len(os.Args) < 2 {
  28. usage()
  29. os.Exit(2)
  30. }
  31. switch os.Args[1] {
  32. case "stage1":
  33. os.Exit(runStage1(os.Args[2:]))
  34. case "-h", "--help", "help":
  35. usage()
  36. os.Exit(0)
  37. default:
  38. fmt.Fprintf(os.Stderr, "未知子命令:%s\n\n", os.Args[1])
  39. usage()
  40. os.Exit(2)
  41. }
  42. }
  43. func usage() {
  44. fmt.Fprint(os.Stderr, `dualrun —— AC-15 双跑 harness(阶段 1:课酬链)
  45. 子命令:
  46. stage1 跑课酬链比对
  47. stage1 参数:
  48. --case-set string real100 | synthetic | all(默认 all)
  49. --all 等价于 --case-set all
  50. --dry-run 只列 case 不执行
  51. --out string JSON 报告路径(默认 dualrun-stage1.json)
  52. --md string Markdown 报告路径(默认 dualrun-stage1.md)
  53. --idempotency 额外做幂等验证(同 case 连跑两次)
  54. --real-count int 真实基线条数(默认 100)
  55. --per-cat int 合成每类条数(默认 5)
  56. `)
  57. }
  58. func runStage1(args []string) int {
  59. fs := flag.NewFlagSet("stage1", flag.ContinueOnError)
  60. fs.SetOutput(os.Stderr)
  61. caseSet := fs.String("case-set", "all", "real100 | synthetic | all")
  62. all := fs.Bool("all", false, "等价于 --case-set all")
  63. dryRun := fs.Bool("dry-run", false, "只列 case 不执行")
  64. out := fs.String("out", "dualrun-stage1.json", "JSON 报告路径")
  65. md := fs.String("md", "dualrun-stage1.md", "Markdown 报告路径")
  66. idem := fs.Bool("idempotency", false, "幂等验证")
  67. realCount := fs.Int("real-count", 100, "真实基线条数")
  68. perCat := fs.Int("per-cat", 5, "合成每类条数")
  69. if err := fs.Parse(args); err != nil {
  70. return 2
  71. }
  72. if *all {
  73. *caseSet = "all"
  74. }
  75. cfg := dualrun.LoadConfigFromEnv()
  76. // 守卫违反会 panic(验收要求),这里转成清晰的退出码与提示
  77. exit := 0
  78. func() {
  79. defer func() {
  80. if r := recover(); r != nil {
  81. fmt.Fprintf(os.Stderr, "FATAL: %v\n", r)
  82. exit = 3
  83. }
  84. }()
  85. rep, err := dualrun.RunStage1(context.Background(), cfg, dualrun.Options{
  86. CaseSet: *caseSet,
  87. DryRun: *dryRun,
  88. OutJSON: *out,
  89. OutMD: *md,
  90. Idempotency: *idem,
  91. RealCount: *realCount,
  92. PerCat: *perCat,
  93. })
  94. if err != nil {
  95. fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
  96. exit = 3
  97. return
  98. }
  99. fmt.Printf("cases=%d dryRun=%v\n", len(rep.Cases)+len(rep.DryRunCases), rep.DryRun)
  100. for _, a := range dualrun.AllAttributions {
  101. if n := rep.Summary[a]; n > 0 {
  102. fmt.Printf(" %-24s %d\n", a, n)
  103. }
  104. }
  105. if rep.DryRun {
  106. fmt.Printf("dry-run 清单已写入 %s / %s\n", *out, *md)
  107. } else {
  108. fmt.Printf("报告已写入 %s / %s\n", *out, *md)
  109. }
  110. }()
  111. return exit
  112. }