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
@@ -9,7 +9,7 @@ import {
9
9
  type Environment,
10
10
  type HostDirs,
11
11
  } from "../../domain/host.ts";
12
- import type { AbsolutePath } from "../../domain/paths.ts";
12
+ import { absolutePath, type AbsolutePath } from "../../domain/paths.ts";
13
13
 
14
14
  /** An explicit banal to use instead of paperlint's (a path to the script). */
15
15
  export const BANAL_ENV = "BANAL";
@@ -29,7 +29,7 @@ export interface BanalSettings {
29
29
 
30
30
  /** `resolve` with an absolute base never consults the process's cwd. */
31
31
  const absolute = (cwd: string, p: string): AbsolutePath =>
32
- resolve(cwd, p) as AbsolutePath;
32
+ absolutePath(resolve(cwd, p));
33
33
 
34
34
  /** An unset variable and an empty one mean the same: not set. */
35
35
  const set = (v: string | undefined): string | null => (v ? v : null);
@@ -15,18 +15,18 @@ import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { pathToFileURL } from "node:url";
17
17
  import { afterAll as after, test } from "vitest";
18
- import type { AbsolutePath } from "../../domain/paths.ts";
18
+ import { absolutePath, joinPath } from "../../domain/paths.ts";
19
19
  import { curlDownload, curlFailure } from "./download.io.ts";
20
20
  import type { ProcessExit } from "../../ports/process.ts";
21
21
  import { spawnProcess } from "../node/index.ts";
22
22
 
23
- const root = realpathSync(
24
- mkdtempSync(join(tmpdir(), "paperlint-download-test-")),
23
+ const root = absolutePath(
24
+ realpathSync(mkdtempSync(join(tmpdir(), "paperlint-download-test-"))),
25
25
  );
26
26
  after(() => {
27
27
  rmSync(root, { recursive: true, force: true });
28
28
  });
29
- const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
29
+ const at = (...p: string[]) => joinPath(root, ...p);
30
30
 
31
31
  const dl = (env: Record<string, string>) => {
32
32
  const tmp = at("dl-tmp");
@@ -2,7 +2,7 @@
2
2
  * In-memory adapters for the app layer's tests: every port, no disk, no processes, no network.
3
3
  * Each one RECORDS what it was asked, so a test asserts on the calls instead of on side effects.
4
4
  */
5
- import type { AbsolutePath } from "../../domain/paths.ts";
5
+ import { absolutePath } from "../../domain/paths.ts";
6
6
  import type { Download } from "../../ports/download.ts";
7
7
  import type { Files } from "../../ports/files.ts";
8
8
  import type { Command, ProcessExit, RunProcess } from "../../ports/process.ts";
@@ -69,11 +69,12 @@ export interface MemoryWorkspace extends Workspace {
69
69
  export function memoryWorkspace(dir = "/scratch"): MemoryWorkspace {
70
70
  const written: MemoryWorkspace["written"] = [];
71
71
  const ended: ("returned" | "threw")[] = [];
72
+ const at = absolutePath(dir);
72
73
  const scratch: Scratch = {
73
- dir: dir as AbsolutePath,
74
+ dir: at,
74
75
  write(name, content, mode) {
75
76
  written.push({ name, content, mode });
76
- return `${dir}/${name}` as AbsolutePath;
77
+ return absolutePath(`${dir}/${name}`);
77
78
  },
78
79
  };
79
80
  return {
@@ -4,14 +4,16 @@ import { existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { afterAll as after, test } from "vitest";
7
- import type { AbsolutePath } from "../../domain/paths.ts";
7
+ import { absolutePath, joinPath } from "../../domain/paths.ts";
8
8
  import { errorCode, nodeFiles } from "./files.io.ts";
9
9
 
10
- const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-files-test-")));
10
+ const root = absolutePath(
11
+ realpathSync(mkdtempSync(join(tmpdir(), "paperlint-files-test-"))),
12
+ );
11
13
  after(() => {
12
14
  rmSync(root, { recursive: true, force: true });
13
15
  });
14
- const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
16
+ const at = (...p: string[]) => joinPath(root, ...p);
15
17
 
16
18
  test("Files: a missing file reads as null and is not a file", () => {
17
19
  assert.equal(nodeFiles.readBytes(at("nope")), null);
@@ -11,8 +11,15 @@ import {
11
11
  } from "node:child_process";
12
12
  import type { Command, ProcessExit, RunProcess } from "../../ports/process.ts";
13
13
 
14
- /** Node's `spawnSync`, or a stand-in with its shape (a harness records the calls through one). */
15
- export type SpawnSync = typeof spawnSync;
14
+ /**
15
+ * Node's `spawnSync` — the one overload this file calls, text in and text out — or a stand-in with its
16
+ * shape (a harness records the calls through one).
17
+ */
18
+ export type SpawnSync = (
19
+ file: string,
20
+ args: readonly string[],
21
+ options: SpawnSyncOptionsWithStringEncoding,
22
+ ) => SpawnSyncReturns<string>;
16
23
 
17
24
  const errno = (r: SpawnSyncReturns<string>): string | undefined =>
18
25
  codeOf(r.error);
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import assert from "node:assert/strict";
7
7
  import { test } from "vitest";
8
- import type { AbsolutePath } from "../../domain/paths.ts";
8
+ import { absolutePath } from "../../domain/paths.ts";
9
9
  import type { Command } from "../../ports/process.ts";
10
10
  import { spawnProcess } from "./process.io.ts";
11
11
 
@@ -62,19 +62,19 @@ test("output past maxOutputBytes is a spawn failure, named by Node's own message
62
62
  });
63
63
 
64
64
  test("cwd is the Command's when it names one", () => {
65
- const r = run.run({ ...sh("pwd"), cwd: "/" as AbsolutePath });
65
+ const r = run.run({ ...sh("pwd"), cwd: absolutePath("/") });
66
66
  assert.deepEqual(r.kind === "exited" && r.stdout, "/\n");
67
67
  });
68
68
 
69
69
  test("a spawn that reports no status, no signal and no error reads as exit 1, not success", () => {
70
- const silent = spawnProcess((() => ({
70
+ const silent = spawnProcess(() => ({
71
71
  pid: 0,
72
72
  output: [],
73
73
  stdout: "",
74
74
  stderr: "",
75
75
  status: null,
76
76
  signal: null,
77
- })) as never);
77
+ }));
78
78
  assert.deepEqual(silent.run(sh("true")), {
79
79
  kind: "exited",
80
80
  status: 1,
@@ -10,14 +10,18 @@ import {
10
10
  writeFileSync,
11
11
  } from "node:fs";
12
12
  import { join } from "node:path";
13
- import type { AbsolutePath } from "../../domain/paths.ts";
13
+ import {
14
+ absolutePath,
15
+ joinPath,
16
+ type AbsolutePath,
17
+ } from "../../domain/paths.ts";
14
18
  import type { NotAPromise, Scratch, Workspace } from "../../ports/workspace.ts";
15
19
 
16
20
  function scratchIn(dir: AbsolutePath): Scratch {
17
21
  return {
18
22
  dir,
19
23
  write(name, content, mode): AbsolutePath {
20
- const at = join(dir, name) as AbsolutePath;
24
+ const at = joinPath(dir, name);
21
25
  writeFileSync(at, content);
22
26
  if (mode === "exec") chmodSync(at, 0o755);
23
27
  return at;
@@ -29,9 +33,10 @@ function scratchIn(dir: AbsolutePath): Scratch {
29
33
  export function tmpWorkspace(tmpDir: string): Workspace {
30
34
  return {
31
35
  within<T>(prefix: string, use: (s: Scratch) => NotAPromise<T>): T {
32
- const dir = realpathSync(mkdtempSync(join(tmpDir, prefix)));
36
+ // `realpath` answers with an absolute path, whatever `tmpDir` was.
37
+ const dir = absolutePath(realpathSync(mkdtempSync(join(tmpDir, prefix))));
33
38
  try {
34
- return use(scratchIn(dir as AbsolutePath));
39
+ return use(scratchIn(dir));
35
40
  } finally {
36
41
  rmSync(dir, { recursive: true, force: true });
37
42
  }
@@ -44,6 +44,7 @@ import type {
44
44
  BibEntry,
45
45
  } from "../../../skills/verify-citations/scripts/bib-authors.mjs";
46
46
  import { messageOf } from "../../domain/text.ts";
47
+ import { isRecord } from "../../domain/record.ts";
47
48
 
48
49
  const describeAuthors = (f: AuthorFinding): string =>
49
50
  [
@@ -188,12 +189,16 @@ function grown(
188
189
  ): LookupCache {
189
190
  const { cache, usable } = loaded;
190
191
  const { store, dblp } = run;
191
- const fresh = Object.entries(store).filter(([k]) => !usable.citations.has(k));
192
+ // verify-cites stores each answer as an object; only such a value is a response the cache can hold.
193
+ const fresh = Object.entries(store).filter(
194
+ (e): e is [string, Readonly<Record<string, unknown>>] =>
195
+ !usable.citations.has(e[0]) && isRecord(e[1]),
196
+ );
192
197
  const freshDblp = [...dblp].filter(([k]) => !usable.dblp.has(k));
193
198
  if (fresh.length === 0 && freshDblp.length === 0) return cache;
194
199
  const dated = fresh.map(([k, response]): [string, CachedResponse] => [
195
200
  k,
196
- { fetched: today(), response: response as CachedResponse["response"] },
201
+ { fetched: today(), response },
197
202
  ]);
198
203
  return {
199
204
  citations: new Map([...cache.citations, ...dated]),
@@ -9,6 +9,7 @@ import { dirname, join } from "node:path";
9
9
  import { test } from "vitest";
10
10
  import { useTempDir } from "../test/support.ts";
11
11
  import { prepareEngine, refusal } from "./build-engine.ts";
12
+ import type { Runner } from "./engine.ts";
12
13
 
13
14
  const work = useTempDir("paperlint-build-engine-test-");
14
15
  const TEX = { packages: { acmart: ["acmart.cls"] }, tools: {} };
@@ -20,11 +21,12 @@ const texAt = (dir: string): string => {
20
21
  return dir;
21
22
  };
22
23
  /** kpsewhich answers per bin directory. */
23
- const fakeRun = (found: Record<string, string>) =>
24
- ((cmd: string) => ({
24
+ const fakeRun =
25
+ (found: Record<string, string>): Runner =>
26
+ (cmd) => ({
25
27
  stdout: found[dirname(cmd)] ?? "",
26
28
  status: 0,
27
- })) as never;
29
+ });
28
30
 
29
31
  async function go(o: Parameters<typeof prepareEngine>[0]) {
30
32
  const out: string[] = [];
@@ -101,7 +103,10 @@ test("a complete cache is used from an environment with no PATH at all", async (
101
103
 
102
104
  test("the refusal line for a decision to use a tree names nothing missing", () => {
103
105
  assert.equal(
104
- refusal({ kind: "use-cache" } as never, { system: null } as never),
106
+ refusal(
107
+ { kind: "use-cache", tree: { label: "cache", bin: "/b", missing: [] } },
108
+ { system: null },
109
+ ),
105
110
  "✗ paperlint build: no TeX Live with every package these papers need — run `npx paperlint toolchain` (missing: 0 package(s): )",
106
111
  );
107
112
  });
@@ -122,14 +127,14 @@ test("without an ask or an install given: Enter is yes, and the default install
122
127
  PAPERLINT_CTAN_MIRROR: "https://mirror.invalid/tlnet",
123
128
  PATH: join(work, "nothing"),
124
129
  },
125
- run: ((cmd: string) => {
130
+ run: (cmd) => {
126
131
  calls.push(cmd.split("/").pop() ?? cmd);
127
132
  return {
128
133
  status: 22,
129
134
  stdout: "",
130
135
  stderr: "curl: (6) could not resolve\n",
131
136
  };
132
- }) as never,
137
+ },
133
138
  });
134
139
  assert.deepEqual(r, { ok: false, code: 1 });
135
140
  assert.ok(calls.includes("curl"), calls.join(","));
@@ -62,10 +62,12 @@ type Resolved = Required<Omit<EngineOptions, "install">> & {
62
62
  };
63
63
 
64
64
  /** The options a caller actually set — `undefined` must still get its default, so no plain spread. */
65
- const defined = <T extends object>(o: T): Partial<T> =>
66
- Object.fromEntries(
67
- Object.entries(o).filter(([, v]) => v !== undefined),
68
- ) as Partial<T>;
65
+ function defined<T extends object>(o: T): Partial<T> {
66
+ const out: Partial<T> = {};
67
+ // `for…in` hands out `o`'s keys typed as its keys; an options literal has no inherited ones.
68
+ for (const k in o) if (o[k] !== undefined) out[k] = o[k];
69
+ return out;
70
+ }
69
71
 
70
72
  function withDefaults(o: EngineOptions): Resolved {
71
73
  const r = {
@@ -133,7 +135,10 @@ const names = (missing: readonly string[]): string =>
133
135
  `${String(missing.length)} package(s): ${missing.join(", ")}`;
134
136
 
135
137
  /** The one line a run without a human gets. It names the command and what is missing. */
136
- export function refusal(d: EngineDecision, facts: EngineFacts): string {
138
+ export function refusal(
139
+ d: EngineDecision,
140
+ facts: Pick<EngineFacts, "system">,
141
+ ): string {
137
142
  const missing = d.kind === "refuse" || d.kind === "ask" ? d.missing : [];
138
143
  const sys =
139
144
  facts.system && facts.system.missing.length
package/src/build.test.ts CHANGED
@@ -8,7 +8,7 @@ import assert from "node:assert/strict";
8
8
  import { existsSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { test } from "vitest";
11
- import { useTempDir, writeTree } from "../test/support.ts";
11
+ import { present, useTempDir, writeTree } from "../test/support.ts";
12
12
  import {
13
13
  bibInput,
14
14
  buildPaper,
@@ -64,16 +64,16 @@ function bibtexRun(f: Fake, cwd: string) {
64
64
  }
65
65
  function fakeRun(f: Fake): NonNullable<BuildOptions["run"]> {
66
66
  let pass = 0;
67
- return ((bin: string, _args: string[], opts: { cwd: string }) => {
67
+ return (bin, _args, opts) => {
68
68
  if (bin === f.missing)
69
69
  return {
70
70
  error: Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" }),
71
71
  status: null,
72
72
  };
73
- return bin === "pdflatex"
74
- ? latexPass(f, opts.cwd, ++pass)
75
- : bibtexRun(f, opts.cwd);
76
- }) as never;
73
+ // The build always runs a pass in the paper's directory.
74
+ const cwd = present(opts.cwd, "the pass's cwd");
75
+ return bin === "pdflatex" ? latexPass(f, cwd, ++pass) : bibtexRun(f, cwd);
76
+ };
77
77
  }
78
78
 
79
79
  const NO_GEOMETRY: Geometry = {
@@ -146,11 +146,10 @@ test("documentclass: an empty name is no class; a macro inside the name contribu
146
146
  // `\documentclass` came back bare has no name to read, and is no class.
147
147
  assert.equal(
148
148
  parseDocumentclass("\\documentclass", {
149
- parse: () =>
150
- ({
151
- type: "root",
152
- content: [{ type: "macro", content: "documentclass" }],
153
- }) as never,
149
+ parse: () => ({
150
+ type: "root",
151
+ content: [{ type: "macro", content: "documentclass" }],
152
+ }),
154
153
  }),
155
154
  null,
156
155
  );
package/src/build.ts CHANGED
@@ -61,7 +61,8 @@ import { banalMeasurer, parseBanalSettings } from "./adapters/banal/index.ts";
61
61
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
62
62
  import { hostDirs, nodeAdapters, nodeFiles } from "./adapters/node/index.ts";
63
63
  import { whyNoGeometry } from "./domain/geometry.ts";
64
- import type { AbsolutePath } from "./domain/paths.ts";
64
+ import { callerPath } from "./caller-path.ts";
65
+ import type { Runner } from "./engine.ts";
65
66
  import type { Files } from "./ports/files.ts";
66
67
  import type { MeasureGeometry } from "./ports/measure-geometry.ts";
67
68
  import type { CheckReferences } from "./ports/check-references.ts";
@@ -76,7 +77,6 @@ import {
76
77
  import {
77
78
  nextStep,
78
79
  summarize,
79
- TRACKED,
80
80
  type BibInput,
81
81
  type Hashes,
82
82
  type Observation,
@@ -128,7 +128,7 @@ export interface PaperFacts {
128
128
  }
129
129
 
130
130
  /** A process runner with `spawnSync`'s shape — the port the harness replaces. */
131
- export type Runner = typeof spawnSync;
131
+ export type { Runner };
132
132
 
133
133
  /** What a step's `run` receives. `env` is the environment accumulated by the steps before it. */
134
134
  export interface BuildContext {
@@ -286,9 +286,8 @@ function hashes(paperDir: string): Hashes {
286
286
  const p = join(paperDir, `${JOB}.${ext}`);
287
287
  return existsSync(p) ? sha(readFileSync(p)) : null;
288
288
  };
289
- const out: Record<string, string | null> = {};
290
- for (const t of TRACKED) out[t] = h(t);
291
- return out as Hashes;
289
+ // A literal of the mapped type: a new tracked extension without a hash here is a compile error.
290
+ return { aux: h("aux"), toc: h("toc"), out: h("out"), bbl: h("bbl") };
292
291
  }
293
292
 
294
293
  const readOr = (path: string, enc: BufferEncoding): string | null =>
@@ -387,8 +386,8 @@ function latexPass(
387
386
 
388
387
  /** The two streams of a finished run, as they really arrive (see `printed`). */
389
388
  interface Streams {
390
- readonly stdout: string | undefined;
391
- readonly stderr: string | undefined;
389
+ readonly stdout?: string | undefined;
390
+ readonly stderr?: string | undefined;
392
391
  }
393
392
 
394
393
  /**
@@ -694,7 +693,7 @@ function defaultMeasurer(
694
693
  return banalMeasurer(
695
694
  nodeAdapters({ tmpDir: dirs.tmp }),
696
695
  parseBanalSettings(b.env, dirs),
697
- b.projectRoot as AbsolutePath,
696
+ callerPath(b.projectRoot),
698
697
  );
699
698
  }
700
699
 
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A path as a caller gave it, made absolute where it enters a `Files` call.
3
+ *
4
+ * The app's use cases take paper directories and files as strings, and some callers pass them as
5
+ * typed on a command line (`skills/render-paper/extract-pdf-facts.mjs <paper.pdf>`). A relative one
6
+ * has always meant the process's cwd — `node:fs` resolved it there. Resolving it HERE, the same way,
7
+ * makes the `AbsolutePath` brand a checked fact rather than an assertion: the value handed to the port
8
+ * is absolute, and nothing downstream has to wonder.
9
+ */
10
+ import { resolve } from "node:path";
11
+ import { absolutePath, type AbsolutePath } from "./domain/paths.ts";
12
+
13
+ export const callerPath = (p: string): AbsolutePath => absolutePath(resolve(p));
@@ -1711,8 +1711,6 @@ console.log(
1711
1711
  typo.out.includes('paperlint.json: unknown key "typographyDept"'),
1712
1712
  );
1713
1713
  settings({
1714
- ledger: "x.jsonl",
1715
- citeChecks: "c",
1716
1714
  timezone: "UTC",
1717
1715
  contactEmail: "a@b.c",
1718
1716
  triggerCases: "t.mjs",
package/src/cli.test.ts CHANGED
@@ -10,6 +10,7 @@ import { existsSync } from "node:fs";
10
10
  import { dirname, join } from "node:path";
11
11
  import { test } from "vitest";
12
12
  import { runNode, useTempDir, writeTree } from "../test/support.ts";
13
+ import type { runToolchain } from "./toolchain.ts";
13
14
  import {
14
15
  chooseVenue,
15
16
  initTexLive,
@@ -57,6 +58,18 @@ test("parseArgs: the boolean flags of build and toolchain, and a value flag with
57
58
  );
58
59
  });
59
60
 
61
+ test("🔴 parseSettings refuses a mistyped `structure` instead of letting `lint` crash on it", () => {
62
+ // Guards: `markers: 5` used to pass the boundary, and `checkStructure` then threw
63
+ // "rules.markers.some is not a function" from inside `paperlint lint`.
64
+ for (const structure of [{ markers: 5 }, "yes", { requireOneOf: ["a"] }]) {
65
+ const r = parseSettings({ structure }, "paperlint.json", "/r");
66
+ assert.equal(r.ok, false, JSON.stringify(structure));
67
+ assert.match(r.error, /^paperlint\.json: "structure" must be false/);
68
+ }
69
+ const off = parseSettings({ structure: false }, "paperlint.json", "/r");
70
+ assert.equal(off.ok && off.value.structure, false);
71
+ });
72
+
60
73
  test("parseSettings refuses a non-object, names one unknown key or several, and a mistyped default", () => {
61
74
  const error = (json: unknown) => {
62
75
  const r = parseSettings(json, "paperlint.json", "/r");
@@ -146,7 +159,7 @@ test("runHook: a runtime with no cli.js beside it, and a hook killed by a signal
146
159
  });
147
160
  const killed = runHook("paper-edit-guard", {
148
161
  err: (m: string) => messages.push(m),
149
- run: (() => ({ status: null, signal: "SIGKILL" })) as never,
162
+ run: () => ({ status: null, signal: "SIGKILL" }),
150
163
  });
151
164
  assert.deepEqual(
152
165
  { noCli, killed, messages: messages.map((m) => m.split(" from ")[0]) },
@@ -363,7 +376,8 @@ test("a paper's paperlint.json that does not parse stops the lint with its path"
363
376
  });
364
377
 
365
378
  test("silentOptionalRules: a config with no rules turns nothing on", async () => {
366
- assert.deepEqual(await silentOptionalRules({} as never, [], {}), []);
379
+ const eslint = { calculateConfigForFile: () => Promise.resolve(undefined) };
380
+ assert.deepEqual(await silentOptionalRules(eslint, [], {}), []);
367
381
  });
368
382
 
369
383
  test("init with no path and --format md sets up the current directory", async () => {
@@ -538,8 +552,8 @@ test("ownVersion: the manifest's version; an unreadable manifest or a non-string
538
552
 
539
553
  test("init's TeX Live install runs the toolchain over the project's papers, never a check", () => {
540
554
  const dir = writeTree(join(root, "init-tex"), { "package.json": "{}" });
541
- const calls: unknown[] = [];
542
- const fake = ((o: unknown) => (calls.push(o), 0)) as never;
555
+ const calls: Parameters<typeof runToolchain>[0][] = [];
556
+ const fake: typeof runToolchain = (o) => (calls.push(o), 0);
543
557
  const log = () => {};
544
558
  const here = initTexLive(
545
559
  parseArgs(["init"]),
@@ -553,10 +567,7 @@ test("init's TeX Live install runs the toolchain over the project's papers, neve
553
567
  );
554
568
  assert.deepEqual([here.install(), there.install()], [0, 0]);
555
569
  assert.deepEqual(
556
- calls.map((c) => {
557
- const o = c as { check: boolean; tex: unknown; banal: unknown };
558
- return [o.check, o.tex, typeof o.banal];
559
- }),
570
+ calls.map((c) => [c.check, c.tex, typeof c.banal]),
560
571
  [
561
572
  [false, toolchainTex(dir), "object"],
562
573
  [false, toolchainTex(join(dir, "sub")), "object"],