paperlint 3.1.0 → 3.1.2
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/.github/workflows/ci.yml +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +21 -105
- package/CONTRIBUTING.md +23 -25
- package/README.md +11 -7
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/clock.io.d.ts +5 -0
- package/dist/adapters/references/clock.io.d.ts.map +1 -0
- package/dist/adapters/references/clock.io.js +7 -0
- package/dist/adapters/references/clock.io.js.map +1 -0
- package/dist/adapters/references/index.d.ts +24 -2
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +131 -12
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build.d.ts +14 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +17 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -55
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/lookup-cache.d.ts +75 -0
- package/dist/domain/lookup-cache.d.ts.map +1 -0
- package/dist/domain/lookup-cache.js +137 -0
- package/dist/domain/lookup-cache.js.map +1 -0
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -4
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/ports/check-references.d.ts +12 -2
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/check-references.js +0 -5
- package/dist/ports/check-references.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts +11 -3
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +63 -15
- package/dist/references.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +3 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +4 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/prior-art/README.md +11 -10
- package/docs/prior-art/citation-checkers.md +174 -0
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/references.md +126 -0
- package/docs/testing.md +118 -0
- package/docs/toolchain.md +6 -4
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +9 -2
- package/eslint-rules/paper-typography.test.mjs +146 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/references/artifact-checklist.md +7 -7
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/checkers-requirements.txt +30 -19
- package/skills/render-paper/ensure-checkers.sh +20 -4
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +30 -14
- package/skills/verify-citations/SKILL.md.spec.ts +29 -13
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +253 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +6 -6
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +360 -215
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +588 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +151 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/clock.io.ts +8 -0
- package/src/adapters/references/index.test.ts +560 -0
- package/src/adapters/references/index.ts +203 -19
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +23 -12
- package/src/cli.harness.mjs +10 -17
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +112 -66
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/lookup-cache.test.ts +178 -0
- package/src/domain/lookup-cache.ts +202 -0
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +27 -7
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +3 -3
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/new-paper.harness.mjs +3 -7
- package/src/new-paper.test.ts +31 -1
- package/src/paper-files.test.ts +12 -0
- package/src/paper-files.ts +9 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/ports/check-references.ts +16 -2
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +257 -10
- package/src/references.ts +93 -17
- package/src/rules-config.harness.mjs +10 -8
- package/src/structure.harness.mjs +3 -7
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +3 -7
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +8 -7
- package/src/venue-rules.test.ts +60 -0
- package/src/venue-rules.ts +13 -11
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -65
- package/src/structure.mutations.mjs +0 -83
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* on 2026-08-29 (the midpoint cut, the review build, years mistaken for line numbers), ported from
|
|
7
7
|
* the harness of the function this replaces.
|
|
8
8
|
*/
|
|
9
|
-
import assert from "node:assert/strict";
|
|
10
9
|
import { dirname, join } from "node:path";
|
|
11
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { createChecker } from "../lib/check.mjs";
|
|
12
12
|
|
|
13
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const {
|
|
@@ -21,11 +21,7 @@ const {
|
|
|
21
21
|
STUB_WORDS,
|
|
22
22
|
} = await import(join(HERE, "pdf-geometry.ts"));
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
const check = (label, cond) => {
|
|
26
|
-
assert.ok(cond, label);
|
|
27
|
-
n++;
|
|
28
|
-
};
|
|
24
|
+
const check = createChecker();
|
|
29
25
|
|
|
30
26
|
const W = 612; // US letter, points
|
|
31
27
|
|
|
@@ -250,5 +246,5 @@ check(
|
|
|
250
246
|
);
|
|
251
247
|
|
|
252
248
|
console.log(
|
|
253
|
-
`✓ ${String(
|
|
249
|
+
`✓ ${String(check.count)} assertions passed — pdf-geometry: columns, stub/review, words from runs, fonts`,
|
|
254
250
|
);
|
package/src/pdf-geometry.ts
CHANGED
|
@@ -136,7 +136,7 @@ export function wordsOf(run: TextRun): Word[] {
|
|
|
136
136
|
const y0 = run.baseline - run.ascent * run.size;
|
|
137
137
|
const y1 = run.baseline - run.descent * run.size;
|
|
138
138
|
return [...run.text.matchAll(/\S+/g)].map((m) => {
|
|
139
|
-
const x0 = run.x +
|
|
139
|
+
const x0 = run.x + m.index * perChar;
|
|
140
140
|
return { x0, y0, x1: x0 + m[0].length * perChar, y1, text: m[0] };
|
|
141
141
|
});
|
|
142
142
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Crossref answer. The adapter (`adapters/references/`) runs the package's two checkers; a test
|
|
4
4
|
* passes a function.
|
|
5
5
|
*/
|
|
6
|
+
import type { LookupCache } from "../domain/lookup-cache.ts";
|
|
6
7
|
|
|
7
8
|
/** What one bibliography entry came to. */
|
|
8
9
|
export interface EntryVerdict {
|
|
@@ -24,5 +25,18 @@ export type ReferencesCheck =
|
|
|
24
25
|
/** Nothing could be asked — no network, every service down. Never a pass. */
|
|
25
26
|
| { readonly kind: "not-checked"; readonly why: string };
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
export
|
|
28
|
+
/** A check's verdicts, and the lookup cache grown by whatever it had to fetch. */
|
|
29
|
+
export interface ReferencesRun {
|
|
30
|
+
readonly check: ReferencesCheck;
|
|
31
|
+
/** The cache passed in, plus every new successful answer; the same object when nothing was fetched. */
|
|
32
|
+
readonly cache: LookupCache;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The bibliography's text (BibTeX) and the paper's lookup cache → the verdicts, derived afresh from
|
|
37
|
+
* cached and newly fetched answers. Never throws: a failure is `not-checked`.
|
|
38
|
+
*/
|
|
39
|
+
export type CheckReferences = (
|
|
40
|
+
bib: string,
|
|
41
|
+
cache: LookupCache,
|
|
42
|
+
) => Promise<ReferencesRun>;
|
package/src/presets.test.ts
CHANGED
|
@@ -230,6 +230,8 @@ describe("labelOf — display only, never used to resolve", () => {
|
|
|
230
230
|
["paperlint:agenticdev", "agenticdev"],
|
|
231
231
|
["./venues/usenix-sec.jsonc", "usenix-sec"],
|
|
232
232
|
["../shared/acl.json", "acl"],
|
|
233
|
+
["./.hidden", ".hidden"],
|
|
234
|
+
["paperlint:", ""],
|
|
233
235
|
])("%s → %s", (spec, label) => {
|
|
234
236
|
expect(labelOf(spec)).toBe(label);
|
|
235
237
|
});
|
package/src/presets.ts
CHANGED
|
@@ -293,5 +293,11 @@ export function paperPresetProblem(
|
|
|
293
293
|
if (p.kind === "preset-problem")
|
|
294
294
|
return `${join(paperDir, CONFIG_FILE)}: ${presetProblemText(p.problem)}`;
|
|
295
295
|
if (p.kind !== "settings-problem") return null;
|
|
296
|
-
return
|
|
296
|
+
return settingsProblemLine(paperDir, p.problem);
|
|
297
297
|
}
|
|
298
|
+
|
|
299
|
+
/** The one line naming a paper's `paperlint.json` that does not parse, and why. */
|
|
300
|
+
export const settingsProblemLine = (
|
|
301
|
+
paperDir: string,
|
|
302
|
+
problem: SettingsProblem,
|
|
303
|
+
): string => `${join(paperDir, CONFIG_FILE)}: ${problem.why}`;
|
package/src/references.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { describe, expect, it } from "vitest";
|
|
|
11
11
|
import { ESLint } from "eslint";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
14
|
+
import { ok } from "./domain/result.ts";
|
|
14
15
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
15
16
|
import type {
|
|
16
17
|
CheckReferences,
|
|
@@ -20,9 +21,16 @@ import { referencesStep } from "./build.ts";
|
|
|
20
21
|
import {
|
|
21
22
|
bibHash,
|
|
22
23
|
bibliographyOf,
|
|
24
|
+
lookupCachePath,
|
|
23
25
|
readReferences,
|
|
24
26
|
referencesPath,
|
|
25
27
|
} from "./references.ts";
|
|
28
|
+
import {
|
|
29
|
+
parseLookupCache,
|
|
30
|
+
serializeLookupCache,
|
|
31
|
+
type CachedResponse,
|
|
32
|
+
type LookupCache,
|
|
33
|
+
} from "./domain/lookup-cache.ts";
|
|
26
34
|
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.ts";
|
|
27
35
|
// @ts-expect-error — an ESLint language in .mjs, it has no types
|
|
28
36
|
import { texLanguage } from "../eslint-rules/latex-language.mjs";
|
|
@@ -55,22 +63,30 @@ const verdict = (
|
|
|
55
63
|
|
|
56
64
|
const checker =
|
|
57
65
|
(entries: readonly EntryVerdict[]): CheckReferences =>
|
|
58
|
-
async () => ({ kind: "checked", entries });
|
|
59
|
-
const offline: CheckReferences = async () => ({
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
async (_bib, cache) => ({ check: { kind: "checked", entries }, cache });
|
|
67
|
+
const offline: CheckReferences = async (_bib, cache) => ({
|
|
68
|
+
check: {
|
|
69
|
+
kind: "not-checked",
|
|
70
|
+
why: "the citation services cannot be reached (fetch failed)",
|
|
71
|
+
},
|
|
72
|
+
cache,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/** The step's context over `files`, with every port it does not use stubbed. */
|
|
76
|
+
const ctx = (files: ReturnType<typeof memoryFiles>) => ({
|
|
77
|
+
paperDir: PAPER,
|
|
78
|
+
env: {},
|
|
79
|
+
run: (() => ({})) as never,
|
|
80
|
+
readPdf: (() => null) as never,
|
|
81
|
+
measure: (() => null) as never,
|
|
82
|
+
files,
|
|
62
83
|
});
|
|
63
84
|
|
|
64
85
|
/** Run the build step on a paper held in memory; returns the files and the step's outcome. */
|
|
65
86
|
async function build(tex: string, check: CheckReferences) {
|
|
66
87
|
const files = memoryFiles({ [`${PAPER}/paper.tex`]: tex });
|
|
67
88
|
const out = await referencesStep.run({
|
|
68
|
-
|
|
69
|
-
env: {},
|
|
70
|
-
run: (() => ({})) as never,
|
|
71
|
-
readPdf: (() => null) as never,
|
|
72
|
-
measure: (() => null) as never,
|
|
73
|
-
files,
|
|
89
|
+
...ctx(files),
|
|
74
90
|
checkReferences: check,
|
|
75
91
|
});
|
|
76
92
|
return { files, out };
|
|
@@ -144,6 +160,108 @@ describe("the references build step", () => {
|
|
|
144
160
|
});
|
|
145
161
|
});
|
|
146
162
|
|
|
163
|
+
const ANSWER: CachedResponse = {
|
|
164
|
+
fetched: "2026-09-27",
|
|
165
|
+
response: { db: "crossref", transport: "ok", query: "doi" },
|
|
166
|
+
};
|
|
167
|
+
/** A checker that fetches `key` unless the cache has it, recording the cache it was handed. */
|
|
168
|
+
function fetching(key: string) {
|
|
169
|
+
const seen: LookupCache[] = [];
|
|
170
|
+
const check: CheckReferences = async (_bib, cache) => {
|
|
171
|
+
seen.push(cache);
|
|
172
|
+
return {
|
|
173
|
+
check: { kind: "checked", entries: [verdict("schick2023")] },
|
|
174
|
+
cache: cache.citations.has(key)
|
|
175
|
+
? cache
|
|
176
|
+
: {
|
|
177
|
+
citations: new Map([...cache.citations, [key, ANSWER]]),
|
|
178
|
+
dblp: cache.dblp,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
return { check, seen };
|
|
183
|
+
}
|
|
184
|
+
const cacheText = (files: ReturnType<typeof memoryFiles>): string | null => {
|
|
185
|
+
const b = files.readBytes(lookupCachePath(PAPER) as AbsolutePath);
|
|
186
|
+
return b === null ? null : new TextDecoder().decode(b);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
describe("the lookup cache — <paper>/repro/references-cache.json", () => {
|
|
190
|
+
it("a build that fetched writes the cache; the next build is handed it and writes nothing", async () => {
|
|
191
|
+
const f = fetching("crossref:doi:10.1/x");
|
|
192
|
+
const { files, out } = await build(TEX(ENTRIES), f.check);
|
|
193
|
+
expect(out).toEqual({
|
|
194
|
+
ok: true,
|
|
195
|
+
note: "references: 1 checked, 0 failing → _build/references.json; 1 new answer → repro/references-cache.json",
|
|
196
|
+
});
|
|
197
|
+
const written = cacheText(files);
|
|
198
|
+
expect(written).toBe(
|
|
199
|
+
serializeLookupCache({
|
|
200
|
+
citations: new Map([["crossref:doi:10.1/x", ANSWER]]),
|
|
201
|
+
dblp: new Map(),
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
const again = await referencesStep.run({
|
|
205
|
+
...ctx(files),
|
|
206
|
+
checkReferences: f.check,
|
|
207
|
+
});
|
|
208
|
+
expect(again).toEqual({
|
|
209
|
+
ok: true,
|
|
210
|
+
note: "references: 1 checked, 0 failing → _build/references.json; nothing fetched",
|
|
211
|
+
});
|
|
212
|
+
// The second build was handed exactly what the first one wrote.
|
|
213
|
+
expect(parseLookupCache(written!)).toEqual(ok(f.seen[1]));
|
|
214
|
+
expect(cacheText(files)).toBe(written);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("the lookup cache — counting and refusing", () => {
|
|
219
|
+
it("two new answers are counted as two", async () => {
|
|
220
|
+
const two: CheckReferences = async (_bib, cache) => ({
|
|
221
|
+
check: { kind: "checked", entries: [verdict("schick2023")] },
|
|
222
|
+
cache: {
|
|
223
|
+
citations: new Map([
|
|
224
|
+
["a", ANSWER],
|
|
225
|
+
["b", ANSWER],
|
|
226
|
+
]),
|
|
227
|
+
dblp: cache.dblp,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
const { out } = await build(TEX(ENTRIES), two);
|
|
231
|
+
expect(out).toEqual({
|
|
232
|
+
ok: true,
|
|
233
|
+
note: "references: 1 checked, 0 failing → _build/references.json; 2 new answers → repro/references-cache.json",
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("no answer fetched and no cache on disk: no cache file is created", async () => {
|
|
238
|
+
const { files } = await build(
|
|
239
|
+
TEX(ENTRIES),
|
|
240
|
+
checker([verdict("schick2023")]),
|
|
241
|
+
);
|
|
242
|
+
expect(cacheText(files)).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("🔴 a cache that does not parse is refused BY NAME and left alone — the check is not run over half a cache", async () => {
|
|
246
|
+
const files = memoryFiles({
|
|
247
|
+
[`${PAPER}/paper.tex`]: TEX(ENTRIES),
|
|
248
|
+
[lookupCachePath(PAPER)]: '{"schema": 7}',
|
|
249
|
+
});
|
|
250
|
+
const f = fetching("k");
|
|
251
|
+
const out = await referencesStep.run({
|
|
252
|
+
...ctx(files),
|
|
253
|
+
checkReferences: f.check,
|
|
254
|
+
});
|
|
255
|
+
expect(out).toEqual({
|
|
256
|
+
ok: true,
|
|
257
|
+
note: "references NOT checked — repro/references-cache.json cannot be read (schema 7: this paperlint reads schema 1) — fix it, or delete it to ask every question again; lint will say so",
|
|
258
|
+
});
|
|
259
|
+
expect(f.seen).toEqual([]);
|
|
260
|
+
expect(cacheText(files)).toBe('{"schema": 7}');
|
|
261
|
+
expect(readReferences(files, PAPER)?.status).toBe("not-checked");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
147
265
|
describe("the reference rules", () => {
|
|
148
266
|
it("one mismatched entry → paper/author-list on that entry's own line", async () => {
|
|
149
267
|
const tex = TEX(ENTRIES);
|
|
@@ -207,3 +325,132 @@ describe("the reference rules", () => {
|
|
|
207
325
|
expect(referencesPath(PAPER)).toBe(`${PAPER}/_build/references.json`);
|
|
208
326
|
});
|
|
209
327
|
});
|
|
328
|
+
|
|
329
|
+
describe("reading what is on disk", () => {
|
|
330
|
+
it("no paper.tex: the bibliography is refs.bib, from its first byte", () => {
|
|
331
|
+
const files = memoryFiles({ [`${PAPER}/refs.bib`]: ENTRIES });
|
|
332
|
+
expect(bibliographyOf(files, PAPER)).toEqual({
|
|
333
|
+
source: "refs.bib",
|
|
334
|
+
text: ENTRIES,
|
|
335
|
+
offset: 0,
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("a references.json of another schema, without entries, or not JSON reads as none", () => {
|
|
340
|
+
for (const body of [
|
|
341
|
+
JSON.stringify({ schema: 999, entries: [] }),
|
|
342
|
+
JSON.stringify({ schema: 1 }),
|
|
343
|
+
"null",
|
|
344
|
+
"{",
|
|
345
|
+
]) {
|
|
346
|
+
const files = memoryFiles({ [referencesPath(PAPER)]: body });
|
|
347
|
+
expect(readReferences(files, PAPER)).toBeNull();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
/** A record for `bib`, as the build would write it, with `patch` applied to the document. */
|
|
353
|
+
const record = (
|
|
354
|
+
files: ReturnType<typeof memoryFiles>,
|
|
355
|
+
body: Record<string, unknown>,
|
|
356
|
+
) => {
|
|
357
|
+
const bib = bibliographyOf(files, PAPER)!;
|
|
358
|
+
files.writeAtomic(
|
|
359
|
+
referencesPath(PAPER) as AbsolutePath,
|
|
360
|
+
new TextEncoder().encode(
|
|
361
|
+
JSON.stringify({
|
|
362
|
+
schema: 1,
|
|
363
|
+
bib: { source: bib.source, sha256: bibHash(bib) },
|
|
364
|
+
...body,
|
|
365
|
+
}),
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
describe("the reference rules over a record written by hand", () => {
|
|
371
|
+
it("not-checked with no reason recorded says so", async () => {
|
|
372
|
+
const tex = TEX(ENTRIES);
|
|
373
|
+
const files = memoryFiles({ [`${PAPER}/paper.tex`]: tex });
|
|
374
|
+
record(files, { status: "not-checked", entries: [] });
|
|
375
|
+
const msgs = await lint(files, tex);
|
|
376
|
+
expect(msgs.map((m) => m.ruleId)).toEqual(["paper/refs-checked"]);
|
|
377
|
+
expect(msgs[0]!.message).toMatch(/no reason recorded/);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("a verdict without a reason, and one for a key the bibliography lacks, still report", async () => {
|
|
381
|
+
const tex = TEX(ENTRIES);
|
|
382
|
+
const files = memoryFiles({ [`${PAPER}/paper.tex`]: tex });
|
|
383
|
+
record(files, {
|
|
384
|
+
status: "checked",
|
|
385
|
+
entries: [
|
|
386
|
+
{ key: "schick2023", exists: "true", authors: "mismatch" },
|
|
387
|
+
{ key: "ghost", exists: "false", authors: "match" },
|
|
388
|
+
],
|
|
389
|
+
});
|
|
390
|
+
const msgs = await lint(files, tex);
|
|
391
|
+
expect(msgs.map((m) => [m.ruleId, m.line])).toEqual([
|
|
392
|
+
["paper/cite-exists", 1],
|
|
393
|
+
["paper/author-list", 3],
|
|
394
|
+
]);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe("the reference rules over refs.bib, and on other files", () => {
|
|
399
|
+
it("an external refs.bib: findings sit at the start of paper.tex, naming the key", async () => {
|
|
400
|
+
const tex = "\\documentclass{acmart}\n\\begin{document}x\\end{document}\n";
|
|
401
|
+
const files = memoryFiles({
|
|
402
|
+
[`${PAPER}/paper.tex`]: tex,
|
|
403
|
+
[`${PAPER}/refs.bib`]: ENTRIES,
|
|
404
|
+
});
|
|
405
|
+
record(files, {
|
|
406
|
+
status: "checked",
|
|
407
|
+
entries: [verdict("schick2023", "mismatch")],
|
|
408
|
+
});
|
|
409
|
+
const msgs = await lint(files, tex);
|
|
410
|
+
expect(msgs.map((m) => [m.ruleId, m.line])).toEqual([
|
|
411
|
+
["paper/author-list", 1],
|
|
412
|
+
]);
|
|
413
|
+
expect(msgs[0]!.message).toMatch(/schick2023/);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("only paper.tex is judged", () => {
|
|
417
|
+
const files = memoryFiles({});
|
|
418
|
+
const rules = referenceRules({ files });
|
|
419
|
+
for (const rule of Object.values(rules))
|
|
420
|
+
expect(rule.create({ filename: `${PAPER}/notes.tex` } as never)).toEqual(
|
|
421
|
+
{},
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe("the lookup cache — refreshed answers", () => {
|
|
427
|
+
// Guards: an answer asked again after MAX_AGE_DAYS keeps its key, so the key count does not change.
|
|
428
|
+
// Counted by size, the refreshed answer was not written, and every later build asked it again.
|
|
429
|
+
it("an answer refreshed under the same key is counted and written", async () => {
|
|
430
|
+
const OLD: CachedResponse = { ...ANSWER, fetched: "2026-08-01" };
|
|
431
|
+
const put =
|
|
432
|
+
(answer: CachedResponse): CheckReferences =>
|
|
433
|
+
async (_bib, cache) => ({
|
|
434
|
+
check: { kind: "checked", entries: [verdict("schick2023")] },
|
|
435
|
+
cache: {
|
|
436
|
+
citations: new Map([...cache.citations, ["a", answer]]),
|
|
437
|
+
dblp: cache.dblp,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
const { files } = await build(TEX(ENTRIES), put(OLD));
|
|
441
|
+
const again = await referencesStep.run({
|
|
442
|
+
...ctx(files),
|
|
443
|
+
checkReferences: put(ANSWER),
|
|
444
|
+
});
|
|
445
|
+
expect({ again, file: cacheText(files) }).toEqual({
|
|
446
|
+
again: {
|
|
447
|
+
ok: true,
|
|
448
|
+
note: "references: 1 checked, 0 failing → _build/references.json; 1 new answer → repro/references-cache.json",
|
|
449
|
+
},
|
|
450
|
+
file: serializeLookupCache({
|
|
451
|
+
citations: new Map([["a", ANSWER]]),
|
|
452
|
+
dblp: new Map(),
|
|
453
|
+
}),
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
});
|
package/src/references.ts
CHANGED
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
* stale when it changes (`pdf_sha256`); these are about the bibliography and are stale when IT
|
|
18
18
|
* changes (`bib.sha256`). One staleness key per file, the way `pdf/fresh` already works.
|
|
19
19
|
*
|
|
20
|
+
* 🔴 WHAT THE CHECKERS ANSWERED IS KEPT, COMMITTED, IN `<paper>/repro/references-cache.json`
|
|
21
|
+
* (`domain/lookup-cache.ts`). Loaded before the check and saved after it — only when something new
|
|
22
|
+
* was fetched, a refreshed answer included — so a build asks each question at most once every
|
|
23
|
+
* MAX_AGE_DAYS, not once per build (#107: 217 s for 27 references, every build). The verdicts above are NOT cached: they are derived from the cached
|
|
24
|
+
* answers on every build, so a fix to the checkers reaches every paper at once.
|
|
25
|
+
*
|
|
20
26
|
* 🔴 THE STEP NEVER FAILS THE BUILD. A build without network still builds the PDF; the reference
|
|
21
27
|
* step records `not-checked` and the lint rule says so, as a warning. Recording "not checked" as
|
|
22
28
|
* a pass would be the counter that counts what it never looked at.
|
|
@@ -25,10 +31,18 @@ import { join } from "node:path";
|
|
|
25
31
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
26
32
|
import { sha256Hex } from "./domain/sha256.ts";
|
|
27
33
|
import type { Files } from "./ports/files.ts";
|
|
34
|
+
import {
|
|
35
|
+
EMPTY_LOOKUP_CACHE,
|
|
36
|
+
parseLookupCache,
|
|
37
|
+
serializeLookupCache,
|
|
38
|
+
type LookupCache,
|
|
39
|
+
} from "./domain/lookup-cache.ts";
|
|
40
|
+
import { ok, type Result } from "./domain/result.ts";
|
|
28
41
|
import type {
|
|
29
42
|
CheckReferences,
|
|
30
43
|
EntryVerdict,
|
|
31
44
|
ReferencesCheck,
|
|
45
|
+
ReferencesRun,
|
|
32
46
|
} from "./ports/check-references.ts";
|
|
33
47
|
// @ts-expect-error — an ESLint rule module in .mjs, it has no types
|
|
34
48
|
import { bibRange } from "../eslint-rules/paper-typography.mjs";
|
|
@@ -36,6 +50,12 @@ import { bibRange } from "../eslint-rules/paper-typography.mjs";
|
|
|
36
50
|
export const REFERENCES_SCHEMA = 1;
|
|
37
51
|
export const REFERENCES_FILE = "references.json";
|
|
38
52
|
|
|
53
|
+
/** The lookup cache's place in a paper: committed, beside the paper's other reproduction files. */
|
|
54
|
+
export const LOOKUP_CACHE_FILE = "repro/references-cache.json";
|
|
55
|
+
|
|
56
|
+
export const lookupCachePath = (paperDir: string): string =>
|
|
57
|
+
join(paperDir, LOOKUP_CACHE_FILE);
|
|
58
|
+
|
|
39
59
|
/** Where a paper's reference verdicts live — beside `paper.facts.json`. */
|
|
40
60
|
export const referencesPath = (paperDir: string): string =>
|
|
41
61
|
join(paperDir, "_build", REFERENCES_FILE);
|
|
@@ -106,10 +126,48 @@ export function readReferences(
|
|
|
106
126
|
}
|
|
107
127
|
}
|
|
108
128
|
|
|
129
|
+
/** The paper's lookup cache: empty when there is none, or why the file on disk cannot be read. */
|
|
130
|
+
export function readLookupCache(
|
|
131
|
+
files: Files,
|
|
132
|
+
paperDir: string,
|
|
133
|
+
): Result<LookupCache, string> {
|
|
134
|
+
const raw = text(files, lookupCachePath(paperDir));
|
|
135
|
+
return raw === null ? ok(EMPTY_LOOKUP_CACHE) : parseLookupCache(raw);
|
|
136
|
+
}
|
|
137
|
+
|
|
109
138
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
139
|
+
* How many answers the run fetched: entries of `after` that are not the very object `before` held
|
|
140
|
+
* under that key. The checker keeps every answer it did not fetch as the same object, so this counts
|
|
141
|
+
* both new keys and answers asked again past MAX_AGE_DAYS under an existing key — which a count of
|
|
142
|
+
* keys would miss, leaving the refreshed answer unwritten and asked again on every build.
|
|
143
|
+
*/
|
|
144
|
+
const newAnswers = (before: LookupCache, after: LookupCache): number => {
|
|
145
|
+
const changed = <V>(b: ReadonlyMap<string, V>, a: ReadonlyMap<string, V>) =>
|
|
146
|
+
[...a].filter(([k, v]) => b.get(k) !== v).length;
|
|
147
|
+
return (
|
|
148
|
+
changed(before.citations, after.citations) +
|
|
149
|
+
changed(before.dblp, after.dblp)
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Run the checker; a throw is `not-checked`, with the cache as it was. */
|
|
154
|
+
async function run(
|
|
155
|
+
check: CheckReferences,
|
|
156
|
+
bib: string,
|
|
157
|
+
cache: LookupCache,
|
|
158
|
+
): Promise<ReferencesRun> {
|
|
159
|
+
try {
|
|
160
|
+
return await check(bib, cache);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { check: { kind: "not-checked", why: (e as Error).message }, cache };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The build step's work: check the bibliography, serving what it can from the paper's lookup
|
|
168
|
+
* cache, and record the verdicts. Returns the one-line note the build prints. A checker that
|
|
169
|
+
* throws, or a cache that does not parse, is recorded as `not-checked` — the step reports, it
|
|
170
|
+
* never refuses. A cache that does not parse is left as it is, and named.
|
|
113
171
|
*/
|
|
114
172
|
export async function recordReferences(
|
|
115
173
|
files: Files,
|
|
@@ -118,21 +176,39 @@ export async function recordReferences(
|
|
|
118
176
|
): Promise<string> {
|
|
119
177
|
const bib = bibliographyOf(files, paperDir);
|
|
120
178
|
if (bib === null) return "no bibliography — nothing to check";
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
179
|
+
const record = (c: ReferencesCheck): ReferencesDocument => {
|
|
180
|
+
const doc = documentOf(bib, c);
|
|
181
|
+
files.writeAtomic(
|
|
182
|
+
at(referencesPath(paperDir)),
|
|
183
|
+
new TextEncoder().encode(`${JSON.stringify(doc, null, 2)}\n`),
|
|
184
|
+
);
|
|
185
|
+
return doc;
|
|
186
|
+
};
|
|
187
|
+
const notChecked = (why: string): string => {
|
|
188
|
+
record({ kind: "not-checked", why });
|
|
189
|
+
return `references NOT checked — ${why}; lint will say so`;
|
|
190
|
+
};
|
|
191
|
+
const cache = readLookupCache(files, paperDir);
|
|
192
|
+
if (!cache.ok)
|
|
193
|
+
return notChecked(
|
|
194
|
+
`${LOOKUP_CACHE_FILE} cannot be read (${cache.error}) — fix it, or delete it to ask every question again`,
|
|
195
|
+
);
|
|
196
|
+
const result = await run(check, bib.text, cache.value);
|
|
197
|
+
if (result.check.kind === "not-checked") return notChecked(result.check.why);
|
|
198
|
+
const doc = record(result.check);
|
|
199
|
+
const fetched = newAnswers(cache.value, result.cache);
|
|
200
|
+
if (fetched > 0)
|
|
201
|
+
files.writeAtomic(
|
|
202
|
+
at(lookupCachePath(paperDir)),
|
|
203
|
+
new TextEncoder().encode(serializeLookupCache(result.cache)),
|
|
204
|
+
);
|
|
134
205
|
const bad = doc.entries.filter(
|
|
135
206
|
(e) => e.exists === "false" || e.authors === "mismatch",
|
|
136
207
|
).length;
|
|
137
|
-
return
|
|
208
|
+
return (
|
|
209
|
+
`references: ${String(doc.entries.length)} checked, ${String(bad)} failing → _build/${REFERENCES_FILE}; ` +
|
|
210
|
+
(fetched > 0
|
|
211
|
+
? `${String(fetched)} new answer${fetched === 1 ? "" : "s"} → ${LOOKUP_CACHE_FILE}`
|
|
212
|
+
: "nothing fetched")
|
|
213
|
+
);
|
|
138
214
|
}
|
|
@@ -3,20 +3,16 @@
|
|
|
3
3
|
* paperlint ships. The end-to-end path (a project's paperlint.json → `paperlint lint`) is in `cli.harness.mjs`;
|
|
4
4
|
* this pins the parser's output shape and each refusal.
|
|
5
5
|
*/
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
6
|
import { dirname, join } from "node:path";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createChecker } from "../lib/check.mjs";
|
|
9
9
|
|
|
10
10
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const { parseRuleBlocks, shippedRuleIds, unknownKeys } = await import(
|
|
12
12
|
join(HERE, "rules-config.ts")
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
const check = (label, cond, detail = "") => {
|
|
17
|
-
assert.ok(cond, detail ? `${label} — ${detail}` : label);
|
|
18
|
-
n++;
|
|
19
|
-
};
|
|
15
|
+
const check = createChecker();
|
|
20
16
|
|
|
21
17
|
const SHIPPED = new Set(["pdf/last-page-balance", "paper/typography"]);
|
|
22
18
|
const parse = (raw) => parseRuleBlocks(raw, "S", SHIPPED, "/consumer");
|
|
@@ -112,7 +108,13 @@ check(
|
|
|
112
108
|
markdown: foreign,
|
|
113
109
|
},
|
|
114
110
|
},
|
|
115
|
-
|
|
111
|
+
// A plugin that carries only a language (the `tex` plugin does) has no rules to ship.
|
|
112
|
+
{
|
|
113
|
+
plugins: {
|
|
114
|
+
paper: { rules: { typography: {} } },
|
|
115
|
+
tex: { languages: {} },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
116
118
|
{ files: ["x"] },
|
|
117
119
|
],
|
|
118
120
|
[foreign],
|
|
@@ -126,5 +128,5 @@ check(
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
console.log(
|
|
129
|
-
`✓ ${String(
|
|
131
|
+
`✓ ${String(check.count)} assertions passed — rules-config: blocks parsed with basePath, every refusal names its key`,
|
|
130
132
|
);
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* So every "found" here has a paired "not found on the neighboring directory", and the defaults
|
|
7
7
|
* are separately checked against the shape of the live corpus.
|
|
8
8
|
*/
|
|
9
|
-
import assert from "node:assert/strict";
|
|
10
9
|
import {
|
|
11
10
|
mkdtempSync,
|
|
12
11
|
mkdirSync,
|
|
@@ -17,16 +16,13 @@ import {
|
|
|
17
16
|
import { tmpdir } from "node:os";
|
|
18
17
|
import { join, dirname } from "node:path";
|
|
19
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { createChecker } from "../lib/check.mjs";
|
|
20
20
|
|
|
21
21
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
22
22
|
const { checkStructure, formatStructure, asEslintResults, STRUCTURE_DEFAULTS } =
|
|
23
23
|
await import(join(HERE, "structure.ts"));
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
const check = (label, cond) => {
|
|
27
|
-
assert.ok(cond, label);
|
|
28
|
-
n++;
|
|
29
|
-
};
|
|
25
|
+
const check = createChecker();
|
|
30
26
|
|
|
31
27
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-struct-")));
|
|
32
28
|
const papers = join(root, "papers");
|
|
@@ -177,5 +173,5 @@ try {
|
|
|
177
173
|
}
|
|
178
174
|
|
|
179
175
|
console.log(
|
|
180
|
-
`✓ ${String(
|
|
176
|
+
`✓ ${String(check.count)} assertions passed — structure: a missing file cannot complain for itself`,
|
|
181
177
|
);
|