sillyspec 3.27.2 → 3.27.5

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 (221) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
  2. package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
  3. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  4. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  5. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  6. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  7. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  8. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  9. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  10. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  11. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
  12. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
  13. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  14. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  15. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  16. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  17. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  18. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
  19. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
  20. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  21. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  22. package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
  23. package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
  24. package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
  25. package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
  26. package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
  27. package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
  28. package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
  29. package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
  30. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  31. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  32. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
  33. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
  34. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
  35. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
  36. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
  37. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
  38. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
  39. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
  40. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
  41. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
  42. package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
  43. package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
  44. package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
  45. package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
  46. package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
  47. package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
  48. package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
  49. package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
  50. package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
  51. package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
  52. package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
  53. package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
  54. package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
  55. package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
  56. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
  57. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
  58. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
  59. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
  60. package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
  61. package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
  62. package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
  63. package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
  64. package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
  65. package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
  66. package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
  67. package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
  68. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
  69. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
  70. package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
  71. package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
  72. package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
  73. package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
  74. package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
  75. package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
  76. package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
  77. package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
  78. package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
  79. package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
  80. package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
  81. package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
  82. package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
  83. package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
  84. package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
  85. package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
  86. package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
  87. package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
  88. package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
  89. package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
  90. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
  91. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
  92. package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
  93. package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
  94. package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
  95. package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
  96. package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
  97. package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
  98. package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
  99. package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
  100. package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
  101. package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
  102. package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
  103. package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
  104. package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
  105. package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
  106. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
  107. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
  108. package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
  109. package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
  110. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
  111. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
  112. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
  113. package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
  114. package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
  115. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
  116. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
  117. package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
  118. package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
  119. package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
  120. package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
  121. package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
  122. package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
  123. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
  124. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
  125. package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
  126. package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
  127. package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
  128. package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
  129. package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
  130. package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
  131. package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
  132. package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
  133. package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
  134. package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
  135. package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
  136. package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
  137. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
  138. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
  139. package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
  140. package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
  141. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
  142. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
  143. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
  144. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
  145. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
  146. package/eval/README.md +86 -0
  147. package/eval/eval.config.json +28 -0
  148. package/eval/lib/adapters/cli.mjs +63 -0
  149. package/eval/lib/adapters/dryrun.mjs +27 -0
  150. package/eval/lib/config.mjs +82 -0
  151. package/eval/lib/db.mjs +76 -0
  152. package/eval/lib/report.mjs +120 -0
  153. package/eval/lib/runner.mjs +157 -0
  154. package/eval/lib/tasks.mjs +73 -0
  155. package/eval/lib/util.mjs +107 -0
  156. package/eval/lib/workdir.mjs +30 -0
  157. package/eval/run.mjs +83 -0
  158. package/eval/tasks/001-slugify/files/package.json +6 -0
  159. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  160. package/eval/tasks/001-slugify/task.json +6 -0
  161. package/eval/tasks/001-slugify/verify.mjs +28 -0
  162. package/eval/tasks/002-each-day/files/package.json +6 -0
  163. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  164. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  165. package/eval/tasks/002-each-day/task.json +6 -0
  166. package/eval/tasks/002-each-day/verify.mjs +31 -0
  167. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  168. package/eval/tasks/003-word-report/files/package.json +6 -0
  169. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  170. package/eval/tasks/003-word-report/task.json +6 -0
  171. package/eval/tasks/003-word-report/verify.mjs +38 -0
  172. package/package.json +4 -2
  173. package/src/agent-session-log.js +45 -5
  174. package/src/change-risk-profile.js +83 -14
  175. package/src/config-schema.js +18 -3
  176. package/src/contract-matrix.js +90 -29
  177. package/src/datetime.js +20 -0
  178. package/src/decision-distill.js +517 -0
  179. package/src/dispatch/backends/local-agent.js +4 -4
  180. package/src/docs-check.js +227 -0
  181. package/src/docs-debt.js +187 -24
  182. package/src/doctor-diagnostics.js +102 -2
  183. package/src/foreign-declared.js +78 -0
  184. package/src/index.js +89 -9
  185. package/src/init.js +9 -7
  186. package/src/knowledge-match.js +107 -6
  187. package/src/local-register.js +7 -1
  188. package/src/module-resolve.js +12 -1
  189. package/src/modules.js +2 -1
  190. package/src/plan-adopt-waves.js +126 -0
  191. package/src/quicklog.js +50 -4
  192. package/src/run/command.js +122 -43
  193. package/src/run/complete-handlers.js +48 -19
  194. package/src/run/complete.js +5 -2
  195. package/src/run/gates.js +87 -22
  196. package/src/run/prompt.js +84 -42
  197. package/src/run/quick-audit.js +11 -0
  198. package/src/run/shared.js +152 -10
  199. package/src/run.js +2 -2
  200. package/src/scan-postcheck.js +30 -17
  201. package/src/stage-contract-spec.js +2 -1
  202. package/src/stage-contract.js +68 -4
  203. package/src/stages/archive.js +53 -12
  204. package/src/stages/brainstorm-auto.js +7 -0
  205. package/src/stages/brainstorm.js +11 -0
  206. package/src/stages/doctor.js +157 -1
  207. package/src/stages/execute.js +32 -6
  208. package/src/stages/plan-postcheck.js +76 -17
  209. package/src/stages/plan.js +1 -0
  210. package/src/stages/quick.js +6 -1
  211. package/src/stages/verify.js +31 -14
  212. package/src/sync.js +41 -3
  213. package/src/task-review.js +2 -2
  214. package/src/taskcard-placeholders.js +21 -0
  215. package/src/taskcard.js +10 -1
  216. package/src/verify-postcheck.js +315 -13
  217. package/src/verify-probes.js +46 -4
  218. package/src/worktree-apply.js +184 -7
  219. package/src/worktree-deps.js +57 -0
  220. package/src/worktree.js +201 -58
  221. package/templates/prompts/taskcard-rules.md +1 -0
@@ -0,0 +1,76 @@
1
+ // eval 结果库(node:sqlite,独立于 src/db.js 的进度库——评测数据与工具自身状态解耦)。
2
+ // 只记录不解释:通过率/flaky/基线对比等统计全部在 report 层算,保持本层无状态可替换。
3
+ import { mkdirSync } from 'node:fs';
4
+ import { dirname } from 'node:path';
5
+ import { DatabaseSync } from 'node:sqlite';
6
+
7
+ const DDL = `
8
+ CREATE TABLE IF NOT EXISTS runs (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ task_id TEXT NOT NULL,
11
+ arm TEXT NOT NULL, -- A=裸 agent / B=SillySpec 流程
12
+ adapter TEXT NOT NULL, -- dryrun | cli
13
+ model_label TEXT,
14
+ config_hash TEXT NOT NULL, -- 实验分组(配置+任务指纹+版本)
15
+ tool_version TEXT, -- sillyspec 版本(发版回归对比用)
16
+ started_at TEXT NOT NULL,
17
+ duration_ms INTEGER NOT NULL DEFAULT 0,
18
+ exit_code INTEGER,
19
+ status TEXT NOT NULL, -- pass | fail | timeout | error
20
+ verify_output TEXT,
21
+ tokens_in INTEGER,
22
+ tokens_out INTEGER,
23
+ cost_usd REAL,
24
+ agent_log_path TEXT,
25
+ notes TEXT
26
+ );
27
+ CREATE INDEX IF NOT EXISTS idx_runs_group ON runs(config_hash, adapter, arm, task_id);
28
+ `;
29
+
30
+ export class EvalDB {
31
+ constructor(dbPath) {
32
+ this.dbPath = dbPath;
33
+ this.db = null;
34
+ }
35
+
36
+ init() {
37
+ mkdirSync(dirname(this.dbPath), { recursive: true });
38
+ this.db = new DatabaseSync(this.dbPath);
39
+ this.db.exec(DDL);
40
+ return this;
41
+ }
42
+
43
+ insertRun(row) {
44
+ this.db.prepare(`
45
+ INSERT INTO runs (task_id, arm, adapter, model_label, config_hash, tool_version,
46
+ started_at, duration_ms, exit_code, status, verify_output,
47
+ tokens_in, tokens_out, cost_usd, agent_log_path, notes)
48
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
49
+ `).run(
50
+ row.task_id, row.arm, row.adapter, row.model_label ?? null, row.config_hash, row.tool_version ?? null,
51
+ row.started_at, row.duration_ms ?? 0, row.exit_code ?? null, row.status, row.verify_output ?? null,
52
+ row.tokens_in ?? null, row.tokens_out ?? null, row.cost_usd ?? null, row.agent_log_path ?? null, row.notes ?? null,
53
+ );
54
+ return this.db.prepare('SELECT last_insert_rowid() AS id').get().id;
55
+ }
56
+
57
+ /** 同 (hash, task, arm) 已有 pass/fail 终态则可跳过重跑(--force 旁路) */
58
+ cachedRun(configHash, taskId, arm) {
59
+ return this.db.prepare(`
60
+ SELECT * FROM runs
61
+ WHERE config_hash = ? AND task_id = ? AND arm = ? AND status IN ('pass','fail')
62
+ ORDER BY id DESC LIMIT 1
63
+ `).get(configHash, taskId, arm) ?? null;
64
+ }
65
+
66
+ runsInGroup(configHash, adapter) {
67
+ return this.db.prepare('SELECT * FROM runs WHERE config_hash = ? AND adapter = ? ORDER BY id').all(configHash, adapter);
68
+ }
69
+
70
+ groups() {
71
+ return this.db.prepare(`
72
+ SELECT config_hash, adapter, COUNT(*) AS n, MAX(id) AS last_id
73
+ FROM runs GROUP BY config_hash, adapter ORDER BY last_id DESC
74
+ `).all();
75
+ }
76
+ }
@@ -0,0 +1,120 @@
1
+ // 报告:分臂通过率 + Wilson 95% CI + Δ(B−A) 区间 + flaky 剔除 + 基线回归对比。
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { computeConfigHash, sillyspecVersion } from './config.mjs';
5
+ import { EvalDB } from './db.mjs';
6
+ import { loadTasks, tasksFingerprint } from './tasks.mjs';
7
+ import { deltaCI, fmtPct, fmtPp, wilsonCI } from './util.mjs';
8
+
9
+ export function buildReport(config, opts = {}) {
10
+ const dbFile = join(config.resultsDir, 'eval.db');
11
+ if (!existsSync(dbFile)) return { empty: true };
12
+ const db = new EvalDB(dbFile).init();
13
+
14
+ const adapterType = opts.dryRun ? 'dryrun' : (config.adapter.type ?? 'cli');
15
+ const currentHash = computeConfigHash(config, adapterType, tasksFingerprint(loadTasks(config.tasksDir)));
16
+ const groups = db.groups();
17
+ const group = groups.find((g) => g.config_hash === currentHash && g.adapter === adapterType)
18
+ ?? groups.find((g) => g.adapter === adapterType);
19
+ if (!group) return { empty: true };
20
+
21
+ const rows = db.runsInGroup(group.config_hash, adapterType);
22
+ const byArm = { A: new Map(), B: new Map() };
23
+ for (const r of rows) {
24
+ if (r.status !== 'pass' && r.status !== 'fail') continue; // timeout/error 不计入通过率
25
+ if (!byArm[r.arm]) continue;
26
+ if (!byArm[r.arm].has(r.task_id)) byArm[r.arm].set(r.task_id, []);
27
+ byArm[r.arm].get(r.task_id).push(r.status);
28
+ }
29
+
30
+ const summary = {
31
+ adapterType,
32
+ configHash: group.config_hash,
33
+ hashCurrent: group.config_hash === currentHash,
34
+ sillyspecVersion: sillyspecVersion(),
35
+ arms: {},
36
+ };
37
+ for (const arm of Object.keys(byArm)) {
38
+ let n = 0;
39
+ let pass = 0;
40
+ const flaky = [];
41
+ for (const [taskId, statuses] of byArm[arm]) {
42
+ if (new Set(statuses).size > 1) flaky.push(taskId); // 同任务同臂结果摇摆 → flaky,剔除
43
+ else { n++; if (statuses[0] === 'pass') pass++; }
44
+ }
45
+ summary.arms[arm] = {
46
+ label: config.arms[arm]?.label ?? arm,
47
+ n,
48
+ pass,
49
+ rate: n ? pass / n : null,
50
+ ci: wilsonCI(pass, n),
51
+ flaky,
52
+ };
53
+ }
54
+ const A = summary.arms.A;
55
+ const B = summary.arms.B;
56
+ if (A?.rate != null && B?.rate != null) {
57
+ summary.delta = { pp: B.rate - A.rate, ci: deltaCI(A.rate, A.n, B.rate, B.n) };
58
+ }
59
+ return summary;
60
+ }
61
+
62
+ export function printReport(config, summary) {
63
+ if (!summary || summary.empty) {
64
+ console.log('暂无可报告的评测数据(先 --dry-run 或真跑一轮)');
65
+ return;
66
+ }
67
+ console.log('\n==== SillySpec Eval 报告 ====');
68
+ console.log(`adapter=${summary.adapterType} hash=${summary.configHash}${summary.hashCurrent ? '' : '(任务/配置已变更,以上为最近一轮)'} sillyspec=${summary.sillyspecVersion}`);
69
+ console.log('\n臂 n pass 通过率 95% CI');
70
+ for (const arm of ['A', 'B']) {
71
+ const s = summary.arms[arm];
72
+ if (!s || (s.n === 0 && s.flaky.length === 0)) continue;
73
+ console.log(
74
+ `${s.label.padEnd(12).slice(0, 12)} ${String(s.n).padEnd(4)} ${String(s.pass).padEnd(5)} `
75
+ + `${fmtPct(s.rate).padEnd(7)} [${fmtPct(s.ci[0])}, ${fmtPct(s.ci[1])}]`,
76
+ );
77
+ }
78
+ if (summary.delta) {
79
+ console.log(`\nΔ(B−A) = ${fmtPp(summary.delta.pp)} 95% CI [${fmtPp(summary.delta.ci[0])}, ${fmtPp(summary.delta.ci[1])}]`);
80
+ if (Math.min(summary.arms.A.n, summary.arms.B.n) < 30) {
81
+ console.log('(单臂样本 < 30,区间仅供方向参考)');
82
+ }
83
+ }
84
+ const flakyAll = ['A', 'B'].flatMap((a) => (summary.arms[a]?.flaky ?? []).map((t) => `${t}(臂${a})`));
85
+ if (flakyAll.length) console.log(`flaky ${flakyAll.length} 个(已从通过率剔除):${flakyAll.join('、')}`);
86
+
87
+ const baselinePath = join(config.resultsDir, 'baseline.json');
88
+ if (existsSync(baselinePath)) {
89
+ const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
90
+ const threshold = (config.regressionThresholdPp ?? 5) / 100;
91
+ const parts = [];
92
+ for (const arm of ['A', 'B']) {
93
+ const cur = summary.arms[arm]?.rate;
94
+ const base = baseline.arms?.[arm]?.rate;
95
+ if (cur == null || base == null) continue;
96
+ const diff = cur - base;
97
+ const flag = diff < -threshold ? ' ⚠️超回归阈值' : '';
98
+ parts.push(`臂${arm} ${fmtPct(base)}→${fmtPct(cur)}(${fmtPp(diff)}${flag})`);
99
+ }
100
+ if (parts.length) console.log(`基线对比(${baseline.createdAt}):${parts.join(' ')}`);
101
+ }
102
+ }
103
+
104
+ export function setBaseline(config, opts) {
105
+ const summary = buildReport(config, opts);
106
+ if (!summary || summary.empty) {
107
+ console.log('暂无数据可固化为基线');
108
+ return;
109
+ }
110
+ const pick = (a) => (a && a.rate != null ? { n: a.n, rate: a.rate } : null);
111
+ const payload = {
112
+ createdAt: new Date().toISOString(),
113
+ configHash: summary.configHash,
114
+ adapter: summary.adapterType,
115
+ sillyspecVersion: summary.sillyspecVersion,
116
+ arms: { A: pick(summary.arms.A), B: pick(summary.arms.B) },
117
+ };
118
+ writeFileSync(join(config.resultsDir, 'baseline.json'), JSON.stringify(payload, null, 2));
119
+ console.log(`基线已固化 → ${join(config.resultsDir, 'baseline.json')}`);
120
+ }
@@ -0,0 +1,157 @@
1
+ // 编排:任务 × 臂 → (环境准备 → agent → verify) → SQLite 落库。
2
+ // 缓存:同 (config_hash, task, arm) 已有 pass/fail 终态则跳过(--force 旁路)——
3
+ // 调提示词/流程后只重跑受影响的臂,是控制 token 成本的主要手段。
4
+ import { mkdirSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { computeConfigHash } from './config.mjs';
7
+ import { EvalDB } from './db.mjs';
8
+ import { loadTasks, tasksFingerprint } from './tasks.mjs';
9
+ import { prepareWorkdir } from './workdir.mjs';
10
+ import { spawnCapture, tail } from './util.mjs';
11
+ import { run as dryRunAdapter } from './adapters/dryrun.mjs';
12
+ import { run as cliAdapter } from './adapters/cli.mjs';
13
+
14
+ const FAILED_STATUSES = new Set(['fail', 'timeout', 'error']);
15
+
16
+ export async function runEval(config, opts) {
17
+ const allTasks = loadTasks(config.tasksDir);
18
+ if (allTasks.length === 0) {
19
+ console.error(`任务目录为空: ${config.tasksDir}(先在 eval/tasks/ 下添加任务)`);
20
+ return { ran: 0, skipped: 0 };
21
+ }
22
+ const fingerprint = tasksFingerprint(allTasks);
23
+
24
+ let tasks = allTasks;
25
+ if (opts.taskIds?.length) tasks = tasks.filter((t) => opts.taskIds.includes(t.id));
26
+ if (opts.sample) tasks = tasks.slice(0, opts.sample);
27
+ if (tasks.length === 0) {
28
+ console.error('没有匹配的任务');
29
+ return { ran: 0, skipped: 0 };
30
+ }
31
+
32
+ const adapterType = opts.dryRun ? 'dryrun' : config.adapter.type;
33
+ const configHash = computeConfigHash(config, adapterType, fingerprint);
34
+ const db = new EvalDB(join(config.resultsDir, 'eval.db')).init();
35
+
36
+ const arms = opts.arms?.length ? opts.arms : ['A', 'B'];
37
+ let plan = [];
38
+ for (const arm of arms) {
39
+ for (const task of tasks) plan.push({ arm, task });
40
+ }
41
+ if (opts.onlyFailed) {
42
+ const latest = latestByTaskArm(db.runsInGroup(configHash, adapterType));
43
+ plan = plan.filter(({ arm, task }) => {
44
+ const prev = latest.get(`${arm}|${task.id}`);
45
+ return prev ? FAILED_STATUSES.has(prev.status) : true; // 无记录的新任务也跑
46
+ });
47
+ }
48
+
49
+ const workdirRoot = join(config.resultsDir, 'workdirs');
50
+ mkdirSync(workdirRoot, { recursive: true });
51
+
52
+ let ran = 0;
53
+ let skipped = 0;
54
+ // --rerun-failed 语义就是「重跑失败者」,必须旁路缓存(否则 fail 终态被缓存拦下,一次都跑不了)
55
+ const useCache = !opts.force && !opts.onlyFailed;
56
+ for (const { arm, task } of plan) {
57
+ const cached = useCache ? db.cachedRun(configHash, task.id, arm) : null;
58
+ if (cached) {
59
+ skipped++;
60
+ console.log(`[cache] ${task.id} 臂${arm} → ${cached.status}(--force 可重跑)`);
61
+ continue;
62
+ }
63
+ const result = await executeRun({ config, opts, adapterType, configHash, db, task, arm, workdirRoot });
64
+ ran++;
65
+ const icon = result.status === 'pass' ? '✓' : result.status === 'fail' ? '✗' : '!';
66
+ console.log(`[${icon}] ${task.id} 臂${arm} ${result.status} ${result.durationMs}ms${result.note ? `(${result.note})` : ''}`);
67
+ }
68
+ console.log(`完成:新跑 ${ran},缓存跳过 ${skipped}(hash=${configHash})`);
69
+ return { ran, skipped, configHash, adapterType };
70
+ }
71
+
72
+ function latestByTaskArm(rows) {
73
+ const m = new Map(); // rows 按 id 升序,后写覆盖即最新
74
+ for (const r of rows) m.set(`${r.arm}|${r.task_id}`, r);
75
+ return m;
76
+ }
77
+
78
+ async function executeRun({ config, opts, adapterType, configHash, db, task, arm, workdirRoot }) {
79
+ const t0 = Date.now();
80
+ const armCfg = config.arms[arm] ?? {};
81
+ const workdir = prepareWorkdir(workdirRoot, task.id, arm, task.filesDir);
82
+ const row = {
83
+ task_id: task.id,
84
+ arm,
85
+ adapter: adapterType,
86
+ model_label: opts.dryRun ? 'dry-run' : (config.adapter.tool ?? adapterType),
87
+ config_hash: configHash,
88
+ tool_version: null,
89
+ started_at: new Date().toISOString(),
90
+ duration_ms: 0,
91
+ exit_code: null,
92
+ status: 'error',
93
+ verify_output: null,
94
+ agent_log_path: null,
95
+ notes: null,
96
+ tokens_in: null,
97
+ tokens_out: null,
98
+ cost_usd: null,
99
+ };
100
+
101
+ // 臂环境准备:B 臂执行 sillyspec init 等(dry-run 跳过,保持零副作用)
102
+ if (!opts.dryRun) {
103
+ for (const cmd of armCfg.setup ?? []) {
104
+ const args = render(cmd, { workdir, tool: config.adapter.tool ?? '' }).split(/\s+/).filter(Boolean);
105
+ const r = await spawnCapture({ args, cwd: workdir, timeoutMs: 120_000 });
106
+ if (r.code !== 0) {
107
+ row.status = 'error';
108
+ row.notes = `臂 setup 失败: ${cmd}`;
109
+ row.verify_output = tail(`${r.stdout}\n${r.stderr}`);
110
+ row.exit_code = r.code;
111
+ row.duration_ms = Date.now() - t0;
112
+ db.insertRun(row);
113
+ return { status: 'error', durationMs: row.duration_ms, note: row.notes };
114
+ }
115
+ }
116
+ }
117
+
118
+ const prompt = (armCfg.promptTemplate ?? '{instruction}').replaceAll('{instruction}', task.instruction);
119
+ const logDir = join(config.resultsDir, 'logs', `${stamp()}-${task.id}-arm${arm}`);
120
+ let res;
121
+ if (opts.dryRun) {
122
+ res = dryRunAdapter({ task, arm, workdir, solutionDir: task.solutionDir, mode: opts.dryMode ?? 'mixed' });
123
+ } else {
124
+ res = await cliAdapter({ adapter: config.adapter, prompt, workdir, logDir });
125
+ }
126
+ row.agent_log_path = res.logDir ?? null;
127
+ row.exit_code = res.code ?? null;
128
+ row.tokens_in = res.tokensIn ?? null;
129
+ row.tokens_out = res.tokensOut ?? null;
130
+ row.cost_usd = res.costUsd ?? null;
131
+
132
+ if (res.timedOut) {
133
+ row.status = 'timeout';
134
+ } else if ((res.code ?? -1) !== 0) {
135
+ row.status = 'error';
136
+ row.verify_output = tail(res.stderr || res.stdout);
137
+ } else {
138
+ // 判分:verify.mjs 以 workdir 为 cwd 执行,退出码 0 = pass
139
+ const v = await spawnCapture({ args: ['node', task.verifyPath], cwd: workdir, timeoutMs: config.verifyTimeoutMs ?? 120_000 });
140
+ row.status = v.code === 0 ? 'pass' : 'fail';
141
+ row.verify_output = tail(`${v.stderr}\n${v.stdout}`);
142
+ }
143
+
144
+ row.duration_ms = Date.now() - t0;
145
+ db.insertRun(row);
146
+ return { status: row.status, durationMs: row.duration_ms, note: null };
147
+ }
148
+
149
+ function render(cmd, vars) {
150
+ let s = cmd;
151
+ for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
152
+ return s;
153
+ }
154
+
155
+ function stamp() {
156
+ return new Date().toISOString().replace(/[:.]/g, '-');
157
+ }
@@ -0,0 +1,73 @@
1
+ // 任务加载与内容指纹。任务 = eval/tasks/<id>/ 下四件套:
2
+ // task.json(id/instruction)· verify.mjs(cwd=被测工作目录,退出码 0=过)· files/(工作副本脚手架)· solution/(dry-run 参考解)
3
+ import { createHash } from 'node:crypto';
4
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ export function loadTasks(tasksDir) {
8
+ if (!existsSync(tasksDir)) return [];
9
+ const out = [];
10
+ for (const entry of readdirSync(tasksDir, { withFileTypes: true })) {
11
+ if (!entry.isDirectory()) continue;
12
+ const dir = join(tasksDir, entry.name);
13
+ const taskFile = join(dir, 'task.json');
14
+ if (!existsSync(taskFile)) continue;
15
+ const meta = JSON.parse(readFileSync(taskFile, 'utf8'));
16
+ const task = {
17
+ id: meta.id ?? entry.name,
18
+ title: meta.title ?? entry.name,
19
+ instruction: meta.instruction,
20
+ tags: meta.tags ?? [],
21
+ dir,
22
+ verifyPath: join(dir, 'verify.mjs'),
23
+ filesDir: join(dir, 'files'),
24
+ solutionDir: join(dir, 'solution'),
25
+ };
26
+ if (typeof task.instruction !== 'string' || !task.instruction.trim()) {
27
+ throw new Error(`任务 ${task.id} 缺少 instruction`);
28
+ }
29
+ if (!existsSync(task.verifyPath)) {
30
+ throw new Error(`任务 ${task.id} 缺少 verify.mjs`);
31
+ }
32
+ out.push(task);
33
+ }
34
+ out.sort((a, b) => a.id.localeCompare(b.id));
35
+ const seen = new Set();
36
+ for (const t of out) {
37
+ if (seen.has(t.id)) throw new Error(`任务 id 重复: ${t.id}`);
38
+ seen.add(t.id);
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /**
44
+ * 全量任务内容指纹(task.json + verify.mjs + files/ 递归内容;solution/ 不参与——
45
+ * 它只服务 dry-run,改动不应让真实评测换组)。始终对目录内全部任务计算,
46
+ * 与实际抽样跑哪些无关——否则 --task 子集会产生另一套分组,报告对不上。
47
+ */
48
+ export function tasksFingerprint(tasks) {
49
+ const h = createHash('sha256');
50
+ for (const t of tasks) {
51
+ h.update(t.id); h.update('\0');
52
+ h.update(readFileSync(join(t.dir, 'task.json')));
53
+ h.update('\0');
54
+ h.update(readFileSync(t.verifyPath));
55
+ h.update('\0');
56
+ for (const rel of walkFiles(t.filesDir)) {
57
+ h.update(rel); h.update('\0');
58
+ h.update(readFileSync(join(t.filesDir, rel))); h.update('\0');
59
+ }
60
+ }
61
+ return h.digest('hex').slice(0, 16);
62
+ }
63
+
64
+ function walkFiles(dir, prefix = '') {
65
+ if (!existsSync(dir)) return [];
66
+ const acc = [];
67
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
68
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
69
+ if (e.isDirectory()) acc.push(...walkFiles(join(dir, e.name), rel));
70
+ else acc.push(rel);
71
+ }
72
+ return acc.sort();
73
+ }
@@ -0,0 +1,107 @@
1
+ // eval 通用工具:稳定哈希、跨平台 shell 执行、统计区间。
2
+ import { spawn } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+
5
+ export function sha256(text) {
6
+ return createHash('sha256').update(text).digest('hex');
7
+ }
8
+
9
+ // 键排序的稳定序列化:同一配置内容(键序无关)必须得到同一哈希,缓存/报告分组才可靠
10
+ export function stableStringify(value) {
11
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
12
+ if (value && typeof value === 'object') {
13
+ const keys = Object.keys(value).sort();
14
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
15
+ }
16
+ return JSON.stringify(value);
17
+ }
18
+
19
+ // 平台感知参数引用:Windows(cmd) 双引号、POSIX 单引号。
20
+ // 评测命令/路径不含引号;prompt 一律走 stdin 注入,绕开跨平台转义深坑。
21
+ export function shQuote(arg) {
22
+ const s = String(arg);
23
+ if (process.platform === 'win32') return `"${s.replace(/"/g, '""')}"`;
24
+ return `'${s.replace(/'/g, `'\\''`)}'`;
25
+ }
26
+
27
+ /**
28
+ * 执行命令并捕获输出(shell 模式,Windows 下解析 .cmd 入口如 npx/claude 需要)。
29
+ * 输出超过 maxOutputChars 停止累积(防 agent 刷屏撑爆内存),标记 truncated。
30
+ * timeoutMs 到点杀进程树(win32 用 taskkill /T,POSIX kill 进程组语义下杀子进程)。
31
+ */
32
+ export function spawnCapture({ args, cwd, timeoutMs = 0, input = null, maxOutputChars = 2_000_000 }) {
33
+ return new Promise((resolve) => {
34
+ const started = Date.now();
35
+ const command = args.map(shQuote).join(' ');
36
+ let child;
37
+ try {
38
+ child = spawn(command, { cwd, shell: true, windowsHide: true });
39
+ } catch (err) {
40
+ resolve({ code: -1, stdout: '', stderr: String(err), timedOut: false, truncated: false, durationMs: 0 });
41
+ return;
42
+ }
43
+ const state = { stdout: '', stderr: '' };
44
+ let truncated = false;
45
+ let timedOut = false;
46
+ let settled = false;
47
+ const onChunk = (key) => (buf) => {
48
+ if (state[key].length >= maxOutputChars) { truncated = true; return; }
49
+ state[key] += buf.toString('utf8');
50
+ };
51
+ child.stdout?.on('data', onChunk('stdout'));
52
+ child.stderr?.on('data', onChunk('stderr'));
53
+ let timer = null;
54
+ if (timeoutMs > 0) {
55
+ timer = setTimeout(() => {
56
+ timedOut = true;
57
+ if (process.platform === 'win32' && child.pid) {
58
+ try { spawn('taskkill /pid ' + child.pid + ' /T /F', { shell: true, windowsHide: true }); } catch { /* 尽力杀 */ }
59
+ }
60
+ child.kill('SIGKILL');
61
+ }, timeoutMs);
62
+ }
63
+ if (input != null) {
64
+ child.stdin?.on('error', () => { /* stdin 中途关闭(进程先退出)不视为致命 */ });
65
+ child.stdin?.end(input);
66
+ }
67
+ const settle = (patch) => {
68
+ if (settled) return;
69
+ settled = true;
70
+ if (timer) clearTimeout(timer);
71
+ resolve({ durationMs: Date.now() - started, truncated, timedOut, ...state, ...patch });
72
+ };
73
+ child.on('error', (err) => settle({ code: -1, stderr: state.stderr + String(err) }));
74
+ child.on('close', (code) => settle({ code: code === null ? -1 : code }));
75
+ });
76
+ }
77
+
78
+ /** Wilson 95% 置信区间(小样本比正态近似稳) */
79
+ export function wilsonCI(pass, n, z = 1.96) {
80
+ if (!n) return [null, null];
81
+ const p = pass / n;
82
+ const den = 1 + (z * z) / n;
83
+ const center = (p + (z * z) / (2 * n)) / den;
84
+ const half = (z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * n * n))) / den;
85
+ return [Math.max(0, center - half), Math.min(1, center + half)];
86
+ }
87
+
88
+ /** 两比例之差(B−A)的正态近似区间 */
89
+ export function deltaCI(pA, nA, pB, nB, z = 1.96) {
90
+ if (!nA || !nB) return [null, null];
91
+ const se = Math.sqrt((pA * (1 - pA)) / nA + (pB * (1 - pB)) / nB);
92
+ const d = pB - pA;
93
+ return [d - z * se, d + z * se];
94
+ }
95
+
96
+ export function fmtPct(x) {
97
+ return x == null ? '—' : `${(x * 100).toFixed(1)}%`;
98
+ }
99
+
100
+ export function fmtPp(x) {
101
+ return x == null ? '—' : `${x >= 0 ? '+' : ''}${(x * 100).toFixed(1)}pp`;
102
+ }
103
+
104
+ export function tail(text, n = 600) {
105
+ const s = (text ?? '').trim();
106
+ return s.length <= n ? s : `…${s.slice(-n)}`;
107
+ }
@@ -0,0 +1,30 @@
1
+ // 被测工作副本:每个 (task, arm) 一个独立目录,从任务 files/ 复制脚手架。
2
+ // 每次重跑前清空重建,保证 agent 面对的初始状态确定。目录保留不删(排查用),在 .gitignore 里。
3
+ import { cpSync, existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ // Windows 下刚退出的子进程句柄可能滞后释放,直接 rmSync 会 EPERM:
7
+ // 先带重试删;仍失败则改名隔离(.stale),新一轮照常进行,遗留目录不影响判分。
8
+ function removeDir(p) {
9
+ try {
10
+ rmSync(p, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
11
+ } catch {
12
+ const stale = `${p}.stale`;
13
+ try {
14
+ rmSync(stale, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
15
+ renameSync(p, stale);
16
+ } catch { /* 双重失败只能上抛,fail-loud */ }
17
+ }
18
+ }
19
+
20
+ export function prepareWorkdir(workdirRoot, taskId, arm, filesDir) {
21
+ const workdir = join(workdirRoot, `${taskId}__arm${arm}`);
22
+ removeDir(workdir);
23
+ mkdirSync(workdir, { recursive: true });
24
+ if (existsSync(filesDir)) cpSync(filesDir, workdir, { recursive: true });
25
+ return workdir;
26
+ }
27
+
28
+ export function copyInto(workdir, sourceDir) {
29
+ if (existsSync(sourceDir)) cpSync(sourceDir, workdir, { recursive: true });
30
+ }
package/eval/run.mjs ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ // SillySpec A/B 评测入口。
3
+ // A 臂 = 裸 agent 直接做任务;B 臂 = 同 agent + SillySpec 流程。同任务、同模型、同判分脚本(verify.mjs),
4
+ // 差值 Δ 即 SillySpec 的真实增量。详细文档见 eval/README.md。
5
+ import { loadConfig } from './lib/config.mjs';
6
+ import { runEval } from './lib/runner.mjs';
7
+ import { buildReport, printReport, setBaseline } from './lib/report.mjs';
8
+ import { spawnCapture, tail } from './lib/util.mjs';
9
+
10
+ function parseArgs(argv) {
11
+ const opts = {
12
+ dryRun: false, dryMode: 'mixed', taskIds: [], arms: [], sample: 0,
13
+ force: false, onlyFailed: false, report: false, setBaseline: false, preflight: false, help: false,
14
+ };
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const a = argv[i];
17
+ if (a === '--dry-run') opts.dryRun = true;
18
+ else if (a.startsWith('--dry-run=')) { opts.dryRun = true; opts.dryMode = a.slice('--dry-run='.length); }
19
+ else if (a === '--task') opts.taskIds.push(argv[++i]);
20
+ else if (a === '--arm') opts.arms.push(...(argv[++i] ?? '').split(',').map((s) => s.trim().toUpperCase()).filter(Boolean));
21
+ else if (a === '--sample') opts.sample = Number(argv[++i]);
22
+ else if (a === '--pilot') opts.sample = 20; // pilot = 前 20 个任务双臂
23
+ else if (a === '--force') opts.force = true;
24
+ else if (a === '--rerun-failed') opts.onlyFailed = true;
25
+ else if (a === '--report') opts.report = true;
26
+ else if (a === '--set-baseline') opts.setBaseline = true;
27
+ else if (a === '--preflight') opts.preflight = true;
28
+ else if (a === '--help' || a === '-h') opts.help = true;
29
+ else { console.error(`未知参数: ${a}`); opts.help = true; }
30
+ }
31
+ return opts;
32
+ }
33
+
34
+ function help() {
35
+ console.log(`SillySpec A/B 评测(A=裸 agent,B=SillySpec 流程,verify.mjs 客观判分)
36
+
37
+ 用法:node eval/run.mjs <选项>
38
+
39
+ --dry-run[=mixed|pass|fail] 零 token 验证管道(假 agent,不调模型不装依赖)
40
+ --preflight 检查 Node 版本与 agent CLI 可用性(零 token)
41
+ --pilot 真跑 pilot:前 20 个任务 × 双臂(消耗真实 token)
42
+ --task <id> 只跑指定任务(可重复)
43
+ --arm A|B|A,B 只跑指定臂
44
+ --sample <N> 只跑前 N 个任务
45
+ --rerun-failed 只重跑上一轮 fail/timeout/error(省 token 的定向重跑)
46
+ --force 忽略缓存强制重跑
47
+ --report [--dry-run] 查看报告(Wilson CI / Δ / flaky / 基线对比)
48
+ --set-baseline 把当前成绩固化为回归基线(发版前对比用)`);
49
+ }
50
+
51
+ async function preflight(config) {
52
+ const major = Number(process.versions.node.split('.')[0]);
53
+ console.log(`Node ${process.version} ${major >= 22 ? '✓' : '✗(eval 需 >= 22,node:sqlite)'}`);
54
+ const bin = config.adapter.command[0];
55
+ const r = await spawnCapture({ args: [bin, '--version'], timeoutMs: 20_000 });
56
+ if (r.code === 0) {
57
+ console.log(`${bin} --version ✓ ${tail(r.stdout, 120)}`);
58
+ } else {
59
+ console.log(`${bin} --version ✗ 不可用(${tail(r.stderr || `exit ${r.code}`, 120)})`);
60
+ console.log(' 提示:先安装该 agent CLI 并在终端手动登录一次;认证沿用其登录态,本评测不接管密钥。');
61
+ }
62
+ }
63
+
64
+ const opts = parseArgs(process.argv.slice(2));
65
+ if (opts.help) {
66
+ help();
67
+ process.exit(0);
68
+ }
69
+ const config = loadConfig();
70
+ if (opts.preflight) {
71
+ await preflight(config);
72
+ process.exit(0);
73
+ }
74
+ if (opts.report) {
75
+ printReport(config, buildReport(config, opts));
76
+ process.exit(0);
77
+ }
78
+ if (opts.setBaseline) {
79
+ setBaseline(config, opts);
80
+ process.exit(0);
81
+ }
82
+ await runEval(config, opts);
83
+ printReport(config, buildReport(config, opts)); // 跑完顺手出报告
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "task-slugify",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "private": true
6
+ }
@@ -0,0 +1,26 @@
1
+ // 参考解(dry-run 假 agent 使用,也供人工对照)。
2
+ // 思路:空白先归一为 \u0000 哨兵;字符过滤同时保留 [a-z0-9]、哨兵和字面分隔符;
3
+ // 再把「哨兵/分隔符的连续串」折叠为单个分隔符并去首尾。
4
+ // 注意不能只折叠哨兵——输入里本来就有的连续分隔符(如 'a--b')也要折叠。
5
+ const SENTINEL = '\u0000';
6
+
7
+ function escapeRe(ch) {
8
+ return ch.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&');
9
+ }
10
+
11
+ export function slugify(value, options = {}) {
12
+ if (value === null || value === undefined) return '';
13
+ const sep = options.hyphen ?? '-';
14
+ if (typeof sep !== 'string' || sep.length !== 1) {
15
+ throw new TypeError('hyphen must be a single character');
16
+ }
17
+ const esc = escapeRe(sep);
18
+ return String(value)
19
+ .toLowerCase()
20
+ .replace(/\s+/g, SENTINEL)
21
+ .replace(new RegExp(`[^a-z0-9\\u0000${esc}]`, 'g'), '')
22
+ .replace(new RegExp(`[\\u0000${esc}]+`, 'g'), sep)
23
+ .replace(new RegExp(`^${esc}+|${esc}+$`, 'g'), '');
24
+ }
25
+
26
+ export default slugify;