paperlint 3.1.0 → 3.1.2

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 (311) hide show
  1. package/.github/workflows/ci.yml +28 -41
  2. package/.github/workflows/dependabot-automerge.yml +1 -1
  3. package/CLAUDE.md +21 -105
  4. package/CONTRIBUTING.md +23 -25
  5. package/README.md +11 -7
  6. package/bin/paperlint.test.mjs +45 -0
  7. package/dist/adapters/node/files.io.d.ts +2 -0
  8. package/dist/adapters/node/files.io.d.ts.map +1 -1
  9. package/dist/adapters/node/files.io.js +3 -2
  10. package/dist/adapters/node/files.io.js.map +1 -1
  11. package/dist/adapters/pdfjs/fill.d.ts +15 -4
  12. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  13. package/dist/adapters/pdfjs/fill.js +31 -21
  14. package/dist/adapters/pdfjs/fill.js.map +1 -1
  15. package/dist/adapters/references/clock.io.d.ts +5 -0
  16. package/dist/adapters/references/clock.io.d.ts.map +1 -0
  17. package/dist/adapters/references/clock.io.js +7 -0
  18. package/dist/adapters/references/clock.io.js.map +1 -0
  19. package/dist/adapters/references/index.d.ts +24 -2
  20. package/dist/adapters/references/index.d.ts.map +1 -1
  21. package/dist/adapters/references/index.js +131 -12
  22. package/dist/adapters/references/index.js.map +1 -1
  23. package/dist/adapters/references/pool.d.ts +8 -0
  24. package/dist/adapters/references/pool.d.ts.map +1 -0
  25. package/dist/adapters/references/pool.js +19 -0
  26. package/dist/adapters/references/pool.js.map +1 -0
  27. package/dist/build.d.ts +14 -2
  28. package/dist/build.d.ts.map +1 -1
  29. package/dist/build.js +17 -11
  30. package/dist/build.js.map +1 -1
  31. package/dist/cli.d.ts +27 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +71 -55
  34. package/dist/cli.js.map +1 -1
  35. package/dist/doctor.d.ts +27 -1
  36. package/dist/doctor.d.ts.map +1 -1
  37. package/dist/doctor.js +4 -3
  38. package/dist/doctor.js.map +1 -1
  39. package/dist/domain/lookup-cache.d.ts +75 -0
  40. package/dist/domain/lookup-cache.d.ts.map +1 -0
  41. package/dist/domain/lookup-cache.js +137 -0
  42. package/dist/domain/lookup-cache.js.map +1 -0
  43. package/dist/domain/result.d.ts +0 -2
  44. package/dist/domain/result.d.ts.map +1 -1
  45. package/dist/domain/result.js +0 -4
  46. package/dist/domain/result.js.map +1 -1
  47. package/dist/hooks-settings.d.ts +8 -5
  48. package/dist/hooks-settings.d.ts.map +1 -1
  49. package/dist/hooks-settings.js +32 -32
  50. package/dist/hooks-settings.js.map +1 -1
  51. package/dist/init.d.ts +8 -0
  52. package/dist/init.d.ts.map +1 -1
  53. package/dist/init.js +3 -3
  54. package/dist/init.js.map +1 -1
  55. package/dist/latex-log.d.ts +0 -2
  56. package/dist/latex-log.d.ts.map +1 -1
  57. package/dist/latex-log.js +7 -13
  58. package/dist/latex-log.js.map +1 -1
  59. package/dist/link-skills.d.ts +15 -2
  60. package/dist/link-skills.d.ts.map +1 -1
  61. package/dist/link-skills.js +36 -20
  62. package/dist/link-skills.js.map +1 -1
  63. package/dist/paper-files.d.ts.map +1 -1
  64. package/dist/paper-files.js +6 -4
  65. package/dist/paper-files.js.map +1 -1
  66. package/dist/pdf-facts.d.ts +11 -0
  67. package/dist/pdf-facts.d.ts.map +1 -1
  68. package/dist/pdf-facts.js +16 -18
  69. package/dist/pdf-facts.js.map +1 -1
  70. package/dist/pdf-geometry.js +1 -1
  71. package/dist/pdf-geometry.js.map +1 -1
  72. package/dist/ports/check-references.d.ts +12 -2
  73. package/dist/ports/check-references.d.ts.map +1 -1
  74. package/dist/ports/check-references.js +0 -5
  75. package/dist/ports/check-references.js.map +1 -1
  76. package/dist/presets.d.ts +2 -0
  77. package/dist/presets.d.ts.map +1 -1
  78. package/dist/presets.js +3 -1
  79. package/dist/presets.js.map +1 -1
  80. package/dist/references.d.ts +11 -3
  81. package/dist/references.d.ts.map +1 -1
  82. package/dist/references.js +63 -15
  83. package/dist/references.js.map +1 -1
  84. package/dist/tex-requirements.d.ts +8 -0
  85. package/dist/tex-requirements.d.ts.map +1 -1
  86. package/dist/tex-requirements.js +6 -5
  87. package/dist/tex-requirements.js.map +1 -1
  88. package/dist/toolchain.d.ts +3 -2
  89. package/dist/toolchain.d.ts.map +1 -1
  90. package/dist/toolchain.js +4 -5
  91. package/dist/toolchain.js.map +1 -1
  92. package/dist/venue-rules.d.ts.map +1 -1
  93. package/dist/venue-rules.js +11 -9
  94. package/dist/venue-rules.js.map +1 -1
  95. package/docs/prior-art/README.md +11 -10
  96. package/docs/prior-art/citation-checkers.md +174 -0
  97. package/docs/prior-art/test-tooling.md +7 -30
  98. package/docs/references.md +126 -0
  99. package/docs/testing.md +118 -0
  100. package/docs/toolchain.md +6 -4
  101. package/eslint-rules/install-path-literals.harness.mjs +17 -1
  102. package/eslint-rules/latex-language.harness.mjs +3 -3
  103. package/eslint-rules/latex-language.mjs +12 -9
  104. package/eslint-rules/latex-language.test.mjs +200 -0
  105. package/eslint-rules/paper-research-question.harness.mjs +73 -7
  106. package/eslint-rules/paper-stages.harness.mjs +9 -14
  107. package/eslint-rules/paper-stages.test.mjs +135 -0
  108. package/eslint-rules/paper-typography.mjs +9 -2
  109. package/eslint-rules/paper-typography.test.mjs +146 -1
  110. package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
  111. package/eslint-rules/review-frontmatter.mjs +2 -1
  112. package/eslint-rules/review-frontmatter.test.mjs +37 -1
  113. package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
  114. package/eslint-rules/tex-build.mjs +13 -13
  115. package/eslint.config.mjs +6 -2
  116. package/hooks/hooks.harness.mjs +6 -11
  117. package/hooks/paper-edit-guard.hook.mjs +2 -2
  118. package/hooks/paper-edit-guard.test.mjs +61 -0
  119. package/hooks/paper-skills-nudge.hook.mjs +2 -2
  120. package/hooks/paper-skills-nudge.test.mjs +38 -0
  121. package/hooks/paper-status-gates.test.mjs +42 -0
  122. package/lib/agent-cli-version.harness.mjs +7 -0
  123. package/lib/check.d.mts +17 -0
  124. package/lib/check.mjs +50 -0
  125. package/lib/check.test.ts +67 -0
  126. package/lib/markdown.mjs +16 -69
  127. package/lib/markdown.test.mjs +246 -0
  128. package/lib/paper-config.harness.mjs +28 -9
  129. package/lib/paper-config.mjs +7 -2
  130. package/lib/skill-checks.mjs +18 -46
  131. package/lib/skill-checks.test.mjs +297 -0
  132. package/lib/skill-corpus.mjs +11 -13
  133. package/lib/skill-corpus.test.mjs +54 -0
  134. package/lib/skill-trigger-cases.harness.mjs +57 -4
  135. package/package.json +6 -5
  136. package/scripts/check.harness.mjs +4 -10
  137. package/scripts/check.mjs +45 -42
  138. package/scripts/check.test.mjs +104 -0
  139. package/scripts/coverage-config.test.ts +406 -0
  140. package/scripts/eslint-report-guard.test.mjs +48 -0
  141. package/scripts/harness-api.frozen.json +0 -2
  142. package/scripts/layer-legacy-frozen.d.mts +9 -1
  143. package/scripts/layer-legacy-frozen.mjs +28 -11
  144. package/scripts/layer-legacy-frozen.test.ts +143 -1
  145. package/scripts/rules-are-content-only.harness.mjs +3 -11
  146. package/scripts/rules-are-content-only.mjs +29 -19
  147. package/scripts/rules-are-content-only.test.mjs +61 -0
  148. package/scripts/rules-see-files.mjs +11 -9
  149. package/scripts/rules-see-files.test.mjs +71 -0
  150. package/scripts/test.mjs +44 -0
  151. package/scripts/test.test.mjs +52 -0
  152. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
  153. package/skills/grade-paper-writing/prose-lint.mjs +31 -12
  154. package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
  155. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
  156. package/skills/paper-pipeline/references/artifact-checklist.md +7 -7
  157. package/skills/paper-pipeline/scripts/README.md +13 -132
  158. package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
  159. package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
  160. package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
  161. package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
  162. package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
  163. package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
  164. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
  165. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
  166. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
  167. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
  168. package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
  169. package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
  170. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
  171. package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
  172. package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
  173. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
  174. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
  175. package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
  176. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
  177. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
  178. package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
  179. package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
  180. package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
  181. package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
  182. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
  183. package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
  184. package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
  185. package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
  186. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
  187. package/skills/paper-pipeline/scripts/status.mjs +2 -2
  188. package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
  189. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
  190. package/skills/render-paper/checkers-requirements.txt +30 -19
  191. package/skills/render-paper/ensure-checkers.sh +20 -4
  192. package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
  193. package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
  194. package/skills/tighten-paper/structure.test.mjs +116 -0
  195. package/skills/verify-citations/SKILL.md +30 -14
  196. package/skills/verify-citations/SKILL.md.spec.ts +29 -13
  197. package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +253 -0
  198. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
  199. package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
  200. package/skills/verify-citations/scripts/bib-authors.mjs +6 -6
  201. package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
  202. package/skills/verify-citations/scripts/verify-cites.mjs +360 -215
  203. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +588 -0
  204. package/skills/verify-citations/scripts/verify-cites.test.mjs +151 -5
  205. package/src/adapters/banal/failure.test.ts +70 -1
  206. package/src/adapters/banal/index.test.ts +16 -0
  207. package/src/adapters/banal/output.test.ts +5 -0
  208. package/src/adapters/curl/download.test.ts +43 -1
  209. package/src/adapters/memory/index.test.ts +25 -0
  210. package/src/adapters/node/files.io.ts +3 -2
  211. package/src/adapters/node/files.test.ts +13 -1
  212. package/src/adapters/node/process.test.ts +38 -0
  213. package/src/adapters/pdfjs/fill.test.ts +34 -0
  214. package/src/adapters/pdfjs/fill.ts +44 -25
  215. package/src/adapters/references/clock.io.ts +8 -0
  216. package/src/adapters/references/index.test.ts +560 -0
  217. package/src/adapters/references/index.ts +203 -19
  218. package/src/adapters/references/pool.test.ts +31 -0
  219. package/src/adapters/references/pool.ts +24 -0
  220. package/src/build-engine.harness.mjs +3 -7
  221. package/src/build-engine.test.ts +137 -0
  222. package/src/build.harness.mjs +5 -9
  223. package/src/build.test.ts +362 -0
  224. package/src/build.ts +23 -12
  225. package/src/cli.harness.mjs +10 -17
  226. package/src/cli.test.ts +584 -0
  227. package/src/cli.ts +112 -66
  228. package/src/doctor.harness.mjs +3 -8
  229. package/src/doctor.test.ts +64 -0
  230. package/src/doctor.ts +12 -4
  231. package/src/domain/lookup-cache.test.ts +178 -0
  232. package/src/domain/lookup-cache.ts +202 -0
  233. package/src/domain/result.test.ts +4 -0
  234. package/src/domain/result.ts +0 -5
  235. package/src/engine.harness.mjs +27 -7
  236. package/src/facts-file.test.ts +110 -0
  237. package/src/hooks-settings.harness.mjs +43 -13
  238. package/src/hooks-settings.ts +41 -35
  239. package/src/init.test.ts +308 -5
  240. package/src/init.ts +3 -3
  241. package/src/latex-log.harness.mjs +32 -7
  242. package/src/latex-log.ts +6 -13
  243. package/src/latex-loop.harness.mjs +8 -7
  244. package/src/link-skills.harness.mjs +3 -8
  245. package/src/link-skills.test.ts +105 -0
  246. package/src/link-skills.ts +75 -21
  247. package/src/new-paper.harness.mjs +3 -7
  248. package/src/new-paper.test.ts +31 -1
  249. package/src/paper-files.test.ts +12 -0
  250. package/src/paper-files.ts +9 -5
  251. package/src/paper-settings.test.ts +20 -0
  252. package/src/pdf-facts.harness.mjs +3 -6
  253. package/src/pdf-facts.test.ts +144 -0
  254. package/src/pdf-facts.ts +17 -19
  255. package/src/pdf-geometry.harness.mjs +3 -7
  256. package/src/pdf-geometry.ts +1 -1
  257. package/src/ports/check-references.ts +16 -2
  258. package/src/presets.test.ts +2 -0
  259. package/src/presets.ts +7 -1
  260. package/src/references.test.ts +257 -10
  261. package/src/references.ts +93 -17
  262. package/src/rules-config.harness.mjs +10 -8
  263. package/src/structure.harness.mjs +3 -7
  264. package/src/tex-requirements.harness.mjs +34 -7
  265. package/src/tex-requirements.ts +12 -6
  266. package/src/toolchain.harness.mjs +3 -7
  267. package/src/toolchain.test.ts +280 -0
  268. package/src/toolchain.ts +8 -7
  269. package/src/venue-rules.test.ts +60 -0
  270. package/src/venue-rules.ts +13 -11
  271. package/action.mutations.mjs +0 -162
  272. package/eslint-rules/install-path-literals.mutations.mjs +0 -62
  273. package/eslint-rules/latex-language.mutations.mjs +0 -196
  274. package/eslint-rules/paper-research-question.mutations.mjs +0 -127
  275. package/eslint-rules/paper-stages.mutations.mjs +0 -81
  276. package/eslint-rules/papers.mutations.mjs +0 -186
  277. package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
  278. package/eslint-rules/tex-build.mutations.mjs +0 -258
  279. package/hooks/hooks.mutations.mjs +0 -337
  280. package/lib/agent-cli-version.mutations.mjs +0 -109
  281. package/lib/mutation-driver.harness.mjs +0 -227
  282. package/lib/mutation-driver.mjs +0 -397
  283. package/lib/mutation-driver.mutations.mjs +0 -68
  284. package/lib/paper-config.mutations.mjs +0 -143
  285. package/lib/skill-trigger-cases.mutations.mjs +0 -65
  286. package/scripts/check.mutations.mjs +0 -110
  287. package/scripts/exclusive.mjs +0 -138
  288. package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
  289. package/scripts/mutation-batteries-frozen.mjs +0 -238
  290. package/scripts/mutation-batteries.frozen.json +0 -108
  291. package/scripts/rules-are-content-only.mutations.mjs +0 -81
  292. package/scripts/rules-see-files.mutations.mjs +0 -131
  293. package/scripts/run-mutations.mjs +0 -100
  294. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
  295. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
  296. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
  297. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
  298. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
  299. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
  300. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
  301. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
  302. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
  303. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
  304. package/skills/skill-contract.mutations.mjs +0 -179
  305. package/src/build.mutations.mjs +0 -196
  306. package/src/cli.mutations.mjs +0 -414
  307. package/src/doctor.mutations.mjs +0 -175
  308. package/src/hooks-settings.mutations.mjs +0 -99
  309. package/src/link-skills.mutations.mjs +0 -140
  310. package/src/new-paper.mutations.mjs +0 -65
  311. package/src/structure.mutations.mjs +0 -83
@@ -0,0 +1,71 @@
1
+ /**
2
+ * A PIPELINE-STATUS.md fixture shared by the pipeline-check harness and its CLI test: the four
3
+ * template sections, every row green and dated `today`, so a test plants exactly one defect.
4
+ */
5
+ export const FAR = "2027-12-31";
6
+
7
+ /**
8
+ * A scorecard with the four sections the template requires, all green. `over` replaces individual
9
+ * cells: `{ cites: { date: "2020-01-01" } }`, `{ verdict: "…" }`, `{ deadline: "2026-08-18" }`,
10
+ * `{ tail: "…" }` for anything appended after the last section, `{ preamble: "…" }` for anything
11
+ * inserted BEFORE the first heading (the only way to plant a row under no heading at all).
12
+ */
13
+ export function scorecard(
14
+ over = {},
15
+ today = new Date().toISOString().slice(0, 10),
16
+ ) {
17
+ const r = (id, status = "☑", date = today) => ({
18
+ id,
19
+ status,
20
+ date,
21
+ ...(over[id] ?? {}),
22
+ });
23
+ const row = (o, ...mid) =>
24
+ `| ${o.id} | work | ${o.skill ?? "skill"} | ${mid.length ? mid.join(" | ") + " | " : ""}${o.status} | ${o.date} | ${o.result ?? "result"} | — |`;
25
+ // The GATES `Requires` cell, canonical by default so the baseline is silent — `{ structure: {
26
+ // requires: "render" } }` plants a dropped edge. `harden` deliberately declares four of its five
27
+ // canonical inputs: the fifth (`priordelta`) has no row in this fixture, and the clean case asserts that the checker stays
28
+ // quiet about an edge there is nowhere to point at.
29
+ const req = (id, dflt) => over[id]?.requires ?? dflt;
30
+ const verdict =
31
+ over.verdict ??
32
+ "Submit-ready — every gate green, artifact reproduces clean, nothing blocking.";
33
+ return [
34
+ "# PIPELINE-STATUS — harness fixture",
35
+ `Venue: Fixture Workshop · Deadline: ${over.deadline ?? FAR} · Blind: double · State: drafting`,
36
+ "",
37
+ `**Readiness verdict:** ${verdict}`,
38
+ "",
39
+ over.preamble ?? "",
40
+ "",
41
+ "### SETUP",
42
+ "| id | Work | Skill | Status | Date | Result | Open |",
43
+ "|----|------|-------|--------|------|--------|------|",
44
+ row(r("idea")),
45
+ row(r("access")),
46
+ row(r("frame")),
47
+ "",
48
+ "### LOOP",
49
+ "| id | Work | Skill | Status | Date | Result | Open |",
50
+ "|----|------|-------|--------|------|--------|------|",
51
+ row(r("study")),
52
+ row(r("draft")),
53
+ row(r("arc")),
54
+ "",
55
+ "### CONTINUOUS",
56
+ "| id | Trigger | Skill | Status | Date | Result | Open |",
57
+ "|----|---------|-------|--------|------|--------|------|",
58
+ row(r("cites")),
59
+ row(r("render")),
60
+ "",
61
+ "### GATES",
62
+ "| id | Gate | Skill | Requires | Status | Date | Result | Open |",
63
+ "|----|------|-------|----------|--------|------|--------|------|",
64
+ row(r("structure"), req("structure", "render, arc")),
65
+ row(r("writing"), req("writing", "draft, arc, structure")),
66
+ row(r("panel"), req("panel", "structure, writing")),
67
+ row(r("harden"), req("harden", "panel, structure, writing, cites")),
68
+ "",
69
+ over.tail ?? "",
70
+ ].join("\n");
71
+ }
@@ -114,7 +114,7 @@ function fixture({ scripts = {}, allow = null, bundle = {}, at = "repro" }) {
114
114
  "explicit",
115
115
  "import random\nrandom.seed(7)\nrows = random.sample(all_rows, 30)\n",
116
116
  ],
117
- // 🔴 THIS FIXTURE WAS VACUOUS IN ITS FIRST FORM and the mutations file caught it. It drew with
117
+ // 🔴 THIS FIXTURE WAS VACUOUS IN ITS FIRST FORM. It drew with
118
118
  // `rnd.sample(...)` off a local `random.Random(...)`, which the DRAWS pattern does not recognise
119
119
  // as a draw at all — so the case passed because NOTHING was detected, not because the seed was.
120
120
  // Neutering the `--seed` alternation changed no verdict. It now draws with `np.random.choice`,
@@ -243,9 +243,9 @@ function fixture({ scripts = {}, allow = null, bundle = {}, at = "repro" }) {
243
243
  // ever fails, one of the two checks should be deleted rather than both kept.
244
244
  {
245
245
  // Silence asserted through --flags-only rather than through the header's count, so that this case
246
- // tests the EXCLUSION and not the count. Sharing the count assertion with case 11 made a mutation
247
- // of the header land here instead, which reads as an off-target kill and hides which property
248
- // actually failed.
246
+ // tests the EXCLUSION and not the count. Sharing the count assertion with case 11 meant a defect
247
+ // in the header would be caught here instead, which reads as an off-target result and hides
248
+ // which property actually failed.
249
249
  const r = run(
250
250
  fixture({ bundle: { "leak.py": 'open("/home/example/secret.tsv")\n' } }),
251
251
  false,
@@ -252,8 +252,9 @@ for (const f of files.sort()) {
252
252
 
253
253
  // 🔴 `ABS.flags`, not a literal 'g'. The first version passed 'g' alone, which silently DROPPED
254
254
  // the case-sensitivity that keeps an Express route `/users/export` from matching `/Users/` —
255
- // so the property the harness asserts lived nowhere the harness could reach it. Found by the
256
- // mutations file: adding an `i` to ABS changed no verdict, i.e. a mutation that did not mutate.
255
+ // so the property the harness asserts lived nowhere the harness could reach it. Found by
256
+ // testing directly: adding an `i` to ABS changed no verdict, i.e. the property was
257
+ // unreachable.
257
258
  const abs = code.match(
258
259
  new RegExp(ABS.source + "[^\\s'\"`)\\],]*", ABS.flags + "g"),
259
260
  );
@@ -0,0 +1,62 @@
1
+ /**
2
+ * generated-code.mjs on the inputs its harness does not build: no argument, a directory that does
3
+ * not exist, and a repro/ holding a dangling link, dot/dependency directories, trees deeper than
4
+ * the scan's depth (more than eight, so the list is elided), and an allow row with no reason.
5
+ */
6
+ import assert from "node:assert/strict";
7
+ import { symlinkSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { test } from "vitest";
11
+ import { runNode, useTempDir, writeTree } from "../../../test/support.mjs";
12
+
13
+ const SCRIPT = join(
14
+ dirname(fileURLToPath(import.meta.url)),
15
+ "generated-code.mjs",
16
+ );
17
+ const root = useTempDir("generated-code-");
18
+ const run = (...args) => runNode(SCRIPT, args);
19
+
20
+ test("no argument prints the usage; a directory that does not exist scanned nothing, and says so", () => {
21
+ assert.deepEqual(run(), {
22
+ status: 0,
23
+ stdout: "",
24
+ stderr: "usage: node generated-code.mjs <paper-dir> [--flags-only]\n",
25
+ });
26
+ const r = run(join(root, "nowhere"));
27
+ assert.equal(r.status, 0);
28
+ assert.match(
29
+ r.stdout,
30
+ /NOTHING WAS SCANNED: no repro\/ here or one level below/,
31
+ );
32
+ });
33
+
34
+ test("repro/: a dangling link is passed over; skipped trees are named, more than eight elided; a reasonless allow row says so", () => {
35
+ const deep = Object.fromEntries(
36
+ Array.from({ length: 10 }, (_, i) => [
37
+ `repro/exp/d${String(i)}/deeper/x.py`,
38
+ "print(1)\n",
39
+ ]),
40
+ );
41
+ const dir = writeTree(join(root, "paper"), {
42
+ ...deep,
43
+ "repro/.cache/x.py": "",
44
+ // eslint-disable-next-line port/js-install-path -- a fixture directory the scan must skip, not an install location
45
+ "repro/node_modules/x.js": "",
46
+ "repro/__pycache__/x.py": "",
47
+ // Draws randomness unseeded: a NO_SEED finding, which the reasonless row below waves through.
48
+ "repro/exp/run.py": "import random\nprint(random.random())\n",
49
+ "repro/generated-code-grandfathered.txt": "exp/run.py\t*\n",
50
+ });
51
+ symlinkSync(join(root, "gone"), join(dir, "repro", "dangling.py"));
52
+ const out = run(dir).stdout;
53
+ assert.match(
54
+ out,
55
+ /• 3 tree\(s\) not entered — dependency, VCS and build trees[^\n]*\n {7}\.cache, __pycache__, node_modules/,
56
+ );
57
+ assert.match(
58
+ out,
59
+ /• 10 tree\(s\) not entered — nested deeper than 3 levels[^\n]*\n {7}(?:exp\/d\d\/deeper, ){7}exp\/d\d\/deeper … and 2 more/,
60
+ );
61
+ assert.match(out, / {7}exp\/run\.py · NO_SEED {2}→ {2}\(NO REASON GIVEN\)/);
62
+ });
@@ -0,0 +1,302 @@
1
+ /**
2
+ * ledger.mjs — the `record`/`status` CLI every wired skill ends with, the refusals `record` makes,
3
+ * the evidence contract on a FINDING, and how rows from the retired vocabulary and rows out of
4
+ * time order are read. The ledger and the consumer are temp paths, set before import.
5
+ *
6
+ * Refusal messages are several lines of explanation; where the wording is not the subject, the
7
+ * FIRST line (which names the defect) and the exit code are compared.
8
+ */
9
+ import assert from "node:assert/strict";
10
+ import { cpSync, rmSync, writeFileSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { afterEach, test } from "vitest";
14
+ import { runNode, useTempDir, writeTree } from "../../../test/support.mjs";
15
+
16
+ const HERE = dirname(fileURLToPath(import.meta.url));
17
+ const SCRIPT = join(HERE, "ledger.mjs");
18
+ const root = useTempDir("ledger-cli-");
19
+ const consumer = join(root, "consumer");
20
+ const LEDGER = join(root, "runs.jsonl");
21
+ const paper = join(consumer, "papers", "p");
22
+ writeTree(paper, {
23
+ "paper.md": "# P\n\ntext\n",
24
+ "counted.md": "---\nfindings: 2\n---\n",
25
+ "uncounted.md": "no frontmatter count\n",
26
+ });
27
+ cpSync(
28
+ join(HERE, "..", "..", "cold-read-diff"),
29
+ join(consumer, ".claude", "skills", "cold-read-diff"),
30
+ {
31
+ recursive: true,
32
+ verbatimSymlinks: true,
33
+ },
34
+ );
35
+ const env = { CLAUDE_PROJECT_DIR: consumer, PIPELINE_LEDGER: LEDGER };
36
+ Object.assign(process.env, env);
37
+ const { readLedger, record, status } = await import("./ledger.mjs");
38
+ afterEach(() => rmSync(LEDGER, { force: true }));
39
+
40
+ const cli = (...args) => runNode(SCRIPT, args, { env });
41
+ const firstLine = (s) => s.trim().split("\n")[0];
42
+ const kinds = () =>
43
+ readLedger().map((r) => `${r.kind} ${r.findings} ${r.blocking}`);
44
+
45
+ test("record FINDING with a report that states the same count: one row, printed, exit 0", () => {
46
+ const r = cli(
47
+ "record",
48
+ "cold-read-diff",
49
+ paper,
50
+ "FINDING",
51
+ "2",
52
+ "counted.md",
53
+ "--blocking",
54
+ "--check=sub",
55
+ );
56
+ const row = JSON.parse(r.stdout);
57
+ assert.deepEqual(
58
+ { status: r.status, stderr: r.stderr, check: row.check, kinds: kinds() },
59
+ { status: 0, stderr: "", check: "sub", kinds: ["FINDING 2 true"] },
60
+ );
61
+ });
62
+
63
+ test("the evidence contract: no path, a path to nothing, a disagreeing count — recorded, then exit 1", () => {
64
+ const cases = [
65
+ ["FINDING", "2"],
66
+ ["FINDING", "2", "missing.md"],
67
+ ["FINDING", "3", "counted.md"],
68
+ ].map((args) => {
69
+ const r = cli("record", "cold-read-diff", paper, ...args);
70
+ return [r.status, firstLine(r.stderr)];
71
+ });
72
+ assert.deepEqual(
73
+ { cases, kinds: kinds() },
74
+ {
75
+ cases: [
76
+ [1, "🔴 cold-read-diff: recorded 2 finding(s) with NO report path."],
77
+ [
78
+ 1,
79
+ '🔴 cold-read-diff: recorded report path "missing.md" — nothing is there.',
80
+ ],
81
+ [
82
+ 1,
83
+ "🔴 cold-read-diff: the ledger says 3 finding(s), counted.md says 2.",
84
+ ],
85
+ ],
86
+ kinds: ["FINDING 2 false", "FINDING 2 false", "FINDING 3 false"],
87
+ },
88
+ );
89
+ });
90
+
91
+ test("a report that states no count is only a nudge: recorded, exit 0", () => {
92
+ const r = cli(
93
+ "record",
94
+ "cold-read-diff",
95
+ paper,
96
+ "FINDING",
97
+ "1",
98
+ "uncounted.md",
99
+ );
100
+ assert.deepEqual(
101
+ { status: r.status, nudge: firstLine(r.stderr) },
102
+ {
103
+ status: 0,
104
+ nudge:
105
+ "⚠️ cold-read-diff: uncounted.md does not state its own finding count.",
106
+ },
107
+ );
108
+ });
109
+
110
+ test("refusals before writing: a retired kind, an unknown kind, a free-text reason, a finding of zero", () => {
111
+ const cases = [
112
+ ["PASS"],
113
+ ["probably fine"],
114
+ ["ABSTAINED", "looked fine"],
115
+ ["FINDING", "0", "counted.md"],
116
+ ["ABSTAINED", "no-witness", "a note"],
117
+ ].map((args) => {
118
+ const r = cli("record", "cold-read-diff", paper, ...args);
119
+ return [
120
+ r.status,
121
+ r.status === 0 ? JSON.parse(r.stdout).reason : firstLine(r.stderr),
122
+ ];
123
+ });
124
+ assert.deepEqual(
125
+ { cases, kinds: kinds() },
126
+ {
127
+ cases: [
128
+ [
129
+ 2,
130
+ '🔴 "PASS" is not a verdict any more — deleted: it stored "nothing was wrong" as a value. Record ABSTAINED `no-witness`, and let the reader derive cleanliness from the absence of findings.',
131
+ ],
132
+ [2, '🔴 kind must be one of FINDING|ABSTAINED, got "probably fine"'],
133
+ [
134
+ 2,
135
+ '🔴 ABSTAINED needs a reason from started|no-witness|input-missing|blocked|crashed, got "looked fine".',
136
+ ],
137
+ [
138
+ 2,
139
+ "🔴 FINDING with findings=0. A finding of nothing is an abstention:",
140
+ ],
141
+ [0, "no-witness"],
142
+ ],
143
+ // Only the valid abstention was written.
144
+ kinds: ["ABSTAINED 0 false"],
145
+ },
146
+ );
147
+ });
148
+
149
+ test("status prints the derived rows; no command prints the usage and exits 2", () => {
150
+ cli("record", "cold-read-diff", paper, "ABSTAINED", "no-witness");
151
+ const s = cli("status", paper);
152
+ const u = cli();
153
+ assert.deepEqual(
154
+ {
155
+ status: s.status,
156
+ rows: JSON.parse(s.stdout).map((r) => [r.key, r.state, r.runs]),
157
+ usage: [u.status, firstLine(u.stderr)],
158
+ },
159
+ {
160
+ status: 0,
161
+ rows: [["cold-read-diff", "FRESH", 1]],
162
+ usage: [
163
+ 2,
164
+ "usage: ledger.mjs record <skill> <paperDir> FINDING <count> <report-path> [--check=<id>] [--blocking]",
165
+ ],
166
+ },
167
+ );
168
+ });
169
+
170
+ test("status with no paper argument reads the current directory", () => {
171
+ const r = runNode(SCRIPT, ["status"], { env, cwd: paper });
172
+ assert.deepEqual([r.status, JSON.parse(r.stdout)], [0, []]);
173
+ });
174
+
175
+ test("retired rows are read as LEGACY, keep their word, and only FINDINGS/FAIL carry a count", () => {
176
+ const legacy = (verdict, extra = {}) =>
177
+ JSON.stringify({
178
+ ts: "2026-08-01T00:00:00Z",
179
+ skill: "old-skill",
180
+ paper: "p",
181
+ verdict,
182
+ ...extra,
183
+ });
184
+ writeFileSync(
185
+ LEDGER,
186
+ [
187
+ legacy("PASS"),
188
+ legacy("FINDINGS", { findings: "4" }),
189
+ legacy("FAIL"),
190
+ legacy(undefined),
191
+ "not json at all",
192
+ "",
193
+ ].join("\n"),
194
+ );
195
+ assert.deepEqual(
196
+ readLedger().map((r) => [
197
+ r.kind,
198
+ r.check,
199
+ r.legacyVerdict,
200
+ r.findings,
201
+ r.blocking,
202
+ ]),
203
+ [
204
+ ["LEGACY", "old-skill", "PASS", 0, false],
205
+ ["LEGACY", "old-skill", "FINDINGS", 4, false],
206
+ ["LEGACY", "old-skill", "FAIL", 0, true],
207
+ ["LEGACY", "old-skill", null, 0, false],
208
+ ],
209
+ );
210
+ });
211
+
212
+ test("the latest run is chosen by timestamp; a stamped row beats unstamped ones; unstamped fall back to file order", () => {
213
+ const row = (ts, findings) =>
214
+ JSON.stringify({
215
+ ...(ts ? { ts } : {}),
216
+ skill: "cold-read-diff",
217
+ paper: "p",
218
+ kind: findings ? "FINDING" : "ABSTAINED",
219
+ findings,
220
+ reason: findings ? null : "no-witness",
221
+ report: findings ? "counted.md" : null,
222
+ });
223
+ const latest = (...lines) => {
224
+ writeFileSync(LEDGER, lines.join("\n") + "\n");
225
+ return status(paper)[0].findings;
226
+ };
227
+ assert.deepEqual(
228
+ [
229
+ latest(row("2026-09-02T00:00:00Z", 2), row("2026-09-01T00:00:00Z", 0)),
230
+ latest(row(null, 0), row("2026-09-01T00:00:00Z", 2), row(null, 0)),
231
+ latest(row(null, 2), row(null, 0)),
232
+ latest(row(null, 0), row(null, 2)),
233
+ ],
234
+ [2, 2, 0, 2],
235
+ );
236
+ });
237
+
238
+ test("record straight from code returns the row it wrote", () => {
239
+ const row = record({
240
+ skill: "cold-read-diff",
241
+ paper,
242
+ kind: "ABSTAINED",
243
+ reason: "blocked",
244
+ });
245
+ assert.deepEqual(
246
+ [row.kind, row.reason, row.check, row.paper, readLedger().length],
247
+ ["ABSTAINED", "blocked", "cold-read-diff", "p", 1],
248
+ );
249
+ });
250
+
251
+ test("status over hand-edited and retired rows: a bare FINDING, and a legacy FINDINGS that still counts", () => {
252
+ writeFileSync(
253
+ LEDGER,
254
+ [
255
+ {
256
+ ts: "2026-09-01T00:00:00Z",
257
+ skill: "hand",
258
+ paper: "p",
259
+ kind: "FINDING",
260
+ },
261
+ {
262
+ ts: "2026-08-01T00:00:00Z",
263
+ skill: "old-skill",
264
+ paper: "p",
265
+ verdict: "FINDINGS",
266
+ findings: 4,
267
+ report: "r.md",
268
+ },
269
+ {
270
+ ts: "2026-08-01T00:00:00Z",
271
+ skill: "failed-skill",
272
+ paper: "p",
273
+ verdict: "FAIL",
274
+ findings: 1,
275
+ },
276
+ ]
277
+ .map((r) => JSON.stringify(r))
278
+ .join("\n") + "\n",
279
+ );
280
+ assert.deepEqual(
281
+ status(paper, { gates: ["hand", "old-skill", "failed-skill"] }).map((r) => [
282
+ r.key,
283
+ r.findings,
284
+ r.report,
285
+ r.legacy,
286
+ r.everFound,
287
+ ]),
288
+ [
289
+ ["hand", 0, null, null, true],
290
+ ["old-skill", 4, "r.md", "FINDINGS", true],
291
+ ["failed-skill", 1, null, "FAIL", true],
292
+ ],
293
+ );
294
+ });
295
+
296
+ test("record FINDING with no count at all is a finding of zero, refused", () => {
297
+ const r = cli("record", "cold-read-diff", paper, "FINDING");
298
+ assert.deepEqual(
299
+ [r.status, firstLine(r.stderr)],
300
+ [2, "🔴 FINDING with findings=0. A finding of nothing is an abstention:"],
301
+ );
302
+ });
@@ -120,7 +120,7 @@ export const LEDGER = ledgerPath(HERE);
120
120
  // `.claude/skills/paper-pipeline/scripts/` that resolves to `…/paper-pipeline/skills`, which does
121
121
  // not exist. And the failure is SILENT in the worst direction — `skillHash()` over a missing
122
122
  // directory is stable, so every recorded verdict reads FRESH forever and staleness detection is
123
- // dead without a single error. Caught only because `ledger.selftest.mjs` asserts the transition.
123
+ // dead without a single error. Caught only because the ledger self-test (now `ledger.test.mjs`) asserts the transition.
124
124
  //
125
125
  // 🔴 AND IT BROKE A SECOND TIME, THE SAME WAY, ON 2026-09-12 — which is why it is no longer a
126
126
  // walk at all. The skills being hashed are the CONSUMER's: `skillHash()` answers "has the checker
@@ -1,54 +1,52 @@
1
- // ledger.selftest.mjs — plant each failure this thing exists to catch, and watch it catch them.
2
- //
3
- // The project's rule, written after five tools reported success while doing nothing:
4
- // "a checker that has never failed does not count as working". So this file does not check that
5
- // the ledger runs. It checks that it says NO when it should, and YES when it should — both
6
- // directions, for every property the design claims.
7
- //
8
- // node .claude/skills/paper-pipeline/scripts/ledger.selftest.mjs
9
-
1
+ /**
2
+ * The ledger says NO when it should and YES when it should — both directions, for every property
3
+ * the design claims (formerly `ledger.selftest.mjs`, which nothing ran).
4
+ *
5
+ * Everything lives under one temp directory: the ledger file (PIPELINE_LEDGER) and a consumer root
6
+ * (CLAUDE_PROJECT_DIR) holding a copy of the one skill whose hash is watched. Both are set BEFORE
7
+ * `ledger.mjs` is imported, because it resolves them at import time.
8
+ */
9
+ import assert from "node:assert/strict";
10
10
  import {
11
+ appendFileSync,
12
+ cpSync,
13
+ mkdirSync,
11
14
  mkdtempSync,
12
- writeFileSync,
13
- rmSync,
14
15
  readFileSync,
15
- appendFileSync,
16
- existsSync,
17
16
  realpathSync,
17
+ rmSync,
18
+ writeFileSync,
18
19
  } from "node:fs";
19
- import { consumerSkillsDir } from "./consumer.mjs";
20
20
  import { tmpdir } from "node:os";
21
- import { join } from "node:path";
21
+ import { dirname, join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { afterAll, test } from "vitest";
22
24
 
23
- const tmp = realpathSync(mkdtempSync(join(tmpdir(), "ledger-selftest-")));
25
+ const HERE = dirname(fileURLToPath(import.meta.url));
26
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), "ledger-test-")));
24
27
  const paper = join(tmp, "a-paper");
25
-
26
- // 🔴 Point the ledger at a throwaway file BEFORE importing it. The first version wrote to the real
27
- // ledger and restored it in a `finally`, which leaks fixture rows into permanent history the moment
28
- // the run dies between the two — and it did, on the day this shipped. Isolation, not cleanup.
28
+ const consumer = join(tmp, "consumer");
29
+ const gate = "cold-read-diff"; // a real skill, so skillHash resolves
30
+ mkdirSync(paper, { recursive: true });
31
+ mkdirSync(join(consumer, ".claude", "skills"), { recursive: true });
32
+ cpSync(
33
+ join(HERE, "..", "..", gate),
34
+ join(consumer, ".claude", "skills", gate),
35
+ {
36
+ recursive: true,
37
+ verbatimSymlinks: true,
38
+ },
39
+ );
29
40
  process.env.PIPELINE_LEDGER = join(tmp, "runs.jsonl");
41
+ process.env.CLAUDE_PROJECT_DIR = consumer;
42
+ const { record, status } = await import("./ledger.mjs");
43
+ afterAll(() => rmSync(tmp, { recursive: true, force: true }));
30
44
 
31
- let failures = 0;
32
- const check = (name, actual, expected) => {
33
- const ok = actual === expected;
34
- if (!ok) failures++;
35
- console.log(
36
- ` ${ok ? "ok " : "FAIL"} ${name}${ok ? "" : ` — expected ${expected}, got ${actual}`}`,
37
- );
38
- };
39
-
40
- try {
41
- // `skillHash` was dropped from the destructuring on 2026-08-28 — no check below was reading it.
42
- const { record, status } = await import("./ledger.mjs");
43
- const mk = (t) => {
44
- writeFileSync(join(paper, "paper.md"), t);
45
- };
46
- const { mkdirSync } = await import("node:fs");
47
- mkdirSync(paper, { recursive: true });
48
-
49
- const gate = "cold-read-diff"; // a real skill, so skillHash resolves
50
- const st = () => status(paper, { gates: [gate] })[0];
45
+ const check = (name, actual, expected) => assert.equal(actual, expected, name);
46
+ const mk = (t) => writeFileSync(join(paper, "paper.md"), t);
47
+ const st = () => status(paper, { gates: [gate] })[0];
51
48
 
49
+ test("the ledger catches every planted error, in order", () => {
52
50
  // ── 1. NEVER-RUN is a state, not an absence ──────────────────────────────────────────────
53
51
  mk("version one");
54
52
  check("a gate nobody has run reads NEVER-RUN", st().state, "NEVER-RUN");
@@ -76,30 +74,11 @@ try {
76
74
  ran();
77
75
  check("back on the original bytes it is FRESH again", st().state, "FRESH");
78
76
 
79
- // 🔴 `../skills/…` until 2026-08-15, when this file moved from `.claude/pipeline/` into the
80
- // skill it serves. A SIXTH form of hidden reference, after literal paths, `join()` segments,
81
- // relative imports, path-shaped regexes and `..`-depth constants: `new URL(rel, import.meta.url)`
82
- // is not an import, so neither a path search nor an import resolver sees it. It broke in CI —
83
- // ENOENT on `.claude/skills/paper-pipeline/skills/cold-read-diff/SKILL.md`, a path assembled from
84
- // the new location and the old relative segment.
85
- //
86
- // 🔴 AND A SEVENTH FORM, 2026-09-12: the segment was right and the ANCHOR moved. Once the
87
- // mechanism ships as a package, `import.meta.url` is inside `node_modules`, so the same two `..`
88
- // now name a skill directory of the PACKAGE — which holds no consumer skill. The subject here
89
- // was never "a file near me"; it is "the SKILL.md whose bytes `skillHash(gate)` folds into the
90
- // key", and that file belongs to the repository under test. So it is resolved the way
91
- // `ledger.mjs` resolves `SKILLS`, from the consumer root, and the two cannot drift apart.
92
- const skillFile = join(consumerSkillsDir(), gate, "SKILL.md");
93
- // Loud, not skipped. This assertion is the only one covering property 2 of the design ("the
94
- // checker's own source is part of the key"); skipping it when the file is absent would leave the
95
- // selftest printing success while proving one property fewer — the green-over-emptiness this
96
- // whole file exists to forbid.
97
- if (!existsSync(skillFile))
98
- throw new Error(
99
- `ledger.selftest: ${skillFile} does not exist.\n` +
100
- `This selftest runs against a CONSUMER of the pipeline — a repository that has the ` +
101
- `pipeline's skills installed under .claude/skills/ — not against this package on its own.`,
102
- );
77
+ // The SKILL.md whose bytes `skillHash(gate)` folds into the key is the CONSUMER's, resolved from
78
+ // CLAUDE_PROJECT_DIR — here a temp copy, so the edit below never touches a tracked file. (The
79
+ // selftest this replaced appended to the real skills/cold-read-diff/SKILL.md and restored it,
80
+ // i.e. it edited the working tree in place while other tests could be reading it.)
81
+ const skillFile = join(consumer, ".claude", "skills", gate, "SKILL.md");
103
82
  const skillBefore = readFileSync(skillFile);
104
83
  try {
105
84
  appendFileSync(
@@ -272,15 +251,4 @@ try {
272
251
  0,
273
252
  );
274
253
  }
275
- } finally {
276
- // The ledger lives inside `tmp`, so removing the fixture removes it too. Nothing to restore,
277
- // which is the point — there is no window in which real history is at risk.
278
- rmSync(tmp, { recursive: true, force: true });
279
- }
280
-
281
- console.log(
282
- failures === 0
283
- ? "\n all planted errors were caught\n"
284
- : `\n 🔴 ${failures} assertion(s) failed — the ledger is not doing what it claims\n`,
285
- );
286
- process.exit(failures === 0 ? 0 : 1);
254
+ });