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
|
@@ -2,17 +2,39 @@
|
|
|
2
2
|
* The online reference checks, as the `CheckReferences` port: `verify-cites` (the cited work
|
|
3
3
|
* exists, the title matches — Crossref, OpenAlex, Semantic Scholar, arXiv) and `bib-authors` (the
|
|
4
4
|
* authors are the published version's — DBLP). Both ship in this package's verify-citations skill;
|
|
5
|
-
* this adapter only runs them
|
|
5
|
+
* this adapter only runs them, shapes their answers, and serves them from the paper's lookup cache.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 THE CACHE HOLDS ANSWERS, AND ONLY THE CHECKERS' OWN SEAMS TOUCH IT. verify-cites takes a
|
|
8
|
+
* `cache` object keyed per resolver × identifier and stores only successful responses; it is
|
|
9
|
+
* handed the paper's cached responses and whatever it adds is new. bib-authors takes a `lookup`
|
|
10
|
+
* (title → DBLP hits) and a `pause`; the lookup is wrapped to answer from the cache, and the pause
|
|
11
|
+
* is skipped after a cached answer — DBLP's 900 ms pace and its retries run only for titles never
|
|
12
|
+
* answered. Neither checker's logic changes. Verdicts are derived from the answers on every build
|
|
13
|
+
* (`domain/lookup-cache.ts` says why).
|
|
6
14
|
*
|
|
7
15
|
* "Not checked" is decided up front, by one request: when Crossref cannot be reached at all, the
|
|
8
16
|
* checkers would degrade every entry to `unresolvable` / `unchecked`, which reads like a result.
|
|
9
|
-
* Saying `not-checked` with the reason is the honest record.
|
|
17
|
+
* Saying `not-checked` with the reason is the honest record. That request is skipped when every
|
|
18
|
+
* question this bibliography would ask is already answered in the cache: a warm build is offline.
|
|
10
19
|
*/
|
|
11
20
|
import type {
|
|
12
21
|
CheckReferences,
|
|
13
22
|
EntryVerdict,
|
|
14
23
|
} from "../../ports/check-references.ts";
|
|
24
|
+
import {
|
|
25
|
+
freshPart,
|
|
26
|
+
type CachedDblp,
|
|
27
|
+
type CachedResponse,
|
|
28
|
+
type DblpHit,
|
|
29
|
+
type LookupCache,
|
|
30
|
+
} from "../../domain/lookup-cache.ts";
|
|
31
|
+
import { sha256Hex } from "../../domain/sha256.ts";
|
|
32
|
+
import { mapLimit } from "./pool.ts";
|
|
15
33
|
import { unreachable } from "./reach.io.ts";
|
|
34
|
+
import { sleep, todayUtc } from "./clock.io.ts";
|
|
35
|
+
|
|
36
|
+
/** Citations looked up at once (#107). */
|
|
37
|
+
const LOOKUPS_IN_FLIGHT = 6;
|
|
16
38
|
// @ts-expect-error — a skill script in .mjs, it has no types
|
|
17
39
|
import * as cites from "../../../skills/verify-citations/scripts/verify-cites.mjs";
|
|
18
40
|
// @ts-expect-error — a skill script in .mjs, it has no types
|
|
@@ -22,6 +44,8 @@ interface CiteResult {
|
|
|
22
44
|
readonly id: string;
|
|
23
45
|
readonly verdict: "true" | "false" | "unresolvable";
|
|
24
46
|
readonly reason?: string;
|
|
47
|
+
/** "<service>: <reason>" for each question a refusing service did not answer (#120). */
|
|
48
|
+
readonly refused?: readonly string[];
|
|
25
49
|
}
|
|
26
50
|
interface AuthorFinding {
|
|
27
51
|
readonly key: string;
|
|
@@ -60,6 +84,18 @@ const authorsOf = (key: string, a: AuthorBuckets): EntryVerdict["authors"] =>
|
|
|
60
84
|
? "match"
|
|
61
85
|
: "skipped";
|
|
62
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Why a work was not confirmed: verify-cites' reason, then — when some registry refused — which
|
|
89
|
+
* one, so "unresolvable" is not read as "every registry looked and found nothing". Nothing for a
|
|
90
|
+
* confirmed work: a refusal after a confirmation changed nothing.
|
|
91
|
+
*/
|
|
92
|
+
const unconfirmed = (
|
|
93
|
+
c: CiteResult | undefined,
|
|
94
|
+
): readonly (string | undefined)[] =>
|
|
95
|
+
c && c.verdict !== "true"
|
|
96
|
+
? [c.reason, ...(c.refused ?? []).map((r) => `not asked: ${r}`)]
|
|
97
|
+
: [];
|
|
98
|
+
|
|
63
99
|
/** One entry's verdict from the two checkers' answers. */
|
|
64
100
|
function entryVerdict(
|
|
65
101
|
key: string,
|
|
@@ -69,7 +105,7 @@ function entryVerdict(
|
|
|
69
105
|
const c = found.find((x) => x.id === key);
|
|
70
106
|
const mismatch = a.findings.find((f) => f.key === key);
|
|
71
107
|
const why = [
|
|
72
|
-
c
|
|
108
|
+
...unconfirmed(c),
|
|
73
109
|
mismatch ? describeAuthors(mismatch) : undefined,
|
|
74
110
|
a.unchecked.find((u) => u.key === key)?.why,
|
|
75
111
|
]
|
|
@@ -83,20 +119,168 @@ function entryVerdict(
|
|
|
83
119
|
};
|
|
84
120
|
}
|
|
85
121
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
interface BibAuthorsEntry {
|
|
123
|
+
readonly key: string;
|
|
124
|
+
readonly title: string;
|
|
125
|
+
readonly author: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Whether bib-authors will ask DBLP about this entry — the filter its `checkAuthors` applies. */
|
|
129
|
+
const asksDblp = (e: BibAuthorsEntry): boolean =>
|
|
130
|
+
Boolean(e.title && e.author) &&
|
|
131
|
+
(authors.claimsPublished(e) as boolean) &&
|
|
132
|
+
!(authors.truncated(e.author) as boolean);
|
|
133
|
+
|
|
134
|
+
/** Is every question this bibliography would ask already answered in `cache`? */
|
|
135
|
+
async function fullyCached(
|
|
136
|
+
citations: readonly object[],
|
|
137
|
+
entries: readonly BibAuthorsEntry[],
|
|
138
|
+
cache: LookupCache,
|
|
139
|
+
store: Readonly<Record<string, unknown>>,
|
|
140
|
+
): Promise<boolean> {
|
|
141
|
+
const asks = await Promise.all(
|
|
142
|
+
citations.map((c) => cites.wouldAsk(c, store) as Promise<boolean>),
|
|
143
|
+
);
|
|
144
|
+
return (
|
|
145
|
+
!asks.includes(true) &&
|
|
146
|
+
entries.filter(asksDblp).every((e) => cache.dblp.has(dblpTitleKey(e.title)))
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The key a DBLP answer is stored under in the lookup cache: the title's identity (verify-cites'
|
|
152
|
+
* `titleIdentity` — the same rule its own title keys use, so the two caches cannot drift), hashed.
|
|
153
|
+
* A re-cased or re-braced title keeps its answer; `C` and `C++` do not share one.
|
|
154
|
+
*/
|
|
155
|
+
export const dblpTitleKey = (title: string): string =>
|
|
156
|
+
sha256Hex(new TextEncoder().encode(cites.titleIdentity(title))).slice(0, 16);
|
|
157
|
+
|
|
158
|
+
export interface ReferencesCheckerOptions {
|
|
159
|
+
/** The day a new answer is stamped with, YYYY-MM-DD. */
|
|
160
|
+
readonly today: () => string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* bib-authors' `lookup` and `pause`, answering from the cached DBLP hits first.
|
|
165
|
+
*
|
|
166
|
+
* 🔴 DBLP IS ASKED UNTIL IT REFUSES ONCE, THEN NOT AT ALL FOR THE REST OF THE RUN (#120). A 429, an
|
|
167
|
+
* HTML page where JSON was asked, a timeout: bib-authors would retry each title three times with
|
|
168
|
+
* backoff, and move on to the next title to do it again — 51 titles × 3 × up to 15 s. After the
|
|
169
|
+
* first refusal every lookup fails at once with the reason, and no pause is waited, so each entry
|
|
170
|
+
* lands in bib-authors' `unchecked` bucket ("never a pass") for the price of ONE request.
|
|
171
|
+
*
|
|
172
|
+
* The 900 ms pace protects DBLP, so it is kept only after an answer DBLP actually gave: not after a
|
|
173
|
+
* cached answer, and not once DBLP is off.
|
|
174
|
+
*/
|
|
175
|
+
function cachedDblp(cache: LookupCache, today: () => string) {
|
|
176
|
+
const dblp = new Map<string, CachedDblp>(cache.dblp);
|
|
177
|
+
let lastWasCached = false;
|
|
178
|
+
let refused: string | null = null;
|
|
179
|
+
return {
|
|
180
|
+
dblp,
|
|
181
|
+
lookup: async (title: string): Promise<readonly DblpHit[]> => {
|
|
182
|
+
const hit = dblp.get(dblpTitleKey(title));
|
|
183
|
+
lastWasCached = hit !== undefined;
|
|
184
|
+
if (hit) return hit.hits;
|
|
185
|
+
if (refused !== null)
|
|
186
|
+
throw new Error(`DBLP refused earlier in this run (${refused})`);
|
|
187
|
+
try {
|
|
188
|
+
const hits = (await authors.dblpHits(title)) as DblpHit[];
|
|
189
|
+
dblp.set(dblpTitleKey(title), { fetched: today(), title, hits });
|
|
190
|
+
return hits;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
refused = (e as Error).message;
|
|
193
|
+
throw new Error(`DBLP refused earlier in this run (${refused})`);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
pause: (ms: number): Promise<void> =>
|
|
197
|
+
lastWasCached || refused !== null ? Promise.resolve() : sleep(ms),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The cache after a run: the old answers with every new one laid over them, dated today — or the
|
|
203
|
+
* very object passed in when nothing was fetched, so the caller can tell there is nothing to write.
|
|
204
|
+
* "New" is measured against `usable`, the part the run answered from: an answer too old to use was
|
|
205
|
+
* asked again, and its fresh copy replaces the old one. An old answer the run could not refresh
|
|
206
|
+
* (the service refused) stays, and is asked again next build.
|
|
207
|
+
*/
|
|
208
|
+
function grown(
|
|
209
|
+
loaded: { readonly cache: LookupCache; readonly usable: LookupCache },
|
|
210
|
+
run: {
|
|
211
|
+
readonly store: Readonly<Record<string, unknown>>;
|
|
212
|
+
readonly dblp: ReadonlyMap<string, CachedDblp>;
|
|
213
|
+
},
|
|
214
|
+
today: () => string,
|
|
215
|
+
): LookupCache {
|
|
216
|
+
const { cache, usable } = loaded;
|
|
217
|
+
const { store, dblp } = run;
|
|
218
|
+
const fresh = Object.entries(store).filter(([k]) => !usable.citations.has(k));
|
|
219
|
+
const freshDblp = [...dblp].filter(([k]) => !usable.dblp.has(k));
|
|
220
|
+
if (fresh.length === 0 && freshDblp.length === 0) return cache;
|
|
221
|
+
const dated = fresh.map(([k, response]): [string, CachedResponse] => [
|
|
222
|
+
k,
|
|
223
|
+
{ fetched: today(), response: response as CachedResponse["response"] },
|
|
99
224
|
]);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
225
|
+
return {
|
|
226
|
+
citations: new Map([...cache.citations, ...dated]),
|
|
227
|
+
dblp: new Map([...cache.dblp, ...freshDblp]),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The checker, with its clock injected; `onlineReferences` is the one the CLI wires. */
|
|
232
|
+
export const referencesChecker =
|
|
233
|
+
({ today }: ReferencesCheckerOptions): CheckReferences =>
|
|
234
|
+
async (bib, cache) => {
|
|
235
|
+
// Answers past MAX_AGE_DAYS are left out, so the run asks them again (and `grown` re-dates them).
|
|
236
|
+
const usable = freshPart(cache, today());
|
|
237
|
+
const citations = (cites.parseBib(bib) as { id?: string }[]).filter(
|
|
238
|
+
(c) => c.id,
|
|
239
|
+
);
|
|
240
|
+
const parsed = authors.parseBib(bib) as BibAuthorsEntry[];
|
|
241
|
+
const store: Record<string, unknown> = Object.fromEntries(
|
|
242
|
+
[...usable.citations].map(([k, v]) => [k, v.response]),
|
|
243
|
+
);
|
|
244
|
+
if (!(await fullyCached(citations, parsed, usable, store))) {
|
|
245
|
+
const why = await unreachable();
|
|
246
|
+
if (why !== null) return { check: { kind: "not-checked", why }, cache };
|
|
247
|
+
}
|
|
248
|
+
// 🔴 #107: the lookups run LOOKUPS_IN_FLIGHT at a time, and the DBLP author pass runs beside
|
|
249
|
+
// them. Each citation's own requests go to four different services one after another, so a
|
|
250
|
+
// pool of 6 is at most 6 requests to any one of them. The store is shared: two citations with
|
|
251
|
+
// the same identifier may both miss it and ask twice — an extra request, never a different
|
|
252
|
+
// answer.
|
|
253
|
+
// One breaker for the run: a service that refuses is not asked again, not once per citation
|
|
254
|
+
// (#120). Requests already in flight when it starts refusing still land — at most six.
|
|
255
|
+
const breaker = cites.createBreaker() as unknown;
|
|
256
|
+
const d = cachedDblp(usable, today);
|
|
257
|
+
const [found, a] = await Promise.all([
|
|
258
|
+
mapLimit(
|
|
259
|
+
citations,
|
|
260
|
+
LOOKUPS_IN_FLIGHT,
|
|
261
|
+
(c) =>
|
|
262
|
+
cites.verifyCitationLive(c, {
|
|
263
|
+
cache: store,
|
|
264
|
+
breaker,
|
|
265
|
+
}) as Promise<CiteResult>,
|
|
266
|
+
),
|
|
267
|
+
authors.checkAuthors(parsed, {
|
|
268
|
+
lookup: d.lookup,
|
|
269
|
+
pause: d.pause,
|
|
270
|
+
}) as Promise<AuthorBuckets>,
|
|
271
|
+
]);
|
|
272
|
+
const keys = new Set([
|
|
273
|
+
...found.map((c) => c.id),
|
|
274
|
+
...a.findings.map((f) => f.key),
|
|
275
|
+
]);
|
|
276
|
+
const entries = [...keys].map((key) => entryVerdict(key, found, a));
|
|
277
|
+
return {
|
|
278
|
+
check: { kind: "checked", entries },
|
|
279
|
+
cache: grown({ cache, usable }, { store, dblp: d.dblp }, today),
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** The checker the CLI wires: the real clock. */
|
|
284
|
+
export const onlineReferences: CheckReferences = referencesChecker({
|
|
285
|
+
today: todayUtc,
|
|
286
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** mapLimit: results in input order, never more than `limit` in flight, an empty list at once. */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { test } from "vitest";
|
|
4
|
+
import { mapLimit } from "./pool.ts";
|
|
5
|
+
|
|
6
|
+
test("results keep the input order although they finish out of order", async () => {
|
|
7
|
+
const out = await mapLimit([30, 5, 20, 1], 2, async (ms) => {
|
|
8
|
+
await new Promise((r) => {
|
|
9
|
+
setTimeout(r, ms);
|
|
10
|
+
});
|
|
11
|
+
return ms * 10;
|
|
12
|
+
});
|
|
13
|
+
assert.deepEqual(out, [300, 50, 200, 10]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("never more than `limit` calls in flight; fewer items than the limit start fewer workers", async () => {
|
|
17
|
+
let now = 0;
|
|
18
|
+
let peak = 0;
|
|
19
|
+
const track = async () => {
|
|
20
|
+
peak = Math.max(peak, ++now);
|
|
21
|
+
await new Promise((r) => {
|
|
22
|
+
setTimeout(r, 2);
|
|
23
|
+
});
|
|
24
|
+
now--;
|
|
25
|
+
};
|
|
26
|
+
await mapLimit(Array.from({ length: 10 }), 3, track);
|
|
27
|
+
const three = peak;
|
|
28
|
+
peak = 0;
|
|
29
|
+
await mapLimit([1, 2], 6, track);
|
|
30
|
+
assert.deepEqual([three, peak, await mapLimit([], 6, track)], [3, 2, []]);
|
|
31
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fn` over `items` with at most `limit` calls in flight, results in `items`' order — a promise
|
|
3
|
+
* pool, the one piece of concurrency this adapter needs, so it is written here rather than
|
|
4
|
+
* depended on. A rejection rejects the whole map, as `Promise.all` would: callers that must not
|
|
5
|
+
* lose the other results catch inside `fn`.
|
|
6
|
+
*/
|
|
7
|
+
export async function mapLimit<T, R>(
|
|
8
|
+
items: readonly T[],
|
|
9
|
+
limit: number,
|
|
10
|
+
fn: (item: T) => Promise<R>,
|
|
11
|
+
): Promise<R[]> {
|
|
12
|
+
const out: R[] = new Array<R>(items.length);
|
|
13
|
+
let next = 0;
|
|
14
|
+
const worker = async (): Promise<void> => {
|
|
15
|
+
while (next < items.length) {
|
|
16
|
+
const i = next++;
|
|
17
|
+
out[i] = await fn(items[i] as T);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
await Promise.all(
|
|
21
|
+
Array.from({ length: Math.min(limit, items.length) }, worker),
|
|
22
|
+
);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* 2. prepareEngine: use the cache · use the system · refuse without a human · ask, decline,
|
|
9
9
|
* accept and install · dry-run never asks or installs.
|
|
10
10
|
*/
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
11
|
import {
|
|
13
12
|
chmodSync,
|
|
14
13
|
mkdirSync,
|
|
@@ -20,15 +19,12 @@ import {
|
|
|
20
19
|
import { tmpdir } from "node:os";
|
|
21
20
|
import { basename, delimiter, dirname, join } from "node:path";
|
|
22
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { createChecker } from "../lib/check.mjs";
|
|
23
23
|
|
|
24
24
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
25
25
|
const E = await import(join(HERE, "build-engine.ts"));
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
const check = (label, cond, detail = "") => {
|
|
29
|
-
assert.ok(cond, `${label}${detail ? ` — ${detail}` : ""}`);
|
|
30
|
-
n++;
|
|
31
|
-
};
|
|
27
|
+
const check = createChecker();
|
|
32
28
|
|
|
33
29
|
// ── 1. texts ────────────────────────────────────────────────────────────────────────────
|
|
34
30
|
const noCache = {
|
|
@@ -311,4 +307,4 @@ const x_run = { found: {} };
|
|
|
311
307
|
}
|
|
312
308
|
|
|
313
309
|
rmSync(work, { recursive: true, force: true });
|
|
314
|
-
console.log(`build-engine: ${
|
|
310
|
+
console.log(`build-engine: ${check.count} checks passed`);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build-engine.ts arms the harness does not reach: a system pdflatex that lacks packages (said
|
|
3
|
+
* before asking), an install that fails, an install that leaves packages missing, a cache used from
|
|
4
|
+
* an environment with no PATH, and the refusal line for a decision that is neither refuse nor ask.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { test } from "vitest";
|
|
10
|
+
import { useTempDir } from "../test/support.mjs";
|
|
11
|
+
import { prepareEngine, refusal } from "./build-engine.ts";
|
|
12
|
+
|
|
13
|
+
const work = useTempDir("paperlint-build-engine-test-");
|
|
14
|
+
const TEX = { packages: { acmart: ["acmart.cls"] }, tools: {} };
|
|
15
|
+
/** A runnable pdflatex in `dir`. */
|
|
16
|
+
const texAt = (dir: string): string => {
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
writeFileSync(join(dir, "pdflatex"), "");
|
|
19
|
+
chmodSync(join(dir, "pdflatex"), 0o755);
|
|
20
|
+
return dir;
|
|
21
|
+
};
|
|
22
|
+
/** kpsewhich answers per bin directory. */
|
|
23
|
+
const fakeRun = (found: Record<string, string>) =>
|
|
24
|
+
((cmd: string) => ({
|
|
25
|
+
stdout: found[dirname(cmd)] ?? "",
|
|
26
|
+
status: 0,
|
|
27
|
+
})) as never;
|
|
28
|
+
|
|
29
|
+
async function go(o: Parameters<typeof prepareEngine>[0]) {
|
|
30
|
+
const out: string[] = [];
|
|
31
|
+
const err: string[] = [];
|
|
32
|
+
const r = await prepareEngine({
|
|
33
|
+
log: (l) => out.push(l),
|
|
34
|
+
err: (l) => err.push(l),
|
|
35
|
+
platform: "linux",
|
|
36
|
+
interactive: true,
|
|
37
|
+
ask: async () => "y",
|
|
38
|
+
...o,
|
|
39
|
+
});
|
|
40
|
+
return { r, out, err };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test("a system pdflatex lacking packages is named before asking; a failed install stops the run", async () => {
|
|
44
|
+
const sys = texAt(join(work, "sys", "bin"));
|
|
45
|
+
const x = await go({
|
|
46
|
+
tex: TEX,
|
|
47
|
+
env: { PAPERLINT_TEXLIVE_DIR: join(work, "empty-cache"), PATH: sys },
|
|
48
|
+
run: fakeRun({}),
|
|
49
|
+
install: () => ({ ok: false }),
|
|
50
|
+
});
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
[x.r, x.out],
|
|
53
|
+
[
|
|
54
|
+
{ ok: false, code: 1 },
|
|
55
|
+
[`${join(sys, "pdflatex")} is not used: it lacks acmart`],
|
|
56
|
+
],
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("an install that verifies but still lacks a package is refused, naming it", async () => {
|
|
61
|
+
const cache = join(work, "cache-lacking");
|
|
62
|
+
const x = await go({
|
|
63
|
+
tex: TEX,
|
|
64
|
+
env: { PAPERLINT_TEXLIVE_DIR: cache, PATH: join(work, "nothing") },
|
|
65
|
+
run: fakeRun({}),
|
|
66
|
+
install: () => {
|
|
67
|
+
texAt(join(cache, "2026", "bin", "x86_64-linux"));
|
|
68
|
+
return { ok: true, tree: { dir: "", year: "2026", bin: "" } };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
assert.deepEqual(x.r, { ok: false, code: 1 });
|
|
72
|
+
assert.match(x.err.join("\n"), /missing: 1 package\(s\): acmart/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("an install that leaves no tree at all is refused with nothing named missing", async () => {
|
|
76
|
+
const x = await go({
|
|
77
|
+
tex: TEX,
|
|
78
|
+
env: {
|
|
79
|
+
PAPERLINT_TEXLIVE_DIR: join(work, "cache-none"),
|
|
80
|
+
PATH: join(work, "nothing"),
|
|
81
|
+
},
|
|
82
|
+
run: fakeRun({}),
|
|
83
|
+
install: () => ({ ok: true, tree: { dir: "", year: "2026", bin: "" } }),
|
|
84
|
+
});
|
|
85
|
+
assert.deepEqual(x.r, { ok: false, code: 1 });
|
|
86
|
+
assert.match(x.err.join("\n"), /missing: 0 package\(s\): \)/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a complete cache is used from an environment with no PATH at all", async () => {
|
|
90
|
+
const bin = texAt(join(work, "cache-ok", "2026", "bin", "x86_64-linux"));
|
|
91
|
+
const x = await go({
|
|
92
|
+
tex: TEX,
|
|
93
|
+
env: { PAPERLINT_TEXLIVE_DIR: join(work, "cache-ok") },
|
|
94
|
+
run: fakeRun({ [bin]: "/t/acmart.cls\n" }),
|
|
95
|
+
});
|
|
96
|
+
assert.deepEqual(x.r, {
|
|
97
|
+
ok: true,
|
|
98
|
+
env: { PAPERLINT_TEXLIVE_DIR: join(work, "cache-ok"), PATH: `${bin}:` },
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("the refusal line for a decision to use a tree names nothing missing", () => {
|
|
103
|
+
assert.equal(
|
|
104
|
+
refusal({ kind: "use-cache" } as never, { system: null } as never),
|
|
105
|
+
"✗ paperlint build: no TeX Live with every package these papers need — run `npx paperlint toolchain` (missing: 0 package(s): )",
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("without an ask or an install given: Enter is yes, and the default install runs (here: every mirror fails)", async () => {
|
|
110
|
+
const cache = join(work, "cache-default");
|
|
111
|
+
const calls: string[] = [];
|
|
112
|
+
const out: string[] = [];
|
|
113
|
+
const err: string[] = [];
|
|
114
|
+
const r = await prepareEngine({
|
|
115
|
+
tex: TEX,
|
|
116
|
+
log: (l) => out.push(l),
|
|
117
|
+
err: (l) => err.push(l),
|
|
118
|
+
platform: "linux",
|
|
119
|
+
interactive: true,
|
|
120
|
+
env: {
|
|
121
|
+
PAPERLINT_TEXLIVE_DIR: cache,
|
|
122
|
+
PAPERLINT_CTAN_MIRROR: "https://mirror.invalid/tlnet",
|
|
123
|
+
PATH: join(work, "nothing"),
|
|
124
|
+
},
|
|
125
|
+
run: ((cmd: string) => {
|
|
126
|
+
calls.push(cmd.split("/").pop() ?? cmd);
|
|
127
|
+
return {
|
|
128
|
+
status: 22,
|
|
129
|
+
stdout: "",
|
|
130
|
+
stderr: "curl: (6) could not resolve\n",
|
|
131
|
+
};
|
|
132
|
+
}) as never,
|
|
133
|
+
});
|
|
134
|
+
assert.deepEqual(r, { ok: false, code: 1 });
|
|
135
|
+
assert.ok(calls.includes("curl"), calls.join(","));
|
|
136
|
+
assert.match(err.join("\n"), /no CTAN mirror returned install-tl/);
|
|
137
|
+
});
|
package/src/build.harness.mjs
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* `build.sh` is never executed (checked by the trace it would leave on disk and by the list of
|
|
11
11
|
* processes started), and a failed build removes the stale `paper.pdf` (checked on disk).
|
|
12
12
|
*/
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
13
|
import {
|
|
15
14
|
mkdtempSync,
|
|
16
15
|
mkdirSync,
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
import { tmpdir } from "node:os";
|
|
24
23
|
import { join, dirname, delimiter } from "node:path";
|
|
25
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { createChecker } from "../lib/check.mjs";
|
|
26
26
|
|
|
27
27
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
28
28
|
const {
|
|
@@ -44,11 +44,7 @@ const { packageVenuesDir } = await import(
|
|
|
44
44
|
join(HERE, "..", "skills", "paper-pipeline", "scripts", "consumer.mjs")
|
|
45
45
|
);
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
const check = (label, cond) => {
|
|
49
|
-
assert.ok(cond, label);
|
|
50
|
-
n++;
|
|
51
|
-
};
|
|
47
|
+
const check = createChecker();
|
|
52
48
|
|
|
53
49
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-build-")));
|
|
54
50
|
// 🔴 banal is looked for under the temp root, never in the developer's cache: with the real
|
|
@@ -511,8 +507,8 @@ try {
|
|
|
511
507
|
check("on full success there is no remedy", remedyFor([r]) === "");
|
|
512
508
|
|
|
513
509
|
// ── 🔴 A STALE PDF NEVER SURVIVES A RUN THAT DID NOT REPLACE IT — every path of buildPapers ──
|
|
514
|
-
// The order matters
|
|
515
|
-
// existence check by the empty
|
|
510
|
+
// The order matters here: the up-front removal is exercised by the no-engine case below, the
|
|
511
|
+
// existence check by the empty-document case, and neither depends on the other defence.
|
|
516
512
|
const REMOVED =
|
|
517
513
|
"paper.pdf removed — a stale PDF must not pass for this build";
|
|
518
514
|
|
|
@@ -680,5 +676,5 @@ try {
|
|
|
680
676
|
}
|
|
681
677
|
|
|
682
678
|
console.log(
|
|
683
|
-
`✓ ${String(
|
|
679
|
+
`✓ ${String(check.count)} assertions passed — build: paperlint compiles, build.sh never runs, a red build leaves no PDF`,
|
|
684
680
|
);
|