sillyspec 3.32.10 → 3.33.1

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 (197) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  2. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  3. package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
  4. package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
  5. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  6. package/.github/workflows/ci.yml +66 -0
  7. package/.husky/pre-push +7 -6
  8. package/AGENTS.md +1 -1
  9. package/assets/command-cards/flow.md +1 -1
  10. package/eval/eval.config.json +3 -3
  11. package/eval/lib/config.mjs +6 -3
  12. package/eval/lib/util.mjs +16 -3
  13. package/eval/lib/workdir.mjs +4 -1
  14. package/eval/results/eval.db +0 -0
  15. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
  16. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
  17. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
  18. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
  19. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
  20. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
  21. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
  22. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
  23. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
  24. package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
  25. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
  26. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
  27. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
  28. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
  29. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
  30. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
  31. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
  32. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
  33. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
  34. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
  35. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
  36. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
  37. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
  38. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
  39. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
  40. package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
  41. package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
  42. package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
  43. package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
  44. package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
  45. package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
  46. package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  47. package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
  48. package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
  49. package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
  50. package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
  51. package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
  52. package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
  53. package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
  54. package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
  55. package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
  56. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
  57. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
  58. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
  59. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
  60. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
  61. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
  62. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
  63. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  64. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
  65. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  66. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  67. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
  68. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  69. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
  70. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
  71. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
  72. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
  73. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
  74. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
  75. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  76. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
  77. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  78. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
  79. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  80. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
  81. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
  82. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  83. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
  84. package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
  85. package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
  86. package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
  87. package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
  88. package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
  89. package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
  90. package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
  91. package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
  92. package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
  93. package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  94. package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
  95. package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
  96. package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
  97. package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
  98. package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
  99. package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
  100. package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
  101. package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
  102. package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
  103. package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
  104. package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  105. package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
  106. package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
  107. package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
  108. package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
  109. package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
  110. package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
  111. package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
  112. package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
  113. package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
  114. package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
  115. package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
  116. package/eval/tasks/004-phase-stats/task.json +6 -0
  117. package/eval/tasks/004-phase-stats/verify.mjs +38 -0
  118. package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
  119. package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
  120. package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
  121. package/eval/tasks/005-orders-export/task.json +6 -0
  122. package/eval/tasks/005-orders-export/verify.mjs +28 -0
  123. package/package.json +1 -1
  124. package/quicklog/tmp-fix-review.mjs +21 -0
  125. package/src/archive-delta.js +4 -3
  126. package/src/ceremony-tier.js +2 -1
  127. package/src/change-delete.js +6 -5
  128. package/src/contract-matrix.js +2 -1
  129. package/src/cross-placement.js +5 -1
  130. package/src/db.js +8 -2
  131. package/src/decision-distill.js +29 -3
  132. package/src/decision-redomain.js +148 -0
  133. package/src/deleted-ledger.js +2 -1
  134. package/src/design-facts.js +3 -3
  135. package/src/docs-debt.js +3 -3
  136. package/src/doctor-diagnostics.js +8 -3
  137. package/src/flow-draft.js +2 -1
  138. package/src/flow-parity.js +10 -2
  139. package/src/flow.js +112 -63
  140. package/src/forensics.js +236 -0
  141. package/src/fr-index.js +2 -1
  142. package/src/friction-ledger.js +2 -1
  143. package/src/friction-tally.js +8 -4
  144. package/src/handoff.js +2 -1
  145. package/src/hooks/worktree-guard.js +5 -5
  146. package/src/index.js +75 -8
  147. package/src/knowledge-graph.js +3 -4
  148. package/src/knowledge-hits.js +2 -1
  149. package/src/knowledge-vector.js +2 -1
  150. package/src/module-impact.js +2 -5
  151. package/src/modules.js +31 -0
  152. package/src/progress/consistency-doctor.js +5 -2
  153. package/src/progress/stage-machine.js +6 -2
  154. package/src/progress.js +19 -8
  155. package/src/quicklog.js +194 -9
  156. package/src/review-dispatch.js +4 -3
  157. package/src/review-tier.js +2 -1
  158. package/src/route-hindsight.js +4 -3
  159. package/src/run/bg-sync.js +10 -3
  160. package/src/run/command.js +13 -11
  161. package/src/run/complete-handlers.js +40 -37
  162. package/src/run/complete.js +12 -8
  163. package/src/run/gate-snapshot-ledger.js +2 -1
  164. package/src/run/gate-snapshot.js +57 -11
  165. package/src/run/gates.js +38 -21
  166. package/src/run/lint-ownership.js +37 -0
  167. package/src/run/prompt.js +46 -30
  168. package/src/run/scan-profile.js +2 -1
  169. package/src/run/shared.js +109 -5
  170. package/src/run/stage.js +5 -4
  171. package/src/run/test-ledger.js +2 -1
  172. package/src/run/verify-quality-scan.js +21 -6
  173. package/src/runtime-paths.js +189 -0
  174. package/src/scan-refresh.js +4 -3
  175. package/src/scope-audit.js +7 -6
  176. package/src/sentinel-assertions.js +37 -0
  177. package/src/spec-sync.js +118 -23
  178. package/src/stage-contract.js +24 -2
  179. package/src/stage-review.js +5 -3
  180. package/src/stages/brainstorm.js +72 -21
  181. package/src/stages/plan-postcheck.js +13 -2
  182. package/src/stages/plan.js +19 -0
  183. package/src/sync-noise.js +3 -2
  184. package/src/sync.js +71 -15
  185. package/src/task-done.js +2 -1
  186. package/src/task-review.js +7 -5
  187. package/src/task-tick.js +23 -1
  188. package/src/test-bindings.js +32 -3
  189. package/src/ui-visual.js +119 -4
  190. package/src/validate-artifacts.js +2 -1
  191. package/src/verify-draft.js +2 -1
  192. package/src/verify-postcheck.js +128 -27
  193. package/src/verify-probes.js +157 -52
  194. package/src/watcher.js +15 -9
  195. package/src/worktree-apply.js +28 -3
  196. package/src/write-audit.js +2 -1
  197. package/templates/agents-instruction.md +1 -1
@@ -0,0 +1,16 @@
1
+ 2026-10-15T09:00:00Z START s1 login
2
+ 2026-10-15T09:00:03Z START s2 upload
3
+ 2026-10-15T09:00:04Z END s1 login
4
+ 2026-10-15T09:00:05Z START s3 download
5
+ 2026-10-15T09:00:20Z END s2 upload
6
+ 2026-10-15T09:00:15Z END s3 download
7
+
8
+ 2026-10-15T09:05:00Z START s4 login
9
+ 2026-10-15T09:05:09Z END s4 login
10
+ 2026-10-15T09:06:00Z START s5 upload
11
+ 2026-10-15T09:07:00Z END s6 download
12
+ 2026-10-15T09:08:00Z START s7 login
13
+ 2026-10-15T09:08:03Z END s7 login
14
+ 2026-10-15T09:08:30Z START s7 login
15
+ 2026-10-15T09:08:36Z END s7 login
16
+
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // 参考解(eval dry-run 参考与判别力测试用;不进 agent 工作副本、不进任务指纹)。
3
+ // 覆盖全部隐含面:剥 UTF-8 BOM;容忍 CRLF 行尾;跳过空行;CSV 引号语义(引号内逗号非
4
+ // 分隔符,引号内金额剥千分位逗号);输出前自建 out/ 目录。
5
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+
8
+ /** 引号感知的单行 CSV 切分(覆盖本任务数据形态:引号包裹含逗号字段)。 */
9
+ function splitCsvLine(line) {
10
+ const fields = [];
11
+ let cur = '';
12
+ let inQuotes = false;
13
+ for (let i = 0; i < line.length; i++) {
14
+ const ch = line[i];
15
+ if (inQuotes) {
16
+ if (ch === '"') {
17
+ if (line[i + 1] === '"') { cur += '"'; i++; } else { inQuotes = false; }
18
+ } else { cur += ch; }
19
+ } else if (ch === '"') {
20
+ inQuotes = true;
21
+ } else if (ch === ',') {
22
+ fields.push(cur); cur = '';
23
+ } else { cur += ch; }
24
+ }
25
+ fields.push(cur);
26
+ return fields;
27
+ }
28
+
29
+ const raw = readFileSync('data/orders.csv', 'utf8').replace(/^\uFEFF/, ''); // 剥 BOM
30
+ const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== ''); // CRLF 容忍 + 空行跳过
31
+ const header = splitCsvLine(lines[0]);
32
+ const col = (name) => header.indexOf(name);
33
+ const iRegion = col('region');
34
+ const iAmount = col('amount');
35
+ const iStatus = col('status');
36
+
37
+ const agg = new Map(); // region -> { total, count }
38
+ for (const line of lines.slice(1)) {
39
+ const f = splitCsvLine(line);
40
+ const status = (f[iStatus] ?? '').trim();
41
+ if (status !== 'paid') continue;
42
+ const region = (f[iRegion] ?? '').trim();
43
+ const amount = Number.parseFloat((f[iAmount] ?? '').replace(/,/g, '')); // 引号内千分位剥除
44
+ if (!Number.isFinite(amount)) continue;
45
+ const a = agg.get(region) ?? { total: 0, count: 0 };
46
+ a.total += amount;
47
+ a.count += 1;
48
+ agg.set(region, a);
49
+ }
50
+
51
+ const regions = [...agg.entries()]
52
+ .map(([region, a]) => ({ region, total: Math.round(a.total * 100) / 100, count: a.count }))
53
+ .sort((x, y) => (x.region < y.region ? -1 : x.region > y.region ? 1 : 0));
54
+
55
+ const outPath = join('out', 'summary.json');
56
+ mkdirSync(dirname(outPath), { recursive: true }); // 隐含面:目录不存在须自建
57
+ writeFileSync(outPath, JSON.stringify({ regions }) + '\n');
@@ -0,0 +1,9 @@
1
+ id,region,amount,status
2
+ 1,EU,"1,234.50",paid
3
+ 2,APAC,200.00,paid
4
+ 3,EU,99.99,pending
5
+ 4,APAC,"2,000.00",paid
6
+
7
+ 5,EU,50.00,paid
8
+ 6,APAC,75.25,refunded
9
+ 7,EU,"1,000.00",paid
@@ -0,0 +1 @@
1
+ {"regions":[{"region":"APAC","total":2200,"count":2},{"region":"EU","total":2284.5,"count":3}]}
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // 参考解(eval dry-run 参考与判别力测试用;不进 agent 工作副本、不进任务指纹)。
3
+ // 覆盖全部隐含面:剥 UTF-8 BOM;容忍 CRLF 行尾;跳过空行;CSV 引号语义(引号内逗号非
4
+ // 分隔符,引号内金额剥千分位逗号);输出前自建 out/ 目录。
5
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+
8
+ /** 引号感知的单行 CSV 切分(覆盖本任务数据形态:引号包裹含逗号字段)。 */
9
+ function splitCsvLine(line) {
10
+ const fields = [];
11
+ let cur = '';
12
+ let inQuotes = false;
13
+ for (let i = 0; i < line.length; i++) {
14
+ const ch = line[i];
15
+ if (inQuotes) {
16
+ if (ch === '"') {
17
+ if (line[i + 1] === '"') { cur += '"'; i++; } else { inQuotes = false; }
18
+ } else { cur += ch; }
19
+ } else if (ch === '"') {
20
+ inQuotes = true;
21
+ } else if (ch === ',') {
22
+ fields.push(cur); cur = '';
23
+ } else { cur += ch; }
24
+ }
25
+ fields.push(cur);
26
+ return fields;
27
+ }
28
+
29
+ const raw = readFileSync('data/orders.csv', 'utf8').replace(/^\uFEFF/, ''); // 剥 BOM
30
+ const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== ''); // CRLF 容忍 + 空行跳过
31
+ const header = splitCsvLine(lines[0]);
32
+ const col = (name) => header.indexOf(name);
33
+ const iRegion = col('region');
34
+ const iAmount = col('amount');
35
+ const iStatus = col('status');
36
+
37
+ const agg = new Map(); // region -> { total, count }
38
+ for (const line of lines.slice(1)) {
39
+ const f = splitCsvLine(line);
40
+ const status = (f[iStatus] ?? '').trim();
41
+ if (status !== 'paid') continue;
42
+ const region = (f[iRegion] ?? '').trim();
43
+ const amount = Number.parseFloat((f[iAmount] ?? '').replace(/,/g, '')); // 引号内千分位剥除
44
+ if (!Number.isFinite(amount)) continue;
45
+ const a = agg.get(region) ?? { total: 0, count: 0 };
46
+ a.total += amount;
47
+ a.count += 1;
48
+ agg.set(region, a);
49
+ }
50
+
51
+ const regions = [...agg.entries()]
52
+ .map(([region, a]) => ({ region, total: Math.round(a.total * 100) / 100, count: a.count }))
53
+ .sort((x, y) => (x.region < y.region ? -1 : x.region > y.region ? 1 : 0));
54
+
55
+ const outPath = join('out', 'summary.json');
56
+ mkdirSync(dirname(outPath), { recursive: true }); // 隐含面:目录不存在须自建
57
+ writeFileSync(outPath, JSON.stringify({ regions }) + '\n');
@@ -0,0 +1,9 @@
1
+ id,region,amount,status
2
+ 1,EU,"1,234.50",paid
3
+ 2,APAC,200.00,paid
4
+ 3,EU,99.99,pending
5
+ 4,APAC,"2,000.00",paid
6
+
7
+ 5,EU,50.00,paid
8
+ 6,APAC,75.25,refunded
9
+ 7,EU,"1,000.00",paid
@@ -0,0 +1 @@
1
+ {"regions":[{"region":"APAC","total":2200,"count":2},{"region":"EU","total":2284.5,"count":3}]}
@@ -0,0 +1,16 @@
1
+ 2026-10-15T09:00:00Z START s1 login
2
+ 2026-10-15T09:00:03Z START s2 upload
3
+ 2026-10-15T09:00:04Z END s1 login
4
+ 2026-10-15T09:00:05Z START s3 download
5
+ 2026-10-15T09:00:20Z END s2 upload
6
+ 2026-10-15T09:00:15Z END s3 download
7
+
8
+ 2026-10-15T09:05:00Z START s4 login
9
+ 2026-10-15T09:05:09Z END s4 login
10
+ 2026-10-15T09:06:00Z START s5 upload
11
+ 2026-10-15T09:07:00Z END s6 download
12
+ 2026-10-15T09:08:00Z START s7 login
13
+ 2026-10-15T09:08:03Z END s7 login
14
+ 2026-10-15T09:08:30Z START s7 login
15
+ 2026-10-15T09:08:36Z END s7 login
16
+
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ // 参考解(eval dry-run 参考与判别力测试用;不进 agent 工作副本、不进任务指纹)。
3
+ // 覆盖全部隐含面:乱序交错行按 (sessionId, phase) 配对;空行/纯空白行跳过;行字段 trim
4
+ // (兼容 CRLF);未闭合 START 与孤儿 END 零贡献;同 (sessionId, phase) 多轮累加。
5
+ import { readFileSync } from 'node:fs';
6
+
7
+ const raw = readFileSync('data/events.log', 'utf8');
8
+ const open = new Map(); // "session|phase" -> start ms
9
+ const agg = new Map(); // phase -> { rounds, totalSeconds }
10
+
11
+ for (const line of raw.split(/\r?\n/)) {
12
+ const t = line.trim();
13
+ if (!t) continue; // 空行 / 纯空白行
14
+ const parts = t.split(/\s+/);
15
+ if (parts.length < 4) continue;
16
+ const [ts, kind, session, phase] = parts;
17
+ const ms = Date.parse(ts);
18
+ if (!Number.isFinite(ms)) continue;
19
+ const key = `${session}|${phase}`;
20
+ if (kind === 'START') {
21
+ if (!open.has(key)) open.set(key, ms); // 重复 START 以首者计,防覆盖丢轮
22
+ } else if (kind === 'END') {
23
+ const start = open.get(key);
24
+ if (start === undefined) continue; // 孤儿 END 零贡献
25
+ open.delete(key);
26
+ const a = agg.get(phase) ?? { rounds: 0, totalSeconds: 0 };
27
+ a.rounds += 1;
28
+ a.totalSeconds += Math.round((ms - start) / 1000);
29
+ agg.set(phase, a);
30
+ }
31
+ // 未闭合 START:循环结束自然零贡献
32
+ }
33
+
34
+ const phases = [...agg.entries()]
35
+ .map(([phase, a]) => ({ phase, rounds: a.rounds, totalSeconds: a.totalSeconds }))
36
+ .sort((x, y) => (x.phase < y.phase ? -1 : x.phase > y.phase ? 1 : 0));
37
+ process.stdout.write(JSON.stringify({ phases }) + '\n');
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "004-phase-stats",
3
+ "title": "日志会话耗时统计 CLI(种植缺陷探针:隐含面在 fixture,不在指令)",
4
+ "tags": ["greenfield", "cli", "planted-defect", "implied-inputs"],
5
+ "instruction": "在当前目录新建 bin/phase-stats.mjs(Node ≥ 18,只用 Node 内置模块,禁止安装任何第三方依赖)。\n\n读取 data/events.log:每行一条事件,格式为「<ISO-8601 UTC 时间戳> <START|END> <sessionId> <phase>」(单个空格分隔,UTF-8)。同一 sessionId 的同名 phase 从 START 到 END 为一轮。\n\n输出一行 JSON 到 stdout(除此之外无任何输出):\n{\"phases\":[{\"phase\":\"<名>\",\"rounds\":<完成轮数>,\"totalSeconds\":<所有完成轮次时长之和,整数秒>}]}\n\nphases 按 phase 字典序升序排列。数据以仓库附带的 data/events.log 为准。确保 node bin/phase-stats.mjs 可运行。"
6
+ }
@@ -0,0 +1,38 @@
1
+ // 判分脚本:cwd=被测工作目录,退出码 0 = 通过。
2
+ // 期望值基于 data/events.log 固定内容硬编码(download r1/10s、login r4/22s、upload r1/17s),
3
+ // 保证判分独立于被测实现。种植缺陷探针(2026-10-15-eval-planted-defect-probe):
4
+ // 指令只命名解析格式与输出契约;五类隐含面(乱序交错/空行/纯空白行/未闭合 START/孤儿 END)
5
+ // 全部由 fixture 承载——失败时下方断言消息逐项定位是哪条隐含面挂了。
6
+ import assert from 'node:assert/strict';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { join } from 'node:path';
9
+ import { cwd } from 'node:process';
10
+
11
+ const run = spawnSync('node', [join('bin', 'phase-stats.mjs')], { encoding: 'utf8' });
12
+ assert.equal(run.status, 0, `CLI 应成功运行(exit=${run.status}): ${String(run.stderr).slice(0, 300)}`);
13
+
14
+ const out = String(run.stdout).trim();
15
+ assert.equal(out.split('\n').length, 1, 'stdout 只允许一行 JSON');
16
+ let parsed;
17
+ assert.doesNotThrow(() => { parsed = JSON.parse(out); }, 'stdout 应为合法 JSON');
18
+ assert.ok(Array.isArray(parsed.phases), 'phases 应为数组');
19
+ assert.deepEqual(
20
+ parsed.phases.map((p) => p.phase),
21
+ ['download', 'login', 'upload'],
22
+ 'phases 应按字典序升序且恰好三个域(隐含面:乱序/空行/空白行的容错解析)'
23
+ );
24
+ assert.deepEqual(
25
+ parsed.phases[0],
26
+ { phase: 'download', rounds: 1, totalSeconds: 10 },
27
+ 'download: s3 一轮 10s(隐含面:孤儿 END s6 零贡献,不得计轮不得崩)'
28
+ );
29
+ assert.deepEqual(
30
+ parsed.phases[1],
31
+ { phase: 'login', rounds: 4, totalSeconds: 22 },
32
+ 'login: s1(4s)+s4(9s)+s7 两轮(3s+6s)=22s(隐含面:跨 session 交错乱序按 (sessionId,phase) 配对、同 session 多轮累加)'
33
+ );
34
+ assert.deepEqual(
35
+ parsed.phases[2],
36
+ { phase: 'upload', rounds: 1, totalSeconds: 17 },
37
+ 'upload: s2 一轮 17s(隐含面:未闭合 START s5 零贡献,不得计入轮数或时长)'
38
+ );
@@ -0,0 +1,9 @@
1
+ id,region,amount,status
2
+ 1,EU,"1,234.50",paid
3
+ 2,APAC,200.00,paid
4
+ 3,EU,99.99,pending
5
+ 4,APAC,"2,000.00",paid
6
+
7
+ 5,EU,50.00,paid
8
+ 6,APAC,75.25,refunded
9
+ 7,EU,"1,000.00",paid
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // 参考解(eval dry-run 参考与判别力测试用;不进 agent 工作副本、不进任务指纹)。
3
+ // 覆盖全部隐含面:剥 UTF-8 BOM;容忍 CRLF 行尾;跳过空行;CSV 引号语义(引号内逗号非
4
+ // 分隔符,引号内金额剥千分位逗号);输出前自建 out/ 目录。
5
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+
8
+ /** 引号感知的单行 CSV 切分(覆盖本任务数据形态:引号包裹含逗号字段)。 */
9
+ function splitCsvLine(line) {
10
+ const fields = [];
11
+ let cur = '';
12
+ let inQuotes = false;
13
+ for (let i = 0; i < line.length; i++) {
14
+ const ch = line[i];
15
+ if (inQuotes) {
16
+ if (ch === '"') {
17
+ if (line[i + 1] === '"') { cur += '"'; i++; } else { inQuotes = false; }
18
+ } else { cur += ch; }
19
+ } else if (ch === '"') {
20
+ inQuotes = true;
21
+ } else if (ch === ',') {
22
+ fields.push(cur); cur = '';
23
+ } else { cur += ch; }
24
+ }
25
+ fields.push(cur);
26
+ return fields;
27
+ }
28
+
29
+ const raw = readFileSync('data/orders.csv', 'utf8').replace(/^\uFEFF/, ''); // 剥 BOM
30
+ const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== ''); // CRLF 容忍 + 空行跳过
31
+ const header = splitCsvLine(lines[0]);
32
+ const col = (name) => header.indexOf(name);
33
+ const iRegion = col('region');
34
+ const iAmount = col('amount');
35
+ const iStatus = col('status');
36
+
37
+ const agg = new Map(); // region -> { total, count }
38
+ for (const line of lines.slice(1)) {
39
+ const f = splitCsvLine(line);
40
+ const status = (f[iStatus] ?? '').trim();
41
+ if (status !== 'paid') continue;
42
+ const region = (f[iRegion] ?? '').trim();
43
+ const amount = Number.parseFloat((f[iAmount] ?? '').replace(/,/g, '')); // 引号内千分位剥除
44
+ if (!Number.isFinite(amount)) continue;
45
+ const a = agg.get(region) ?? { total: 0, count: 0 };
46
+ a.total += amount;
47
+ a.count += 1;
48
+ agg.set(region, a);
49
+ }
50
+
51
+ const regions = [...agg.entries()]
52
+ .map(([region, a]) => ({ region, total: Math.round(a.total * 100) / 100, count: a.count }))
53
+ .sort((x, y) => (x.region < y.region ? -1 : x.region > y.region ? 1 : 0));
54
+
55
+ const outPath = join('out', 'summary.json');
56
+ mkdirSync(dirname(outPath), { recursive: true }); // 隐含面:目录不存在须自建
57
+ writeFileSync(outPath, JSON.stringify({ regions }) + '\n');
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "005-orders-export",
3
+ "title": "订单汇总导出 CLI(种植缺陷探针 II:数据陷阱族在 fixture,指令零提示)",
4
+ "tags": ["greenfield", "cli", "planted-defect", "implied-inputs", "data-traps"],
5
+ "instruction": "在当前目录新建 bin/export-orders.mjs(Node ≥ 18,只用 Node 内置模块,禁止安装任何第三方依赖)。\n\n读取 data/orders.csv(UTF-8,逗号分隔,首行为表头:id,region,amount,status)。按 region 汇总 status=paid 的订单金额总和,输出到 out/summary.json:\n{\"regions\":[{\"region\":\"<名>\",\"total\":<金额总和,数值>,\"count\":<订单数,整数>}]}\n\nregions 按 region 字母序升序。数据以仓库附带的 data/orders.csv 为准。确保 node bin/export-orders.mjs 可运行且产物符合契约。"
6
+ }
@@ -0,0 +1,28 @@
1
+ // 判分脚本:cwd=被测工作目录,退出码 0 = 通过。
2
+ // 期望值基于 data/orders.csv 固定内容硬编码(APAC total=2200 count=2;EU total=2284.5 count=3),
3
+ // 判分独立于被测实现。种植缺陷探针 II(2026-10-15-eval-probe2-orders-export):
4
+ // 指令只命名表头/过滤/输出契约;五类隐含面(out/ 目录自建、UTF-8 BOM、CRLF 行尾、
5
+ // 引号内千分位逗号、混排空行)全部由 fixture 承载——失败时下方断言逐项定位。
6
+ import assert from 'node:assert/strict';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { join } from 'node:path';
10
+ import { cwd } from 'node:process';
11
+
12
+ const run = spawnSync('node', [join('bin', 'export-orders.mjs')], { encoding: 'utf8' });
13
+ assert.equal(run.status, 0, `CLI 应成功运行(exit=${run.status}): ${String(run.stderr).slice(0, 300)}`);
14
+
15
+ const summaryPath = join(cwd(), 'out', 'summary.json');
16
+ assert.ok(existsSync(summaryPath), 'out/summary.json 应存在(隐含面:out/ 目录不存在须自建)');
17
+
18
+ let parsed;
19
+ assert.doesNotThrow(() => { parsed = JSON.parse(readFileSync(summaryPath, 'utf8')); }, 'out/summary.json 应为合法 JSON');
20
+ assert.ok(Array.isArray(parsed.regions), 'regions 应为数组');
21
+ assert.deepEqual(
22
+ parsed.regions,
23
+ [
24
+ { region: 'APAC', total: 200 + 2000, count: 2 },
25
+ { region: 'EU', total: 1234.5 + 50 + 1000, count: 3 },
26
+ ],
27
+ 'regions 应按字母序且金额/计数正确(隐含面:BOM 剥除、CRLF 容忍、引号内逗号按 CSV 语义、空行跳过、pending/refunded 排除)'
28
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.32.10",
3
+ "version": "3.33.1",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -0,0 +1,21 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+ import { unlinkSync } from 'node:fs'
3
+
4
+ const p = '.sillyspec/changes/2026-10-15-eval-workdir-naming/review.json'
5
+ const BS = String.fromCharCode(92)
6
+ let raw = readFileSync(p, 'utf8')
7
+ const bad = '[^' + BS + 'd]'
8
+ const good = '[^' + BS + BS + 'd]'
9
+ if (raw.includes(bad)) {
10
+ raw = raw.replace(bad, good)
11
+ writeFileSync(p, raw)
12
+ console.log('已修复 \\d → \\\\d')
13
+ } else {
14
+ console.log('未找到待修复序列')
15
+ }
16
+ JSON.parse(readFileSync(p, 'utf8'))
17
+ console.log('JSON.parse 通过')
18
+ try {
19
+ unlinkSync('quicklog/tmp-scan-review.mjs')
20
+ console.log('临时脚本已删')
21
+ } catch {}
@@ -29,6 +29,7 @@
29
29
  * endpoint-extractor,被依赖者均不反向 import 本模块。
30
30
  */
31
31
  import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
32
+ import { runtimeStatePath } from './runtime-paths.js'
32
33
  import { basename, dirname, join } from 'node:path'
33
34
  import { parseDecisionDomains, loadModuleMap, deriveActualModules } from './design-facts.js'
34
35
  import { parseModuleMapSimple } from './modules.js'
@@ -153,7 +154,7 @@ export function collectDeltaSources({ changeDir, specRoot, project, runtimeRoot,
153
154
  // 兜底(Gap-2/D-003):全无 reconcile → apply-pathspec-<change>.txt 逐行路径作交付清单
154
155
  let deliverables = []
155
156
  if (!reconcile && runtimeRoot && change) {
156
- const text = readTextSafe(join(runtimeRoot, `apply-pathspec-${change}.txt`))
157
+ const text = readTextSafe(runtimeStatePath(runtimeRoot, `apply-pathspec-${change}.txt`))
157
158
  if (text !== null) {
158
159
  deliverables = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean)
159
160
  }
@@ -454,8 +455,8 @@ export function writeLastDeltaSidecar(runtimeRoot, summary) {
454
455
  updatedAt: new Date().toISOString(),
455
456
  }
456
457
  mkdirSync(runtimeRoot, { recursive: true })
457
- writeFileSync(join(runtimeRoot, 'last-delta.json'), JSON.stringify(payload, null, 2) + '\n')
458
- return { ok: true, path: join(runtimeRoot, 'last-delta.json') }
458
+ writeFileSync(runtimeStatePath(runtimeRoot, 'last-delta.json'), JSON.stringify(payload, null, 2) + '\n')
459
+ return { ok: true, path: runtimeStatePath(runtimeRoot, 'last-delta.json') }
459
460
  } catch (e) {
460
461
  console.error(`[sillyspec] last-delta sidecar 写入失败(advisory 将缺席,不阻断 delta 生成):${e && e.message ? e.message : e}`)
461
462
  return { ok: false, reason: e && e.message ? e.message : String(e) }
@@ -37,6 +37,7 @@
37
37
  * (R-02:上线前用 friction-ledger 历史变更回放标定再定值)。
38
38
  */
39
39
  import { RISK_TO_TIER } from './change-risk-profile.js'
40
+ import { modulePathMatches } from './modules.js'
40
41
  import { matchSpanRiskPatterns } from './span-risk-surface.js'
41
42
 
42
43
  /** 仪式档位序(只升不降的偏序基准,index 即高低:S0=0 … S3=3) */
@@ -124,7 +125,7 @@ function matchModuleForFile(posix, modulesObj) {
124
125
  for (const raw of prefixes) {
125
126
  const p = String(raw).replace(/\\/g, '/').replace(/\/+$/, '')
126
127
  if (!p) continue
127
- if (posix === p || posix.startsWith(p + '/')) return id
128
+ if (modulePathMatches(posix, p)) return id
128
129
  }
129
130
  }
130
131
  return null
@@ -12,7 +12,7 @@
12
12
  * 不因删变更丢用户唯一副本的代码;顺带清 execute/stage-review runId marker
13
13
  * + 该变更的 runtime 取证(pruneArchivedChangeRuntime))
14
14
  * - git:safeGit add -A 暂存目录删除(best-effort,对齐 archive CLI 下沉 git add)
15
- * - 平台:triggerSync 推终态(sync.js 对 status='deleted' 上行墓碑,平台收敛软删)
15
+ * - 平台:triggerSyncTerminalStrict 推终态(sync.js 对 status='deleted' 上行墓碑,平台收敛软删;文件树推送失败显性化)
16
16
  *
17
17
  * 两段式安全(对齐 --cleanup-remnant / --cleanup-ghosts / --align-execute-progress 家族):
18
18
  * 默认 dry-run 只报告将发生什么,--confirm 才执行。
@@ -23,7 +23,7 @@
23
23
  import { existsSync, readdirSync, rmSync, statSync } from 'fs';
24
24
  import { join, relative } from 'path';
25
25
  import { safeGit } from './git-helper.js';
26
- import { assertSafeChangeName, triggerSync } from './run/shared.js';
26
+ import { assertSafeChangeName, triggerSyncTerminalStrict } from './run/shared.js';
27
27
  import { archiveWorktreeCleanup } from './run/complete-handlers.js';
28
28
 
29
29
  function countFiles(dir) {
@@ -156,10 +156,11 @@ export async function deleteChange({ cwd, specDir = null, changeName, confirm =
156
156
  safeGit(cwd, ['add', '-A', '--', relDir]);
157
157
  }
158
158
  } catch { /* 非 git 仓等场景:删除在 git status 可见,暂存非必需 */ }
159
- // 5. 平台终态/墓碑上行(triggerSync 内建 8s 熔断 + 未连接静默;change-delete 是终态
160
- // 一次性命令,await 保证墓碑发出后再退出,输出顺序确定)
159
+ // 5. 平台终态/墓碑上行(终态强校验:内联有界同步,文件树推送失败显性化——🚨 告警 +
160
+ // 自救命令 + exit 1;未连接静默。change-delete 是终态一次性命令,await 保证墓碑
161
+ // 发出后再退出,输出顺序确定)
161
162
  try {
162
- await triggerSync(cwd, changeName, { specRoot: specBase });
163
+ await triggerSyncTerminalStrict(cwd, changeName, { specRoot: specBase });
163
164
  } catch (e) {
164
165
  warnings.push(`平台终态同步失败(不阻断删除,下次常规同步重试):${e.message}`);
165
166
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'
12
+ import { runtimeStatePath } from './runtime-paths.js'
12
13
  import { join, resolve, basename, dirname, relative, isAbsolute } from 'path'
13
14
  import { gitQuiet } from './git-helper.js'
14
15
  import { splitOwnVsForeignDiffFiles } from './foreign-declared.js'
@@ -440,7 +441,7 @@ function _resolveDiffFilesForParity(specBase, scanRoot, changeName, runtimeRoot)
440
441
  // 主仓 diff 空(apply 后已 commit / cleanup 完毕)→ apply-pathspec 兜底:apply 成功时落盘的
441
442
  // 本变更精确文件清单(与 gates 归因提示②同源)。仍拿不到才回 null(调用方退全仓)。
442
443
  try {
443
- const pathspecFile = join(runtimeRoot || join(specBase, '.runtime'), `apply-pathspec-${changeName}.txt`)
444
+ const pathspecFile = runtimeStatePath(runtimeRoot || join(specBase, '.runtime'), `apply-pathspec-${changeName}.txt`)
444
445
  if (existsSync(pathspecFile)) {
445
446
  const fromPathspec = readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean)
446
447
  if (fromPathspec.length > 0) {
@@ -48,9 +48,13 @@ export function crossWorktreePath(specBase, changeName, repoKey, placementRoot =
48
48
  * @param {string} mainRoot 主仓根(相对路径解析基准)
49
49
  * @returns {string} 绝对落位根
50
50
  */
51
+ // Windows 盘符形态判定(坑跨平台 CI:POSIX isAbsolute('C:/x')=false → 带引号的盘符路径
52
+ // 被相对主仓根拼接成 test/C:/quoted 伪路径)。跨仓落位本就面向 Windows 盘符场景设计,
53
+ // 语义上盘符路径恒绝对(与 win32 path.isAbsolute 同判,跨平台自洽)。
54
+ const WIN_DRIVE_ABS_RE = /^[A-Za-z]:[/\\]/;
51
55
  export function resolvePlacementRoot(raw, mainRoot) {
52
56
  const trimmed = String(raw || '').trim().replace(/^['"]|['"]$/g, '');
53
- if (isAbsolute(trimmed)) return resolvePath(trimmed);
57
+ if (isAbsolute(trimmed) || WIN_DRIVE_ABS_RE.test(trimmed)) return resolvePath(trimmed);
54
58
  return resolvePath(mainRoot, trimmed);
55
59
  }
56
60
 
package/src/db.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { openDatabase, applyPragmas, runTransaction } from './db-engine.js';
2
+ import { runtimeTempPath } from './runtime-paths.js'
2
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
3
4
  import { dirname } from 'path';
4
5
 
@@ -102,7 +103,8 @@ export class DB {
102
103
  * 全新 node:sqlite 项目不产生主 .bak)。
103
104
  */
104
105
  _openWithFallback() {
105
- const bakPath = `${this.dbPath}.bak`;
106
+ // .bak 2026-10-18 布局分层后归 temp/;根路径保留兜底(未迁移旧仓/混版本窗口)
107
+ const bakPath = existsSync(`${this.dbPath}.bak`) ? `${this.dbPath}.bak` : runtimeTempPath(dirname(this.dbPath), 'sillyspec.db.bak');
106
108
  // readValid: null=文件不存在;'empty'=存在但 0 字节;'ok'=有内容
107
109
  const readValid = (p) => {
108
110
  if (!existsSync(p)) return null;
@@ -156,7 +158,11 @@ export class DB {
156
158
  // 覆盖前救援快照(2026-08-21 审查 BUG-7):主库有内容但打不开("损坏")时,其中可能
157
159
  // 含比 .bak 更新的数据——静默覆盖即静默丢。best-effort 留一份 .corrupt-<ts> 副本供人工恢复。
158
160
  if (primaryRaw === 'ok') {
159
- try { copyFileSync(this.dbPath, `${this.dbPath}.corrupt-${Date.now()}`); } catch { /* 救援失败不阻断恢复 */ }
161
+ try {
162
+ const corruptPath = runtimeTempPath(dirname(this.dbPath), `sillyspec.db.corrupt-${Date.now()}`);
163
+ try { mkdirSync(dirname(corruptPath), { recursive: true }); } catch { /* fail-open */ }
164
+ copyFileSync(this.dbPath, corruptPath);
165
+ } catch { /* 救援失败不阻断恢复 */ }
160
166
  }
161
167
  // node:sqlite 绑定路径,无 _save:把 .bak 内容 copy 回主库,统一用 dbPath。
162
168
  // 体检 BUG-18:主库可能正被其他进程(daemon/另一 CLI)打开——Windows 覆盖他人
@@ -22,6 +22,7 @@
22
22
  * 零输出:无 decisions.md 或无入选条目 → skipped 带原因,不写任何文件、不动 INDEX。
23
23
  */
24
24
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
25
+ import { modulePathMatches } from './modules.js'
25
26
  import { basename, dirname, join } from 'node:path'
26
27
 
27
28
  /** FR-02:有实现影响的五类 type(scope 与未知 type 的 confirmed/accepted 不入选) */
@@ -36,7 +37,29 @@ const UNMAPPED_DOMAIN = 'unmapped'
36
37
  /** 旧格式容错占位值(FR-01:缺锚点/缺 headHash 不阻断) */
37
38
  const NOT_RECORDED = '未记录'
38
39
 
39
- /** 多行/带空白值压成单行字段值(条目行是单行契约) */
40
+ /**
41
+ * 锚点自动回填候选(2026-10-15-decision-anchor-backfill 单一源;收割端 flow-parity 与
42
+ * 蒸馏渲染端共用)。条目正文(答案/理由散文)中的路径形态 token——「恰一个唯一」即高置信
43
+ * 落点,返回该 token;多候选/无 token 返回 null(保守:错锚比无锚更误导 impact 查询与
44
+ * 悬空检查)。收益率探针(2026-10-15):恰一路径口径救 28%(本仓 174/616、平台 150/543)。
45
+ * 过滤黑名单(存量干跑实测坏锚类):知识治理面(.sillyspec/)、运行时面(.runtime/)、
46
+ * 归档与变更工件(changes/、allowed_paths 段)、知识互引(knowledge/、decisions/、fr/)、
47
+ * 消费仓坑文档目录(docs/sillyspec/)、前导 / 路径碎片、遥测日志扩展(.jsonl/.log)、
48
+ * repo:// 跨仓协议前缀剥除。token 须带扩展名收尾(:行号 形态锚点契约原生支持)。
49
+ * @param {string} text 条目正文拼接(答案/理由/question 等)
50
+ * @returns {string|null}
51
+ */
52
+ export function anchorBackfillCandidate(text) {
53
+ const raw = String(text || '').match(/[A-Za-z0-9_\-.\\/:]+/g) || []
54
+ const pathed = [...new Set(raw
55
+ .map((t) => t.replace(/\\/g, '/').replace(/^repo:\/\//, '').replace(/\/+$/, '').replace(/:$/, ''))
56
+ .filter((t) => t.includes('/')
57
+ && /\.[A-Za-z0-9]{1,10}(:\d+)?$/.test(t)
58
+ && !/^(\.sillyspec|\.runtime|changes|knowledge|decisions|fr|docs\/sillyspec|allowed_paths)(\/|$)/.test(t)
59
+ && !t.startsWith('/')
60
+ && !/\.(jsonl|log)$/i.test(t)))]
61
+ return pathed.length === 1 ? pathed[0] : null
62
+ }/** 多行/带空白值压成单行字段值(条目行是单行契约) */
40
63
  function oneLine(s) {
41
64
  return String(s ?? '').replace(/\s*\r?\n\s*/g, ' ').trim()
42
65
  }
@@ -274,7 +297,7 @@ function matchImpactsToModules(impacts, moduleIndex) {
274
297
  const hits = []
275
298
  for (const t of tokens) {
276
299
  for (const { id, p } of modPaths) {
277
- if (t === p || t.startsWith(p + '/')) { if (!hits.includes(id)) hits.push(id); break }
300
+ if (modulePathMatches(t, p)) { if (!hits.includes(id)) hits.push(id); break }
278
301
  }
279
302
  }
280
303
  if (hits.length === 0) {
@@ -320,7 +343,10 @@ function renderBlockLines(entry, headHash, supersedesNote, changeName, sectionNu
320
343
  // 原编号(2026-10-14-decision-global-id-epoch):epoch 形态溯源字段——变更行后/锚点行前
321
344
  // (不破「文件:紧跟锚点」行序红线);legacy 复用段(originId=null)不渲染,形态零变化
322
345
  if (originId) lines.push(`原编号:${oneLine(originId)}`)
323
- lines.push(`锚点:${oneLine(entry.anchor) || NOT_RECORDED}`)
346
+ // 锚点自动回填(2026-10-15-decision-anchor-backfill):源条目未填锚点时,从条目正文
347
+ // (答案/理由)提取路径形态 token——「恰一个唯一」即高置信落点(正文明确只谈一个文件),
348
+ // 多候选/无 token 保持未记录(保守:错锚比无锚更误导 impact 查询与悬空检查)。
349
+ lines.push(`锚点:${oneLine(entry.anchor) || anchorBackfillCandidate([entry.answer, entry.reason, entry.normalizedRequirement, entry.question].join('\n')) || NOT_RECORDED}`)
324
350
  // 文件行(FR-01/D-001@v1):仅非空渲染——存量条目零迁移,幂等重归档不添空行
325
351
  if (entry.files && entry.files.length > 0) lines.push(`文件:${entry.files.join(', ')}`)
326
352
  lines.push(`最近确认:${oneLine(headHash) || NOT_RECORDED}`)