paperlint 2.1.0 → 3.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 (191) hide show
  1. package/CLAUDE.md +6 -1
  2. package/README.md +233 -224
  3. package/action.yml +4 -6
  4. package/dist/adapters/references/index.d.ts +13 -0
  5. package/dist/adapters/references/index.d.ts.map +1 -0
  6. package/dist/adapters/references/index.js +55 -0
  7. package/dist/adapters/references/index.js.map +1 -0
  8. package/dist/adapters/references/reach.io.d.ts +3 -0
  9. package/dist/adapters/references/reach.io.d.ts.map +1 -0
  10. package/dist/adapters/references/reach.io.js +14 -0
  11. package/dist/adapters/references/reach.io.js.map +1 -0
  12. package/dist/build.d.ts +15 -0
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +38 -4
  15. package/dist/build.js.map +1 -1
  16. package/dist/cli.d.ts +28 -25
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +277 -265
  19. package/dist/cli.js.map +1 -1
  20. package/dist/doctor.d.ts.map +1 -1
  21. package/dist/doctor.js +40 -72
  22. package/dist/doctor.js.map +1 -1
  23. package/dist/facts-file.d.ts +2 -2
  24. package/dist/facts-file.js +2 -2
  25. package/dist/hooks-settings.d.ts +0 -21
  26. package/dist/hooks-settings.d.ts.map +1 -1
  27. package/dist/hooks-settings.js +17 -135
  28. package/dist/hooks-settings.js.map +1 -1
  29. package/dist/init.d.ts +39 -29
  30. package/dist/init.d.ts.map +1 -1
  31. package/dist/init.js +143 -172
  32. package/dist/init.js.map +1 -1
  33. package/dist/link-skills.d.ts +1 -1
  34. package/dist/link-skills.d.ts.map +1 -1
  35. package/dist/link-skills.js +11 -29
  36. package/dist/link-skills.js.map +1 -1
  37. package/dist/new-paper.d.ts.map +1 -1
  38. package/dist/new-paper.js +2 -13
  39. package/dist/new-paper.js.map +1 -1
  40. package/dist/paper-files.d.ts +63 -0
  41. package/dist/paper-files.d.ts.map +1 -0
  42. package/dist/paper-files.js +69 -0
  43. package/dist/paper-files.js.map +1 -0
  44. package/dist/paper-settings.d.ts +13 -41
  45. package/dist/paper-settings.d.ts.map +1 -1
  46. package/dist/paper-settings.js +71 -71
  47. package/dist/paper-settings.js.map +1 -1
  48. package/dist/ports/check-references.d.ts +31 -0
  49. package/dist/ports/check-references.d.ts.map +1 -0
  50. package/dist/ports/check-references.js +7 -0
  51. package/dist/ports/check-references.js.map +1 -0
  52. package/dist/presets.d.ts +1 -1
  53. package/dist/presets.d.ts.map +1 -1
  54. package/dist/presets.js +4 -6
  55. package/dist/presets.js.map +1 -1
  56. package/dist/reference-rules.d.ts +47 -0
  57. package/dist/reference-rules.d.ts.map +1 -0
  58. package/dist/reference-rules.js +156 -0
  59. package/dist/reference-rules.js.map +1 -0
  60. package/dist/references.d.ts +36 -0
  61. package/dist/references.d.ts.map +1 -0
  62. package/dist/references.js +91 -0
  63. package/dist/references.js.map +1 -0
  64. package/dist/rules-config.d.ts +5 -3
  65. package/dist/rules-config.d.ts.map +1 -1
  66. package/dist/rules-config.js +15 -6
  67. package/dist/rules-config.js.map +1 -1
  68. package/dist/structure.js +3 -3
  69. package/dist/structure.js.map +1 -1
  70. package/dist/tex-requirements.js +2 -2
  71. package/dist/tex-requirements.js.map +1 -1
  72. package/dist/types.d.ts +20 -18
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/venue-rules.d.ts.map +1 -1
  75. package/dist/venue-rules.js +11 -9
  76. package/dist/venue-rules.js.map +1 -1
  77. package/docs/configuration.md +119 -104
  78. package/docs/e2e.md +1 -1
  79. package/docs/install.md +25 -40
  80. package/docs/optional-rules.md +5 -4
  81. package/docs/rules.md +60 -23
  82. package/eslint-rules/bib-reachable-entry.mjs +75 -0
  83. package/eslint-rules/latex-language.mjs +29 -1
  84. package/eslint-rules/paper-stages.harness.mjs +1 -117
  85. package/eslint-rules/paper-stages.mjs +0 -120
  86. package/eslint-rules/paper-stages.mutations.mjs +0 -76
  87. package/eslint-rules/paper-typography.mjs +267 -192
  88. package/eslint-rules/paper-typography.test.mjs +297 -0
  89. package/eslint-rules/papers.harness.mjs +8 -12
  90. package/eslint-rules/papers.mjs +17 -31
  91. package/eslint-rules/pdf-last-page-balance.mjs +4 -7
  92. package/eslint-rules/review-frontmatter.mjs +118 -0
  93. package/eslint-rules/review-frontmatter.schema.json +36 -0
  94. package/eslint-rules/review-frontmatter.test.mjs +118 -0
  95. package/eslint-rules/sibling-frontmatter.mjs +19 -0
  96. package/eslint-rules/sibling-frontmatter.schema.json +14 -0
  97. package/eslint.config.mjs +0 -15
  98. package/fixtures/real-markdown-paper/baseline.json +6 -7
  99. package/hooks/hooks.harness.mjs +39 -98
  100. package/hooks/hooks.mutations.mjs +6 -6
  101. package/hooks/paper-edit-guard.hook.d.mts +2 -3
  102. package/hooks/paper-edit-guard.hook.mjs +31 -42
  103. package/hooks/paper-skills-nudge.hook.mjs +21 -19
  104. package/hooks/paper-status-gates.hook.mjs +21 -19
  105. package/hooks/paper-status-gates.sh +8 -13
  106. package/lib/paper-config.d.mts +15 -25
  107. package/lib/paper-config.harness.mjs +18 -35
  108. package/lib/paper-config.mjs +83 -105
  109. package/lib/paper-config.mutations.mjs +2 -2
  110. package/lib/skill-checks.mjs +1 -1
  111. package/lib/skill-trigger-cases.harness.mjs +9 -8
  112. package/lib/skill-trigger-cases.mjs +7 -15
  113. package/package.json +1 -1
  114. package/scripts/eslint-report-guard.mjs +1 -1
  115. package/scripts/harness-api.frozen.json +0 -3
  116. package/scripts/mutation-batteries.frozen.json +4 -13
  117. package/skills/analyze-sibling-paper/SKILL.md +16 -12
  118. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
  119. package/skills/cold-read-diff/SKILL.md +20 -1
  120. package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
  121. package/skills/grade-paper-writing/SKILL.md +20 -1
  122. package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
  123. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
  124. package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
  125. package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
  126. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
  127. package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
  128. package/skills/plan-paper-timeline/SKILL.md +2 -2
  129. package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
  130. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
  131. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
  132. package/skills/render-paper/check-render.sh +2 -3
  133. package/skills/render-paper/render-paper.harness.mjs +2 -2
  134. package/skills/verify-citations/SKILL.md +10 -1
  135. package/skills/verify-citations/SKILL.md.spec.ts +9 -0
  136. package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
  137. package/src/adapters/references/index.ts +102 -0
  138. package/src/adapters/references/reach.io.ts +12 -0
  139. package/src/build.harness.mjs +8 -4
  140. package/src/build.ts +62 -6
  141. package/src/cli.harness.mjs +147 -292
  142. package/src/cli.mutations.mjs +35 -44
  143. package/src/cli.ts +344 -304
  144. package/src/doctor.harness.mjs +41 -84
  145. package/src/doctor.mutations.mjs +9 -9
  146. package/src/doctor.ts +44 -89
  147. package/src/facts-file.test.ts +0 -7
  148. package/src/facts-file.ts +2 -2
  149. package/src/hooks-settings.harness.mjs +7 -92
  150. package/src/hooks-settings.mutations.mjs +2 -17
  151. package/src/hooks-settings.ts +18 -162
  152. package/src/init.test.ts +206 -0
  153. package/src/init.ts +186 -230
  154. package/src/link-skills.harness.mjs +1 -38
  155. package/src/link-skills.mutations.mjs +9 -5
  156. package/src/link-skills.ts +10 -34
  157. package/src/lint-fix.test.ts +120 -0
  158. package/src/new-paper.test.ts +1 -13
  159. package/src/new-paper.ts +2 -17
  160. package/src/paper-files.test.ts +290 -0
  161. package/src/paper-files.ts +144 -0
  162. package/src/paper-settings-commands.test.ts +84 -121
  163. package/src/paper-settings.test.ts +93 -64
  164. package/src/paper-settings.ts +99 -97
  165. package/src/ports/check-references.ts +28 -0
  166. package/src/presets.ts +5 -15
  167. package/src/reference-rules.ts +229 -0
  168. package/src/references.test.ts +209 -0
  169. package/src/references.ts +138 -0
  170. package/src/rules-config.harness.mjs +24 -8
  171. package/src/rules-config.ts +19 -7
  172. package/src/structure.harness.mjs +3 -1
  173. package/src/structure.ts +3 -3
  174. package/src/tex-requirements.ts +2 -2
  175. package/src/types.ts +30 -18
  176. package/src/venue-rules.test.ts +20 -11
  177. package/src/venue-rules.ts +11 -13
  178. package/eslint-rules/doc-fields.harness.mjs +0 -336
  179. package/eslint-rules/doc-fields.mjs +0 -186
  180. package/eslint-rules/doc-fields.mutations.mjs +0 -96
  181. package/eslint-rules/paper-typography.harness.mjs +0 -291
  182. package/eslint-rules/paper-typography.mutations.mjs +0 -131
  183. package/eslint-rules/review-findings-cause.harness.mjs +0 -228
  184. package/eslint-rules/review-findings-cause.mjs +0 -135
  185. package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
  186. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
  187. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
  188. package/fixtures/review-findings-cause/clean.md +0 -17
  189. package/fixtures/review-findings-cause/defect.md +0 -14
  190. package/fixtures/review-findings-cause/old-debt.md +0 -14
  191. package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The online reference checks, as the `CheckReferences` port: `verify-cites` (the cited work
3
+ * exists, the title matches — Crossref, OpenAlex, Semantic Scholar, arXiv) and `bib-authors` (the
4
+ * authors are the published version's — DBLP). Both ship in this package's verify-citations skill;
5
+ * this adapter only runs them and shapes their answers.
6
+ *
7
+ * "Not checked" is decided up front, by one request: when Crossref cannot be reached at all, the
8
+ * checkers would degrade every entry to `unresolvable` / `unchecked`, which reads like a result.
9
+ * Saying `not-checked` with the reason is the honest record.
10
+ */
11
+ import type {
12
+ CheckReferences,
13
+ EntryVerdict,
14
+ } from "../../ports/check-references.ts";
15
+ import { unreachable } from "./reach.io.ts";
16
+ // @ts-expect-error — a skill script in .mjs, it has no types
17
+ import * as cites from "../../../skills/verify-citations/scripts/verify-cites.mjs";
18
+ // @ts-expect-error — a skill script in .mjs, it has no types
19
+ import * as authors from "../../../skills/verify-citations/scripts/bib-authors.mjs";
20
+
21
+ interface CiteResult {
22
+ readonly id: string;
23
+ readonly verdict: "true" | "false" | "unresolvable";
24
+ readonly reason?: string;
25
+ }
26
+ interface AuthorFinding {
27
+ readonly key: string;
28
+ readonly venue: string;
29
+ readonly missing: readonly string[];
30
+ readonly extra: readonly string[];
31
+ readonly orderDiffers: boolean;
32
+ }
33
+ interface KeyWhy {
34
+ readonly key: string;
35
+ readonly why: string;
36
+ }
37
+
38
+ const describeAuthors = (f: AuthorFinding): string =>
39
+ [
40
+ f.missing.length ? `missing ${f.missing.join(", ")}` : "",
41
+ f.extra.length ? `extra ${f.extra.join(", ")}` : "",
42
+ f.orderDiffers ? "order differs" : "",
43
+ ]
44
+ .filter(Boolean)
45
+ .join("; ") + ` (DBLP: ${f.venue})`;
46
+
47
+ interface AuthorBuckets {
48
+ readonly findings: readonly AuthorFinding[];
49
+ readonly skipped: readonly KeyWhy[];
50
+ readonly unchecked: readonly KeyWhy[];
51
+ readonly matched: readonly string[];
52
+ }
53
+
54
+ const authorsOf = (key: string, a: AuthorBuckets): EntryVerdict["authors"] =>
55
+ a.findings.some((f) => f.key === key)
56
+ ? "mismatch"
57
+ : a.unchecked.some((u) => u.key === key)
58
+ ? "unchecked"
59
+ : a.matched.includes(key)
60
+ ? "match"
61
+ : "skipped";
62
+
63
+ /** One entry's verdict from the two checkers' answers. */
64
+ function entryVerdict(
65
+ key: string,
66
+ found: readonly CiteResult[],
67
+ a: AuthorBuckets,
68
+ ): EntryVerdict {
69
+ const c = found.find((x) => x.id === key);
70
+ const mismatch = a.findings.find((f) => f.key === key);
71
+ const why = [
72
+ c && c.verdict !== "true" ? c.reason : undefined,
73
+ mismatch ? describeAuthors(mismatch) : undefined,
74
+ a.unchecked.find((u) => u.key === key)?.why,
75
+ ]
76
+ .filter(Boolean)
77
+ .join("; ");
78
+ return {
79
+ key,
80
+ exists: c?.verdict ?? "unresolvable",
81
+ authors: authorsOf(key, a),
82
+ ...(why ? { why } : {}),
83
+ };
84
+ }
85
+
86
+ export const onlineReferences: CheckReferences = async (bib) => {
87
+ const why = await unreachable();
88
+ if (why !== null) return { kind: "not-checked", why };
89
+ const cache = {};
90
+ const found: CiteResult[] = [];
91
+ for (const c of cites.parseBib(bib) as { id?: string }[])
92
+ if (c.id) found.push(await cites.verifyCitationLive(c, { cache }));
93
+ const a = (await authors.checkAuthors(
94
+ authors.parseBib(bib),
95
+ )) as AuthorBuckets;
96
+ const keys = new Set([
97
+ ...found.map((c) => c.id),
98
+ ...a.findings.map((f) => f.key),
99
+ ]);
100
+ const entries = [...keys].map((key) => entryVerdict(key, found, a));
101
+ return { kind: "checked", entries };
102
+ };
@@ -0,0 +1,12 @@
1
+ /** Whether the citation services answer at all — the one request that decides "not checked". */
2
+ export async function unreachable(): Promise<string | null> {
3
+ try {
4
+ await fetch("https://api.crossref.org/", {
5
+ method: "HEAD",
6
+ signal: AbortSignal.timeout(10_000),
7
+ });
8
+ return null;
9
+ } catch (e) {
10
+ return `the citation services cannot be reached (${(e as Error).message})`;
11
+ }
12
+ }
@@ -273,10 +273,14 @@ try {
273
273
  // ── the measure step: facts for the lint rules ────────────────────────────────────────
274
274
  const factsFile = join(clean, "_build", "paper.facts.json");
275
275
  check(
276
- "measure: the plan's last step writes the facts, and says it judges nothing",
277
- r.plan.at(-1)?.step === "measure" &&
278
- /pdf\.js → _build\/paper\.facts\.json/.test(r.plan.at(-1)?.why ?? "") &&
279
- /nothing is judged/.test(r.plan.at(-1)?.why ?? ""),
276
+ "measure: the plan's measure step writes the facts, and says it judges nothing",
277
+ r.plan.find((p) => p.step === "measure") !== undefined &&
278
+ /pdf\.js → _build\/paper\.facts\.json/.test(
279
+ r.plan.find((p) => p.step === "measure")?.why ?? "",
280
+ ) &&
281
+ /nothing is judged/.test(
282
+ r.plan.find((p) => p.step === "measure")?.why ?? "",
283
+ ),
280
284
  );
281
285
  check(
282
286
  "🔴 measure: a green build wrote _build/paper.facts.json through the readPdf port, on paper.pdf",
package/src/build.ts CHANGED
@@ -45,7 +45,7 @@ import { delimiter, join, relative } from "node:path";
45
45
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
46
46
  import { getParser } from "@unified-latex/unified-latex-util-parse";
47
47
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
48
- import { PAPER_SETTINGS_FILE } from "../lib/paper-config.mjs";
48
+ import { CONFIG_FILE } from "../lib/paper-config.mjs";
49
49
  import {
50
50
  declaredVenue,
51
51
  factsPath,
@@ -64,6 +64,8 @@ import { whyNoGeometry } from "./domain/geometry.ts";
64
64
  import type { AbsolutePath } from "./domain/paths.ts";
65
65
  import type { Files } from "./ports/files.ts";
66
66
  import type { MeasureGeometry } from "./ports/measure-geometry.ts";
67
+ import type { CheckReferences } from "./ports/check-references.ts";
68
+ import { recordReferences, REFERENCES_FILE } from "./references.ts";
67
69
  import {
68
70
  auxBib,
69
71
  bibtexExcerpt,
@@ -87,7 +89,7 @@ export const PAPER_MARKERS = [
87
89
  "PIPELINE-STATUS.md",
88
90
  "paper.tex",
89
91
  "paper.md",
90
- PAPER_SETTINGS_FILE,
92
+ CONFIG_FILE,
91
93
  ];
92
94
 
93
95
  /** The source paperlint compiles, and the job name every output file carries. */
@@ -135,6 +137,8 @@ export interface BuildContext {
135
137
  readonly measure: MeasureGeometry;
136
138
  /** Where the facts file is written. */
137
139
  readonly files: Files;
140
+ /** The online reference checks; the CLI wires the real ones, a test passes a function. */
141
+ readonly checkReferences: CheckReferences;
138
142
  }
139
143
 
140
144
  export type StepOutcome =
@@ -521,11 +525,36 @@ export const measureStep: BuildStep = {
521
525
  },
522
526
  };
523
527
 
528
+ // ── step: references ────────────────────────────────────────────────────────────────────
529
+
530
+ /**
531
+ * Check the bibliography online — the cited work exists and its title matches (verify-cites), its
532
+ * authors are the published version's (bib-authors) — and record the verdicts in
533
+ * `_build/references.json` for the offline lint rules (`reference-rules.ts`). OPTIONAL and NEVER
534
+ * FAILING: without network the PDF is still built, and the record says `not-checked`.
535
+ */
536
+ export const referencesStep: BuildStep = {
537
+ name: "references",
538
+ required: false,
539
+ applies: (facts) =>
540
+ facts.main
541
+ ? {
542
+ yes: true,
543
+ why: `online: citations exist, titles and authors match → ${FACTS_DIR}/${REFERENCES_FILE} (never fails the build)`,
544
+ }
545
+ : { yes: false, why: "nothing is compiled" },
546
+ run: async (ctx) => ({
547
+ ok: true,
548
+ note: await recordReferences(ctx.files, ctx.paperDir, ctx.checkReferences),
549
+ }),
550
+ };
551
+
524
552
  /** The build, in order. A new step is one entry here. */
525
553
  export const STEPS: readonly BuildStep[] = [
526
554
  inputsStep,
527
555
  compileStep,
528
556
  measureStep,
557
+ referencesStep,
529
558
  ];
530
559
 
531
560
  // ── the command ─────────────────────────────────────────────────────────────────────────
@@ -574,6 +603,11 @@ export interface BuildOptions {
574
603
  measure?: MeasureGeometry;
575
604
  /** Where the facts file is written. Default: the disk. */
576
605
  files?: Files;
606
+ /**
607
+ * The online reference checks. Default: none — the record then says `not-checked`, and lint
608
+ * warns. The CLI passes the real ones (`adapters/references`).
609
+ */
610
+ checkReferences?: CheckReferences;
577
611
  }
578
612
 
579
613
  /**
@@ -594,13 +628,21 @@ function baseDefaults({
594
628
  dryRun = false,
595
629
  readPdf = pdfjsReader,
596
630
  projectRoot = env["CLAUDE_PROJECT_DIR"] || cwd,
597
- }: BuildOptions): Required<Omit<BuildOptions, "measure" | "files">> {
631
+ }: BuildOptions): Required<
632
+ Omit<BuildOptions, "measure" | "files" | "checkReferences">
633
+ > {
598
634
  return { run, cwd, env, steps, log, dryRun, readPdf, projectRoot };
599
635
  }
600
636
 
637
+ /** Without a checker the record says so — never a pass. The CLI wires the real one. */
638
+ const notWired: CheckReferences = async () => ({
639
+ kind: "not-checked",
640
+ why: "no reference checker was wired into this build",
641
+ });
642
+
601
643
  /** banal as the measurer, wired from the build's environment: the one piece of root work left here (#76). */
602
644
  function defaultMeasurer(
603
- b: Required<Omit<BuildOptions, "measure" | "files">>,
645
+ b: Required<Omit<BuildOptions, "measure" | "files" | "checkReferences">>,
604
646
  ): MeasureGeometry {
605
647
  const dirs = hostDirs({ cwd: b.cwd });
606
648
  return banalMeasurer(
@@ -614,7 +656,12 @@ function defaultMeasurer(
614
656
  function withDefaults(o: BuildOptions): Required<BuildOptions> {
615
657
  const base = baseDefaults(o);
616
658
  const measure = o.measure ?? defaultMeasurer(base);
617
- return { ...base, measure, files: o.files ?? nodeFiles };
659
+ return {
660
+ ...base,
661
+ measure,
662
+ files: o.files ?? nodeFiles,
663
+ checkReferences: o.checkReferences ?? notWired,
664
+ };
618
665
  }
619
666
 
620
667
  /** Run the applicable steps in order, each on the environment the steps before it left. */
@@ -622,7 +669,15 @@ async function runSteps(
622
669
  paperDir: string,
623
670
  dir: string,
624
671
  plan: PlanLine[],
625
- { run, env, steps, readPdf, measure, files }: Required<BuildOptions>,
672
+ {
673
+ run,
674
+ env,
675
+ steps,
676
+ readPdf,
677
+ measure,
678
+ files,
679
+ checkReferences,
680
+ }: Required<BuildOptions>,
626
681
  ): Promise<BuildResult> {
627
682
  let stepEnv = env;
628
683
  const notes: string[] = [];
@@ -635,6 +690,7 @@ async function runSteps(
635
690
  readPdf,
636
691
  measure,
637
692
  files,
693
+ checkReferences,
638
694
  });
639
695
  if (!out.ok) {
640
696
  // The PDF THIS run wrote and the step then rejected (a partial pass).