paperlint 4.2.0 → 6.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 +15 -2
- package/CONTRIBUTING.md +3 -0
- package/README.md +23 -12
- package/action.harness.mjs +54 -36
- package/action.yml +12 -27
- package/bin/paperlint.mjs +5 -1
- package/dist/adapters/latex/includes.d.ts +5 -1
- package/dist/adapters/latex/includes.d.ts.map +1 -1
- package/dist/adapters/latex/includes.js +8 -2
- package/dist/adapters/latex/includes.js.map +1 -1
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/layout.d.ts +27 -0
- package/dist/adapters/latex/layout.d.ts.map +1 -0
- package/dist/adapters/latex/layout.js +62 -0
- package/dist/adapters/latex/layout.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +11 -1
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +5 -1
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -3
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +2 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +92 -42
- package/dist/cli.js.map +1 -1
- package/dist/domain/anonymity.d.ts +68 -0
- package/dist/domain/anonymity.d.ts.map +1 -0
- package/dist/domain/anonymity.js +94 -0
- package/dist/domain/anonymity.js.map +1 -0
- package/dist/domain/body-pages.d.ts +69 -0
- package/dist/domain/body-pages.d.ts.map +1 -0
- package/dist/domain/body-pages.js +141 -0
- package/dist/domain/body-pages.js.map +1 -0
- package/dist/domain/paper-source.js +2 -2
- package/dist/domain/tex-document.d.ts +9 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.d.ts +5 -1
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +6 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.d.ts +20 -0
- package/dist/eslint-rules/paper-context.d.ts.map +1 -0
- package/dist/eslint-rules/paper-context.js +44 -0
- package/dist/eslint-rules/paper-context.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +1 -1
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +32 -18
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +7 -1
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/facts-file.d.ts +28 -2
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +54 -9
- package/dist/facts-file.js.map +1 -1
- package/dist/init.d.ts +4 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +24 -29
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/package-dirs.d.ts +2 -0
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +8 -0
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-files.d.ts +2 -2
- package/dist/paper-includes.d.ts +17 -0
- package/dist/paper-includes.d.ts.map +1 -0
- package/dist/paper-includes.js +51 -0
- package/dist/paper-includes.js.map +1 -0
- package/dist/paper-settings.d.ts +11 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +38 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-content.d.ts +40 -0
- package/dist/pdf-content.d.ts.map +1 -0
- package/dist/pdf-content.js +97 -0
- package/dist/pdf-content.js.map +1 -0
- package/dist/pdf-facts.d.ts +30 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +37 -1
- package/dist/pdf-facts.js.map +1 -1
- package/dist/ports/latex.d.ts +3 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -0
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.d.ts +1 -0
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-paper.d.ts +17 -3
- package/dist/tex-paper.d.ts.map +1 -1
- package/dist/tex-paper.js +36 -38
- package/dist/tex-paper.js.map +1 -1
- package/dist/tex-requirements.d.ts +4 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +3 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +7 -1
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +40 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +38 -8
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +179 -41
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +23 -20
- package/docs/e2e.md +32 -11
- package/docs/install.md +8 -8
- package/docs/prior-art/paper-folder-scaffolding.md +4 -7
- package/docs/rules/anonymity/identity.md +111 -0
- package/docs/rules/format/layout-override.md +61 -0
- package/docs/rules/format/page-limit.md +108 -0
- package/docs/rules/pdf/body-size.md +87 -0
- package/docs/rules.md +55 -46
- package/eslint-rules/latex-language.ts +10 -4
- package/eslint-rules/paper-context.test.ts +47 -0
- package/eslint-rules/paper-context.ts +57 -0
- package/eslint-rules/paper-typography.ts +67 -23
- package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
- package/eslint-rules/pdf-last-page-balance.ts +2 -2
- package/eslint-rules/tex-build.ts +8 -1
- package/fixtures/build-e2e/aidc/expect.json +1 -1
- package/fixtures/build-e2e/aidc/paperlint.json +1 -1
- package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-ack/expect.json +19 -0
- package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
- package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-author/expect.json +19 -0
- package/fixtures/build-e2e/anon-author/paper.tex +29 -0
- package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-bib/expect.json +20 -0
- package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
- package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-href/expect.json +19 -0
- package/fixtures/build-e2e/anon-href/paper.tex +30 -0
- package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-macro/expect.json +19 -0
- package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
- package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
- package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
- package/fixtures/build-e2e/unbalanced/expect.json +2 -2
- package/fixtures/real-markdown-paper/baseline.json +1 -2
- package/lib/paper-config.ts +3 -1
- package/package.json +1 -1
- package/presets/acm-sigconf.jsonc +3 -1
- package/presets/aidc.jsonc +16 -17
- package/presets/ieee-conference.jsonc +8 -0
- package/presets/venue-profile.schema.json +9 -0
- package/scripts/check.ts +2 -2
- package/scripts/eslint-report-guard.mjs +5 -2
- package/scripts/harness-api.frozen.json +0 -1
- package/skills/paper-pipeline/references/pipeline-status-template.md +2 -16
- package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
- package/src/adapters/latex/includes.ts +10 -2
- package/src/adapters/latex/index.test.ts +8 -0
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/layout.ts +106 -0
- package/src/adapters/latex/nodes.ts +11 -3
- package/src/adapters/latex/parse.ts +5 -1
- package/src/build.harness.mjs +6 -4
- package/src/build.test.ts +23 -1
- package/src/build.ts +7 -3
- package/src/cli.harness.mjs +22 -54
- package/src/cli.test.ts +15 -18
- package/src/cli.ts +114 -46
- package/src/domain/anonymity.test.ts +163 -0
- package/src/domain/anonymity.ts +129 -0
- package/src/domain/body-pages.test.ts +300 -0
- package/src/domain/body-pages.ts +223 -0
- package/src/domain/paper-source.ts +2 -2
- package/src/domain/tex-document.ts +10 -0
- package/src/facts-file.test.ts +100 -4
- package/src/facts-file.ts +91 -9
- package/src/init.test.ts +33 -13
- package/src/init.ts +25 -48
- package/src/linted-set.test.ts +11 -1
- package/src/new-paper.harness.mjs +1 -5
- package/src/new-paper.test.ts +7 -1
- package/src/package-dirs.ts +9 -0
- package/src/paper-files.ts +2 -2
- package/src/paper-includes.test.ts +161 -0
- package/src/paper-includes.ts +77 -0
- package/src/paper-settings.test.ts +63 -6
- package/src/paper-settings.ts +53 -5
- package/src/pdf-content.test.ts +71 -0
- package/src/pdf-content.ts +125 -0
- package/src/pdf-facts.test.ts +110 -2
- package/src/pdf-facts.ts +106 -8
- package/src/ports/latex.ts +3 -0
- package/src/presets.ts +8 -8
- package/src/rule-cases.test.ts +29 -33
- package/src/rule-docs.test.ts +0 -3
- package/src/rules-config.ts +1 -0
- package/src/structure.harness.ts +1 -3
- package/src/structure.ts +1 -1
- package/src/tex-paper.test.ts +78 -61
- package/src/tex-paper.ts +57 -48
- package/src/tex-requirements.ts +9 -0
- package/src/tex-venue-rules.test.ts +111 -6
- package/src/tex-venue-rules.ts +57 -2
- package/src/types.ts +2 -0
- package/src/venue-rules.test.ts +351 -39
- package/src/venue-rules.ts +247 -47
- package/templates/paper/PIPELINE-STATUS.md +0 -4
- package/dist/action-ref.d.ts +0 -12
- package/dist/action-ref.d.ts.map +0 -1
- package/dist/action-ref.js +0 -16
- package/dist/action-ref.js.map +0 -1
- package/dist/eslint-rules/paper-research-question.d.ts +0 -31
- package/dist/eslint-rules/paper-research-question.d.ts.map +0 -1
- package/dist/eslint-rules/paper-research-question.js +0 -188
- package/dist/eslint-rules/paper-research-question.js.map +0 -1
- package/docs/rules/tex/missing-input.md +0 -58
- package/eslint-rules/paper-research-question.harness.mjs +0 -212
- package/eslint-rules/paper-research-question.ts +0 -199
- package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/comment-only/paper.tex +0 -7
- package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +0 -6
- package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +0 -6
- package/fixtures/paper-research-question/draft/paper.tex +0 -2
- package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/markdown-no-rq/paper.md +0 -4
- package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +0 -12
- package/fixtures/paper-research-question/shipped-no-rq/paper.tex +0 -3
- package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/shipped-with-rq/paper.tex +0 -2
- package/src/action-ref.test.ts +0 -26
- package/src/action-ref.ts +0 -15
package/src/facts-file.ts
CHANGED
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
* fonts are all named `Type3`; `embedded` means a usable program is present, so a corrupted one is
|
|
27
27
|
* false. `pdf` is now relative to the PAPER directory, not to wherever the command ran.
|
|
28
28
|
*
|
|
29
|
+
* ── SCHEMA 3 (2026-09-29) ───────────────────────────────────────────────────────
|
|
30
|
+
* Adds what the PDF SAYS, for the rules that judge text rather than geometry: `pages_text` (each
|
|
31
|
+
* page's text, in content order), `metadata` (the Info dictionary and the XMP packet, flat),
|
|
32
|
+
* `links` (every link annotation's target and page), `bib_anchor_page` and `appendix_anchor_page`
|
|
33
|
+
* (the pages hyperref anchored the bibliography's first entry and the appendix on, or null). Every schema-2 field keeps its name and meaning. The number moves anyway: `anonymity/identity` and `format/page-limit` read these fields,
|
|
34
|
+
* and schema-2 facts without them would be judged as a PDF that says nothing — a clean pass over no
|
|
35
|
+
* input. A schema-2 file is refused by `pdf/fresh` with "rebuild", which is one `paperlint build`.
|
|
36
|
+
*
|
|
29
37
|
* 🔴 STALENESS. The PDF is not committed, so neither are these facts: they live in `_build/` and
|
|
30
38
|
* carry `pdf_sha256`. A rule compares it with the PDF on disk and refuses facts about another build.
|
|
31
39
|
*/
|
|
@@ -52,7 +60,7 @@ import { paperPreset, paperPresetProblem } from "./presets.ts";
|
|
|
52
60
|
import { presetsDir } from "./package-dirs.ts";
|
|
53
61
|
import { messageOf } from "./domain/text.ts";
|
|
54
62
|
|
|
55
|
-
export const FACTS_SCHEMA =
|
|
63
|
+
export const FACTS_SCHEMA = 3;
|
|
56
64
|
export const FACTS_DIR = "_build";
|
|
57
65
|
export const FACTS_FILE = "paper.facts.json";
|
|
58
66
|
|
|
@@ -123,7 +131,17 @@ export type FactsDocument = {
|
|
|
123
131
|
readonly last_page: LastPageEntry;
|
|
124
132
|
/** Schema 1's field, kept: the two heights, or null when the last page is a stub or a review build. */
|
|
125
133
|
readonly last_page_cols_pt: readonly [number, number] | null;
|
|
126
|
-
} &
|
|
134
|
+
} & FactsTextFields &
|
|
135
|
+
FactsGeometryFields;
|
|
136
|
+
|
|
137
|
+
/** Schema 3's fields: what the PDF says (see the module header). */
|
|
138
|
+
export interface FactsTextFields {
|
|
139
|
+
readonly pages_text: readonly string[];
|
|
140
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
141
|
+
readonly links: readonly { readonly page: number; readonly uri: string }[];
|
|
142
|
+
readonly bib_anchor_page: number | null;
|
|
143
|
+
readonly appendix_anchor_page: number | null;
|
|
144
|
+
}
|
|
127
145
|
|
|
128
146
|
export function fontEntry(f: FontFact): FontEntry {
|
|
129
147
|
return {
|
|
@@ -170,6 +188,11 @@ export function factsDocument(i: FactsInput): FactsDocument {
|
|
|
170
188
|
i.read.fonts.kind === "drawn" ? i.read.fonts.list.map(fontEntry) : [],
|
|
171
189
|
last_page: last,
|
|
172
190
|
last_page_cols_pt: last.kind === "measured" ? last.columns_pt : null,
|
|
191
|
+
pages_text: i.read.pageTexts,
|
|
192
|
+
metadata: i.read.metadata,
|
|
193
|
+
links: i.read.links,
|
|
194
|
+
bib_anchor_page: i.read.bibAnchorPage,
|
|
195
|
+
appendix_anchor_page: i.read.appendixAnchorPage,
|
|
173
196
|
...flatGeometry(i.geometry),
|
|
174
197
|
};
|
|
175
198
|
}
|
|
@@ -249,6 +272,19 @@ export interface ReadFacts {
|
|
|
249
272
|
readonly fonts: readonly FontEntry[];
|
|
250
273
|
/** The geometry columns, or null when no measurer ran (`geometry_source` is null). */
|
|
251
274
|
readonly geometry: FlatGeometry | null;
|
|
275
|
+
/** What the PDF says: schema 3's fields, parsed. */
|
|
276
|
+
readonly text: TextFacts;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Schema 3's fields as a rule reads them. */
|
|
280
|
+
export interface TextFacts {
|
|
281
|
+
readonly pages: readonly string[];
|
|
282
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
283
|
+
readonly links: readonly { readonly page: number; readonly uri: string }[];
|
|
284
|
+
/** The page of the first bibliography entry hyperref anchored, or null. */
|
|
285
|
+
readonly bibAnchorPage: number | null;
|
|
286
|
+
/** The page hyperref anchored the appendix on, or null. */
|
|
287
|
+
readonly appendixAnchorPage: number | null;
|
|
252
288
|
}
|
|
253
289
|
|
|
254
290
|
/** Why a facts file cannot be judged. */
|
|
@@ -331,6 +367,56 @@ function geometryOf(
|
|
|
331
367
|
});
|
|
332
368
|
}
|
|
333
369
|
|
|
370
|
+
const isPage = (v: unknown): v is number =>
|
|
371
|
+
typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
372
|
+
const isPageOrNull = (v: unknown): v is number | null =>
|
|
373
|
+
v === null || isPage(v);
|
|
374
|
+
const isStrings = (v: unknown): v is readonly string[] =>
|
|
375
|
+
Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
376
|
+
const isStringRecord = (v: unknown): v is Readonly<Record<string, string>> =>
|
|
377
|
+
isRecord(v) && Object.values(v).every((x) => typeof x === "string");
|
|
378
|
+
const isLinks = (
|
|
379
|
+
v: unknown,
|
|
380
|
+
): v is readonly { readonly page: number; readonly uri: string }[] =>
|
|
381
|
+
Array.isArray(v) &&
|
|
382
|
+
v.every(
|
|
383
|
+
(l) => isRecord(l) && isPage(l["page"]) && typeof l["uri"] === "string",
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
/** Schema 3's fields, or which one is wrong. */
|
|
387
|
+
function textOf(
|
|
388
|
+
d: Readonly<Record<string, unknown>>,
|
|
389
|
+
): Result<TextFacts, string> {
|
|
390
|
+
const { pages_text, metadata, links } = d;
|
|
391
|
+
const { bib_anchor_page, appendix_anchor_page } = d;
|
|
392
|
+
if (!isStrings(pages_text))
|
|
393
|
+
return err("`pages_text` is not a list of strings");
|
|
394
|
+
if (!isStringRecord(metadata))
|
|
395
|
+
return err("`metadata` is not an object of strings");
|
|
396
|
+
if (!isLinks(links)) return err("`links` is not a list of { page, uri }");
|
|
397
|
+
if (!isPageOrNull(bib_anchor_page))
|
|
398
|
+
return err("`bib_anchor_page` is neither a page number nor null");
|
|
399
|
+
if (!isPageOrNull(appendix_anchor_page))
|
|
400
|
+
return err("`appendix_anchor_page` is neither a page number nor null");
|
|
401
|
+
return ok({
|
|
402
|
+
pages: pages_text,
|
|
403
|
+
metadata,
|
|
404
|
+
links,
|
|
405
|
+
bibAnchorPage: bib_anchor_page,
|
|
406
|
+
appendixAnchorPage: appendix_anchor_page,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** What the PDF measures and what it says: the geometry columns and schema 3's fields. */
|
|
411
|
+
function contentOf(
|
|
412
|
+
d: Readonly<Record<string, unknown>>,
|
|
413
|
+
): Result<Pick<ReadFacts, "geometry" | "text">, string> {
|
|
414
|
+
const geometry = geometryOf(d);
|
|
415
|
+
if (!geometry.ok) return geometry;
|
|
416
|
+
const text = textOf(d);
|
|
417
|
+
return text.ok ? ok({ geometry: geometry.value, text: text.value }) : text;
|
|
418
|
+
}
|
|
419
|
+
|
|
334
420
|
/** The PDF the facts describe: its path and its sha256. */
|
|
335
421
|
function artifactOf(
|
|
336
422
|
d: Readonly<Record<string, unknown>>,
|
|
@@ -368,11 +454,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
|
|
|
368
454
|
kind: "broken",
|
|
369
455
|
why: "`fonts` is not a list of { name, type, embedded, program }",
|
|
370
456
|
});
|
|
371
|
-
const
|
|
372
|
-
if (!
|
|
373
|
-
return ok({
|
|
374
|
-
...artifact.value,
|
|
375
|
-
fonts,
|
|
376
|
-
geometry: geometry.value,
|
|
377
|
-
});
|
|
457
|
+
const content = contentOf(d);
|
|
458
|
+
if (!content.ok) return err({ kind: "broken", why: content.error });
|
|
459
|
+
return ok({ ...artifact.value, fonts, ...content.value });
|
|
378
460
|
}
|
package/src/init.test.ts
CHANGED
|
@@ -21,8 +21,9 @@ import { tmpdir } from "node:os";
|
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { afterEach, describe, expect, it } from "vitest";
|
|
23
23
|
import { FRESH_CLONE_NOTE } from "./hooks-settings.ts";
|
|
24
|
+
import { load as yamlLoad } from "js-yaml";
|
|
25
|
+
import { z } from "zod";
|
|
24
26
|
import {
|
|
25
|
-
UNPINNED_REF,
|
|
26
27
|
WORKFLOW_PATH,
|
|
27
28
|
choosePapers,
|
|
28
29
|
declarePapers,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
reportSkillLinks,
|
|
34
35
|
reportTexLive,
|
|
35
36
|
reportWorkflow,
|
|
37
|
+
workflowYaml,
|
|
36
38
|
} from "./init.ts";
|
|
37
39
|
import { runNode } from "../test/support.ts";
|
|
38
40
|
|
|
@@ -353,30 +355,48 @@ describe("paperlint init — how several candidate directories were decided, sai
|
|
|
353
355
|
const here = (p: string) => `<${p}>`;
|
|
354
356
|
|
|
355
357
|
describe("paperlint init — the workflow section, whole", () => {
|
|
356
|
-
it("the workflow
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
it("the written workflow installs the package, then runs the action from node_modules", () => {
|
|
359
|
+
// Guards: one revision. The action comes from the lockfile-pinned install, so there is no
|
|
360
|
+
// `zernie/paperlint@<ref>` to drift from package.json, and no `paths` to repeat papersDir.
|
|
361
|
+
const Workflow = z.object({
|
|
362
|
+
jobs: z.object({
|
|
363
|
+
papers: z.object({
|
|
364
|
+
steps: z.array(z.record(z.string(), z.unknown())),
|
|
365
|
+
}),
|
|
366
|
+
}),
|
|
367
|
+
});
|
|
368
|
+
expect(Workflow.parse(yamlLoad(workflowYaml())).jobs.papers.steps).toEqual([
|
|
369
|
+
{ uses: "actions/checkout@v4" },
|
|
370
|
+
{ uses: "actions/setup-node@v4", with: { "node-version": 22 } },
|
|
371
|
+
{ run: "npm ci" },
|
|
372
|
+
{ uses: "./node_modules/paperlint" },
|
|
360
373
|
]);
|
|
361
|
-
|
|
362
|
-
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("the workflow report: written, kept, declined, not asked", () => {
|
|
377
|
+
const ctx = { why: "stdin is not a terminal" };
|
|
378
|
+
expect(reportWorkflow("written", ctx)).toEqual([
|
|
379
|
+
` ✓ wrote ${WORKFLOW_PATH}`,
|
|
363
380
|
]);
|
|
364
381
|
expect(reportWorkflow("kept", ctx)).toEqual([
|
|
365
382
|
` ✓ ${WORKFLOW_PATH} is already there — kept, nothing overwritten`,
|
|
366
383
|
]);
|
|
367
|
-
const
|
|
368
|
-
" to run the same checks in CI, add
|
|
369
|
-
|
|
384
|
+
const steps = [
|
|
385
|
+
" to run the same checks in CI, add these steps to a workflow:",
|
|
386
|
+
" - uses: actions/checkout@v4",
|
|
387
|
+
" - uses: actions/setup-node@v4",
|
|
370
388
|
" with:",
|
|
371
|
-
"
|
|
389
|
+
" node-version: 22",
|
|
390
|
+
" - run: npm ci",
|
|
391
|
+
" - uses: ./node_modules/paperlint",
|
|
372
392
|
];
|
|
373
393
|
expect(reportWorkflow("declined", ctx)).toEqual([
|
|
374
394
|
" · declined — nothing written",
|
|
375
|
-
...
|
|
395
|
+
...steps,
|
|
376
396
|
]);
|
|
377
397
|
expect(reportWorkflow("not-asked", ctx)).toEqual([
|
|
378
398
|
" · stdin is not a terminal, so nothing was asked. Default taken: NO file written.",
|
|
379
|
-
...
|
|
399
|
+
...steps,
|
|
380
400
|
]);
|
|
381
401
|
});
|
|
382
402
|
});
|
package/src/init.ts
CHANGED
|
@@ -55,7 +55,6 @@ import {
|
|
|
55
55
|
} from "./doctor.ts";
|
|
56
56
|
import { PAPER_MARKERS, papersIn } from "./build.ts";
|
|
57
57
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
58
|
-
import { actionRef } from "./action-ref.ts";
|
|
59
58
|
import {
|
|
60
59
|
FRESH_CLONE_NOTE,
|
|
61
60
|
SETTINGS_PATH,
|
|
@@ -270,29 +269,33 @@ export async function declarePapers(
|
|
|
270
269
|
|
|
271
270
|
export const WORKFLOW_PATH = join(".github", "workflows", "papers.yml");
|
|
272
271
|
|
|
273
|
-
/** Where the action is pinned when no release tag is known — obviously a placeholder. */
|
|
274
|
-
export const UNPINNED_REF = "<commit-sha>";
|
|
275
|
-
|
|
276
272
|
/**
|
|
277
|
-
* The CI
|
|
278
|
-
*
|
|
279
|
-
*
|
|
273
|
+
* The CI steps, one list for the file `init` writes and for the steps it prints. The action is
|
|
274
|
+
* called from the installed package, after `npm ci`: it and the CLI it runs are then one revision,
|
|
275
|
+
* set by the lockfile, and the action finds papersDir in paperlint.json itself.
|
|
280
276
|
*/
|
|
281
|
-
|
|
277
|
+
const CI_STEPS = [
|
|
278
|
+
"- uses: actions/checkout@v4",
|
|
279
|
+
"- uses: actions/setup-node@v4",
|
|
280
|
+
" with:",
|
|
281
|
+
" node-version: 22",
|
|
282
|
+
"- run: npm ci",
|
|
283
|
+
"- uses: ./node_modules/paperlint",
|
|
284
|
+
] as const;
|
|
285
|
+
|
|
286
|
+
const indent = (by: number) => (line: string) => `${" ".repeat(by)}${line}`;
|
|
287
|
+
|
|
288
|
+
/** The CI workflow `init` offers to write. */
|
|
289
|
+
export function workflowYaml(): string {
|
|
282
290
|
return [
|
|
283
|
-
|
|
284
|
-
? `# Written by \`paperlint init\`. Replace ${UNPINNED_REF} with a commit or release tag of the action.`
|
|
285
|
-
: `# Written by \`paperlint init\`, pinned to ${ref} — the release you installed.`,
|
|
291
|
+
`# Written by \`paperlint init\`.`,
|
|
286
292
|
`name: papers`,
|
|
287
293
|
`on: [push, pull_request]`,
|
|
288
294
|
`jobs:`,
|
|
289
295
|
` papers:`,
|
|
290
296
|
` runs-on: ubuntu-latest`,
|
|
291
297
|
` steps:`,
|
|
292
|
-
|
|
293
|
-
` - uses: zernie/paperlint@${ref ?? UNPINNED_REF}`,
|
|
294
|
-
` with:`,
|
|
295
|
-
` paths: ${papers}`,
|
|
298
|
+
...CI_STEPS.map(indent(6)),
|
|
296
299
|
``,
|
|
297
300
|
].join("\n");
|
|
298
301
|
}
|
|
@@ -301,16 +304,12 @@ export type WorkflowResult = "written" | "kept" | "declined" | "not-asked";
|
|
|
301
304
|
|
|
302
305
|
export async function offerWorkflow(
|
|
303
306
|
root: string,
|
|
304
|
-
papers: string,
|
|
305
307
|
{
|
|
306
308
|
ask,
|
|
307
309
|
interactive,
|
|
308
|
-
version,
|
|
309
310
|
}: {
|
|
310
311
|
ask?: (q: string) => Promise<string>;
|
|
311
312
|
interactive: boolean;
|
|
312
|
-
/** The running package's version; see `InitOptions.version`. */
|
|
313
|
-
version?: string | undefined;
|
|
314
313
|
},
|
|
315
314
|
): Promise<WorkflowResult> {
|
|
316
315
|
const path = join(root, WORKFLOW_PATH);
|
|
@@ -327,27 +326,17 @@ export async function offerWorkflow(
|
|
|
327
326
|
// No answer — an empty line, or a stream that ended — is the safe default, which is "no file".
|
|
328
327
|
if (answer !== "y" && answer !== "yes") return "declined";
|
|
329
328
|
mkdirSync(dirname(path), { recursive: true });
|
|
330
|
-
writeFileSync(path, workflowYaml(
|
|
329
|
+
writeFileSync(path, workflowYaml(), "utf8");
|
|
331
330
|
return "written";
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
/** What `init` says about the CI workflow — and, when none was written, the step to paste. */
|
|
335
334
|
export function reportWorkflow(
|
|
336
335
|
wf: WorkflowResult,
|
|
337
|
-
{
|
|
338
|
-
version,
|
|
339
|
-
papersDir,
|
|
340
|
-
why,
|
|
341
|
-
}: { version?: string | undefined; papersDir: string; why: string },
|
|
336
|
+
{ why }: { why: string },
|
|
342
337
|
): string[] {
|
|
343
|
-
const ref = actionRef(version);
|
|
344
338
|
const out: string[] = [];
|
|
345
|
-
if (wf === "written")
|
|
346
|
-
out.push(
|
|
347
|
-
ref === null
|
|
348
|
-
? ` ✓ wrote ${WORKFLOW_PATH} — pin ${UNPINNED_REF} before pushing it`
|
|
349
|
-
: ` ✓ wrote ${WORKFLOW_PATH}, pinned to ${ref}`,
|
|
350
|
-
);
|
|
339
|
+
if (wf === "written") out.push(` ✓ wrote ${WORKFLOW_PATH}`);
|
|
351
340
|
else if (wf === "kept")
|
|
352
341
|
out.push(
|
|
353
342
|
` ✓ ${WORKFLOW_PATH} is already there — kept, nothing overwritten`,
|
|
@@ -359,10 +348,8 @@ export function reportWorkflow(
|
|
|
359
348
|
);
|
|
360
349
|
if (wf !== "written" && wf !== "kept")
|
|
361
350
|
out.push(
|
|
362
|
-
` to run the same checks in CI, add
|
|
363
|
-
|
|
364
|
-
` with:`,
|
|
365
|
-
` paths: ${papersDir}`,
|
|
351
|
+
` to run the same checks in CI, add these steps to a workflow:`,
|
|
352
|
+
...CI_STEPS.map(indent(8)),
|
|
366
353
|
);
|
|
367
354
|
return out;
|
|
368
355
|
}
|
|
@@ -603,11 +590,6 @@ export interface InitOptions {
|
|
|
603
590
|
resolveCliPapers?: (root: string) => string | null;
|
|
604
591
|
/** Links the skills. Injected only so a test can stand in for the installed package. */
|
|
605
592
|
link?: (root: string) => LinkReport;
|
|
606
|
-
/**
|
|
607
|
-
* The version of the running package, read by the CLI from its own `package.json`. The CI
|
|
608
|
-
* workflow is pinned to its release tag (`actionRef`); absent or unreleased, the placeholder.
|
|
609
|
-
*/
|
|
610
|
-
version?: string | undefined;
|
|
611
593
|
/**
|
|
612
594
|
* TeX Live for `paperlint build`: whether paperlint's own tree is installed, and how to install
|
|
613
595
|
* it (`paperlint toolchain`). Passed in by the CLI; without them the step only names the command.
|
|
@@ -734,7 +716,6 @@ export async function init(
|
|
|
734
716
|
run = spawnSync,
|
|
735
717
|
resolveCliPapers,
|
|
736
718
|
link = (r: string) => linkSkills(r),
|
|
737
|
-
version,
|
|
738
719
|
} = opts;
|
|
739
720
|
// An injected `interactive` is a test standing in for a terminal; its reason is the classic one.
|
|
740
721
|
const { interactive, why } =
|
|
@@ -816,12 +797,8 @@ export async function init(
|
|
|
816
797
|
// ── 5. the one expensive, unguessable thing ───────────────────────────────────────────
|
|
817
798
|
log(``);
|
|
818
799
|
log(`CI`);
|
|
819
|
-
const wf = await offerWorkflow(root,
|
|
820
|
-
|
|
821
|
-
interactive,
|
|
822
|
-
version,
|
|
823
|
-
});
|
|
824
|
-
for (const line of reportWorkflow(wf, { version, papersDir, why })) log(line);
|
|
800
|
+
const wf = await offerWorkflow(root, { ask, interactive });
|
|
801
|
+
for (const line of reportWorkflow(wf, { why })) log(line);
|
|
825
802
|
|
|
826
803
|
// ── 6. a first paper — offered only where there is none, and only to a human ──────────
|
|
827
804
|
log(``);
|
package/src/linted-set.test.ts
CHANGED
|
@@ -26,7 +26,11 @@ afterEach(() => {
|
|
|
26
26
|
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// The body is partly in `sections/intro.tex`, brought in by `\input` in the document; the preamble
|
|
30
|
+
// `\input`s a macro file and paperlint's own `paper-guards`. Only the body file is the paper.
|
|
31
|
+
const TEX =
|
|
32
|
+
"\\documentclass{article}\n\\input{macros}\n\\input{paper-guards}\n" +
|
|
33
|
+
"\\begin{document}x\n\\input{sections/intro}\n\\end{document}\n";
|
|
30
34
|
const JS = "export const x = 1;\n";
|
|
31
35
|
|
|
32
36
|
/** The files paperlint owns in the fixture paper. */
|
|
@@ -35,6 +39,7 @@ const OWNED = [
|
|
|
35
39
|
"papers/a/PIPELINE-STATUS.md",
|
|
36
40
|
"papers/a/reviews/r1.md",
|
|
37
41
|
"papers/a/siblings/smith2025.md",
|
|
42
|
+
"papers/a/sections/intro.tex",
|
|
38
43
|
];
|
|
39
44
|
|
|
40
45
|
/** Everything else a real paper folder and project carry. None of it may be linted. */
|
|
@@ -46,6 +51,10 @@ const NEIGHBOURS = {
|
|
|
46
51
|
"papers/a/repro/lib/y.mjs": JS,
|
|
47
52
|
"papers/a/repro/z.cjs": "module.exports = 1;\n",
|
|
48
53
|
"papers/a/repro/t.ts": "const t: number = 1;\n",
|
|
54
|
+
// A preamble include, a frozen version and a .tex nothing includes: none is the paper's body.
|
|
55
|
+
"papers/a/macros.tex": "\\newcommand{\\x}{y}\n",
|
|
56
|
+
"papers/a/versions/2026-01-01-submitted.tex": TEX,
|
|
57
|
+
"papers/a/notes/scratch.tex": "Scratch.\n",
|
|
49
58
|
"script.js": JS,
|
|
50
59
|
"node_modules/pkg/index.js": JS,
|
|
51
60
|
};
|
|
@@ -75,6 +84,7 @@ function project(): string {
|
|
|
75
84
|
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
76
85
|
"papers/a/reviews/r1.md": "# review\n",
|
|
77
86
|
"papers/a/siblings/smith2025.md": "---\nread: full\n---\n# Smith 2025\n",
|
|
87
|
+
"papers/a/sections/intro.tex": "Intro.\n",
|
|
78
88
|
...NEIGHBOURS,
|
|
79
89
|
};
|
|
80
90
|
for (const [p, text] of Object.entries(all)) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - the scaffold is what `paperlint lint` ACCEPTS: the old first run opened with "missing
|
|
6
6
|
* PIPELINE-STATUS.md", and a scaffold that still produced a finding would move that error
|
|
7
7
|
* from a missing file into a created one;
|
|
8
|
-
* - the scorecard carries
|
|
8
|
+
* - the scorecard carries NO `stages` — a new paper has shipped nothing,
|
|
9
9
|
* and a placeholder stage would be a red `paper/stages` on the very first run;
|
|
10
10
|
* - the scorecard carries the `**Readiness verdict:**` line the status hook surfaces on every
|
|
11
11
|
* paper edit — without it the hook reports that the paper cannot say whether it is ready;
|
|
@@ -104,10 +104,6 @@ try {
|
|
|
104
104
|
"🔴 the scorecard has NO `stages` — a new paper has shipped nothing and owes nothing",
|
|
105
105
|
!("stages" in fm),
|
|
106
106
|
);
|
|
107
|
-
check(
|
|
108
|
-
"and carries `researchQuestion`, present and empty",
|
|
109
|
-
"researchQuestion" in fm && fm.researchQuestion === "",
|
|
110
|
-
);
|
|
111
107
|
check(
|
|
112
108
|
"the name is filled in, and no placeholder survives in any file",
|
|
113
109
|
/PIPELINE-STATUS — demo/.test(status) &&
|
package/src/new-paper.test.ts
CHANGED
|
@@ -51,7 +51,13 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
51
51
|
expect(String(s["$comment"])).toMatch(/paperlint:agenticdev/);
|
|
52
52
|
expect(parsePaperSettings(s)).toEqual({
|
|
53
53
|
ok: true,
|
|
54
|
-
value: {
|
|
54
|
+
value: {
|
|
55
|
+
extends: null,
|
|
56
|
+
kind: null,
|
|
57
|
+
pdf: null,
|
|
58
|
+
rules: null,
|
|
59
|
+
identity: null,
|
|
60
|
+
},
|
|
55
61
|
});
|
|
56
62
|
});
|
|
57
63
|
|
package/src/package-dirs.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* presets/ the venue presets (`paperlint:<name>`) and their JSON Schema
|
|
7
7
|
* presets/tex/ the LaTeX inputs `paperlint build` puts on TEXINPUTS
|
|
8
|
+
*
|
|
9
|
+
* `texSearchPath` is where an `\input` is looked for, in order. The build hands it to TeX and the
|
|
10
|
+
* lint rules read includes through it, so the two cannot disagree about which file a paper means.
|
|
8
11
|
*/
|
|
9
12
|
import { fileURLToPath } from "node:url";
|
|
10
13
|
|
|
@@ -15,3 +18,9 @@ export const presetsDir = (): string =>
|
|
|
15
18
|
/** The LaTeX inputs a paper `\input`s by name (`paper-guards`, a venue's numbers). */
|
|
16
19
|
export const texInputsDir = (): string =>
|
|
17
20
|
fileURLToPath(new URL("../presets/tex", import.meta.url));
|
|
21
|
+
|
|
22
|
+
/** Where an `\input` in the paper in `paperDir` is looked for, first to last. */
|
|
23
|
+
export const texSearchPath = (paperDir: string): readonly string[] => [
|
|
24
|
+
paperDir,
|
|
25
|
+
texInputsDir(),
|
|
26
|
+
];
|
package/src/paper-files.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*
|
|
17
17
|
* 2. **Each rule scoped to the files it was written for** (`ruleOwners` + `narrowToOwners`).
|
|
18
18
|
* One plugin NAME is bound to different plugin objects in different blocks: `paper` holds
|
|
19
|
-
* `stages`/`source`/`author-list` beside `PIPELINE-STATUS.md` and `
|
|
20
|
-
* `
|
|
19
|
+
* `stages`/`source`/`author-list` beside `PIPELINE-STATUS.md` and `typography` beside
|
|
20
|
+
* `paper.md`/`paper.tex`. So a consumer block that named `paper/source`
|
|
21
21
|
* for every file under the papers directory reached `paper.tex`, where `paper` has no `source`
|
|
22
22
|
* — and ESLint threw `Could not find "source" in plugin "paper"` from inside `lintFiles`,
|
|
23
23
|
* after paperlint's own validation had (correctly) accepted the id. A block is therefore split
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperlint lint` over a paper whose body is in other files: the rules over ESLint's LaTeX text read
|
|
3
|
+
* every file `paper.tex` includes from its document body, report at that file's own path and line,
|
|
4
|
+
* and fix that file. What a rule decides for the whole paper — the majority reference form and
|
|
5
|
+
* whether the build is in review mode — it decides over all of the paper's files, not over the one
|
|
6
|
+
* it is reporting in.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
mkdirSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join, relative } from "node:path";
|
|
18
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
19
|
+
import { run } from "./cli.ts";
|
|
20
|
+
import { lintReport } from "../test/lint-report.ts";
|
|
21
|
+
|
|
22
|
+
const dirs: string[] = [];
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const doc = (cls: string, preamble: string, body: string): string =>
|
|
28
|
+
`\\documentclass${cls}\n${preamble}\n\\begin{document}\n${body}\n\\end{document}\n`;
|
|
29
|
+
|
|
30
|
+
const INTRO = "See §3 and p < .05.\nAs Fig.~\\ref{f3} shows.\n";
|
|
31
|
+
|
|
32
|
+
function project(files: Record<string, string>): string {
|
|
33
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-inc-")));
|
|
34
|
+
dirs.push(root);
|
|
35
|
+
const all: Record<string, string> = {
|
|
36
|
+
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
37
|
+
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
38
|
+
"papers/a/paper.tex": doc(
|
|
39
|
+
"{article}",
|
|
40
|
+
"\\input{macros}",
|
|
41
|
+
"Figure~\\ref{f1} and Figure~\\ref{f2}.\n\\input{sections/intro}",
|
|
42
|
+
),
|
|
43
|
+
// Prose in a preamble include is not the body: were it linted, `§2` would be reported.
|
|
44
|
+
"papers/a/macros.tex": "% See §2.\n\\newcommand{\\half}{0.5}\nSee §2.\n",
|
|
45
|
+
"papers/a/sections/intro.tex": INTRO,
|
|
46
|
+
// A frozen version beside the paper is not the paper.
|
|
47
|
+
"papers/a/versions/2026-01-01-submitted.tex": doc(
|
|
48
|
+
"{article}",
|
|
49
|
+
"",
|
|
50
|
+
"See §9.",
|
|
51
|
+
),
|
|
52
|
+
...files,
|
|
53
|
+
};
|
|
54
|
+
for (const [p, text] of Object.entries(all)) {
|
|
55
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
56
|
+
writeFileSync(join(root, p), text);
|
|
57
|
+
}
|
|
58
|
+
return root;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `paperlint lint --json` → rule ids per linted file (relative to the project), and stderr. */
|
|
62
|
+
async function lint(
|
|
63
|
+
root: string,
|
|
64
|
+
args: string[] = [],
|
|
65
|
+
): Promise<{ byFile: Record<string, string[]>; err: string }> {
|
|
66
|
+
const out: string[] = [];
|
|
67
|
+
const err: string[] = [];
|
|
68
|
+
await run(["lint", "--json", ...args], {
|
|
69
|
+
cwd: root,
|
|
70
|
+
log: (s: string) => out.push(s),
|
|
71
|
+
err: (s: string) => err.push(s),
|
|
72
|
+
});
|
|
73
|
+
const byFile = Object.fromEntries(
|
|
74
|
+
lintReport(out.join("\n")).map((r) => [
|
|
75
|
+
relative(root, r.filePath),
|
|
76
|
+
r.messages.map((m) => m.ruleId ?? "(fatal)").sort(),
|
|
77
|
+
]),
|
|
78
|
+
);
|
|
79
|
+
return { byFile, err: err.join("\n") };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("lint reads the files paper.tex includes from its body", () => {
|
|
83
|
+
it("🔴 findings in an included file land at that file; the majority form is the paper's", async () => {
|
|
84
|
+
const { byFile } = await lint(project({}));
|
|
85
|
+
expect(byFile["papers/a/sections/intro.tex"]).toEqual([
|
|
86
|
+
"paper/figure-ref-style",
|
|
87
|
+
"paper/leading-zero",
|
|
88
|
+
"paper/section-word",
|
|
89
|
+
]);
|
|
90
|
+
// Two `Figure` in paper.tex against one `Fig.` in the section: the section holds the minority.
|
|
91
|
+
expect(byFile["papers/a/paper.tex"]).not.toContain(
|
|
92
|
+
"paper/figure-ref-style",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("a preamble include and a frozen version beside the paper are not linted", async () => {
|
|
97
|
+
const { byFile } = await lint(project({}));
|
|
98
|
+
expect(Object.keys(byFile).sort()).toEqual([
|
|
99
|
+
"papers/a/PIPELINE-STATUS.md",
|
|
100
|
+
"papers/a/paper.tex",
|
|
101
|
+
"papers/a/sections/intro.tex",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("--fix rewrites the included file, to the paper's majority form", async () => {
|
|
106
|
+
const root = project({});
|
|
107
|
+
const before = readFileSync(join(root, "papers/a/paper.tex"), "utf8");
|
|
108
|
+
const { byFile } = await lint(root, ["--fix"]);
|
|
109
|
+
expect(byFile["papers/a/sections/intro.tex"]).toEqual([]);
|
|
110
|
+
expect(
|
|
111
|
+
readFileSync(join(root, "papers/a/sections/intro.tex"), "utf8"),
|
|
112
|
+
).toBe("See Section 3 and p < 0.05.\nAs Figure~\\ref{f3} shows.\n");
|
|
113
|
+
expect(readFileSync(join(root, "papers/a/paper.tex"), "utf8")).toBe(before);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("what lint decides for the whole paper, it decides over every file of it", () => {
|
|
118
|
+
it("tex/future-promise in an included file follows the build mode paper.tex declares", async () => {
|
|
119
|
+
const promise = {
|
|
120
|
+
"papers/a/sections/intro.tex":
|
|
121
|
+
"The harness will be released at camera-ready.\n",
|
|
122
|
+
};
|
|
123
|
+
const final = await lint(project(promise));
|
|
124
|
+
expect(final.byFile["papers/a/sections/intro.tex"]).toEqual([
|
|
125
|
+
"tex/future-promise",
|
|
126
|
+
]);
|
|
127
|
+
const review = await lint(
|
|
128
|
+
project({
|
|
129
|
+
...promise,
|
|
130
|
+
"papers/a/paper.tex": doc(
|
|
131
|
+
"[review]{article}",
|
|
132
|
+
"",
|
|
133
|
+
"Text.\n\\input{sections/intro}",
|
|
134
|
+
),
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
expect(review.byFile["papers/a/sections/intro.tex"]).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("an include that resolves nowhere is named as a file lint did not read", async () => {
|
|
141
|
+
const { err } = await lint(
|
|
142
|
+
project({
|
|
143
|
+
"papers/a/paper.tex": doc("{article}", "", "\\input{sections/gone}"),
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
expect(err).toMatch(
|
|
147
|
+
/papers\/a\/paper\.tex: lint did not read `sections\/gone`/,
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("the note is about the papers this run lints, not another paper of the project", async () => {
|
|
152
|
+
const root = project({
|
|
153
|
+
"papers/b/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
154
|
+
"papers/b/paper.tex": doc("{article}", "", "\\input{gone}"),
|
|
155
|
+
});
|
|
156
|
+
expect((await lint(root, ["papers/a"])).err).not.toMatch(/did not read/);
|
|
157
|
+
expect((await lint(root)).err).toMatch(
|
|
158
|
+
/papers\/b\/paper\.tex: lint did not read `gone`/,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
});
|