paperlint 2.1.0 → 3.1.0

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 (219) hide show
  1. package/CLAUDE.md +6 -1
  2. package/README.md +243 -235
  3. package/action.yml +4 -6
  4. package/dist/adapters/references/index.d.ts +13 -0
  5. package/dist/adapters/references/index.d.ts.map +1 -0
  6. package/dist/adapters/references/index.js +55 -0
  7. package/dist/adapters/references/index.js.map +1 -0
  8. package/dist/adapters/references/reach.io.d.ts +3 -0
  9. package/dist/adapters/references/reach.io.d.ts.map +1 -0
  10. package/dist/adapters/references/reach.io.js +14 -0
  11. package/dist/adapters/references/reach.io.js.map +1 -0
  12. package/dist/build-engine.d.ts.map +1 -1
  13. package/dist/build-engine.js +6 -2
  14. package/dist/build-engine.js.map +1 -1
  15. package/dist/build.d.ts +15 -0
  16. package/dist/build.d.ts.map +1 -1
  17. package/dist/build.js +60 -7
  18. package/dist/build.js.map +1 -1
  19. package/dist/cli.d.ts +94 -30
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +500 -288
  22. package/dist/cli.js.map +1 -1
  23. package/dist/doctor.d.ts.map +1 -1
  24. package/dist/doctor.js +40 -72
  25. package/dist/doctor.js.map +1 -1
  26. package/dist/engine.d.ts +9 -0
  27. package/dist/engine.d.ts.map +1 -1
  28. package/dist/engine.js +27 -0
  29. package/dist/engine.js.map +1 -1
  30. package/dist/facts-file.d.ts +2 -2
  31. package/dist/facts-file.js +2 -2
  32. package/dist/hooks-settings.d.ts +0 -21
  33. package/dist/hooks-settings.d.ts.map +1 -1
  34. package/dist/hooks-settings.js +17 -135
  35. package/dist/hooks-settings.js.map +1 -1
  36. package/dist/init.d.ts +39 -29
  37. package/dist/init.d.ts.map +1 -1
  38. package/dist/init.js +144 -173
  39. package/dist/init.js.map +1 -1
  40. package/dist/link-skills.d.ts +1 -1
  41. package/dist/link-skills.d.ts.map +1 -1
  42. package/dist/link-skills.js +11 -29
  43. package/dist/link-skills.js.map +1 -1
  44. package/dist/new-paper.d.ts +8 -1
  45. package/dist/new-paper.d.ts.map +1 -1
  46. package/dist/new-paper.js +49 -25
  47. package/dist/new-paper.js.map +1 -1
  48. package/dist/paper-files.d.ts +85 -0
  49. package/dist/paper-files.d.ts.map +1 -0
  50. package/dist/paper-files.js +96 -0
  51. package/dist/paper-files.js.map +1 -0
  52. package/dist/paper-settings.d.ts +13 -41
  53. package/dist/paper-settings.d.ts.map +1 -1
  54. package/dist/paper-settings.js +71 -71
  55. package/dist/paper-settings.js.map +1 -1
  56. package/dist/ports/check-references.d.ts +31 -0
  57. package/dist/ports/check-references.d.ts.map +1 -0
  58. package/dist/ports/check-references.js +7 -0
  59. package/dist/ports/check-references.js.map +1 -0
  60. package/dist/presets.d.ts +1 -1
  61. package/dist/presets.d.ts.map +1 -1
  62. package/dist/presets.js +4 -6
  63. package/dist/presets.js.map +1 -1
  64. package/dist/reference-rules.d.ts +47 -0
  65. package/dist/reference-rules.d.ts.map +1 -0
  66. package/dist/reference-rules.js +156 -0
  67. package/dist/reference-rules.js.map +1 -0
  68. package/dist/references.d.ts +36 -0
  69. package/dist/references.d.ts.map +1 -0
  70. package/dist/references.js +91 -0
  71. package/dist/references.js.map +1 -0
  72. package/dist/rules-config.d.ts +5 -3
  73. package/dist/rules-config.d.ts.map +1 -1
  74. package/dist/rules-config.js +15 -6
  75. package/dist/rules-config.js.map +1 -1
  76. package/dist/structure.js +3 -3
  77. package/dist/structure.js.map +1 -1
  78. package/dist/tex-requirements.js +2 -2
  79. package/dist/tex-requirements.js.map +1 -1
  80. package/dist/toolchain.d.ts +5 -1
  81. package/dist/toolchain.d.ts.map +1 -1
  82. package/dist/toolchain.js +22 -5
  83. package/dist/toolchain.js.map +1 -1
  84. package/dist/types.d.ts +26 -18
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/venue-rules.d.ts.map +1 -1
  87. package/dist/venue-rules.js +12 -10
  88. package/dist/venue-rules.js.map +1 -1
  89. package/docs/configuration.md +163 -104
  90. package/docs/e2e.md +1 -1
  91. package/docs/install.md +25 -40
  92. package/docs/optional-rules.md +5 -4
  93. package/docs/rules.md +65 -24
  94. package/docs/skills.md +80 -0
  95. package/eslint-rules/bib-reachable-entry.mjs +75 -0
  96. package/eslint-rules/latex-language.mjs +29 -1
  97. package/eslint-rules/paper-stages.harness.mjs +1 -117
  98. package/eslint-rules/paper-stages.mjs +0 -120
  99. package/eslint-rules/paper-stages.mutations.mjs +0 -76
  100. package/eslint-rules/paper-typography.mjs +306 -191
  101. package/eslint-rules/paper-typography.test.mjs +337 -0
  102. package/eslint-rules/papers.harness.mjs +8 -12
  103. package/eslint-rules/papers.mjs +17 -31
  104. package/eslint-rules/pdf-last-page-balance.mjs +4 -7
  105. package/eslint-rules/review-frontmatter.mjs +118 -0
  106. package/eslint-rules/review-frontmatter.schema.json +36 -0
  107. package/eslint-rules/review-frontmatter.test.mjs +118 -0
  108. package/eslint-rules/sibling-frontmatter.mjs +19 -0
  109. package/eslint-rules/sibling-frontmatter.schema.json +14 -0
  110. package/eslint.config.mjs +0 -15
  111. package/fixtures/real-markdown-paper/baseline.json +6 -7
  112. package/fixtures/toolchain-mirror/catalog.txt +1 -0
  113. package/fixtures/toolchain-mirror/install-tl +4 -1
  114. package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
  115. package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
  116. package/hooks/hooks.harness.mjs +39 -98
  117. package/hooks/hooks.mutations.mjs +6 -6
  118. package/hooks/paper-edit-guard.hook.d.mts +2 -3
  119. package/hooks/paper-edit-guard.hook.mjs +31 -42
  120. package/hooks/paper-skills-nudge.hook.mjs +21 -19
  121. package/hooks/paper-status-gates.hook.mjs +21 -19
  122. package/hooks/paper-status-gates.sh +8 -13
  123. package/lib/paper-config.d.mts +15 -25
  124. package/lib/paper-config.harness.mjs +18 -35
  125. package/lib/paper-config.mjs +83 -105
  126. package/lib/paper-config.mutations.mjs +2 -2
  127. package/lib/skill-checks.mjs +1 -1
  128. package/lib/skill-trigger-cases.harness.mjs +9 -8
  129. package/lib/skill-trigger-cases.mjs +7 -15
  130. package/package.json +1 -1
  131. package/scripts/eslint-report-guard.mjs +1 -1
  132. package/scripts/harness-api.frozen.json +0 -3
  133. package/scripts/mutation-batteries.frozen.json +5 -14
  134. package/skills/analyze-sibling-paper/SKILL.md +16 -12
  135. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
  136. package/skills/cold-read-diff/SKILL.md +20 -1
  137. package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
  138. package/skills/grade-paper-writing/SKILL.md +20 -1
  139. package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
  140. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
  141. package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
  142. package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
  143. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
  144. package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
  145. package/skills/plan-paper-timeline/SKILL.md +2 -2
  146. package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
  147. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
  148. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
  149. package/skills/render-paper/check-render.sh +2 -3
  150. package/skills/render-paper/render-paper.harness.mjs +2 -2
  151. package/skills/verify-citations/SKILL.md +10 -1
  152. package/skills/verify-citations/SKILL.md.spec.ts +9 -0
  153. package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
  154. package/src/adapters/references/index.ts +102 -0
  155. package/src/adapters/references/reach.io.ts +12 -0
  156. package/src/build-engine.ts +6 -1
  157. package/src/build.harness.mjs +54 -6
  158. package/src/build.ts +89 -11
  159. package/src/cli.harness.mjs +149 -294
  160. package/src/cli.mutations.mjs +35 -44
  161. package/src/cli.ts +637 -328
  162. package/src/doctor.harness.mjs +41 -84
  163. package/src/doctor.mutations.mjs +9 -9
  164. package/src/doctor.ts +44 -89
  165. package/src/engine.harness.mjs +50 -0
  166. package/src/engine.ts +35 -0
  167. package/src/facts-file.test.ts +0 -7
  168. package/src/facts-file.ts +2 -2
  169. package/src/hooks-settings.harness.mjs +7 -92
  170. package/src/hooks-settings.mutations.mjs +2 -17
  171. package/src/hooks-settings.ts +18 -162
  172. package/src/init.test.ts +206 -0
  173. package/src/init.ts +187 -231
  174. package/src/link-skills.harness.mjs +1 -38
  175. package/src/link-skills.mutations.mjs +9 -5
  176. package/src/link-skills.ts +10 -34
  177. package/src/lint-fix.test.ts +120 -0
  178. package/src/linted-set.test.ts +118 -0
  179. package/src/new-paper.harness.mjs +2 -0
  180. package/src/new-paper.mutations.mjs +0 -14
  181. package/src/new-paper.test.ts +173 -14
  182. package/src/new-paper.ts +82 -30
  183. package/src/paper-files.test.ts +395 -0
  184. package/src/paper-files.ts +200 -0
  185. package/src/paper-settings-commands.test.ts +121 -122
  186. package/src/paper-settings.test.ts +93 -64
  187. package/src/paper-settings.ts +99 -97
  188. package/src/parse-args.test.ts +48 -0
  189. package/src/ports/check-references.ts +28 -0
  190. package/src/presets.ts +5 -15
  191. package/src/reference-rules.ts +229 -0
  192. package/src/references.test.ts +209 -0
  193. package/src/references.ts +138 -0
  194. package/src/rules-config.harness.mjs +24 -8
  195. package/src/rules-config.ts +19 -7
  196. package/src/rules-off.test.ts +111 -0
  197. package/src/structure.harness.mjs +4 -2
  198. package/src/structure.ts +3 -3
  199. package/src/tex-requirements.ts +2 -2
  200. package/src/toolchain.harness.mjs +41 -6
  201. package/src/toolchain.ts +29 -5
  202. package/src/types.ts +36 -18
  203. package/src/venue-rules.test.ts +23 -11
  204. package/src/venue-rules.ts +12 -14
  205. package/templates/paper/paperlint.json +1 -1
  206. package/eslint-rules/doc-fields.harness.mjs +0 -336
  207. package/eslint-rules/doc-fields.mjs +0 -186
  208. package/eslint-rules/doc-fields.mutations.mjs +0 -96
  209. package/eslint-rules/paper-typography.harness.mjs +0 -291
  210. package/eslint-rules/paper-typography.mutations.mjs +0 -131
  211. package/eslint-rules/review-findings-cause.harness.mjs +0 -228
  212. package/eslint-rules/review-findings-cause.mjs +0 -135
  213. package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
  214. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
  215. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
  216. package/fixtures/review-findings-cause/clean.md +0 -17
  217. package/fixtures/review-findings-cause/defect.md +0 -14
  218. package/fixtures/review-findings-cause/old-debt.md +0 -14
  219. package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
@@ -45,7 +45,7 @@ const since = () => {
45
45
 
46
46
  assert.deepEqual(
47
47
  Object.keys(stages.rules).sort(),
48
- ["author-list", "source", "stages"],
48
+ ["source", "stages"],
49
49
  "rule set changed",
50
50
  );
51
51
 
@@ -237,120 +237,4 @@ console.log(
237
237
  `✓ ${String(since())} assertions passed — paper/source, frozen bytes instead of a sha`,
238
238
  );
239
239
 
240
- // ── `paper/author-list`: a shipped paper owes itself a run of the author-list cross-check ────
241
- //
242
- // This rule's subject is DIFFERENT from its two neighbors above: those check the declaration
243
- // against the bytes, this one checks the declaration against a record of a run. What they share
244
- // is exactly one input, the `stages` field, and that's why the rule lives in this module.
245
- {
246
- const lintAuthors = (name, opts = {}) => {
247
- const file = join(FIX, name, "PIPELINE-STATUS.md");
248
- const msgs = linter.verify(
249
- readFileSync(file, "utf-8"),
250
- [
251
- {
252
- files: ["**/*.md"],
253
- plugins: { markdown, paper: stages },
254
- language: "markdown/gfm",
255
- languageOptions: { frontmatter: "yaml" },
256
- rules: { "paper/author-list": ["error", opts] },
257
- },
258
- ],
259
- file,
260
- );
261
- assert.deepEqual(
262
- msgs.filter((m) => m.fatal),
263
- [],
264
- `${name}: the rule threw`,
265
- );
266
- return msgs.map((m) => m.message);
267
- };
268
-
269
- // ── stays silent where it must ──
270
- check(
271
- "a run is recorded in the scorecard — silent",
272
- lintAuthors("authors-ran").length === 0,
273
- );
274
- // A draft never asked anyone to read it, so it owes nothing. This is not a carve-out: the
275
- // rule's subject is the DEBT of a shipped paper, and an unshipped one has no debt.
276
- check(
277
- "no stage declared at all — silent, a draft owes nothing",
278
- lintAuthors("nothing").length === 0,
279
- );
280
- // An empty list is not "a stage exists": the record `stages: []` shows up on a paper that was
281
- // set up but never submitted anywhere.
282
- check(
283
- "an empty stage list — silent",
284
- linter.verify(
285
- "---\nstages: []\n---\n# S\n",
286
- [
287
- {
288
- files: ["**/*.md"],
289
- plugins: { markdown, paper: stages },
290
- language: "markdown/gfm",
291
- languageOptions: { frontmatter: "yaml" },
292
- rules: { "paper/author-list": "error" },
293
- },
294
- ],
295
- join(FIX, "x", "PIPELINE-STATUS.md"),
296
- ).length === 0,
297
- );
298
-
299
- // ── fires on a planted defect ──
300
- const owed = lintAuthors("ok");
301
- check("a stage is declared, no run — a finding", owed.length === 1);
302
- // 🔴 The message must name the CLASS, not just the fact of a miss: otherwise the reader takes
303
- // it for a duplicate of the citation-existence check and closes it as noise. The class is
304
- // preprint authors under a declared conference, and it is invisible to a check that a
305
- // citation merely resolves.
306
- check(
307
- "and it names the class the cross-check catches, not just the miss",
308
- /PREPRINT/.test(owed[0]),
309
- );
310
-
311
- // ── the whole reason the move was made ──
312
- // The predecessor derived the stage with a REGEX OVER THE SCORECARD'S PROSE. Remeasured
313
- // 09-17: for `agenticdev-2026` the prose reads `submitted`, while the frontmatter reads
314
- // `submitted, camera-ready`. Here the list comes from the field, so both stages land in the
315
- // finding's text.
316
- const two = lintAuthors("twice");
317
- check(
318
- "the stage list in the message comes from the FIELD and carries all of them",
319
- two.length === 1 && /submitted\/submitted/.test(two[0]),
320
- );
321
-
322
- // ── consumer data stays with the consumer ──
323
- // The predecessor hardcoded the path `.claude/skills/verify-citations/...` into the message
324
- // text — the address of ONE repository inside a public package.
325
- const withCmd = lintAuthors("ok", {
326
- command: "node scripts/bib-authors.mjs <paper>",
327
- });
328
- check(
329
- "the run command comes in as an option and lands in the message",
330
- /scripts\/bib-authors\.mjs/.test(withCmd[0]),
331
- );
332
- check(
333
- "and without the option the message does not invent a path",
334
- !/bib-authors\.mjs/.test(owed[0]),
335
- );
336
- // The marker is also data: the package cannot know EXACTLY how a given consumer records a run.
337
- check(
338
- "the marker is configurable — with a different marker the same paper becomes a debtor",
339
- lintAuthors("authors-ran", { marker: "no-such-marker" }).length === 1,
340
- );
341
-
342
- // ── parsing versus grep: the one case where they diverge ──
343
- // 🔴 This assert is the proof of the move to a parser. All the fixtures above pass IDENTICALLY
344
- // either way, because their marker sits in a cell. Here it sits in PROSE — "still need to run
345
- // bib-authors", an intention, not a record — and grep would read it as evidence of a run. The
346
- // evidence must sit in the scorecard.
347
- check(
348
- "a marker in prose OUTSIDE the table is not a record of a run",
349
- lintAuthors("marker-in-prose").length === 1,
350
- );
351
- }
352
- console.log(
353
- `✓ ${String(since())} assertions passed — paper/author-list, the debt of a shipped paper`,
354
- );
355
-
356
240
  console.log(`✓ ${String(n)} assertions passed in total`);
@@ -254,126 +254,6 @@ export default {
254
254
  * because a stage frozen BEFORE this convention existed cannot be fixed at all — those
255
255
  * bytes are gone. Failing a build over unrecoverable history is a gate nobody can clear.
256
256
  */
257
- /**
258
- * `paper/author-list` — a shipped paper OWES ITSELF a run of the author-list cross-check.
259
- *
260
- * The class this cross-check catches is invisible to an existence check on citations: the
261
- * citation exists, the identifier resolves, and the authors are taken from the PREPRINT
262
- * while the entry declares a conference. On the live corpus this turned up seven records
263
- * across three papers, including a DROPPED LIVING PERSON (`schick2023toolformer` — Eric
264
- * Hambro is missing; the NeurIPS version has nine authors, the preprint has eight). Two of
265
- * the seven were found on a paper ALREADY SUBMITTED.
266
- *
267
- * 🔴 WHAT THE MOVE FIXED, and this is a measurement, not taste. The predecessor derived the
268
- * declared stage with a REGEX OVER THE PROSE of that same file. Remeasured 2026-09-17 on the
269
- * live corpus: for `agenticdev-2026` the prose reads `submitted`, the frontmatter declares
270
- * `submitted, camera-ready` — the pattern `/camera-ready (?:uploaded|submitted|shipped)/i`
271
- * does not catch the form the stage is actually recorded in. Today's set of findings did not
272
- * change from this (both checks only asked "is there ANY stage at all"), but the MESSAGE
273
- * printed the wrong list of stages. The subject here is the same `stages` field that
274
- * `paper/stages` already checks against the bytes in both directions.
275
- *
276
- * 🔴 THE MARKER IS LOOKED FOR IN TABLE CELLS, NOT GREPPED OVER THE FILE. The first draft did
277
- * `context.sourceCode.text.includes(marker)` and justified it with a comment saying "a
278
- * footnote cell has no node of its own". That turned out to be SIMPLY WRONG — a 2026-09-17
279
- * measurement showed the markdown parser hands back `table`, `tableRow` and `tableCell`
280
- * (twelve cells on a three-row table). The base rule says, word for word, "parse markdown
281
- * with a parser".
282
- *
283
- * Parsing is also STRICTER than grep, and the difference is real: a marker mentioned in
284
- * prose outside the scorecard — in a heading, in a paragraph saying "still need to run
285
- * bib-authors", in a quote from someone else — no longer counts as a record of a run. To
286
- * grep, those three cases are indistinguishable from a genuine record.
287
- *
288
- * ⚠️ WHAT THIS STILL DOES NOT FIX: inside the cell the evidence is still PROSE, and a run
289
- * phrased in different words will not be seen by the rule. The real fix is a frontmatter
290
- * field (`gates.cites.ran`), not a smarter text search; that's separate work, touching four
291
- * live scorecards.
292
- *
293
- * So severity is set by the CONSUMER, and the default is not `error`: the proof of a run is
294
- * prose, and a false positive at a blocking level costs more than a miss.
295
- */
296
- "author-list": {
297
- meta: {
298
- type: "suggestion",
299
- docs: {
300
- description:
301
- "a paper that declares a stage records that the author-list check ran — a class the existence check cannot see",
302
- },
303
- schema: [
304
- {
305
- type: "object",
306
- properties: {
307
- // The run marker in the scorecard. This is consumer data: the package cannot
308
- // know EXACTLY how a given consumer records that the cross-check happened.
309
- marker: { type: "string" },
310
- // What to run. This is a CONSUMER-SPECIFIC PATH, and it has no place in a public
311
- // package: the predecessor hardcoded
312
- // `.claude/skills/verify-citations/scripts/bib-authors.mjs` right into the message text.
313
- command: { type: "string" },
314
- },
315
- additionalProperties: false,
316
- },
317
- ],
318
- messages: {
319
- neverRan:
320
- "stage «{{stages}}» is declared, but the scorecard records no author-list run (looked for «{{marker}}» in its table). It catches what an existence check cannot see: the citation resolves, the id resolves, and the authors are the PREPRINT's while the entry declares a conference{{how}}",
321
- },
322
- },
323
- create(context) {
324
- const opts = context.options?.[0] ?? {};
325
- const marker = opts.marker ?? "bib-authors";
326
- const command = opts.command ?? "";
327
-
328
- // The verdict is deferred to the end of the file: the frontmatter node arrives FIRST,
329
- // and the table comes after it. Reporting on `yaml` would mean a verdict rendered
330
- // without ever seeing the scorecard.
331
- let declaredAt = null;
332
- let stages = [];
333
- let recorded = false;
334
-
335
- return {
336
- yaml(node) {
337
- let data;
338
- try {
339
- data = load(node.value ?? "");
340
- } catch {
341
- return; // `paper/stages` has already reported the unreadable YAML
342
- }
343
- const raw = data?.stages;
344
- if (!Array.isArray(raw)) return;
345
- stages = raw.map((r) => r?.stage).filter(Boolean);
346
- // One guard, not two: `raw.length === 0` would be a SPECIAL CASE of this same
347
- // condition, and a mutation against it would turn out unkillable — the second guard
348
- // mutes it. This also covers a non-empty list made of records with no `stage` field.
349
- if (stages.length === 0) return; // nothing shipped — nothing is owed
350
- declaredAt = node;
351
- },
352
-
353
- // The evidence is a SCORECARD CELL, not any occurrence of the string in the file. It
354
- // has a node of its own; the first draft claimed otherwise and grepped the whole text.
355
- tableCell(node) {
356
- if (recorded) return;
357
- if (context.sourceCode.getText(node).includes(marker))
358
- recorded = true;
359
- },
360
-
361
- "root:exit"() {
362
- if (!declaredAt || recorded) return;
363
- context.report({
364
- node: declaredAt,
365
- messageId: "neverRan",
366
- data: {
367
- stages: stages.join("/"),
368
- marker,
369
- how: command ? `. Run: ${command}` : "",
370
- },
371
- });
372
- },
373
- };
374
- },
375
- },
376
-
377
257
  source: {
378
258
  meta: {
379
259
  type: "problem",
@@ -76,82 +76,6 @@ process.exit(
76
76
  ],
77
77
  ],
78
78
  },
79
- {
80
- // 🔴 A REGRESSION BACK TO GREP. The rule's first draft did exactly this and justified it
81
- // by saying "a footnote cell has no node of its own" — a measurement showed the
82
- // opposite: the parser hands back `tableCell`. The mutation is caught by ONE fixture out
83
- // of six: in the rest the marker already sits in a cell, so grep and parsing are
84
- // indistinguishable. Without `marker-in-prose` this regression would have passed
85
- // silently, and the rule would again read an intention as a fact.
86
- name: "the evidence is grepped over the WHOLE FILE again",
87
- harness: HARNESS,
88
- expect: "a marker in prose OUTSIDE the table is not a record of a run",
89
- disables:
90
- "parsing in favor of a string search: 'still need to run bib-authors' — an intention, " +
91
- "not a record — counts as a run again, and the paper silently stops being a debtor",
92
- edits: [
93
- [
94
- RULE,
95
- "if (context.sourceCode.getText(node).includes(marker))\n recorded = true;",
96
- "if (context.sourceCode.text.includes(marker))\n recorded = true;",
97
- ],
98
- ],
99
- },
100
- {
101
- name: "the rule stops requiring an author cross-check run",
102
- harness: HARNESS,
103
- expect: "a stage is declared, no run — a finding",
104
- disables:
105
- "the debt itself: a shipped paper no longer owes anything, and the rule stays silent " +
106
- "across the whole corpus — silence is its success state, so from the outside this is " +
107
- "indistinguishable",
108
- // Target retargeted 09-17: the old line disappeared when the rule moved to parsing, and
109
- // the driver honestly said UNUSABLE — "the patch did not land", not "the test is weak".
110
- // Now the mutation declares the run recorded before the file is even read.
111
- edits: [[RULE, "let recorded = false;", "let recorded = true;"]],
112
- },
113
- {
114
- name: "the exemption for an unshipped paper becomes WIDER than it should",
115
- harness: HARNESS,
116
- expect: "an empty stage list — silent",
117
- disables:
118
- "the distinction between 'no stages' and 'there are stages': a draft starts getting a " +
119
- "finding, and a rule that scolds drafts gets turned off within a week",
120
- edits: [
121
- [
122
- RULE,
123
- "if (stages.length === 0) return; // nothing shipped — nothing is owed",
124
- "",
125
- ],
126
- ],
127
- },
128
- {
129
- name: "the stage list is taken from somewhere OTHER than the field again",
130
- harness: HARNESS,
131
- expect:
132
- "the stage list in the message comes from the FIELD and carries all of them",
133
- disables:
134
- "the exact reason the move was made. The predecessor derived the stage with a regex " +
135
- "over the prose, and on agenticdev printed `submitted` where `submitted, " +
136
- "camera-ready` was declared. The mutation brings back a hardcoded list — the set of " +
137
- "findings does not change, only the TEXT lies, and without this assert the regression " +
138
- "would have passed silently",
139
- edits: [[RULE, 'stages: stages.join("/"),', 'stages: "submitted",']],
140
- },
141
- {
142
- name: "a consumer-specific path comes back into the package's text",
143
- harness: HARNESS,
144
- expect:
145
- "the run command comes in as an option and lands in the message",
146
- disables:
147
- "the boundary 'the mechanism goes in the package, the data stays with the consumer': " +
148
- "the command stops arriving as an option. The predecessor hardcoded " +
149
- "`.claude/skills/verify-citations/...` — the path of one private repository — right " +
150
- "into a public rule's message",
151
- edits: [
152
- [RULE, 'const command = opts.command ?? "";', 'const command = "";'],
153
- ],
154
- },
155
79
  ],
156
80
  }),
157
81
  );