paperlint 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/.github/workflows/ci.yml +28 -41
  2. package/.github/workflows/dependabot-automerge.yml +1 -1
  3. package/CLAUDE.md +51 -56
  4. package/CONTRIBUTING.md +30 -18
  5. package/README.md +179 -180
  6. package/bin/paperlint.test.mjs +45 -0
  7. package/dist/adapters/node/files.io.d.ts +2 -0
  8. package/dist/adapters/node/files.io.d.ts.map +1 -1
  9. package/dist/adapters/node/files.io.js +3 -2
  10. package/dist/adapters/node/files.io.js.map +1 -1
  11. package/dist/adapters/pdfjs/fill.d.ts +15 -4
  12. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  13. package/dist/adapters/pdfjs/fill.js +31 -21
  14. package/dist/adapters/pdfjs/fill.js.map +1 -1
  15. package/dist/adapters/references/index.d.ts.map +1 -1
  16. package/dist/adapters/references/index.js +14 -5
  17. package/dist/adapters/references/index.js.map +1 -1
  18. package/dist/adapters/references/pool.d.ts +8 -0
  19. package/dist/adapters/references/pool.d.ts.map +1 -0
  20. package/dist/adapters/references/pool.js +19 -0
  21. package/dist/adapters/references/pool.js.map +1 -0
  22. package/dist/build-engine.d.ts.map +1 -1
  23. package/dist/build-engine.js +6 -2
  24. package/dist/build-engine.js.map +1 -1
  25. package/dist/build.d.ts +10 -2
  26. package/dist/build.d.ts.map +1 -1
  27. package/dist/build.js +29 -11
  28. package/dist/build.js.map +1 -1
  29. package/dist/cli.d.ts +93 -6
  30. package/dist/cli.d.ts.map +1 -1
  31. package/dist/cli.js +296 -80
  32. package/dist/cli.js.map +1 -1
  33. package/dist/doctor.d.ts +27 -1
  34. package/dist/doctor.d.ts.map +1 -1
  35. package/dist/doctor.js +4 -3
  36. package/dist/doctor.js.map +1 -1
  37. package/dist/domain/result.d.ts +0 -2
  38. package/dist/domain/result.d.ts.map +1 -1
  39. package/dist/domain/result.js +0 -4
  40. package/dist/domain/result.js.map +1 -1
  41. package/dist/engine.d.ts +9 -0
  42. package/dist/engine.d.ts.map +1 -1
  43. package/dist/engine.js +27 -0
  44. package/dist/engine.js.map +1 -1
  45. package/dist/hooks-settings.d.ts +8 -5
  46. package/dist/hooks-settings.d.ts.map +1 -1
  47. package/dist/hooks-settings.js +32 -32
  48. package/dist/hooks-settings.js.map +1 -1
  49. package/dist/init.d.ts +8 -0
  50. package/dist/init.d.ts.map +1 -1
  51. package/dist/init.js +4 -4
  52. package/dist/init.js.map +1 -1
  53. package/dist/latex-log.d.ts +0 -2
  54. package/dist/latex-log.d.ts.map +1 -1
  55. package/dist/latex-log.js +7 -13
  56. package/dist/latex-log.js.map +1 -1
  57. package/dist/link-skills.d.ts +15 -2
  58. package/dist/link-skills.d.ts.map +1 -1
  59. package/dist/link-skills.js +36 -20
  60. package/dist/link-skills.js.map +1 -1
  61. package/dist/new-paper.d.ts +8 -1
  62. package/dist/new-paper.d.ts.map +1 -1
  63. package/dist/new-paper.js +50 -15
  64. package/dist/new-paper.js.map +1 -1
  65. package/dist/paper-files.d.ts +30 -8
  66. package/dist/paper-files.d.ts.map +1 -1
  67. package/dist/paper-files.js +59 -30
  68. package/dist/paper-files.js.map +1 -1
  69. package/dist/pdf-facts.d.ts +11 -0
  70. package/dist/pdf-facts.d.ts.map +1 -1
  71. package/dist/pdf-facts.js +16 -18
  72. package/dist/pdf-facts.js.map +1 -1
  73. package/dist/pdf-geometry.js +1 -1
  74. package/dist/pdf-geometry.js.map +1 -1
  75. package/dist/presets.d.ts +2 -0
  76. package/dist/presets.d.ts.map +1 -1
  77. package/dist/presets.js +3 -1
  78. package/dist/presets.js.map +1 -1
  79. package/dist/references.js +2 -2
  80. package/dist/references.js.map +1 -1
  81. package/dist/structure.js +1 -1
  82. package/dist/structure.js.map +1 -1
  83. package/dist/tex-requirements.d.ts +8 -0
  84. package/dist/tex-requirements.d.ts.map +1 -1
  85. package/dist/tex-requirements.js +6 -5
  86. package/dist/tex-requirements.js.map +1 -1
  87. package/dist/toolchain.d.ts +8 -3
  88. package/dist/toolchain.d.ts.map +1 -1
  89. package/dist/toolchain.js +26 -10
  90. package/dist/toolchain.js.map +1 -1
  91. package/dist/types.d.ts +6 -0
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/venue-rules.d.ts.map +1 -1
  94. package/dist/venue-rules.js +12 -10
  95. package/dist/venue-rules.js.map +1 -1
  96. package/docs/configuration.md +48 -4
  97. package/docs/prior-art/README.md +1 -1
  98. package/docs/prior-art/test-tooling.md +7 -30
  99. package/docs/rules.md +29 -25
  100. package/docs/skills.md +80 -0
  101. package/eslint-rules/install-path-literals.harness.mjs +17 -1
  102. package/eslint-rules/latex-language.harness.mjs +3 -3
  103. package/eslint-rules/latex-language.mjs +12 -9
  104. package/eslint-rules/latex-language.test.mjs +200 -0
  105. package/eslint-rules/paper-research-question.harness.mjs +73 -7
  106. package/eslint-rules/paper-stages.harness.mjs +9 -14
  107. package/eslint-rules/paper-stages.test.mjs +135 -0
  108. package/eslint-rules/paper-typography.mjs +56 -9
  109. package/eslint-rules/paper-typography.test.mjs +186 -1
  110. package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
  111. package/eslint-rules/review-frontmatter.mjs +2 -1
  112. package/eslint-rules/review-frontmatter.schema.json +1 -1
  113. package/eslint-rules/review-frontmatter.test.mjs +37 -1
  114. package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
  115. package/eslint-rules/tex-build.mjs +13 -13
  116. package/eslint.config.mjs +6 -2
  117. package/fixtures/toolchain-mirror/catalog.txt +1 -0
  118. package/fixtures/toolchain-mirror/install-tl +4 -1
  119. package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
  120. package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
  121. package/hooks/hooks.harness.mjs +6 -11
  122. package/hooks/paper-edit-guard.hook.mjs +2 -2
  123. package/hooks/paper-edit-guard.test.mjs +61 -0
  124. package/hooks/paper-skills-nudge.hook.mjs +2 -2
  125. package/hooks/paper-skills-nudge.test.mjs +38 -0
  126. package/hooks/paper-status-gates.test.mjs +42 -0
  127. package/lib/agent-cli-version.harness.mjs +7 -0
  128. package/lib/check.d.mts +17 -0
  129. package/lib/check.mjs +50 -0
  130. package/lib/check.test.ts +67 -0
  131. package/lib/markdown.mjs +16 -69
  132. package/lib/markdown.test.mjs +246 -0
  133. package/lib/paper-config.harness.mjs +28 -9
  134. package/lib/paper-config.mjs +7 -2
  135. package/lib/skill-checks.mjs +18 -46
  136. package/lib/skill-checks.test.mjs +297 -0
  137. package/lib/skill-corpus.mjs +11 -13
  138. package/lib/skill-corpus.test.mjs +54 -0
  139. package/lib/skill-trigger-cases.harness.mjs +57 -4
  140. package/package.json +6 -5
  141. package/scripts/check.harness.mjs +4 -10
  142. package/scripts/check.mjs +45 -42
  143. package/scripts/check.test.mjs +104 -0
  144. package/scripts/coverage-config.test.ts +406 -0
  145. package/scripts/eslint-report-guard.test.mjs +48 -0
  146. package/scripts/harness-api.frozen.json +0 -2
  147. package/scripts/layer-legacy-frozen.d.mts +9 -1
  148. package/scripts/layer-legacy-frozen.mjs +28 -11
  149. package/scripts/layer-legacy-frozen.test.ts +143 -1
  150. package/scripts/rules-are-content-only.harness.mjs +3 -11
  151. package/scripts/rules-are-content-only.mjs +29 -19
  152. package/scripts/rules-are-content-only.test.mjs +61 -0
  153. package/scripts/rules-see-files.mjs +11 -9
  154. package/scripts/rules-see-files.test.mjs +71 -0
  155. package/scripts/test.mjs +44 -0
  156. package/scripts/test.test.mjs +52 -0
  157. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
  158. package/skills/grade-paper-writing/prose-lint.mjs +31 -12
  159. package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
  160. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
  161. package/skills/paper-pipeline/scripts/README.md +13 -132
  162. package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
  163. package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
  164. package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
  165. package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
  166. package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
  167. package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
  168. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
  169. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
  170. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
  171. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
  172. package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
  173. package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
  174. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
  175. package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
  176. package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
  177. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
  178. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
  179. package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
  180. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
  181. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
  182. package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
  183. package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
  184. package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
  185. package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
  186. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
  187. package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
  188. package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
  189. package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
  190. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
  191. package/skills/paper-pipeline/scripts/status.mjs +2 -2
  192. package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
  193. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
  194. package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
  195. package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
  196. package/skills/tighten-paper/structure.test.mjs +116 -0
  197. package/skills/verify-citations/SKILL.md +5 -5
  198. package/skills/verify-citations/SKILL.md.spec.ts +4 -4
  199. package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
  200. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
  201. package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
  202. package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
  203. package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
  204. package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
  205. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
  206. package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
  207. package/src/adapters/banal/failure.test.ts +70 -1
  208. package/src/adapters/banal/index.test.ts +16 -0
  209. package/src/adapters/banal/output.test.ts +5 -0
  210. package/src/adapters/curl/download.test.ts +43 -1
  211. package/src/adapters/memory/index.test.ts +25 -0
  212. package/src/adapters/node/files.io.ts +3 -2
  213. package/src/adapters/node/files.test.ts +13 -1
  214. package/src/adapters/node/process.test.ts +38 -0
  215. package/src/adapters/pdfjs/fill.test.ts +34 -0
  216. package/src/adapters/pdfjs/fill.ts +44 -25
  217. package/src/adapters/references/index.test.ts +228 -0
  218. package/src/adapters/references/index.ts +21 -6
  219. package/src/adapters/references/pool.test.ts +31 -0
  220. package/src/adapters/references/pool.ts +24 -0
  221. package/src/build-engine.harness.mjs +3 -7
  222. package/src/build-engine.test.ts +137 -0
  223. package/src/build-engine.ts +6 -1
  224. package/src/build.harness.mjs +51 -11
  225. package/src/build.test.ts +362 -0
  226. package/src/build.ts +40 -14
  227. package/src/cli.harness.mjs +12 -19
  228. package/src/cli.test.ts +584 -0
  229. package/src/cli.ts +409 -94
  230. package/src/doctor.harness.mjs +3 -8
  231. package/src/doctor.test.ts +64 -0
  232. package/src/doctor.ts +12 -4
  233. package/src/domain/result.test.ts +4 -0
  234. package/src/domain/result.ts +0 -5
  235. package/src/engine.harness.mjs +77 -7
  236. package/src/engine.ts +35 -0
  237. package/src/facts-file.test.ts +110 -0
  238. package/src/hooks-settings.harness.mjs +43 -13
  239. package/src/hooks-settings.ts +41 -35
  240. package/src/init.test.ts +308 -5
  241. package/src/init.ts +4 -4
  242. package/src/latex-log.harness.mjs +32 -7
  243. package/src/latex-log.ts +6 -13
  244. package/src/latex-loop.harness.mjs +8 -7
  245. package/src/link-skills.harness.mjs +3 -8
  246. package/src/link-skills.test.ts +105 -0
  247. package/src/link-skills.ts +75 -21
  248. package/src/linted-set.test.ts +118 -0
  249. package/src/new-paper.harness.mjs +5 -7
  250. package/src/new-paper.test.ts +203 -2
  251. package/src/new-paper.ts +84 -17
  252. package/src/paper-files.test.ts +122 -5
  253. package/src/paper-files.ts +99 -39
  254. package/src/paper-settings-commands.test.ts +41 -5
  255. package/src/paper-settings.test.ts +20 -0
  256. package/src/parse-args.test.ts +48 -0
  257. package/src/pdf-facts.harness.mjs +3 -6
  258. package/src/pdf-facts.test.ts +144 -0
  259. package/src/pdf-facts.ts +17 -19
  260. package/src/pdf-geometry.harness.mjs +3 -7
  261. package/src/pdf-geometry.ts +1 -1
  262. package/src/presets.test.ts +2 -0
  263. package/src/presets.ts +7 -1
  264. package/src/references.test.ts +97 -0
  265. package/src/references.ts +2 -2
  266. package/src/rules-config.harness.mjs +10 -8
  267. package/src/rules-off.test.ts +111 -0
  268. package/src/structure.harness.mjs +5 -9
  269. package/src/structure.ts +1 -1
  270. package/src/tex-requirements.harness.mjs +34 -7
  271. package/src/tex-requirements.ts +12 -6
  272. package/src/toolchain.harness.mjs +44 -13
  273. package/src/toolchain.test.ts +280 -0
  274. package/src/toolchain.ts +37 -12
  275. package/src/types.ts +6 -0
  276. package/src/venue-rules.test.ts +63 -0
  277. package/src/venue-rules.ts +14 -12
  278. package/templates/paper/paperlint.json +1 -1
  279. package/action.mutations.mjs +0 -162
  280. package/eslint-rules/install-path-literals.mutations.mjs +0 -62
  281. package/eslint-rules/latex-language.mutations.mjs +0 -196
  282. package/eslint-rules/paper-research-question.mutations.mjs +0 -127
  283. package/eslint-rules/paper-stages.mutations.mjs +0 -81
  284. package/eslint-rules/papers.mutations.mjs +0 -186
  285. package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
  286. package/eslint-rules/tex-build.mutations.mjs +0 -258
  287. package/hooks/hooks.mutations.mjs +0 -337
  288. package/lib/agent-cli-version.mutations.mjs +0 -109
  289. package/lib/mutation-driver.harness.mjs +0 -227
  290. package/lib/mutation-driver.mjs +0 -397
  291. package/lib/mutation-driver.mutations.mjs +0 -68
  292. package/lib/paper-config.mutations.mjs +0 -143
  293. package/lib/skill-trigger-cases.mutations.mjs +0 -65
  294. package/scripts/check.mutations.mjs +0 -110
  295. package/scripts/exclusive.mjs +0 -138
  296. package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
  297. package/scripts/mutation-batteries-frozen.mjs +0 -238
  298. package/scripts/mutation-batteries.frozen.json +0 -108
  299. package/scripts/rules-are-content-only.mutations.mjs +0 -81
  300. package/scripts/rules-see-files.mutations.mjs +0 -131
  301. package/scripts/run-mutations.mjs +0 -100
  302. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
  303. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
  304. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
  305. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
  306. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
  307. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
  308. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
  309. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
  310. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
  311. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
  312. package/skills/skill-contract.mutations.mjs +0 -179
  313. package/src/build.mutations.mjs +0 -196
  314. package/src/cli.mutations.mjs +0 -414
  315. package/src/doctor.mutations.mjs +0 -175
  316. package/src/hooks-settings.mutations.mjs +0 -99
  317. package/src/link-skills.mutations.mjs +0 -140
  318. package/src/new-paper.mutations.mjs +0 -79
  319. package/src/structure.mutations.mjs +0 -83
@@ -0,0 +1,144 @@
1
+ /**
2
+ * pdf-facts.ts on document shapes real test PDFs do not carry, through a fake pdf.js document
3
+ * (the real pdf.js library for its transforms and operator codes): a font object that is not one,
4
+ * a font without a name or metrics, an item whose font never resolved, zero-scale transforms, a
5
+ * document with no pages, text with no font, and a failure that is not an Error.
6
+ */
7
+ import assert from "node:assert/strict";
8
+ import { getResolvedPDFJS } from "unpdf";
9
+ import { test } from "vitest";
10
+ import { factsOf, failureOf, rawFontOf } from "./pdf-facts.ts";
11
+
12
+ const lib = await getResolvedPDFJS();
13
+ const H = 792;
14
+
15
+ interface FakeItem {
16
+ str: string;
17
+ fontName: string;
18
+ transform: number[];
19
+ width: number;
20
+ height: number;
21
+ }
22
+ /** A page whose text items and loaded fonts are given; its viewport flips y like pdf.js's. */
23
+ function page(items: FakeItem[], fonts: Record<string, unknown>) {
24
+ const objs = new Map(Object.entries(fonts));
25
+ return {
26
+ getOperatorList: async () => ({ fnArray: [], argsArray: [] }),
27
+ getTextContent: async () => ({ items }),
28
+ // Iterable like pdf.js's `commonObjs`, with its `has`/`get`.
29
+ commonObjs: {
30
+ has: (id: string) => objs.has(id),
31
+ get: (id: string) => objs.get(id),
32
+ [Symbol.iterator]: () => objs[Symbol.iterator](),
33
+ },
34
+ getViewport: () => ({
35
+ width: 612,
36
+ height: H,
37
+ transform: [1, 0, 0, -1, 0, H],
38
+ convertToViewportPoint: (x: number, y: number) => [x, H - y],
39
+ }),
40
+ };
41
+ }
42
+ const doc = (pages: ReturnType<typeof page>[]) =>
43
+ ({
44
+ numPages: pages.length,
45
+ getPage: async (i: number) => pages[i - 1],
46
+ }) as never;
47
+ const item = (
48
+ fontName: string,
49
+ transform: number[],
50
+ height = 10,
51
+ ): FakeItem => ({
52
+ str: "word",
53
+ fontName,
54
+ transform,
55
+ width: 20,
56
+ height,
57
+ });
58
+
59
+ test("rawFontOf: not an object, not a font, and a font without a name", () => {
60
+ assert.deepEqual(
61
+ [
62
+ rawFontOf("a", null),
63
+ rawFontOf("b", { x: 1 }),
64
+ rawFontOf("c", { loadedName: "L" }),
65
+ ],
66
+ [
67
+ null,
68
+ null,
69
+ {
70
+ id: "c",
71
+ name: "",
72
+ type: undefined,
73
+ subtype: undefined,
74
+ isType3Font: false,
75
+ missingFile: false,
76
+ },
77
+ ],
78
+ );
79
+ });
80
+
81
+ test("a document with no pages is `empty`; text drawn with no font is refused", async () => {
82
+ assert.deepEqual(await factsOf(doc([]), lib), {
83
+ ok: false,
84
+ reason: "empty",
85
+ detail: "the PDF has no pages",
86
+ });
87
+ const r = await factsOf(
88
+ doc([page([item("f0", [10, 0, 0, 10, 50, 700])], {})]),
89
+ lib,
90
+ );
91
+ assert.equal(r.ok, false);
92
+ assert.equal(!r.ok && r.reason, "zero-fonts-on-text");
93
+ });
94
+
95
+ test("metrics, sizes and names fall back when pdf.js gives none", async () => {
96
+ const fonts = {
97
+ // a font with a name and an ascent but no descent
98
+ f1: { loadedName: "f1", name: "Named", ascent: 0.8 },
99
+ // a font with no name and no metrics at all
100
+ f2: { loadedName: "f2" },
101
+ };
102
+ const r = await factsOf(
103
+ doc([
104
+ page(
105
+ [
106
+ item("f1", [10, 0, 0, 10, 50, 700]),
107
+ // zero horizontal scale: the size comes from the item's height…
108
+ item("f2", [0, 0, 0, 0, 100, 700], 12),
109
+ // …or, with no height either, from the vertical scale
110
+ item("f2", [0, 0, 0, 9, 150, 700], 0),
111
+ // an item whose font pdf.js never resolved
112
+ item("gone", [10, 0, 0, 10, 200, 700]),
113
+ ],
114
+ fonts,
115
+ ),
116
+ ]),
117
+ lib,
118
+ );
119
+ assert.equal(r.ok, true);
120
+ const boxes = r.ok ? r.facts.layout[0]!.boxes : [];
121
+ assert.deepEqual(
122
+ boxes.map((b) => [b.font, b.size, b.fill]),
123
+ [
124
+ ["Named", 10, { kind: "unknown" }],
125
+ ["f2", 12, { kind: "unknown" }],
126
+ ["f2", 9, { kind: "unknown" }],
127
+ ["gone", 10, { kind: "unknown" }],
128
+ ],
129
+ );
130
+ assert.deepEqual(r.ok ? r.facts.last.words.map((w) => w.text) : [], [
131
+ "word",
132
+ "word",
133
+ "word",
134
+ "word",
135
+ ]);
136
+ });
137
+
138
+ test("a failure that is not an Error is named `Error` with its text", () => {
139
+ assert.deepEqual(failureOf("boom"), {
140
+ ok: false,
141
+ reason: "unreadable",
142
+ detail: "Error: boom",
143
+ });
144
+ });
package/src/pdf-facts.ts CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  } from "./pdf-geometry.ts";
34
34
  import type { PageLayout, TextBox } from "./domain/page-layout.ts";
35
35
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
36
- import { fillsOf, isUpright } from "./adapters/pdfjs/fill.ts";
36
+ import { fillsFor, isUpright } from "./adapters/pdfjs/fill.ts";
37
37
 
38
38
  type PdfJs = Awaited<ReturnType<typeof import("unpdf").getResolvedPDFJS>>;
39
39
  type Doc = Awaited<ReturnType<PdfJs["getDocument"]>["promise"]>;
@@ -241,20 +241,18 @@ function layoutOf(
241
241
  lib: PdfJs,
242
242
  ): PageLayout {
243
243
  const vp = page.getViewport({ scale: 1 });
244
- const fills = fillsOf(
245
- lib.OPS,
246
- ops,
247
- items.map((it) => it.str),
244
+ const boxes = fillsFor(lib.OPS, ops, items, (it) => it.str).map(
245
+ ({ item, fill }): TextBox => ({ ...boxOf(page, vp, item, lib), fill }),
248
246
  );
249
- const boxes = items.map((it, k): TextBox => ({
250
- ...boxOf(page, vp, it, lib),
251
- fill: fills[k] ?? { kind: "unknown" },
252
- }));
253
247
  return { widthPt: vp.width, heightPt: vp.height, boxes };
254
248
  }
255
249
 
256
- /** Every page's fonts, and the last page's words. */
257
- async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
250
+ /**
251
+ * Every page's fonts, and the last page's words. Exported with `failureOf` so a test can hand it a
252
+ * document whose objects pdf.js shapes only in rare files (a font without metrics, a zero-scale
253
+ * transform, no pages) — `readPdf` below is the one production caller.
254
+ */
255
+ export async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
258
256
  const read: {
259
257
  page: Page;
260
258
  raw: RawPage;
@@ -290,7 +288,7 @@ async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
290
288
  }
291
289
 
292
290
  /** pdf.js's own exception names for the two conditions a caller can act on. */
293
- function failureOf(e: unknown): PdfRead {
291
+ export function failureOf(e: unknown): PdfRead {
294
292
  const err = e as { name?: string; message?: string };
295
293
  const detail = `${err.name ?? "Error"}: ${err.message ?? String(e)}`;
296
294
  return fail(
@@ -309,13 +307,13 @@ export const readPdf: PdfReader = async (path) => {
309
307
  }
310
308
  const lib = await loadPdfJs();
311
309
  const task = lib.getDocument({ data, ...PDFJS_OPTIONS });
312
- try {
313
- return await factsOf(await task.promise, lib);
314
- } catch (e) {
315
- return failureOf(e);
316
- } finally {
317
- await task.destroy();
318
- }
310
+ // A promise chain rather than try/catch/finally: every path returns a value here, and the code
311
+ // after such a statement is a block that can never run — which coverage reports as unrun.
312
+ const read = await task.promise
313
+ .then((doc) => factsOf(doc, lib))
314
+ .catch(failureOf);
315
+ await task.destroy();
316
+ return read;
319
317
  };
320
318
 
321
319
  /** The one wording of a failed read, as a build step or a CLI prints it. */
@@ -6,9 +6,9 @@
6
6
  * on 2026-08-29 (the midpoint cut, the review build, years mistaken for line numbers), ported from
7
7
  * the harness of the function this replaces.
8
8
  */
9
- import assert from "node:assert/strict";
10
9
  import { dirname, join } from "node:path";
11
10
  import { fileURLToPath } from "node:url";
11
+ import { createChecker } from "../lib/check.mjs";
12
12
 
13
13
  const HERE = dirname(fileURLToPath(import.meta.url));
14
14
  const {
@@ -21,11 +21,7 @@ const {
21
21
  STUB_WORDS,
22
22
  } = await import(join(HERE, "pdf-geometry.ts"));
23
23
 
24
- let n = 0;
25
- const check = (label, cond) => {
26
- assert.ok(cond, label);
27
- n++;
28
- };
24
+ const check = createChecker();
29
25
 
30
26
  const W = 612; // US letter, points
31
27
 
@@ -250,5 +246,5 @@ check(
250
246
  );
251
247
 
252
248
  console.log(
253
- `✓ ${String(n)} assertions passed — pdf-geometry: columns, stub/review, words from runs, fonts`,
249
+ `✓ ${String(check.count)} assertions passed — pdf-geometry: columns, stub/review, words from runs, fonts`,
254
250
  );
@@ -136,7 +136,7 @@ export function wordsOf(run: TextRun): Word[] {
136
136
  const y0 = run.baseline - run.ascent * run.size;
137
137
  const y1 = run.baseline - run.descent * run.size;
138
138
  return [...run.text.matchAll(/\S+/g)].map((m) => {
139
- const x0 = run.x + (m.index ?? 0) * perChar;
139
+ const x0 = run.x + m.index * perChar;
140
140
  return { x0, y0, x1: x0 + m[0].length * perChar, y1, text: m[0] };
141
141
  });
142
142
  }
@@ -230,6 +230,8 @@ describe("labelOf — display only, never used to resolve", () => {
230
230
  ["paperlint:agenticdev", "agenticdev"],
231
231
  ["./venues/usenix-sec.jsonc", "usenix-sec"],
232
232
  ["../shared/acl.json", "acl"],
233
+ ["./.hidden", ".hidden"],
234
+ ["paperlint:", ""],
233
235
  ])("%s → %s", (spec, label) => {
234
236
  expect(labelOf(spec)).toBe(label);
235
237
  });
package/src/presets.ts CHANGED
@@ -293,5 +293,11 @@ export function paperPresetProblem(
293
293
  if (p.kind === "preset-problem")
294
294
  return `${join(paperDir, CONFIG_FILE)}: ${presetProblemText(p.problem)}`;
295
295
  if (p.kind !== "settings-problem") return null;
296
- return `${join(paperDir, CONFIG_FILE)}: ${p.problem.why}`;
296
+ return settingsProblemLine(paperDir, p.problem);
297
297
  }
298
+
299
+ /** The one line naming a paper's `paperlint.json` that does not parse, and why. */
300
+ export const settingsProblemLine = (
301
+ paperDir: string,
302
+ problem: SettingsProblem,
303
+ ): string => `${join(paperDir, CONFIG_FILE)}: ${problem.why}`;
@@ -207,3 +207,100 @@ describe("the reference rules", () => {
207
207
  expect(referencesPath(PAPER)).toBe(`${PAPER}/_build/references.json`);
208
208
  });
209
209
  });
210
+
211
+ describe("reading what is on disk", () => {
212
+ it("no paper.tex: the bibliography is refs.bib, from its first byte", () => {
213
+ const files = memoryFiles({ [`${PAPER}/refs.bib`]: ENTRIES });
214
+ expect(bibliographyOf(files, PAPER)).toEqual({
215
+ source: "refs.bib",
216
+ text: ENTRIES,
217
+ offset: 0,
218
+ });
219
+ });
220
+
221
+ it("a references.json of another schema, without entries, or not JSON reads as none", () => {
222
+ for (const body of [
223
+ JSON.stringify({ schema: 999, entries: [] }),
224
+ JSON.stringify({ schema: 1 }),
225
+ "null",
226
+ "{",
227
+ ]) {
228
+ const files = memoryFiles({ [referencesPath(PAPER)]: body });
229
+ expect(readReferences(files, PAPER)).toBeNull();
230
+ }
231
+ });
232
+ });
233
+
234
+ /** A record for `bib`, as the build would write it, with `patch` applied to the document. */
235
+ const record = (
236
+ files: ReturnType<typeof memoryFiles>,
237
+ body: Record<string, unknown>,
238
+ ) => {
239
+ const bib = bibliographyOf(files, PAPER)!;
240
+ files.writeAtomic(
241
+ referencesPath(PAPER) as AbsolutePath,
242
+ new TextEncoder().encode(
243
+ JSON.stringify({
244
+ schema: 1,
245
+ bib: { source: bib.source, sha256: bibHash(bib) },
246
+ ...body,
247
+ }),
248
+ ),
249
+ );
250
+ };
251
+
252
+ describe("the reference rules over a record written by hand", () => {
253
+ it("not-checked with no reason recorded says so", async () => {
254
+ const tex = TEX(ENTRIES);
255
+ const files = memoryFiles({ [`${PAPER}/paper.tex`]: tex });
256
+ record(files, { status: "not-checked", entries: [] });
257
+ const msgs = await lint(files, tex);
258
+ expect(msgs.map((m) => m.ruleId)).toEqual(["paper/refs-checked"]);
259
+ expect(msgs[0]!.message).toMatch(/no reason recorded/);
260
+ });
261
+
262
+ it("a verdict without a reason, and one for a key the bibliography lacks, still report", async () => {
263
+ const tex = TEX(ENTRIES);
264
+ const files = memoryFiles({ [`${PAPER}/paper.tex`]: tex });
265
+ record(files, {
266
+ status: "checked",
267
+ entries: [
268
+ { key: "schick2023", exists: "true", authors: "mismatch" },
269
+ { key: "ghost", exists: "false", authors: "match" },
270
+ ],
271
+ });
272
+ const msgs = await lint(files, tex);
273
+ expect(msgs.map((m) => [m.ruleId, m.line])).toEqual([
274
+ ["paper/cite-exists", 1],
275
+ ["paper/author-list", 3],
276
+ ]);
277
+ });
278
+ });
279
+
280
+ describe("the reference rules over refs.bib, and on other files", () => {
281
+ it("an external refs.bib: findings sit at the start of paper.tex, naming the key", async () => {
282
+ const tex = "\\documentclass{acmart}\n\\begin{document}x\\end{document}\n";
283
+ const files = memoryFiles({
284
+ [`${PAPER}/paper.tex`]: tex,
285
+ [`${PAPER}/refs.bib`]: ENTRIES,
286
+ });
287
+ record(files, {
288
+ status: "checked",
289
+ entries: [verdict("schick2023", "mismatch")],
290
+ });
291
+ const msgs = await lint(files, tex);
292
+ expect(msgs.map((m) => [m.ruleId, m.line])).toEqual([
293
+ ["paper/author-list", 1],
294
+ ]);
295
+ expect(msgs[0]!.message).toMatch(/schick2023/);
296
+ });
297
+
298
+ it("only paper.tex is judged", () => {
299
+ const files = memoryFiles({});
300
+ const rules = referenceRules({ files });
301
+ for (const rule of Object.values(rules))
302
+ expect(rule.create({ filename: `${PAPER}/notes.tex` } as never)).toEqual(
303
+ {},
304
+ );
305
+ });
306
+ });
package/src/references.ts CHANGED
@@ -129,8 +129,8 @@ export async function recordReferences(
129
129
  at(referencesPath(paperDir)),
130
130
  new TextEncoder().encode(`${JSON.stringify(doc, null, 2)}\n`),
131
131
  );
132
- if (doc.status === "not-checked")
133
- return `references NOT checked — ${doc.why ?? "no reason given"}; lint will say so`;
132
+ if (result.kind === "not-checked")
133
+ return `references NOT checked — ${result.why}; lint will say so`;
134
134
  const bad = doc.entries.filter(
135
135
  (e) => e.exists === "false" || e.authors === "mismatch",
136
136
  ).length;
@@ -3,20 +3,16 @@
3
3
  * paperlint ships. The end-to-end path (a project's paperlint.json → `paperlint lint`) is in `cli.harness.mjs`;
4
4
  * this pins the parser's output shape and each refusal.
5
5
  */
6
- import assert from "node:assert/strict";
7
6
  import { dirname, join } from "node:path";
8
7
  import { fileURLToPath } from "node:url";
8
+ import { createChecker } from "../lib/check.mjs";
9
9
 
10
10
  const HERE = dirname(fileURLToPath(import.meta.url));
11
11
  const { parseRuleBlocks, shippedRuleIds, unknownKeys } = await import(
12
12
  join(HERE, "rules-config.ts")
13
13
  );
14
14
 
15
- let n = 0;
16
- const check = (label, cond, detail = "") => {
17
- assert.ok(cond, detail ? `${label} — ${detail}` : label);
18
- n++;
19
- };
15
+ const check = createChecker();
20
16
 
21
17
  const SHIPPED = new Set(["pdf/last-page-balance", "paper/typography"]);
22
18
  const parse = (raw) => parseRuleBlocks(raw, "S", SHIPPED, "/consumer");
@@ -112,7 +108,13 @@ check(
112
108
  markdown: foreign,
113
109
  },
114
110
  },
115
- { plugins: { paper: { rules: { typography: {} } } } },
111
+ // A plugin that carries only a language (the `tex` plugin does) has no rules to ship.
112
+ {
113
+ plugins: {
114
+ paper: { rules: { typography: {} } },
115
+ tex: { languages: {} },
116
+ },
117
+ },
116
118
  { files: ["x"] },
117
119
  ],
118
120
  [foreign],
@@ -126,5 +128,5 @@ check(
126
128
  }
127
129
 
128
130
  console.log(
129
- `✓ ${String(n)} assertions passed — rules-config: blocks parsed with basePath, every refusal names its key`,
131
+ `✓ ${String(check.count)} assertions passed — rules-config: blocks parsed with basePath, every refusal names its key`,
130
132
  );
@@ -0,0 +1,111 @@
1
+ /**
2
+ * A consumer that ALSO lints paper files with its own ESLint config (#104).
3
+ *
4
+ * 3.0.0 made `% eslint-disable-next-line <rule> -- <why>` the way to keep a deliberate exception.
5
+ * A project that runs its own `eslint` over the same `paper.tex` — for its own rules, with
6
+ * paperlint's LaTeX language — did not register paperlint's rule plugins, so that run failed on the
7
+ * directive: "Definition for rule 'paper/leading-zero' was not found". `rulesOff` is the fragment
8
+ * such a config spreads in: every rule paperlint ships, registered and turned off.
9
+ */
10
+ import { ESLint, type Linter } from "eslint";
11
+ import { describe, expect, it } from "vitest";
12
+ // @ts-expect-error — the module is .mjs and has no types.
13
+ import { texLanguage } from "../eslint-rules/latex-language.mjs";
14
+ import { rulePlugins, rulesOff, SHIPPED_RULES } from "./cli.ts";
15
+
16
+ const PAPER = [
17
+ "\\documentclass{article}",
18
+ "\\begin{document}",
19
+ "% eslint-disable-next-line paper/leading-zero -- quoted from the reviewer",
20
+ "We use a threshold of .05 throughout.",
21
+ "% eslint-disable-next-line bib/reachable-entry -- kept for the artifact",
22
+ "Nothing else.",
23
+ "\\end{document}",
24
+ "",
25
+ ].join("\n");
26
+
27
+ /** The consumer's own config: its LaTeX files, with paperlint's language, and its own rules. */
28
+ async function lintAsConsumer(
29
+ fragment: Linter.Config[],
30
+ ): Promise<Linter.LintMessage[]> {
31
+ const eslint = new ESLint({
32
+ overrideConfigFile: true,
33
+ overrideConfig: [
34
+ ...fragment,
35
+ {
36
+ files: ["**/*.tex"],
37
+ plugins:
38
+ fragment.length > 0
39
+ ? {}
40
+ : { tex: { languages: { latex: texLanguage } } },
41
+ language: "tex/latex",
42
+ },
43
+ ],
44
+ });
45
+ const [res] = await eslint.lintText(PAPER, {
46
+ filePath: "papers/a/paper.tex",
47
+ });
48
+ return res!.messages;
49
+ }
50
+
51
+ describe("rulesOff — paperlint's rules, registered and off, for a consumer's own ESLint run", () => {
52
+ it("without it, the documented escape hatch breaks the consumer's run (the defect)", async () => {
53
+ const messages = await lintAsConsumer([]);
54
+ expect(messages.map((m) => m.message)).toContain(
55
+ "Definition for rule 'paper/leading-zero' was not found.",
56
+ );
57
+ });
58
+
59
+ it("🔴 spreading it: no 'Definition … not found', and no paperlint finding runs", async () => {
60
+ const messages = await lintAsConsumer(rulesOff(texLanguage));
61
+ // Guards: the defect — every directive names a rule ESLint now knows.
62
+ expect(messages.map((m) => m.message).join("\n")).not.toMatch(
63
+ /Definition for rule/,
64
+ );
65
+ // Guards: off means off — paperlint's own run judges these files, not the consumer's.
66
+ expect(messages.filter((m) => m.ruleId?.startsWith("paper/"))).toEqual([]);
67
+ // Guards: a directive naming an OFF rule suppresses nothing, and ESLint would call it unused
68
+ // ("Unused eslint-disable directive (no problems were reported from 'paper/leading-zero')").
69
+ expect(messages).toEqual([]);
70
+ });
71
+
72
+ it("registers every rule paperlint ships, each off, and carries the LaTeX language", () => {
73
+ const [cfg] = rulesOff(texLanguage);
74
+ expect(new Set(Object.keys(cfg?.rules ?? {}))).toEqual(SHIPPED_RULES);
75
+ expect(Object.values(cfg?.rules ?? {}).every((e) => e === "off")).toBe(
76
+ true,
77
+ );
78
+ const plugins = rulePlugins(texLanguage);
79
+ const defined = new Set<string>();
80
+ for (const [name, p] of Object.entries(plugins))
81
+ for (const r of Object.keys(p.rules ?? {})) defined.add(`${name}/${r}`);
82
+ expect(defined).toEqual(SHIPPED_RULES);
83
+ expect(plugins["tex"]?.languages?.["latex"]).toBe(texLanguage);
84
+ // `@eslint/markdown` is a dependency's plugin: the consumer registers its own.
85
+ expect(plugins["markdown"]).toBeUndefined();
86
+ });
87
+
88
+ it("the unused-directive report is off on paper files only, not on the consumer's code", async () => {
89
+ const eslint = new ESLint({
90
+ overrideConfigFile: true,
91
+ overrideConfig: [
92
+ ...rulesOff(texLanguage),
93
+ {
94
+ files: ["**/*.js"],
95
+ linterOptions: { reportUnusedDisableDirectives: "warn" },
96
+ },
97
+ ],
98
+ });
99
+ const [res] = await eslint.lintText(
100
+ "// eslint-disable-next-line paper/leading-zero -- nothing to silence\nconst x = 1;\n",
101
+ { filePath: "src/x.js" },
102
+ );
103
+ expect(res!.messages.map((m) => m.message)).toEqual([
104
+ "Unused eslint-disable directive (no problems were reported from 'paper/leading-zero').",
105
+ ]);
106
+ });
107
+
108
+ it("without a LaTeX language, the `tex` plugin carries rules only", () => {
109
+ expect(rulePlugins()["tex"]?.languages).toBeUndefined();
110
+ });
111
+ });
@@ -6,7 +6,6 @@
6
6
  * So every "found" here has a paired "not found on the neighboring directory", and the defaults
7
7
  * are separately checked against the shape of the live corpus.
8
8
  */
9
- import assert from "node:assert/strict";
10
9
  import {
11
10
  mkdtempSync,
12
11
  mkdirSync,
@@ -17,16 +16,13 @@ import {
17
16
  import { tmpdir } from "node:os";
18
17
  import { join, dirname } from "node:path";
19
18
  import { fileURLToPath } from "node:url";
19
+ import { createChecker } from "../lib/check.mjs";
20
20
 
21
21
  const HERE = dirname(fileURLToPath(import.meta.url));
22
22
  const { checkStructure, formatStructure, asEslintResults, STRUCTURE_DEFAULTS } =
23
23
  await import(join(HERE, "structure.ts"));
24
24
 
25
- let n = 0;
26
- const check = (label, cond) => {
27
- assert.ok(cond, label);
28
- n++;
29
- };
25
+ const check = createChecker();
30
26
 
31
27
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-struct-")));
32
28
  const papers = join(root, "papers");
@@ -71,8 +67,8 @@ try {
71
67
  );
72
68
  check(
73
69
  "and the message names the CONSEQUENCE, not a restatement of the condition",
74
- /is checked/.test(at("no-scorecard")[0].message) &&
75
- /stages, sources or research question/.test(
70
+ /are skipped/.test(at("no-scorecard")[0].message) &&
71
+ /stage, source and research-question checks/.test(
76
72
  at("no-scorecard")[0].message,
77
73
  ),
78
74
  );
@@ -177,5 +173,5 @@ try {
177
173
  }
178
174
 
179
175
  console.log(
180
- `✓ ${String(n)} assertions passed — structure: a missing file cannot complain for itself`,
176
+ `✓ ${String(check.count)} assertions passed — structure: a missing file cannot complain for itself`,
181
177
  );
package/src/structure.ts CHANGED
@@ -122,7 +122,7 @@ export function checkStructure(
122
122
  */
123
123
  function whyMissingMatters(file: string, dirName: string): string {
124
124
  if (file === "PIPELINE-STATUS.md")
125
- return `\`paper/stages\`, \`paper/source\` and \`paper/research-question\` read this file, so nothing \`${dirName}\` declares about its stages, sources or research question is checked`;
125
+ return `the stage, source and research-question checks are skipped for \`${dirName}\``;
126
126
  return `declared as required by your \`structure\` configuration`;
127
127
  }
128
128
 
@@ -7,7 +7,6 @@
7
7
  * 2. the schema REJECTS what it must — each bad profile names the offending path;
8
8
  * 3. what a paper gets: its venue plus the base set, or the base set with the reason said.
9
9
  */
10
- import assert from "node:assert/strict";
11
10
  import {
12
11
  copyFileSync,
13
12
  mkdtempSync,
@@ -19,6 +18,7 @@ import { tmpdir } from "node:os";
19
18
  import { dirname, join } from "node:path";
20
19
  import { fileURLToPath } from "node:url";
21
20
  import ts from "typescript";
21
+ import { createChecker } from "../lib/check.mjs";
22
22
 
23
23
  const HERE = dirname(fileURLToPath(import.meta.url));
24
24
  const VENUES = join(
@@ -31,11 +31,7 @@ const VENUES = join(
31
31
  );
32
32
  const R = await import(join(HERE, "tex-requirements.ts"));
33
33
 
34
- let n = 0;
35
- const check = (label, cond, detail = "") => {
36
- assert.ok(cond, `${label}${detail ? ` — ${detail}` : ""}`);
37
- n++;
38
- };
34
+ const check = createChecker();
39
35
  const throws = (fn) => {
40
36
  try {
41
37
  fn();
@@ -229,4 +225,35 @@ rmSync(tmp, { recursive: true, force: true });
229
225
  );
230
226
  }
231
227
 
232
- console.log(`tex-requirements: ${n} checks passed`);
228
+ // ── 5. the parser's tolerances: an optional block left out, and Ajv's optional fields ───
229
+ {
230
+ const bare = R.parsePreset(
231
+ '{ "extends": "paperlint:aisec" }',
232
+ "bare.jsonc",
233
+ VENUES,
234
+ );
235
+ check(
236
+ "a preset that only extends another: no tex block, `rules` an empty object",
237
+ JSON.stringify([bare.extends, bare.tex, bare.rules]) ===
238
+ '["paperlint:aisec",null,{}]',
239
+ bare,
240
+ );
241
+ // Guards: `packages` is read without a fallback because the schema refuses a tex block
242
+ // without it — this is the refusal the type relies on.
243
+ check(
244
+ "a tex block without `packages` is refused by the schema, before anything reads it",
245
+ /tex\.jsonc: \.tex should have required property 'packages'/.test(
246
+ throws(() => R.parsePreset('{ "tex": {} }', "tex.jsonc", VENUES)),
247
+ ),
248
+ throws(() => R.parsePreset('{ "tex": {} }', "tex.jsonc", VENUES)),
249
+ );
250
+ check(
251
+ "violations: Ajv's null `errors` is no violation; a message Ajv left out is an empty one",
252
+ R.violations("f.jsonc", { errors: null }).length === 0 &&
253
+ R.violations("f.jsonc", {
254
+ errors: [{ dataPath: "", message: undefined }],
255
+ })[0] === "f.jsonc: (top level) ",
256
+ );
257
+ }
258
+
259
+ console.log(`tex-requirements: ${check.count} checks passed`);