paperlint 7.1.0 → 8.0.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 (261) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/.github/workflows/deadlines.yml +100 -0
  3. package/README.md +6 -0
  4. package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
  5. package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
  6. package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
  7. package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
  8. package/dist/adapters/hotcrp/deadlines.io.js +26 -0
  9. package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
  10. package/dist/adapters/hotcrp/deadlines.js +134 -0
  11. package/dist/adapters/hotcrp/deadlines.js.map +1 -0
  12. package/dist/adapters/hotcrp/index.d.ts +2 -0
  13. package/dist/adapters/hotcrp/index.d.ts.map +1 -1
  14. package/dist/adapters/hotcrp/index.js +2 -0
  15. package/dist/adapters/hotcrp/index.js.map +1 -1
  16. package/dist/build.d.ts.map +1 -1
  17. package/dist/build.js +1 -6
  18. package/dist/build.js.map +1 -1
  19. package/dist/cli.d.ts +3 -4
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +53 -47
  22. package/dist/cli.js.map +1 -1
  23. package/dist/cycle-rules.d.ts +24 -0
  24. package/dist/cycle-rules.d.ts.map +1 -0
  25. package/dist/cycle-rules.js +212 -0
  26. package/dist/cycle-rules.js.map +1 -0
  27. package/dist/domain/cycle.d.ts +148 -0
  28. package/dist/domain/cycle.d.ts.map +1 -0
  29. package/dist/domain/cycle.js +281 -0
  30. package/dist/domain/cycle.js.map +1 -0
  31. package/dist/domain/deadline.d.ts +128 -0
  32. package/dist/domain/deadline.d.ts.map +1 -0
  33. package/dist/domain/deadline.js +281 -0
  34. package/dist/domain/deadline.js.map +1 -0
  35. package/dist/eslint-rules/cycle-stage.d.ts +39 -0
  36. package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
  37. package/dist/eslint-rules/cycle-stage.js +119 -0
  38. package/dist/eslint-rules/cycle-stage.js.map +1 -0
  39. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  40. package/dist/eslint-rules/latex-language.js +10 -13
  41. package/dist/eslint-rules/latex-language.js.map +1 -1
  42. package/dist/eslint-rules/paper-context.js +2 -2
  43. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  44. package/dist/eslint-rules/paper-typography.js +22 -91
  45. package/dist/eslint-rules/paper-typography.js.map +1 -1
  46. package/dist/eslint-rules/papers.d.ts +1 -3
  47. package/dist/eslint-rules/papers.d.ts.map +1 -1
  48. package/dist/eslint-rules/papers.js +2 -4
  49. package/dist/eslint-rules/papers.js.map +1 -1
  50. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  51. package/dist/eslint-rules/tex-build.js +8 -25
  52. package/dist/eslint-rules/tex-build.js.map +1 -1
  53. package/dist/init.d.ts +1 -4
  54. package/dist/init.d.ts.map +1 -1
  55. package/dist/init.js +3 -9
  56. package/dist/init.js.map +1 -1
  57. package/dist/lib/markdown.d.ts +5 -81
  58. package/dist/lib/markdown.d.ts.map +1 -1
  59. package/dist/lib/markdown.js +5 -116
  60. package/dist/lib/markdown.js.map +1 -1
  61. package/dist/lib/paper-config.d.ts.map +1 -1
  62. package/dist/lib/paper-config.js +2 -2
  63. package/dist/lib/paper-config.js.map +1 -1
  64. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  65. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  66. package/dist/lib/skill-eval-fixture.js +25 -18
  67. package/dist/lib/skill-eval-fixture.js.map +1 -1
  68. package/dist/new-paper.d.ts +5 -11
  69. package/dist/new-paper.d.ts.map +1 -1
  70. package/dist/new-paper.js +12 -23
  71. package/dist/new-paper.js.map +1 -1
  72. package/dist/paper-files.d.ts +4 -4
  73. package/dist/paper-prose.d.ts +17 -0
  74. package/dist/paper-prose.d.ts.map +1 -0
  75. package/dist/paper-prose.js +46 -0
  76. package/dist/paper-prose.js.map +1 -0
  77. package/dist/paper-settings.d.ts +7 -0
  78. package/dist/paper-settings.d.ts.map +1 -1
  79. package/dist/paper-settings.js +61 -8
  80. package/dist/paper-settings.js.map +1 -1
  81. package/dist/ports/venue-deadlines.d.ts +14 -0
  82. package/dist/ports/venue-deadlines.d.ts.map +1 -0
  83. package/dist/ports/venue-deadlines.js +2 -0
  84. package/dist/ports/venue-deadlines.js.map +1 -0
  85. package/dist/presets.d.ts +12 -0
  86. package/dist/presets.d.ts.map +1 -1
  87. package/dist/presets.js +19 -11
  88. package/dist/presets.js.map +1 -1
  89. package/dist/structure.d.ts.map +1 -1
  90. package/dist/structure.js +4 -3
  91. package/dist/structure.js.map +1 -1
  92. package/dist/submission.d.ts +8 -0
  93. package/dist/submission.d.ts.map +1 -1
  94. package/dist/submission.js +41 -10
  95. package/dist/submission.js.map +1 -1
  96. package/dist/tex-requirements.d.ts +3 -0
  97. package/dist/tex-requirements.d.ts.map +1 -1
  98. package/dist/tex-requirements.js +19 -0
  99. package/dist/tex-requirements.js.map +1 -1
  100. package/dist/tex-venue-rules.d.ts +19 -0
  101. package/dist/tex-venue-rules.d.ts.map +1 -1
  102. package/dist/tex-venue-rules.js +69 -0
  103. package/dist/tex-venue-rules.js.map +1 -1
  104. package/dist/types.d.ts +0 -2
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/venue-rules.d.ts +10 -0
  107. package/dist/venue-rules.d.ts.map +1 -1
  108. package/dist/venue-rules.js +26 -10
  109. package/dist/venue-rules.js.map +1 -1
  110. package/docs/configuration.md +146 -20
  111. package/docs/e2e.md +4 -6
  112. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  113. package/docs/rules/cycle/evidence.md +87 -0
  114. package/docs/rules/cycle/port-done.md +75 -0
  115. package/docs/rules/cycle/record.md +152 -0
  116. package/docs/rules/cycle/stage.md +85 -0
  117. package/docs/rules/tex/claim-provenance.md +0 -1
  118. package/docs/rules.md +48 -47
  119. package/docs/submission.md +5 -0
  120. package/eslint-rules/cycle-stage.test.mjs +131 -0
  121. package/eslint-rules/cycle-stage.ts +141 -0
  122. package/eslint-rules/latex-language.harness.mjs +3 -3
  123. package/eslint-rules/latex-language.ts +10 -13
  124. package/eslint-rules/paper-context.ts +2 -2
  125. package/eslint-rules/paper-typography.test.mjs +17 -85
  126. package/eslint-rules/paper-typography.ts +22 -98
  127. package/eslint-rules/papers.harness.mjs +1 -1
  128. package/eslint-rules/papers.ts +2 -4
  129. package/eslint-rules/tex-build.harness.mjs +2 -2
  130. package/eslint-rules/tex-build.ts +8 -25
  131. package/eslint.config.mjs +41 -2
  132. package/hooks/hooks.harness.ts +15 -15
  133. package/hooks/paper-edit-guard.hook.mjs +5 -8
  134. package/hooks/paper-edit-guard.test.ts +12 -3
  135. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  136. package/hooks/paper-skills-nudge.test.ts +13 -3
  137. package/hooks/paper-status-gates.hook.mjs +2 -2
  138. package/hooks/paper-status-gates.test.ts +14 -2
  139. package/lib/markdown.test.mjs +5 -60
  140. package/lib/markdown.ts +9 -136
  141. package/lib/paper-config.ts +3 -2
  142. package/lib/skill-checks.test.mjs +4 -1
  143. package/lib/skill-eval-fixture.ts +25 -18
  144. package/package.json +1 -3
  145. package/presets/aidc.jsonc +24 -3
  146. package/presets/aidc.md +6 -3
  147. package/presets/msr.jsonc +25 -1
  148. package/presets/msr.md +5 -1
  149. package/presets/realm.jsonc +1 -1
  150. package/presets/venue-profile.schema.json +39 -0
  151. package/scripts/check.ts +4 -0
  152. package/scripts/harness-api.frozen.json +0 -5
  153. package/scripts/refresh-deadlines.test.ts +431 -0
  154. package/scripts/refresh-deadlines.ts +387 -0
  155. package/skills/cold-read-diff/SKILL.md +3 -3
  156. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  157. package/skills/grade-paper-writing/SKILL.md +14 -22
  158. package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
  159. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  160. package/skills/grade-paper-writing/prose-lint.mjs +108 -278
  161. package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
  162. package/skills/paper-adversarial-review/SKILL.md +2 -2
  163. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  164. package/skills/paper-pipeline/SKILL.md +9 -12
  165. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  166. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  167. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  168. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  169. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  170. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  171. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  172. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  173. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  174. package/skills/paper-pipeline/scripts/README.md +0 -4
  175. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
  176. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
  177. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
  178. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  179. package/skills/paper-status/SKILL.md +2 -2
  180. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  181. package/skills/render-paper/SKILL.md +2 -2
  182. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  183. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  184. package/skills/tighten-paper/SKILL.md +29 -42
  185. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  186. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
  187. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
  188. package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
  189. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  190. package/src/adapters/hotcrp/cassette/README.md +22 -0
  191. package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
  192. package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
  193. package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
  194. package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
  195. package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
  196. package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
  197. package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
  198. package/src/adapters/hotcrp/deadlines.io.ts +27 -0
  199. package/src/adapters/hotcrp/deadlines.test.ts +170 -0
  200. package/src/adapters/hotcrp/deadlines.ts +161 -0
  201. package/src/adapters/hotcrp/index.ts +2 -0
  202. package/src/build.harness.mjs +2 -2
  203. package/src/build.test.ts +1 -1
  204. package/src/build.ts +1 -6
  205. package/src/cli.harness.mjs +26 -46
  206. package/src/cli.test.ts +41 -40
  207. package/src/cli.ts +57 -58
  208. package/src/cycle-rules.test.ts +278 -0
  209. package/src/cycle-rules.ts +273 -0
  210. package/src/domain/cycle.test.ts +456 -0
  211. package/src/domain/cycle.ts +432 -0
  212. package/src/domain/deadline.test.ts +386 -0
  213. package/src/domain/deadline.ts +472 -0
  214. package/src/folder-venue-rule.test.ts +1 -1
  215. package/src/init.test.ts +6 -7
  216. package/src/init.ts +3 -22
  217. package/src/lint-fix.test.ts +10 -4
  218. package/src/new-paper.harness.mjs +9 -28
  219. package/src/new-paper.test.ts +7 -6
  220. package/src/new-paper.ts +11 -30
  221. package/src/paper-files.test.ts +5 -4
  222. package/src/paper-files.ts +4 -4
  223. package/src/paper-prose.test.ts +49 -0
  224. package/src/paper-prose.ts +67 -0
  225. package/src/paper-settings.test.ts +167 -0
  226. package/src/paper-settings.ts +85 -11
  227. package/src/parse-args.test.ts +0 -1
  228. package/src/ports/venue-deadlines.ts +14 -0
  229. package/src/presets.test.ts +105 -0
  230. package/src/presets.ts +43 -10
  231. package/src/rule-cases.test.ts +144 -0
  232. package/src/structure.harness.ts +5 -11
  233. package/src/structure.ts +4 -3
  234. package/src/submission.test.ts +105 -1
  235. package/src/submission.ts +73 -12
  236. package/src/tex-requirements.ts +31 -0
  237. package/src/tex-venue-rules.test.ts +76 -0
  238. package/src/tex-venue-rules.ts +85 -0
  239. package/src/types.ts +0 -2
  240. package/src/venue-rules.test.ts +82 -0
  241. package/src/venue-rules.ts +44 -12
  242. package/fixtures/real-markdown-paper/baseline.json +0 -22
  243. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  244. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  245. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  246. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  247. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  248. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  249. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  250. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  251. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  252. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  253. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  254. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  255. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  256. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  257. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  258. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  259. package/skills/tighten-paper/structure.mjs +0 -364
  260. package/skills/tighten-paper/structure.test.ts +0 -117
  261. package/templates/paper/paper.md +0 -4
@@ -1,5 +1,5 @@
1
1
  /**
2
- * prose-lint.mjs as a process — the way the skill runs it. Every mode, on
2
+ * prose-lint.mjs as a process — the way the skill runs it. Every mode, on an accepted paper and on
3
3
  * fixtures written here, with the WHOLE output compared: the report is the product, so a changed
4
4
  * number or a dropped line is a changed result.
5
5
  */
@@ -11,84 +11,111 @@ import { expect, test } from "vitest";
11
11
  import { runNode, useTempDir, writeTree } from "../../test/support.ts";
12
12
 
13
13
  const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "prose-lint.mjs");
14
+ /**
15
+ * One process that loads the LaTeX parser and reads a full accepted paper: about 1 s alone, 5.6 s
16
+ * measured under coverage with the suite running in parallel — past vitest's 5 s default.
17
+ */
18
+ const SPAWN_LATEX_MS = 60_000;
19
+ /** An accepted paper whose bibliography sits inline in `filecontents`, beside a long preamble. */
20
+ const ACCEPTED = join(
21
+ dirname(fileURLToPath(import.meta.url)),
22
+ "..",
23
+ "..",
24
+ "fixtures",
25
+ "accepted-papers",
26
+ "agenticdev-acm26",
27
+ "paper.tex",
28
+ );
14
29
  const root = useTempDir("prose-lint-");
15
30
 
16
31
  const LONG_CAPTION_SENTENCE = Array.from(
17
32
  { length: 45 },
18
33
  (_, i) => `word${String(i)}`,
19
34
  ).join(" ");
20
- const PAPER = `---
21
- title: A title the linter must not read as a sentence
22
- ---
23
- # The paper
24
-
25
- <!-- TIGHTEN: a working note, not prose -->
26
-
27
- ## Introduction
28
-
29
- In this paper we present a linter. It might possibly help, but this is not a proof. Clearly the
30
- first result matters (Section 3). Prior work agrees [1, 2]. The speedup was 40% on average across the
31
- full suite of papers, which suggests the method might hold generally.
32
-
33
- - The first item is short.
34
- - The second item is not long either.
35
-
36
- \`\`\`
37
- # a heading inside a code block is code
38
- \`\`\`
39
-
40
- | a | table |
41
- |---|---|
42
- | is | data |
43
-
44
- ### Method
45
35
 
46
- We believe that rather than guessing, we measure. Instead of prose, the numbers speak.
47
-
48
- ## References
49
-
50
- [1] A reference that is not prose.
51
- `;
36
+ /** A paper.tex whose body is `body`. */
37
+ const tex = (body: string): string =>
38
+ `\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
52
39
 
53
40
  writeTree(root, {
54
- "p/paper.md": PAPER,
41
+ "p/paper.tex": tex(
42
+ "\\section{Introduction}\nIn this paper we present a linter. It might possibly help.\n\\input{figures/fig1}\n\\input{figures/fig2}\n\\input{figures/fig4}\n\\input{figures/fig5}",
43
+ ),
55
44
  "p/figures/fig1.tex": `\\begin{figure}\\caption{\\textbf{Big.} ${LONG_CAPTION_SENTENCE}. ${"More words here. ".repeat(20)}}\\end{figure}`,
56
45
  // Empty once LaTeX commands and braces are stripped: skipped, not counted as a zero-word caption.
57
46
  "p/figures/fig2.tex": "\\caption{\\centering}",
58
- // A lone "#": no word at all, and no sentence either.
47
+ // A lone "#": no word at all — measured as zero words, never flagged.
59
48
  "p/figures/fig4.tex": "\\caption{{#}}",
49
+ // Rules only: seen by the reader, but no word once the dashes are stripped — skipped.
50
+ "p/figures/fig5.tex": "\\caption{---}",
60
51
  "p/figures/notes.txt": "not a figure",
61
- // Reads as a markdown heading once LaTeX is stripped ("# Big"), so it splits into NO sentence:
62
- // the caption scan must not flag it and must not crash.
63
- "p/figures/fig3.tex": "\\caption{{#} Big}",
64
- "clean/paper.md": "## Intro\n\nShort and plain. Nothing else.\n",
52
+ "clean/paper.tex": tex("Short and plain. Nothing else."),
53
+ // Captions in the paper itself and in a section it includes — not under figures/.
54
+ "inline/paper.tex": tex(
55
+ `Prose here. \\begin{figure}\\caption{${LONG_CAPTION_SENTENCE}.}\\end{figure}\n\\input{sec/results}`,
56
+ ),
57
+ "inline/sec/results.tex": `\\begin{figure}\\caption{${"Short words here. ".repeat(40)}}\\end{figure}`,
58
+ // One real over-long caption, and the same caption commented out and inside verbatim.
59
+ "commented/paper.tex": tex(
60
+ `Prose here.\n% \\caption{Commented ${LONG_CAPTION_SENTENCE}.}\n\\begin{verbatim}\n\\caption{Quoted ${LONG_CAPTION_SENTENCE}.}\n\\end{verbatim}\n\\begin{figure}\\caption{Real ${LONG_CAPTION_SENTENCE}.}\\end{figure}`,
61
+ ),
62
+ "ends-on-marks/paper.tex": tex(
63
+ "The method holds on every input~\\cite{knuth}. The proof is given in full (Section~\\ref{proof}). The result is new.",
64
+ ),
65
+ "empty/paper.tex": "\\documentclass{article}",
65
66
  "page.txt":
66
67
  "A rendered page. It was hyphen-\nated across a line.\n\n12\n\fNext page text here.\nReferences\n[1] cut here.\n",
67
- "paper.tex": "\\documentclass{article}",
68
+ "notes.rtf": "not a paper",
68
69
  });
69
70
 
70
71
  test("no arguments: the usage line, exit 0", () => {
71
72
  assert.deepEqual(runNode(SCRIPT), {
72
73
  status: 0,
73
74
  stdout: "",
74
- stderr: "usage: node prose-lint.mjs <file.md|file.txt> [more files]\n",
75
+ stderr:
76
+ "usage: node prose-lint.mjs <paper.tex|page.txt> [more files] [--flags-only|--headings]\n",
75
77
  });
76
78
  });
77
79
 
78
- test("a .tex file is refused with exit 2, and nothing is measured", () => {
79
- const r = runNode(SCRIPT, [join(root, "paper.tex")]);
80
- assert.deepEqual(
81
- { status: r.status, stdout: r.stdout },
82
- { status: 2, stdout: "" },
83
- );
84
- expect(r.stderr.replaceAll(root, "<root>")).toMatchSnapshot();
85
- });
80
+ test(
81
+ "🔴 a paper.tex is measured on the prose a reader sees — not its preamble, comments or inline .bib",
82
+ { timeout: SPAWN_LATEX_MS },
83
+ () => {
84
+ const r = runNode(SCRIPT, [ACCEPTED]);
85
+ // Guards: a .tex read as raw text counted the preamble, the comments and the `filecontents`
86
+ // bibliography — 8127 "words" on this paper's submitted version against about 4636 real ones.
87
+ const words = Number(
88
+ /^=== paper\.tex — (\d+) words, \d+ sentences ===$/m.exec(r.stdout)?.[1],
89
+ );
90
+ assert.deepEqual(
91
+ {
92
+ status: r.status,
93
+ stderr: r.stderr,
94
+ inBand: words > 4000 && words < 5500,
95
+ },
96
+ { status: 0, stderr: "", inBand: true },
97
+ );
98
+ // No BibTeX field and no preamble command reaches the measured text.
99
+ assert.doesNotMatch(r.stdout, /author\s*=|\\usepackage|\\documentclass/);
100
+ expect(r.stdout).toMatchSnapshot();
101
+ },
102
+ );
86
103
 
87
- test("the full report over markdown and rendered text", () => {
88
- const r = runNode(SCRIPT, [
89
- join(root, "p", "paper.md"),
90
- join(root, "page.txt"),
91
- ]);
104
+ test(
105
+ "--headings on a paper.tex lists its section titles, in order",
106
+ { timeout: SPAWN_LATEX_MS },
107
+ () => {
108
+ const r = runNode(SCRIPT, ["--headings", ACCEPTED]);
109
+ assert.deepEqual(
110
+ { status: r.status, stderr: r.stderr },
111
+ { status: 0, stderr: "" },
112
+ );
113
+ expect(r.stdout).toMatchSnapshot();
114
+ },
115
+ );
116
+
117
+ test("the report over a rendered page: page numbers, hyphenation and the references are dropped", () => {
118
+ const r = runNode(SCRIPT, [join(root, "page.txt")]);
92
119
  assert.deepEqual(
93
120
  { status: r.status, stderr: r.stderr },
94
121
  { status: 0, stderr: "" },
@@ -96,17 +123,26 @@ test("the full report over markdown and rendered text", () => {
96
123
  expect(r.stdout).toMatchSnapshot();
97
124
  });
98
125
 
99
- test("--headings lists every heading from level 2 to 4, in order", () => {
100
- const r = runNode(SCRIPT, ["--headings", join(root, "p", "paper.md")]);
101
- assert.deepEqual(
102
- { status: r.status, stderr: r.stderr },
103
- { status: 0, stderr: "" },
104
- );
105
- expect(r.stdout).toMatchSnapshot();
126
+ test("--headings on a rendered page is refused: it marks no heading", () => {
127
+ const file = join(root, "page.txt");
128
+ assert.deepEqual(runNode(SCRIPT, ["--headings", file]), {
129
+ status: 2,
130
+ stdout: "",
131
+ stderr: `prose-lint: --headings reads a paper.tex — a rendered page (${file}) marks no heading\n`,
132
+ });
133
+ });
134
+
135
+ test("a file that is neither a paper.tex nor a rendered page is refused with exit 2, and nothing is measured", () => {
136
+ const file = join(root, "notes.rtf");
137
+ assert.deepEqual(runNode(SCRIPT, [file]), {
138
+ status: 2,
139
+ stdout: "",
140
+ stderr: `prose-lint reads a paper.tex or the text of its rendered PDF (.txt), not: ${file}\n`,
141
+ });
106
142
  });
107
143
 
108
144
  test("--flags-only: an over-long caption flags and exits 1", () => {
109
- const r = runNode(SCRIPT, ["--flags-only", join(root, "p", "paper.md")]);
145
+ const r = runNode(SCRIPT, ["--flags-only", join(root, "p", "paper.tex")]);
110
146
  assert.deepEqual(
111
147
  { status: r.status, stdout: r.stdout },
112
148
  { status: 1, stdout: "" },
@@ -116,38 +152,105 @@ test("--flags-only: an over-long caption flags and exits 1", () => {
116
152
 
117
153
  test("--flags-only: a paper with no figures directory is clean, exit 0 and silent", () => {
118
154
  assert.deepEqual(
119
- runNode(SCRIPT, ["--flags-only", join(root, "clean", "paper.md")]),
120
- {
121
- status: 0,
122
- stdout: "",
123
- stderr: "",
124
- },
155
+ runNode(SCRIPT, ["--flags-only", join(root, "clean", "paper.tex")]),
156
+ { status: 0, stdout: "", stderr: "" },
125
157
  );
126
158
  });
127
159
 
128
- test("a file with no prose left to measure is refused with exit 2, not reported as NaN", () => {
129
- writeTree(root, {
130
- "empty/paper.md":
131
- "---\ntitle: only frontmatter\n---\n<!-- and a note -->\n",
132
- });
133
- const file = join(root, "empty", "paper.md");
160
+ test("a .tex with no prose in its body is refused with exit 2, not reported as NaN", () => {
161
+ const file = join(root, "empty", "paper.tex");
134
162
  assert.deepEqual(runNode(SCRIPT, [file]), {
135
163
  status: 2,
136
164
  stdout: "",
137
165
  stderr:
138
- `prose-lint: ${file} has no prose to measure — after the frontmatter, comments, code ` +
139
- "blocks, tables and the reference section are stripped, no word is left.\n",
166
+ `prose-lint: ${file} has no prose to measure — after the preamble, comments, floats and ` +
167
+ "the back matter are left out, no word is left.\n",
140
168
  });
141
169
  });
142
170
 
143
- test("a body of headings only has no sentence to measure, and is refused the same way", () => {
144
- writeTree(root, { "headings/paper.md": "## Intro\n\n### Method\n" });
145
- const file = join(root, "headings", "paper.md");
171
+ test("a rendered page with no sentence before its references is refused the same way", () => {
172
+ writeTree(root, { "blank.txt": "12\nReferences\n[1] only this.\n" });
173
+ const file = join(root, "blank.txt");
146
174
  assert.deepEqual(runNode(SCRIPT, [file]), {
147
175
  status: 2,
148
176
  stdout: "",
149
- stderr:
150
- `prose-lint: ${file} has no prose to measure — after the frontmatter, comments, code ` +
151
- "blocks, tables and the reference section are stripped, no word is left.\n",
177
+ stderr: `prose-lint: ${file} has no prose to measure — no sentence is left before the references.\n`,
178
+ });
179
+ });
180
+
181
+ test("🔴 in a paper.tex, a sentence ending on a citation or a cross-reference wastes its stress position", () => {
182
+ const r = runNode(SCRIPT, [join(root, "ends-on-marks", "paper.tex")]);
183
+ // Guards: the marks used to be dropped from the prose, so neither ending could be seen.
184
+ assert.match(
185
+ r.stdout,
186
+ /^FLAG sentences ending on a cross-ref\/citation\/hedge \(wasted stress position\): 2$/m,
187
+ );
188
+ });
189
+
190
+ test("🔴 --flags-only reads the captions of the paper.tex and of the files it includes", () => {
191
+ const r = runNode(SCRIPT, [
192
+ "--flags-only",
193
+ join(root, "inline", "paper.tex"),
194
+ ]);
195
+ // Guards: captions were read only from figures/*.tex beside the paper, so a caption set inline,
196
+ // or in an included section, was never checked and the run exited clean.
197
+ assert.deepEqual(
198
+ {
199
+ status: r.status,
200
+ stdout: r.stdout,
201
+ flagged: r.stderr
202
+ .split("\n")
203
+ .filter((l) => l.startsWith(" "))
204
+ .map((l) => l.split(":")[0]),
205
+ },
206
+ {
207
+ status: 1,
208
+ stdout: "",
209
+ flagged: [
210
+ " paper.tex",
211
+ " sec/results.tex",
212
+ " run prose-lint.mjs on the file without --flags-only for the sentences",
213
+ ],
214
+ },
215
+ );
216
+ });
217
+
218
+ test("--flags-only on a rendered page reads the captions in figures/*.tex beside it", () => {
219
+ writeTree(root, {
220
+ "rendered/page.txt": "A rendered page. Its prose is fine.\n",
221
+ "rendered/figures/f.tex": `\\caption{${LONG_CAPTION_SENTENCE}.}`,
222
+ "rendered/figures/notes.txt": "not a figure",
223
+ });
224
+ const r = runNode(SCRIPT, [
225
+ "--flags-only",
226
+ join(root, "rendered", "page.txt"),
227
+ ]);
228
+ assert.deepEqual(
229
+ { status: r.status, first: r.stderr.split("\n")[1]?.split(":")[0] },
230
+ { status: 1, first: " figures/f.tex" },
231
+ );
232
+ // …and a page with no figures/ beside it has no caption to flag.
233
+ assert.deepEqual(runNode(SCRIPT, ["--flags-only", join(root, "page.txt")]), {
234
+ status: 0,
235
+ stdout: "",
236
+ stderr: "",
152
237
  });
153
238
  });
239
+
240
+ test("🔴 a caption commented out or quoted in verbatim is not measured; a real one still is", () => {
241
+ const r = runNode(SCRIPT, [
242
+ "--flags-only",
243
+ join(root, "commented", "paper.tex"),
244
+ ]);
245
+ // Guards: the scan matched every `\caption{` in the raw text, comments and verbatim included.
246
+ assert.deepEqual(
247
+ {
248
+ status: r.status,
249
+ findings: r.stderr
250
+ .split("\n")
251
+ .filter((l) => l.startsWith(" paper.tex:"))
252
+ .map((l) => /— "(\w+)/.exec(l)?.[1]),
253
+ },
254
+ { status: 1, findings: ["Real"] },
255
+ );
256
+ });
@@ -5,7 +5,7 @@ context: fork
5
5
  allowed-tools: [Read, Write, Grep, Glob, Agent, Skill]
6
6
  ---
7
7
 
8
- <!-- vigiles:sha256:fe41e7c71eb4672d compiled from skills/paper-adversarial-review/SKILL.md.spec.ts -->
8
+ <!-- vigiles:sha256:cb852f6fb5fb9988 compiled from skills/paper-adversarial-review/SKILL.md.spec.ts -->
9
9
 
10
10
  # paper-adversarial-review — be the reviewer who wants to reject it
11
11
 
@@ -22,7 +22,7 @@ reviewer and returns an actionable critique.
22
22
 
23
23
  ## How to run it
24
24
  Prefer a **separate model as the reviewer** (e.g. Fable via a subagent) so it's not the author
25
- grading itself. Give the reviewer the full paper text (Read the `.tex`/`.md`/PDF) and the target
25
+ grading itself. Give the reviewer the full paper text (Read the `.tex`/PDF) and the target
26
26
  venue + paper type (short/full/position/benchmark). Ask for the structured output below.
27
27
 
28
28
  ## The review must cover (ranked by how often it kills a paper)
@@ -28,7 +28,7 @@ reviewer and returns an actionable critique.
28
28
 
29
29
  ## How to run it
30
30
  Prefer a **separate model as the reviewer** (e.g. Fable via a subagent) so it's not the author
31
- grading itself. Give the reviewer the full paper text (Read the \`.tex\`/\`.md\`/PDF) and the target
31
+ grading itself. Give the reviewer the full paper text (Read the \`.tex\`/PDF) and the target
32
32
  venue + paper type (short/full/position/benchmark). Ask for the structured output below.
33
33
 
34
34
  ## The review must cover (ranked by how often it kills a paper)
@@ -4,7 +4,7 @@ description: The orchestrator for writing a research paper end-to-end, from idea
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent, Skill]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:a8a20289badf1fa8 compiled from skills/paper-pipeline/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:67a3989629952dc1 compiled from skills/paper-pipeline/SKILL.md.spec.ts -->
8
8
 
9
9
  # paper-pipeline — the conductor for the whole organism
10
10
 
@@ -191,7 +191,7 @@ These have no position, because a position is what made them look finished. Each
191
191
  | any `\cite` added or moved | **`verify-citations`** | An edit on the last day adds a citation the last run never saw. "We checked last cycle" does not count — the project rule, and it was numbered as a one-shot anyway. |
192
192
  | a genuine sibling surfaces | **`analyze-sibling-paper`** | Deep-read, scoop/delta, saved in `<paper-dir>/siblings/`. |
193
193
  | the contribution's framing moves | **`map-prior-work`** (delta mode) | A cheap re-sweep. ⚠️ If a rival first surfaces during citation checking, the SETUP sweep was skipped and you are reshaping the contribution under deadline. |
194
- | any `.tex` / `paper.md` edit | **`render-paper`** + page count | **Not a stage — a command.** Render, then eyeball pages 1–2 before believing anything about the paper. 🔴 **Rendering is continuous; ACTING on the page count is not** — see the rule below. |
194
+ | any `.tex` edit | **`render-paper`** + page count | **Not a stage — a command.** Render, then eyeball pages 1–2 before believing anything about the paper. 🔴 **Rendering is continuous; ACTING on the page count is not** — see the rule below. |
195
195
  | the deadline approaches | the `access` row | Re-check that the profile is still ACTIVE and the portal still opens. External state rots without telling you. |
196
196
 
197
197
  ## GATES — late, expensive, each blocked on a named input
@@ -310,25 +310,22 @@ binds every row from now on, and the existing rows are *enumerated rather than e
310
310
  says out loud how many figures still have no machine-checked path to data. On the paper that has it:
311
311
  7 guarded, 248 grandfathered. That ratio is a finding, not a score.
312
312
 
313
- **Adopting it in another paper:** copy `repro/paper_numbers.py`, `repro/paper_numbers.selftest.py` and
314
- the three lines in `md2submission.py` that call `expand()`; generate the grandfather list from the
315
- current draft; register the numbers a reviewer will actually check first. `check-provenance.mjs`
316
- reports `no-numbers-gate` for any paper that has not.
313
+ **Adopting it in another paper:** copy `repro/paper_numbers.py`, `repro/paper_numbers.selftest.py`
314
+ and the build step that calls `expand()` on `paper.tex` before `pdflatex`; generate the
315
+ grandfather list from the current draft; register the numbers a reviewer will actually check first.
317
316
 
318
317
  🔴 **Do not write the substitution half — it is `jinja2`, and `{{name}}` is already its syntax.**
319
- Eleven candidates were installed and run on a real markdown-source ACL paper on 2026-08-05. The
320
- result is not a matter of taste:
318
+ Eleven candidates were installed and run on a real ACL paper on 2026-08-05. The result is not a
319
+ matter of taste:
321
320
 
322
321
  | candidate | verdict, from an actual run |
323
322
  |---|---|
324
323
  | **jinja2** | **adopt.** Our `{{annotated.failRate}}` was already valid Jinja2; dotted names are attribute access. Zero characters changed in the paper, byte-identical `.tex`. This is Manubot's mechanism, taken without Manubot. Use `StrictUndefined`, and move the block/comment delimiters off `{%`/`{#` if the paper might ever quote a config file |
325
324
  | **knitr** | equally proven, also byte-identical. Rejected only for putting an R runtime into an artifact that otherwise needs `python3` + `pdflatex` |
326
- | `\newcommand` + `numbers.tex` | works (1 line in the converter); the folk convention, ~12 GitHub hits, all hand-rolled. No package exists |
327
- | **Quarto**, **pandoc lua filters** | ❌ **destroy the paper.** Markdown→markdown is a pandoc AST round-trip: HTML comments become raw blocks and `[58]` becomes `\[58\]`, so every citation renders as literal `\textbackslash{}[58]`. 3,120 diff lines on the real source. No ACL format exists for Quarto either |
325
+ | `\newcommand` + `numbers.tex` | works; the folk convention, ~12 GitHub hits, all hand-rolled. No package exists |
328
326
  | **cog.py** | ❌ line-oriented; cannot substitute mid-sentence |
329
327
  | **pythontex** | ❌ not in Ubuntu TeX Live at any level, not on PyPI; CTAN zip + `.ins` build |
330
- | **showyourwork** | ❌ LaTeX-only, and it manages *figures*, not in-text numbers |
331
- | **Pweave** | ❌ dead — the one candidate with a markdown native format, broken against modern IPython |
328
+ | **showyourwork** | ❌ it manages *figures*, not in-text numbers |
332
329
  | DVC · datalad · papermill | orthogonal and **worth adding**: they pin the data file to the pipeline that made it, a layer below anything here |
333
330
 
334
331
  **🔴 Computing the value DISSOLVES the freshness problem — do not build a checker for it.** With no
@@ -208,7 +208,7 @@ These have no position, because a position is what made them look finished. Each
208
208
  | any \`\\cite\` added or moved | **\`verify-citations\`** | An edit on the last day adds a citation the last run never saw. "We checked last cycle" does not count — the project rule, and it was numbered as a one-shot anyway. |
209
209
  | a genuine sibling surfaces | **\`analyze-sibling-paper\`** | Deep-read, scoop/delta, saved in \`<paper-dir>/siblings/\`. |
210
210
  | the contribution's framing moves | **\`map-prior-work\`** (delta mode) | A cheap re-sweep. ⚠️ If a rival first surfaces during citation checking, the SETUP sweep was skipped and you are reshaping the contribution under deadline. |
211
- | any \`.tex\` / \`paper.md\` edit | **\`render-paper\`** + page count | **Not a stage — a command.** Render, then eyeball pages 1–2 before believing anything about the paper. 🔴 **Rendering is continuous; ACTING on the page count is not** — see the rule below. |
211
+ | any \`.tex\` edit | **\`render-paper\`** + page count | **Not a stage — a command.** Render, then eyeball pages 1–2 before believing anything about the paper. 🔴 **Rendering is continuous; ACTING on the page count is not** — see the rule below. |
212
212
  | the deadline approaches | the \`access\` row | Re-check that the profile is still ACTIVE and the portal still opens. External state rots without telling you. |
213
213
 
214
214
  ## GATES — late, expensive, each blocked on a named input
@@ -327,25 +327,22 @@ binds every row from now on, and the existing rows are *enumerated rather than e
327
327
  says out loud how many figures still have no machine-checked path to data. On the paper that has it:
328
328
  7 guarded, 248 grandfathered. That ratio is a finding, not a score.
329
329
 
330
- **Adopting it in another paper:** copy \`repro/paper_numbers.py\`, \`repro/paper_numbers.selftest.py\` and
331
- the three lines in \`md2submission.py\` that call \`expand()\`; generate the grandfather list from the
332
- current draft; register the numbers a reviewer will actually check first. \`check-provenance.mjs\`
333
- reports \`no-numbers-gate\` for any paper that has not.
330
+ **Adopting it in another paper:** copy \`repro/paper_numbers.py\`, \`repro/paper_numbers.selftest.py\`
331
+ and the build step that calls \`expand()\` on \`paper.tex\` before \`pdflatex\`; generate the
332
+ grandfather list from the current draft; register the numbers a reviewer will actually check first.
334
333
 
335
334
  🔴 **Do not write the substitution half — it is \`jinja2\`, and \`{{name}}\` is already its syntax.**
336
- Eleven candidates were installed and run on a real markdown-source ACL paper on 2026-08-05. The
337
- result is not a matter of taste:
335
+ Eleven candidates were installed and run on a real ACL paper on 2026-08-05. The result is not a
336
+ matter of taste:
338
337
 
339
338
  | candidate | verdict, from an actual run |
340
339
  |---|---|
341
340
  | **jinja2** | **adopt.** Our \`{{annotated.failRate}}\` was already valid Jinja2; dotted names are attribute access. Zero characters changed in the paper, byte-identical \`.tex\`. This is Manubot's mechanism, taken without Manubot. Use \`StrictUndefined\`, and move the block/comment delimiters off \`{%\`/\`{#\` if the paper might ever quote a config file |
342
341
  | **knitr** | equally proven, also byte-identical. Rejected only for putting an R runtime into an artifact that otherwise needs \`python3\` + \`pdflatex\` |
343
- | \`\\newcommand\` + \`numbers.tex\` | works (1 line in the converter); the folk convention, ~12 GitHub hits, all hand-rolled. No package exists |
344
- | **Quarto**, **pandoc lua filters** | ❌ **destroy the paper.** Markdown→markdown is a pandoc AST round-trip: HTML comments become raw blocks and \`[58]\` becomes \`\\[58\\]\`, so every citation renders as literal \`\\textbackslash{}[58]\`. 3,120 diff lines on the real source. No ACL format exists for Quarto either |
342
+ | \`\\newcommand\` + \`numbers.tex\` | works; the folk convention, ~12 GitHub hits, all hand-rolled. No package exists |
345
343
  | **cog.py** | ❌ line-oriented; cannot substitute mid-sentence |
346
344
  | **pythontex** | ❌ not in Ubuntu TeX Live at any level, not on PyPI; CTAN zip + \`.ins\` build |
347
- | **showyourwork** | ❌ LaTeX-only, and it manages *figures*, not in-text numbers |
348
- | **Pweave** | ❌ dead — the one candidate with a markdown native format, broken against modern IPython |
345
+ | **showyourwork** | ❌ it manages *figures*, not in-text numbers |
349
346
  | DVC · datalad · papermill | orthogonal and **worth adding**: they pin the data file to the pipeline that made it, a layer below anything here |
350
347
 
351
348
  **🔴 Computing the value DISSOLVES the freshness problem — do not build a checker for it.** With no
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * description-language.eval.mjs — does adding Russian to a skill's DESCRIPTION close the language gap?
3
3
  *
4
+ * ⚠️ THE FIXTURE CHANGED AFTER THE RECORDED RUNS. The paper the model is shown is now `paper.tex`;
5
+ * every result recorded for this eval (here and in `repro/`) was measured on the previous fixture,
6
+ * the same paper in a source form paperlint no longer reads. The next run is a fresh, deliberate
7
+ * measurement — record it, do not compare it against those numbers.
8
+ *
4
9
  * Run: node .claude/skills/paper-pipeline/description-language.eval.mjs --arm before|after [--trials N]
5
10
  *
6
11
  * --arm before measure with the descriptions AS THEY ARE (English only) <- run this FIRST
@@ -119,42 +124,47 @@ if (ARM !== "before" && ARM !== "after")
119
124
  // Byte-identical to the other two evals. If these drift, a difference between runs stops being
120
125
  // attributable to the descriptions.
121
126
  const FIXTURE = {
122
- "paper/paper.md": [
123
- "# Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable",
127
+ "paper/paper.tex": [
128
+ "\\documentclass{article}",
129
+ "\\title{Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable}",
130
+ "\\begin{document}",
131
+ "\\maketitle",
124
132
  "",
125
- "## Abstract",
133
+ "\\begin{abstract}",
126
134
  "Agent configuration files state rules in prose and assume the model obeys them. We compile a",
127
- "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84%",
135
+ "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84\\%",
128
136
  "of rules in our corpus are enforceable, and that LLM-authored checkers for the remainder leak",
129
- "silently in 84-96% of adversarial cases \\cite{greshake2023}.",
137
+ "silently in 84--96\\% of adversarial cases~\\cite{greshake2023}.",
138
+ "\\end{abstract}",
130
139
  "",
131
- "## 1 Introduction",
140
+ "\\section{Introduction}",
132
141
  "Every agent harness ships a natural-language rulebook. Nothing checks it. This is the same",
133
- "mistake as a code comment that claims an invariant no test enforces \\cite{thompson1984}.",
142
+ "mistake as a code comment that claims an invariant no test enforces~\\cite{thompson1984}.",
134
143
  "",
135
- "## 2 Method",
144
+ "\\section{Method}",
136
145
  "We gather rules from public repositories, classify each by enforceability, and build a",
137
146
  "two-stage adversarial gate that validates a synthesized rule against a blind gold set.",
138
147
  "",
139
- "## 3 Results",
140
- "See Table 1. The headline number is 84%.",
148
+ "\\section{Results}",
149
+ "See Table~1. The headline number is 84\\%.",
141
150
  "",
142
- "## 4 Discussion",
151
+ "\\section{Discussion}",
143
152
  "The result generalizes beyond our corpus in the sense that the mechanism is not corpus-specific,",
144
153
  "though of course the specific percentages are, and it is important to note in this context that",
145
154
  "the framing itself may be what carries, rather than the measurement.",
146
155
  "",
147
- "## 5 Threats to Validity",
156
+ "\\section{Threats to Validity}",
148
157
  "Our corpus is drawn from public repositories and may not represent private configurations.",
149
158
  "",
150
- "## 6 Related Work",
159
+ "\\section{Related Work}",
151
160
  "TODO",
152
161
  "",
153
- "## 7 Conclusion",
162
+ "\\section{Conclusion}",
154
163
  "Prose is not policy.",
164
+ "\\end{document}",
155
165
  ].join("\n"),
156
166
  "paper/repro/README.md":
157
- "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.md.\n",
167
+ "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.tex.\n",
158
168
  "paper/PIPELINE-STATUS.md":
159
169
  "# Pipeline status\n\n| gate | state |\n|---|---|\n| numbers | pass |\n| structure | not run |\n| citations | not run |\n",
160
170
  };
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * framing-vs-vocabulary.eval.mjs — WHY does a skill fire only when the user names its verb?
3
3
  *
4
+ * ⚠️ THE FIXTURE CHANGED AFTER THE RECORDED RUNS. The paper the model is shown is now `paper.tex`;
5
+ * every result recorded for this eval (here and in `repro/`) was measured on the previous fixture,
6
+ * the same paper in a source form paperlint no longer reads. The next run is a fresh, deliberate
7
+ * measurement — record it, do not compare it against those numbers.
8
+ *
4
9
  * Run: node .claude/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs [--trials N] [--mode main|oracle|preflight]
5
10
  *
6
11
  * --mode preflight run every guard and print the labelling matrix; spends NO tokens <- run this FIRST
@@ -287,42 +292,47 @@ if (!["main", "oracle", "preflight"].includes(MODE))
287
292
  // Byte-identical to BOTH siblings. If it drifts, a difference between this run and theirs stops
288
293
  // being attributable to the prompts.
289
294
  const FIXTURE = {
290
- "paper/paper.md": [
291
- "# Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable",
295
+ "paper/paper.tex": [
296
+ "\\documentclass{article}",
297
+ "\\title{Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable}",
298
+ "\\begin{document}",
299
+ "\\maketitle",
292
300
  "",
293
- "## Abstract",
301
+ "\\begin{abstract}",
294
302
  "Agent configuration files state rules in prose and assume the model obeys them. We compile a",
295
- "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84%",
303
+ "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84\\%",
296
304
  "of rules in our corpus are enforceable, and that LLM-authored checkers for the remainder leak",
297
- "silently in 84-96% of adversarial cases \\cite{greshake2023}.",
305
+ "silently in 84--96\\% of adversarial cases~\\cite{greshake2023}.",
306
+ "\\end{abstract}",
298
307
  "",
299
- "## 1 Introduction",
308
+ "\\section{Introduction}",
300
309
  "Every agent harness ships a natural-language rulebook. Nothing checks it. This is the same",
301
- "mistake as a code comment that claims an invariant no test enforces \\cite{thompson1984}.",
310
+ "mistake as a code comment that claims an invariant no test enforces~\\cite{thompson1984}.",
302
311
  "",
303
- "## 2 Method",
312
+ "\\section{Method}",
304
313
  "We gather rules from public repositories, classify each by enforceability, and build a",
305
314
  "two-stage adversarial gate that validates a synthesized rule against a blind gold set.",
306
315
  "",
307
- "## 3 Results",
308
- "See Table 1. The headline number is 84%.",
316
+ "\\section{Results}",
317
+ "See Table~1. The headline number is 84\\%.",
309
318
  "",
310
- "## 4 Discussion",
319
+ "\\section{Discussion}",
311
320
  "The result generalizes beyond our corpus in the sense that the mechanism is not corpus-specific,",
312
321
  "though of course the specific percentages are, and it is important to note in this context that",
313
322
  "the framing itself may be what carries, rather than the measurement.",
314
323
  "",
315
- "## 5 Threats to Validity",
324
+ "\\section{Threats to Validity}",
316
325
  "Our corpus is drawn from public repositories and may not represent private configurations.",
317
326
  "",
318
- "## 6 Related Work",
327
+ "\\section{Related Work}",
319
328
  "TODO",
320
329
  "",
321
- "## 7 Conclusion",
330
+ "\\section{Conclusion}",
322
331
  "Prose is not policy.",
332
+ "\\end{document}",
323
333
  ].join("\n"),
324
334
  "paper/repro/README.md":
325
- "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.md.\n",
335
+ "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.tex.\n",
326
336
  "paper/PIPELINE-STATUS.md":
327
337
  "# Pipeline status\n\n| gate | state |\n|---|---|\n| numbers | pass |\n| structure | not run |\n| citations | not run |\n",
328
338
  };