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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The references a build checked, end to end without a network: the `references` build step
|
|
3
|
+
* records what a (fake) checker answers, and the offline rules judge that record.
|
|
4
|
+
*
|
|
5
|
+
* a pass is recorded, with the hash of the bibliography checked
|
|
6
|
+
* one mismatched entry → paper/author-list on THAT entry's line
|
|
7
|
+
* the bibliography edited after the build → paper/refs-fresh, and the per-entry rules go quiet
|
|
8
|
+
* offline → the step still succeeds (the build is not failed) and paper/refs-checked warns
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { ESLint } from "eslint";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
14
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
15
|
+
import type {
|
|
16
|
+
CheckReferences,
|
|
17
|
+
EntryVerdict,
|
|
18
|
+
} from "./ports/check-references.ts";
|
|
19
|
+
import { referencesStep } from "./build.ts";
|
|
20
|
+
import {
|
|
21
|
+
bibHash,
|
|
22
|
+
bibliographyOf,
|
|
23
|
+
readReferences,
|
|
24
|
+
referencesPath,
|
|
25
|
+
} from "./references.ts";
|
|
26
|
+
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.ts";
|
|
27
|
+
// @ts-expect-error — an ESLint language in .mjs, it has no types
|
|
28
|
+
import { texLanguage } from "../eslint-rules/latex-language.mjs";
|
|
29
|
+
|
|
30
|
+
const PAPER = "/work/papers/p";
|
|
31
|
+
const TEX = (entries: string) =>
|
|
32
|
+
[
|
|
33
|
+
"\\documentclass{acmart}",
|
|
34
|
+
"\\begin{filecontents*}{refs.bib}",
|
|
35
|
+
entries,
|
|
36
|
+
"\\end{filecontents*}",
|
|
37
|
+
"\\begin{document}",
|
|
38
|
+
"x",
|
|
39
|
+
"\\end{document}",
|
|
40
|
+
"",
|
|
41
|
+
].join("\n");
|
|
42
|
+
const ENTRIES =
|
|
43
|
+
"@inproceedings{schick2023,\n title = {Toolformer},\n author = {Schick, Timo},\n booktitle = {NeurIPS}\n}\n@misc{other,\n title = {Other},\n url = {https://x.org}\n}";
|
|
44
|
+
|
|
45
|
+
const verdict = (
|
|
46
|
+
key: string,
|
|
47
|
+
authors: EntryVerdict["authors"] = "match",
|
|
48
|
+
exists: EntryVerdict["exists"] = "true",
|
|
49
|
+
): EntryVerdict => ({
|
|
50
|
+
key,
|
|
51
|
+
exists,
|
|
52
|
+
authors,
|
|
53
|
+
...(authors === "mismatch" ? { why: "missing hambro" } : {}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const checker =
|
|
57
|
+
(entries: readonly EntryVerdict[]): CheckReferences =>
|
|
58
|
+
async () => ({ kind: "checked", entries });
|
|
59
|
+
const offline: CheckReferences = async () => ({
|
|
60
|
+
kind: "not-checked",
|
|
61
|
+
why: "the citation services cannot be reached (fetch failed)",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** Run the build step on a paper held in memory; returns the files and the step's outcome. */
|
|
65
|
+
async function build(tex: string, check: CheckReferences) {
|
|
66
|
+
const files = memoryFiles({ [`${PAPER}/paper.tex`]: tex });
|
|
67
|
+
const out = await referencesStep.run({
|
|
68
|
+
paperDir: PAPER,
|
|
69
|
+
env: {},
|
|
70
|
+
run: (() => ({})) as never,
|
|
71
|
+
readPdf: (() => null) as never,
|
|
72
|
+
measure: (() => null) as never,
|
|
73
|
+
files,
|
|
74
|
+
checkReferences: check,
|
|
75
|
+
});
|
|
76
|
+
return { files, out };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The rules as an ESLint plugin. Their context type is the narrow one they read (a custom
|
|
81
|
+
* language's SourceCode), which ESLint's JS-centred plugin type does not describe.
|
|
82
|
+
*/
|
|
83
|
+
const asPlugin = (rules: object): ESLint.Plugin => ({ rules }) as ESLint.Plugin;
|
|
84
|
+
|
|
85
|
+
/** Lint `tex` as the paper's paper.tex against what `files` recorded. */
|
|
86
|
+
async function lint(files: ReturnType<typeof memoryFiles>, tex: string) {
|
|
87
|
+
files.writeAtomic(
|
|
88
|
+
`${PAPER}/paper.tex` as AbsolutePath,
|
|
89
|
+
new TextEncoder().encode(tex),
|
|
90
|
+
);
|
|
91
|
+
const eslint = new ESLint({
|
|
92
|
+
cwd: "/work",
|
|
93
|
+
overrideConfigFile: true,
|
|
94
|
+
overrideConfig: [
|
|
95
|
+
{
|
|
96
|
+
files: ["**/paper.tex"],
|
|
97
|
+
plugins: {
|
|
98
|
+
tex: { languages: { latex: texLanguage } },
|
|
99
|
+
paper: asPlugin(referenceRules({ files })),
|
|
100
|
+
},
|
|
101
|
+
language: "tex/latex",
|
|
102
|
+
rules: REFERENCE_RULE_LEVELS,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
const [res] = await eslint.lintText(tex, {
|
|
107
|
+
filePath: join(PAPER, "paper.tex"),
|
|
108
|
+
});
|
|
109
|
+
return res!.messages;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe("the references build step", () => {
|
|
113
|
+
it("records a pass, with the hash of the bibliography it checked", async () => {
|
|
114
|
+
const tex = TEX(ENTRIES);
|
|
115
|
+
const { files, out } = await build(
|
|
116
|
+
tex,
|
|
117
|
+
checker([verdict("schick2023"), verdict("other")]),
|
|
118
|
+
);
|
|
119
|
+
expect(out.ok).toBe(true);
|
|
120
|
+
const doc = readReferences(files, PAPER);
|
|
121
|
+
expect(doc?.status).toBe("checked");
|
|
122
|
+
expect(doc?.bib.sha256).toBe(bibHash(bibliographyOf(files, PAPER)!));
|
|
123
|
+
expect(await lint(files, tex)).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("🔴 offline: the step succeeds — the build is not failed — and lint warns once", async () => {
|
|
127
|
+
const tex = TEX(ENTRIES);
|
|
128
|
+
const { files, out } = await build(tex, offline);
|
|
129
|
+
expect(out.ok).toBe(true);
|
|
130
|
+
expect(readReferences(files, PAPER)?.status).toBe("not-checked");
|
|
131
|
+
const msgs = await lint(files, tex);
|
|
132
|
+
expect(msgs.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
133
|
+
["paper/refs-checked", 1],
|
|
134
|
+
]);
|
|
135
|
+
expect(msgs[0]!.message).toMatch(/cannot be reached/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("a checker that throws is recorded as not checked, not as a failed build", async () => {
|
|
139
|
+
const { files, out } = await build(TEX(ENTRIES), async () => {
|
|
140
|
+
throw new Error("DBLP exploded");
|
|
141
|
+
});
|
|
142
|
+
expect(out.ok).toBe(true);
|
|
143
|
+
expect(readReferences(files, PAPER)?.why).toMatch(/DBLP exploded/);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("the reference rules", () => {
|
|
148
|
+
it("one mismatched entry → paper/author-list on that entry's own line", async () => {
|
|
149
|
+
const tex = TEX(ENTRIES);
|
|
150
|
+
const { files } = await build(
|
|
151
|
+
tex,
|
|
152
|
+
checker([verdict("schick2023", "mismatch"), verdict("other")]),
|
|
153
|
+
);
|
|
154
|
+
const msgs = await lint(files, tex);
|
|
155
|
+
expect(msgs.map((m) => m.ruleId)).toEqual(["paper/author-list"]);
|
|
156
|
+
expect(msgs[0]!.line).toBe(3);
|
|
157
|
+
expect(msgs[0]!.message).toMatch(/`schick2023`.*missing hambro/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("an identifier that provably fails → paper/cite-exists on that entry", async () => {
|
|
161
|
+
const tex = TEX(ENTRIES);
|
|
162
|
+
const { files } = await build(
|
|
163
|
+
tex,
|
|
164
|
+
checker([verdict("schick2023"), verdict("other", "skipped", "false")]),
|
|
165
|
+
);
|
|
166
|
+
const msgs = await lint(files, tex);
|
|
167
|
+
expect(msgs.map((m) => [m.ruleId, m.line])).toEqual([
|
|
168
|
+
["paper/cite-exists", 8],
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("🔴 the bibliography edited after the build → refs-fresh, and the stale verdicts are not judged", async () => {
|
|
173
|
+
const { files } = await build(
|
|
174
|
+
TEX(ENTRIES),
|
|
175
|
+
checker([verdict("schick2023", "mismatch"), verdict("other")]),
|
|
176
|
+
);
|
|
177
|
+
const edited = TEX(
|
|
178
|
+
ENTRIES.replace("Schick, Timo", "Schick, Timo and Hambro, Eric"),
|
|
179
|
+
);
|
|
180
|
+
const msgs = await lint(files, edited);
|
|
181
|
+
expect(msgs.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
182
|
+
["paper/refs-fresh", 2],
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("the same bibliography, unchanged → silent", async () => {
|
|
187
|
+
const tex = TEX(ENTRIES);
|
|
188
|
+
const { files } = await build(tex, checker([verdict("schick2023")]));
|
|
189
|
+
expect(await lint(files, tex)).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("never built → one warning naming the command", async () => {
|
|
193
|
+
const files = memoryFiles({});
|
|
194
|
+
const msgs = await lint(files, TEX(ENTRIES));
|
|
195
|
+
expect(msgs.map((m) => m.ruleId)).toEqual(["paper/refs-checked"]);
|
|
196
|
+
expect(msgs[0]!.message).toMatch(/npx paperlint build/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("a paper with no bibliography gets nothing", async () => {
|
|
200
|
+
const files = memoryFiles({});
|
|
201
|
+
expect(
|
|
202
|
+
await lint(
|
|
203
|
+
files,
|
|
204
|
+
"\\documentclass{article}\n\\begin{document}x\\end{document}\n",
|
|
205
|
+
),
|
|
206
|
+
).toEqual([]);
|
|
207
|
+
expect(referencesPath(PAPER)).toBe(`${PAPER}/_build/references.json`);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE REFERENCES A BUILD CHECKED — `<paper>/_build/references.json`, written by `paperlint build`
|
|
3
|
+
* and read, offline, by the lint rules in `reference-rules.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The package ships two online checks: `verify-cites` (the cited work exists and its title
|
|
6
|
+
* matches) and `bib-authors` (the authors are those of the version cited, not the preprint's).
|
|
7
|
+
* Until 3.0.0 lint could only ask whether someone had run them — by finding the word
|
|
8
|
+
* "bib-authors" in a scorecard table, or a command the project configured. Now the build runs
|
|
9
|
+
* them and records the result as data, beside the facts it already writes about the PDF:
|
|
10
|
+
*
|
|
11
|
+
* { "schema": 1,
|
|
12
|
+
* "bib": { "source": "paper.tex", "sha256": "…" }, the bibliography that was checked
|
|
13
|
+
* "status": "checked" | "not-checked", "why": "…", not-checked: nothing could be asked
|
|
14
|
+
* "entries": [ { "key", "exists", "authors", "why" } ] }
|
|
15
|
+
*
|
|
16
|
+
* A sibling file, not a new field of `paper.facts.json`: those facts are about ONE PDF and are
|
|
17
|
+
* stale when it changes (`pdf_sha256`); these are about the bibliography and are stale when IT
|
|
18
|
+
* changes (`bib.sha256`). One staleness key per file, the way `pdf/fresh` already works.
|
|
19
|
+
*
|
|
20
|
+
* 🔴 THE STEP NEVER FAILS THE BUILD. A build without network still builds the PDF; the reference
|
|
21
|
+
* step records `not-checked` and the lint rule says so, as a warning. Recording "not checked" as
|
|
22
|
+
* a pass would be the counter that counts what it never looked at.
|
|
23
|
+
*/
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
26
|
+
import { sha256Hex } from "./domain/sha256.ts";
|
|
27
|
+
import type { Files } from "./ports/files.ts";
|
|
28
|
+
import type {
|
|
29
|
+
CheckReferences,
|
|
30
|
+
EntryVerdict,
|
|
31
|
+
ReferencesCheck,
|
|
32
|
+
} from "./ports/check-references.ts";
|
|
33
|
+
// @ts-expect-error — an ESLint rule module in .mjs, it has no types
|
|
34
|
+
import { bibRange } from "../eslint-rules/paper-typography.mjs";
|
|
35
|
+
|
|
36
|
+
export const REFERENCES_SCHEMA = 1;
|
|
37
|
+
export const REFERENCES_FILE = "references.json";
|
|
38
|
+
|
|
39
|
+
/** Where a paper's reference verdicts live — beside `paper.facts.json`. */
|
|
40
|
+
export const referencesPath = (paperDir: string): string =>
|
|
41
|
+
join(paperDir, "_build", REFERENCES_FILE);
|
|
42
|
+
|
|
43
|
+
const at = (p: string): AbsolutePath => p as AbsolutePath;
|
|
44
|
+
const text = (files: Files, p: string): string | null => {
|
|
45
|
+
const b = files.readBytes(at(p));
|
|
46
|
+
return b === null ? null : new TextDecoder().decode(b);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** The bibliography a paper carries: inside `paper.tex` (`filecontents`), else `refs.bib`. */
|
|
50
|
+
export interface Bibliography {
|
|
51
|
+
readonly source: "paper.tex" | "refs.bib";
|
|
52
|
+
readonly text: string;
|
|
53
|
+
/** Where `text` starts in the source file — so a finding can point at an entry. */
|
|
54
|
+
readonly offset: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function bibliographyOf(
|
|
58
|
+
files: Files,
|
|
59
|
+
paperDir: string,
|
|
60
|
+
): Bibliography | null {
|
|
61
|
+
const tex = text(files, join(paperDir, "paper.tex"));
|
|
62
|
+
const inline = tex === null ? null : bibRange(tex);
|
|
63
|
+
if (inline)
|
|
64
|
+
return { source: "paper.tex", text: inline.body, offset: inline.bodyStart };
|
|
65
|
+
const bib = text(files, join(paperDir, "refs.bib"));
|
|
66
|
+
return bib === null ? null : { source: "refs.bib", text: bib, offset: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const bibHash = (b: Bibliography): string =>
|
|
70
|
+
sha256Hex(new TextEncoder().encode(b.text));
|
|
71
|
+
|
|
72
|
+
/** What `_build/references.json` holds. */
|
|
73
|
+
export interface ReferencesDocument {
|
|
74
|
+
readonly schema: number;
|
|
75
|
+
readonly bib: { readonly source: string; readonly sha256: string };
|
|
76
|
+
readonly status: "checked" | "not-checked";
|
|
77
|
+
readonly why?: string;
|
|
78
|
+
readonly entries: readonly EntryVerdict[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const documentOf = (
|
|
82
|
+
bib: Bibliography,
|
|
83
|
+
check: ReferencesCheck,
|
|
84
|
+
): ReferencesDocument => ({
|
|
85
|
+
schema: REFERENCES_SCHEMA,
|
|
86
|
+
bib: { source: bib.source, sha256: bibHash(bib) },
|
|
87
|
+
...(check.kind === "checked"
|
|
88
|
+
? { status: "checked" as const, entries: check.entries }
|
|
89
|
+
: { status: "not-checked" as const, why: check.why, entries: [] }),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** The recorded verdicts, or null when there are none or they do not parse. */
|
|
93
|
+
export function readReferences(
|
|
94
|
+
files: Files,
|
|
95
|
+
paperDir: string,
|
|
96
|
+
): ReferencesDocument | null {
|
|
97
|
+
const raw = text(files, referencesPath(paperDir));
|
|
98
|
+
if (raw === null) return null;
|
|
99
|
+
try {
|
|
100
|
+
const d = JSON.parse(raw) as ReferencesDocument;
|
|
101
|
+
return d?.schema === REFERENCES_SCHEMA && Array.isArray(d.entries)
|
|
102
|
+
? d
|
|
103
|
+
: null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The build step's work: check the bibliography and record the verdicts. Returns the one-line
|
|
111
|
+
* note the build prints. A checker that throws is recorded as `not-checked` — the step reports,
|
|
112
|
+
* it never refuses.
|
|
113
|
+
*/
|
|
114
|
+
export async function recordReferences(
|
|
115
|
+
files: Files,
|
|
116
|
+
paperDir: string,
|
|
117
|
+
check: CheckReferences,
|
|
118
|
+
): Promise<string> {
|
|
119
|
+
const bib = bibliographyOf(files, paperDir);
|
|
120
|
+
if (bib === null) return "no bibliography — nothing to check";
|
|
121
|
+
let result: ReferencesCheck;
|
|
122
|
+
try {
|
|
123
|
+
result = await check(bib.text);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
result = { kind: "not-checked", why: (e as Error).message };
|
|
126
|
+
}
|
|
127
|
+
const doc = documentOf(bib, result);
|
|
128
|
+
files.writeAtomic(
|
|
129
|
+
at(referencesPath(paperDir)),
|
|
130
|
+
new TextEncoder().encode(`${JSON.stringify(doc, null, 2)}\n`),
|
|
131
|
+
);
|
|
132
|
+
if (doc.status === "not-checked")
|
|
133
|
+
return `references NOT checked — ${doc.why ?? "no reason given"}; lint will say so`;
|
|
134
|
+
const bad = doc.entries.filter(
|
|
135
|
+
(e) => e.exists === "false" || e.authors === "mismatch",
|
|
136
|
+
).length;
|
|
137
|
+
return `references: ${String(doc.entries.length)} checked, ${String(bad)} failing → _build/${REFERENCES_FILE}`;
|
|
138
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `rules-config.ts` — parsing the `rules` key into ESLint config blocks, and deriving which rules
|
|
3
|
-
* paperlint ships. The end-to-end path (a
|
|
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
6
|
import assert from "node:assert/strict";
|
|
@@ -46,23 +46,39 @@ check(
|
|
|
46
46
|
JSON.stringify(r),
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
+
{
|
|
50
|
+
const r = parse({ "pdf/last-page-balance": "error" });
|
|
51
|
+
check(
|
|
52
|
+
"the object form is ONE block with no `files` — narrowed to each rule's own files later",
|
|
53
|
+
r.ok &&
|
|
54
|
+
JSON.stringify(r.value) ===
|
|
55
|
+
JSON.stringify([
|
|
56
|
+
{
|
|
57
|
+
basePath: "/consumer",
|
|
58
|
+
rules: { "pdf/last-page-balance": "error" },
|
|
59
|
+
},
|
|
60
|
+
]),
|
|
61
|
+
JSON.stringify(r),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
49
64
|
check(
|
|
50
65
|
"both severity spellings are accepted",
|
|
51
66
|
parse([{ rules: { "paper/typography": 0, "pdf/last-page-balance": "warn" } }])
|
|
52
67
|
.ok,
|
|
53
68
|
);
|
|
54
69
|
const refusals = [
|
|
55
|
-
[
|
|
56
|
-
[
|
|
57
|
-
[[
|
|
58
|
-
[[{
|
|
70
|
+
["pdf/last-page-balance", 'S → "rules" must be { "<rule>": "<severity>" }'],
|
|
71
|
+
[{ a: 1 }, '"a" is not a rule paperlint ships'],
|
|
72
|
+
[[42], "S → rules[0] must be an object"],
|
|
73
|
+
[[{ rules: [] }], "S → rules[0].rules must be an object"],
|
|
74
|
+
[[{ files: [], rules: {} }], "S → rules[0].files must be a non-empty list"],
|
|
59
75
|
[
|
|
60
76
|
[{ files: ["x"], ignores: [1], rules: {} }],
|
|
61
|
-
"S
|
|
77
|
+
"S → rules[0].ignores must be a non-empty list",
|
|
62
78
|
],
|
|
63
79
|
[
|
|
64
80
|
[{ languageOptions: {}, rules: {} }],
|
|
65
|
-
'S
|
|
81
|
+
'S → rules[0]: unknown key "languageOptions"',
|
|
66
82
|
],
|
|
67
83
|
[
|
|
68
84
|
[{ rules: { "markdown/no-html": "error" } }],
|
|
@@ -70,7 +86,7 @@ const refusals = [
|
|
|
70
86
|
],
|
|
71
87
|
[
|
|
72
88
|
[{ rules: { "paper/typography": ["loud"] } }],
|
|
73
|
-
'S
|
|
89
|
+
'S → rules[0].rules["paper/typography"]: ["loud"] is not a severity',
|
|
74
90
|
],
|
|
75
91
|
];
|
|
76
92
|
for (const [raw, says] of refusals) {
|
package/src/rules-config.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type Parsed<T> =
|
|
|
44
44
|
| { readonly ok: false; readonly error: string };
|
|
45
45
|
|
|
46
46
|
const SEVERITIES: readonly unknown[] = ["off", "warn", "error", 0, 1, 2];
|
|
47
|
+
|
|
47
48
|
const BLOCK_KEYS = new Set(["files", "ignores", "rules"]);
|
|
48
49
|
|
|
49
50
|
const bad = <T>(error: string): Parsed<T> => ({ ok: false, error });
|
|
@@ -65,24 +66,29 @@ function parseEntry(v: unknown): RuleEntry | null {
|
|
|
65
66
|
return v as RuleEntry;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* A `rules` object — rule id → severity or `[severity, ...options]` — with only the rules paperlint
|
|
71
|
+
* ships. Read from every `paperlint.json`, the root's and each paper's.
|
|
72
|
+
*
|
|
73
|
+
* @param where the key path to name in messages, e.g. `package.json → "paperlint".rules[0].rules`
|
|
74
|
+
*/
|
|
75
|
+
export function parseRuleEntries(
|
|
70
76
|
v: unknown,
|
|
71
|
-
|
|
77
|
+
where: string,
|
|
72
78
|
shipped: ReadonlySet<string>,
|
|
73
79
|
): Parsed<Record<string, RuleEntry>> {
|
|
74
80
|
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
75
|
-
return bad(`${
|
|
81
|
+
return bad(`${where} must be an object of rule id → severity`);
|
|
76
82
|
const out: Record<string, RuleEntry> = {};
|
|
77
83
|
for (const [id, raw] of Object.entries(v)) {
|
|
78
84
|
if (!shipped.has(id))
|
|
79
85
|
return bad(
|
|
80
|
-
`${
|
|
86
|
+
`${where}: "${id}" is not a rule paperlint ships — known: ${[...shipped].sort().join(", ")}`,
|
|
81
87
|
);
|
|
82
88
|
const entry = parseEntry(raw);
|
|
83
89
|
if (entry === null)
|
|
84
90
|
return bad(
|
|
85
|
-
`${
|
|
91
|
+
`${where}["${id}"]: ${JSON.stringify(raw)} is not a severity — use "off", "warn" or "error", or ["error", { options }]`,
|
|
86
92
|
);
|
|
87
93
|
out[id] = entry;
|
|
88
94
|
}
|
|
@@ -103,17 +109,20 @@ const globsOf = (o: Record<string, unknown>) => ({
|
|
|
103
109
|
...(o["ignores"] ? { ignores: o["ignores"] as string[] } : {}),
|
|
104
110
|
});
|
|
105
111
|
|
|
112
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
113
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
114
|
+
|
|
106
115
|
/** One element of the `rules` list. */
|
|
107
116
|
function parseBlock(
|
|
108
117
|
v: unknown,
|
|
109
118
|
at: string,
|
|
110
119
|
ctx: { shipped: ReadonlySet<string>; baseDir: string },
|
|
111
120
|
): Parsed<RuleBlock> {
|
|
112
|
-
if (
|
|
121
|
+
if (!isRecord(v))
|
|
113
122
|
return bad(
|
|
114
123
|
`${at} must be an object like { "files": [...], "rules": {...} }`,
|
|
115
124
|
);
|
|
116
|
-
const o = v
|
|
125
|
+
const o = v;
|
|
117
126
|
const extra = Object.keys(o).filter((k) => !BLOCK_KEYS.has(k));
|
|
118
127
|
if (extra.length)
|
|
119
128
|
return bad(
|
|
@@ -121,7 +130,7 @@ function parseBlock(
|
|
|
121
130
|
);
|
|
122
131
|
const globs = badGlobs(o, at);
|
|
123
132
|
if (globs) return bad(globs);
|
|
124
|
-
const rules =
|
|
133
|
+
const rules = parseRuleEntries(o["rules"], `${at}.rules`, ctx.shipped);
|
|
125
134
|
if (!rules.ok) return rules;
|
|
126
135
|
return {
|
|
127
136
|
ok: true,
|
|
@@ -130,10 +139,12 @@ function parseBlock(
|
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
/**
|
|
133
|
-
* The `rules` key → config blocks, or one error naming the key path.
|
|
142
|
+
* The `rules` key → config blocks, or one error naming the key path. Two shapes, at every level:
|
|
143
|
+
* `{ id: severity }` is one block over every paper file under `baseDir`; a list is ESLint blocks
|
|
144
|
+
* (`files`, `ignores`, `rules`), their globs relative to `baseDir`.
|
|
134
145
|
*
|
|
135
146
|
* @param raw the value under `rules` (undefined when absent)
|
|
136
|
-
* @param where how the settings are named in messages, e.g. `
|
|
147
|
+
* @param where how the settings are named in messages, e.g. `paperlint.json`
|
|
137
148
|
* @param shipped the rule ids paperlint ships (`shippedRuleIds`)
|
|
138
149
|
* @param baseDir the directory of the file holding the settings
|
|
139
150
|
*/
|
|
@@ -144,13 +155,19 @@ export function parseRuleBlocks(
|
|
|
144
155
|
baseDir: string,
|
|
145
156
|
): Parsed<readonly RuleBlock[]> {
|
|
146
157
|
if (raw === undefined) return { ok: true, value: [] };
|
|
158
|
+
if (isRecord(raw)) {
|
|
159
|
+
const rules = parseRuleEntries(raw, `${where} → "rules"`, shipped);
|
|
160
|
+
return rules.ok
|
|
161
|
+
? { ok: true, value: [{ basePath: baseDir, rules: rules.value }] }
|
|
162
|
+
: rules;
|
|
163
|
+
}
|
|
147
164
|
if (!Array.isArray(raw))
|
|
148
165
|
return bad(
|
|
149
|
-
`${where}
|
|
166
|
+
`${where} → "rules" must be { "<rule>": "<severity>" }, or a list of blocks like ESLint's flat config: [{ "files": [...], "rules": {...} }]`,
|
|
150
167
|
);
|
|
151
168
|
const out: RuleBlock[] = [];
|
|
152
169
|
for (const [i, v] of raw.entries()) {
|
|
153
|
-
const b = parseBlock(v, `${where}
|
|
170
|
+
const b = parseBlock(v, `${where} → rules[${i}]`, { shipped, baseDir });
|
|
154
171
|
if (!b.ok) return b;
|
|
155
172
|
out.push(b.value);
|
|
156
173
|
}
|
|
@@ -72,7 +72,9 @@ try {
|
|
|
72
72
|
check(
|
|
73
73
|
"and the message names the CONSEQUENCE, not a restatement of the condition",
|
|
74
74
|
/is checked/.test(at("no-scorecard")[0].message) &&
|
|
75
|
-
/stages, sources or
|
|
75
|
+
/stages, sources or research question/.test(
|
|
76
|
+
at("no-scorecard")[0].message,
|
|
77
|
+
),
|
|
76
78
|
);
|
|
77
79
|
check(
|
|
78
80
|
"and the consequence names THIS directory by name",
|
|
@@ -132,9 +134,9 @@ try {
|
|
|
132
134
|
!STRUCTURE_DEFAULTS.require.includes("paper.pdf"),
|
|
133
135
|
);
|
|
134
136
|
check(
|
|
135
|
-
"and `
|
|
136
|
-
STRUCTURE_DEFAULTS.markers.includes("
|
|
137
|
-
!STRUCTURE_DEFAULTS.require.includes("
|
|
137
|
+
"and `paperlint.json` counts as a marker but not a requirement",
|
|
138
|
+
STRUCTURE_DEFAULTS.markers.includes("paperlint.json") &&
|
|
139
|
+
!STRUCTURE_DEFAULTS.require.includes("paperlint.json"),
|
|
138
140
|
);
|
|
139
141
|
|
|
140
142
|
// ── one schema for both halves ────────────────────────────────────────────────────────
|
package/src/structure.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { readdirSync, existsSync } from "node:fs";
|
|
26
26
|
import { join, relative, basename, isAbsolute, sep } from "node:path";
|
|
27
27
|
import type { StructureConfig, StructureFinding } from "./types.ts";
|
|
28
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
28
29
|
|
|
29
30
|
/** The requirements after the consumer's config is laid over the defaults. */
|
|
30
31
|
type Rules = Required<StructureConfig>;
|
|
@@ -37,7 +38,7 @@ type Rules = Required<StructureConfig>;
|
|
|
37
38
|
* byte-compared by the `paper/stages` rule anyway.
|
|
38
39
|
*/
|
|
39
40
|
export const STRUCTURE_DEFAULTS = {
|
|
40
|
-
markers: ["PIPELINE-STATUS.md", "paper.tex", "paper.md",
|
|
41
|
+
markers: ["PIPELINE-STATUS.md", "paper.tex", "paper.md", CONFIG_FILE],
|
|
41
42
|
require: ["PIPELINE-STATUS.md"],
|
|
42
43
|
requireOneOf: [["paper.tex", "paper.md"]],
|
|
43
44
|
ignore: [],
|
|
@@ -121,7 +122,7 @@ export function checkStructure(
|
|
|
121
122
|
*/
|
|
122
123
|
function whyMissingMatters(file: string, dirName: string): string {
|
|
123
124
|
if (file === "PIPELINE-STATUS.md")
|
|
124
|
-
return `\`paper/stages\`, \`paper/source\` and \`paper/
|
|
125
|
+
return `\`paper/stages\`, \`paper/source\` and \`paper/research-question\` read this file, so nothing \`${dirName}\` declares about its stages, sources or research question is checked`;
|
|
125
126
|
return `declared as required by your \`structure\` configuration`;
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -103,7 +103,9 @@ for (const v of acm) {
|
|
|
103
103
|
);
|
|
104
104
|
}
|
|
105
105
|
// Venues on ONE template must not drift apart: a second list is how the old installer's three
|
|
106
|
-
// dictionaries diverged.
|
|
106
|
+
// dictionaries diverged. Since the ACM venues extend the `acm-sigconf` family, only the family
|
|
107
|
+
// declares the template's packages, so this holds by construction; the check stays for any two
|
|
108
|
+
// standalone presets that name the same template.
|
|
107
109
|
const byTemplate = new Map();
|
|
108
110
|
for (const v of venues) {
|
|
109
111
|
const t = templateOf(v);
|
|
@@ -173,30 +175,22 @@ rmSync(tmp, { recursive: true, force: true });
|
|
|
173
175
|
const r = R.requirementsFor(null, VENUES);
|
|
174
176
|
// Guards: saying WHY a paper runs on the base set.
|
|
175
177
|
check(
|
|
176
|
-
"no venue
|
|
177
|
-
r.source.includes("no venue.json") &&
|
|
178
|
+
"no venue preset → the base set, and the source says so",
|
|
179
|
+
r.source.includes("no venue preset in paperlint.json") &&
|
|
178
180
|
"hyperref" in r.tex.packages &&
|
|
179
181
|
!("acmart" in r.tex.packages),
|
|
180
182
|
r.source,
|
|
181
183
|
);
|
|
182
184
|
}
|
|
185
|
+
// Which preset a paper resolves to — a typo, the base file, a missing one — is src/presets.ts's
|
|
186
|
+
// question now, tested in src/presets.test.ts; this function only adds a resolved preset's packages.
|
|
183
187
|
{
|
|
184
|
-
const r = R.requirementsFor(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
r.source.includes("venue nowhere has no profile") &&
|
|
188
|
-
!("acmart" in r.tex.packages),
|
|
189
|
-
r.source,
|
|
188
|
+
const r = R.requirementsFor(
|
|
189
|
+
{ label: acm[0], tex: read(`${acm[0]}.jsonc`) },
|
|
190
|
+
VENUES,
|
|
190
191
|
);
|
|
191
|
-
}
|
|
192
|
-
check(
|
|
193
|
-
"the base file is not a venue: `tex-base` gets the base set with the reason",
|
|
194
|
-
R.requirementsFor("tex-base", VENUES).source.includes("has no profile"),
|
|
195
|
-
);
|
|
196
|
-
{
|
|
197
|
-
const r = R.requirementsFor(acm[0], VENUES);
|
|
198
192
|
check(
|
|
199
|
-
"a
|
|
193
|
+
"a resolved preset → its packages ON TOP OF the base set, and the source names its label",
|
|
200
194
|
r.source === `venue ${acm[0]}` &&
|
|
201
195
|
"acmart" in r.tex.packages &&
|
|
202
196
|
"hyperref" in r.tex.packages &&
|