@customize-agent/server 4.59.1 → 4.59.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +33 -28
  3. package/.next/prerender-manifest.json +3 -3
  4. package/.next/required-server-files.json +5 -2
  5. package/.next/routes-manifest.json +16 -0
  6. package/.next/server/chunks/1001.js +2 -56
  7. package/.next/server/chunks/1243.js +3 -0
  8. package/.next/server/chunks/1928.js +1 -1
  9. package/.next/server/chunks/1936.js +2 -2
  10. package/.next/server/chunks/2328.js +6 -0
  11. package/.next/server/chunks/2434.js +4 -4
  12. package/.next/server/chunks/3806.js +1 -0
  13. package/.next/server/chunks/4974.js +2 -0
  14. package/.next/server/chunks/5304.js +2 -0
  15. package/.next/server/chunks/5746.js +5 -0
  16. package/.next/server/chunks/6321.js +1 -3
  17. package/.next/server/chunks/7931.js +142 -0
  18. package/.next/server/chunks/8204.js +1 -1
  19. package/.next/server/chunks/9666.js +142 -0
  20. package/.next/server/instrumentation.js +1 -0
  21. package/.next/server/instrumentation.js.nft.json +1 -0
  22. package/.next/server/middleware-build-manifest.js +1 -1
  23. package/.next/server/pages/404.html +1 -1
  24. package/.next/server/pages/500.html +1 -1
  25. package/.next/server/pages/api/assets/generated/preview.js +1 -1
  26. package/.next/server/pages/api/assets/generated/preview.js.nft.json +1 -1
  27. package/.next/server/pages/api/assets/generated.js +1 -1
  28. package/.next/server/pages/api/assets/generated.js.nft.json +1 -1
  29. package/.next/server/pages/api/config/embedding/healthCheck.js +2 -1
  30. package/.next/server/pages/api/config/embedding/healthCheck.js.nft.json +1 -1
  31. package/.next/server/pages/api/config/embedding.js +2 -1
  32. package/.next/server/pages/api/config/embedding.js.nft.json +1 -1
  33. package/.next/server/pages/api/config/healthCheck.js +2 -2
  34. package/.next/server/pages/api/config/healthCheck.js.nft.json +1 -1
  35. package/.next/server/pages/api/config/models.js +2 -1
  36. package/.next/server/pages/api/config/models.js.nft.json +1 -1
  37. package/.next/server/pages/api/config/providers/[provider].js +2 -1
  38. package/.next/server/pages/api/config/providers/[provider].js.nft.json +1 -1
  39. package/.next/server/pages/api/config/providers.js +2 -2
  40. package/.next/server/pages/api/config/providers.js.nft.json +1 -1
  41. package/.next/server/pages/api/config/web-access.js +2 -1
  42. package/.next/server/pages/api/config/web-access.js.nft.json +1 -1
  43. package/.next/server/pages/api/documents/canvas/[id].js +1 -2
  44. package/.next/server/pages/api/documents/canvas/[id].js.nft.json +1 -1
  45. package/.next/server/pages/api/documents/export.js +11 -9
  46. package/.next/server/pages/api/documents/export.js.nft.json +1 -1
  47. package/.next/server/pages/api/documents/generate/preview.js +1 -2
  48. package/.next/server/pages/api/documents/generate/preview.js.nft.json +1 -1
  49. package/.next/server/pages/api/documents/generate.js +1 -2
  50. package/.next/server/pages/api/documents/generate.js.nft.json +1 -1
  51. package/.next/server/pages/api/documents/generated/[id]/events.js +2 -3
  52. package/.next/server/pages/api/documents/generated/[id]/events.js.nft.json +1 -1
  53. package/.next/server/pages/api/documents/generated/[id]/figure/[name].js +1 -1
  54. package/.next/server/pages/api/documents/generated/[id]/figure/[name].js.nft.json +1 -1
  55. package/.next/server/pages/api/documents/generated/[id]/instruction.js +2 -0
  56. package/.next/server/pages/api/documents/generated/[id]/instruction.js.nft.json +1 -0
  57. package/.next/server/pages/api/documents/generated/[id]/nodes/[nodeId].js +1 -2
  58. package/.next/server/pages/api/documents/generated/[id]/nodes/[nodeId].js.nft.json +1 -1
  59. package/.next/server/pages/api/documents/generated/[id]/nodes.js +1 -2
  60. package/.next/server/pages/api/documents/generated/[id]/nodes.js.nft.json +1 -1
  61. package/.next/server/pages/api/documents/generated/[id]/pause.js +1 -2
  62. package/.next/server/pages/api/documents/generated/[id]/pause.js.nft.json +1 -1
  63. package/.next/server/pages/api/documents/generated/[id]/preview.js +1 -2
  64. package/.next/server/pages/api/documents/generated/[id]/preview.js.nft.json +1 -1
  65. package/.next/server/pages/api/documents/generated/[id]/repair/[rid]/confirm.js +1 -1
  66. package/.next/server/pages/api/documents/generated/[id]/repair/[rid]/confirm.js.nft.json +1 -1
  67. package/.next/server/pages/api/documents/generated/[id]/repair/[rid]/rollback.js +1 -1
  68. package/.next/server/pages/api/documents/generated/[id]/repair/[rid]/rollback.js.nft.json +1 -1
  69. package/.next/server/pages/api/documents/generated/[id]/repair.js +1 -1
  70. package/.next/server/pages/api/documents/generated/[id]/repair.js.nft.json +1 -1
  71. package/.next/server/pages/api/documents/generated/[id]/report.js +1 -0
  72. package/.next/server/pages/api/documents/generated/[id]/report.js.nft.json +1 -0
  73. package/.next/server/pages/api/documents/generated/[id]/resume-generate.js +1 -2
  74. package/.next/server/pages/api/documents/generated/[id]/resume-generate.js.nft.json +1 -1
  75. package/.next/server/pages/api/documents/generated/[id].js +1 -2
  76. package/.next/server/pages/api/documents/generated/[id].js.nft.json +1 -1
  77. package/.next/server/pages/api/documents/generated.js +1 -1
  78. package/.next/server/pages/api/documents/generated.js.nft.json +1 -1
  79. package/.next/server/pages/api/health.js +1 -1
  80. package/.next/server/pages/api/jobs/[id].js +2 -5
  81. package/.next/server/pages/api/jobs/[id].js.nft.json +1 -1
  82. package/.next/server/pages/api/kb/categories.js +1 -4
  83. package/.next/server/pages/api/kb/categories.js.nft.json +1 -1
  84. package/.next/server/pages/api/kb/duplicates.js +1 -4
  85. package/.next/server/pages/api/kb/duplicates.js.nft.json +1 -1
  86. package/.next/server/pages/api/kb/features.js +2 -1
  87. package/.next/server/pages/api/kb/features.js.nft.json +1 -1
  88. package/.next/server/pages/api/kb/files/detail.js +1 -6
  89. package/.next/server/pages/api/kb/files/detail.js.nft.json +1 -1
  90. package/.next/server/pages/api/kb/files/export.js +1 -5
  91. package/.next/server/pages/api/kb/files/export.js.nft.json +1 -1
  92. package/.next/server/pages/api/kb/files/open.js +1 -4
  93. package/.next/server/pages/api/kb/files/open.js.nft.json +1 -1
  94. package/.next/server/pages/api/kb/files/preview-pdf-page.js +1 -4
  95. package/.next/server/pages/api/kb/files/preview-pdf-page.js.nft.json +1 -1
  96. package/.next/server/pages/api/kb/files/reindex.js +2 -1
  97. package/.next/server/pages/api/kb/files/reindex.js.nft.json +1 -1
  98. package/.next/server/pages/api/kb/files/search.js +1 -5
  99. package/.next/server/pages/api/kb/files/search.js.nft.json +1 -1
  100. package/.next/server/pages/api/kb/files.js +2 -6
  101. package/.next/server/pages/api/kb/files.js.nft.json +1 -1
  102. package/.next/server/pages/api/kb/ignore.js +1 -4
  103. package/.next/server/pages/api/kb/ignore.js.nft.json +1 -1
  104. package/.next/server/pages/api/kb/material-groups.js +1 -2
  105. package/.next/server/pages/api/kb/material-groups.js.nft.json +1 -1
  106. package/.next/server/pages/api/kb/operations.js +2 -5
  107. package/.next/server/pages/api/kb/operations.js.nft.json +1 -1
  108. package/.next/server/pages/api/kb/reindex.js +2 -1
  109. package/.next/server/pages/api/kb/reindex.js.nft.json +1 -1
  110. package/.next/server/pages/api/kb/stats.js +1 -4
  111. package/.next/server/pages/api/kb/stats.js.nft.json +1 -1
  112. package/.next/server/pages/api/kb/tags.js +1 -4
  113. package/.next/server/pages/api/kb/tags.js.nft.json +1 -1
  114. package/.next/server/pages/api/kb/upload/complete.js +2 -1
  115. package/.next/server/pages/api/kb/upload/complete.js.nft.json +1 -1
  116. package/.next/server/pages/api/kb/upload.js +2 -1
  117. package/.next/server/pages/api/kb/upload.js.nft.json +1 -1
  118. package/.next/server/pages/api/prompt/chat.js +5 -11
  119. package/.next/server/pages/api/prompt/chat.js.nft.json +1 -1
  120. package/.next/server/pages/api/prompt.js +2 -1
  121. package/.next/server/pages/api/prompt.js.nft.json +1 -1
  122. package/.next/server/pages/api/skills/[id].js +1 -1
  123. package/.next/server/pages/api/skills/[id].js.nft.json +1 -1
  124. package/.next/server/pages/api/skills/validate.js +1 -1
  125. package/.next/server/pages/api/skills/validate.js.nft.json +1 -1
  126. package/.next/server/pages/api/skills.js +1 -1
  127. package/.next/server/pages/api/skills.js.nft.json +1 -1
  128. package/.next/server/pages/api/system/data-health.js +2 -0
  129. package/.next/server/pages/api/system/data-health.js.nft.json +1 -0
  130. package/.next/server/pages/api/system/logs.js +2 -2
  131. package/.next/server/pages/api/system/logs.js.nft.json +1 -1
  132. package/.next/server/pages/api/system/reset.js +1 -0
  133. package/.next/server/pages/api/{kb/intelligence.js.nft.json → system/reset.js.nft.json} +1 -1
  134. package/.next/server/pages/api/system/stats.js +2 -1
  135. package/.next/server/pages/api/system/stats.js.nft.json +1 -1
  136. package/.next/server/pages/canvas/[id].html +1 -1
  137. package/.next/server/pages/canvas.html +1 -1
  138. package/.next/server/pages/documents.html +1 -1
  139. package/.next/server/pages/guide.html +1 -1
  140. package/.next/server/pages/index.html +1 -1
  141. package/.next/server/pages/knowledge/file-detail.html +1 -1
  142. package/.next/server/pages/knowledge/files.html +1 -1
  143. package/.next/server/pages/knowledge/manage.html +1 -1
  144. package/.next/server/pages/knowledge.html +1 -1
  145. package/.next/server/pages/models.html +1 -1
  146. package/.next/server/pages/overview.html +1 -1
  147. package/.next/server/pages/prompt.html +1 -1
  148. package/.next/server/pages/settings.html +1 -1
  149. package/.next/server/pages/skills.html +1 -1
  150. package/.next/server/pages/system/logs.html +1 -1
  151. package/.next/server/pages-manifest.json +36 -38
  152. package/.next/server/webpack-runtime.js +1 -1
  153. package/.next/static/7819131dbd58574b80d0784549af250fc9b202d0e2317de83fe25857af834b79/_buildManifest.js +1 -0
  154. package/.next/static/chunks/278-31fbe7e1b733a9b7.js +1 -0
  155. package/.next/static/chunks/530-d9f0418ec7ab040f.js +1 -0
  156. package/.next/static/chunks/709-14ed28f1f6d4b25e.js +1 -0
  157. package/.next/static/chunks/713-b1ce7a4a4ef015f4.js +1 -0
  158. package/.next/static/chunks/795-48065c56c3b80339.js +1 -0
  159. package/.next/static/chunks/864-b06700e1a4e573f2.js +6 -0
  160. package/.next/static/chunks/90-3a5ea00990b4e72b.js +1 -0
  161. package/.next/static/chunks/964-7e9fe2d80a53aeee.js +78 -0
  162. package/.next/static/chunks/pages/{_app-ebb535779b63fd5d.js → _app-591ac7a50d506b05.js} +20 -20
  163. package/.next/static/chunks/pages/canvas/[id]-3c2b950c135f6699.js +1 -0
  164. package/.next/static/chunks/pages/{canvas-323d44fe56d685a7.js → canvas-9086d8d3a7d32776.js} +1 -1
  165. package/.next/static/chunks/pages/guide-a8eecc6c02b5bb9c.js +1 -0
  166. package/.next/static/chunks/pages/knowledge/file-detail-bd4b6e4264e812ff.js +1 -0
  167. package/.next/static/chunks/pages/knowledge-a65ec687334ffed0.js +1 -0
  168. package/.next/static/chunks/pages/models-baf1d15d79cb366b.js +1 -0
  169. package/.next/static/chunks/pages/prompt-b8af2cef904e3e19.js +1 -0
  170. package/.next/static/chunks/pages/settings-81dffd92939c1d36.js +1 -0
  171. package/.next/static/chunks/pages/skills-648c64f8cb37ef5d.js +1 -0
  172. package/.next/static/chunks/pages/system/logs-1f54b6c8791daa77.js +1 -0
  173. package/.next/static/css/f35b901a72bb3ade.css +1 -0
  174. package/README.md +48 -45
  175. package/messages/en-US.json +76 -65
  176. package/messages/zh-CN.json +76 -65
  177. package/next.config.ts +5 -0
  178. package/package.json +7 -9
  179. package/public/fonts/NotoSansCJKsc-Regular.otf +0 -0
  180. package/public/fonts/OFL-Noto-CJK.txt +142 -0
  181. package/public/fonts/font-manifest.json +8 -0
  182. package/scripts/build-identity.cjs +67 -0
  183. package/scripts/capture-baseline.mjs +100 -290
  184. package/scripts/check-workspace-deps.cjs +141 -0
  185. package/scripts/half-built-scan.allow.json +142 -0
  186. package/scripts/half-built-scan.baseline.json +206 -0
  187. package/scripts/half-built-scan.mjs +275 -55
  188. package/scripts/kb-fingerprint-worker.cjs +28 -0
  189. package/scripts/kb-index-worker.cjs +27 -20
  190. package/scripts/kb-worker-ipc.cjs +34 -0
  191. package/scripts/purge-next-artifacts.cjs +70 -0
  192. package/scripts/verify-next-static.js +2 -2
  193. package/.next/server/chunks/299.js +0 -50
  194. package/.next/server/chunks/5547.js +0 -350
  195. package/.next/server/chunks/6087.js +0 -3
  196. package/.next/server/pages/api/jobs.js +0 -5
  197. package/.next/server/pages/api/jobs.js.nft.json +0 -1
  198. package/.next/server/pages/api/kb/evaluate.js +0 -10
  199. package/.next/server/pages/api/kb/evaluate.js.nft.json +0 -1
  200. package/.next/server/pages/api/kb/files/tree.js +0 -5
  201. package/.next/server/pages/api/kb/files/tree.js.nft.json +0 -1
  202. package/.next/server/pages/api/kb/intelligence.js +0 -1
  203. package/.next/server/pages/api/kb/search.js +0 -4
  204. package/.next/server/pages/api/kb/search.js.nft.json +0 -1
  205. package/.next/server/pages/api/kb/upload/progress.js +0 -5
  206. package/.next/server/pages/api/kb/upload/progress.js.nft.json +0 -1
  207. package/.next/static/4xQDkixSretJDM503EzIq/_buildManifest.js +0 -1
  208. package/.next/static/chunks/243-03eba51d5d737e98.js +0 -1
  209. package/.next/static/chunks/289-0cccd8131332e041.js +0 -78
  210. package/.next/static/chunks/422-46f07037c58d2309.js +0 -1
  211. package/.next/static/chunks/445-d12731f107978b10.js +0 -6
  212. package/.next/static/chunks/530-6f3f5848c6f702ae.js +0 -1
  213. package/.next/static/chunks/709-a7c652953ccd3b4b.js +0 -1
  214. package/.next/static/chunks/795-9e0cb0bb11e8c89f.js +0 -1
  215. package/.next/static/chunks/pages/canvas/[id]-515edd3e72acaf82.js +0 -1
  216. package/.next/static/chunks/pages/guide-7367305082617e3c.js +0 -1
  217. package/.next/static/chunks/pages/knowledge/file-detail-aee7c15541d1d130.js +0 -1
  218. package/.next/static/chunks/pages/knowledge-09dde24cdce1b195.js +0 -1
  219. package/.next/static/chunks/pages/models-8b96ba85131d5408.js +0 -1
  220. package/.next/static/chunks/pages/prompt-6a224562d194cb45.js +0 -1
  221. package/.next/static/chunks/pages/settings-0a7709bf039a2a4d.js +0 -1
  222. package/.next/static/chunks/pages/skills-3d77213c3e9ef0b5.js +0 -1
  223. package/.next/static/chunks/pages/system/logs-70790bee46e5a08d.js +0 -1
  224. package/.next/static/css/85c434f8fd3a17ae.css +0 -1
  225. /package/.next/static/{4xQDkixSretJDM503EzIq → 7819131dbd58574b80d0784549af250fc9b202d0e2317de83fe25857af834b79}/_ssgManifest.js +0 -0
@@ -12,7 +12,7 @@
12
12
  * - ❌ **无逐调用 token/缓存遥测**——成本与缓存命中率**当前不可重建**。
13
13
  *
14
14
  * 于是「改了之后是不是退化了」在数据上无从判断(S4.2)。本脚本把这两样**在生成结束的那一刻**
15
- * 抄下来,与六维分一起冻结成一份可 diff 的基线。
15
+ * 抄下来,与七维评价回执一起冻结成一份可 diff 的基线。
16
16
  *
17
17
  * ## 为什么是「采集归档」而不是「另跑一遍管线」
18
18
  *
@@ -20,47 +20,19 @@
20
20
  * 落成一份 `*.baseline.json`。这样:
21
21
  * 1. 采集本身**零 LLM 成本**(跑一次真实文档 ~50 分钟,重跑一遍纯属浪费);
22
22
  * 2. 基线与被评的那份文档**逐字段同源**(不会出现「基线是另一轮跑的」这种不可比);
23
- * 3. A/B 两组的差异**只来自开关**(`REPAIR_CHAIN=off` / `AGENT_TOOLS=on`),不是两套采集代码。
24
23
  *
25
24
  * ## 用法
26
25
  *
27
26
  * ```bash
28
27
  * # 采集一份既有归档(docId 见 generatedDocuments/index.json)
29
28
  * node apps/server/scripts/capture-baseline.mjs --project-root . --doc doc-1790407778481-d7a36837
30
- * # 采集最近 N 份(自带 --run 时先触发一次生成再采集)
29
+ * # 采集最近 N 份
31
30
  * node apps/server/scripts/capture-baseline.mjs --project-root . --latest 2
32
- * node apps/server/scripts/capture-baseline.mjs --project-root . --run tpl-1790019955043 --run tpl-1790187804600
33
31
  * ```
34
32
  *
35
33
  * 落盘:`~/.customize-agent/baselines/<时间戳>-<label>.baseline.json`(**append-only**,
36
34
  * 不覆盖历史基线——「同代码重跑波动 ±7」这件事只能靠多份基线看出来)。
37
35
  *
38
- * ## A/B 模式(S11.2:**同一组 N 份归档,开关开/关各跑一次**)
39
- *
40
- * ```bash
41
- * # 腿 1(off)——需要**服务端以 REPAIR_CHAIN=off 启动**(开关读的是服务端进程的 env,脚本设不了)
42
- * node apps/server/scripts/capture-baseline.mjs --project-root . --latest 8 --ab REPAIR_CHAIN --ab-leg off
43
- * # 腿 2(on)——服务端以默认 env 重启后再跑;配对文件由腿 1 打印,无需手抄
44
- * node apps/server/scripts/capture-baseline.mjs --project-root . --latest 8 --ab REPAIR_CHAIN --ab-leg on
45
- * # 判定(**不在本脚本里**):两腿各跑完后由 TS 侧 `services/agent/abExecution.ts` 出结论
46
- * ```
47
- *
48
- * 分工(这是本模式最重要的一条):**脚本只做机制 + 抄写原始证据,绝不下结论**。
49
- * | 环节 | 谁 |
50
- * |---|---|
51
- * | 拒绝"这场 A/B 根本不成立"(腿值非法 / 同一腿跑两次 / 两腿输入不同 / 标签与历史基线撞车) | **本脚本**(全部在任何网络调用**之前**或落盘**之前**) |
52
- * | 从归档里抄 `repairChainMode` 等**原始**证据 | 本脚本(抄写,不判定) |
53
- * | 「这一腿的开关真的切了吗」+「on 腿是否退化」 | TS 侧 `services/agent/abExecution.ts`(复用 S4.2 的 `costModel`) |
54
- *
55
- * 为什么判定不放这儿:脚本的产物是**文件**,它不会拒绝给结论——而"标签写错的一腿"这种错
56
- * 只能靠"声明值 vs 归档证据"的对账抓出来,那需要两腿的归档同时在手,且必须**宁可不出结论**。
57
- * 脚本在此处的克制是刻意的:它连"declare=on 且归档无标记 ⇒ 视为 on"这种推断都不做。
58
- *
59
- * ★ 未核实:`AGENT_TOOLS` 这条腿今天**采集得出、判定不出**——写手循环的 `onTurn`
60
- * (`writerLoop.ts:134` 的 `turn.*` 载荷)在生产路径上零消费点,节点日志里没有 `turn` 节点,
61
- * 归档里也没有任何字段记录这个开关。脚本会对这种腿打出显式警告(跑完 8+8 份仍会拒判),
62
- * 免得白跑 16 次真实生成。
63
- * ═══════════════════════════════════════════════════════════════════════════
64
36
  */
65
37
 
66
38
  import crypto from 'node:crypto';
@@ -71,21 +43,17 @@ import path from 'node:path';
71
43
  const DEFAULT_BASE_URL = process.env.BASELINE_BASE_URL || 'http://localhost:17321';
72
44
 
73
45
  function parseArgs(argv) {
74
- const args = { projectRoot: process.cwd(), docs: [], runs: [], latest: 0, label: '', baseUrl: DEFAULT_BASE_URL, timeoutMinutes: 90, ab: '', abLeg: '', abPair: '', abSize: null };
46
+ const args = { projectRoot: process.cwd(), docs: [], corpora: [], latest: 0, label: '', baseUrl: DEFAULT_BASE_URL, timeoutMinutes: 90 };
75
47
  for (let index = 0; index < argv.length; index += 1) {
76
48
  const token = argv[index];
77
49
  const next = () => argv[++index];
78
50
  if (token === '--project-root') args.projectRoot = path.resolve(next());
79
51
  else if (token === '--doc') args.docs.push(next());
80
- else if (token === '--run') args.runs.push(next());
52
+ else if (token === '--corpus') args.corpora.push(next());
81
53
  else if (token === '--latest') args.latest = Number(next());
82
54
  else if (token === '--label') args.label = next();
83
55
  else if (token === '--base-url') args.baseUrl = next();
84
56
  else if (token === '--timeout-minutes') args.timeoutMinutes = Number(next());
85
- else if (token === '--ab') args.ab = next();
86
- else if (token === '--ab-leg') args.abLeg = next();
87
- else if (token === '--ab-pair') args.abPair = next();
88
- else if (token === '--ab-size') args.abSize = Number(next());
89
57
  else if (token === '--help' || token === '-h') { printHelp(); process.exit(0); }
90
58
  else throw new Error(`未知参数:${token}(--help 看用法)`);
91
59
  }
@@ -99,16 +67,14 @@ function printHelp() {
99
67
  ' --project-root <dir> 文档所属项目根(默认当前目录;决定归档目录)',
100
68
  ' --doc <docId> 采集指定文档(可重复)',
101
69
  ' --latest <N> 采集最近 N 份已完成文档',
102
- ' --run <templateId> 先触发一次生成再采集(可重复;需要服务在跑)',
103
- ' --label <name> 基线标签(默认按开关自动命名 a-on / b-off)',
70
+ ' --corpus <关键词> **按既有归档的输入原样重跑一份**再采集(可重复):从归档里找最近一份',
71
+ ' 匹配该关键词(材料组名/要求文本)的记录,用它的 promptIds/requirement/',
72
+ ' title/materialRoot 重跑——这是"改了代码之后同一份语料还能不能达到同样质量"',
73
+ ' 的取数方式(含巢湖/合工大这类真实语料)',
74
+ ' --label <name> 基线标签(默认 baseline)',
104
75
  ' --base-url <url> 服务地址(默认 http://localhost:17321)',
105
76
  ' --timeout-minutes <N> 单份生成等待上限(默认 90)',
106
77
  '',
107
- 'A/B(S11.2:同 N 份归档,开/关各跑一次;判定在 TS 侧 abExecution.ts):',
108
- ' --ab <SWITCH> 开关名(本工具支持 REPAIR_CHAIN / AGENT_TOOLS)',
109
- ' --ab-leg <on|off> 本腿的开关取值(同一腿只能采一次)',
110
- ' --ab-pair <file> 配对文件(默认按 开关+项目 定形,腿 1 打印出来供腿 2 复用)',
111
- ' --ab-size <N> 本场 A/B 的归档份数(腿 1 默认 8;腿 2 缺省沿用本场已声明的份数)',
112
78
  '',
113
79
  ].join('\n'));
114
80
  }
@@ -138,17 +104,20 @@ function num(value) {
138
104
  }
139
105
 
140
106
  /**
141
- * 六维 + 专业分 + 阻断数(评分口径单源在服务端;脚本只做**抄写**,不做换算)。
107
+ * 冻结合同七维回执与阻断数,直接读取当前正文的实际测量。
142
108
  * 换算会让基线与被评文档的差异混入「脚本口径」这个第三变量。
143
109
  */
144
110
  function scoresOf(record) {
145
- const report = record?.reviewMetadata?.qualityReport || {};
146
- const dims = report.dimensions || report.dimensionScores || null;
111
+ const report = record?.reviewMetadata?.contractQuality;
112
+ const score = report?.score;
147
113
  return {
148
- overall: num(report.overall ?? report.overallScore),
149
- dimensions: dims,
150
- requirementChecklist: Array.isArray(report.requirementChecklist) ? report.requirementChecklist.length : null,
151
- // 阻断数:导出闸门里的 blockingIssues(**不是**六维分——两套口径,方案 §六 顺带发现)
114
+ contractId: report?.contractId ?? null,
115
+ documentHash: report?.documentHash ?? null,
116
+ overallInterval: { lower: num(score?.lower), upper: num(score?.upper) },
117
+ dimensions: score?.dimensions ?? null,
118
+ meetsInitialTargets: score?.meetsInitialTargets ?? null,
119
+ measuredCriteria: Array.isArray(report?.findings) ? report.findings.length : null,
120
+ // 导出门禁保留实际阻断数;缺少评价回执不能当作零分或通过。
152
121
  blockingCount: Array.isArray(record?.draft?.exportGate?.blockingIssues) ? record.draft.exportGate.blockingIssues.length : null,
153
122
  exportGatePassed: record?.draft?.exportGate?.passed ?? null,
154
123
  };
@@ -195,18 +164,20 @@ function repairChainOf(record) {
195
164
 
196
165
  function baselineOf(record, options) {
197
166
  const diagnostics = record?.reviewMetadata?.diagnostics || {};
167
+ const startedAt = num(record.createdAt);
168
+ const endedAt = num(record.completedAt);
169
+ const elapsedMs = num(record.elapsedMs) ?? (startedAt !== null && endedAt !== null && endedAt >= startedAt ? endedAt - startedAt : null);
198
170
  return {
199
171
  schema: 'capture-baseline/v1',
200
172
  capturedAt: new Date().toISOString(),
201
- // ★ 开关状态必须进基线:A/B 两组**只差开关**,基线里没有开关就分不清这份是哪一组
202
- switches: {
203
- repairChain: diagnostics.repairChain?.mode || 'on',
204
- agentTools: process.env.AGENT_TOOLS || 'off',
205
- agendEmitNodeStageMirror: process.env.AGENT_EMIT_NODE_STAGE_MIRROR || 'default',
206
- },
173
+ recordContentHash: crypto.createHash('sha256').update(JSON.stringify(record)).digest('hex'),
174
+ runId: record.runId ?? null,
175
+ workflowVersion: record.workflowVersion ?? null,
176
+ // 阶段墙钟不能相加当成等待时间;并发阶段与调用耗时可能重叠。
177
+ metrics: diagnostics.metrics ?? record.reviewMetadata?.metrics ?? record.draft?.reviewMetadata?.diagnostics?.metrics ?? null,
207
178
  doc: {
208
179
  id: record.id, title: record.title, templateId: record.templateId, templateName: record.templateName,
209
- status: record.status, elapsedMs: num(record.elapsedMs), wordCount: num(record.wordCount),
180
+ status: record.status, elapsedMs, wordCount: num(record.wordCount),
210
181
  markdownChars: typeof record.markdown === 'string' ? record.markdown.length : null,
211
182
  completedAt: record.completedAt ?? null,
212
183
  },
@@ -218,200 +189,7 @@ function baselineOf(record, options) {
218
189
  };
219
190
  }
220
191
 
221
- // ── A/B 配对(S11.2:同 N 份归档,开/关各跑一次) ─────────────────────────
222
-
223
- /**
224
- * 本工具认识的 A/B 开关(与 TS 侧 `services/agent/abExecution.ts` 的 `abSwitchEvidence()` 逐条同集合)。
225
- *
226
- * ★ 刻意**不**接受 `SKILLS`/`PAUSE`:它们的关闭效果在归档里没有可对账的痕迹
227
- * (技能不参与 ≠ 模板缺字段,暂停被忽略 ≠ 终态不是 paused,两者都可能由别的原因造成)。
228
- * 在这里放行会让操作者以为"A/B 做过了",而判定侧只会拒判——把"跑过"当成"验过"是本仓红线。
229
- */
230
- const AB_SWITCHES = ['REPAIR_CHAIN', 'AGENT_TOOLS'];
231
- const AB_LEGS = ['on', 'off'];
232
- /** 归档证据**当前不可用**的开关(跑完仍会拒判)——采集不拦,但必须**事先**说清 */
233
- const AB_UNVERIFIABLE = { AGENT_TOOLS: '归档里没有任何字段记录这个开关:写手循环的 onTurn(writerLoop.ts:134 的 turn.* 载荷)在生产路径上零消费点,节点日志里没有 turn 节点' };
234
-
235
- function baselinesDir() {
236
- return path.join(os.homedir(), '.customize-agent', 'baselines');
237
- }
238
-
239
- /** 配对文件:显式 `--ab-pair` 优先;否则按「开关 + 项目」定形(腿 2 无需手抄路径) */
240
- function abPairPathOf(args) {
241
- if (args.abPair) return path.resolve(args.abPair);
242
- return path.join(baselinesDir(), `ab-${args.ab.toLowerCase()}-${projectIdOf(args.projectRoot)}.abpair.json`);
243
- }
244
-
245
- function loadPairOrNull(file) {
246
- if (!fs.existsSync(file)) return null;
247
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
248
- if (parsed?.schema !== 'ab-pair/v1' || !Array.isArray(parsed.legs)) {
249
- throw new Error(`配对文件形状不对(期望 schema=ab-pair/v1):${file}`);
250
- }
251
- return parsed;
252
- }
253
-
254
- /**
255
- * A/B 的全部「这场实验不成立」判定——**在任何网络调用之前**跑完。
256
- *
257
- * 为什么必须前置:`--run` 会触发真实生成(一份 40–60 分钟、真金白银)。等到采完 8+8 份
258
- * 才发现"同一腿采了两次",代价是十几小时机时与一份**没法用**的数据——而那种数据最危险的地方
259
- * 是它看起来能用。故这里宁可多拦。
260
- */
261
- function abPreflight(args) {
262
- const touched = args.ab !== '' || args.abLeg !== '' || args.abPair !== '' || args.abSize !== null;
263
- if (!touched) return null;
264
- if (!args.ab) throw new Error('--ab-leg / --ab-pair / --ab-size 需要同时给 --ab <SWITCH>(否则参数会被静默忽略)');
265
- if (!AB_SWITCHES.includes(args.ab)) {
266
- throw new Error(`--ab 不支持 ${args.ab}:本工具只做 ${AB_SWITCHES.join(' / ')}(判定侧同集合;其它开关在归档里没有可对账的痕迹)`);
267
- }
268
- const leg = args.abLeg;
269
- if (leg === '') throw new Error(`--ab ${args.ab} 必须给 --ab-leg <${AB_LEGS.join('|')}>:不给就分不清这份归档属于哪一腿,而两腿的归档**除开关外必须一致**`);
270
- if (!AB_LEGS.includes(leg)) throw new Error(`--ab-leg 只认 ${AB_LEGS.join('|')},收到「${args.abLeg}」(含糊取值在 A/B 里是结论级差错:'true'/'1' 到底算哪一腿没有唯一答案)`);
271
- if (args.abSize !== null && (!Number.isInteger(args.abSize) || args.abSize <= 0)) {
272
- throw new Error(`--ab-size 需要正整数,收到「${args.abSize}」(0 / 负数 / 小数都不收:含糊值一律拒绝,不静默取默认)`);
273
- }
274
- // 两腿必须各有自己的标签:判定侧**按标签配对**,两腿同标签会让 on/off 指到同一份基线文件
275
- if (args.label !== '') {
276
- throw new Error('A/B 模式下不允许 --label:两腿必须各有自己的标签(本模式自动命名为 ab-<开关>-<腿>-<时刻>),同标签会让判定侧把两腿指到同一份基线');
277
- }
278
-
279
- const pairFile = abPairPathOf(args);
280
- const pair = loadPairOrNull(pairFile);
281
- // 份数缺省取**本场已声明的**(腿 2 不必重抄 --ab-size;显式给了不一致的值仍然拒)
282
- const size = args.abSize === null ? (pair?.size ?? 8) : args.abSize;
283
- if (pair) {
284
- if (pair.switchName !== args.ab) throw new Error(`配对文件是给 ${pair.switchName} 的,本次是 ${args.ab}:${pairFile}`);
285
- const existing = pair.legs.map(item => item.leg);
286
- if (existing.includes(leg)) {
287
- throw new Error(`「${leg}」腿已经采过了(${pairFile}):A/B 是**开/关各一次**,同一腿采两次只会让配对无法收敛到"两腿只差开关"(已采:${existing.join('、')})`);
288
- }
289
- if (pair.size !== size) throw new Error(`配对文件的份数是 ${pair.size},本次 --ab-size ${size}:两腿必须同份数,改份数等于换了一场实验(请新建一场)`);
290
- }
291
-
292
- // 事前把"这条腿的证据不存在"说清——免得白跑 8 份真实生成
293
- if (AB_UNVERIFIABLE[args.ab]) {
294
- process.stdout.write(`[baseline] ⚠ 本腿(${args.ab}=${leg})可采集但**判定会拒绝出结论**:${AB_UNVERIFIABLE[args.ab]}\n`);
295
- process.stdout.write('[baseline] ⚠ 若目的是"建立"该证据通道,先把写入接上再跑;否则这 8 份归档只能当原始素材,不能当 A/B 证据。\n');
296
- }
297
- // 开关读的是**服务端进程**的 env(生成跑在服务端,本脚本设不了)——本地 env 只是提示,不是证据
298
- const localEnv = process.env[args.ab] || '(未设)';
299
- if (args.runs.length > 0) {
300
- process.stdout.write(`[baseline] ⚠ 本腿要求服务端 env:${args.ab}=${leg}(脚本设不了它;本地 env=${localEnv})——请**重启服务**后再跑 --run,否则采到的是另一腿。\n`);
301
- }
302
-
303
- return { switchName: args.ab, leg, size, pairFile, pair, legCount: (pair?.legs.length ?? 0) + 1 };
304
- }
305
-
306
- /** 归档证据:关链标记(`documentPipeline.ts:160` 只在**关**时写)+ 节点日志里 turn 节点条数(今天恒 0) */
307
- function abObserveLeg({ projectRoot, records }) {
308
- const modes = [];
309
- const turnCounts = [];
310
- const notes = [];
311
- for (const record of records) {
312
- const mode = record?.reviewMetadata?.diagnostics?.repairChain?.mode ?? record?.draft?.reviewMetadata?.diagnostics?.repairChain?.mode ?? null;
313
- modes.push(mode);
314
- const count = turnNodeCountOf({ projectRoot, docId: record.id });
315
- turnCounts.push(count);
316
- if (count === null) notes.push(`${record.id}:无节点日志(turn 条数记为「未知」,不是 0)`);
317
- }
318
- const distinct = [...new Set(modes.map(mode => mode ?? '(缺)'))].sort();
319
- return {
320
- // 关腿应全体带标记;混着来(如 'off+(缺)')在判定侧会被判为"标签对不上"并给出这段原文
321
- repairChainMode: modes.length > 0 && distinct.length === 1 && distinct[0] === '(缺)' ? null : distinct.join('+'),
322
- turnNodeCount: turnCounts.some(count => count !== null) ? turnCounts.reduce((sum, count) => sum + (count ?? 0), 0) : null,
323
- notes,
324
- perDoc: records.map((record, index) => ({ docId: record.id, repairChainMode: modes[index], turnNodeCount: turnCounts[index] })),
325
- };
326
- }
327
-
328
- /** 节点日志里 `turn.*` 节点条数;**日志不存在 ⇒ null(未知)**,绝不写成 0(0 是"查过了,没有") */
329
- function turnNodeCountOf({ projectRoot, docId }) {
330
- const file = path.join(archiveDir(projectRoot), 'drafts', `${docId}.nodes.jsonl`);
331
- if (!fs.existsSync(file)) return null;
332
- let count = 0;
333
- for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
334
- if (line.trim() === '') continue;
335
- try {
336
- const parsed = JSON.parse(line);
337
- if (String(parsed?.type || '').startsWith('turn.')) count += 1;
338
- } catch { /* 撕裂尾行:不计入(也不当成 0——上面的 existsSync 已经区分了"没有日志") */ }
339
- }
340
- return count;
341
- }
342
-
343
- /**
344
- * 落盘**之前**的输入集校验:两腿必须跑同一组输入。
345
- *
346
- * 为什么这条值钱:`--latest 8` 的选片随"最近完成"变化。若两腿之间又完成了一份文档,
347
- * 腿 2 采到的就是另一组——而**分数差异会被读成开关的效果**。这种错在结果里看不出来
348
- * (两组分数都"正常"),只能靠这次集合比对拦住。
349
- */
350
- function abAssertInputs(ab, baselines) {
351
- if (baselines.length !== ab.size) {
352
- throw new Error(`本腿采到 ${baselines.length} 份,与 --ab-size ${ab.size} 不符:A/B 的前提是两腿同份数(拒绝落盘)`);
353
- }
354
- const templates = baselines.map(item => item.doc.templateId || item.doc.templateName || '(未知模板)');
355
- const docs = baselines.map(item => item.doc.id);
356
- const other = ab.pair?.legs?.[0];
357
- if (other) {
358
- const mine = [...templates].sort().join('\u0000');
359
- const theirs = [...other.templates].sort().join('\u0000');
360
- if (mine !== theirs) {
361
- throw new Error(`两腿输入不是同一组模板(本腿 ${templates.join('、')} vs 另一腿 ${other.templates.join('、')}):差的就不是开关了(拒绝落盘)`);
362
- }
363
- const overlap = docs.filter(id => other.docIds.includes(id));
364
- if (overlap.length > 0) {
365
- throw new Error(`两腿归档重合 ${overlap.length} 份(${overlap.slice(0, 3).join('、')}…):同一份归档不能同时代表"开"与"关"(拒绝落盘)`);
366
- }
367
- }
368
- return { templates, docs };
369
- }
370
-
371
- /** A/B 模式下拒绝与历史基线**撞标签**:标签是判定侧的配对键,撞了就会把两场会话混成一场 */
372
- function abAssertLabelFree(label) {
373
- const dir = baselinesDir();
374
- if (!fs.existsSync(dir)) return;
375
- for (const name of fs.readdirSync(dir)) {
376
- if (!name.endsWith('.baseline.json')) continue;
377
- try {
378
- const parsed = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
379
- if (parsed?.label === label) {
380
- throw new Error(`标签「${label}」已被 ${name} 占用:A/B 的判定按标签配对,撞标签会把两场会话混成一场(A/B 模式下 --label 不可用,把旧基线移出目录或删掉即可;正常情况不会撞——标签里带采集时刻)`);
381
- }
382
- } catch (error) {
383
- if (error instanceof Error && error.message.includes('已被')) throw error; // 上面的拒绝照原样抛出
384
- /* 解析失败的历史文件不参与撞标签判定(`loadBaselines` 侧同样跳过并告警) */
385
- }
386
- }
387
- }
388
-
389
- /** 写配对状态(腿记录**只追加不删**;文件本身重写是因为它是状态不是产物) */
390
- function abWriteLeg({ ab, label, outFile, records }) {
391
- const observed = abObserveLeg({ projectRoot: ab.projectRoot, records });
392
- const leg = {
393
- leg: ab.leg,
394
- label,
395
- baselineFile: path.basename(outFile),
396
- declared: ab.leg,
397
- capturedAt: new Date().toISOString(),
398
- templates: ab.observed.templates,
399
- docIds: ab.observed.docs,
400
- observed,
401
- };
402
- const pair = {
403
- schema: 'ab-pair/v1',
404
- switchName: ab.switchName,
405
- pairId: `ab-${ab.switchName.toLowerCase()}-${projectIdOf(ab.projectRoot)}`,
406
- projectRoot: path.resolve(ab.projectRoot),
407
- size: ab.size,
408
- legs: [...(ab.pair?.legs ?? []), leg],
409
- };
410
- fs.writeFileSync(ab.pairFile, `${JSON.stringify(pair, null, 2)}\n`);
411
- return { pair, openLeg: AB_LEGS.filter(item => !pair.legs.some(entry => entry.leg === item)) };
412
- }
413
-
414
- // ── 服务交互:触发与轮询 ────────────────────────────────────────────────
192
+ function baselinesDir() { return path.join(os.homedir(), '.customize-agent', 'baselines'); }
415
193
 
416
194
  async function postJson(url, body, timeoutMs = 60000) {
417
195
  const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs) });
@@ -421,22 +199,42 @@ async function postJson(url, body, timeoutMs = 60000) {
421
199
  return { status: response.status, body: parsed ?? text };
422
200
  }
423
201
 
424
- async function getJson(url, timeoutMs = 60000) {
425
- const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
426
- const text = await response.text();
427
- try { return { status: response.status, body: JSON.parse(text) }; } catch { return { status: response.status, body: text }; }
202
+ /**
203
+ * ★ 实测修正(2026-09-30,A3 腿):轮询超时 60s → **180s**,且**轮询失败不再终止采集**。
204
+ *
205
+ * 现场:生成中的服务端被 LLM 调用打满时,列表端点可能 >60s 才回答,`AbortSignal.timeout(60000)`
206
+ * 直接抛 `The operation was aborted due to timeout` ⇒ **采集脚本死在轮询上,而生成还在跑**
207
+ * (本轮 A3 腿就是这么丢的:脚本 05:29 报失败、文档仍在 generating)。
208
+ * 采集器的职责是"等",不是"因为服务忙就放弃"——超时只该由 `deadline`(`--timeout-minutes`)决定。
209
+ */
210
+ async function getJson(url, timeoutMs = 180000) {
211
+ try {
212
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
213
+ const text = await response.text();
214
+ try { return { status: response.status, body: JSON.parse(text) }; } catch { return { status: response.status, body: text }; }
215
+ } catch (error) {
216
+ // 轮询失败 ⇒ 返回空体(调用方继续等);**不 throw**(服务忙不等于生成失败)
217
+ process.stdout.write(`[baseline] 轮询失败(继续等,不影响生成):${error instanceof Error ? error.message : String(error)}\n`);
218
+ return { status: 0, body: undefined };
219
+ }
428
220
  }
429
221
 
430
222
  const TERMINAL = new Set(['completed', 'completed_with_issues', 'warning', 'failed', 'aborted']);
431
223
 
432
- async function triggerAndWait({ baseUrl, projectRoot, templateId, timeoutMinutes }) {
433
- const started = await postJson(`${baseUrl}/api/documents/generate`, { templateId, requirement: '', projectRoot }, 120000);
224
+ /**
225
+ * ★ 2026-09-28:触发入参改成 **§12.1 三字段 + 语料四件套**(原实现传的是已退役的 `templateId` 单字段)。
226
+ * `corpus` 给出时,用它的 `promptIds/requirement/title/materialRoot` **原样重跑**——
227
+ * 语料可复现是基线可比的前提(换了提示词或材料组就不是同一份文档了)。
228
+ */
229
+ async function triggerAndWait({ baseUrl, projectRoot, corpus, timeoutMinutes }) {
230
+ const started = await postJson(`${baseUrl}/api/documents/generate`,
231
+ { promptIds: corpus.promptIds, requirement: corpus.requirement || '', title: corpus.title, materialRoot: corpus.materialRoot, projectRoot }, 120000);
434
232
  if (started.status !== 202 && started.status !== 200) {
435
233
  throw new Error(`触发失败(HTTP ${started.status}):${typeof started.body === 'string' ? started.body : JSON.stringify(started.body)}`);
436
234
  }
437
235
  const documentId = started.body?.documentId || started.body?.record?.id || started.body?.id;
438
236
  if (!documentId) throw new Error(`触发返回里没有 documentId:${JSON.stringify(started.body).slice(0, 400)}`);
439
- process.stdout.write(`[baseline] 已触发 ${templateId} → ${documentId}(等待生成,通常 40–60 分钟)\n`);
237
+ process.stdout.write(`[baseline] 已触发 ${corpus.title}(语料:${corpus.materialRoot}) → ${documentId}(等待生成,通常 40–60 分钟)\n`);
440
238
  const deadline = Date.now() + timeoutMinutes * 60000;
441
239
  let lastLog = 0;
442
240
  for (;;) {
@@ -459,6 +257,35 @@ function loadArchivedRecord({ projectRoot, docId }) {
459
257
  return JSON.parse(fs.readFileSync(file, 'utf8'));
460
258
  }
461
259
 
260
+ /**
261
+ * ★ 2026-09-28:按关键词找**最近的语料记录**并抽出重跑所需的输入(四件套 + 项目根)。
262
+ * 匹配面:`materialRoot`(材料组名)与 `requirement`(要求文本)——真实语料通常两者都带关键词。
263
+ */
264
+ function findCorpus({ projectRoot, keyword }) {
265
+ const dir = path.join(archiveDir(projectRoot), 'drafts');
266
+ if (!fs.existsSync(dir)) throw new Error(`归档目录不存在:${dir}`);
267
+ const candidates = fs.readdirSync(dir)
268
+ .filter(name => name.endsWith('.json') && !name.endsWith('.meta.json'))
269
+ .map(name => {
270
+ const full = path.join(dir, name);
271
+ try { return { full, stat: fs.statSync(full), record: JSON.parse(fs.readFileSync(full, 'utf8')) }; } catch { return undefined; }
272
+ })
273
+ .filter(Boolean)
274
+ .filter(item => `${item.record.materialRoot || ''} ${item.record.requirement || ''} ${item.record.title || ''}`.includes(keyword))
275
+ .sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs);
276
+ if (candidates.length === 0) throw new Error(`归档里找不到含「${keyword}」的记录(材料组名/要求文本/标题都没命中):${dir}`);
277
+ const newest = candidates[0].record;
278
+ if (!newest.promptIds?.length) throw new Error(`「${keyword}」最近一份记录没有 promptIds(旧记录),无法原样重跑`);
279
+ return {
280
+ sourceDocId: newest.id,
281
+ promptIds: newest.promptIds,
282
+ requirement: newest.requirement || '',
283
+ title: newest.templateName || newest.title || '',
284
+ materialRoot: newest.materialRoot || '',
285
+ projectRoot: newest.projectRoot || projectRoot,
286
+ };
287
+ }
288
+
462
289
  function latestDocIds({ projectRoot, count }) {
463
290
  const indexFile = path.join(archiveDir(projectRoot), 'index.json');
464
291
  if (!fs.existsSync(indexFile)) throw new Error(`索引不存在:${indexFile}`);
@@ -474,16 +301,17 @@ function latestDocIds({ projectRoot, count }) {
474
301
 
475
302
  async function main() {
476
303
  const args = parseArgs(process.argv.slice(2));
477
- // ★ A/B 的拒绝**全部**在这里(任何网络调用之前):腿值非法 / 同一腿采两次 / 份数不一致…
478
- const ab = abPreflight(args);
479
- if (ab) {
480
- ab.projectRoot = args.projectRoot;
481
- process.stdout.write(`[baseline] A/B:${ab.switchName}=${ab.leg}(第 ${ab.legCount}/2 腿,共 ${ab.size} 份)配对文件 ${ab.pairFile}\n`);
482
- }
483
304
  const records = [];
484
305
 
485
- for (const templateId of args.runs) {
486
- records.push(await triggerAndWait({ baseUrl: args.baseUrl, projectRoot: args.projectRoot, templateId, timeoutMinutes: args.timeoutMinutes }));
306
+ /**
307
+ * ★ 2026-09-28:**按既有归档的输入原样重跑**(真实语料基线:巢湖 / 合工大这类)。
308
+ * 每个关键词解析出一份语料四件套,逐份触发 + 等待 + 采集——同一份语料在改动前后各跑一次,
309
+ * 两次的 `*.baseline.json` 可直接 diff(脚本的产物是可 diff 的基线,判定仍不在这里)。
310
+ */
311
+ for (const keyword of args.corpora) {
312
+ const corpus = findCorpus({ projectRoot: args.projectRoot, keyword });
313
+ process.stdout.write(`[baseline] 语料「${keyword}」命中 ${corpus.sourceDocId}:标题「${corpus.title}」材料组「${corpus.materialRoot}」\n`);
314
+ records.push(await triggerAndWait({ baseUrl: args.baseUrl, projectRoot: args.projectRoot, corpus, timeoutMinutes: args.timeoutMinutes }));
487
315
  }
488
316
  if (args.latest > 0) {
489
317
  for (const docId of latestDocIds({ projectRoot: args.projectRoot, count: args.latest })) {
@@ -495,16 +323,9 @@ async function main() {
495
323
 
496
324
  const baselines = records.map(record => baselineOf(record, args));
497
325
  const stamp = new Date().toISOString().replace(/[:.]/gu, '-');
498
- // A/B 的标签带**时刻**:判定侧按标签配对,两场会话天然可分(同开关可重复做 A/B,不必先归档旧基线)
499
- const label = args.label || (ab ? `ab-${ab.switchName.toLowerCase()}-${ab.leg}-${stamp}` : (baselines[0].switches.repairChain === 'off' ? 'b-off' : 'a-on'));
326
+ const label = args.label || 'baseline';
500
327
  const outDir = baselinesDir();
501
328
  fs.mkdirSync(outDir, { recursive: true });
502
- if (ab) {
503
- // ★ 落盘前的最后两道拒绝:两腿输入不同 / 标签与历史基线撞车。
504
- // 抛在这里 ⇒ 本腿**一份文件都不写**(不留"半套配对"这种看起来能用的状态)
505
- ab.observed = abAssertInputs(ab, baselines);
506
- abAssertLabelFree(label);
507
- }
508
329
  const outFile = path.join(outDir, `${stamp}-${label}.baseline.json`);
509
330
  fs.writeFileSync(outFile, `${JSON.stringify({ capturedAt: new Date().toISOString(), label, projectRoot: path.resolve(args.projectRoot), baselines }, null, 2)}\n`);
510
331
 
@@ -512,27 +333,16 @@ async function main() {
512
333
  for (const item of baselines) {
513
334
  const dims = item.scores.dimensions ? JSON.stringify(item.scores.dimensions) : '(无 dimensions)';
514
335
  process.stdout.write(
515
- ` ${item.doc.id} ${item.doc.title} 状态=${item.doc.status} ${Math.round((item.doc.elapsedMs || 0) / 60000)}min\n`
516
- + ` 综合=${item.scores.overall ?? '?'} 阻断=${item.scores.blockingCount ?? '?'} 六维=${dims}\n`
336
+ ` ${item.doc.id} ${item.doc.title} 状态=${item.doc.status} ${item.doc.elapsedMs === null ? '耗时未知' : Math.round(item.doc.elapsedMs / 60000) + 'min'}\n`
337
+ + ` 综合区间=${item.scores.overallInterval.lower ?? '?'}~${item.scores.overallInterval.upper ?? '?'} 阻断=${item.scores.blockingCount ?? '?'} 七维=${dims}\n`
517
338
  + ` 块账=${item.blockLedger.rows} 块 Σ目标=${item.blockLedger.targetChars} Σ实际=${item.blockLedger.actualChars}\n`
518
339
  + ` LLM 调用=${item.llm.calls ?? '?'} 缓存命中/未命中=${item.llm.promptCacheHitTokens ?? '?'}/${item.llm.promptCacheMissTokens ?? '?'}\n`
519
- + ` 修复链净增=${item.repairChain.netChars?.delta ?? '?'}(${item.repairChain.netChars ? (item.repairChain.netChars.ratio * 100).toFixed(1) + '%' : '?'})开关=${item.switches.repairChain}\n`,
340
+ + ` 修复链净增=${item.repairChain.netChars?.delta ?? '?'}(${item.repairChain.netChars ? (item.repairChain.netChars.ratio * 100).toFixed(1) + '%' : '?'})\n`,
520
341
  );
521
342
  }
522
343
  process.stdout.write(`[baseline] 落盘:${outFile}\n`);
523
- if (ab) {
524
- const { pair, openLeg } = abWriteLeg({ ab, label, outFile, records });
525
- process.stdout.write(`[baseline] A/B 状态:${ab.pairFile}(已采 ${pair.legs.map(item => item.leg).join('、')})\n`);
526
- if (openLeg.length > 0) {
527
- process.stdout.write(`[baseline] 还差「${openLeg.join('、')}」腿:服务端 env 切到 ${ab.switchName}=${openLeg[0]} 并重启后,用**同一批输入**再跑一次\n`);
528
- process.stdout.write(`[baseline] (--ab ${ab.switchName} --ab-leg ${openLeg[0]} --ab-pair ${ab.pairFile};--latest/--doc 的选片必须与上一腿同组)\n`);
529
- } else {
530
- process.stdout.write('[baseline] 两腿齐了。判定(**不在本脚本里**,跑 TS 侧车道):\n');
531
- process.stdout.write(`[baseline] AB_PAIR=${ab.pairFile} npx vitest run --config vitest.manual.config.ts apps/server/test/services/agent/abExecution.manual.ts\n`);
532
- }
533
- process.stdout.write('[baseline] ★ 本脚本只采集与抄写证据,**不下结论**:标签对不对、是否退化,由 TS 侧 abExecution(复用 S4.2)判。\n');
534
- }
535
- process.stdout.write('[baseline] ★ 同代码重跑波动 ±7(方案 14.5 族 6 实测)——单点差值不足以归因,A/B 要比的是**维度构成**。\n');
344
+ process.stdout.write('[baseline] 基线只抄写归档已有值,未测项保留为空。\n');
345
+
536
346
  }
537
347
 
538
348
  main().catch(error => {