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
@@ -113,6 +113,7 @@ function lint(
113
113
  p: Paper,
114
114
  filename = `${PAPER}/paper.tex`,
115
115
  options: Json = {},
116
+ cwd?: string,
116
117
  ): Finding[] {
117
118
  const files = memoryFiles(paperFiles(p));
118
119
  const rules = venueRules({ files, venuesDir: VENUES });
@@ -123,6 +124,7 @@ function lint(
123
124
  ][]) {
124
125
  const visitor = rule.create({
125
126
  filename,
127
+ ...(cwd === undefined ? {} : { cwd }),
126
128
  sourceCode: { text: TEX },
127
129
  options:
128
130
  name === "geometry" && Object.keys(options).length ? [options] : [],
@@ -555,3 +557,61 @@ describe("paperlint's own config turns the venue rules on for every paper.tex",
555
557
  expect(OPTIONAL_RULES.has("pdf/last-page-balance")).toBe(true);
556
558
  });
557
559
  });
560
+
561
+ describe("a project's own preset, and the facts' other spellings", () => {
562
+ // A standalone preset (no `extends`, so it carries a `tex` block) that sets a body size with no
563
+ // tolerance, a reference range, two columns β€” and no page size and no font families.
564
+ const HOUSE = {
565
+ [`${PAPER}/house.jsonc`]: JSON.stringify({
566
+ tex: { packages: { acmart: ["acmart.cls"] } },
567
+ format: { columns: 2, body_pt: 9, ref_pt_min: 7, ref_pt_max: 8 },
568
+ }),
569
+ };
570
+ const house = (patch: (f: Json) => void = () => {}) =>
571
+ lint({
572
+ venue: { extends: "./house.jsonc" },
573
+ facts: withFacts(patch),
574
+ extra: HOUSE,
575
+ });
576
+
577
+ it("no page size in the preset: nothing to compare, whatever the PDF measures", () => {
578
+ expect(ids(house((f) => ((f.page_w_in = 5), (f.page_h_in = 5))))).toEqual(
579
+ [],
580
+ );
581
+ });
582
+
583
+ it("a body size with no tolerance is not judged; a reference range with none is judged exactly", () => {
584
+ expect(ids(house((f) => (f.body_pt = 12)))).toEqual([]);
585
+ // 6.9 pt passes agenticdev (its body_pt_tol widens the range) and fails here, where nothing does.
586
+ expect(ids(house((f) => (f.ref_pt = 6.9)))).toEqual(["pdf/limits:refPt"]);
587
+ });
588
+
589
+ it("facts naming the PDF by an absolute path are checked against that file", () => {
590
+ expect(
591
+ lint({
592
+ venue: DECL,
593
+ facts: withFacts((f) => (f.pdf = `${PAPER}/paper.pdf`)),
594
+ }),
595
+ ).toEqual([]);
596
+ });
597
+
598
+ it("a PDF with no fonts at all: each missing family says so", () => {
599
+ const fs = lint({ venue: DECL, facts: withFacts((f) => (f.fonts = [])) });
600
+ expect(ids(fs)).toEqual(["pdf/fonts:noFamily", "pdf/fonts:noFamily"]);
601
+ expect(fs[0]?.message).toMatch(/\(no fonts\)/);
602
+ });
603
+
604
+ it("file names in messages are relative to ESLint's cwd β€” and never empty", () => {
605
+ const at = (cwd: string) =>
606
+ lint(
607
+ { venue: { kind: "short" }, facts: null, pdf: null },
608
+ undefined,
609
+ {},
610
+ cwd,
611
+ )[0]?.message;
612
+ expect(at("/work")).toMatch(/set "extends" in papers\/p\/paperlint\.json/);
613
+ expect(at(`${PAPER}/paperlint.json`)).toMatch(
614
+ /set "extends" in \/work\/papers\/p\/paperlint\.json/,
615
+ );
616
+ });
617
+ });
@@ -253,17 +253,19 @@ function judgePages(g: FlatGeometry, resolved: Resolved): Finding[] {
253
253
  [kind.limits.bodyPagesMax, g.body_pages, "body pages"],
254
254
  [kind.limits.refPagesMax, g.ref_pages, "reference pages"],
255
255
  ];
256
- return pages
257
- .filter(([max, got]) => max !== null && got > max)
258
- .map(([max, got, what]) =>
259
- finding("pages", {
260
- what,
261
- got,
262
- max: max ?? 0,
263
- venue: resolved.venue,
264
- kind: kind.name,
265
- }),
266
- );
256
+ return pages.flatMap(([max, got, what]) =>
257
+ max !== null && got > max
258
+ ? [
259
+ finding("pages", {
260
+ what,
261
+ got,
262
+ max,
263
+ venue: resolved.venue,
264
+ kind: kind.name,
265
+ }),
266
+ ]
267
+ : [],
268
+ );
267
269
  }
268
270
 
269
271
  function judgeRefPt(g: FlatGeometry, resolved: Resolved): Finding[] {
@@ -1,162 +0,0 @@
1
- /**
2
- * Mutation battery over `action.yml` and `scripts/eslint-report-guard.mjs`.
3
- * Run: `node action.mutations.mjs` (not `vigiles test` β€” this is not a harness).
4
- *
5
- * πŸ”΄ EVERY MUTATION CARRIES A "THE PATCH LANDED" ASSERTION: the target must occur EXACTLY ONCE,
6
- * the mutant must be present in the file re-read from disk, the original text must be GONE, and
7
- * after the rollback the contents must return byte for byte. In this corpus a green mutation has
8
- * three times meant not "the defence holds" but "the patch never applied".
9
- *
10
- * A GREEN HARNESS UNDER A MUTATION is a finding about the TEST, not a conclusion about the defence.
11
- * Rank the causes: the wrong test Β· the patch did not land Β· the set is too narrow Β· THE RUN WAS
12
- * ALREADY RED BEFORE THE MUTATION. The last is why the baseline runs FIRST and its failure stops
13
- * the battery.
14
- *
15
- * ⚠️ Mutation 1 is the load-bearing one and it is deliberately asymmetric: it sends the step back
16
- * to calling `npx eslint` directly while LEAVING the words `paperlint lint` in the comments above. A
17
- * harness that grepped the file would stay green; this one parses the YAML and addresses the step
18
- * as a node, so it dies. That is the whole argument for the parser, made executable.
19
- *
20
- * It replaced a mutation over `--no-config-lookup`, which stopped having a target on 2026-09-18:
21
- * the flag went away with the eslint call, and the guarantee it carried moved INTO the CLI
22
- * (`overrideConfigFile: true`). The bypass it protected against is the same one β€” a consumer's
23
- * nested config deciding the rule set β€” so the case moved rather than disappeared.
24
- */
25
- import { readFileSync, writeFileSync } from "node:fs";
26
- import { execFileSync } from "node:child_process";
27
-
28
- const HARNESS = "action.harness.mjs";
29
-
30
- if (process.env.MUTATIONS_REPORT_COVERAGE) {
31
- console.log(`MUTATION-COVERS\t${HARNESS}`);
32
- process.exit(0);
33
- }
34
-
35
- const ACTION = "action.yml";
36
- const GUARD = "scripts/eslint-report-guard.mjs";
37
- const PRISTINE = new Map(
38
- [ACTION, GUARD].map((f) => [f, readFileSync(f, "utf8")]),
39
- );
40
- const restoreAll = () => {
41
- for (const [f, body] of PRISTINE) writeFileSync(f, body);
42
- };
43
-
44
- const M = [
45
- [
46
- ACTION,
47
- "the step goes back around the CLI to eslint (the words `paperlint lint` left in the comments)",
48
- "reinstate the bypass: the settings unread, the directory-structure check absent in CI, and a " +
49
- "consumer's nested config free to decide the rule set β€” while a grep still finds `paperlint lint`",
50
- "npx paperlint lint $PAPERLINT_PATHS",
51
- 'npx eslint --config "$PAPERLINT_CONFIG" $PAPERLINT_PATHS',
52
- ],
53
- [
54
- ACTION,
55
- "the guard is no longer called",
56
- "keep the lint run and drop the green-zero check β€” the job passes loudest when it measured nothing",
57
- 'node "$GITHUB_ACTION_PATH/scripts/eslint-report-guard.mjs" \\',
58
- "true # MUT \\",
59
- ],
60
- [
61
- ACTION,
62
- "texcount probed by the installer's exit code instead of by the binary",
63
- "trust an installer that can put nothing in place and report success",
64
- "command -v texcount >/dev/null || {",
65
- "true || {",
66
- ],
67
- [
68
- GUARD,
69
- "a report of ZERO linted files treated as clean",
70
- "reinstate the exact indistinguishability the guard exists to break",
71
- " if (res.length === 0) {",
72
- " if (false) { /* MUT */",
73
- ],
74
- [
75
- GUARD,
76
- "ESLint's return code masked",
77
- "swallow a real ESLint failure and report success",
78
- " return { code: eslintRc, lines };",
79
- " return { code: 0, lines }; /* MUT */",
80
- ],
81
- [
82
- GUARD,
83
- "an unparsable report treated as clean",
84
- "let a missing or corrupt report pass β€” nothing was measured either way",
85
- " lines.push(\n `::error::ESLint wrote no parsable report to ${reportPath} (rc=${eslintRc}). Nothing was measured.`,\n );\n return { code: 1, lines };",
86
- " return { code: 0, lines }; /* MUT */",
87
- ],
88
- ];
89
-
90
- const run = () => {
91
- try {
92
- return {
93
- ok: true,
94
- out: execFileSync("npx", ["vigiles", "test", HARNESS], {
95
- encoding: "utf8",
96
- stdio: "pipe",
97
- }),
98
- };
99
- } catch (e) {
100
- return { ok: false, out: `${e.stdout ?? ""}${e.stderr ?? ""}` };
101
- }
102
- };
103
-
104
- const base = run();
105
- if (!base.ok) {
106
- console.log(
107
- "❌ THE HARNESS IS RED BEFORE ANY MUTATION β€” the battery cannot tell a killed mutation from that:\n" +
108
- base.out.slice(-1200),
109
- );
110
- process.exit(1);
111
- }
112
-
113
- let ok = 0;
114
- let bad = 0;
115
- const rows = [];
116
-
117
- for (const [file, label, what, from, to] of M) {
118
- const pristine = PRISTINE.get(file);
119
- const hits = pristine.split(from).length - 1;
120
- if (hits !== 1) {
121
- console.log(
122
- `❌ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} in ${file}`,
123
- );
124
- bad++;
125
- continue;
126
- }
127
- writeFileSync(file, pristine.replace(from, to));
128
- const on = readFileSync(file, "utf8");
129
- if (!on.includes(to) || on.includes(from)) {
130
- console.log(
131
- `❌ ${label}: THE MUTATION DID NOT LAND (checked by re-reading ${file})`,
132
- );
133
- bad++;
134
- restoreAll();
135
- continue;
136
- }
137
- const res = run();
138
- restoreAll();
139
- if (readFileSync(file, "utf8") !== pristine) {
140
- console.log(`❌ ${label}: THE ROLLBACK FAILED on ${file}`);
141
- bad++;
142
- continue;
143
- }
144
- if (!res.ok) {
145
- ok++;
146
- const why =
147
- (res.out.match(/AssertionError[^\n]*?: ([^\n]*)/) ?? [])[1] ??
148
- "(the harness died)";
149
- rows.push([label, what, why.trim().slice(0, 100)]);
150
- } else {
151
- console.log(
152
- `πŸ”΄ ${label}: THE HARNESS IS GREEN UNDER THE MUTATION β€” a finding about the TEST, not a conclusion about the defence`,
153
- );
154
- bad++;
155
- }
156
- }
157
-
158
- console.log("\n| property | mutation | what the harness died on |");
159
- console.log("|---|---|---|");
160
- for (const r of rows) console.log(`| ${r[0]} | ${r[1]} | ${r[2]} |`);
161
- console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
162
- process.exit(bad === 0 ? 0 : 1);
@@ -1,62 +0,0 @@
1
- /**
2
- * Battery on the two `port/*-install-path` rules: four mutations, each removing one
3
- * load-bearing property β€” the detection itself, the markdown carriers, the template form on
4
- * the code side, and the ratchet.
5
- *
6
- * There is no fourth mutation here anymore β€” for the ratchet: the ratchet itself was removed.
7
- */
8
- import { fileURLToPath } from "node:url";
9
- import { dirname, join, resolve } from "node:path";
10
- import { runMutations } from "../lib/mutation-driver.mjs";
11
-
12
- const HERE = dirname(fileURLToPath(import.meta.url));
13
- const ROOT = resolve(HERE, "..");
14
- const RULE = join(HERE, "install-path-literals.mjs");
15
- const HARNESS = join(HERE, "install-path-literals.harness.mjs");
16
-
17
- process.exit(
18
- runMutations({
19
- root: ROOT,
20
- runner: "node",
21
- cases: [
22
- {
23
- name: "the detector stops finding anything",
24
- harness: HARNESS,
25
- expect: "expected at least three findings",
26
- disables:
27
- "the check itself β€” an install-specific path passes silently in both languages",
28
- edits: [
29
- [
30
- RULE,
31
- "for (const p of CHANNEL_PREFIXES) if (s.includes(p)) return p;",
32
- "void CHANNEL_PREFIXES;",
33
- ],
34
- ],
35
- },
36
- {
37
- name: "markdown stops seeing frontmatter and code",
38
- harness: HARNESS,
39
- expect: "expected at least three findings",
40
- disables:
41
- "the carriers in a skill β€” only prose is left, while the command lives in a fence and in allowed-tools",
42
- edits: [
43
- [
44
- RULE,
45
- "return { yaml: check, code: check, inlineCode: check, text: check };",
46
- "return { text: check };",
47
- ],
48
- ],
49
- },
50
- {
51
- name: "code stops seeing the template string",
52
- harness: HARNESS,
53
- expect: "expected one plain and one template finding",
54
- disables:
55
- "the second literal form β€” a path assembled by a template stays a path",
56
- edits: [
57
- [RULE, "TemplateElement(node) {", "TemplateElement_disabled(node) {"],
58
- ],
59
- },
60
- ],
61
- }),
62
- );
@@ -1,196 +0,0 @@
1
- /**
2
- * Mutation battery for the LaTeX language `eslint-rules/latex-language.mjs`.
3
- * Run: `node eslint-rules/latex-language.mutations.mjs` (not `vigiles test` β€” not a harness).
4
- *
5
- * πŸ”΄ EVERY MUTATION CARRIES A "THE PATCH LANDED" ASSERTION: the file is re-read from disk, the
6
- * mutant must be in it, the original text must be GONE (otherwise the target was not unique),
7
- * and after the rollback the contents must return byte for byte. In the repository this came
8
- * from, a green mutation has three times meant not "the defence holds" but "the patch never
9
- * applied".
10
- *
11
- * A GREEN HARNESS UNDER A MUTATION is a finding about the TEST, not a conclusion about the
12
- * defence. Rank the causes:
13
- * 1. the wrong test β€” it pins a side effect rather than the property itself;
14
- * 2. the patch did not land β€” removed by the assertion above;
15
- * 3. the set is too narrow β€” the property is observable in no fixture;
16
- * 4. πŸ”΄ THE RUN WAS ALREADY RED BEFORE THE MUTATION β€” then "killed" and "was failing anyway"
17
- * are indistinguishable.
18
- * Cause 4 is why the baseline run comes FIRST and why its failure stops the battery.
19
- *
20
- * ⚠️ WHAT IS NOT HERE ANY MORE. The original battery had a second group of three mutations
21
- * against the `case "html"` patch in three consumer rule modules β€” the patch that taught them to
22
- * recognise a `%` comment, not only an `<!--` one. Those modules were not extracted, so the
23
- * mutations have no target. The property itself did NOT disappear with them: the language still
24
- * trims both edges of a comment node, and the two mutations named
25
- * `comment/RIGHT edge` and `comment/LEFT edge` below cover the language's half of it. What is
26
- * genuinely gone is the evidence that a CONSUMER acts on it.
27
- */
28
- import { readFileSync, writeFileSync } from "node:fs";
29
- import { execFileSync } from "node:child_process";
30
-
31
- const HARNESS = "eslint-rules/latex-language.harness.mjs";
32
-
33
- // Coverage mode β€” see `scripts/run-mutations.mjs` guard 2 and the block in
34
- // `lib/mutation-driver.mjs`. Says what this battery can kill, without touching a file.
35
- if (process.env.MUTATIONS_REPORT_COVERAGE) {
36
- console.log(`MUTATION-COVERS\t${HARNESS}`);
37
- process.exit(0);
38
- }
39
-
40
- const LANG = "eslint-rules/latex-language.mjs";
41
- const PRISTINE = readFileSync(LANG, "utf8");
42
-
43
- const M = [
44
- [
45
- "synthesis of `## References` from `\\bibliography{}`",
46
- "stop declaring the bibliography a heading",
47
- ' if (node.content === "bibliography" && pos) {',
48
- " if (false && pos) { /* MUT */",
49
- ],
50
- [
51
- "synthesis of `## Abstract` from the environment",
52
- "stop declaring the abstract a heading",
53
- ' if (env === "abstract" && pos) {',
54
- " if (false && pos) { /* MUT */",
55
- ],
56
- [
57
- "preamble cut-off during synthesis",
58
- "treat preamble constructs (e.g. `\\AtBeginDocument{\\bibliography{}}`) as nodes of the document",
59
- ' if (pos && pos.start.offset < preEnd) {\n for (const k of ["content", "args"]) if (node[k]) walk(node[k]);\n return;\n }',
60
- " if (false) { return; } /* MUT */",
61
- ],
62
- [
63
- "preamble blanking",
64
- "leave the preamble (package list, author macros) visible as prose",
65
- " if (preEnd) {\n blank(0, preEnd);",
66
- " if (preEnd) {\n void 0; /* MUT */",
67
- ],
68
- [
69
- "comment/RIGHT edge trim",
70
- "leave the trailing `\\n` inside the comment node",
71
- ' while (\n ce > pos.start.offset &&\n (src[ce - 1] === "\\n" || src[ce - 1] === "\\r")\n )\n ce--;',
72
- " /* MUT */",
73
- ],
74
- [
75
- "comment/LEFT edge trim",
76
- "leave the preceding `\\n` inside the comment node",
77
- ' while (cs < ce && src[cs] !== "%") cs++;',
78
- " /* MUT */",
79
- ],
80
- [
81
- "`**` written in place of `\\te`",
82
- "write the `**` before the content, leaving the line to start with spaces",
83
- ' while (pendingStrong.length) {\n const o = pendingStrong.pop();\n chars[o] = "*";\n chars[o + 1] = "*";\n }',
84
- " while (pendingStrong.length) { pendingStrong.pop(); } /* MUT */",
85
- ],
86
- [
87
- "`\\texttt{X}` β†’ `` `X` ``",
88
- "stop reproducing the one markdown markup the projection can express",
89
- ' if (node.content === "texttt" || node.content === "lstinline") {',
90
- " if (false) { /* MUT */",
91
- ],
92
- [
93
- "heading projected to ATX",
94
- "stop writing `#` where `\\section` was",
95
- ' for (let i = 0; i < depth; i++) chars[pos.start.offset + i] = "#";',
96
- ' for (let i = 0; i < 0; i++) chars[pos.start.offset + i] = "#"; /* MUT */',
97
- ],
98
- [
99
- "OPAQUE macro blanking",
100
- "leave `\\cite` / `\\label` / `\\input` as prose",
101
- " if (OPAQUE.test(node.content)) {\n blank(pos.start.offset, argEnd(node).offset + 1);\n return;\n }",
102
- " if (false) { return; } /* MUT */",
103
- ],
104
- [
105
- "math blanking",
106
- "leave formulas as prose",
107
- ' if (\n (node.type === "inlinemath" ||\n node.type === "displaymath" ||\n node.type === "verbatim") &&\n pos\n ) {',
108
- " if (false) { /* MUT */",
109
- ],
110
- [
111
- "float prose kept (caption / footnote)",
112
- "swallow the caption together with the float again β€” the double lock returns",
113
- "const FLOAT_PROSE = /^(caption|footnote)$/;",
114
- "const FLOAT_PROSE = /^(?!)$/; /* MUT */",
115
- ],
116
- ];
117
-
118
- const runHarness = () => {
119
- try {
120
- execFileSync("npx", ["vigiles", "test", HARNESS], {
121
- encoding: "utf8",
122
- stdio: ["ignore", "pipe", "pipe"],
123
- });
124
- return { failed: false, out: "" };
125
- } catch (e) {
126
- return { failed: true, out: (e.stdout ?? "") + (e.stderr ?? "") };
127
- }
128
- };
129
-
130
- // πŸ”΄ A BASELINE RUN BEFORE ANY MUTATION β€” see cause 4 in the header.
131
- {
132
- const base = runHarness();
133
- if (base.failed) {
134
- console.log(
135
- "❌ THE HARNESS IS RED BEFORE ANY MUTATION β€” the battery cannot tell a killed mutation " +
136
- "from that:\n" +
137
- base.out.slice(-1500),
138
- );
139
- process.exit(1);
140
- }
141
- }
142
-
143
- let ok = 0;
144
- let bad = 0;
145
- const rows = [];
146
- for (const [label, what, from, to] of M) {
147
- const hits = PRISTINE.split(from).length - 1;
148
- if (hits !== 1) {
149
- console.log(
150
- `❌ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} β€” ${from.slice(0, 70)}`,
151
- );
152
- bad++;
153
- continue;
154
- }
155
- writeFileSync(LANG, PRISTINE.replace(from, to));
156
- const on = readFileSync(LANG, "utf8");
157
- if (!on.includes(to) || on.includes(from)) {
158
- console.log(
159
- `❌ ${label}: THE MUTATION DID NOT LAND (checked by re-reading the file)`,
160
- );
161
- bad++;
162
- writeFileSync(LANG, PRISTINE);
163
- continue;
164
- }
165
- const { failed, out } = runHarness();
166
- writeFileSync(LANG, PRISTINE);
167
- if (readFileSync(LANG, "utf8") !== PRISTINE) {
168
- console.log(`❌ ${label}: THE ROLLBACK FAILED`);
169
- bad++;
170
- continue;
171
- }
172
- const at = (out.match(/latex-language\.harness\.mjs:(\d+)/) ?? [])[1];
173
- const msg =
174
- (out.match(/AssertionError[^:]*: ([^\n]+)/) ?? [])[1] ??
175
- (out.match(/((?:Error|ENOENT)[^\n]*)/) ?? [])[1] ??
176
- (out.match(
177
- /([^\n]*(?:must|drifted|was lost|is stale|leaked|swallowed)[^\n]*)/,
178
- ) ?? [])[1] ??
179
- "";
180
- const where = (at ? `harness:${at} Β· ` : "") + msg;
181
- if (failed) {
182
- ok++;
183
- rows.push([label, what, where.trim().slice(0, 130)]);
184
- } else {
185
- console.log(
186
- `πŸ”΄ ${label}: THE HARNESS IS GREEN UNDER THE MUTATION β€” a finding about the TEST, not a conclusion about the defence`,
187
- );
188
- bad++;
189
- }
190
- }
191
- console.log("\n| property | mutation | what the harness died on |");
192
- console.log("|---|---|---|");
193
- for (const [a, b, c] of rows)
194
- console.log(`| \`${a}\` | ${b} | ${c.replace(/\|/g, "\\|")} |`);
195
- console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
196
- process.exit(bad ? 1 : 0);
@@ -1,127 +0,0 @@
1
- /**
2
- * Battery for `paper/research-question` β€” five mutations, each removes ITS OWN load-bearing
3
- * property.
4
- *
5
- * πŸ”΄ Why the battery matters here specifically. The rule's success state is SILENCE, and on the
6
- * live corpus it only produces two findings out of four papers. "It passed" and "it cannot fire"
7
- * look identical from the outside, and only this tells them apart.
8
- *
9
- * Two mutations do not target the finding but the SCOPE and the LANGUAGE β€” the halves a test
10
- * forgets. Without the stage gate the rule scolds every draft; without markdown it loses
11
- * `compile-rules`, i.e. half of the real findings, while staying green.
12
- */
13
- import { fileURLToPath } from "node:url";
14
- import { dirname, join, resolve } from "node:path";
15
- import { runMutations } from "../lib/mutation-driver.mjs";
16
-
17
- const HERE = dirname(fileURLToPath(import.meta.url));
18
- const ROOT = resolve(HERE, "..");
19
- const RULE = join(HERE, "paper-research-question.mjs");
20
- const HARNESS = join(HERE, "paper-research-question.harness.mjs");
21
-
22
- process.exit(
23
- runMutations({
24
- root: ROOT,
25
- runner: "node",
26
- cases: [
27
- {
28
- name: "the rule stops noticing that nothing was declared",
29
- harness: HARNESS,
30
- expect: "shipped and no question β€” a finding",
31
- disables:
32
- "the subject itself: the rule stays silent across the whole corpus, and silence is " +
33
- "its success state β€” from the outside a disabled rule is indistinguishable from a clean paper",
34
- edits: [[RULE, 'if (question.trim() === "") {', "if (false) {"]],
35
- },
36
- {
37
- // πŸ”΄ The half that did not exist before 2026-09-19, and the reason the redesign happened.
38
- // Without this case the second step could be deleted and the battery would stay green on
39
- // the strength of the first β€” which is how a two-step check quietly becomes a one-step one.
40
- name: "the CONTENT check is dropped β€” a declaration nobody carries passes",
41
- harness: HARNESS,
42
- expect:
43
- "declared but absent from the paper β€” a finding, where the old pattern was silent",
44
- disables:
45
- "step two. The scorecard would only have to SAY a question exists, never to have it in " +
46
- "the paper β€” which is the checklist the `bytes` field exists to not be. The predecessor " +
47
- "failed the same way from the other side: it matched Β«we leave the research question to " +
48
- "future workΒ» and reported a paper that states no question as clean",
49
- edits: [
50
- [
51
- RULE,
52
- "if (flatten(raw).includes(flatten(question))) return;",
53
- "return;",
54
- ],
55
- ],
56
- },
57
- {
58
- name: "the STAGE GATE is removed β€” drafts get scolded too",
59
- harness: HARNESS,
60
- expect:
61
- "a draft (no stages) β€” silent, even though it has no question either",
62
- disables:
63
- "the scope. The rule does not ask 'is there a question', it asks 'is there a question " +
64
- "FOR SOMETHING SHIPPED'. Without the gate every draft gets a finding β€” and a rule that " +
65
- "scolds drafts gets turned off within a week, at which point both real findings disappear too",
66
- edits: [
67
- [
68
- RULE,
69
- "if (stages.length === 0) return; // not shipped β€” owes nothing",
70
- "",
71
- ],
72
- ],
73
- },
74
- {
75
- name: "the LANGUAGE narrows to LaTeX β€” a markdown paper becomes invisible",
76
- harness: HARNESS,
77
- expect: "a paper in markdown is checked the same way",
78
- disables:
79
- "the second half of the language. For the `tex/latex` language the text sits in `raw`, " +
80
- "for markdown it sits in `text`. The mutation keeps only the first, and " +
81
- "`compile-rules-2026` (paper written in markdown, no question) stops being found β€” HALF " +
82
- "of the live corpus's findings disappear silently, and the run stays green",
83
- edits: [
84
- [
85
- RULE,
86
- "const raw = context.sourceCode.raw ?? context.sourceCode.text;",
87
- "const raw = context.sourceCode.raw;",
88
- ],
89
- ],
90
- },
91
- {
92
- name: "the stage list is taken from somewhere OTHER than the field again",
93
- harness: HARNESS,
94
- expect:
95
- "the stage list in the message comes from the field and carries BOTH",
96
- disables:
97
- "the whole reason the move was made. The predecessor derived the stage with a regex " +
98
- "over the scorecard's prose and on agenticdev printed `submitted` where `submitted, " +
99
- "camera-ready` was declared. The mutation does not change the set of findings β€” only " +
100
- "the TEXT lies, and without its own assert the regression would have passed silently",
101
- edits: [
102
- [
103
- RULE,
104
- 'data: { stages: stages.join("/") }',
105
- 'data: { stages: "submitted" }',
106
- ],
107
- ],
108
- },
109
- {
110
- name: "the scorecard's name stops arriving as an option",
111
- harness: HARNESS,
112
- expect:
113
- "and with a nonexistent scorecard a shipped paper is also silent β€” the stage gate is load-bearing",
114
- disables:
115
- "the boundary 'the mechanism goes in the package, the data stays with the consumer'. " +
116
- "`PIPELINE-STATUS.md` is ONE repository's convention, and hardcoding it makes the rule unusable for everyone else",
117
- edits: [
118
- [
119
- RULE,
120
- 'const statusName =\n context.options?.[0]?.statusFile ?? "PIPELINE-STATUS.md";',
121
- 'const statusName =\n "PIPELINE-STATUS.md";',
122
- ],
123
- ],
124
- },
125
- ],
126
- }),
127
- );