paperlint 13.2.0 → 13.3.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 (38) hide show
  1. package/.github/workflows/ci.yml +10 -0
  2. package/dist/doctor.d.ts.map +1 -1
  3. package/dist/doctor.js +2 -2
  4. package/dist/doctor.js.map +1 -1
  5. package/dist/skills/assess-novelty/scripts/candidate_page.d.ts +1 -0
  6. package/dist/skills/assess-novelty/scripts/novelty_reader_brief.d.ts +25 -0
  7. package/docs/toolchain.md +1 -1
  8. package/eslint.config.mjs +52 -0
  9. package/package.json +2 -1
  10. package/presets/agentverify.md +15 -4
  11. package/skills/assess-novelty/scripts/candidate_page.css +31 -0
  12. package/skills/assess-novelty/scripts/candidate_page.html +934 -0
  13. package/skills/assess-novelty/scripts/candidate_page.js +165 -0
  14. package/skills/assess-novelty/scripts/candidate_page.test.ts +401 -0
  15. package/skills/assess-novelty/scripts/candidate_page.ts +194 -0
  16. package/skills/assess-novelty/scripts/check_novelty.py +230 -0
  17. package/skills/assess-novelty/scripts/check_novelty.test.ts +490 -0
  18. package/skills/assess-novelty/scripts/check_novelty_coverage.test.ts +176 -0
  19. package/skills/assess-novelty/scripts/crop_quote.py +104 -0
  20. package/skills/assess-novelty/scripts/crop_quote.test.ts +130 -0
  21. package/skills/assess-novelty/scripts/fixtures/crop-quote/make_sample.py +42 -0
  22. package/skills/assess-novelty/scripts/fixtures/crop-quote/sample.pdf +106 -0
  23. package/skills/assess-novelty/scripts/fixtures/novelty/CLAIMS.md +11 -0
  24. package/skills/assess-novelty/scripts/fixtures/novelty/paper.tex +10 -0
  25. package/skills/assess-novelty/scripts/fixtures/novelty/siblings/2026-10-27-novelty-refutation.md +8 -0
  26. package/skills/assess-novelty/scripts/fixtures/novelty/siblings/data/doe-preprint.txt +63 -0
  27. package/skills/assess-novelty/scripts/fixtures/novelty/siblings/novelty.json +54 -0
  28. package/skills/assess-novelty/scripts/novelty_candidates.py +136 -0
  29. package/skills/assess-novelty/scripts/novelty_candidates.test.ts +222 -0
  30. package/skills/assess-novelty/scripts/novelty_cells.py +88 -0
  31. package/skills/assess-novelty/scripts/novelty_cells.test.ts +159 -0
  32. package/skills/assess-novelty/scripts/novelty_coverage.py +55 -0
  33. package/skills/assess-novelty/scripts/novelty_reader_brief.js +165 -0
  34. package/skills/assess-novelty/scripts/novelty_reader_brief.test.ts +251 -0
  35. package/skills/assess-novelty/scripts/novelty_reader_brief.ts +215 -0
  36. package/skills/assess-novelty/scripts/quote_match.py +34 -0
  37. package/skills/assess-novelty/scripts/script_edges.test.ts +174 -0
  38. package/src/doctor.ts +4 -2
@@ -0,0 +1,174 @@
1
+ /**
2
+ * candidate_page.ts and novelty_reader_brief.ts, the edges the other tests do not reach: the fallbacks for a
3
+ * missing field, a missing file, a wrong argument count. Each case runs the script as a process on a fixture copy.
4
+ * Run: npx vitest run skills/assess-novelty/scripts/script_edges.test.ts
5
+ */
6
+ import {
7
+ cpSync,
8
+ mkdirSync,
9
+ mkdtempSync,
10
+ readFileSync,
11
+ realpathSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { spawnSync } from "node:child_process";
16
+ import { tmpdir } from "node:os";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import assert from "node:assert/strict";
20
+ import { test } from "vitest";
21
+
22
+ const here = dirname(fileURLToPath(import.meta.url));
23
+ const FIXTURE = join(here, "fixtures", "novelty");
24
+ const PAGE = join(here, "candidate_page.ts");
25
+ const BRIEF = join(here, "novelty_reader_brief.ts");
26
+ const PNG = Buffer.from(
27
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
28
+ "base64",
29
+ );
30
+ const SENTENCE = "A sentence of at least eight words, quoted from the text.";
31
+
32
+ const node = (script: string, ...args: string[]) => {
33
+ const r = spawnSync(process.execPath, [script, ...args], {
34
+ encoding: "utf8",
35
+ });
36
+ return { code: r.status, out: r.stdout, err: r.stderr };
37
+ };
38
+
39
+ const withDir = (body: (dir: string) => void) => {
40
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "script-edges-")));
41
+ try {
42
+ body(dir);
43
+ } finally {
44
+ rmSync(dir, { recursive: true, force: true });
45
+ }
46
+ };
47
+
48
+ const paperWith = (dir: string, ids: string[]) => {
49
+ cpSync(FIXTURE, dir, { recursive: true });
50
+ const siblings = join(dir, "siblings");
51
+ for (const d of ["candidates", "data/crops"])
52
+ mkdirSync(join(siblings, d), { recursive: true });
53
+ for (const id of ids) {
54
+ writeFileSync(
55
+ join(siblings, "candidates", `${id}.json`),
56
+ JSON.stringify({
57
+ id,
58
+ title: "A title",
59
+ url: `https://example.org/${id}`,
60
+ text: `siblings/data/${id}.txt`,
61
+ found_by: [{ source: "paper.tex ref1" }],
62
+ summary: "What it does.",
63
+ abstract: { page: 1, text: SENTENCE },
64
+ conclusion: { page: 2, text: SENTENCE },
65
+ passages: [
66
+ {
67
+ claim: "P5-1",
68
+ page: 1,
69
+ quote: SENTENCE,
70
+ crop: `siblings/data/crops/${id}-1.png`,
71
+ meaning: "It matters.",
72
+ },
73
+ ],
74
+ suggestion: "cite",
75
+ suggestion_why: "Because.",
76
+ }),
77
+ );
78
+ writeFileSync(join(siblings, "data", `${id}.txt`), `${SENTENCE}\n`);
79
+ writeFileSync(join(siblings, "data", "crops", `${id}-1.png`), PNG);
80
+ }
81
+ };
82
+
83
+ const setJson = (dir: string, file: string, change: (j: any) => any) => {
84
+ const p = join(dir, "siblings", file);
85
+ writeFileSync(p, JSON.stringify(change(JSON.parse(readFileSync(p, "utf8")))));
86
+ };
87
+
88
+ test("candidate_page: usage, no candidates, a broken candidate file", () => {
89
+ assert.equal(node(PAGE).code, 2, "no arguments is a usage error");
90
+ assert.match(node(PAGE).err, /usage: node candidate_page.ts/);
91
+ withDir((dir) => {
92
+ cpSync(FIXTURE, dir, { recursive: true });
93
+ const r = node(PAGE, "build", dir, join(dir, "out.html"));
94
+ assert.equal(r.code, 1);
95
+ assert.match(r.err, /no candidates in/);
96
+ });
97
+ withDir((dir) => {
98
+ paperWith(dir, ["a1"]);
99
+ writeFileSync(join(dir, "siblings", "candidates", "a1.json"), "{not json");
100
+ const r = node(PAGE, "build", dir, join(dir, "out.html"));
101
+ assert.equal(r.code, 1, "a candidate file that is not JSON is refused");
102
+ });
103
+ });
104
+
105
+ test("candidate_page build: a page config without session or pdfs, claims without sentences or absent", () => {
106
+ withDir((dir) => {
107
+ paperWith(dir, ["a1"]);
108
+ writeFileSync(join(dir, "siblings", "candidate-page.json"), "{}");
109
+ setJson(dir, "novelty.json", (j) => {
110
+ delete j.claims[0].sentence;
111
+ return j;
112
+ });
113
+ const out = join(dir, "out.html");
114
+ const r = node(PAGE, "build", dir, out);
115
+ assert.equal(r.code, 0, r.err);
116
+ assert.match(readFileSync(out, "utf8"), /"session":""/);
117
+ setJson(dir, "novelty.json", (j) => {
118
+ delete j.claims;
119
+ return j;
120
+ });
121
+ assert.equal(node(PAGE, "build", dir, out).code, 0);
122
+ assert.match(readFileSync(out, "utf8"), /"claims":\{\}/);
123
+ });
124
+ });
125
+
126
+ test("candidate_page export: a flat folder, a wrapped document, no reason, an already decided and an undecided candidate", () => {
127
+ withDir((dir) => {
128
+ paperWith(dir, ["a1", "a2", "a3"]);
129
+ setJson(dir, "candidates/a2.json", (j) => ({
130
+ ...j,
131
+ decision: {
132
+ choice: "skip",
133
+ reason: "r",
134
+ at: "2026-01-01T00:00:00Z",
135
+ source: "chat",
136
+ },
137
+ }));
138
+ const flat = join(dir, "dump");
139
+ mkdirSync(flat);
140
+ writeFileSync(
141
+ join(flat, "a1.json"),
142
+ JSON.stringify({ data: { choice: "cite" } }),
143
+ );
144
+ const r = node(PAGE, "export", dir, flat);
145
+ assert.equal(r.code, 0, r.err);
146
+ assert.match(r.out, /^a1: cite$/m, "no reason, no dash");
147
+ assert.match(r.out, /^still undecided: a3$/m, "a2 decided already, a3 not");
148
+ });
149
+ });
150
+
151
+ test("novelty_reader_brief: a missing novelty.json, no neighbours, a neighbour without title or url", () => {
152
+ withDir((dir) => {
153
+ const check = node(BRIEF, "check", dir, "n1");
154
+ assert.equal(check.code, 1);
155
+ assert.match(check.err, /cannot read .*novelty\.json/);
156
+ mkdirSync(join(dir, "siblings"));
157
+ writeFileSync(join(dir, "siblings", "novelty.json"), "{}");
158
+ const none = node(BRIEF, dir, "n1");
159
+ assert.equal(none.code, 1);
160
+ assert.match(none.err, /no neighbour n1 in novelty\.json \(have: \)/);
161
+ assert.equal(node(BRIEF, "check", dir, "n1").code, 1);
162
+ writeFileSync(
163
+ join(dir, "siblings", "novelty.json"),
164
+ JSON.stringify({ neighbours: [{ id: "n1", text: "t.txt" }] }),
165
+ );
166
+ const missing = node(BRIEF, dir, "n1");
167
+ assert.equal(missing.code, 2);
168
+ assert.match(missing.err, /arxiv\.org\/pdf\/<id>/);
169
+ writeFileSync(join(dir, "t.txt"), "");
170
+ const brief = node(BRIEF, dir, "n1");
171
+ assert.equal(brief.code, 0, brief.err);
172
+ assert.match(brief.out, /Neighbour: n1 — {2}\n/);
173
+ });
174
+ });
package/src/doctor.ts CHANGED
@@ -95,8 +95,10 @@ export const PROGRAMS: readonly Program[] = [
95
95
  {
96
96
  bin: "python3",
97
97
  from: "your system",
98
- without: "the analysis and report scripts do not start",
99
- install: "apt-get install -y python3",
98
+ without:
99
+ "the analysis and report scripts and the novelty checks (assess-novelty) do not start",
100
+ install:
101
+ "apt-get install -y python3; crop_quote.py also needs PyMuPDF: pip install pymupdf",
100
102
  },
101
103
  ];
102
104