paperlint 2.0.1 → 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.
- package/CLAUDE.md +6 -1
- package/CONTRIBUTING.md +21 -0
- package/README.md +231 -180
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +5 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +16 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +40 -5
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +37 -20
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +377 -251
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +37 -52
- package/dist/doctor.js.map +1 -1
- package/dist/facts-file.d.ts +33 -9
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +117 -17
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +40 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +144 -109
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +2 -2
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +6 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +63 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +69 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +40 -0
- package/dist/paper-settings.d.ts.map +1 -0
- package/dist/paper-settings.js +144 -0
- package/dist/paper-settings.js.map +1 -0
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +184 -0
- package/dist/presets.js.map +1 -0
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +11 -2
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +25 -11
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +3 -2
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +63 -7
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +91 -24
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +20 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +109 -0
- package/dist/venue-rules.d.ts.map +1 -0
- package/dist/venue-rules.js +389 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +152 -77
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +26 -24
- package/docs/rules.md +181 -15
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +267 -192
- package/eslint-rules/paper-typography.test.mjs +297 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -27
- package/eslint-rules/pdf-last-page-balance.mjs +23 -5
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -18
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +90 -81
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +4 -13
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
- package/skills/paper-pipeline/SKILL.md +4 -3
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/extract-pdf-facts.harness.mjs +5 -5
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/study-accepted-papers/SKILL.md +2 -1
- package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -0
- package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
- package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
- package/skills/submit-paper/references/venues/realm.jsonc +45 -43
- package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
- package/skills/submit-paper/references/venues/venue-profile.schema.json +119 -29
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build-engine.ts +10 -2
- package/src/build.harness.mjs +31 -22
- package/src/build.ts +65 -8
- package/src/cli.harness.mjs +151 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +457 -288
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +41 -61
- package/src/facts-file.test.ts +47 -7
- package/src/facts-file.ts +145 -24
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +193 -159
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/new-paper.harness.mjs +9 -6
- package/src/new-paper.test.ts +133 -0
- package/src/new-paper.ts +7 -2
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +274 -0
- package/src/paper-settings.test.ts +249 -0
- package/src/paper-settings.ts +208 -0
- package/src/ports/check-references.ts +28 -0
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +297 -0
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +30 -13
- package/src/structure.harness.mjs +6 -4
- package/src/structure.ts +3 -2
- package/src/tex-requirements.harness.mjs +11 -17
- package/src/tex-requirements.ts +179 -29
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +554 -0
- package/src/venue-rules.ts +571 -0
- package/templates/paper/paperlint.json +4 -0
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/build-e2e/acmart/venue.json +0 -1
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
|
@@ -4,7 +4,7 @@ description: Verify every citation is a real work with correct metadata, and tha
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:2bb9f6f1233094cf compiled from skills/verify-citations/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# verify-citations — every cite real, the delta explicit
|
|
10
10
|
|
|
@@ -136,6 +136,15 @@ any `false` (fabrication)** — `unresolvable` alone is advisory and does NOT fa
|
|
|
136
136
|
narrowed-false philosophy. API responses are cached to `scripts/.cite-cache.json` (gitignored) so re-runs are
|
|
137
137
|
cheap and deterministic.
|
|
138
138
|
|
|
139
|
+
## 5a. Both checks run on every `paperlint build`
|
|
140
|
+
|
|
141
|
+
`npx paperlint build` runs `verify-cites` and `bib-authors` over the paper's bibliography after the PDF is
|
|
142
|
+
built and records the verdicts, with the SHA-256 of the bibliography it checked, in
|
|
143
|
+
`<paper>/_build/references.json`. `paperlint lint` reads that record offline: `paper/cite-exists` and
|
|
144
|
+
`paper/author-list` report a failing entry on its own line, `paper/refs-fresh` says when the bibliography
|
|
145
|
+
changed since, and `paper/refs-checked` warns when nothing was recorded or the build had no network. The
|
|
146
|
+
step never fails the build. Run the scripts by hand (below) to read a single verdict in full.
|
|
147
|
+
|
|
139
148
|
## 5b. The author-list gate — `scripts/bib-authors.mjs`
|
|
140
149
|
|
|
141
150
|
`verify-cites.mjs` answers *"does this citation exist, and does the id point at it?"*. There is a second
|
|
@@ -152,6 +152,15 @@ any \`false\` (fabrication)** — \`unresolvable\` alone is advisory and does NO
|
|
|
152
152
|
narrowed-false philosophy. API responses are cached to \`scripts/.cite-cache.json\` (gitignored) so re-runs are
|
|
153
153
|
cheap and deterministic.
|
|
154
154
|
|
|
155
|
+
## 5a. Both checks run on every \`paperlint build\`
|
|
156
|
+
|
|
157
|
+
\`npx paperlint build\` runs \`verify-cites\` and \`bib-authors\` over the paper's bibliography after the PDF is
|
|
158
|
+
built and records the verdicts, with the SHA-256 of the bibliography it checked, in
|
|
159
|
+
\`<paper>/_build/references.json\`. \`paperlint lint\` reads that record offline: \`paper/cite-exists\` and
|
|
160
|
+
\`paper/author-list\` report a failing entry on its own line, \`paper/refs-fresh\` says when the bibliography
|
|
161
|
+
changed since, and \`paper/refs-checked\` warns when nothing was recorded or the build had no network. The
|
|
162
|
+
step never fails the build. Run the scripts by hand (below) to read a single verdict in full.
|
|
163
|
+
|
|
155
164
|
## 5b. The author-list gate — \`scripts/bib-authors.mjs\`
|
|
156
165
|
|
|
157
166
|
\`verify-cites.mjs\` answers *"does this citation exist, and does the id point at it?"*. There is a second
|
|
@@ -192,7 +192,7 @@ function bibTextFrom(target) {
|
|
|
192
192
|
* Only three fields are needed (type, key, author, title, booktitle/journal), and a full
|
|
193
193
|
* BibTeX grammar would be a second thing to maintain. Brace-depth counting is enough and
|
|
194
194
|
* is exercised by the colocated test. */
|
|
195
|
-
function parseBib(text) {
|
|
195
|
+
export function parseBib(text) {
|
|
196
196
|
const out = [];
|
|
197
197
|
const re = /@(\w+)\s*\{\s*([^,\s]+)\s*,/g;
|
|
198
198
|
let m;
|
|
@@ -289,7 +289,7 @@ export function claimsPublished(e) {
|
|
|
289
289
|
|
|
290
290
|
/* ---------- DBLP ---------- */
|
|
291
291
|
|
|
292
|
-
async function dblpHits(title) {
|
|
292
|
+
export async function dblpHits(title) {
|
|
293
293
|
const url = `${DBLP}/?q=${encodeURIComponent(title)}&format=json&h=6`;
|
|
294
294
|
// Per-request timeout: a check that hangs is indistinguishable from a check that is dead,
|
|
295
295
|
// and this one runs 25+ requests. Measured 2026-08-24: a single unbounded query stalled the
|
|
@@ -335,23 +335,31 @@ export function compare(ourSurnames, theirSurnames) {
|
|
|
335
335
|
return { missing, extra, orderDiffers };
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
338
|
+
/**
|
|
339
|
+
* The comparison over parsed entries, the network behind `lookup` (DBLP by default). Returns the
|
|
340
|
+
* buckets main() prints and `paperlint build` records: `findings` (the author list differs from
|
|
341
|
+
* the published version's), `matched` (compared, equal), `skipped` (not applicable), `unchecked`
|
|
342
|
+
* (the lookup FAILED — never a pass). An entry in none of them was a preprint entry, which may
|
|
343
|
+
* carry preprint metadata.
|
|
344
|
+
*
|
|
345
|
+
* @param lookup title → DBLP-shaped hits; throws on a failed request (`retryable` for a 429)
|
|
346
|
+
* @param pause ms → a promise; DBLP asks for gentle clients. A test passes `() => {}`.
|
|
347
|
+
*/
|
|
348
|
+
export async function checkAuthors(
|
|
349
|
+
parsed,
|
|
350
|
+
{
|
|
351
|
+
lookup = dblpHits,
|
|
352
|
+
pause = (ms) =>
|
|
353
|
+
new Promise((r) => {
|
|
354
|
+
setTimeout(r, ms);
|
|
355
|
+
}),
|
|
356
|
+
} = {},
|
|
357
|
+
) {
|
|
352
358
|
const findings = [];
|
|
353
359
|
const skipped = []; // legitimately not applicable
|
|
354
360
|
const unchecked = []; // we FAILED to check — must never be reported as a pass
|
|
361
|
+
const matched = []; // compared against the published record, and equal
|
|
362
|
+
const entries = parsed.filter((e) => e.title && e.author);
|
|
355
363
|
|
|
356
364
|
for (const e of parsed.filter((x) => x.unparsed)) {
|
|
357
365
|
skipped.push({
|
|
@@ -377,7 +385,7 @@ async function main() {
|
|
|
377
385
|
let hits = null;
|
|
378
386
|
for (let attempt = 0; attempt < 3 && hits === null; attempt++) {
|
|
379
387
|
try {
|
|
380
|
-
hits = await
|
|
388
|
+
hits = await lookup(e.title.replace(/[{}]/g, ""));
|
|
381
389
|
} catch (err) {
|
|
382
390
|
const last = attempt === 2;
|
|
383
391
|
if (last)
|
|
@@ -385,10 +393,7 @@ async function main() {
|
|
|
385
393
|
key: e.key,
|
|
386
394
|
why: `DBLP lookup failed: ${err.message}`,
|
|
387
395
|
});
|
|
388
|
-
else
|
|
389
|
-
await new Promise((r) => {
|
|
390
|
-
setTimeout(r, 1500 * (attempt + 1));
|
|
391
|
-
});
|
|
396
|
+
else await pause(1500 * (attempt + 1));
|
|
392
397
|
}
|
|
393
398
|
}
|
|
394
399
|
if (hits === null) continue;
|
|
@@ -408,7 +413,9 @@ async function main() {
|
|
|
408
413
|
const ours = surnames(e.author);
|
|
409
414
|
const theirs = surnames(rec.authors.join(" and "));
|
|
410
415
|
const d = compare(ours, theirs);
|
|
411
|
-
if (d.missing.length || d.extra.length || d.orderDiffers)
|
|
416
|
+
if (!(d.missing.length || d.extra.length || d.orderDiffers))
|
|
417
|
+
matched.push(e.key);
|
|
418
|
+
else {
|
|
412
419
|
findings.push({
|
|
413
420
|
key: e.key,
|
|
414
421
|
venue: `${rec.venue} ${rec.year}`.trim(),
|
|
@@ -417,12 +424,27 @@ async function main() {
|
|
|
417
424
|
...d,
|
|
418
425
|
});
|
|
419
426
|
}
|
|
420
|
-
|
|
421
|
-
await new Promise((r) => {
|
|
422
|
-
setTimeout(r, 900);
|
|
423
|
-
}); // DBLP asks for gentle clients; 350 ms drew 429s
|
|
427
|
+
await pause(900); // DBLP asks for gentle clients; 350 ms drew 429s
|
|
424
428
|
}
|
|
425
429
|
|
|
430
|
+
return { findings, skipped, unchecked, matched };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function die(msg) {
|
|
434
|
+
console.error(`bib-authors: ${msg}`);
|
|
435
|
+
process.exit(2);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function main() {
|
|
439
|
+
const args = process.argv.slice(2).filter((a) => a !== "--json");
|
|
440
|
+
const asJson = process.argv.includes("--json");
|
|
441
|
+
if (!args[0])
|
|
442
|
+
die("usage: bib-authors.mjs <paper-dir|file.bib|file.tex> [--json]");
|
|
443
|
+
|
|
444
|
+
const { text, file, markdown } = bibTextFrom(args[0]);
|
|
445
|
+
const parsed = markdown ? parseMarkdownRefs(text) : parseBib(text);
|
|
446
|
+
const entries = parsed.filter((e) => e.title && e.author);
|
|
447
|
+
const { findings, skipped, unchecked } = await checkAuthors(parsed);
|
|
426
448
|
if (asJson) {
|
|
427
449
|
console.log(
|
|
428
450
|
JSON.stringify(
|
|
@@ -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
|
+
}
|
package/src/build-engine.ts
CHANGED
|
@@ -29,7 +29,11 @@ import {
|
|
|
29
29
|
usableTree,
|
|
30
30
|
type InstallResult,
|
|
31
31
|
} from "./toolchain.ts";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
declaredUnion,
|
|
34
|
+
mergeRequirements,
|
|
35
|
+
type TexRequirements,
|
|
36
|
+
} from "./tex-requirements.ts";
|
|
33
37
|
|
|
34
38
|
export interface EngineOptions {
|
|
35
39
|
/** What the targeted papers need: the base set plus each paper's venue. */
|
|
@@ -81,7 +85,11 @@ function withDefaults(o: EngineOptions): Resolved {
|
|
|
81
85
|
};
|
|
82
86
|
return {
|
|
83
87
|
...r,
|
|
84
|
-
|
|
88
|
+
// The shipped union AND what the targeted papers need: a project's own preset lives outside
|
|
89
|
+
// the package, and its packages are only in `r.tex`.
|
|
90
|
+
install:
|
|
91
|
+
o.install ??
|
|
92
|
+
(() => ensureTexLive(mergeRequirements(declaredUnion().tex, r.tex), r)),
|
|
85
93
|
};
|
|
86
94
|
}
|
|
87
95
|
|
package/src/build.harness.mjs
CHANGED
|
@@ -137,6 +137,24 @@ const fakeRead = async (pdf) => {
|
|
|
137
137
|
const CLEAN_TEX = "\\documentclass{article}\\begin{document}x\\end{document}";
|
|
138
138
|
|
|
139
139
|
try {
|
|
140
|
+
// ── inputs: paperlint's own venues directory, no configuration ───────────────────────────────
|
|
141
|
+
// First: the facts below resolve a paper's venue preset in this same directory, so a wrong one
|
|
142
|
+
// must be named HERE, not as a missing venue label two checks later.
|
|
143
|
+
const venues = packageVenuesDir();
|
|
144
|
+
check(
|
|
145
|
+
"the venues directory is paperlint's own and holds paper-guards.tex",
|
|
146
|
+
existsSync(join(venues, "paper-guards.tex")),
|
|
147
|
+
);
|
|
148
|
+
check(
|
|
149
|
+
"🔴 with no TEXINPUTS set, the value ENDS in the separator — otherwise the system tree stops resolving",
|
|
150
|
+
withTexInputs({}, [venues]).TEXINPUTS === `${venues}${delimiter}`,
|
|
151
|
+
);
|
|
152
|
+
check(
|
|
153
|
+
"an existing TEXINPUTS is kept, after ours",
|
|
154
|
+
withTexInputs({ TEXINPUTS: `/mine${delimiter}` }, [venues]).TEXINPUTS ===
|
|
155
|
+
`${venues}${delimiter}/mine${delimiter}`,
|
|
156
|
+
);
|
|
157
|
+
|
|
140
158
|
// ── facts: parsed from the paper, not configured ──────────────────────────────────────
|
|
141
159
|
check(
|
|
142
160
|
"documentclass and options come from the parser",
|
|
@@ -157,33 +175,20 @@ try {
|
|
|
157
175
|
);
|
|
158
176
|
const venued = paper("venued", {
|
|
159
177
|
"paper.tex": CLEAN_TEX,
|
|
160
|
-
"
|
|
178
|
+
"paperlint.json": JSON.stringify({
|
|
179
|
+
extends: "paperlint:agenticdev",
|
|
180
|
+
kind: "short",
|
|
181
|
+
}),
|
|
161
182
|
"build.sh": "exit 0\n",
|
|
162
183
|
});
|
|
163
184
|
const facts = readFacts(venued);
|
|
164
|
-
check("the venue comes from
|
|
185
|
+
check("the venue comes from paperlint.json", facts.venue === "agenticdev");
|
|
165
186
|
check(
|
|
166
187
|
"a leftover build.sh is a FACT (reported), not a step",
|
|
167
188
|
JSON.stringify(facts.ignoredScripts) === JSON.stringify(["build.sh"]) &&
|
|
168
189
|
IGNORED_SCRIPTS.includes("repro/build-submission.sh"),
|
|
169
190
|
);
|
|
170
191
|
|
|
171
|
-
// ── inputs: paperlint's own venues directory, no configuration ───────────────────────────────
|
|
172
|
-
const venues = packageVenuesDir();
|
|
173
|
-
check(
|
|
174
|
-
"the venues directory is paperlint's own and holds paper-guards.tex",
|
|
175
|
-
existsSync(join(venues, "paper-guards.tex")),
|
|
176
|
-
);
|
|
177
|
-
check(
|
|
178
|
-
"🔴 with no TEXINPUTS set, the value ENDS in the separator — otherwise the system tree stops resolving",
|
|
179
|
-
withTexInputs({}, [venues]).TEXINPUTS === `${venues}${delimiter}`,
|
|
180
|
-
);
|
|
181
|
-
check(
|
|
182
|
-
"an existing TEXINPUTS is kept, after ours",
|
|
183
|
-
withTexInputs({ TEXINPUTS: `/mine${delimiter}` }, [venues]).TEXINPUTS ===
|
|
184
|
-
`${venues}${delimiter}/mine${delimiter}`,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
192
|
// ── the plan ──────────────────────────────────────────────────────────────────────────
|
|
188
193
|
const plan = planFor(facts);
|
|
189
194
|
check(
|
|
@@ -268,10 +273,14 @@ try {
|
|
|
268
273
|
// ── the measure step: facts for the lint rules ────────────────────────────────────────
|
|
269
274
|
const factsFile = join(clean, "_build", "paper.facts.json");
|
|
270
275
|
check(
|
|
271
|
-
"measure: the plan's
|
|
272
|
-
r.plan.
|
|
273
|
-
/pdf\.js → _build\/paper\.facts\.json/.test(
|
|
274
|
-
|
|
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
|
+
),
|
|
275
284
|
);
|
|
276
285
|
check(
|
|
277
286
|
"🔴 measure: a green build wrote _build/paper.facts.json through the readPdf port, on paper.pdf",
|
package/src/build.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* ── THE SHAPE: an ordered list of STEPS, composed, not configured ────────────
|
|
14
14
|
* Each step says whether it applies to THIS paper and why — decided from FACTS parsed out of the
|
|
15
|
-
* paper (its `\documentclass` and options, the venue named in `
|
|
15
|
+
* paper (its `\documentclass` and options, the venue named in `paperlint.json`), never from a config
|
|
16
16
|
* flag. `paperlint build` prints that plan before running anything, and `--dry-run` prints only the
|
|
17
17
|
* plan. Adding a step is one entry in `STEPS`.
|
|
18
18
|
*
|
|
@@ -45,6 +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 { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
48
49
|
import {
|
|
49
50
|
declaredVenue,
|
|
50
51
|
factsPath,
|
|
@@ -63,6 +64,8 @@ import { whyNoGeometry } from "./domain/geometry.ts";
|
|
|
63
64
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
64
65
|
import type { Files } from "./ports/files.ts";
|
|
65
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";
|
|
66
69
|
import {
|
|
67
70
|
auxBib,
|
|
68
71
|
bibtexExcerpt,
|
|
@@ -86,7 +89,7 @@ export const PAPER_MARKERS = [
|
|
|
86
89
|
"PIPELINE-STATUS.md",
|
|
87
90
|
"paper.tex",
|
|
88
91
|
"paper.md",
|
|
89
|
-
|
|
92
|
+
CONFIG_FILE,
|
|
90
93
|
];
|
|
91
94
|
|
|
92
95
|
/** The source paperlint compiles, and the job name every output file carries. */
|
|
@@ -114,7 +117,7 @@ export interface PaperFacts {
|
|
|
114
117
|
readonly name: string;
|
|
115
118
|
readonly options: readonly string[];
|
|
116
119
|
} | null;
|
|
117
|
-
/** The
|
|
120
|
+
/** The label of the venue preset `paperlint.json` extends, or null. */
|
|
118
121
|
readonly venue: string | null;
|
|
119
122
|
/** Paper-supplied build scripts found on disk — reported as ignored. */
|
|
120
123
|
readonly ignoredScripts: readonly string[];
|
|
@@ -134,6 +137,8 @@ export interface BuildContext {
|
|
|
134
137
|
readonly measure: MeasureGeometry;
|
|
135
138
|
/** Where the facts file is written. */
|
|
136
139
|
readonly files: Files;
|
|
140
|
+
/** The online reference checks; the CLI wires the real ones, a test passes a function. */
|
|
141
|
+
readonly checkReferences: CheckReferences;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
export type StepOutcome =
|
|
@@ -222,7 +227,7 @@ export function readFacts(paperDir: string): PaperFacts {
|
|
|
222
227
|
return {
|
|
223
228
|
main,
|
|
224
229
|
documentclass: documentclassOf(ast),
|
|
225
|
-
venue: venue?.
|
|
230
|
+
venue: venue?.label ?? null,
|
|
226
231
|
ignoredScripts: IGNORED_SCRIPTS.filter((s) =>
|
|
227
232
|
existsSync(join(paperDir, s)),
|
|
228
233
|
),
|
|
@@ -520,11 +525,36 @@ export const measureStep: BuildStep = {
|
|
|
520
525
|
},
|
|
521
526
|
};
|
|
522
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
|
+
|
|
523
552
|
/** The build, in order. A new step is one entry here. */
|
|
524
553
|
export const STEPS: readonly BuildStep[] = [
|
|
525
554
|
inputsStep,
|
|
526
555
|
compileStep,
|
|
527
556
|
measureStep,
|
|
557
|
+
referencesStep,
|
|
528
558
|
];
|
|
529
559
|
|
|
530
560
|
// ── the command ─────────────────────────────────────────────────────────────────────────
|
|
@@ -573,6 +603,11 @@ export interface BuildOptions {
|
|
|
573
603
|
measure?: MeasureGeometry;
|
|
574
604
|
/** Where the facts file is written. Default: the disk. */
|
|
575
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;
|
|
576
611
|
}
|
|
577
612
|
|
|
578
613
|
/**
|
|
@@ -593,13 +628,21 @@ function baseDefaults({
|
|
|
593
628
|
dryRun = false,
|
|
594
629
|
readPdf = pdfjsReader,
|
|
595
630
|
projectRoot = env["CLAUDE_PROJECT_DIR"] || cwd,
|
|
596
|
-
}: BuildOptions): Required<
|
|
631
|
+
}: BuildOptions): Required<
|
|
632
|
+
Omit<BuildOptions, "measure" | "files" | "checkReferences">
|
|
633
|
+
> {
|
|
597
634
|
return { run, cwd, env, steps, log, dryRun, readPdf, projectRoot };
|
|
598
635
|
}
|
|
599
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
|
+
|
|
600
643
|
/** banal as the measurer, wired from the build's environment: the one piece of root work left here (#76). */
|
|
601
644
|
function defaultMeasurer(
|
|
602
|
-
b: Required<Omit<BuildOptions, "measure" | "files">>,
|
|
645
|
+
b: Required<Omit<BuildOptions, "measure" | "files" | "checkReferences">>,
|
|
603
646
|
): MeasureGeometry {
|
|
604
647
|
const dirs = hostDirs({ cwd: b.cwd });
|
|
605
648
|
return banalMeasurer(
|
|
@@ -613,7 +656,12 @@ function defaultMeasurer(
|
|
|
613
656
|
function withDefaults(o: BuildOptions): Required<BuildOptions> {
|
|
614
657
|
const base = baseDefaults(o);
|
|
615
658
|
const measure = o.measure ?? defaultMeasurer(base);
|
|
616
|
-
return {
|
|
659
|
+
return {
|
|
660
|
+
...base,
|
|
661
|
+
measure,
|
|
662
|
+
files: o.files ?? nodeFiles,
|
|
663
|
+
checkReferences: o.checkReferences ?? notWired,
|
|
664
|
+
};
|
|
617
665
|
}
|
|
618
666
|
|
|
619
667
|
/** Run the applicable steps in order, each on the environment the steps before it left. */
|
|
@@ -621,7 +669,15 @@ async function runSteps(
|
|
|
621
669
|
paperDir: string,
|
|
622
670
|
dir: string,
|
|
623
671
|
plan: PlanLine[],
|
|
624
|
-
{
|
|
672
|
+
{
|
|
673
|
+
run,
|
|
674
|
+
env,
|
|
675
|
+
steps,
|
|
676
|
+
readPdf,
|
|
677
|
+
measure,
|
|
678
|
+
files,
|
|
679
|
+
checkReferences,
|
|
680
|
+
}: Required<BuildOptions>,
|
|
625
681
|
): Promise<BuildResult> {
|
|
626
682
|
let stepEnv = env;
|
|
627
683
|
const notes: string[] = [];
|
|
@@ -634,6 +690,7 @@ async function runSteps(
|
|
|
634
690
|
readPdf,
|
|
635
691
|
measure,
|
|
636
692
|
files,
|
|
693
|
+
checkReferences,
|
|
637
694
|
});
|
|
638
695
|
if (!out.ok) {
|
|
639
696
|
// The PDF THIS run wrote and the step then rejected (a partial pass).
|