paperlint 3.2.0 → 4.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 (312) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/adapters/banal/failure.d.ts +1 -7
  3. package/dist/adapters/banal/failure.d.ts.map +1 -1
  4. package/dist/adapters/banal/failure.js +5 -7
  5. package/dist/adapters/banal/failure.js.map +1 -1
  6. package/dist/adapters/banal/index.d.ts.map +1 -1
  7. package/dist/adapters/banal/index.js +8 -11
  8. package/dist/adapters/banal/index.js.map +1 -1
  9. package/dist/adapters/banal/install.d.ts.map +1 -1
  10. package/dist/adapters/banal/install.js +4 -3
  11. package/dist/adapters/banal/install.js.map +1 -1
  12. package/dist/adapters/banal/invocation.d.ts +9 -4
  13. package/dist/adapters/banal/invocation.d.ts.map +1 -1
  14. package/dist/adapters/banal/invocation.js +13 -1
  15. package/dist/adapters/banal/invocation.js.map +1 -1
  16. package/dist/adapters/banal/locate.d.ts +12 -9
  17. package/dist/adapters/banal/locate.d.ts.map +1 -1
  18. package/dist/adapters/banal/locate.js +19 -12
  19. package/dist/adapters/banal/locate.js.map +1 -1
  20. package/dist/adapters/banal/run.d.ts +10 -1
  21. package/dist/adapters/banal/run.d.ts.map +1 -1
  22. package/dist/adapters/banal/run.js +8 -4
  23. package/dist/adapters/banal/run.js.map +1 -1
  24. package/dist/adapters/banal/settings.d.ts +1 -1
  25. package/dist/adapters/banal/settings.d.ts.map +1 -1
  26. package/dist/adapters/banal/settings.js +2 -1
  27. package/dist/adapters/banal/settings.js.map +1 -1
  28. package/dist/adapters/memory/index.d.ts.map +1 -1
  29. package/dist/adapters/memory/index.js +8 -2
  30. package/dist/adapters/memory/index.js.map +1 -1
  31. package/dist/adapters/node/process.io.d.ts +6 -3
  32. package/dist/adapters/node/process.io.d.ts.map +1 -1
  33. package/dist/adapters/node/process.io.js.map +1 -1
  34. package/dist/adapters/node/workspace.io.d.ts.map +1 -1
  35. package/dist/adapters/node/workspace.io.js +4 -2
  36. package/dist/adapters/node/workspace.io.js.map +1 -1
  37. package/dist/adapters/references/index.d.ts.map +1 -1
  38. package/dist/adapters/references/index.js +4 -2
  39. package/dist/adapters/references/index.js.map +1 -1
  40. package/dist/build-engine.d.ts +1 -1
  41. package/dist/build-engine.d.ts.map +1 -1
  42. package/dist/build-engine.js +8 -1
  43. package/dist/build-engine.js.map +1 -1
  44. package/dist/build.d.ts +2 -3
  45. package/dist/build.d.ts.map +1 -1
  46. package/dist/build.js +5 -6
  47. package/dist/build.js.map +1 -1
  48. package/dist/caller-path.d.ts +3 -0
  49. package/dist/caller-path.d.ts.map +1 -0
  50. package/dist/caller-path.js +13 -0
  51. package/dist/caller-path.js.map +1 -0
  52. package/dist/cli.d.ts +10 -17
  53. package/dist/cli.d.ts.map +1 -1
  54. package/dist/cli.js +62 -12
  55. package/dist/cli.js.map +1 -1
  56. package/dist/doctor.d.ts +12 -3
  57. package/dist/doctor.d.ts.map +1 -1
  58. package/dist/doctor.js +2 -1
  59. package/dist/doctor.js.map +1 -1
  60. package/dist/domain/paths.d.ts +8 -2
  61. package/dist/domain/paths.d.ts.map +1 -1
  62. package/dist/domain/paths.js +12 -1
  63. package/dist/domain/paths.js.map +1 -1
  64. package/dist/domain/ready.d.ts +24 -0
  65. package/dist/domain/ready.d.ts.map +1 -0
  66. package/dist/domain/ready.js +11 -0
  67. package/dist/domain/ready.js.map +1 -0
  68. package/dist/domain/record.d.ts +8 -0
  69. package/dist/domain/record.d.ts.map +1 -0
  70. package/dist/domain/record.js +8 -0
  71. package/dist/domain/record.js.map +1 -0
  72. package/dist/domain/sha256.d.ts.map +1 -1
  73. package/dist/domain/sha256.js +4 -2
  74. package/dist/domain/sha256.js.map +1 -1
  75. package/dist/engine.d.ts +23 -22
  76. package/dist/engine.d.ts.map +1 -1
  77. package/dist/engine.js.map +1 -1
  78. package/dist/eslint-rules/bib-reachable-entry.d.ts +1 -1
  79. package/dist/eslint-rules/bib-reachable-entry.js.map +1 -1
  80. package/dist/eslint-rules/install-path-literals.d.ts +2 -2
  81. package/dist/eslint-rules/install-path-literals.js.map +1 -1
  82. package/dist/eslint-rules/paper-research-question.d.ts +1 -1
  83. package/dist/eslint-rules/paper-research-question.js.map +1 -1
  84. package/dist/eslint-rules/paper-stages.d.ts +2 -2
  85. package/dist/eslint-rules/paper-stages.js.map +1 -1
  86. package/dist/eslint-rules/paper-typography.d.ts +6 -6
  87. package/dist/eslint-rules/paper-typography.js.map +1 -1
  88. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  89. package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -1
  90. package/dist/eslint-rules/review-frontmatter.d.ts +2 -2
  91. package/dist/eslint-rules/review-frontmatter.js.map +1 -1
  92. package/dist/eslint-rules/sibling-frontmatter.d.ts +1 -1
  93. package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -1
  94. package/dist/eslint-rules/temp-root-realpath.d.ts +1 -1
  95. package/dist/eslint-rules/temp-root-realpath.js.map +1 -1
  96. package/dist/eslint-rules/tex-build.d.ts +2 -2
  97. package/dist/eslint-rules/tex-build.js.map +1 -1
  98. package/dist/facts-file.d.ts.map +1 -1
  99. package/dist/facts-file.js +29 -26
  100. package/dist/facts-file.js.map +1 -1
  101. package/dist/hooks-settings.d.ts.map +1 -1
  102. package/dist/hooks-settings.js +17 -2
  103. package/dist/hooks-settings.js.map +1 -1
  104. package/dist/init.d.ts +3 -4
  105. package/dist/init.d.ts.map +1 -1
  106. package/dist/init.js +18 -8
  107. package/dist/init.js.map +1 -1
  108. package/dist/lib/paper-config.d.ts.map +1 -1
  109. package/dist/lib/paper-config.js +1 -5
  110. package/dist/lib/paper-config.js.map +1 -1
  111. package/dist/new-paper.d.ts.map +1 -1
  112. package/dist/new-paper.js +5 -1
  113. package/dist/new-paper.js.map +1 -1
  114. package/dist/paper-files.d.ts +4 -4
  115. package/dist/paper-files.d.ts.map +1 -1
  116. package/dist/paper-files.js +6 -6
  117. package/dist/paper-files.js.map +1 -1
  118. package/dist/paper-settings.js +3 -3
  119. package/dist/paper-settings.js.map +1 -1
  120. package/dist/pdf-facts.d.ts +51 -1
  121. package/dist/pdf-facts.d.ts.map +1 -1
  122. package/dist/pdf-facts.js +23 -15
  123. package/dist/pdf-facts.js.map +1 -1
  124. package/dist/pdf-geometry.d.ts.map +1 -1
  125. package/dist/pdf-geometry.js +2 -3
  126. package/dist/pdf-geometry.js.map +1 -1
  127. package/dist/ports/tool-installer.d.ts +2 -14
  128. package/dist/ports/tool-installer.d.ts.map +1 -1
  129. package/dist/presets.d.ts.map +1 -1
  130. package/dist/presets.js +16 -4
  131. package/dist/presets.js.map +1 -1
  132. package/dist/references.d.ts.map +1 -1
  133. package/dist/references.js +4 -4
  134. package/dist/references.js.map +1 -1
  135. package/dist/rules-config.d.ts +26 -1
  136. package/dist/rules-config.d.ts.map +1 -1
  137. package/dist/rules-config.js +9 -9
  138. package/dist/rules-config.js.map +1 -1
  139. package/dist/tex-requirements.d.ts +4 -0
  140. package/dist/tex-requirements.d.ts.map +1 -1
  141. package/dist/tex-requirements.js +14 -2
  142. package/dist/tex-requirements.js.map +1 -1
  143. package/dist/types.d.ts +5 -3
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/venue-rules.d.ts.map +1 -1
  146. package/dist/venue-rules.js +20 -7
  147. package/dist/venue-rules.js.map +1 -1
  148. package/docs/configuration.md +2 -2
  149. package/eslint-rules/bib-reachable-entry.ts +1 -1
  150. package/eslint-rules/install-path-literals.ts +2 -2
  151. package/eslint-rules/paper-research-question.ts +1 -1
  152. package/eslint-rules/paper-stages.ts +2 -2
  153. package/eslint-rules/paper-typography.ts +2 -2
  154. package/eslint-rules/pdf-last-page-balance.ts +1 -1
  155. package/eslint-rules/review-frontmatter.ts +1 -1
  156. package/eslint-rules/temp-root-realpath.ts +1 -1
  157. package/eslint-rules/tex-build.ts +2 -2
  158. package/eslint.config.mjs +7 -0
  159. package/fixtures/install-path-literals/clean.fixture.mjs +1 -1
  160. package/fixtures/install-path-literals/clean.md +2 -2
  161. package/fixtures/install-path-literals/defect.fixture.mjs +1 -1
  162. package/fixtures/install-path-literals/defect.md +2 -2
  163. package/hooks/paper-status-gates.sh +9 -19
  164. package/lib/paper-config.harness.mjs +1 -1
  165. package/lib/paper-config.ts +1 -5
  166. package/lib/skill-checks.mjs +24 -381
  167. package/lib/skill-checks.test.mjs +63 -115
  168. package/lib/skill-corpus.mjs +10 -251
  169. package/lib/skill-corpus.test.mjs +2 -13
  170. package/lib/skill-trigger-cases.mjs +1 -1
  171. package/package.json +1 -1
  172. package/scripts/coverage-config.test.ts +0 -1
  173. package/scripts/harness-api.frozen.json +0 -1
  174. package/skills/analyze-sibling-paper/SKILL.md +1 -39
  175. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +0 -38
  176. package/skills/argument-arc/SKILL.md +2 -36
  177. package/skills/argument-arc/SKILL.md.spec.ts +1 -43
  178. package/skills/build-benchmark/SKILL.md +1 -41
  179. package/skills/build-benchmark/SKILL.md.spec.ts +0 -40
  180. package/skills/camera-ready/SKILL.md +1 -34
  181. package/skills/camera-ready/SKILL.md.spec.ts +0 -33
  182. package/skills/cold-read-diff/SKILL.md +1 -35
  183. package/skills/cold-read-diff/SKILL.md.spec.ts +0 -34
  184. package/skills/draft-paper/SKILL.md +2 -41
  185. package/skills/draft-paper/SKILL.md.spec.ts +1 -50
  186. package/skills/extend-paper/SKILL.md +2 -33
  187. package/skills/extend-paper/SKILL.md.spec.ts +1 -42
  188. package/skills/find-venue/SKILL.md +2 -38
  189. package/skills/find-venue/SKILL.md.spec.ts +1 -47
  190. package/skills/grade-paper-writing/SKILL.md +3 -38
  191. package/skills/grade-paper-writing/SKILL.md.spec.ts +2 -47
  192. package/skills/grade-paper-writing/prose-lint.mjs +2 -3
  193. package/skills/grade-paper-writing/prose-lint.test.ts +1 -1
  194. package/skills/harden-paper/SKILL.md +1 -39
  195. package/skills/harden-paper/SKILL.md.spec.ts +0 -38
  196. package/skills/map-prior-work/SKILL.md +1 -35
  197. package/skills/map-prior-work/SKILL.md.spec.ts +0 -34
  198. package/skills/osf-artifact-upload/osf-artifact-upload.harness.ts +3 -4
  199. package/skills/paper-adversarial-review/SKILL.md +2 -35
  200. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -43
  201. package/skills/paper-pipeline/SKILL.md +2 -28
  202. package/skills/paper-pipeline/SKILL.md.spec.ts +1 -27
  203. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
  204. package/skills/paper-pipeline/paper-pipeline.harness.mjs +25 -40
  205. package/skills/paper-pipeline/references/acceptance-gate.md +1 -6
  206. package/skills/paper-pipeline/references/pipeline-status-template.md +8 -35
  207. package/skills/paper-pipeline/scripts/README.md +30 -225
  208. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +4 -8
  209. package/skills/paper-pipeline/scripts/consumer.harness.mjs +6 -104
  210. package/skills/paper-pipeline/scripts/consumer.mjs +23 -104
  211. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +2 -3
  212. package/skills/paper-pipeline/scripts/population-map.harness.mjs +3 -3
  213. package/skills/paper-status/SKILL.md +2 -42
  214. package/skills/paper-status/SKILL.md.spec.ts +1 -41
  215. package/skills/pc-panel-review/SKILL.md +2 -46
  216. package/skills/pc-panel-review/SKILL.md.spec.ts +1 -45
  217. package/skills/plan-paper-timeline/SKILL.md +2 -36
  218. package/skills/plan-paper-timeline/SKILL.md.spec.ts +0 -36
  219. package/skills/render-paper/SKILL.md +1 -37
  220. package/skills/render-paper/SKILL.md.spec.ts +0 -36
  221. package/skills/research-ideate/SKILL.md +2 -39
  222. package/skills/research-ideate/SKILL.md.spec.ts +1 -47
  223. package/skills/study-accepted-papers/SKILL.md +1 -34
  224. package/skills/study-accepted-papers/SKILL.md.spec.ts +0 -33
  225. package/skills/submit-paper/SKILL.md +1 -33
  226. package/skills/submit-paper/SKILL.md.spec.ts +0 -32
  227. package/skills/sweep-design-space/SKILL.md +1 -38
  228. package/skills/sweep-design-space/SKILL.md.spec.ts +0 -37
  229. package/skills/tighten-paper/SKILL.md +2 -39
  230. package/skills/tighten-paper/SKILL.md.spec.ts +1 -47
  231. package/skills/tighten-paper/structure.mjs +4 -11
  232. package/skills/verify-citations/SKILL.md +2 -38
  233. package/skills/verify-citations/SKILL.md.spec.ts +1 -37
  234. package/src/adapters/banal/failure.ts +9 -10
  235. package/src/adapters/banal/index.test.ts +2 -2
  236. package/src/adapters/banal/index.ts +8 -10
  237. package/src/adapters/banal/install.ts +9 -4
  238. package/src/adapters/banal/invocation.test.ts +20 -8
  239. package/src/adapters/banal/invocation.ts +23 -8
  240. package/src/adapters/banal/locate.test.ts +2 -2
  241. package/src/adapters/banal/locate.ts +27 -21
  242. package/src/adapters/banal/run.test.ts +2 -2
  243. package/src/adapters/banal/run.ts +16 -10
  244. package/src/adapters/banal/settings.ts +2 -2
  245. package/src/adapters/curl/download.test.ts +4 -4
  246. package/src/adapters/memory/index.ts +4 -3
  247. package/src/adapters/node/files.test.ts +5 -3
  248. package/src/adapters/node/process.io.ts +9 -2
  249. package/src/adapters/node/process.test.ts +4 -4
  250. package/src/adapters/node/workspace.io.ts +9 -4
  251. package/src/adapters/references/index.ts +7 -2
  252. package/src/build-engine.test.ts +11 -6
  253. package/src/build-engine.ts +10 -5
  254. package/src/build.test.ts +10 -11
  255. package/src/build.ts +8 -9
  256. package/src/caller-path.ts +13 -0
  257. package/src/cli.harness.mjs +0 -2
  258. package/src/cli.test.ts +19 -8
  259. package/src/cli.ts +82 -25
  260. package/src/doctor.test.ts +1 -1
  261. package/src/doctor.ts +14 -5
  262. package/src/domain/paths.test.ts +15 -0
  263. package/src/domain/paths.ts +20 -1
  264. package/src/domain/ready.ts +31 -0
  265. package/src/domain/record.test.ts +20 -0
  266. package/src/domain/record.ts +12 -0
  267. package/src/domain/sha256.ts +5 -3
  268. package/src/engine.ts +30 -2
  269. package/src/facts-file.ts +43 -28
  270. package/src/hooks-settings.harness.ts +17 -0
  271. package/src/hooks-settings.ts +28 -4
  272. package/src/init.test.ts +25 -5
  273. package/src/init.ts +26 -12
  274. package/src/new-paper.harness.mjs +7 -23
  275. package/src/new-paper.ts +7 -3
  276. package/src/paper-files.test.ts +22 -19
  277. package/src/paper-files.ts +16 -27
  278. package/src/paper-settings.test.ts +2 -2
  279. package/src/paper-settings.ts +5 -5
  280. package/src/pdf-facts.test.ts +16 -5
  281. package/src/pdf-facts.ts +79 -22
  282. package/src/pdf-geometry.ts +5 -3
  283. package/src/ports/tool-installer.ts +2 -17
  284. package/src/presets.ts +28 -14
  285. package/src/references.test.ts +25 -19
  286. package/src/references.ts +4 -5
  287. package/src/rules-config.ts +40 -11
  288. package/src/tex-requirements.test.ts +21 -0
  289. package/src/tex-requirements.ts +29 -6
  290. package/src/toolchain.test.ts +9 -5
  291. package/src/toolchain.ts +2 -2
  292. package/src/types.ts +5 -3
  293. package/src/venue-rules.test.ts +8 -4
  294. package/src/venue-rules.ts +21 -11
  295. package/skills/paper-pipeline/scripts/announce.mjs +0 -67
  296. package/skills/paper-pipeline/scripts/announce.test.mjs +0 -102
  297. package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +0 -51
  298. package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +0 -52
  299. package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +0 -6
  300. package/skills/paper-pipeline/scripts/fixtures/scorecard.ts +0 -100
  301. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +0 -302
  302. package/skills/paper-pipeline/scripts/ledger.mjs +0 -623
  303. package/skills/paper-pipeline/scripts/ledger.test.mjs +0 -254
  304. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +0 -474
  305. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +0 -328
  306. package/skills/paper-pipeline/scripts/pipeline-check.mjs +0 -750
  307. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +0 -169
  308. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +0 -73
  309. package/skills/paper-pipeline/scripts/run-mechanical.mjs +0 -648
  310. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +0 -468
  311. package/skills/paper-pipeline/scripts/status.mjs +0 -295
  312. package/skills/paper-pipeline/scripts/status.test.mjs +0 -232
package/src/pdf-facts.ts CHANGED
@@ -35,12 +35,67 @@ 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
36
  import { fillsFor, isUpright } from "./adapters/pdfjs/fill.ts";
37
37
  import { messageOf } from "./domain/text.ts";
38
+ import { fieldOf, isRecord, numbersOf } from "./domain/record.ts";
38
39
 
39
40
  type PdfJs = Awaited<ReturnType<typeof import("unpdf").getResolvedPDFJS>>;
40
- type Doc = Awaited<ReturnType<PdfJs["getDocument"]>["promise"]>;
41
- type Page = Awaited<ReturnType<Doc["getPage"]>>;
42
- type Item = Awaited<ReturnType<Page["getTextContent"]>>["items"][number];
43
- type TextItem = Extract<Item, { str: string }>;
41
+
42
+ /** A text item as this module reads it; pdf.js's `TextItem` is one. */
43
+ interface TextItem {
44
+ readonly str: string;
45
+ readonly fontName: string;
46
+ /** pdf.js types it `any[]`; `numbersOf` reads it. */
47
+ readonly transform: readonly unknown[];
48
+ readonly width: number;
49
+ readonly height: number;
50
+ }
51
+ /** What else pdf.js lists among a page's items: a marked-content boundary, with no text. */
52
+ interface MarkedContent {
53
+ readonly type: string;
54
+ readonly str?: undefined;
55
+ }
56
+ type Item = TextItem | MarkedContent;
57
+
58
+ /** A page's operators, as `fillsFor` walks them; pdf.js's operator list is one. */
59
+ interface OperatorList {
60
+ readonly fnArray: readonly number[];
61
+ readonly argsArray: readonly unknown[];
62
+ }
63
+
64
+ /** A viewport as this module reads it; pdf.js's `PageViewport` is one. */
65
+ interface Viewport {
66
+ readonly width: number;
67
+ readonly height: number;
68
+ readonly transform: readonly number[];
69
+ convertToViewportPoint(x: number, y: number): readonly unknown[];
70
+ }
71
+
72
+ /**
73
+ * A page as this module reads it. pdf.js's `PDFPageProxy` is one, and so is a test's fake page: the
74
+ * module depends on these five members, not on the rest of pdf.js's page.
75
+ */
76
+ interface Page {
77
+ getOperatorList(): Promise<OperatorList>;
78
+ getTextContent(): Promise<{ readonly items: readonly Item[] }>;
79
+ /** pdf.js's `commonObjs`: loaded objects by id, iterable as `[id, object]` pairs. */
80
+ readonly commonObjs: {
81
+ has(id: string): boolean;
82
+ get(id: string): unknown;
83
+ [Symbol.iterator](): Iterator<readonly unknown[]>;
84
+ };
85
+ getViewport(params: { scale: number }): Viewport;
86
+ }
87
+
88
+ /** A document as this module reads it; pdf.js's `PDFDocumentProxy` is one. */
89
+ interface Doc {
90
+ readonly numPages: number;
91
+ getPage(pageNumber: number): Promise<Page>;
92
+ }
93
+
94
+ /** A viewport point, which pdf.js types as `any[]`: the two numbers it holds. */
95
+ const pointOf = (xy: readonly unknown[]): [number, number] => [
96
+ Number(xy[0]),
97
+ Number(xy[1]),
98
+ ];
44
99
 
45
100
  /**
46
101
  * What `readPdf` measured. `last` is the last page's words; `classifyLastPage` reads it. `layout` is
@@ -95,8 +150,8 @@ const fail = (reason: PdfReadFailure, detail: string): PdfRead => ({
95
150
 
96
151
  /** A font object pdf.js left in `commonObjs`, read into a `RawFont`, or null when it is not one. */
97
152
  export function rawFontOf(id: string, obj: unknown): RawFont | null {
98
- if (typeof obj !== "object" || obj === null) return null;
99
- const o = obj as Record<string, unknown>;
153
+ if (!isRecord(obj)) return null;
154
+ const o = obj;
100
155
  if (!("loadedName" in o) && !("isType3Font" in o)) return null;
101
156
  const str = (v: unknown): string | undefined =>
102
157
  typeof v === "string" ? v : undefined;
@@ -111,7 +166,7 @@ export function rawFontOf(id: string, obj: unknown): RawFont | null {
111
166
  }
112
167
 
113
168
  const isText = (it: Item): it is TextItem =>
114
- "str" in it && it.str.trim() !== "";
169
+ typeof it.str === "string" && it.str.trim() !== "";
115
170
 
116
171
  /** The font pdf.js resolved for `id`, if it did. `get` throws on an unresolved id; `has` does not. */
117
172
  function resolved(page: Page, id: string): unknown {
@@ -124,9 +179,10 @@ function resolved(page: Page, id: string): unknown {
124
179
  */
125
180
  function loadedFonts(page: Page): Map<string, RawFont> {
126
181
  const out = new Map<string, RawFont>();
127
- for (const [id, obj] of page.commonObjs as Iterable<[string, unknown]>) {
128
- const f = rawFontOf(id, obj);
129
- if (f) out.set(id, f);
182
+ // pdf.js yields `[id, object]` pairs, typed `any[]`.
183
+ for (const entry of page.commonObjs) {
184
+ const f = rawFontOf(String(entry[0]), entry[1]);
185
+ if (f) out.set(f.id, f);
130
186
  }
131
187
  return out;
132
188
  }
@@ -167,7 +223,7 @@ async function readPage(
167
223
  * Both are far below any balance tolerance, so the simpler rule stays.
168
224
  */
169
225
  function metricsOf(font: unknown): { ascent: number; descent: number } {
170
- const f = (font ?? {}) as Record<string, unknown>;
226
+ const f = isRecord(font) ? font : {};
171
227
  const ascent = f["ascent"];
172
228
  if (typeof ascent !== "number" || !(ascent > 0))
173
229
  return { ascent: DEFAULT_ASCENT, descent: DEFAULT_DESCENT };
@@ -179,8 +235,8 @@ function metricsOf(font: unknown): { ascent: number; descent: number } {
179
235
  function pageText(page: Page, items: readonly TextItem[]): PageText {
180
236
  const vp = page.getViewport({ scale: 1 });
181
237
  const runs: TextRun[] = items.map((it) => {
182
- const [a = 0, b = 0, , d = 0, e = 0, f = 0] = it.transform as number[];
183
- const [x, baseline] = vp.convertToViewportPoint(e, f) as [number, number];
238
+ const [a = 0, b = 0, , d = 0, e = 0, f = 0] = numbersOf(it.transform);
239
+ const [x, baseline] = pointOf(vp.convertToViewportPoint(e, f));
184
240
  const size = Math.hypot(a, b) || it.height || Math.abs(d);
185
241
  return {
186
242
  text: it.str,
@@ -200,7 +256,7 @@ function pageText(page: Page, items: readonly TextItem[]): PageText {
200
256
 
201
257
  /** The font name pdf.js resolved for an item, else its internal id. */
202
258
  function fontNameOf(font: unknown, id: string): string {
203
- const name = (font as { name?: unknown } | null)?.name;
259
+ const name = fieldOf(font, "name");
204
260
  return typeof name === "string" && name ? name : id;
205
261
  }
206
262
 
@@ -211,12 +267,12 @@ function fontNameOf(font: unknown, id: string): string {
211
267
  */
212
268
  function boxOf(
213
269
  page: Page,
214
- vp: ReturnType<Page["getViewport"]>,
270
+ vp: Viewport,
215
271
  it: TextItem,
216
272
  lib: PdfJs,
217
273
  ): Omit<TextBox, "fill"> {
218
- const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = it.transform as number[];
219
- const [x, baseline] = vp.convertToViewportPoint(e, f) as [number, number];
274
+ const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = numbersOf(it.transform);
275
+ const [x, baseline] = pointOf(vp.convertToViewportPoint(e, f));
220
276
  const size = Math.hypot(c, d) || Math.hypot(a, b) || it.height;
221
277
  const font = resolved(page, it.fontName);
222
278
  const m = metricsOf(font);
@@ -229,7 +285,7 @@ function boxOf(
229
285
  font: fontNameOf(font, it.fontName),
230
286
  text: it.str,
231
287
  upright: isUpright(
232
- lib.Util.transform(vp.transform, it.transform) as number[],
288
+ numbersOf(lib.Util.transform(vp.transform, it.transform)),
233
289
  ),
234
290
  };
235
291
  }
@@ -238,7 +294,7 @@ function boxOf(
238
294
  function layoutOf(
239
295
  page: Page,
240
296
  items: readonly TextItem[],
241
- ops: Awaited<ReturnType<Page["getOperatorList"]>>,
297
+ ops: OperatorList,
242
298
  lib: PdfJs,
243
299
  ): PageLayout {
244
300
  const vp = page.getViewport({ scale: 1 });
@@ -290,10 +346,11 @@ export async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
290
346
 
291
347
  /** pdf.js's own exception names for the two conditions a caller can act on. */
292
348
  export function failureOf(e: unknown): PdfRead {
293
- const err = e as { name?: string; message?: string };
294
- const detail = `${err.name ?? "Error"}: ${err.message ?? String(e)}`;
349
+ const name = fieldOf(e, "name");
350
+ const message = fieldOf(e, "message");
351
+ const detail = `${typeof name === "string" ? name : "Error"}: ${typeof message === "string" ? message : String(e)}`;
295
352
  return fail(
296
- err.name === "PasswordException" ? "encrypted" : "unreadable",
353
+ name === "PasswordException" ? "encrypted" : "unreadable",
297
354
  detail,
298
355
  );
299
356
  }
@@ -160,6 +160,10 @@ export type FontProgram =
160
160
  /** A program pdf.js named but this module does not know — kept verbatim, never guessed. */
161
161
  export type UnknownProgram = `unknown:${string}`;
162
162
 
163
+ const isUnknownProgram = (
164
+ p: FontProgram | UnknownProgram,
165
+ ): p is UnknownProgram => p.startsWith("unknown:");
166
+
163
167
  /**
164
168
  * One font the PDF draws text with.
165
169
  *
@@ -296,7 +300,5 @@ export const POPPLER_TYPE: Readonly<Record<FontProgram, string>> = {
296
300
  /** The type column for any fact, the unknown ones verbatim. */
297
301
  export function popplerType(f: FontFact): string {
298
302
  if (f.kind === "type3") return POPPLER_TYPE.Type3;
299
- return f.program.startsWith("unknown:")
300
- ? f.program
301
- : POPPLER_TYPE[f.program as FontProgram];
303
+ return isUnknownProgram(f.program) ? f.program : POPPLER_TYPE[f.program];
302
304
  }
@@ -2,26 +2,11 @@
2
2
  * The port `paperlint toolchain` installs and checks an external program through. One port, many
3
3
  * programs: banal implements it now, TeX Live in #76.
4
4
  */
5
- import type { Opaque } from "ts-essentials";
6
5
  import type { Result } from "../domain/result.ts";
7
6
  import type { Lines } from "../domain/text.ts";
7
+ import type { Ready } from "../domain/ready.ts";
8
8
 
9
- /**
10
- * Present, verified, running. BRANDED: a literal `{ where, fresh, verified }` is not a `Ready`, so
11
- * only an installer's adapter — one minting function each (banal's is `ready` in
12
- * `adapters/banal/index.ts`) — can make one, and a ready line cannot be printed without it.
13
- */
14
- export type Ready = Opaque<
15
- {
16
- /** Where it is: the program's path or its bin directory. */
17
- readonly where: string;
18
- /** This run installed it. */
19
- readonly fresh: boolean;
20
- /** What was checked before calling it ready, as a person reads it. */
21
- readonly verified: string;
22
- },
23
- "Ready"
24
- >;
9
+ export type { Ready };
25
10
 
26
11
  export interface ToolInstaller {
27
12
  /** The program as a person reads it: `banal 1.2 (HotCRP f3e4352)`. */
package/src/presets.ts CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  type TexRequirements,
40
40
  type VenueFormat,
41
41
  } from "./tex-requirements.ts";
42
- import type { AbsolutePath } from "./domain/paths.ts";
42
+ import { callerPath } from "./caller-path.ts";
43
43
  import { err, ok, type Result } from "./domain/result.ts";
44
44
  import type { Files } from "./ports/files.ts";
45
45
  import {
@@ -120,7 +120,7 @@ function readPreset(
120
120
  ): Result<{ file: string; preset: PresetFile }, PresetProblem> {
121
121
  const where = fileOf(spec, from, deps.venuesDir);
122
122
  if (!where.ok) return where;
123
- const bytes = deps.files.readBytes(where.value as AbsolutePath);
123
+ const bytes = deps.files.readBytes(callerPath(where.value));
124
124
  if (bytes === null)
125
125
  return err({ kind: "not-found", spec, file: where.value, shipped: [] });
126
126
  try {
@@ -166,14 +166,22 @@ export function mergeFormat(
166
166
  parent: VenueFormat,
167
167
  child: VenueFormat,
168
168
  ): VenueFormat {
169
- const keys = Object.keys(NO_FORMAT).filter((k) => k !== "kinds") as Exclude<
170
- keyof VenueFormat,
171
- "kinds"
172
- >[];
173
- const scalars = Object.fromEntries(
174
- keys.map((k) => [k, child[k] ?? parent[k]]),
175
- ) as Omit<VenueFormat, "kinds">;
176
- return { ...scalars, kinds: new Map([...parent.kinds, ...child.kinds]) };
169
+ // Every field listed: a VenueFormat literal missing one is a compile error.
170
+ const pick = <K extends Exclude<keyof VenueFormat, "kinds">>(
171
+ k: K,
172
+ ): VenueFormat[K] => child[k] ?? parent[k];
173
+ return {
174
+ pageWidthIn: pick("pageWidthIn"),
175
+ pageHeightIn: pick("pageHeightIn"),
176
+ columns: pick("columns"),
177
+ bodyPt: pick("bodyPt"),
178
+ bodyPtTol: pick("bodyPtTol"),
179
+ refPtMin: pick("refPtMin"),
180
+ refPtMax: pick("refPtMax"),
181
+ fontsText: pick("fontsText"),
182
+ fontsTitle: pick("fontsTitle"),
183
+ kinds: new Map([...parent.kinds, ...child.kinds]),
184
+ };
177
185
  }
178
186
 
179
187
  /** The chain, root first, merged into one preset. Pure. */
@@ -182,12 +190,18 @@ function merged(
182
190
  rootFirst: readonly PresetFile[],
183
191
  files: readonly string[],
184
192
  ): Preset {
185
- const base = {
186
- name: null as string | null,
187
- template: null as string | null,
193
+ const base: {
194
+ name: string | null;
195
+ template: string | null;
196
+ tex: TexRequirements;
197
+ format: VenueFormat;
198
+ rules: Readonly<Record<string, unknown>>;
199
+ } = {
200
+ name: null,
201
+ template: null,
188
202
  tex: NO_REQUIREMENTS,
189
203
  format: NO_FORMAT,
190
- rules: {} as Readonly<Record<string, unknown>>,
204
+ rules: {},
191
205
  };
192
206
  const m = rootFirst.reduce(
193
207
  (acc, p) => ({
@@ -9,10 +9,11 @@
9
9
  */
10
10
  import { describe, expect, it } from "vitest";
11
11
  import { ESLint } from "eslint";
12
+ import { eslintConfig } from "./cli.ts";
12
13
  import { join } from "node:path";
13
14
  import { memoryFiles } from "./adapters/memory/index.ts";
14
15
  import { ok } from "./domain/result.ts";
15
- import type { AbsolutePath } from "./domain/paths.ts";
16
+ import { absolutePath } from "./domain/paths.ts";
16
17
  import type {
17
18
  CheckReferences,
18
19
  EntryVerdict,
@@ -78,9 +79,13 @@ const offline: CheckReferences = (_bib, cache) =>
78
79
  const ctx = (files: ReturnType<typeof memoryFiles>) => ({
79
80
  paperDir: PAPER,
80
81
  env: {},
81
- run: (() => ({})) as never,
82
- readPdf: (() => null) as never,
83
- measure: (() => null) as never,
82
+ run: () => ({ status: 0 }),
83
+ readPdf: () => Promise.reject(new Error("the references step reads no PDF")),
84
+ measure: {
85
+ measure: () => {
86
+ throw new Error("the references step measures nothing");
87
+ },
88
+ },
84
89
  files,
85
90
  });
86
91
 
@@ -94,32 +99,26 @@ async function build(tex: string, check: CheckReferences) {
94
99
  return { files, out };
95
100
  }
96
101
 
97
- /**
98
- * The rules as an ESLint plugin. Their context type is the narrow one they read (a custom
99
- * language's SourceCode), which ESLint's JS-centred plugin type does not describe.
100
- */
101
- const asPlugin = (rules: object): ESLint.Plugin => ({ rules }) as ESLint.Plugin;
102
-
103
102
  /** Lint `tex` as the paper's paper.tex against what `files` recorded. */
104
103
  async function lint(files: ReturnType<typeof memoryFiles>, tex: string) {
105
104
  files.writeAtomic(
106
- `${PAPER}/paper.tex` as AbsolutePath,
105
+ absolutePath(`${PAPER}/paper.tex`),
107
106
  new TextEncoder().encode(tex),
108
107
  );
109
108
  const eslint = new ESLint({
110
109
  cwd: "/work",
111
110
  overrideConfigFile: true,
112
- overrideConfig: [
111
+ overrideConfig: eslintConfig([
113
112
  {
114
113
  files: ["**/paper.tex"],
115
114
  plugins: {
116
115
  tex: { languages: { latex: texLanguage } },
117
- paper: asPlugin(referenceRules({ files })),
116
+ paper: { rules: referenceRules({ files }) },
118
117
  },
119
118
  language: "tex/latex",
120
119
  rules: REFERENCE_RULE_LEVELS,
121
120
  },
122
- ],
121
+ ]),
123
122
  });
124
123
  const [res] = await eslint.lintText(tex, {
125
124
  filePath: join(PAPER, "paper.tex"),
@@ -186,7 +185,7 @@ function fetching(key: string) {
186
185
  return { check, seen };
187
186
  }
188
187
  const cacheText = (files: ReturnType<typeof memoryFiles>): string | null => {
189
- const b = files.readBytes(lookupCachePath(PAPER) as AbsolutePath);
188
+ const b = files.readBytes(absolutePath(lookupCachePath(PAPER)));
190
189
  return b === null ? null : new TextDecoder().decode(b);
191
190
  };
192
191
 
@@ -363,7 +362,7 @@ const record = (
363
362
  ) => {
364
363
  const bib = present(bibliographyOf(files, PAPER), "the bibliography");
365
364
  files.writeAtomic(
366
- referencesPath(PAPER) as AbsolutePath,
365
+ absolutePath(referencesPath(PAPER)),
367
366
  new TextEncoder().encode(
368
367
  JSON.stringify({
369
368
  schema: 1,
@@ -424,9 +423,16 @@ describe("the reference rules over refs.bib, and on other files", () => {
424
423
  const files = memoryFiles({});
425
424
  const rules = referenceRules({ files });
426
425
  for (const rule of Object.values(rules))
427
- expect(rule.create({ filename: `${PAPER}/notes.tex` } as never)).toEqual(
428
- {},
429
- );
426
+ expect(
427
+ rule.create({
428
+ filename: `${PAPER}/notes.tex`,
429
+ cwd: "/work",
430
+ sourceCode: { getLocFromIndex: () => ({ line: 1, column: 0 }) },
431
+ report: () => {
432
+ throw new Error("a file that is not paper.tex is never reported");
433
+ },
434
+ }),
435
+ ).toEqual({});
430
436
  });
431
437
  });
432
438
 
package/src/references.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  * a pass would be the counter that counts what it never looked at.
29
29
  */
30
30
  import { join } from "node:path";
31
- import type { AbsolutePath } from "./domain/paths.ts";
31
+ import { callerPath } from "./caller-path.ts";
32
32
  import { sha256Hex } from "./domain/sha256.ts";
33
33
  import type { Files } from "./ports/files.ts";
34
34
  import {
@@ -60,9 +60,8 @@ export const lookupCachePath = (paperDir: string): string =>
60
60
  export const referencesPath = (paperDir: string): string =>
61
61
  join(paperDir, "_build", REFERENCES_FILE);
62
62
 
63
- const at = (p: string): AbsolutePath => p as AbsolutePath;
64
63
  const text = (files: Files, p: string): string | null => {
65
- const b = files.readBytes(at(p));
64
+ const b = files.readBytes(callerPath(p));
66
65
  return b === null ? null : new TextDecoder().decode(b);
67
66
  };
68
67
 
@@ -186,7 +185,7 @@ export async function recordReferences(
186
185
  const record = (c: ReferencesCheck): ReferencesDocument => {
187
186
  const doc = documentOf(bib, c);
188
187
  files.writeAtomic(
189
- at(referencesPath(paperDir)),
188
+ callerPath(referencesPath(paperDir)),
190
189
  new TextEncoder().encode(`${JSON.stringify(doc, null, 2)}\n`),
191
190
  );
192
191
  return doc;
@@ -206,7 +205,7 @@ export async function recordReferences(
206
205
  const fetched = newAnswers(cache.value, result.cache);
207
206
  if (fetched > 0)
208
207
  files.writeAtomic(
209
- at(lookupCachePath(paperDir)),
208
+ callerPath(lookupCachePath(paperDir)),
210
209
  new TextEncoder().encode(serializeLookupCache(result.cache)),
211
210
  );
212
211
  const bad = doc.entries.filter(
@@ -39,11 +39,40 @@ export interface RuleBlock {
39
39
  readonly rules: Readonly<Record<string, RuleEntry>>;
40
40
  }
41
41
 
42
+ /**
43
+ * A plugin as a config block registers it, typed to what paperlint reads of it: its rules, and the
44
+ * languages it brings (`tex` brings LaTeX).
45
+ */
46
+ export interface ConfigPlugin {
47
+ readonly rules?: Readonly<Record<string, unknown>>;
48
+ readonly languages?: Readonly<Record<string, unknown>>;
49
+ }
50
+
51
+ /**
52
+ * An ESLint config block as paperlint builds and reads one. Not `Linter.Config`: ESLint types a
53
+ * plugin's rule as one that runs on ANY language, and paperlint's rules are written for the language
54
+ * they read (LaTeX, markdown), so none of them is assignable to it. ESLint checks a block when it
55
+ * loads it; `eslintConfig` in cli.ts is the one place these blocks become `Linter.Config`.
56
+ */
57
+ export interface ConfigBlock {
58
+ readonly name?: string;
59
+ readonly basePath?: string;
60
+ readonly files?: readonly (string | readonly string[])[];
61
+ readonly ignores?: readonly string[];
62
+ readonly plugins?: Readonly<Record<string, ConfigPlugin>>;
63
+ readonly language?: string;
64
+ readonly languageOptions?: Readonly<Record<string, unknown>>;
65
+ readonly linterOptions?: Readonly<Record<string, unknown>>;
66
+ readonly rules?: Readonly<Record<string, unknown>>;
67
+ }
68
+
42
69
  export type Parsed<T> =
43
70
  | { readonly ok: true; readonly value: T }
44
71
  | { readonly ok: false; readonly error: string };
45
72
 
46
- const SEVERITIES: readonly unknown[] = ["off", "warn", "error", 0, 1, 2];
73
+ const SEVERITIES: readonly Severity[] = ["off", "warn", "error", 0, 1, 2];
74
+ const isSeverity = (v: unknown): v is Severity =>
75
+ SEVERITIES.some((s) => s === v);
47
76
 
48
77
  const BLOCK_KEYS = new Set(["files", "ignores", "rules"]);
49
78
 
@@ -61,9 +90,10 @@ const isStringList = (v: unknown): v is string[] =>
61
90
 
62
91
  /** A severity, or `[severity, ...options]`. */
63
92
  function parseEntry(v: unknown): RuleEntry | null {
64
- const sev: unknown = Array.isArray(v) ? v[0] : v;
65
- if (!SEVERITIES.includes(sev)) return null;
66
- return v as RuleEntry;
93
+ const entry: readonly unknown[] = Array.isArray(v) ? v : [v];
94
+ const [sev, ...options] = entry;
95
+ if (!isSeverity(sev)) return null;
96
+ return Array.isArray(v) ? [sev, ...options] : sev;
67
97
  }
68
98
 
69
99
  /**
@@ -105,8 +135,8 @@ function badGlobs(o: Record<string, unknown>, at: string): string | null {
105
135
 
106
136
  /** The block's `files`/`ignores`, present only when given. */
107
137
  const globsOf = (o: Record<string, unknown>) => ({
108
- ...(o["files"] ? { files: o["files"] as string[] } : {}),
109
- ...(o["ignores"] ? { ignores: o["ignores"] as string[] } : {}),
138
+ ...(isStringList(o["files"]) ? { files: o["files"] } : {}),
139
+ ...(isStringList(o["ignores"]) ? { ignores: o["ignores"] } : {}),
110
140
  });
111
141
 
112
142
  const isRecord = (v: unknown): v is Record<string, unknown> =>
@@ -182,16 +212,15 @@ export function parseRuleBlocks(
182
212
  * `foreign` (a dependency's plugin, such as `@eslint/markdown`) are not paperlint's and are skipped.
183
213
  */
184
214
  export function shippedRuleIds(
185
- config: readonly unknown[],
215
+ config: readonly ConfigBlock[],
186
216
  foreign: readonly unknown[],
187
217
  ): Set<string> {
188
218
  const ids = new Set<string>();
189
219
  for (const block of config) {
190
- const plugins = (block as { plugins?: Record<string, unknown> }).plugins;
191
- for (const [name, plugin] of Object.entries(plugins ?? {})) {
220
+ for (const [name, plugin] of Object.entries(block.plugins ?? {})) {
192
221
  if (foreign.includes(plugin)) continue;
193
- const rules = (plugin as { rules?: Record<string, unknown> }).rules;
194
- for (const rule of Object.keys(rules ?? {})) ids.add(`${name}/${rule}`);
222
+ for (const rule of Object.keys(plugin.rules ?? {}))
223
+ ids.add(`${name}/${rule}`);
195
224
  }
196
225
  }
197
226
  return ids;
@@ -0,0 +1,21 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { loadTypescript } from "./tex-requirements.ts";
4
+
5
+ test("loadTypescript refuses a module without the two functions a profile is read with", () => {
6
+ assert.throws(
7
+ () => loadTypescript(() => ({})),
8
+ /loaded without parseConfigFileTextToJson/,
9
+ );
10
+ });
11
+
12
+ test("loadTypescript hands back what `require` loaded when it has them", () => {
13
+ const ts = {
14
+ parseConfigFileTextToJson: () => ({ config: {} }),
15
+ flattenDiagnosticMessageText: () => "",
16
+ };
17
+ assert.equal(
18
+ loadTypescript(() => ts),
19
+ ts,
20
+ );
21
+ });
@@ -27,6 +27,7 @@ import { join } from "node:path";
27
27
  import Ajv from "ajv";
28
28
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
29
29
  import { CONFIG_FILE } from "#lib/paper-config";
30
+ import { fieldOf } from "./domain/record.ts";
30
31
 
31
32
  /** CTAN package name → the names that prove it is installed. */
32
33
  export type PackageProofs = Readonly<Record<string, readonly string[]>>;
@@ -51,15 +52,37 @@ const PROFILE_EXT = ".jsonc";
51
52
 
52
53
  export const NO_REQUIREMENTS: TexRequirements = { packages: {}, tools: {} };
53
54
 
54
- type Ts = typeof import("typescript");
55
- let ts: Ts | null = null;
55
+ /** The two functions of `typescript` a profile is read with. */
56
+ type TsJsonc = Pick<
57
+ typeof import("typescript"),
58
+ "parseConfigFileTextToJson" | "flattenDiagnosticMessageText"
59
+ >;
60
+
61
+ const isTsJsonc = (m: unknown): m is TsJsonc =>
62
+ typeof fieldOf(m, "parseConfigFileTextToJson") === "function" &&
63
+ typeof fieldOf(m, "flattenDiagnosticMessageText") === "function";
64
+
65
+ /** `typescript` as `load` returns it — `require` answers `any` — checked for the two functions used. */
66
+ export function loadTypescript(load: (id: string) => unknown): TsJsonc {
67
+ const m = load("typescript");
68
+ if (!isTsJsonc(m))
69
+ throw new Error(
70
+ "`typescript` loaded without parseConfigFileTextToJson — reinstall this package",
71
+ );
72
+ return m;
73
+ }
74
+
75
+ let ts: TsJsonc | null = null;
56
76
  /** TypeScript is loaded only when a profile is read: `paperlint lint` never pays for it. */
57
- function typescript(): Ts {
58
- ts ??= createRequire(import.meta.url)("typescript") as Ts;
77
+ function typescript(): TsJsonc {
78
+ ts ??= loadTypescript(createRequire(import.meta.url));
59
79
  return ts;
60
80
  }
61
81
 
62
82
  type Validate = ReturnType<Ajv.Ajv["compile"]>;
83
+ /** `PresetJson` is the TypeScript side of the profile schema: a document the schema accepts is one. */
84
+ const matchesSchema = (v: unknown, validate: Validate): v is PresetJson =>
85
+ validate(v) === true;
63
86
  const validators = new Map<string, Validate>();
64
87
  function validatorFor(dir: string): Validate {
65
88
  const known = validators.get(dir);
@@ -223,11 +246,11 @@ export function parsePreset(
223
246
  `${file}: not valid JSONC — ${typescript().flattenDiagnosticMessageText(error.messageText, " ")}`,
224
247
  );
225
248
  const validate = validatorFor(dir);
226
- if (!validate(config))
249
+ if (!matchesSchema(config, validate))
227
250
  throw new Error(
228
251
  `${file} does not match ${SCHEMA_FILE}:\n ${violations(file, validate).join("\n ")}`,
229
252
  );
230
- const j = config as PresetJson;
253
+ const j = config;
231
254
  return {
232
255
  extends: orNull(j.extends),
233
256
  name: orNull(j.name),
@@ -34,19 +34,19 @@ type Answer = {
34
34
  };
35
35
  /** A runner answering by program name (the last path segment); `onRun` may write files first. */
36
36
  function io(
37
- answers: Record<string, (args: string[]) => Answer>,
37
+ answers: Record<string, (args: readonly string[]) => Answer>,
38
38
  ): ToolchainIO & { lines: string[] } {
39
39
  const lines: string[] = [];
40
40
  return {
41
41
  lines,
42
42
  log: (l) => lines.push(l),
43
43
  env: {},
44
- run: ((cmd: string, args: string[]) => {
44
+ run: (cmd, args) => {
45
45
  const name = cmd.split("/").pop() ?? cmd;
46
46
  const answer = answers[`${name} ${args[0] ?? ""}`] ?? answers[name];
47
47
  if (!answer) throw new Error(`unexpected ${name} ${args.join(" ")}`);
48
48
  return answer(args);
49
- }) as never,
49
+ },
50
50
  };
51
51
  }
52
52
  const ok = (stdout = ""): Answer => ({ status: 0, stdout, stderr: "" });
@@ -194,7 +194,7 @@ function cacheWith(name: string, year: string): string {
194
194
  const ACMART = { packages: { acmart: ["acmart.cls"] }, tools: {} };
195
195
  function ensure(
196
196
  cache: string,
197
- answers: Record<string, (args: string[]) => Answer>,
197
+ answers: Record<string, (args: readonly string[]) => Answer>,
198
198
  ) {
199
199
  const t = io(answers);
200
200
  const errs: string[] = [];
@@ -272,7 +272,11 @@ test("runToolchain with only its installer: the console is the default output",
272
272
  const saved = console.error;
273
273
  console.error = (...a: unknown[]) => void seen.push(a.join(" "));
274
274
  try {
275
- assert.equal(runToolchain({ platform: "win32", banal: {} as never }), 1);
275
+ const unused = (): never => {
276
+ throw new Error("an unsupported platform installs nothing");
277
+ };
278
+ const banal = { label: "banal", ensure: unused, check: unused };
279
+ assert.equal(runToolchain({ platform: "win32", banal }), 1);
276
280
  } finally {
277
281
  console.error = saved;
278
282
  }