paperlint 3.1.0 → 3.1.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 (284) hide show
  1. package/.github/workflows/ci.yml +28 -41
  2. package/.github/workflows/dependabot-automerge.yml +1 -1
  3. package/CLAUDE.md +51 -56
  4. package/CONTRIBUTING.md +30 -18
  5. package/bin/paperlint.test.mjs +45 -0
  6. package/dist/adapters/node/files.io.d.ts +2 -0
  7. package/dist/adapters/node/files.io.d.ts.map +1 -1
  8. package/dist/adapters/node/files.io.js +3 -2
  9. package/dist/adapters/node/files.io.js.map +1 -1
  10. package/dist/adapters/pdfjs/fill.d.ts +15 -4
  11. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  12. package/dist/adapters/pdfjs/fill.js +31 -21
  13. package/dist/adapters/pdfjs/fill.js.map +1 -1
  14. package/dist/adapters/references/index.d.ts.map +1 -1
  15. package/dist/adapters/references/index.js +14 -5
  16. package/dist/adapters/references/index.js.map +1 -1
  17. package/dist/adapters/references/pool.d.ts +8 -0
  18. package/dist/adapters/references/pool.d.ts.map +1 -0
  19. package/dist/adapters/references/pool.js +19 -0
  20. package/dist/adapters/references/pool.js.map +1 -0
  21. package/dist/build.d.ts +10 -2
  22. package/dist/build.d.ts.map +1 -1
  23. package/dist/build.js +7 -8
  24. package/dist/build.js.map +1 -1
  25. package/dist/cli.d.ts +27 -1
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +71 -55
  28. package/dist/cli.js.map +1 -1
  29. package/dist/doctor.d.ts +27 -1
  30. package/dist/doctor.d.ts.map +1 -1
  31. package/dist/doctor.js +4 -3
  32. package/dist/doctor.js.map +1 -1
  33. package/dist/domain/result.d.ts +0 -2
  34. package/dist/domain/result.d.ts.map +1 -1
  35. package/dist/domain/result.js +0 -4
  36. package/dist/domain/result.js.map +1 -1
  37. package/dist/hooks-settings.d.ts +8 -5
  38. package/dist/hooks-settings.d.ts.map +1 -1
  39. package/dist/hooks-settings.js +32 -32
  40. package/dist/hooks-settings.js.map +1 -1
  41. package/dist/init.d.ts +8 -0
  42. package/dist/init.d.ts.map +1 -1
  43. package/dist/init.js +3 -3
  44. package/dist/init.js.map +1 -1
  45. package/dist/latex-log.d.ts +0 -2
  46. package/dist/latex-log.d.ts.map +1 -1
  47. package/dist/latex-log.js +7 -13
  48. package/dist/latex-log.js.map +1 -1
  49. package/dist/link-skills.d.ts +15 -2
  50. package/dist/link-skills.d.ts.map +1 -1
  51. package/dist/link-skills.js +36 -20
  52. package/dist/link-skills.js.map +1 -1
  53. package/dist/paper-files.d.ts.map +1 -1
  54. package/dist/paper-files.js +6 -4
  55. package/dist/paper-files.js.map +1 -1
  56. package/dist/pdf-facts.d.ts +11 -0
  57. package/dist/pdf-facts.d.ts.map +1 -1
  58. package/dist/pdf-facts.js +16 -18
  59. package/dist/pdf-facts.js.map +1 -1
  60. package/dist/pdf-geometry.js +1 -1
  61. package/dist/pdf-geometry.js.map +1 -1
  62. package/dist/presets.d.ts +2 -0
  63. package/dist/presets.d.ts.map +1 -1
  64. package/dist/presets.js +3 -1
  65. package/dist/presets.js.map +1 -1
  66. package/dist/references.js +2 -2
  67. package/dist/references.js.map +1 -1
  68. package/dist/tex-requirements.d.ts +8 -0
  69. package/dist/tex-requirements.d.ts.map +1 -1
  70. package/dist/tex-requirements.js +6 -5
  71. package/dist/tex-requirements.js.map +1 -1
  72. package/dist/toolchain.d.ts +3 -2
  73. package/dist/toolchain.d.ts.map +1 -1
  74. package/dist/toolchain.js +4 -5
  75. package/dist/toolchain.js.map +1 -1
  76. package/dist/venue-rules.d.ts.map +1 -1
  77. package/dist/venue-rules.js +11 -9
  78. package/dist/venue-rules.js.map +1 -1
  79. package/docs/prior-art/README.md +1 -1
  80. package/docs/prior-art/test-tooling.md +7 -30
  81. package/eslint-rules/install-path-literals.harness.mjs +17 -1
  82. package/eslint-rules/latex-language.harness.mjs +3 -3
  83. package/eslint-rules/latex-language.mjs +12 -9
  84. package/eslint-rules/latex-language.test.mjs +200 -0
  85. package/eslint-rules/paper-research-question.harness.mjs +73 -7
  86. package/eslint-rules/paper-stages.harness.mjs +9 -14
  87. package/eslint-rules/paper-stages.test.mjs +135 -0
  88. package/eslint-rules/paper-typography.mjs +9 -2
  89. package/eslint-rules/paper-typography.test.mjs +146 -1
  90. package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
  91. package/eslint-rules/review-frontmatter.mjs +2 -1
  92. package/eslint-rules/review-frontmatter.test.mjs +37 -1
  93. package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
  94. package/eslint-rules/tex-build.mjs +13 -13
  95. package/eslint.config.mjs +6 -2
  96. package/hooks/hooks.harness.mjs +6 -11
  97. package/hooks/paper-edit-guard.hook.mjs +2 -2
  98. package/hooks/paper-edit-guard.test.mjs +61 -0
  99. package/hooks/paper-skills-nudge.hook.mjs +2 -2
  100. package/hooks/paper-skills-nudge.test.mjs +38 -0
  101. package/hooks/paper-status-gates.test.mjs +42 -0
  102. package/lib/agent-cli-version.harness.mjs +7 -0
  103. package/lib/check.d.mts +17 -0
  104. package/lib/check.mjs +50 -0
  105. package/lib/check.test.ts +67 -0
  106. package/lib/markdown.mjs +16 -69
  107. package/lib/markdown.test.mjs +246 -0
  108. package/lib/paper-config.harness.mjs +28 -9
  109. package/lib/paper-config.mjs +7 -2
  110. package/lib/skill-checks.mjs +18 -46
  111. package/lib/skill-checks.test.mjs +297 -0
  112. package/lib/skill-corpus.mjs +11 -13
  113. package/lib/skill-corpus.test.mjs +54 -0
  114. package/lib/skill-trigger-cases.harness.mjs +57 -4
  115. package/package.json +6 -5
  116. package/scripts/check.harness.mjs +4 -10
  117. package/scripts/check.mjs +45 -42
  118. package/scripts/check.test.mjs +104 -0
  119. package/scripts/coverage-config.test.ts +406 -0
  120. package/scripts/eslint-report-guard.test.mjs +48 -0
  121. package/scripts/harness-api.frozen.json +0 -2
  122. package/scripts/layer-legacy-frozen.d.mts +9 -1
  123. package/scripts/layer-legacy-frozen.mjs +28 -11
  124. package/scripts/layer-legacy-frozen.test.ts +143 -1
  125. package/scripts/rules-are-content-only.harness.mjs +3 -11
  126. package/scripts/rules-are-content-only.mjs +29 -19
  127. package/scripts/rules-are-content-only.test.mjs +61 -0
  128. package/scripts/rules-see-files.mjs +11 -9
  129. package/scripts/rules-see-files.test.mjs +71 -0
  130. package/scripts/test.mjs +44 -0
  131. package/scripts/test.test.mjs +52 -0
  132. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
  133. package/skills/grade-paper-writing/prose-lint.mjs +31 -12
  134. package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
  135. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
  136. package/skills/paper-pipeline/scripts/README.md +13 -132
  137. package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
  138. package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
  139. package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
  140. package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
  141. package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
  142. package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
  143. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
  144. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
  145. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
  146. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
  147. package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
  148. package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
  149. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
  150. package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
  151. package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
  152. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
  153. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
  154. package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
  155. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
  156. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
  157. package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
  158. package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
  159. package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
  160. package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
  161. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
  162. package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
  163. package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
  164. package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
  165. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
  166. package/skills/paper-pipeline/scripts/status.mjs +2 -2
  167. package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
  168. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
  169. package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
  170. package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
  171. package/skills/tighten-paper/structure.test.mjs +116 -0
  172. package/skills/verify-citations/SKILL.md +5 -5
  173. package/skills/verify-citations/SKILL.md.spec.ts +4 -4
  174. package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
  175. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
  176. package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
  177. package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
  178. package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
  179. package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
  180. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
  181. package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
  182. package/src/adapters/banal/failure.test.ts +70 -1
  183. package/src/adapters/banal/index.test.ts +16 -0
  184. package/src/adapters/banal/output.test.ts +5 -0
  185. package/src/adapters/curl/download.test.ts +43 -1
  186. package/src/adapters/memory/index.test.ts +25 -0
  187. package/src/adapters/node/files.io.ts +3 -2
  188. package/src/adapters/node/files.test.ts +13 -1
  189. package/src/adapters/node/process.test.ts +38 -0
  190. package/src/adapters/pdfjs/fill.test.ts +34 -0
  191. package/src/adapters/pdfjs/fill.ts +44 -25
  192. package/src/adapters/references/index.test.ts +228 -0
  193. package/src/adapters/references/index.ts +21 -6
  194. package/src/adapters/references/pool.test.ts +31 -0
  195. package/src/adapters/references/pool.ts +24 -0
  196. package/src/build-engine.harness.mjs +3 -7
  197. package/src/build-engine.test.ts +137 -0
  198. package/src/build.harness.mjs +5 -9
  199. package/src/build.test.ts +362 -0
  200. package/src/build.ts +13 -9
  201. package/src/cli.harness.mjs +10 -17
  202. package/src/cli.test.ts +584 -0
  203. package/src/cli.ts +112 -66
  204. package/src/doctor.harness.mjs +3 -8
  205. package/src/doctor.test.ts +64 -0
  206. package/src/doctor.ts +12 -4
  207. package/src/domain/result.test.ts +4 -0
  208. package/src/domain/result.ts +0 -5
  209. package/src/engine.harness.mjs +27 -7
  210. package/src/facts-file.test.ts +110 -0
  211. package/src/hooks-settings.harness.mjs +43 -13
  212. package/src/hooks-settings.ts +41 -35
  213. package/src/init.test.ts +308 -5
  214. package/src/init.ts +3 -3
  215. package/src/latex-log.harness.mjs +32 -7
  216. package/src/latex-log.ts +6 -13
  217. package/src/latex-loop.harness.mjs +8 -7
  218. package/src/link-skills.harness.mjs +3 -8
  219. package/src/link-skills.test.ts +105 -0
  220. package/src/link-skills.ts +75 -21
  221. package/src/new-paper.harness.mjs +3 -7
  222. package/src/new-paper.test.ts +31 -1
  223. package/src/paper-files.test.ts +12 -0
  224. package/src/paper-files.ts +9 -5
  225. package/src/paper-settings.test.ts +20 -0
  226. package/src/pdf-facts.harness.mjs +3 -6
  227. package/src/pdf-facts.test.ts +144 -0
  228. package/src/pdf-facts.ts +17 -19
  229. package/src/pdf-geometry.harness.mjs +3 -7
  230. package/src/pdf-geometry.ts +1 -1
  231. package/src/presets.test.ts +2 -0
  232. package/src/presets.ts +7 -1
  233. package/src/references.test.ts +97 -0
  234. package/src/references.ts +2 -2
  235. package/src/rules-config.harness.mjs +10 -8
  236. package/src/structure.harness.mjs +3 -7
  237. package/src/tex-requirements.harness.mjs +34 -7
  238. package/src/tex-requirements.ts +12 -6
  239. package/src/toolchain.harness.mjs +3 -7
  240. package/src/toolchain.test.ts +280 -0
  241. package/src/toolchain.ts +8 -7
  242. package/src/venue-rules.test.ts +60 -0
  243. package/src/venue-rules.ts +13 -11
  244. package/action.mutations.mjs +0 -162
  245. package/eslint-rules/install-path-literals.mutations.mjs +0 -62
  246. package/eslint-rules/latex-language.mutations.mjs +0 -196
  247. package/eslint-rules/paper-research-question.mutations.mjs +0 -127
  248. package/eslint-rules/paper-stages.mutations.mjs +0 -81
  249. package/eslint-rules/papers.mutations.mjs +0 -186
  250. package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
  251. package/eslint-rules/tex-build.mutations.mjs +0 -258
  252. package/hooks/hooks.mutations.mjs +0 -337
  253. package/lib/agent-cli-version.mutations.mjs +0 -109
  254. package/lib/mutation-driver.harness.mjs +0 -227
  255. package/lib/mutation-driver.mjs +0 -397
  256. package/lib/mutation-driver.mutations.mjs +0 -68
  257. package/lib/paper-config.mutations.mjs +0 -143
  258. package/lib/skill-trigger-cases.mutations.mjs +0 -65
  259. package/scripts/check.mutations.mjs +0 -110
  260. package/scripts/exclusive.mjs +0 -138
  261. package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
  262. package/scripts/mutation-batteries-frozen.mjs +0 -238
  263. package/scripts/mutation-batteries.frozen.json +0 -108
  264. package/scripts/rules-are-content-only.mutations.mjs +0 -81
  265. package/scripts/rules-see-files.mutations.mjs +0 -131
  266. package/scripts/run-mutations.mjs +0 -100
  267. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
  268. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
  269. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
  270. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
  271. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
  272. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
  273. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
  274. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
  275. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
  276. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
  277. package/skills/skill-contract.mutations.mjs +0 -179
  278. package/src/build.mutations.mjs +0 -196
  279. package/src/cli.mutations.mjs +0 -414
  280. package/src/doctor.mutations.mjs +0 -175
  281. package/src/hooks-settings.mutations.mjs +0 -99
  282. package/src/link-skills.mutations.mjs +0 -140
  283. package/src/new-paper.mutations.mjs +0 -65
  284. package/src/structure.mutations.mjs +0 -83
@@ -1,397 +0,0 @@
1
- /**
2
- * mutation-driver.mjs — the shared engine behind every `*.mutations.mjs` in this repo.
3
- *
4
- * NOT IN vigiles, AND THAT IS A DECISION WITH A DATE. On 2026-08-14 this engine was extracted into
5
- * vigiles as `runMutations` (`vigiles/testing`, shipped in 15.3.0). The owner killed it the next
6
- * day with one question: «how is this vigiles' responsibility, it's just nodejs?» — and he is
7
- * right. Measured against the rest of
8
- * that package: `runHook` knows the Claude Code hook protocol, `measureTriggerRate` knows what a
9
- * skill is, `skillContract` reads `allowed-tools` out of frontmatter. This file knows none of that.
10
- * It replaces a substring, spawns `node`, and reads stdout; lift it into any Node repo and it works
11
- * unchanged. Confirming measurement: no test in vigiles used it — the product did not dogfood its
12
- * own published API.
13
- *
14
- * The rule "mechanism → vigiles" was applied too bluntly, and this is the correction worth keeping:
15
- * what fixed the ten drifted copies was CONSOLIDATION, not RELOCATION. One copy instead of ten
16
- * buys every protection listed below. Moving that copy into a product for harness authors was a
17
- * second, separate step, and it was never justified — it only bought a newcomer the question the
18
- * owner asked. `checkProductCodeInKb` flags this file on sight, which is correct and stays visible: the
19
- * heuristic is right that this is mechanism-shaped, and the answer is «yes, and it was rejected
20
- * upstream on 2026-08-15», not «no».
21
- *
22
- * 🔴 2026-09-12 — IT NOW LIVES IN A PACKAGE ANYWAY, AND THAT DOES NOT REOPEN THE DECISION
23
- * ABOVE. The rejected move was into vigiles: a product for people writing agent harnesses, where
24
- * a generic string-replacing test driver is off-topic and, measured, was not used by a single one
25
- * of its own tests. This package is the opposite case — it IS the batteries' own repository, they
26
- * are its tests, and it cannot run them without the engine. So the destination differs on the
27
- * exact axis the rejection turned on: «does the package this lives in actually use it». Do not
28
- * read this file's presence here as the vigiles decision being reversed; it was not re-argued.
29
- *
30
- * The one shape that WOULD belong in vigiles is a different function that does not exist yet:
31
- * mutate the HOOK — remove one branch of a guard — and require the assembled harness test to
32
- * notice. Only vigiles knows what a hook is. Backlog, not this file.
33
- *
34
- * WHAT A MUTATION RUN IS. Plant one defect in a checker, run that checker's harness, and require
35
- * the harness to go red AND to name the case that owns the defect. A harness that stays green has
36
- * an assertion that cannot fail; a harness that reddens with someone else's message has two
37
- * defects sharing one assertion, which means neither is really watched.
38
- *
39
- * WHY THIS FILE EXISTS (2026-08-14). Ten mutation files, 1799 lines, ran the same twenty-five-line
40
- * driver ten times — and they had DRIFTED, so the copies were not equivalent. Counted against the
41
- * committed versions, not estimated:
42
- *
43
- * - NO-OP GUARD (a replacement equal to the original leaves a green harness that proves nothing):
44
- * present in 4 of 10 — `ledger`, `round-diff`, `textidote`, `uncited-refs`. SIX lacked it.
45
- * - RETRY (a non-kill is re-run once before it is believed, because a row killed as named came
46
- * back DIFFERENT in a later full run and reproduced clean when replayed alone): present in
47
- * 1 of 10 — `ledger`. NINE lacked it.
48
- * - `skills.mutations.mjs` alone mutated a COPY of the tree; the other nine rewrite the real
49
- * source and restore it in a `finally`.
50
- * - Two files pointed at `.claude/pipeline/skills.harness.mjs`, deleted by the 2026-08-11
51
- * colocation (`ledger`, six cases; and `skills` itself). `vigiles test` on a path matching
52
- * nothing exits 0, so those cases reported SURVIVED forever — a mutation runner producing
53
- * confident wrong answers about mutation runs.
54
- *
55
- * Every protection above was written AFTER it caught something, in one file, and never travelled
56
- * to the other nine. That is the argument for one engine, and it is stronger than "less code".
57
- *
58
- * So the split is not cosmetic DRY: nine files GAIN two protections they never had, and the dead
59
- * path becomes expressible once instead of three times.
60
- *
61
- * WHAT STAYS IN THE CALLER. The CASES TABLE — which defect, which bytes, which message. That is
62
- * data about OUR checkers and belongs next to them (root CLAUDE.md: "mechanism — in vigiles,
63
- * data — in mine"; the same line separates mechanism from data inside this repo). This file knows
64
- * nothing about papers, skills or ledgers.
65
- *
66
- * WHAT DOES NOT USE THIS ENGINE, AND WHY. `skills.mutations.mjs` mutates a COPY of `.claude` in a
67
- * temp root and runs the harnesses there with `CLAUDE_PROJECT_DIR` repointed. That is the right
68
- * strategy for it — its cases break the SKILL CORPUS itself, so a copy is cheap (one directory) and
69
- * the real corpus is never touched. It is the only one CI runs, and it stays as it is. Two
70
- * strategies is a smell only when they differ by accident; here the difference is the subject.
71
- *
72
- * 🔴 THE IN-PLACE STRATEGY IS PRESERVED, NOT "FIXED". Nine callers rewrite a real file, and moving
73
- * them to a temp copy is not a refactor: their harnesses resolve every path from the repo root, so
74
- * a copy means copying the whole repo per mutation — minutes instead of seconds. What IS fixed is
75
- * the failure mode: the restore now also runs on SIGINT/SIGTERM, and a run refuses to start when
76
- * any target file is already dirty, so an interrupted run cannot leave a neutered checker behind
77
- * and then pass.
78
- */
79
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
80
- import { spawnSync } from "node:child_process";
81
-
82
- /**
83
- * @typedef {object} MutationCase
84
- * @property {string} name Short id, printed in the table.
85
- * @property {string} disables What the mutation takes away, in words — the column a reader scans.
86
- * @property {[string, string, string][]} edits `[absolutePath, find, replace]`. A LIST because some
87
- * defects are only expressible as more than one edit: an acquittal reaching the ledger needs both
88
- * the retired-name guard and the vocabulary check removed, and either alone throws for an
89
- * unrelated reason.
90
- * @property {string} harness Absolute path to the harness that must go red.
91
- * @property {string} expect Substring the harness's complaint must contain.
92
- */
93
-
94
- /** Runs a harness file. `node` for a plain harness, `npx vigiles test` when the caller says so. */
95
- const runners = {
96
- node: (harness, root) =>
97
- spawnSync("node", [harness], { cwd: root, encoding: "utf8" }),
98
- vigiles: (harness, root) =>
99
- spawnSync("npx", ["vigiles", "test", harness], {
100
- cwd: root,
101
- encoding: "utf8",
102
- }),
103
- };
104
-
105
- /**
106
- * vigiles' own protocol constant: `skip()` ends the process with this code. Its docs state it —
107
- * `harness-assert.d.ts`: «Exit 77 is the runner's `SKIP_EXIT_CODE` (run-scripts.ts)» — but the
108
- * package exports `skip` and not the number, so it is NAMED here instead of appearing as a bare
109
- * literal in a comparison.
110
- *
111
- * ⚠️ If a future vigiles changes it, this degrades GRACEFULLY rather than silently: an unknown
112
- * non-zero code reads as "red", so the battery reports UNJUDGEABLE instead of claiming a
113
- * survivor. Wrong, but loudly wrong — which is the direction this whole file errs in.
114
- */
115
- const VIGILES_SKIP_EXIT = 77;
116
-
117
- /**
118
- * 🔴 A SKIPPED HARNESS IS A THIRD OUTCOME, AND CONFLATING IT WITH "GREEN" PRODUCES A CONFIDENT
119
- * WRONG DIAGNOSIS — the failure this file already documents twice, arriving by a new door.
120
- *
121
- * MEASURED 2026-09-15, chain complete, every link observed rather than argued:
122
- *
123
- * the harness itself, `claude` CLI absent exit 77 "SKIPPED: … no run can be observed"
124
- * `npx vigiles test <harness>` exit 0 "0 passed, 1 skipped."
125
- * the driver reads 0 ⇒ "🔴 SURVIVED"
126
- *
127
- * So `plan-paper-timeline.effects.mutations.mjs` reported TWO survivors in CI and none locally,
128
- * and the difference was not the code under test — it was that CI has no `claude` binary. The
129
- * reader is sent to rewrite a working assertion, which is strictly worse than a red with no
130
- * explanation.
131
- *
132
- * WHY THE FLAG AND NOT A SUBSTRING. `--no-skip` exists precisely to turn "skipped" into a
133
- * failure, so asking the tool with it is asking the THING; matching "SKIPPED" in the output
134
- * would be matching its shadow, and the shadow has spellings. The plain run then disambiguates:
135
- * a harness that is green without the flag and red with it SKIPPED; one that is red both ways was
136
- * already red, which the caller below already handles.
137
- *
138
- * ⚠️ The extra probe runs ONLY for a harness that is not plainly green, so the common path costs
139
- * exactly what it cost before.
140
- *
141
- * @returns {"green"|"red"|"skip"}
142
- */
143
- function probeHarness(runner, harness, root) {
144
- // The node runner needs no probe: a skipping harness exits with vigiles' own skip code, and
145
- // that code IS the signal. It is only the `vigiles` wrapper that swallows it into a 0.
146
- if (runner === "node") {
147
- const r = runners.node(harness, root);
148
- return r.status === 0
149
- ? "green"
150
- : r.status === VIGILES_SKIP_EXIT
151
- ? "skip"
152
- : "red";
153
- }
154
- const strict = spawnSync("npx", ["vigiles", "test", harness, "--no-skip"], {
155
- cwd: root,
156
- encoding: "utf8",
157
- });
158
- if (strict.status === 0) return "green";
159
- return runners.vigiles(harness, root).status === 0 ? "skip" : "red";
160
- }
161
-
162
- /**
163
- * @param {object} o
164
- * @param {string} o.root Repo root; every path in `edits` and `harness` is absolute.
165
- * @param {MutationCase[]} o.cases
166
- * @param {"node"|"vigiles"} [o.runner] How to invoke a harness. Default `node`.
167
- * @param {string[]} [o.cleanHarnesses] Run after restoring to prove the restore worked. Defaults
168
- * to the distinct harnesses named by the cases.
169
- * @returns {number} process exit code — 0 only if every mutation was killed by its OWN case.
170
- */
171
- export function runMutations({ root, cases, runner = "node", cleanHarnesses }) {
172
- const run = runners[runner];
173
- if (!run)
174
- throw new Error(
175
- `unknown runner ${JSON.stringify(runner)}; expected node or vigiles`,
176
- );
177
- if (!cases.length)
178
- throw new Error(
179
- "no mutation cases were passed — a runner with nothing to run reports success",
180
- );
181
-
182
- const targets = [...new Set(cases.flatMap((c) => c.edits.map(([f]) => f)))];
183
- const harnesses = [...new Set(cases.map((c) => c.harness))];
184
-
185
- // COVERAGE MODE — report which harnesses this battery can kill, then stop without touching a
186
- // file. Read by `scripts/run-mutations.mjs` to answer «is every harness in the repo killable by
187
- // SOMETHING», which it previously answered by FILENAME (`x.harness.mjs` ⇄ `x.mutations.mjs`).
188
- //
189
- // 🔴 THE FILENAME WAS A PROXY, AND IT WAS WRONG ON THIS CORPUS. `ledger.mutations.mjs` is the
190
- // battery for `gates.harness.mjs` — it plants defects in the ledger and requires the GATES
191
- // harness to notice — and no naming convention expresses that. Under the old rule it read as
192
- // two problems at once: a battery covering nothing and a harness covered by nobody. Pairing is
193
- // now derived from the `harness` field of the cases that actually run, i.e. from the same data
194
- // structure that does the work, so it cannot disagree with what the battery does.
195
- //
196
- // ⚠️ An exemption list was the other available answer and is the one NOT taken: a guard with a
197
- // list of things it agrees not to look at is decoration. Nothing here is exempt — a harness is
198
- // covered only when some battery names it, and a battery that names a harness it does not
199
- // really exercise is caught by that case reporting SURVIVED.
200
- if (process.env.MUTATIONS_REPORT_COVERAGE) {
201
- for (const h of harnesses) console.log(`MUTATION-COVERS\t${h}`);
202
- return 0;
203
- }
204
-
205
- // 🔴 A harness path that resolves to nothing is the defect that hid for three days: `vigiles
206
- // test` prints "No files found" and exits 0, so every case naming it reports SURVIVED and the
207
- // reader goes looking for a broken assertion that was never reached. Checked BEFORE any file is
208
- // touched, so the message arrives with a clean working tree.
209
- const missing = harnesses.filter((h) => !existsSync(h));
210
- if (missing.length) {
211
- console.error(
212
- `refusing to run: harness file(s) do not exist:\n ${missing.join("\n ")}`,
213
- );
214
- return 2;
215
- }
216
-
217
- const git = (...a) =>
218
- spawnSync("git", a, { cwd: root, encoding: "utf8" }).stdout ?? "";
219
- const dirty = targets.filter((f) => git("status", "--porcelain", f).trim());
220
- if (dirty.length) {
221
- console.error(
222
- `refusing to run: this script rewrites the file(s) below and they have uncommitted changes.\n ${dirty.join("\n ")}\nCommit first (do NOT stash — the stash is repository-global and shared with any other agent).`,
223
- );
224
- return 2;
225
- }
226
-
227
- // 🔴 BASELINE FIRST, and it is not an optimisation. The old "did the restore work" check ran the
228
- // harnesses at the END and called any red one a FAILED RESTORE. `gates.harness.mjs` is red today
229
- // on purpose — its own message says "these are OPEN FINDINGS about the gates themselves, not
230
- // harness bugs" — so every ledger run printed "🔴 the restore failed" about a working restore and
231
- // exited 1. A confident wrong diagnosis, produced by the tool whose whole job is catching those.
232
- // Knowing which harnesses were ALREADY red makes the end-of-run claim mean what it says.
233
- // Three outcomes, not two — see `probeHarness`. A harness that SKIPS here can never testify
234
- // about a mutation, so the battery is refused BEFORE a single file is touched, exactly like the
235
- // missing-path guard above: the message arrives with a clean working tree.
236
- const probes = new Map(
237
- harnesses.map((h) => [h, probeHarness(runner, h, root)]),
238
- );
239
- const skipped = harnesses.filter((h) => probes.get(h) === "skip");
240
- if (skipped.length) {
241
- console.error(
242
- `refusing to run: harness(es) SKIP on clean source, so this battery can prove nothing:\n ` +
243
- skipped.join("\n ") +
244
- `\nA skipped harness exits 0 through \`vigiles test\`, which this driver would read as ` +
245
- `"the mutation survived" — a claim about the TEST that is really a claim about the ` +
246
- `ENVIRONMENT. Install the capability the harness names (it says so on its skip line), ` +
247
- `or run the battery where that capability exists.`,
248
- );
249
- return 2;
250
- }
251
- const redBefore = new Set(harnesses.filter((h) => probes.get(h) === "red"));
252
- if (redBefore.size) {
253
- console.log(
254
- `ℹ️ already red before any mutation: ${[...redBefore].map(base).join(", ")} — excluded from the restore check, and any case naming one is reported as UNJUDGEABLE.`,
255
- );
256
- }
257
-
258
- const originals = new Map(targets.map((f) => [f, readFileSync(f, "utf8")]));
259
- const restore = () => {
260
- for (const [f, text] of originals) writeFileSync(f, text);
261
- };
262
-
263
- // Restoring only in `finally` leaves a neutered checker on the disk when the run is killed —
264
- // and the next run then measures a repo that is already broken.
265
- const onSignal = (sig) => {
266
- restore();
267
- process.exit(sig === "SIGINT" ? 130 : 143);
268
- };
269
- process.on("SIGINT", onSignal);
270
- process.on("SIGTERM", onSignal);
271
-
272
- const rows = [];
273
- try {
274
- for (const c of cases) {
275
- let problem = null;
276
- for (const [file, find, replace] of c.edits) {
277
- const src = readFileSync(file, "utf8");
278
- const n = src.split(find).length - 1;
279
- if (n !== 1) {
280
- problem =
281
- n === 0
282
- ? `NOT FOUND in ${base(file)}`
283
- : `AMBIGUOUS (${n}) in ${base(file)}`;
284
- break;
285
- }
286
- const next = src.replace(find, replace);
287
- // The mutation-that-does-not-mutate guard, confirmed against the BYTES rather than the
288
- // intent. Nine of the ten files lacked it; a replacement equal to the original leaves a
289
- // green harness that proves nothing and reads exactly like a killed mutation.
290
- if (next === src) {
291
- problem = "NO-OP (replacement equals original)";
292
- break;
293
- }
294
- writeFileSync(file, next);
295
- }
296
- if (problem) {
297
- rows.push([c.name, problem, c.disables]);
298
- restore();
299
- continue;
300
- }
301
-
302
- // A non-kill is retried ONCE before it is believed. Observed on the ledger table: a row
303
- // killed as named came back DIFFERENT in a later full run and then reproduced as a clean
304
- // kill when replayed alone. Reporting a flake as a survivor sends the next reader to rewrite
305
- // a working assertion — a worse outcome than one extra run. A row that fails BOTH times is
306
- // a real finding and is printed without hedging.
307
- const attempt = () => {
308
- const r = run(c.harness, root);
309
- const out = (r.stdout || "") + (r.stderr || "");
310
- if (r.status === 0) return "🔴 SURVIVED";
311
- if (out.includes(c.expect)) return "RED (expected case)";
312
- // When the harness was ALREADY red, "red" carries no information — but the MESSAGE still
313
- // does, because node's assert aborts at the first failure, so a message that appears was
314
- // genuinely reached. Absent it, the two causes are indistinguishable and saying "different
315
- // case" would be a guess dressed as a verdict.
316
- return redBefore.has(c.harness)
317
- ? `🔴 UNJUDGEABLE (harness red before the run; "${c.expect}" never printed)`
318
- : `RED (but a DIFFERENT case: expected "${c.expect}")`;
319
- };
320
- let verdict = attempt();
321
- if (verdict !== "RED (expected case)") {
322
- const second = attempt();
323
- if (second === "RED (expected case)")
324
- verdict = "RED (expected case, on retry)";
325
- else verdict = second;
326
- }
327
- rows.push([c.name, verdict, c.disables]);
328
- restore();
329
- }
330
- } finally {
331
- restore();
332
- process.off("SIGINT", onSignal);
333
- process.off("SIGTERM", onSignal);
334
- }
335
-
336
- const width = Math.max(...rows.map((r) => r[0].length));
337
- for (const [name, verdict, disables] of rows)
338
- console.log(`${name.padEnd(width)} ${verdict.padEnd(38)} ${disables}`);
339
-
340
- // The restore is asserted, not assumed: every harness the run touched must be green again.
341
- // Only harnesses that were GREEN before can testify about the restore. One that was already red
342
- // proves nothing either way, and counting it was how this line came to lie.
343
- const cleanSet = (cleanHarnesses ?? harnesses).filter(
344
- (h) => !redBefore.has(h),
345
- );
346
- const stillRed = cleanSet.filter((h) => run(h, root).status !== 0);
347
- console.log(
348
- `\nrestored source: ${
349
- cleanSet.length === 0
350
- ? "NOT CHECKED — every harness here was already red before the run"
351
- : stillRed.length === 0
352
- ? "harness GREEN"
353
- : `🔴 RED — the restore failed (${stillRed.map(base).join(", ")})`
354
- }`,
355
- );
356
-
357
- // 🔴 `RED (expected`, not `RED`. Nine of the ten files counted a mutation killed by SOMEONE
358
- // ELSE'S assertion as a kill; only `ledger` required the named one. Taking the weak rule while
359
- // printing "each at its own assertion" below would have been a claim the mechanism did not back —
360
- // exactly the defect this whole directory exists to catch. Measured cost of the strict rule on
361
- // today's corpus: zero rows change verdict, because every row already came back as its own case.
362
- // 🔴 A MUTATION THAT NEVER LANDED IS NOT A SURVIVOR, and until 2026-09-17 this line said it
363
- // was: every row that did not start with `RED (expected` went into `survived`, including
364
- // `NOT FOUND`, `AMBIGUOUS` and `NO-OP` — cases where the file was never edited at all. The two
365
- // verdicts send a reader to opposite places: a survivor means STRENGTHEN THE TEST, an unusable
366
- // case means FIX THE MUTATION. Observed cost: a stale `find` string (the rule had been
367
- // rewritten) was reported as "1 mutation survived", and the next step would have been rewriting
368
- // a harness that was fine. The docstring two hundred lines above warns about exactly this
369
- // confusion — the summary contradicted it.
370
- const unusable = rows.filter(
371
- (r) =>
372
- r[1].startsWith("NOT FOUND") ||
373
- r[1].startsWith("AMBIGUOUS") ||
374
- r[1].startsWith("NO-OP"),
375
- );
376
- const survived = rows.filter(
377
- (r) => !r[1].startsWith("RED (expected") && !unusable.includes(r),
378
- );
379
- if (unusable.length)
380
- console.log(
381
- `🔴 ${unusable.length} mutation(s) UNUSABLE — the patch never landed, so nothing was ` +
382
- `measured about the test: ${unusable.map((r) => `${r[0]} [${r[1]}]`).join(", ")}`,
383
- );
384
- console.log(
385
- survived.length
386
- ? `🔴 ${survived.length} mutation(s) survived: ${survived.map((r) => r[0]).join(", ")}`
387
- : // "each at its own assertion" is not decoration — a kill only counts when the harness went
388
- // red with the message the case named. A mutation killed by SOMEONE ELSE'S assertion means
389
- // two defects share one assertion and neither is really watched, and it lands in `survived`.
390
- unusable.length
391
- ? `(${rows.length - unusable.length} of ${rows.length} mutations killed; the rest were never applied)`
392
- : `✓ all ${rows.length} mutations killed by the harness, each at its own assertion`,
393
- );
394
- return survived.length || unusable.length || stillRed.length ? 1 : 0;
395
- }
396
-
397
- const base = (p) => p.split("/").pop();
@@ -1,68 +0,0 @@
1
- /**
2
- * Battery for the mutation engine itself — the one it didn't have.
3
- *
4
- * Both cases target ONE property, added 2026-09-15: a skipped harness is a third outcome,
5
- * and a battery whose harness skips must REFUSE to judge. The first removes the refusal
6
- * itself, the second removes the ability to recognize a skip. Different causes, one
7
- * observable effect, and that effect already cost a false "two mutations survived"
8
- * diagnosis in CI (run 34966606186).
9
- */
10
- import { fileURLToPath } from "node:url";
11
- import { dirname, join, resolve } from "node:path";
12
- import { runMutations } from "./mutation-driver.mjs";
13
-
14
- const HERE = dirname(fileURLToPath(import.meta.url));
15
- const ROOT = resolve(HERE, "..");
16
- const DRIVER = join(HERE, "mutation-driver.mjs");
17
- const HARNESS = join(HERE, "mutation-driver.harness.mjs");
18
-
19
- process.exit(
20
- runMutations({
21
- root: ROOT,
22
- runner: "node",
23
- cases: [
24
- {
25
- // Revert to how it was before 09-17: anything not killed is declared survived — and
26
- // "the patch never landed" becomes indistinguishable from "the test is weak". The
27
- // mutation is caught ONLY by the harness's third outcome; the first two pass through
28
- // it unchanged.
29
- name: '"never landed" collapses back into "survived"',
30
- harness: HARNESS,
31
- expect: "the verdict must be named unusable, not survived",
32
- disables:
33
- "the distinction between two verdicts that send the reader in opposite directions: " +
34
- "survived — strengthen the test, unusable — fix the mutation. Conflating them sends " +
35
- "the engine to rewrite a working harness",
36
- edits: [
37
- [
38
- DRIVER,
39
- "if (unusable.length)\n console.log(",
40
- "if (false)\n console.log(",
41
- ],
42
- ],
43
- },
44
- {
45
- name: "the refusal for a skipping harness is removed",
46
- harness: HARNESS,
47
- expect: "a skip must REFUSE",
48
- disables:
49
- "the refusal itself — the driver goes back to judging by a harness that never ran",
50
- edits: [[DRIVER, " if (skipped.length) {", " if (false) {"]],
51
- },
52
- {
53
- name: "the skip exit code stops being recognized",
54
- harness: HARNESS,
55
- expect: "a skip must REFUSE",
56
- disables:
57
- 'recognizing the protocol code — a skip starts reading as "red"',
58
- edits: [
59
- [
60
- DRIVER,
61
- "const VIGILES_SKIP_EXIT = 77;",
62
- "const VIGILES_SKIP_EXIT = 76;",
63
- ],
64
- ],
65
- },
66
- ],
67
- }),
68
- );
@@ -1,143 +0,0 @@
1
- /**
2
- * Battery for `lib/paper-config.mjs` and the cross-check of its carriers.
3
- *
4
- * The subject is agreement among several files about one value. The failure here is silent by
5
- * construction: a drifted key breaks neither the build nor the run, it only makes one carrier
6
- * read settings the consumer never wrote. Every case below reintroduces that kind of drift.
7
- */
8
- import { fileURLToPath } from "node:url";
9
- import { dirname, join, resolve } from "node:path";
10
- import { runMutations } from "./mutation-driver.mjs";
11
-
12
- const HERE = dirname(fileURLToPath(import.meta.url));
13
- const ROOT = resolve(HERE, "..");
14
- const HARNESS = join(HERE, "paper-config.harness.mjs");
15
- const HOOK = join(ROOT, "hooks", "paper-skills-nudge.hook.mjs");
16
- const RULE = join(ROOT, "eslint-rules", "papers.mjs");
17
- const DOCTOR = join(ROOT, "src", "doctor.ts");
18
-
19
- process.exit(
20
- runMutations({
21
- root: ROOT,
22
- runner: "node",
23
- cases: [
24
- {
25
- name: "the key drifts in one of the hooks",
26
- harness: HARNESS,
27
- expect: "all carriers agree on CONFIG_KEY",
28
- disables:
29
- "the whole point of the forced duplication. A hook with a mismatched key reads " +
30
- "settings the consumer never wrote, falls back to the default, and guards the wrong " +
31
- "directory — silently, because silence is exactly what its success looks like",
32
- edits: [
33
- [
34
- HOOK,
35
- 'export const CONFIG_KEY = "paperlint";',
36
- 'export const CONFIG_KEY = "paperlints";',
37
- ],
38
- ],
39
- },
40
- {
41
- name: "a hook's copy of the papers-directory field name drifts",
42
- harness: HARNESS,
43
- expect: "all carriers agree on PAPERS_DIR_FIELD",
44
- disables:
45
- "the check on the one value this package renamed. A hook reading a different field " +
46
- "name finds nothing, falls back to the default directory, and says nothing about it",
47
- edits: [
48
- [
49
- HOOK,
50
- // Prefix the value rather than spell it, so a future rename does not touch this file.
51
- 'export const PAPERS_DIR_FIELD = "',
52
- 'export const PAPERS_DIR_FIELD = "drifted-',
53
- ],
54
- ],
55
- },
56
- {
57
- name: "the ESLint rule grows its OWN copy of the default again",
58
- harness: HARNESS,
59
- expect: "all carriers agree on DEFAULT_PAPERS_ROOT",
60
- disables:
61
- "the re-export the consolidation existed for in the first place. A non-hook's own " +
62
- "copy serves nothing — the import restriction does not apply to it — and exactly this " +
63
- "kind of copy sat OUTSIDE the cross-check for two days, because the cross-check " +
64
- "compared hooks against each other",
65
- edits: [
66
- [
67
- RULE,
68
- "import {\n CONFIG_KEY,\n DEFAULT_PAPERS_ROOT,\n PAPERS_DIR_FIELD,",
69
- 'const DEFAULT_PAPERS_ROOT = "drafts";\nimport {\n CONFIG_KEY,\n PAPERS_DIR_FIELD,',
70
- ],
71
- ],
72
- },
73
- {
74
- name: "the config key stops being the package name",
75
- harness: HARNESS,
76
- expect: "config key equals the package name",
77
- disables:
78
- "the link between what the package is called and the name the consumer declares its " +
79
- "settings under. They drift apart when the package is renamed: the docs will say one " +
80
- "thing, a different one gets read, and there will be no error at all",
81
- // The PACKAGE is renamed, not the constant: the carriers stay in agreement with each
82
- // other, and the only thing that drifts is the "key = package name" link. Swapping the
83
- // constant itself would kill the neighboring carrier-agreement assertion first and
84
- // never reach this one.
85
- edits: [
86
- [
87
- join(ROOT, "package.json"),
88
- '"name": "paperlint",',
89
- '"name": "paperlint-renamed",',
90
- ],
91
- ],
92
- },
93
- {
94
- name: "a hook smuggles a capability past the vocabulary",
95
- harness: HARNESS,
96
- expect: "pulls in nothing outside the hook vocabulary",
97
- disables:
98
- "the one guarantee the hooks put up with duplication for: a hook's capabilities equal " +
99
- "its API surface. A hook that reaches `node:fs` still runs and passes the value " +
100
- "cross-check — the violation is visible only through this check",
101
- edits: [
102
- [
103
- HOOK,
104
- "import {\n experimental_defineReact,",
105
- 'import { readFileSync } from "node:fs";\nimport {\n experimental_defineReact,',
106
- ],
107
- ],
108
- },
109
- {
110
- name: "a constant is written into a sixth file instead of imported",
111
- harness: HARNESS,
112
- expect: "no declaration outside the carrier list",
113
- disables:
114
- "the COMPLETENESS check. The carrier list is explicit, so a new copy planted anywhere " +
115
- "else would have nothing to compare against — which is exactly how the duplication " +
116
- "spread last time",
117
- edits: [
118
- [
119
- DOCTOR,
120
- 'import {\n papersRoot,\n DEFAULT_PAPERS_ROOT,\n PAPERS_DIR_FIELD,\n CONFIG_FILE,\n} from "../hooks/paper-edit-guard.hook.mjs";',
121
- 'import {\n papersRoot,\n DEFAULT_PAPERS_ROOT,\n PAPERS_DIR_FIELD,\n} from "../hooks/paper-edit-guard.hook.mjs";\nconst CONFIG_FILE = "paperlint.json";',
122
- ],
123
- ],
124
- },
125
- {
126
- name: "the completeness walk stops looking into the hooks",
127
- harness: HARNESS,
128
- expect: "the search itself is not empty",
129
- disables:
130
- "the guard against a FALSE ZERO. A search that found nothing looks like a clean " +
131
- "corpus; without this assertion, the completeness check, having started looking in " +
132
- "the wrong place, would report all-clear",
133
- edits: [
134
- [
135
- HARNESS,
136
- 'const skip = new Set([\n "node_modules",\n ".git",\n "dist",\n "_build",\n "fixtures",\n "repro",\n ]);',
137
- 'const skip = new Set([\n "node_modules",\n ".git",\n "dist",\n "_build",\n "fixtures",\n "repro",\n "hooks",\n "lib",\n ]);',
138
- ],
139
- ],
140
- },
141
- ],
142
- }),
143
- );
@@ -1,65 +0,0 @@
1
- /**
2
- * skill-trigger-cases.mutations.mjs — the non-vacuity proof for the carrier harness.
3
- *
4
- * Run: `node lib/skill-trigger-cases.mutations.mjs`
5
- *
6
- * Every case here removes ONE property the harness claims to hold and requires the harness to
7
- * name that property rather than merely go red. Written with the carrier, not after it: a harness
8
- * whose success state is a single printed line cannot be observed working, and this package's own
9
- * `scripts/run-mutations.mjs` refuses a harness no battery can kill for exactly that reason.
10
- */
11
- import { dirname, join } from "node:path";
12
- import { fileURLToPath } from "node:url";
13
- import { runMutations } from "./mutation-driver.mjs";
14
- import { consumerRoot } from "../skills/paper-pipeline/scripts/consumer.mjs";
15
-
16
- const HERE = dirname(fileURLToPath(import.meta.url));
17
- const SRC = join(HERE, "skill-trigger-cases.mjs");
18
- const HARNESS = join(HERE, "skill-trigger-cases.harness.mjs");
19
-
20
- const MUTATIONS = [
21
- [
22
- "a typoed declaration falls back to the default instead of throwing",
23
- [
24
- [
25
- SRC,
26
- 'if (declared === undefined && e?.code === "ERR_MODULE_NOT_FOUND") return [];',
27
- 'if (e?.code === "ERR_MODULE_NOT_FOUND") return [];',
28
- ],
29
- ],
30
- "a typoed declaration loaded quietly",
31
- "the undeclared/declared distinction — a mistyped path becomes a silently halved corpus",
32
- ],
33
- [
34
- "the consumer may shadow a case this package owns",
35
- [[SRC, "if (mine.has(c.skill))", "if (false && mine.has(c.skill))"]],
36
- "a shadowing case was accepted",
37
- "the one-truth rule: two tables answer «what should fire here» and the package's keeps looking authoritative",
38
- ],
39
- [
40
- "the consumer's cases are dropped entirely",
41
- [
42
- [
43
- SRC,
44
- "export const CASES = [...PACKAGE_CASES, ...(await consumerCases())];",
45
- "export const CASES = [...PACKAGE_CASES];",
46
- ],
47
- ],
48
- "the file at the default location was not merged",
49
- "the merge itself — every consumer measures precision against the package's skills alone",
50
- ],
51
- ];
52
-
53
- process.exit(
54
- runMutations({
55
- root: consumerRoot(),
56
- runner: "vigiles",
57
- cases: MUTATIONS.map(([name, edits, expect, disables]) => ({
58
- name,
59
- disables,
60
- edits,
61
- harness: HARNESS,
62
- expect,
63
- })),
64
- }),
65
- );