paperlint 7.2.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 (156) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +1 -6
  3. package/dist/build.js.map +1 -1
  4. package/dist/cli.d.ts +3 -4
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +13 -42
  7. package/dist/cli.js.map +1 -1
  8. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  9. package/dist/eslint-rules/latex-language.js +10 -13
  10. package/dist/eslint-rules/latex-language.js.map +1 -1
  11. package/dist/eslint-rules/paper-context.js +2 -2
  12. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  13. package/dist/eslint-rules/paper-typography.js +22 -91
  14. package/dist/eslint-rules/paper-typography.js.map +1 -1
  15. package/dist/eslint-rules/papers.d.ts +1 -3
  16. package/dist/eslint-rules/papers.d.ts.map +1 -1
  17. package/dist/eslint-rules/papers.js +2 -4
  18. package/dist/eslint-rules/papers.js.map +1 -1
  19. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  20. package/dist/eslint-rules/tex-build.js +8 -25
  21. package/dist/eslint-rules/tex-build.js.map +1 -1
  22. package/dist/init.d.ts +1 -4
  23. package/dist/init.d.ts.map +1 -1
  24. package/dist/init.js +3 -9
  25. package/dist/init.js.map +1 -1
  26. package/dist/lib/markdown.d.ts +5 -81
  27. package/dist/lib/markdown.d.ts.map +1 -1
  28. package/dist/lib/markdown.js +5 -116
  29. package/dist/lib/markdown.js.map +1 -1
  30. package/dist/lib/paper-config.d.ts.map +1 -1
  31. package/dist/lib/paper-config.js +0 -2
  32. package/dist/lib/paper-config.js.map +1 -1
  33. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  34. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  35. package/dist/lib/skill-eval-fixture.js +25 -18
  36. package/dist/lib/skill-eval-fixture.js.map +1 -1
  37. package/dist/new-paper.d.ts +5 -11
  38. package/dist/new-paper.d.ts.map +1 -1
  39. package/dist/new-paper.js +12 -23
  40. package/dist/new-paper.js.map +1 -1
  41. package/dist/paper-files.d.ts +4 -4
  42. package/dist/paper-prose.d.ts +17 -0
  43. package/dist/paper-prose.d.ts.map +1 -0
  44. package/dist/paper-prose.js +46 -0
  45. package/dist/paper-prose.js.map +1 -0
  46. package/dist/structure.d.ts.map +1 -1
  47. package/dist/structure.js +4 -3
  48. package/dist/structure.js.map +1 -1
  49. package/dist/types.d.ts +0 -2
  50. package/dist/types.d.ts.map +1 -1
  51. package/docs/configuration.md +7 -9
  52. package/docs/e2e.md +4 -6
  53. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  54. package/docs/rules/tex/claim-provenance.md +0 -1
  55. package/docs/rules.md +2 -6
  56. package/eslint-rules/latex-language.harness.mjs +3 -3
  57. package/eslint-rules/latex-language.ts +10 -13
  58. package/eslint-rules/paper-context.ts +2 -2
  59. package/eslint-rules/paper-typography.test.mjs +17 -85
  60. package/eslint-rules/paper-typography.ts +22 -98
  61. package/eslint-rules/papers.harness.mjs +1 -1
  62. package/eslint-rules/papers.ts +2 -4
  63. package/eslint-rules/tex-build.harness.mjs +2 -2
  64. package/eslint-rules/tex-build.ts +8 -25
  65. package/eslint.config.mjs +0 -1
  66. package/hooks/hooks.harness.ts +15 -15
  67. package/hooks/paper-edit-guard.hook.mjs +5 -8
  68. package/hooks/paper-edit-guard.test.ts +12 -3
  69. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  70. package/hooks/paper-skills-nudge.test.ts +13 -3
  71. package/hooks/paper-status-gates.hook.mjs +2 -2
  72. package/hooks/paper-status-gates.test.ts +14 -2
  73. package/lib/markdown.test.mjs +5 -60
  74. package/lib/markdown.ts +9 -136
  75. package/lib/paper-config.ts +0 -2
  76. package/lib/skill-checks.test.mjs +4 -1
  77. package/lib/skill-eval-fixture.ts +25 -18
  78. package/package.json +1 -3
  79. package/presets/realm.jsonc +1 -1
  80. package/scripts/harness-api.frozen.json +0 -5
  81. package/skills/cold-read-diff/SKILL.md +3 -3
  82. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  83. package/skills/grade-paper-writing/SKILL.md +14 -22
  84. package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
  85. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  86. package/skills/grade-paper-writing/prose-lint.mjs +108 -278
  87. package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
  88. package/skills/paper-adversarial-review/SKILL.md +2 -2
  89. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  90. package/skills/paper-pipeline/SKILL.md +9 -12
  91. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  92. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  93. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  94. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  95. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  96. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  97. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  98. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  99. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  100. package/skills/paper-pipeline/scripts/README.md +0 -4
  101. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
  102. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
  103. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
  104. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  105. package/skills/paper-status/SKILL.md +2 -2
  106. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  107. package/skills/render-paper/SKILL.md +2 -2
  108. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  109. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  110. package/skills/tighten-paper/SKILL.md +29 -42
  111. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  112. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
  113. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
  114. package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
  115. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  116. package/src/build.harness.mjs +2 -2
  117. package/src/build.test.ts +1 -1
  118. package/src/build.ts +1 -6
  119. package/src/cli.harness.mjs +26 -46
  120. package/src/cli.test.ts +32 -40
  121. package/src/cli.ts +14 -54
  122. package/src/folder-venue-rule.test.ts +1 -1
  123. package/src/init.test.ts +6 -7
  124. package/src/init.ts +3 -22
  125. package/src/lint-fix.test.ts +10 -4
  126. package/src/new-paper.harness.mjs +9 -28
  127. package/src/new-paper.test.ts +6 -6
  128. package/src/new-paper.ts +11 -30
  129. package/src/paper-files.test.ts +5 -4
  130. package/src/paper-files.ts +4 -4
  131. package/src/paper-prose.test.ts +49 -0
  132. package/src/paper-prose.ts +67 -0
  133. package/src/parse-args.test.ts +0 -1
  134. package/src/structure.harness.ts +5 -11
  135. package/src/structure.ts +4 -3
  136. package/src/types.ts +0 -2
  137. package/fixtures/real-markdown-paper/baseline.json +0 -22
  138. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  139. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  140. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  141. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  142. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  143. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  144. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  145. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  146. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  147. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  148. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  149. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  150. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  151. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  152. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  153. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  154. package/skills/tighten-paper/structure.mjs +0 -364
  155. package/skills/tighten-paper/structure.test.ts +0 -117
  156. package/templates/paper/paper.md +0 -4
@@ -15,7 +15,7 @@ const dir = hookConsumer(useTempDir("paper-edit-guard-"), {
15
15
  });
16
16
  /** `>` built rather than typed: a literal one here would be a redirection to the guard's own scan. */
17
17
  const GT = String.fromCharCode(62);
18
- const P = "papers/alpha/paper.md";
18
+ const P = "papers/alpha/paper.tex";
19
19
 
20
20
  /** The guard's exit code for one Bash command: 2 denies, 0 lets it through. */
21
21
  const guard = (command: string): number =>
@@ -35,8 +35,8 @@ test("a `>` inside quotes, after an escaped quote, or escaped itself is not a re
35
35
  test("a redirection target is read through backslashes, quoted or not, and ends at a delimiter", () => {
36
36
  assert.deepEqual(
37
37
  [
38
- guard(`echo x ${GT} "papers/alpha/pa\\per.md"`),
39
- guard(`echo x ${GT} papers/alpha/pa\\per.md`),
38
+ guard(`echo x ${GT} "papers/alpha/pa\\per.tex"`),
39
+ guard(`echo x ${GT} papers/alpha/pa\\per.tex`),
40
40
  guard(`echo x ${GT}${P}; true`),
41
41
  ],
42
42
  [2, 2, 2],
@@ -55,3 +55,12 @@ test("a command cut off right after a backslash still names its target, quoted o
55
55
  [2, 2],
56
56
  );
57
57
  });
58
+
59
+ test("only paper.tex is a paper source: copying over another file of the paper is let through", () => {
60
+ // Guards: the extension filter of the argv leg. (A redirection is refused for ANY file under the
61
+ // papers root, so the argv leg is the one that says which files are sources.)
62
+ assert.deepEqual(
63
+ [guard("cp /tmp/x papers/alpha/notes.txt"), guard(`cp /tmp/x ${P}`)],
64
+ [0, 2],
65
+ );
66
+ });
@@ -96,9 +96,8 @@ const papersRoot = (rawConfig) => {
96
96
  return root;
97
97
  };
98
98
 
99
- /** A paper SOURCE — a `.tex`, or a markdown-drafted `paper.md` / `draft.md`. Not every note or README. */
100
- const isPaperSource = (p) =>
101
- p.endsWith(".tex") || /\/(paper|draft)\.md$/.test(p);
99
+ /** A paper SOURCE — a `.tex`. Not every note or README. */
100
+ const isPaperSource = (p) => p.endsWith(".tex");
102
101
 
103
102
  const CHECKLIST = `📄 Editing a paper — drive it with the \`paper-pipeline\` skill, don't wing the review.
104
103
  → Update <paper-dir>/PIPELINE-STATUS.md: mark the row for any stage you (re)ran, and read its
@@ -10,11 +10,11 @@ import { useTempDir } from "../test/support.ts";
10
10
 
11
11
  const root = useTempDir("paper-skills-nudge-");
12
12
 
13
- test("no paperlint.json: an edit to papers/<p>/paper.md gets the checklist", () => {
13
+ test("no paperlint.json: an edit to papers/<p>/paper.tex gets the checklist", () => {
14
14
  const dir = hookConsumer(join(root, "default"));
15
15
  const r = runShippedHook(
16
16
  "paper-skills-nudge",
17
- onEdit("papers/alpha/paper.md"),
17
+ onEdit("papers/alpha/paper.tex"),
18
18
  dir,
19
19
  );
20
20
  assert.equal(r.exitCode, 0);
@@ -27,7 +27,17 @@ test("a paperlint.json that does not parse: silent, exit 0", () => {
27
27
  });
28
28
  const r = runShippedHook(
29
29
  "paper-skills-nudge",
30
- onEdit("papers/alpha/paper.md"),
30
+ onEdit("papers/alpha/paper.tex"),
31
+ dir,
32
+ );
33
+ assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
34
+ });
35
+
36
+ test("an edit to a file of the paper other than its source: silent", () => {
37
+ const dir = hookConsumer(join(root, "not-a-source"));
38
+ const r = runShippedHook(
39
+ "paper-skills-nudge",
40
+ onEdit("papers/alpha/notes.txt"),
31
41
  dir,
32
42
  );
33
43
  assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
@@ -31,7 +31,7 @@
31
31
  * harness's own edit helper builds a RELATIVE path. The live harness sends an ABSOLUTE one.
32
32
  * Measured against the real runtime:
33
33
  *
34
- * file_path "<root>/<paper>/paper.md" → fires
34
+ * file_path "<root>/<paper>/paper.tex" → fires
35
35
  * file_path "/abs/path/to/repo/<root>/<paper>/…" → SILENT
36
36
  *
37
37
  * So the hook would have been dead in production and green in the tests: precisely the
@@ -120,7 +120,7 @@ const papersRoot = (rawConfig) => {
120
120
  */
121
121
  const paperSourceRe = (root) =>
122
122
  new RegExp(
123
- `(?:^|/)${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([A-Za-z0-9._-]+)/(?:[^/]*\\.tex|(?:.*/)?(?:paper|draft)\\.md)$`,
123
+ `(?:^|/)${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([A-Za-z0-9._-]+)/[^/]*\\.tex$`,
124
124
  );
125
125
 
126
126
  /**
@@ -17,7 +17,7 @@ test("no paperlint.json: an edit under papers/ runs the gates for that paper", (
17
17
  });
18
18
  const r = runShippedHook(
19
19
  "paper-status-gates",
20
- onEdit("papers/alpha/paper.md"),
20
+ onEdit("papers/alpha/paper.tex"),
21
21
  dir,
22
22
  );
23
23
  assert.equal(r.exitCode, 0);
@@ -31,7 +31,19 @@ test("a paperlint.json that does not parse: silent, exit 0", () => {
31
31
  });
32
32
  const r = runShippedHook(
33
33
  "paper-status-gates",
34
- onEdit("papers/alpha/paper.md"),
34
+ onEdit("papers/alpha/paper.tex"),
35
+ dir,
36
+ );
37
+ assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
38
+ });
39
+
40
+ test("an edit to a file of the paper other than its source: no gates run", () => {
41
+ const dir = hookConsumer(join(root, "not-a-source"), {
42
+ "papers/alpha/PIPELINE-STATUS.md": STATUS,
43
+ });
44
+ const r = runShippedHook(
45
+ "paper-status-gates",
46
+ onEdit("papers/alpha/notes.txt"),
35
47
  dir,
36
48
  );
37
49
  assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * markdown.mjs — the markup readers every script shares, on the shapes their callers rely on:
3
3
  * frontmatter blanked (offsets kept) or cut, headings from the parser (not from a `#` inside a
4
- * fence), sections by level, fences, paragraphs and tables. And the one thing a test run never
4
+ * fence), and fences. And the one thing a test run never
5
5
  * sees by accident: markdown-it failing to resolve, where every reader must degrade to its
6
6
  * documented empty value and `requireMarkdown` must refuse with exit 2.
7
7
  */
@@ -17,11 +17,8 @@ import {
17
17
  frontmatterBlock,
18
18
  headings,
19
19
  requireMarkdown,
20
- paragraphs,
21
- splitSections,
22
20
  stripFences,
23
21
  stripFrontmatter,
24
- tables,
25
22
  } from "./markdown.ts";
26
23
 
27
24
  const DOC = [
@@ -93,27 +90,6 @@ test("an empty ATX heading has empty text", () => {
93
90
  );
94
91
  });
95
92
 
96
- test("splitSections: a preamble always first, then each heading of the level range", () => {
97
- assert.deepEqual(
98
- splitSections(DOC, { min: 2, max: 2 }).map((s) => [
99
- s.heading?.text ?? null,
100
- s.body.split("\n")[0],
101
- ]),
102
- [
103
- [null, "---"],
104
- ["One", "Body one."],
105
- ["Two", "last"],
106
- ],
107
- );
108
- assert.deepEqual(
109
- splitSections("## A").map((s) => [s.heading?.text ?? null, s.body]),
110
- [
111
- [null, ""],
112
- ["A", ""],
113
- ],
114
- );
115
- });
116
-
117
93
  test("fences and indented code, as content; stripped as lines, or blanked in place", () => {
118
94
  const src = "a\n```\nx\n```\n\n y\nb";
119
95
  assert.deepEqual(
@@ -126,30 +102,6 @@ test("fences and indented code, as content; stripped as lines, or blanked in pla
126
102
  );
127
103
  });
128
104
 
129
- test("paragraphs are line ranges; tables are rows of cells, the alignment row skipped", () => {
130
- assert.deepEqual(
131
- { paragraphs: paragraphs(DOC), tables: tables(DOC) },
132
- {
133
- paragraphs: [
134
- { line: 6, endLine: 7 },
135
- { line: 9, endLine: 10 },
136
- { line: 13, endLine: 14 },
137
- { line: 27, endLine: 28 },
138
- ],
139
- tables: [
140
- {
141
- line: 22,
142
- endLine: 25,
143
- rows: [
144
- ["a", "b"],
145
- ["1", "2"],
146
- ],
147
- },
148
- ],
149
- },
150
- );
151
- });
152
-
153
105
  const root = useTempDir("markdown-no-parser-");
154
106
  writeTree(root, {
155
107
  // Registered with --import: resolving `markdown-it` fails, as it does with no node_modules.
@@ -159,7 +111,7 @@ writeTree(root, {
159
111
  "probe.mjs":
160
112
  `const m = await import(${JSON.stringify(join(dirname(fileURLToPath(import.meta.url)), "markdown.ts"))});\n` +
161
113
  "const t = '# H\\n\\n```\\nx\\n```\\n';\n" +
162
- "console.log(JSON.stringify([m.MD_AVAILABLE(), m.headings(t), m.fences(t), m.stripFences(t), m.paragraphs(t), m.tables(t)]));\n" +
114
+ "console.log(JSON.stringify([m.MD_AVAILABLE(), m.headings(t), m.fences(t), m.stripFences(t)]));\n" +
163
115
  "m.requireMarkdown();\n" +
164
116
  "console.log('not reached');\n",
165
117
  "probe-plugin.mjs":
@@ -188,22 +140,15 @@ test("with no markdown-it, every reader degrades to its empty value and requireM
188
140
  });
189
141
  assert.deepEqual(r, {
190
142
  status: 2,
191
- stdout:
192
- JSON.stringify([false, [], [], "# H\n\n```\nx\n```\n", [], []]) + "\n",
143
+ stdout: JSON.stringify([false, [], [], "# H\n\n```\nx\n```\n"]) + "\n",
193
144
  stderr:
194
145
  "markdown-it does not resolve — this script parses markup with a parser and without it would produce a confident zero instead of an error.\n" +
195
146
  " Cured by `npm i` at the repo root.\n",
196
147
  });
197
148
  });
198
149
 
199
- test("splitSections of a document with no heading is the preamble alone; requireMarkdown is silent with a parser", () => {
200
- assert.deepEqual(
201
- [
202
- splitSections("just text").map((s) => [s.heading, s.body]),
203
- requireMarkdown(),
204
- ],
205
- [[[null, "just text"]], undefined],
206
- );
150
+ test("requireMarkdown is silent with a parser", () => {
151
+ assert.equal(requireMarkdown(), undefined);
207
152
  });
208
153
 
209
154
  test("headings read a token stream that breaks markdown-it's shape as empty text and offset 0", () => {
package/lib/markdown.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * markdown.mjs — markup parsing for EVERYTHING in this repo that reads markdown.
3
3
  *
4
- * Three kinds of consumer: two advisory linters (`kb-lint`, `paper-lint` — through the
5
- * re-export from `lint-core.mjs`) and skill scripts (`tighten-paper/structure.mjs`,
6
- * `grade-paper-writing/prose-lint.mjs`, `paper-pipeline/scripts/*`). Hence the home is
7
- * here and not in the linter core: importing "the linter core" from a skill script
8
- * would be a lie about the direction of the dependency.
4
+ * Its consumers read the package's own markdown — skills' `SKILL.md` frontmatter
5
+ * (`skill-corpus.mjs`), fenced commands — and a consumer's linters of its own files.
6
+ * Hence the home is here and not in a linter core: importing "the linter core" from a
7
+ * skill script would be a lie about the direction of the dependency.
9
8
  *
10
9
  * CHECKS NOTHING AND PRINTS NOTHING: text in, data out. What to do with a missing
11
10
  * parser is the caller's decision, and the decisions DIFFER:
@@ -43,13 +42,6 @@ export interface Heading {
43
42
  readonly offset: number;
44
43
  }
45
44
 
46
- /** A chunk of the document: the heading it starts with (`null` for the preamble), with and without it. */
47
- export interface Section {
48
- readonly heading: Heading | null;
49
- readonly raw: string;
50
- readonly body: string;
51
- }
52
-
53
45
  /** A block tokenizer with markdown-it's shape — the default, or one a test passes. */
54
46
  export type Parse = (text: string) => readonly Token[];
55
47
 
@@ -169,56 +161,6 @@ export function headings(
169
161
  return out;
170
162
  }
171
163
 
172
- /**
173
- * Slicing the document into chunks "a heading + everything up to the next heading FROM
174
- * THE SAME RANGE of levels". → `[{ heading, raw, body }]`.
175
- *
176
- * This is exactly what four scripts did as `split(/^(?=## )/m)` and
177
- * `split(/^(?=#{2,3} )/m)`. The shape of the expression lied about the intent: "two or
178
- * three hashes" is about CHARACTERS, while what is needed is the LEVEL, and `#{2,3}`
179
- * also caught a hash inside a ```-block, that is, it cut a quotation from someone
180
- * else's paper as a section of its own.
181
- *
182
- * `heading` — `null` on the zeroth element (the preamble before the first heading). It
183
- * is ALWAYS there, including empty: the former `split` also always returned the chunk
184
- * before the first separator as its zeroth element, and callers rely on that
185
- * (`sub === chunk` in `artifact-coverage` meant "there are no subsections").
186
- *
187
- * `raw` — WITH the heading line (it is used to count `§` references and bold numbers,
188
- * which occur in the heading itself too), `body` — without it (it is used to count words).
189
- *
190
- * 2026-08-11: moved here rather than copied into every script, because the chunk
191
- * boundary is the one place where an off-by-one is easy, and such a mistake has already
192
- * happened (`recordBlock` in `skill-corpus.mjs` returned a single "#" character for all
193
- * 21 skills).
194
- */
195
- export function splitSections(
196
- text: string,
197
- { min = 2, max = 6 }: { min?: number; max?: number } = {},
198
- ): Section[] {
199
- const hs = headings(text).filter((h) => h.depth >= min && h.depth <= max);
200
- const out: Section[] = [];
201
- const [first] = hs;
202
- const preEnd = first ? first.offset : text.length;
203
- out.push({
204
- heading: null,
205
- raw: text.slice(0, preEnd),
206
- body: text.slice(0, preEnd),
207
- });
208
- for (const [i, h] of hs.entries()) {
209
- const next = hs[i + 1];
210
- const end = next ? next.offset : text.length;
211
- const nl = text.indexOf("\n", h.offset);
212
- const bodyStart = nl === -1 || nl + 1 > end ? end : nl + 1;
213
- out.push({
214
- heading: h,
215
- raw: text.slice(h.offset, end),
216
- body: text.slice(bodyStart, end),
217
- });
218
- }
219
- return out;
220
- }
221
-
222
164
  /**
223
165
  * The text without the contents of ```-blocks (the fence lines themselves go too).
224
166
  *
@@ -229,14 +171,11 @@ export function splitSections(
229
171
  * `blank: true` — the block's lines are NOT deleted but REPLACED with empty ones. The
230
172
  * difference is not cosmetic: deleting the lines GLUES the paragraph before the block to
231
173
  * the paragraph after it, and a code block is a block boundary — on the other side a
232
- * different paragraph starts. Measured 2026-08-11 on `<papers-root>/<paper>/README.md`:
233
- * `prose-lint` counted 82 sentences there, and after the lines were deleted it counted
234
- * 81 — the lead paragraph "**Reproduction commands:**" merged with the text AFTER the
235
- * block into one sentence. That is exactly the failure against which `prose-lint` itself
236
- * says "Paragraph and heading boundaries END a sentence", and it inflates the
237
- * claims-per-sentence counter — the threshold at which the file emits findings. The
238
- * former regex preserved the boundary (it left one newline on each side), so `blank` is
239
- * also a preservation of the former behaviour where that behaviour was right.
174
+ * different paragraph starts. Measured 2026-08-11 on a README: a sentence counter counted
175
+ * 82 sentences there, and after the lines were deleted it counted 81 — the lead paragraph
176
+ * "**Reproduction commands:**" merged with the text AFTER the block into one sentence.
177
+ * The former regex preserved the boundary (it left one newline on each side), so `blank`
178
+ * is also a preservation of the former behaviour where that behaviour was right.
240
179
  */
241
180
  /**
242
181
  * The CONTENTS of fenced blocks — the other side of `stripFences`.
@@ -304,69 +243,3 @@ export function requireMarkdown({
304
243
  );
305
244
  process.exit(2);
306
245
  }
307
-
308
- /**
309
- * GFM tables — rows, cells and the line range, WITH THE PARSER.
310
- *
311
- * The only consumer as of 2026-08-20 is `crosspost-article`, which needs to re-lay a
312
- * table into a monospace block for a platform without tables (Medium). It lives here
313
- * and not in the skill, by the rule "markup is parsed with a parser, and in one place":
314
- * with a regex a table is told apart from a line of text with pipes only by its
315
- * neighbours, and that is exactly the class that has already cost this repo 27 places.
316
- *
317
- * Returns `{ line, endLine, rows }`, where `rows[0]` is the header; the alignment row
318
- * (`| --- |`) is thrown away, because in monospace output it has nothing to express.
319
- */
320
- /**
321
- * The document's paragraphs — `paragraph_open` line ranges, WITH THE PARSER.
322
- * `[{ line, endLine }]`.
323
- *
324
- * The only consumer as of 2026-08-21 is `crosspost-article`: the blog's sources are
325
- * hard-wrapped at a column (an editorial habit, not markdown), and the platforms render
326
- * a single `\n` inside a paragraph DIFFERENTLY — dev.to (Redcarpet) keeps it as a
327
- * literal line break, and the paragraph arrives torn into short lines exactly at the
328
- * source file's boundaries. Medium has no such defect not because the platform is more
329
- * honest, but because the paste goes through "Import a story" — the importer takes the
330
- * ALREADY RENDERED page of the site (where the breaks were collapsed by the browser long
331
- * ago), not the raw markdown.
332
- *
333
- * The range is EXACTLY the one `t.map` gives on `paragraph_open` — that is, a code
334
- * fence, a heading, a table, a list (in a TIGHT list the item is not wrapped in a
335
- * `paragraph_open` at all, it has no paragraph of its own) fall outside this function
336
- * automatically, by the construction of the parser, and not by a separate line-type
337
- * check.
338
- */
339
- export function paragraphs(text: string): { line: number; endLine: number }[] {
340
- if (md === null) return [];
341
- return md
342
- .parse(blankFrontmatter(text), {})
343
- .flatMap((t) =>
344
- t.type === "paragraph_open" && t.map
345
- ? [{ line: t.map[0], endLine: t.map[1] }]
346
- : [],
347
- );
348
- }
349
-
350
- export function tables(
351
- text: string,
352
- ): { line: number; endLine: number; rows: string[][] }[] {
353
- if (md === null) return [];
354
- const lines = blankFrontmatter(text).split("\n");
355
- const out: { line: number; endLine: number; rows: string[][] }[] = [];
356
- for (const t of md.parse(blankFrontmatter(text), {})) {
357
- if (t.type !== "table_open" || !t.map) continue;
358
- const rows: string[][] = [];
359
- for (const raw of lines.slice(t.map[0], t.map[1])) {
360
- if (/^\s*\|?[\s:|-]+\|?\s*$/.test(raw) && raw.includes("-")) continue; // alignment
361
- rows.push(
362
- raw
363
- .replace(/^\s*\|/, "")
364
- .replace(/\|\s*$/, "")
365
- .split("|")
366
- .map((c) => c.trim()),
367
- );
368
- }
369
- out.push({ line: t.map[0], endLine: t.map[1], rows });
370
- }
371
- return out;
372
- }
@@ -49,8 +49,6 @@ export const CONFIG_FILE: string = "paperlint.json";
49
49
  */
50
50
  export const PAPER_MARKERS: readonly string[] = Object.freeze([
51
51
  "paper.tex",
52
- "paper.md",
53
- "draft.md",
54
52
  "PIPELINE-STATUS.md",
55
53
  ]);
56
54
 
@@ -236,7 +236,10 @@ test("a Bash entry narrowed to the instructed script permits it, and one narrowe
236
236
  await brokenBy("verify-citations", narrowTo("extract-ref-facts.mjs")),
237
237
  null,
238
238
  );
239
- const lines = await brokenBy("verify-citations", narrowTo("round-diff.mjs"));
239
+ const lines = await brokenBy(
240
+ "verify-citations",
241
+ narrowTo("generated-code.mjs"),
242
+ );
240
243
  assert.equal(
241
244
  lines?.[0],
242
245
  '2 assertion(s) failed for skill "verify-citations":',
@@ -10,54 +10,61 @@
10
10
  * and the zero was an artifact of the empty directory, not a fact about the skill.
11
11
  *
12
12
  * Extracted from `paper-pipeline/pipeline-firing.eval.mjs`, which had it right
13
- * from the start, so the two runners cannot drift apart on what the model sees.
13
+ * from the start; the `paper-pipeline/*.eval.mjs` experiments carry the same
14
+ * block, byte for byte.
14
15
  *
15
16
  * ⚠️ AND THE PRICE, so this file is not a one-sided story (measured 2026-08-12).
16
17
  * With a fixture present the model OPENS THE PAPER and looks around, so the number
17
18
  * stops being "did the DESCRIPTION fire" and becomes "did the model go investigate
18
19
  * and then fire". A different quantity, and a costlier one: ~590k → ~780k tokens
19
- * per skill (+190k) and 67s → 84s, while this whole fixture is 1,537 characters
20
- * ≈ 384 tokens. The growth is reading, not input size (`cache` 470k → 652k).
20
+ * per skill (+190k) and 67s → 84s, while the fixture measured then was 1,537
21
+ * characters ≈ 384 tokens. The growth is reading, not input size (`cache`
22
+ * 470k → 652k).
21
23
  * Cite the trigger numbers with that caveat attached — it is written out in
22
24
  * `vigiles/repro/skill-trigger-2026-08-11/README.md` under "How to read".
23
25
  */
24
26
  export const FIXTURE = {
25
- "paper/paper.md": [
26
- "# Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable",
27
+ "paper/paper.tex": [
28
+ "\\documentclass{article}",
29
+ "\\title{Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable}",
30
+ "\\begin{document}",
31
+ "\\maketitle",
27
32
  "",
28
- "## Abstract",
33
+ "\\begin{abstract}",
29
34
  "Agent configuration files state rules in prose and assume the model obeys them. We compile a",
30
- "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84%",
35
+ "corpus of real rules and measure what fraction can be mechanically enforced. We find that 84\\%",
31
36
  "of rules in our corpus are enforceable, and that LLM-authored checkers for the remainder leak",
32
- "silently in 84-96% of adversarial cases \\cite{greshake2023}.",
37
+ "silently in 84--96\\% of adversarial cases~\\cite{greshake2023}.",
38
+ "\\end{abstract}",
33
39
  "",
34
- "## 1 Introduction",
40
+ "\\section{Introduction}",
35
41
  "Every agent harness ships a natural-language rulebook. Nothing checks it. This is the same",
36
- "mistake as a code comment that claims an invariant no test enforces \\cite{thompson1984}.",
42
+ "mistake as a code comment that claims an invariant no test enforces~\\cite{thompson1984}.",
37
43
  "",
38
- "## 2 Method",
44
+ "\\section{Method}",
39
45
  "We gather rules from public repositories, classify each by enforceability, and build a",
40
46
  "two-stage adversarial gate that validates a synthesized rule against a blind gold set.",
41
47
  "",
42
- "## 3 Results",
43
- "See Table 1. The headline number is 84%.",
48
+ "\\section{Results}",
49
+ "See Table~1. The headline number is 84\\%.",
44
50
  "",
45
- "## 4 Discussion",
51
+ "\\section{Discussion}",
46
52
  "The result generalizes beyond our corpus in the sense that the mechanism is not corpus-specific,",
47
53
  "though of course the specific percentages are, and it is important to note in this context that",
48
54
  "the framing itself may be what carries, rather than the measurement.",
49
55
  "",
50
- "## 5 Threats to Validity",
56
+ "\\section{Threats to Validity}",
51
57
  "Our corpus is drawn from public repositories and may not represent private configurations.",
52
58
  "",
53
- "## 6 Related Work",
59
+ "\\section{Related Work}",
54
60
  "TODO",
55
61
  "",
56
- "## 7 Conclusion",
62
+ "\\section{Conclusion}",
57
63
  "Prose is not policy.",
64
+ "\\end{document}",
58
65
  ].join("\n"),
59
66
  "paper/repro/README.md":
60
- "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.md.\n",
67
+ "# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.tex.\n",
61
68
  "paper/PIPELINE-STATUS.md":
62
69
  "# Pipeline status\n\n| gate | state |\n|---|---|\n| numbers | pass |\n| structure | not run |\n| citations | not run |\n",
63
70
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "7.2.0",
3
+ "version": "8.0.0",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -114,8 +114,6 @@
114
114
  "eslint.config.mjs",
115
115
  "fixtures",
116
116
  "!fixtures/accepted-papers",
117
- "!fixtures/real-markdown-paper",
118
- "fixtures/real-markdown-paper/baseline.*",
119
117
  "hooks",
120
118
  "lib",
121
119
  "presets",
@@ -72,7 +72,7 @@
72
72
  //
73
73
  // A gate on banal's number would give 13 against a limit of 8 and would redden on a paper that
74
74
  // is within the limit. For an `error`-level rule that is the worst outcome. The limit stays
75
- // owned by the counter inside `build-submission.sh`: it reads the markdown source and therefore
75
+ // owned by the counter inside `build-submission.sh`: it reads the paper's source and therefore
76
76
  // KNOWS where the body ends, which the PDF does not.
77
77
  //
78
78
  // The numbers below are DATA for that counter and for humans, not an input to the rule. Source:
@@ -10,7 +10,6 @@
10
10
  "eslint-rules/pdf-last-page-balance.harness.mjs",
11
11
  "eslint-rules/temp-root-realpath.harness.mjs",
12
12
  "eslint-rules/tex-build.harness.mjs",
13
- "fixtures/real-markdown-paper/real-markdown-paper.harness.ts",
14
13
  "lib/agent-cli-version.harness.mjs",
15
14
  "lib/paper-config.harness.mjs",
16
15
  "lib/skill-trigger-cases.harness.mjs",
@@ -30,13 +29,9 @@
30
29
  "skills/osf-artifact-upload/osf-artifact-upload.harness.ts",
31
30
  "skills/paper-adversarial-review/paper-adversarial-review.harness.mjs",
32
31
  "skills/paper-pipeline/paper-pipeline.harness.mjs",
33
- "skills/paper-pipeline/scripts/artifact-coverage.harness.mjs",
34
32
  "skills/paper-pipeline/scripts/consumer.harness.mjs",
35
33
  "skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs",
36
34
  "skills/paper-pipeline/scripts/generated-code.harness.mjs",
37
- "skills/paper-pipeline/scripts/population-map.harness.mjs",
38
- "skills/paper-pipeline/scripts/provenance.harness.mjs",
39
- "skills/paper-pipeline/scripts/round-diff.harness.ts",
40
35
  "skills/paper-status/paper-status.harness.mjs",
41
36
  "skills/pc-panel-review/pc-panel-review.harness.mjs",
42
37
  "skills/plan-paper-timeline/plan-paper-timeline.harness.mjs",
@@ -5,7 +5,7 @@ context: fork
5
5
  allowed-tools: [Bash, Read, Agent]
6
6
  ---
7
7
 
8
- <!-- vigiles:sha256:293f0627e6873774 compiled from skills/cold-read-diff/SKILL.md.spec.ts -->
8
+ <!-- vigiles:sha256:c3a1f2644781eb6f compiled from skills/cold-read-diff/SKILL.md.spec.ts -->
9
9
 
10
10
  # cold-read-diff — the reader who cannot fake understanding
11
11
 
@@ -55,7 +55,7 @@ one, and its confusion is the finding.
55
55
 
56
56
  ## How to run it
57
57
 
58
- 1. **Take the diff.** `git diff -U0 -- <paper.md>` since the last commit, or the `new_string` of the
58
+ 1. **Take the diff.** `git diff -U0 -- <paper.tex>` since the last commit, or the `new_string` of the
59
59
  edits just applied. Prose only — skip comments, tables, bibliography.
60
60
  2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
61
61
  changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
@@ -105,7 +105,7 @@ Two more things the same run proved:
105
105
  - **Expand build-time macros first.** The reader saw seven raw `{{placeholders}}` and correctly
106
106
  reported that every headline number was missing. That is a true statement about the extract and a
107
107
  false one about the paper.
108
- - **Strip working comments.** He graded `<!-- JUSTIFY … -->` notes as prose because he could not tell
108
+ - **Strip working comments.** He graded `% JUSTIFY …` comment notes as prose because he could not tell
109
109
  them apart — and said so, which is itself a finding about the extract.
110
110
 
111
111
  **So:** take the changed lines, then widen each to its enclosing paragraph, expand macros, drop
@@ -60,7 +60,7 @@ one, and its confusion is the finding.
60
60
 
61
61
  ## How to run it
62
62
 
63
- 1. **Take the diff.** \`git diff -U0 -- <paper.md>\` since the last commit, or the \`new_string\` of the
63
+ 1. **Take the diff.** \`git diff -U0 -- <paper.tex>\` since the last commit, or the \`new_string\` of the
64
64
  edits just applied. Prose only — skip comments, tables, bibliography.
65
65
  2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
66
66
  changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
@@ -110,7 +110,7 @@ Two more things the same run proved:
110
110
  - **Expand build-time macros first.** The reader saw seven raw \`{{placeholders}}\` and correctly
111
111
  reported that every headline number was missing. That is a true statement about the extract and a
112
112
  false one about the paper.
113
- - **Strip working comments.** He graded \`<!-- JUSTIFY … -->\` notes as prose because he could not tell
113
+ - **Strip working comments.** He graded \`% JUSTIFY …\` comment notes as prose because he could not tell
114
114
  them apart — and said so, which is itself a finding about the extract.
115
115
 
116
116
  **So:** take the changed lines, then widen each to its enclosing paragraph, expand macros, drop