sillyspec 3.27.5 → 3.27.6

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 (194) hide show
  1. package/.worktrees/8d2d0392/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  2. package/.worktrees/8d2d0392/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  3. package/.worktrees/8d2d0392/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  4. package/.worktrees/8d2d0392/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  5. package/.worktrees/8d2d0392/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  6. package/.worktrees/8d2d0392/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  7. package/.worktrees/8d2d0392/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  8. package/.worktrees/8d2d0392/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  9. package/.worktrees/8d2d0392/.claude/skills/sillyspec-export/SKILL.md +57 -0
  10. package/.worktrees/8d2d0392/.claude/skills/sillyspec-init/SKILL.md +177 -0
  11. package/.worktrees/8d2d0392/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  12. package/.worktrees/8d2d0392/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  13. package/.worktrees/8d2d0392/.claude/skills/sillyspec-propose/SKILL.md +87 -0
  14. package/.worktrees/8d2d0392/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  15. package/.worktrees/8d2d0392/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  16. package/.worktrees/8d2d0392/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  17. package/.worktrees/8d2d0392/.claude/skills/sillyspec-state/SKILL.md +64 -0
  18. package/.worktrees/8d2d0392/.claude/skills/sillyspec-status/SKILL.md +54 -0
  19. package/.worktrees/8d2d0392/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  20. package/.worktrees/8d2d0392/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  21. package/.worktrees/8d2d0392/.claude/skills/verify-per-user/SKILL.md +6 -0
  22. package/.worktrees/8d2d0392/.gitattributes +2 -0
  23. package/.worktrees/8d2d0392/.husky/pre-push +20 -0
  24. package/.worktrees/8d2d0392/README.md +203 -0
  25. package/.worktrees/8d2d0392/SKILL.md +91 -0
  26. package/.worktrees/8d2d0392/architecture-analysis.md +448 -0
  27. package/.worktrees/8d2d0392/bin/sillyspec.js +2 -0
  28. package/.worktrees/8d2d0392/eval/README.md +86 -0
  29. package/.worktrees/8d2d0392/eval/eval.config.json +28 -0
  30. package/.worktrees/8d2d0392/eval/lib/adapters/cli.mjs +63 -0
  31. package/.worktrees/8d2d0392/eval/lib/adapters/dryrun.mjs +27 -0
  32. package/.worktrees/8d2d0392/eval/lib/config.mjs +82 -0
  33. package/.worktrees/8d2d0392/eval/lib/db.mjs +76 -0
  34. package/.worktrees/8d2d0392/eval/lib/report.mjs +120 -0
  35. package/.worktrees/8d2d0392/eval/lib/runner.mjs +157 -0
  36. package/.worktrees/8d2d0392/eval/lib/tasks.mjs +73 -0
  37. package/.worktrees/8d2d0392/eval/lib/util.mjs +107 -0
  38. package/.worktrees/8d2d0392/eval/lib/workdir.mjs +30 -0
  39. package/.worktrees/8d2d0392/eval/run.mjs +83 -0
  40. package/.worktrees/8d2d0392/eval/tasks/001-slugify/files/package.json +6 -0
  41. package/.worktrees/8d2d0392/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  42. package/.worktrees/8d2d0392/eval/tasks/001-slugify/task.json +6 -0
  43. package/.worktrees/8d2d0392/eval/tasks/001-slugify/verify.mjs +28 -0
  44. package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/package.json +6 -0
  45. package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  46. package/.worktrees/8d2d0392/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  47. package/.worktrees/8d2d0392/eval/tasks/002-each-day/task.json +6 -0
  48. package/.worktrees/8d2d0392/eval/tasks/002-each-day/verify.mjs +31 -0
  49. package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  50. package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/package.json +6 -0
  51. package/.worktrees/8d2d0392/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  52. package/.worktrees/8d2d0392/eval/tasks/003-word-report/task.json +6 -0
  53. package/.worktrees/8d2d0392/eval/tasks/003-word-report/verify.mjs +38 -0
  54. package/.worktrees/8d2d0392/logo.jpg +0 -0
  55. package/.worktrees/8d2d0392/package-lock.json +977 -0
  56. package/.worktrees/8d2d0392/package.json +41 -0
  57. package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  58. package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  59. package/.worktrees/8d2d0392/packages/dashboard/dist/favicon.jpg +0 -0
  60. package/.worktrees/8d2d0392/packages/dashboard/dist/index.html +16 -0
  61. package/.worktrees/8d2d0392/packages/dashboard/dist/logo.jpg +0 -0
  62. package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-dashboard.html +836 -0
  63. package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-overview.html +256 -0
  64. package/.worktrees/8d2d0392/packages/dashboard/package.json +25 -0
  65. package/.worktrees/8d2d0392/packages/dashboard/server/executor.js +95 -0
  66. package/.worktrees/8d2d0392/packages/dashboard/server/index.js +679 -0
  67. package/.worktrees/8d2d0392/packages/dashboard/server/parser.js +526 -0
  68. package/.worktrees/8d2d0392/packages/dashboard/server/watcher.js +357 -0
  69. package/.worktrees/8d2d0392/src/agent-session-log.js +803 -0
  70. package/.worktrees/8d2d0392/src/change-list.js +290 -0
  71. package/.worktrees/8d2d0392/src/change-risk-profile.js +339 -0
  72. package/.worktrees/8d2d0392/src/check-primitives.js +65 -0
  73. package/.worktrees/8d2d0392/src/classify-change.js +109 -0
  74. package/.worktrees/8d2d0392/src/commit-suggest.js +202 -0
  75. package/.worktrees/8d2d0392/src/config-cat.js +88 -0
  76. package/.worktrees/8d2d0392/src/config-schema.js +328 -0
  77. package/.worktrees/8d2d0392/src/constants.js +97 -0
  78. package/.worktrees/8d2d0392/src/contract-matrix.js +533 -0
  79. package/.worktrees/8d2d0392/src/datetime.js +20 -0
  80. package/.worktrees/8d2d0392/src/db-engine.js +55 -0
  81. package/.worktrees/8d2d0392/src/db.js +381 -0
  82. package/.worktrees/8d2d0392/src/decision-distill.js +517 -0
  83. package/.worktrees/8d2d0392/src/dispatch/backends/local-agent.js +155 -0
  84. package/.worktrees/8d2d0392/src/dispatch/backends/sillyhub-mcp.js +245 -0
  85. package/.worktrees/8d2d0392/src/dispatch/probe.js +246 -0
  86. package/.worktrees/8d2d0392/src/dispatch/strategy.js +121 -0
  87. package/.worktrees/8d2d0392/src/docs-check.js +825 -0
  88. package/.worktrees/8d2d0392/src/docs-debt.js +393 -0
  89. package/.worktrees/8d2d0392/src/docs-gate.js +107 -0
  90. package/.worktrees/8d2d0392/src/doctor-diagnostics.js +1099 -0
  91. package/.worktrees/8d2d0392/src/endpoint-extractor.js +409 -0
  92. package/.worktrees/8d2d0392/src/foreign-declared.js +78 -0
  93. package/.worktrees/8d2d0392/src/fs-atomic.js +73 -0
  94. package/.worktrees/8d2d0392/src/git-helper.js +150 -0
  95. package/.worktrees/8d2d0392/src/hooks/claude-pre-tool-use.cjs +130 -0
  96. package/.worktrees/8d2d0392/src/hooks/worktree-guard.js +892 -0
  97. package/.worktrees/8d2d0392/src/index.js +2854 -0
  98. package/.worktrees/8d2d0392/src/init.js +626 -0
  99. package/.worktrees/8d2d0392/src/knowledge-match.js +231 -0
  100. package/.worktrees/8d2d0392/src/local-detect.js +128 -0
  101. package/.worktrees/8d2d0392/src/local-register.js +104 -0
  102. package/.worktrees/8d2d0392/src/machine-interface.js +543 -0
  103. package/.worktrees/8d2d0392/src/migrate.js +127 -0
  104. package/.worktrees/8d2d0392/src/module-impact.js +166 -0
  105. package/.worktrees/8d2d0392/src/module-resolve.js +222 -0
  106. package/.worktrees/8d2d0392/src/modules.js +580 -0
  107. package/.worktrees/8d2d0392/src/plan-adopt-waves.js +126 -0
  108. package/.worktrees/8d2d0392/src/progress/change-registry.js +336 -0
  109. package/.worktrees/8d2d0392/src/progress/consistency-doctor.js +355 -0
  110. package/.worktrees/8d2d0392/src/progress/shared.js +36 -0
  111. package/.worktrees/8d2d0392/src/progress/stage-machine.js +594 -0
  112. package/.worktrees/8d2d0392/src/progress/step-store.js +265 -0
  113. package/.worktrees/8d2d0392/src/progress.js +1304 -0
  114. package/.worktrees/8d2d0392/src/quick-recommend.js +115 -0
  115. package/.worktrees/8d2d0392/src/quicklog.js +883 -0
  116. package/.worktrees/8d2d0392/src/review-tier.js +77 -0
  117. package/.worktrees/8d2d0392/src/run/command.js +1641 -0
  118. package/.worktrees/8d2d0392/src/run/complete-handlers.js +1552 -0
  119. package/.worktrees/8d2d0392/src/run/complete.js +1259 -0
  120. package/.worktrees/8d2d0392/src/run/concurrent-detect.js +156 -0
  121. package/.worktrees/8d2d0392/src/run/gates.js +1094 -0
  122. package/.worktrees/8d2d0392/src/run/multi-repo-context.js +208 -0
  123. package/.worktrees/8d2d0392/src/run/next.js +128 -0
  124. package/.worktrees/8d2d0392/src/run/prompt.js +843 -0
  125. package/.worktrees/8d2d0392/src/run/quick-audit.js +186 -0
  126. package/.worktrees/8d2d0392/src/run/scan-profile.js +269 -0
  127. package/.worktrees/8d2d0392/src/run/shared.js +1562 -0
  128. package/.worktrees/8d2d0392/src/run/stage.js +587 -0
  129. package/.worktrees/8d2d0392/src/run.js +23 -0
  130. package/.worktrees/8d2d0392/src/scan-diff.js +388 -0
  131. package/.worktrees/8d2d0392/src/scan-postcheck.js +524 -0
  132. package/.worktrees/8d2d0392/src/scan-staleness.js +95 -0
  133. package/.worktrees/8d2d0392/src/setup.js +414 -0
  134. package/.worktrees/8d2d0392/src/sillyhub-mcp/client.js +597 -0
  135. package/.worktrees/8d2d0392/src/sillyhub-mcp/config.js +62 -0
  136. package/.worktrees/8d2d0392/src/spec-dir-typo.js +50 -0
  137. package/.worktrees/8d2d0392/src/spec-sync.js +358 -0
  138. package/.worktrees/8d2d0392/src/stage-contract-engine.js +171 -0
  139. package/.worktrees/8d2d0392/src/stage-contract-spec.js +551 -0
  140. package/.worktrees/8d2d0392/src/stage-contract.js +948 -0
  141. package/.worktrees/8d2d0392/src/stage-review.js +597 -0
  142. package/.worktrees/8d2d0392/src/stages/archive.js +199 -0
  143. package/.worktrees/8d2d0392/src/stages/brainstorm-auto.js +285 -0
  144. package/.worktrees/8d2d0392/src/stages/brainstorm.js +624 -0
  145. package/.worktrees/8d2d0392/src/stages/cmd-existence.js +145 -0
  146. package/.worktrees/8d2d0392/src/stages/doctor.js +540 -0
  147. package/.worktrees/8d2d0392/src/stages/execute.js +1277 -0
  148. package/.worktrees/8d2d0392/src/stages/explore.js +43 -0
  149. package/.worktrees/8d2d0392/src/stages/index.js +29 -0
  150. package/.worktrees/8d2d0392/src/stages/knowledge.js +514 -0
  151. package/.worktrees/8d2d0392/src/stages/plan-postcheck.js +1398 -0
  152. package/.worktrees/8d2d0392/src/stages/plan.js +651 -0
  153. package/.worktrees/8d2d0392/src/stages/quick.js +167 -0
  154. package/.worktrees/8d2d0392/src/stages/scan.js +608 -0
  155. package/.worktrees/8d2d0392/src/stages/status.js +64 -0
  156. package/.worktrees/8d2d0392/src/stages/verify.js +246 -0
  157. package/.worktrees/8d2d0392/src/sync.js +1470 -0
  158. package/.worktrees/8d2d0392/src/task-review.js +1498 -0
  159. package/.worktrees/8d2d0392/src/taskcard-placeholders.js +21 -0
  160. package/.worktrees/8d2d0392/src/taskcard.js +217 -0
  161. package/.worktrees/8d2d0392/src/verify-postcheck.js +1718 -0
  162. package/.worktrees/8d2d0392/src/verify-probes.js +327 -0
  163. package/.worktrees/8d2d0392/src/version.js +19 -0
  164. package/.worktrees/8d2d0392/src/workflow.js +600 -0
  165. package/.worktrees/8d2d0392/src/workspace.js +125 -0
  166. package/.worktrees/8d2d0392/src/worktree-apply.js +1611 -0
  167. package/.worktrees/8d2d0392/src/worktree-deps.js +576 -0
  168. package/.worktrees/8d2d0392/src/worktree.js +1838 -0
  169. package/.worktrees/8d2d0392/templates/claude-instruction.md +23 -0
  170. package/.worktrees/8d2d0392/templates/prompts/taskcard-rules.md +29 -0
  171. package/.worktrees/8d2d0392/templates/prompts/testcase-design.md +12 -0
  172. package/.worktrees/8d2d0392/templates/prompts/verify-probes.md +32 -0
  173. package/.worktrees/8d2d0392/templates/workflows/archive-impact.yaml +79 -0
  174. package/.worktrees/8d2d0392/templates/workflows/scan-docs.yaml +132 -0
  175. package/architecture-analysis.md +448 -0
  176. package/package.json +1 -1
  177. package/src/contract-matrix.js +1 -1
  178. package/src/endpoint-extractor.js +41 -52
  179. package/src/foreign-declared.js +78 -1
  180. package/src/index.js +9 -7
  181. package/src/run/command.js +2 -1
  182. package/src/run/gates.js +17 -0
  183. package/src/run/stage.js +8 -5
  184. package/src/stage-contract-spec.js +1 -1
  185. package/src/stages/brainstorm-auto.js +43 -1
  186. package/src/stages/brainstorm.js +2 -0
  187. package/src/stages/plan.js +8 -4
  188. package/src/stages/verify.js +1 -1
  189. package/src/taskcard.js +33 -0
  190. package/src/verify-probes.js +27 -6
  191. package/src/worktree-apply.js +451 -286
  192. package/src/worktree-deps.js +93 -1
  193. package/src/worktree.js +84 -19
  194. package/templates/prompts/taskcard-rules.md +1 -1
@@ -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;
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "001-slugify",
3
+ "title": "实现 slugify 工具函数",
4
+ "tags": ["greenfield", "utility", "edge-case"],
5
+ "instruction": "在当前目录新建 src/slugify.mjs(package.json 已存在且 type=module,禁止安装任何第三方依赖),实现并导出具名函数 slugify(同时提供指向同一函数的 default 导出)。\n\n函数签名:slugify(value, options = {}),返回字符串。\n\n转换规则(按顺序执行,分隔符记为 sep,默认 '-'):\n1. value 为 null 或 undefined 时返回空字符串;其余值先 String() 转字符串。\n2. 整体 toLowerCase() 小写化。\n3. 连续空白字符(\\s+)替换为单个 sep。\n4. 删除所有不属于 [a-z0-9] 且不等于 sep 的字符。\n5. 连续多个 sep 合并为一个,并去掉首尾的 sep。\n\noptions 支持:\n- options.hyphen:分隔符 sep,默认 '-'。必须是长度为 1 的字符串,否则抛出 TypeError('hyphen must be a single character')。\n\n示例:\nslugify(' Hello World! ') === 'hello-world'\nslugify('a--b') === 'a-b'\nslugify('Ünïcode Test') === 'ncode-test'\nslugify(null) === ''\nslugify(42) === '42'\nslugify('Hello World', { hyphen: '_' }) === 'hello_world'"
6
+ }
@@ -0,0 +1,28 @@
1
+ // 判分脚本:以「被测工作目录」为 cwd 运行(node <本文件>,cwd=workdir)。退出码 0 = 通过。
2
+ import assert from 'node:assert/strict';
3
+ import { join } from 'node:path';
4
+ import { cwd } from 'node:process';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const mod = await import(pathToFileURL(join(cwd(), 'src', 'slugify.mjs')).href);
8
+ const slugify = mod.slugify ?? mod.default?.slugify;
9
+ assert.equal(typeof slugify, 'function', '应导出具名函数 slugify');
10
+ assert.equal(typeof mod.default, 'function', '应提供 default 导出');
11
+
12
+ assert.equal(slugify(null), '');
13
+ assert.equal(slugify(undefined), '');
14
+ assert.equal(slugify(''), '');
15
+ assert.equal(slugify(42), '42');
16
+ assert.equal(slugify(' Hello World! '), 'hello-world');
17
+ assert.equal(slugify('Foo Bar BAZ'), 'foo-bar-baz');
18
+ assert.equal(slugify('a--b'), 'a-b');
19
+ assert.equal(slugify('!!!'), '');
20
+ assert.equal(slugify('Ünïcode Test'), 'ncode-test');
21
+ assert.equal(slugify('tab\tand\nnewline'), 'tab-and-newline');
22
+ assert.equal(slugify('-leading and trailing-'), 'leading-and-trailing');
23
+ assert.equal(slugify('Hello World', { hyphen: '_' }), 'hello_world');
24
+ assert.equal(slugify('a b', { hyphen: '+' }), 'a+b');
25
+ assert.throws(() => slugify('a b', { hyphen: '--' }), TypeError);
26
+ assert.throws(() => slugify('a b', { hyphen: '' }), TypeError);
27
+
28
+ console.log('001-slugify verify OK');
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "task-each-day",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "private": true
6
+ }
@@ -0,0 +1,4 @@
1
+ // 待实现:eachDay(start, end) —— 契约见任务说明
2
+ export function eachDay(start, end) {
3
+ throw new Error('not implemented');
4
+ }
@@ -0,0 +1,30 @@
1
+ // 参考解(dry-run 假 agent 使用,也供人工对照)。
2
+ const DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})(?:[T ].*)?$/;
3
+ const DAY_MS = 86_400_000;
4
+
5
+ function parseUtc(value) {
6
+ if (typeof value !== 'string') throw new TypeError('invalid date');
7
+ const m = DATE_PREFIX.exec(value.trim());
8
+ if (!m) throw new TypeError('invalid date');
9
+ const y = Number(m[1]);
10
+ const mo = Number(m[2]);
11
+ const d = Number(m[3]);
12
+ const ts = Date.UTC(y, mo - 1, d);
13
+ const dt = new Date(ts);
14
+ // 往返校验拦住 '2024-02-30' 这类被 Date 自动进位的假日期
15
+ if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d) {
16
+ throw new TypeError('invalid date');
17
+ }
18
+ return ts;
19
+ }
20
+
21
+ export function eachDay(start, end) {
22
+ const s = parseUtc(start);
23
+ const e = parseUtc(end);
24
+ if (e < s) throw new RangeError('end must not be before start');
25
+ const days = [];
26
+ for (let t = s; t <= e; t += DAY_MS) {
27
+ days.push(new Date(t).toISOString().slice(0, 10));
28
+ }
29
+ return days;
30
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "002-each-day",
3
+ "title": "实现 eachDay 日期区间函数(棕地补全)",
4
+ "tags": ["brownfield", "date", "validation"],
5
+ "instruction": "src/dates.mjs 已有函数骨架 eachDay(start, end)(当前直接 throw),在原文件完成实现,保持具名导出 eachDay 不变,禁止引入第三方依赖。\n\n契约:\n- 入参为字符串,接受 'YYYY-MM-DD',或以它开头的时间形式(如 '2024-01-01T10:00:00Z'、'2024-01-01 08:00');只取日期部分参与运算。\n- 返回闭区间 [start, end] 内每一天的 'YYYY-MM-DD' 字符串数组(含两端;起止同天返回单元素数组)。\n- 日期运算一律用 UTC(避免时区/DST 干扰)。\n- end 的日期小于 start 时抛 RangeError。\n- 格式非法(不匹配 'YYYY-MM-DD' 前缀、日期不存在如 '2024-02-30'、或非字符串)抛 TypeError('invalid date')。\n\n示例:\neachDay('2024-02-27', '2024-03-02') → ['2024-02-27','2024-02-28','2024-02-29','2024-03-01','2024-03-02']\neachDay('2024-01-15', '2024-01-15') → ['2024-01-15']"
6
+ }
@@ -0,0 +1,31 @@
1
+ // 判分脚本:cwd=被测工作目录,退出码 0 = 通过。
2
+ import assert from 'node:assert/strict';
3
+ import { join } from 'node:path';
4
+ import { cwd } from 'node:process';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const mod = await import(pathToFileURL(join(cwd(), 'src', 'dates.mjs')).href);
8
+ const eachDay = mod.eachDay ?? mod.default?.eachDay;
9
+ assert.equal(typeof eachDay, 'function', '应导出具名函数 eachDay');
10
+
11
+ assert.deepEqual(
12
+ eachDay('2024-02-27', '2024-03-02'),
13
+ ['2024-02-27', '2024-02-28', '2024-02-29', '2024-03-01', '2024-03-02'],
14
+ );
15
+ assert.deepEqual(
16
+ eachDay('2024-12-30', '2025-01-02'),
17
+ ['2024-12-30', '2024-12-31', '2025-01-01', '2025-01-02'],
18
+ );
19
+ assert.deepEqual(eachDay('2024-01-15', '2024-01-15'), ['2024-01-15']);
20
+ assert.deepEqual(
21
+ eachDay('2024-01-01T10:00:00Z', '2024-01-03T23:00:00Z'),
22
+ ['2024-01-01', '2024-01-02', '2024-01-03'],
23
+ );
24
+ // 2100 是「逢百年不闰」的非闰年
25
+ assert.deepEqual(eachDay('2100-02-28', '2100-03-01'), ['2100-02-28', '2100-03-01']);
26
+ assert.throws(() => eachDay('2024-03-01', '2024-02-28'), RangeError);
27
+ assert.throws(() => eachDay('2024-02-30', '2024-03-01'), TypeError);
28
+ assert.throws(() => eachDay('2024-1-1', '2024-01-02'), TypeError);
29
+ assert.throws(() => eachDay(null, '2024-01-02'), TypeError);
30
+
31
+ console.log('002-each-day verify OK');
@@ -0,0 +1,5 @@
1
+ the quick brown fox jumps over the lazy dog
2
+ the dog barks and the fox runs
3
+ quick quick slow
4
+ Apple apple APPLE
5
+ tea for two and two for tea
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "task-word-report",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "private": true
6
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ // 参考解(dry-run 假 agent 使用,也供人工对照)。
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ function parseArgs(argv) {
6
+ const out = { input: 'data/notes.txt', top: 5, minLen: 1 };
7
+ for (let i = 0; i < argv.length; i++) {
8
+ const a = argv[i];
9
+ if (a === '--input') out.input = argv[++i];
10
+ else if (a === '--top') out.top = Number(argv[++i]);
11
+ else if (a === '--min-len') out.minLen = Number(argv[++i]);
12
+ }
13
+ return out;
14
+ }
15
+
16
+ const { input, top, minLen } = parseArgs(process.argv.slice(2));
17
+ let text;
18
+ try {
19
+ text = readFileSync(input, 'utf8');
20
+ } catch (err) {
21
+ console.error(`无法读取输入文件: ${input}(${err.code ?? err.message})`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const counts = new Map();
26
+ for (const word of text.toLowerCase().split(/\s+/)) {
27
+ if (!word || word.length < minLen) continue;
28
+ counts.set(word, (counts.get(word) ?? 0) + 1);
29
+ }
30
+ const totalWords = [...counts.values()].reduce((a, b) => a + b, 0);
31
+ const topWords = [...counts.entries()]
32
+ .sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))
33
+ .slice(0, top)
34
+ .map(([word, count]) => ({ word, count }));
35
+
36
+ process.stdout.write(`${JSON.stringify({ totalWords, top: topWords })}\n`);
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "003-word-report",
3
+ "title": "实现词频统计 CLI",
4
+ "tags": ["greenfield", "cli", "output-contract"],
5
+ "instruction": "当前目录 data/notes.txt 已提供输入样本。新建 bin/report.mjs(纯 Node 实现,禁止第三方依赖,package.json 已是 type=module)。\n\n功能:词频统计 CLI。用法:node bin/report.mjs [--input <路径>] [--top N] [--min-len M]\n- --input:输入文件路径,默认 data/notes.txt(相对当前工作目录解析)。\n- 读取文本,小写化后按空白切分为词。\n- --min-len:过滤掉长度小于 M 的词,默认 1。\n- --top:输出频次最高的前 N 个词,默认 5;频次相同时按词的字典序升序排列。\n- stdout 输出且仅输出一行 JSON:{\"totalWords\":<过滤后的总词数>,\"top\":[{\"word\":<词>,\"count\":<次数>}, ...]}\n- 输入文件不存在或不可读时:向 stderr 输出错误信息,退出码 1。"
6
+ }
@@ -0,0 +1,38 @@
1
+ // 判分脚本:cwd=被测工作目录,退出码 0 = 通过。
2
+ // 期望值基于 data/notes.txt 的固定内容硬编码,保证判分独立于被测实现。
3
+ import assert from 'node:assert/strict';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { join } from 'node:path';
6
+ import { cwd } from 'node:process';
7
+
8
+ const reportCli = join(cwd(), 'bin', 'report.mjs');
9
+ const run = (args) => spawnSync('node', [reportCli, ...args], { encoding: 'utf8' });
10
+
11
+ const run1 = run(['--top', '3']);
12
+ assert.equal(run1.status, 0, `CLI 应成功运行: ${run1.stderr}`);
13
+ assert.ok(run1.stdout.trim().startsWith('{'), 'stdout 应为 JSON');
14
+ assert.equal(run1.stdout.trim().split('\n').length, 1, '只允许输出一行 JSON');
15
+ assert.deepEqual(JSON.parse(run1.stdout), {
16
+ totalWords: 29,
17
+ top: [
18
+ { word: 'the', count: 4 },
19
+ { word: 'apple', count: 3 },
20
+ { word: 'quick', count: 3 },
21
+ ],
22
+ });
23
+
24
+ const run2 = run(['--top', '2', '--min-len', '5']);
25
+ assert.equal(run2.status, 0);
26
+ assert.deepEqual(JSON.parse(run2.stdout), {
27
+ totalWords: 9,
28
+ top: [
29
+ { word: 'apple', count: 3 },
30
+ { word: 'quick', count: 3 },
31
+ ],
32
+ });
33
+
34
+ const run3 = run(['--input', 'no-such-file.txt']);
35
+ assert.notEqual(run3.status, 0, '文件不存在应非零退出');
36
+ assert.ok((run3.stderr ?? '').trim().length > 0, '应向 stderr 输出错误信息');
37
+
38
+ console.log('003-word-report verify OK');
Binary file