paperlint 4.0.0 → 4.2.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 (236) hide show
  1. package/.github/workflows/ci.yml +16 -23
  2. package/CLAUDE.md +55 -11
  3. package/CONTRIBUTING.md +30 -7
  4. package/README.md +11 -8
  5. package/dist/adapters/banal/invocation.d.ts +1 -1
  6. package/dist/adapters/latex/document-class.d.ts +23 -0
  7. package/dist/adapters/latex/document-class.d.ts.map +1 -0
  8. package/dist/adapters/latex/document-class.js +110 -0
  9. package/dist/adapters/latex/document-class.js.map +1 -0
  10. package/dist/adapters/latex/hidden.d.ts +16 -0
  11. package/dist/adapters/latex/hidden.d.ts.map +1 -0
  12. package/dist/adapters/latex/hidden.js +49 -0
  13. package/dist/adapters/latex/hidden.js.map +1 -0
  14. package/dist/adapters/latex/includes.d.ts +12 -0
  15. package/dist/adapters/latex/includes.d.ts.map +1 -0
  16. package/dist/adapters/latex/includes.js +42 -0
  17. package/dist/adapters/latex/includes.js.map +1 -0
  18. package/dist/adapters/latex/index.d.ts +33 -0
  19. package/dist/adapters/latex/index.d.ts.map +1 -0
  20. package/dist/adapters/latex/index.js +25 -0
  21. package/dist/adapters/latex/index.js.map +1 -0
  22. package/dist/adapters/latex/nodes.d.ts +58 -0
  23. package/dist/adapters/latex/nodes.d.ts.map +1 -0
  24. package/dist/adapters/latex/nodes.js +97 -0
  25. package/dist/adapters/latex/nodes.js.map +1 -0
  26. package/dist/adapters/latex/outline.d.ts +10 -0
  27. package/dist/adapters/latex/outline.d.ts.map +1 -0
  28. package/dist/adapters/latex/outline.js +39 -0
  29. package/dist/adapters/latex/outline.js.map +1 -0
  30. package/dist/adapters/latex/parse.d.ts +42 -0
  31. package/dist/adapters/latex/parse.d.ts.map +1 -0
  32. package/dist/adapters/latex/parse.js +95 -0
  33. package/dist/adapters/latex/parse.js.map +1 -0
  34. package/dist/adapters/latex/prose.d.ts +8 -0
  35. package/dist/adapters/latex/prose.d.ts.map +1 -0
  36. package/dist/adapters/latex/prose.js +353 -0
  37. package/dist/adapters/latex/prose.js.map +1 -0
  38. package/dist/adapters/latex/rendered.d.ts +10 -0
  39. package/dist/adapters/latex/rendered.d.ts.map +1 -0
  40. package/dist/adapters/latex/rendered.js +39 -0
  41. package/dist/adapters/latex/rendered.js.map +1 -0
  42. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  43. package/dist/adapters/pdfjs/fill.js +12 -12
  44. package/dist/adapters/pdfjs/fill.js.map +1 -1
  45. package/dist/build.js +4 -4
  46. package/dist/build.js.map +1 -1
  47. package/dist/claim-provenance.d.ts +34 -0
  48. package/dist/claim-provenance.d.ts.map +1 -0
  49. package/dist/claim-provenance.js +146 -0
  50. package/dist/claim-provenance.js.map +1 -0
  51. package/dist/cli.d.ts.map +1 -1
  52. package/dist/cli.js +49 -5
  53. package/dist/cli.js.map +1 -1
  54. package/dist/domain/paper-source.d.ts +58 -0
  55. package/dist/domain/paper-source.d.ts.map +1 -0
  56. package/dist/domain/paper-source.js +109 -0
  57. package/dist/domain/paper-source.js.map +1 -0
  58. package/dist/domain/sentences.d.ts +26 -0
  59. package/dist/domain/sentences.d.ts.map +1 -0
  60. package/dist/domain/sentences.js +48 -0
  61. package/dist/domain/sentences.js.map +1 -0
  62. package/dist/domain/tex-document.d.ts +119 -0
  63. package/dist/domain/tex-document.d.ts.map +1 -0
  64. package/dist/domain/tex-document.js +43 -0
  65. package/dist/domain/tex-document.js.map +1 -0
  66. package/dist/eslint-rules/paper-typography.d.ts +7 -0
  67. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  68. package/dist/eslint-rules/paper-typography.js +53 -1
  69. package/dist/eslint-rules/paper-typography.js.map +1 -1
  70. package/dist/facts-file.d.ts.map +1 -1
  71. package/dist/facts-file.js +2 -2
  72. package/dist/facts-file.js.map +1 -1
  73. package/dist/new-paper.d.ts +5 -0
  74. package/dist/new-paper.d.ts.map +1 -1
  75. package/dist/new-paper.js +3 -0
  76. package/dist/new-paper.js.map +1 -1
  77. package/dist/package-dirs.d.ts +5 -0
  78. package/dist/package-dirs.d.ts.map +1 -0
  79. package/dist/package-dirs.js +14 -0
  80. package/dist/package-dirs.js.map +1 -0
  81. package/dist/ports/latex.d.ts +35 -0
  82. package/dist/ports/latex.d.ts.map +1 -0
  83. package/dist/ports/latex.js +2 -0
  84. package/dist/ports/latex.js.map +1 -0
  85. package/dist/presets.d.ts +15 -2
  86. package/dist/presets.d.ts.map +1 -1
  87. package/dist/presets.js +19 -6
  88. package/dist/presets.js.map +1 -1
  89. package/dist/register.d.ts +40 -0
  90. package/dist/register.d.ts.map +1 -0
  91. package/dist/register.js +134 -0
  92. package/dist/register.js.map +1 -0
  93. package/dist/tex-paper.d.ts +19 -0
  94. package/dist/tex-paper.d.ts.map +1 -0
  95. package/dist/tex-paper.js +105 -0
  96. package/dist/tex-paper.js.map +1 -0
  97. package/dist/tex-requirements.d.ts +11 -0
  98. package/dist/tex-requirements.d.ts.map +1 -1
  99. package/dist/tex-requirements.js +14 -8
  100. package/dist/tex-requirements.js.map +1 -1
  101. package/dist/tex-venue-rules.d.ts +88 -0
  102. package/dist/tex-venue-rules.d.ts.map +1 -0
  103. package/dist/tex-venue-rules.js +268 -0
  104. package/dist/tex-venue-rules.js.map +1 -0
  105. package/dist/venue-rules.d.ts +1 -1
  106. package/dist/venue-rules.d.ts.map +1 -1
  107. package/docs/configuration.md +3 -2
  108. package/docs/e2e.md +69 -40
  109. package/docs/install.md +1 -1
  110. package/docs/prior-art/README.md +1 -1
  111. package/docs/prior-art/package-location.md +5 -5
  112. package/docs/prior-art/repro/README.md +1 -1
  113. package/docs/prior-art/test-tooling.md +21 -10
  114. package/docs/rules/tex/claim-provenance.md +83 -0
  115. package/docs/rules/tex/missing-input.md +58 -0
  116. package/docs/rules/tex/register.md +77 -0
  117. package/docs/rules/tex/required-section.md +75 -0
  118. package/docs/rules/tex/template.md +74 -0
  119. package/docs/rules/tex/venue-leftover.md +73 -0
  120. package/docs/rules.md +73 -48
  121. package/docs/testing.md +17 -4
  122. package/docs/texlive-install-decision.md +1 -1
  123. package/docs/toolchain.md +2 -2
  124. package/eslint-rules/paper-typography.test.mjs +48 -1
  125. package/eslint-rules/paper-typography.ts +72 -1
  126. package/eslint.config.mjs +156 -15
  127. package/fixtures/build-e2e/acmart/expect.json +26 -0
  128. package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
  129. package/fixtures/build-e2e/aidc/expect.json +14 -0
  130. package/fixtures/build-e2e/aidc/paper.tex +46 -0
  131. package/fixtures/build-e2e/aidc/paperlint.json +1 -0
  132. package/fixtures/build-e2e/broken/expect.json +11 -0
  133. package/fixtures/build-e2e/cite/expect.json +27 -0
  134. package/fixtures/build-e2e/empty/expect.json +12 -0
  135. package/fixtures/build-e2e/fallback/expect.json +53 -0
  136. package/fixtures/build-e2e/guards/expect.json +6 -0
  137. package/fixtures/build-e2e/no-source/expect.json +10 -0
  138. package/fixtures/build-e2e/unbalanced/expect.json +60 -0
  139. package/fixtures/pdf-facts/README.md +1 -1
  140. package/fixtures/real-markdown-paper/baseline.ts +8 -7
  141. package/fixtures/render-paper/build-clean.sh +3 -5
  142. package/package.json +14 -2
  143. package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
  144. package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
  145. package/presets/aidc.jsonc +73 -0
  146. package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
  147. package/presets/ieee-conference.jsonc +66 -0
  148. package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
  149. package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
  150. package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
  151. package/scripts/check.harness.ts +64 -10
  152. package/scripts/check.test.ts +60 -13
  153. package/scripts/check.ts +83 -30
  154. package/scripts/test.test.ts +1 -1
  155. package/scripts/test.ts +1 -1
  156. package/skills/cold-read-diff/SKILL.md +31 -10
  157. package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
  158. package/skills/draft-paper/SKILL.md +2 -2
  159. package/skills/draft-paper/SKILL.md.spec.ts +1 -1
  160. package/skills/grade-paper-writing/SKILL.md +109 -44
  161. package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
  162. package/skills/harden-paper/SKILL.md +35 -36
  163. package/skills/harden-paper/SKILL.md.spec.ts +34 -35
  164. package/skills/paper-adversarial-review/SKILL.md +2 -2
  165. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  166. package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
  167. package/skills/paper-pipeline/SKILL.md +4 -4
  168. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -3
  169. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
  170. package/skills/paper-pipeline/references/writing-craft.md +53 -18
  171. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
  172. package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
  173. package/skills/pc-panel-review/SKILL.md +36 -11
  174. package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
  175. package/skills/render-paper/SKILL.md +2 -2
  176. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  177. package/skills/render-paper/check-render.sh +16 -63
  178. package/skills/render-paper/render-paper.harness.mjs +17 -59
  179. package/src/adapters/banal/index.test.ts +1 -1
  180. package/src/adapters/banal/invocation.ts +1 -1
  181. package/src/adapters/banal/output.test.ts +1 -1
  182. package/src/adapters/banal/run.test.ts +1 -1
  183. package/src/adapters/banal/xml.test.ts +1 -1
  184. package/src/adapters/latex/document-class.ts +155 -0
  185. package/src/adapters/latex/hidden.ts +70 -0
  186. package/src/adapters/latex/includes.ts +66 -0
  187. package/src/adapters/latex/index.test.ts +535 -0
  188. package/src/adapters/latex/index.ts +65 -0
  189. package/src/adapters/latex/nodes.ts +150 -0
  190. package/src/adapters/latex/outline.ts +64 -0
  191. package/src/adapters/latex/parse.ts +137 -0
  192. package/src/adapters/latex/prose.ts +484 -0
  193. package/src/adapters/latex/rendered.ts +59 -0
  194. package/src/adapters/pdfjs/fill.test.ts +1 -1
  195. package/src/adapters/pdfjs/fill.ts +18 -10
  196. package/src/build.harness.mjs +5 -7
  197. package/src/build.ts +4 -4
  198. package/src/claim-provenance.test.ts +352 -0
  199. package/src/claim-provenance.ts +217 -0
  200. package/src/cli.harness.mjs +1 -1
  201. package/src/cli.test.ts +2 -8
  202. package/src/cli.ts +53 -8
  203. package/src/domain/paper-source.test.ts +156 -0
  204. package/src/domain/paper-source.ts +233 -0
  205. package/src/domain/sentences.ts +67 -0
  206. package/src/domain/tex-document.test.ts +80 -0
  207. package/src/domain/tex-document.ts +143 -0
  208. package/src/engine.harness.mjs +1 -1
  209. package/src/facts-file.test.ts +4 -7
  210. package/src/facts-file.ts +2 -2
  211. package/src/latex-loop.harness.ts +1 -1
  212. package/src/new-paper.test.ts +28 -3
  213. package/src/new-paper.ts +8 -0
  214. package/src/package-dirs.ts +17 -0
  215. package/src/paper-settings-commands.test.ts +6 -1
  216. package/src/ports/latex.ts +42 -0
  217. package/src/presets.test.ts +65 -4
  218. package/src/presets.ts +38 -15
  219. package/src/register.test.ts +165 -0
  220. package/src/register.ts +177 -0
  221. package/src/rule-cases.test.ts +524 -0
  222. package/src/rule-docs.test.ts +159 -0
  223. package/src/tex-paper.test.ts +131 -0
  224. package/src/tex-paper.ts +152 -0
  225. package/src/tex-requirements.harness.ts +13 -23
  226. package/src/tex-requirements.ts +35 -8
  227. package/src/tex-venue-rules.test.ts +741 -0
  228. package/src/tex-venue-rules.ts +461 -0
  229. package/src/toolchain.harness.mjs +1 -1
  230. package/src/venue-rules.test.ts +96 -4
  231. package/src/venue-rules.ts +1 -1
  232. package/templates/paper/paper.tex +2 -1
  233. package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
  234. package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
  235. /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
  236. /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
package/scripts/check.ts CHANGED
@@ -34,9 +34,11 @@
34
34
  * (no TeX, no network), and the tail names every skip. "I ran check" then means one thing.
35
35
  */
36
36
  import { spawnSync } from "node:child_process";
37
- import { existsSync } from "node:fs";
37
+ import { existsSync, readFileSync, rmSync } from "node:fs";
38
+ import { tmpdir } from "node:os";
38
39
  import { delimiter, dirname, join } from "node:path";
39
40
  import { fileURLToPath } from "node:url";
41
+ import { z } from "zod";
40
42
  import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
41
43
 
42
44
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -72,11 +74,17 @@ type GateCommand =
72
74
  readonly script: string;
73
75
  readonly run?: undefined;
74
76
  readonly inCi?: RunInCi;
77
+ /**
78
+ * The script is a vitest run, so its JSON report says how many tests were SKIPPED: a run
79
+ * whose exit is 0 and whose report counts a skip is a skip, not a pass.
80
+ */
81
+ readonly vitest?: true;
75
82
  }
76
83
  | {
77
84
  readonly run: readonly [string, ...string[]];
78
85
  readonly script?: undefined;
79
86
  readonly inCi?: undefined;
87
+ readonly vitest?: undefined;
80
88
  };
81
89
 
82
90
  /**
@@ -160,22 +168,14 @@ export const GATES: readonly Gate[] = [
160
168
  {
161
169
  name: "install e2e — pack, install under npm and pnpm, run the binary",
162
170
  job: "gates",
163
- run: ["node", "test/e2e/install.ts"],
171
+ script: "test:e2e:install",
172
+ vitest: true,
164
173
  },
165
174
  {
166
- name: "build e2e — a real pdflatex, and the PDF's fonts are measured",
175
+ name: "TeX e2e — real TeX Live: paperlint toolchain, a real pdflatex build of every fixture, the real banal",
167
176
  job: "build-e2e",
168
- run: ["node", "test/e2e/build.ts"],
169
- },
170
- {
171
- name: "banal e2e — the real banal on pdf.js-written XML gives banal-on-pdftohtml's numbers",
172
- job: "build-e2e",
173
- run: ["node", "test/e2e/banal.ts"],
174
- },
175
- {
176
- name: "toolchain e2e — real TeX Live into $PAPERLINT_TEXLIVE_DIR, then a build with only it on PATH",
177
- job: "build-e2e",
178
- run: ["node", "test/e2e/toolchain.ts"],
177
+ script: "test:e2e:tex",
178
+ vitest: true,
179
179
  },
180
180
  ];
181
181
 
@@ -205,18 +205,41 @@ export const NOT_COVERED: Readonly<Record<string, string>> = {
205
205
  };
206
206
 
207
207
  /**
208
- * Exit 77 means "declared skip" — the same code vigiles' runner uses (`SKIP_EXIT_CODE`). The e2e steps exit 77 when a tool they need is
209
- * absent and --strict is off. Anything else nonzero is a failure.
208
+ * vitest's JSON report, as far as the verdict reads it: skipped (`pending`) and `todo` tests.
210
209
  *
211
- * 🔴 A SKIP IS A THIRD OUTCOME. Until the Codex review on #45 the e2e steps exited 0 on a skip,
212
- * this loop counted that as a pass, and `skipped` below was declared and never filled: a machine
213
- * without pnpm or TeX printed "all gates passed" for runs that never happened.
210
+ * 🔴 A SKIP IS A THIRD OUTCOME. Until the Codex review on #45 the e2e runs exited 0 on a skip and
211
+ * this loop counted that as a pass: a machine without pnpm or TeX printed "all gates passed" for
212
+ * runs that never happened. The count comes from the runner's own report, not from an exit code
213
+ * the tests would have to agree on.
214
214
  */
215
- export const SKIP_EXIT: number = 77;
216
- export function outcome(status: number | null): Outcome {
217
- if (status === 0) return "pass";
218
- if (status === SKIP_EXIT) return "skip";
219
- return "fail";
215
+ const VitestReport = z.looseObject({
216
+ numPendingTests: z.number().int().nonnegative(),
217
+ numTodoTests: z.number().int().nonnegative(),
218
+ });
219
+
220
+ /** How many tests a vitest JSON report says were not run, or null when it is not one. */
221
+ export function skippedIn(report: string | null): number | null {
222
+ if (report === null) return null;
223
+ try {
224
+ const r = VitestReport.safeParse(JSON.parse(report));
225
+ return r.success ? r.data.numPendingTests + r.data.numTodoTests : null;
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * A gate's outcome from its exit status and, for a vitest gate, the skip count its report gave
233
+ * (`null`: the report is missing or unreadable, which cannot be read as a pass).
234
+ */
235
+ export function outcome(
236
+ status: number | null,
237
+ vitest?: { readonly skipped: number | null },
238
+ ): Outcome {
239
+ if (status !== 0) return "fail";
240
+ if (vitest === undefined) return "pass";
241
+ if (vitest.skipped === null) return "fail";
242
+ return vitest.skipped > 0 ? "skip" : "pass";
220
243
  }
221
244
 
222
245
  /** The gates that did not pass, each as the line the verdict prints, split by outcome. */
@@ -225,11 +248,29 @@ interface NotPassed {
225
248
  skipped: string[];
226
249
  }
227
250
 
251
+ /** A report file's text, read once and removed; null when the run wrote none. */
252
+ export type ReadReport = (file: string) => string | null;
253
+ const readReport: ReadReport = (file) => {
254
+ if (!existsSync(file)) return null;
255
+ const text = readFileSync(file, "utf8");
256
+ rmSync(file, { force: true });
257
+ return text;
258
+ };
259
+
260
+ /** The arguments that make a vitest gate write its JSON report to `file`, beside the usual output. */
261
+ export const reportArgs = (file: string): string[] => [
262
+ "--",
263
+ "--reporter=default",
264
+ "--reporter=json",
265
+ `--outputFile.json=${file}`,
266
+ ];
267
+
228
268
  /** Run each gate in order; `write` announces it before it starts. */
229
269
  function runEach(
230
270
  gates: readonly Gate[],
231
271
  spawn: Spawn,
232
272
  write: (text: string) => unknown,
273
+ reportOf: ReadReport,
233
274
  ): NotPassed {
234
275
  const BIN_FIRST_PATH = [join(ROOT, "node_modules", ".bin"), process.env.PATH]
235
276
  .filter(Boolean)
@@ -237,10 +278,14 @@ function runEach(
237
278
  const failed: string[] = [];
238
279
  const skipped: string[] = [];
239
280
 
240
- for (const g of gates) {
281
+ for (const [i, g] of gates.entries()) {
241
282
  write(`── ${g.name}\n`);
242
283
  const [cmd, ...args] = commandOf(g);
243
- const r = spawn(cmd, args, {
284
+ const file = join(
285
+ tmpdir(),
286
+ `paperlint-check-${String(process.pid)}-${String(i)}.json`,
287
+ );
288
+ const r = spawn(cmd, g.vitest ? [...args, ...reportArgs(file)] : args, {
244
289
  cwd: ROOT,
245
290
  stdio: "inherit",
246
291
  encoding: "utf8",
@@ -249,12 +294,18 @@ function runEach(
249
294
  // 🔴 The exit code is read from the command itself, never from a pipe. `cmd | tail && …`
250
295
  // reports the FILTER's status, which is almost always zero — that is how a red harness
251
296
  // shipped on 2026-09-16.
252
- const o = outcome(r.status);
297
+ const skips = g.vitest ? { skipped: skippedIn(reportOf(file)) } : undefined;
298
+ const o = outcome(r.status, skips);
253
299
  if (o === "pass") continue;
254
300
  const shown = commandOf(g).join(" ");
255
301
  if (o === "skip")
256
- skipped.push(`${g.name} (${shown} → ${String(SKIP_EXIT)})`);
257
- else failed.push(`${g.name} (${shown} → ${String(r.status)})`);
302
+ skipped.push(
303
+ `${g.name} (${shown} → ${String(skips?.skipped)} test(s) skipped)`,
304
+ );
305
+ else
306
+ failed.push(
307
+ `${g.name} (${shown} → ${r.status === 0 ? "exit 0, but no vitest JSON report to read skips from" : String(r.status)})`,
308
+ );
258
309
  }
259
310
  return { failed, skipped };
260
311
  }
@@ -299,14 +350,16 @@ export function runGates({
299
350
  log = console.log,
300
351
  err = console.error,
301
352
  write = (s: string) => process.stdout.write(s),
353
+ reportOf = readReport,
302
354
  }: {
303
355
  gates?: readonly Gate[];
304
356
  spawn?: Spawn;
305
357
  log?: (line: string) => void;
306
358
  err?: (line: string) => void;
307
359
  write?: (text: string) => unknown;
360
+ reportOf?: ReadReport;
308
361
  } = {}): number {
309
- const notPassed = runEach(gates, spawn, write);
362
+ const notPassed = runEach(gates, spawn, write, reportOf);
310
363
  report(gates, notPassed, { log, err });
311
364
  return notPassed.failed.length ? 1 : 0;
312
365
  }
@@ -24,7 +24,7 @@ test("a failing vitest does not skip the harnesses, and is named as the failure"
24
24
  1,
25
25
  );
26
26
  assert.deepEqual(calls, [
27
- ["vitest", "run"],
27
+ ["vitest", "run", "--project", "unit"],
28
28
  ["vigiles", "test", "--min=1", "--no-skip"],
29
29
  ]);
30
30
  assert.deepEqual(said, [
package/scripts/test.ts CHANGED
@@ -46,7 +46,7 @@ export function withSources(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
46
46
  }
47
47
 
48
48
  export const SUITES: readonly Suite[] = [
49
- { name: "vitest", command: ["vitest", "run"] },
49
+ { name: "vitest", command: ["vitest", "run", "--project", "unit"] },
50
50
  { name: "harnesses", command: ["vigiles", "test", "--min=1"] },
51
51
  ];
52
52
 
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: cold-read-diff
3
- description: Send just-changed paper prose to a reader with NO context and ask what each sentence claims — the only check that catches a sentence which is short, true, jargon-free and still meaningless to anyone who does not already know the idea. Run after every prose edit to a paper, before calling the edit done, and before reporting a section as fixed. Scoped to the diff, so it is cheap enough to run every time. Not a writing grade (grade-paper-writing), not a structural pass (tighten-paper), not a defect review.
3
+ description: Send just-changed paper prose to two readers with NO context — an engineer who asks what each sentence claims, and a skimming workshop reviewer who asks whose result it states and whether it reads like a paper — the only check that catches a sentence which is short, true, jargon-free and still meaningless to anyone who does not already know the idea. Run after every prose edit to a paper, before calling the edit done, and before reporting a section as fixed. Scoped to the diff, so it is cheap enough to run every time. Not a writing grade (grade-paper-writing), not a structural pass (tighten-paper), not a defect review.
4
4
  context: fork
5
5
  allowed-tools: [Bash, Read, Agent]
6
6
  ---
7
7
 
8
- <!-- vigiles:sha256:e45391841e689aa9 compiled from skills/cold-read-diff/SKILL.md.spec.ts -->
8
+ <!-- vigiles:sha256:293f0627e6873774 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
 
@@ -57,21 +57,41 @@ one, and its confusion is the finding.
57
57
 
58
58
  1. **Take the diff.** `git diff -U0 -- <paper.md>` since the last commit, or the `new_string` of the
59
59
  edits just applied. Prose only — skip comments, tables, bibliography.
60
- 2. **Spawn ONE agent with no repository context.** Give it the changed paragraphs **as text in the
61
- prompt**, not as a file path. A path is an invitation to read the neighbours, and reading the
62
- neighbours is how the check fails silently. `sonnet` is the right model: competent, cheap,
63
- and not the one that wrote the sentence.
64
- 3. **Ask exactly two questions per sentence:**
60
+ 2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
61
+ changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
62
+ the neighbours, and reading the neighbours is how the check fails silently. `sonnet` is the right
63
+ model: competent, cheap, and not the one that wrote the sentence.
64
+
65
+ | persona | who | what only it can report |
66
+ |---|---|---|
67
+ | **the engineer** | a strong practitioner outside academia, reading closely | a sentence that parses only for a reader who already holds the idea |
68
+ | **the workshop reviewer** | a PC member at the target venue with 40 minutes for the whole paper, skimming the abstract, the introduction and the conclusion, Confidence 3 | a sentence whose result has no owner, a relation between claims left to infer, a sentence that does not read like a paper at this venue |
69
+
70
+ The engineer cannot see the second column: a sentence with no stated owner reads to him as
71
+ confident, and he does not know the venue's register. Both run, every time; neither replaces the
72
+ other. The reviewer is told the venue's name and nothing else. When the diff touches none of the
73
+ abstract, the introduction and the conclusion, the reviewer still reads it — as the paragraph a
74
+ skimming reviewer lands on.
75
+ 3. **Ask the engineer exactly two questions per sentence:**
65
76
  - *What does this sentence claim? Restate it in your own words.* If restating requires guessing,
66
77
  the answer is `CANNOT PARSE` plus what stopped them — an undefined term, a pronoun with no
67
78
  referent, a number with no denominator, two clauses that do not connect.
68
79
  - *Would a reader who stops here understand it?* Yes / No / Only-if-they-know-X.
80
+
81
+ **Ask the reviewer two different ones:**
82
+ - *Whose result does this sentence state — the authors' measurement, a cited work, or a property
83
+ of the world?* If it cannot tell, the answer is `WHOSE?` with the sentence quoted.
84
+ - *Does this read like a paper at this venue?* If not, `REGISTER` with the sentence quoted and
85
+ what a paper there would write instead — an aphorism, a fragment, a colloquial turn, or two
86
+ claims side by side with the relation between them unstated.
69
87
  4. **Ask for the dangerous class explicitly:** *which sentences sound clever but say less than they
70
88
  appear to?* Those read fine and carry nothing, and they are the ones a knowing reader waves
71
89
  through.
72
90
  5. **Apply the fixes — as rewrites of the whole thought, never as excisions.** See the rule below.
73
- 6. **Re-run on the fix.** An edit is not done until the same kind of reader parses the replacement.
74
- One confirming round; do not loop past it.
91
+ 6. **Re-run on the fix, with both personas.** An edit is not done until the engineer parses the
92
+ replacement AND the reviewer finds its owner and its register. A fix for the engineer that goes
93
+ colloquial is a new `REGISTER` finding, and the reverse holds too. One confirming round; do not
94
+ loop past it.
75
95
 
76
96
  ## 🔴 Hand the reader WHOLE PARAGRAPHS, never `+` lines
77
97
 
@@ -115,7 +135,8 @@ findings:
115
135
 
116
136
  - 🔴 **No context in the prompt. Ever.** Not the title, not the abstract, not "this is a paper about
117
137
  agent rules files". Every sentence of background handed to the reader is a sentence of confusion
118
- they will no longer report.
138
+ they will no longer report. The one exception is the venue's name, given to the reviewer persona:
139
+ judging register without knowing the venue is guessing.
119
140
  - 🔴 **A fix that removes the flagged construction and keeps the compression is not a fix.** Proven
120
141
  twice in one hour: *"not through any defect in how we write it, but because that is what English
121
142
  is"* → *"that is what English is for"* (tic gone, meaning inverted into nonsense); *"the agent
@@ -8,7 +8,7 @@ import { experimental_skill } from "vigiles/spec";
8
8
  export default experimental_skill({
9
9
  name: "cold-read-diff",
10
10
  description:
11
- "Send just-changed paper prose to a reader with NO context and ask what each sentence claims — the only check that catches a sentence which is short, true, jargon-free and still meaningless to anyone who does not already know the idea. Run after every prose edit to a paper, before calling the edit done, and before reporting a section as fixed. Scoped to the diff, so it is cheap enough to run every time. Not a writing grade (grade-paper-writing), not a structural pass (tighten-paper), not a defect review.",
11
+ "Send just-changed paper prose to two readers with NO context — an engineer who asks what each sentence claims, and a skimming workshop reviewer who asks whose result it states and whether it reads like a paper — the only check that catches a sentence which is short, true, jargon-free and still meaningless to anyone who does not already know the idea. Run after every prose edit to a paper, before calling the edit done, and before reporting a section as fixed. Scoped to the diff, so it is cheap enough to run every time. Not a writing grade (grade-paper-writing), not a structural pass (tighten-paper), not a defect review.",
12
12
  context: "fork",
13
13
  tools: ["Bash", "Read", "Agent"],
14
14
  body: `
@@ -62,21 +62,41 @@ one, and its confusion is the finding.
62
62
 
63
63
  1. **Take the diff.** \`git diff -U0 -- <paper.md>\` since the last commit, or the \`new_string\` of the
64
64
  edits just applied. Prose only — skip comments, tables, bibliography.
65
- 2. **Spawn ONE agent with no repository context.** Give it the changed paragraphs **as text in the
66
- prompt**, not as a file path. A path is an invitation to read the neighbours, and reading the
67
- neighbours is how the check fails silently. \`sonnet\` is the right model: competent, cheap,
68
- and not the one that wrote the sentence.
69
- 3. **Ask exactly two questions per sentence:**
65
+ 2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
66
+ changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
67
+ the neighbours, and reading the neighbours is how the check fails silently. \`sonnet\` is the right
68
+ model: competent, cheap, and not the one that wrote the sentence.
69
+
70
+ | persona | who | what only it can report |
71
+ |---|---|---|
72
+ | **the engineer** | a strong practitioner outside academia, reading closely | a sentence that parses only for a reader who already holds the idea |
73
+ | **the workshop reviewer** | a PC member at the target venue with 40 minutes for the whole paper, skimming the abstract, the introduction and the conclusion, Confidence 3 | a sentence whose result has no owner, a relation between claims left to infer, a sentence that does not read like a paper at this venue |
74
+
75
+ The engineer cannot see the second column: a sentence with no stated owner reads to him as
76
+ confident, and he does not know the venue's register. Both run, every time; neither replaces the
77
+ other. The reviewer is told the venue's name and nothing else. When the diff touches none of the
78
+ abstract, the introduction and the conclusion, the reviewer still reads it — as the paragraph a
79
+ skimming reviewer lands on.
80
+ 3. **Ask the engineer exactly two questions per sentence:**
70
81
  - *What does this sentence claim? Restate it in your own words.* If restating requires guessing,
71
82
  the answer is \`CANNOT PARSE\` plus what stopped them — an undefined term, a pronoun with no
72
83
  referent, a number with no denominator, two clauses that do not connect.
73
84
  - *Would a reader who stops here understand it?* Yes / No / Only-if-they-know-X.
85
+
86
+ **Ask the reviewer two different ones:**
87
+ - *Whose result does this sentence state — the authors' measurement, a cited work, or a property
88
+ of the world?* If it cannot tell, the answer is \`WHOSE?\` with the sentence quoted.
89
+ - *Does this read like a paper at this venue?* If not, \`REGISTER\` with the sentence quoted and
90
+ what a paper there would write instead — an aphorism, a fragment, a colloquial turn, or two
91
+ claims side by side with the relation between them unstated.
74
92
  4. **Ask for the dangerous class explicitly:** *which sentences sound clever but say less than they
75
93
  appear to?* Those read fine and carry nothing, and they are the ones a knowing reader waves
76
94
  through.
77
95
  5. **Apply the fixes — as rewrites of the whole thought, never as excisions.** See the rule below.
78
- 6. **Re-run on the fix.** An edit is not done until the same kind of reader parses the replacement.
79
- One confirming round; do not loop past it.
96
+ 6. **Re-run on the fix, with both personas.** An edit is not done until the engineer parses the
97
+ replacement AND the reviewer finds its owner and its register. A fix for the engineer that goes
98
+ colloquial is a new \`REGISTER\` finding, and the reverse holds too. One confirming round; do not
99
+ loop past it.
80
100
 
81
101
  ## 🔴 Hand the reader WHOLE PARAGRAPHS, never \`+\` lines
82
102
 
@@ -120,7 +140,8 @@ findings:
120
140
 
121
141
  - 🔴 **No context in the prompt. Ever.** Not the title, not the abstract, not "this is a paper about
122
142
  agent rules files". Every sentence of background handed to the reader is a sentence of confusion
123
- they will no longer report.
143
+ they will no longer report. The one exception is the venue's name, given to the reviewer persona:
144
+ judging register without knowing the venue is guessing.
124
145
  - 🔴 **A fix that removes the flagged construction and keeps the compression is not a fix.** Proven
125
146
  twice in one hour: *"not through any defect in how we write it, but because that is what English
126
147
  is"* → *"that is what English is for"* (tic gone, meaning inverted into nonsense); *"the agent
@@ -4,7 +4,7 @@ description: Use when the numbers exist and it's time to WRITE the paper — "dr
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Agent, Skill]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:46bb85bfaa45e595 compiled from skills/draft-paper/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:c8d25c21dc01a742 compiled from skills/draft-paper/SKILL.md.spec.ts -->
8
8
 
9
9
  # draft-paper — prose that survives review and gets cited
10
10
 
@@ -78,7 +78,7 @@ grade.
78
78
 
79
79
  ## Run the prose through the PAPER suite's QA (do not skip — and do not use the blog skills)
80
80
  Before any review pass, run the draft through the paper-side prose QA — it catches what a self-read won't:
81
- - **`grade-paper-writing`** — the 9-dim rubric + the PERSONA cold-read stall pass (a committed
81
+ - **`grade-paper-writing`** — the 10-dim rubric + the PERSONA cold-read stall pass (a committed
82
82
  non-academic persona, per-section — the localized "wait, what does that even mean?" axis authors are
83
83
  blind to). Its avoid-list grep also strips the AI-writing tropes (hedge-stacking, "it's worth
84
84
  noting", limp connectives, fake balance) that make a draft read unserious to a human PC.
@@ -84,7 +84,7 @@ grade.
84
84
 
85
85
  ## Run the prose through the PAPER suite's QA (do not skip — and do not use the blog skills)
86
86
  Before any review pass, run the draft through the paper-side prose QA — it catches what a self-read won't:
87
- - **\`grade-paper-writing\`** — the 9-dim rubric + the PERSONA cold-read stall pass (a committed
87
+ - **\`grade-paper-writing\`** — the 10-dim rubric + the PERSONA cold-read stall pass (a committed
88
88
  non-academic persona, per-section — the localized "wait, what does that even mean?" axis authors are
89
89
  blind to). Its avoid-list grep also strips the AI-writing tropes (hedge-stacking, "it's worth
90
90
  noting", limp connectives, fake balance) that make a draft read unserious to a human PC.