paperlint 4.0.0 → 4.2.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/.github/workflows/ci.yml +16 -23
- package/CLAUDE.md +55 -11
- package/CONTRIBUTING.md +30 -7
- package/README.md +11 -8
- package/dist/adapters/banal/invocation.d.ts +1 -1
- package/dist/adapters/latex/document-class.d.ts +23 -0
- package/dist/adapters/latex/document-class.d.ts.map +1 -0
- package/dist/adapters/latex/document-class.js +110 -0
- package/dist/adapters/latex/document-class.js.map +1 -0
- package/dist/adapters/latex/hidden.d.ts +16 -0
- package/dist/adapters/latex/hidden.d.ts.map +1 -0
- package/dist/adapters/latex/hidden.js +49 -0
- package/dist/adapters/latex/hidden.js.map +1 -0
- package/dist/adapters/latex/includes.d.ts +12 -0
- package/dist/adapters/latex/includes.d.ts.map +1 -0
- package/dist/adapters/latex/includes.js +42 -0
- package/dist/adapters/latex/includes.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +33 -0
- package/dist/adapters/latex/index.d.ts.map +1 -0
- package/dist/adapters/latex/index.js +25 -0
- package/dist/adapters/latex/index.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts +58 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -0
- package/dist/adapters/latex/nodes.js +97 -0
- package/dist/adapters/latex/nodes.js.map +1 -0
- package/dist/adapters/latex/outline.d.ts +10 -0
- package/dist/adapters/latex/outline.d.ts.map +1 -0
- package/dist/adapters/latex/outline.js +39 -0
- package/dist/adapters/latex/outline.js.map +1 -0
- package/dist/adapters/latex/parse.d.ts +42 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -0
- package/dist/adapters/latex/parse.js +95 -0
- package/dist/adapters/latex/parse.js.map +1 -0
- package/dist/adapters/latex/prose.d.ts +8 -0
- package/dist/adapters/latex/prose.d.ts.map +1 -0
- package/dist/adapters/latex/prose.js +353 -0
- package/dist/adapters/latex/prose.js.map +1 -0
- package/dist/adapters/latex/rendered.d.ts +10 -0
- package/dist/adapters/latex/rendered.d.ts.map +1 -0
- package/dist/adapters/latex/rendered.js +39 -0
- package/dist/adapters/latex/rendered.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +12 -12
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/build.js +4 -4
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +34 -0
- package/dist/claim-provenance.d.ts.map +1 -0
- package/dist/claim-provenance.js +146 -0
- package/dist/claim-provenance.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +49 -5
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +58 -0
- package/dist/domain/paper-source.d.ts.map +1 -0
- package/dist/domain/paper-source.js +109 -0
- package/dist/domain/paper-source.js.map +1 -0
- package/dist/domain/sentences.d.ts +26 -0
- package/dist/domain/sentences.d.ts.map +1 -0
- package/dist/domain/sentences.js +48 -0
- package/dist/domain/sentences.js.map +1 -0
- package/dist/domain/tex-document.d.ts +119 -0
- package/dist/domain/tex-document.d.ts.map +1 -0
- package/dist/domain/tex-document.js +43 -0
- package/dist/domain/tex-document.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +7 -0
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +53 -1
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +2 -2
- package/dist/facts-file.js.map +1 -1
- package/dist/new-paper.d.ts +5 -0
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +3 -0
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts +5 -0
- package/dist/package-dirs.d.ts.map +1 -0
- package/dist/package-dirs.js +14 -0
- package/dist/package-dirs.js.map +1 -0
- package/dist/ports/latex.d.ts +35 -0
- package/dist/ports/latex.d.ts.map +1 -0
- package/dist/ports/latex.js +2 -0
- package/dist/ports/latex.js.map +1 -0
- package/dist/presets.d.ts +15 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -6
- package/dist/presets.js.map +1 -1
- package/dist/register.d.ts +40 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +134 -0
- package/dist/register.js.map +1 -0
- package/dist/tex-paper.d.ts +19 -0
- package/dist/tex-paper.d.ts.map +1 -0
- package/dist/tex-paper.js +105 -0
- package/dist/tex-paper.js.map +1 -0
- package/dist/tex-requirements.d.ts +11 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +14 -8
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +88 -0
- package/dist/tex-venue-rules.d.ts.map +1 -0
- package/dist/tex-venue-rules.js +268 -0
- package/dist/tex-venue-rules.js.map +1 -0
- package/dist/venue-rules.d.ts +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/docs/configuration.md +3 -2
- package/docs/e2e.md +69 -40
- package/docs/install.md +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/package-location.md +5 -5
- package/docs/prior-art/repro/README.md +1 -1
- package/docs/prior-art/test-tooling.md +21 -10
- package/docs/rules/tex/claim-provenance.md +83 -0
- package/docs/rules/tex/missing-input.md +58 -0
- package/docs/rules/tex/register.md +77 -0
- package/docs/rules/tex/required-section.md +75 -0
- package/docs/rules/tex/template.md +74 -0
- package/docs/rules/tex/venue-leftover.md +73 -0
- package/docs/rules.md +73 -48
- package/docs/testing.md +17 -4
- package/docs/texlive-install-decision.md +1 -1
- package/docs/toolchain.md +2 -2
- package/eslint-rules/paper-typography.test.mjs +48 -1
- package/eslint-rules/paper-typography.ts +72 -1
- package/eslint.config.mjs +156 -15
- package/fixtures/build-e2e/acmart/expect.json +26 -0
- package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/aidc/expect.json +14 -0
- package/fixtures/build-e2e/aidc/paper.tex +46 -0
- package/fixtures/build-e2e/aidc/paperlint.json +1 -0
- package/fixtures/build-e2e/broken/expect.json +11 -0
- package/fixtures/build-e2e/cite/expect.json +27 -0
- package/fixtures/build-e2e/empty/expect.json +12 -0
- package/fixtures/build-e2e/fallback/expect.json +53 -0
- package/fixtures/build-e2e/guards/expect.json +6 -0
- package/fixtures/build-e2e/no-source/expect.json +10 -0
- package/fixtures/build-e2e/unbalanced/expect.json +60 -0
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.ts +8 -7
- package/fixtures/render-paper/build-clean.sh +3 -5
- package/package.json +14 -2
- package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
- package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
- package/presets/aidc.jsonc +73 -0
- package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
- package/presets/ieee-conference.jsonc +66 -0
- package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
- package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
- package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
- package/scripts/check.harness.ts +64 -10
- package/scripts/check.test.ts +60 -13
- package/scripts/check.ts +83 -30
- package/scripts/test.test.ts +1 -1
- package/scripts/test.ts +1 -1
- package/skills/cold-read-diff/SKILL.md +31 -10
- package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
- package/skills/draft-paper/SKILL.md +2 -2
- package/skills/draft-paper/SKILL.md.spec.ts +1 -1
- package/skills/grade-paper-writing/SKILL.md +109 -44
- package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
- package/skills/harden-paper/SKILL.md +35 -36
- package/skills/harden-paper/SKILL.md.spec.ts +34 -35
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
- package/skills/paper-pipeline/SKILL.md +4 -4
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -3
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
- package/skills/paper-pipeline/references/writing-craft.md +53 -18
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
- package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
- package/skills/pc-panel-review/SKILL.md +36 -11
- package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/check-render.sh +16 -63
- package/skills/render-paper/render-paper.harness.mjs +17 -59
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.ts +1 -1
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +1 -1
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/latex/document-class.ts +155 -0
- package/src/adapters/latex/hidden.ts +70 -0
- package/src/adapters/latex/includes.ts +66 -0
- package/src/adapters/latex/index.test.ts +535 -0
- package/src/adapters/latex/index.ts +65 -0
- package/src/adapters/latex/nodes.ts +150 -0
- package/src/adapters/latex/outline.ts +64 -0
- package/src/adapters/latex/parse.ts +137 -0
- package/src/adapters/latex/prose.ts +484 -0
- package/src/adapters/latex/rendered.ts +59 -0
- package/src/adapters/pdfjs/fill.test.ts +1 -1
- package/src/adapters/pdfjs/fill.ts +18 -10
- package/src/build.harness.mjs +5 -7
- package/src/build.ts +4 -4
- package/src/claim-provenance.test.ts +352 -0
- package/src/claim-provenance.ts +217 -0
- package/src/cli.harness.mjs +1 -1
- package/src/cli.test.ts +2 -8
- package/src/cli.ts +53 -8
- package/src/domain/paper-source.test.ts +156 -0
- package/src/domain/paper-source.ts +233 -0
- package/src/domain/sentences.ts +67 -0
- package/src/domain/tex-document.test.ts +80 -0
- package/src/domain/tex-document.ts +143 -0
- package/src/engine.harness.mjs +1 -1
- package/src/facts-file.test.ts +4 -7
- package/src/facts-file.ts +2 -2
- package/src/latex-loop.harness.ts +1 -1
- package/src/new-paper.test.ts +28 -3
- package/src/new-paper.ts +8 -0
- package/src/package-dirs.ts +17 -0
- package/src/paper-settings-commands.test.ts +6 -1
- package/src/ports/latex.ts +42 -0
- package/src/presets.test.ts +65 -4
- package/src/presets.ts +38 -15
- package/src/register.test.ts +165 -0
- package/src/register.ts +177 -0
- package/src/rule-cases.test.ts +524 -0
- package/src/rule-docs.test.ts +159 -0
- package/src/tex-paper.test.ts +131 -0
- package/src/tex-paper.ts +152 -0
- package/src/tex-requirements.harness.ts +13 -23
- package/src/tex-requirements.ts +35 -8
- package/src/tex-venue-rules.test.ts +741 -0
- package/src/tex-venue-rules.ts +461 -0
- package/src/toolchain.harness.mjs +1 -1
- package/src/venue-rules.test.ts +96 -4
- package/src/venue-rules.ts +1 -1
- package/templates/paper/paper.tex +2 -1
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
- /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
- /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tex/claim-provenance` on real parses: every sentence of the body that states a number is
|
|
3
|
+
* reported once unless it says whose the number is — a citation or a link, the authors (we, our),
|
|
4
|
+
* a place in the paper (`\ref`, "Section 3", "Table 2"), or the sample it counts ("n = 134",
|
|
5
|
+
* "48 runs"). Both halves for every kind of owner, and the places a number is not the body's.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { latexReader } from "./adapters/latex/index.ts";
|
|
9
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
10
|
+
import {
|
|
11
|
+
claimProvenanceRule,
|
|
12
|
+
judgeClaimProvenance,
|
|
13
|
+
} from "./claim-provenance.ts";
|
|
14
|
+
|
|
15
|
+
const doc = (body: string): string =>
|
|
16
|
+
`\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
|
|
17
|
+
|
|
18
|
+
/** The source text of every sentence the rule reports, in order. */
|
|
19
|
+
const reported = (body: string): readonly string[] => {
|
|
20
|
+
const src = doc(body);
|
|
21
|
+
return judgeClaimProvenance(latexReader.bodyProse(src)).map((f) =>
|
|
22
|
+
f.at === null ? "<unplaced>" : src.slice(f.at.start, f.at.end),
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("a sentence with a number and no owner is reported, once, as its whole source", () => {
|
|
27
|
+
it("a percentage", () => {
|
|
28
|
+
expect(
|
|
29
|
+
reported("Agents ignore 42\\% of the rules they are given."),
|
|
30
|
+
).toEqual(["Agents ignore 42\\% of the rules they are given."]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("a decimal, a multiplier and a count with no noun after it — each sentence once", () => {
|
|
34
|
+
expect(
|
|
35
|
+
reported(
|
|
36
|
+
"The rate rose from 0.37 to 2.1. It runs 3x faster. Violations went from 12 to 40.",
|
|
37
|
+
),
|
|
38
|
+
).toEqual([
|
|
39
|
+
"The rate rose from 0.37 to 2.1.",
|
|
40
|
+
"It runs 3x faster.",
|
|
41
|
+
"Violations went from 12 to 40.",
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("only the sentence without an owner, in a passage that has both", () => {
|
|
46
|
+
expect(
|
|
47
|
+
reported(
|
|
48
|
+
"We measured 48 runs. Under test the rules held in 97\\% of cases. See~\\cite{a}.",
|
|
49
|
+
),
|
|
50
|
+
).toEqual(["Under test the rules held in 97\\% of cases."]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("🔴 a count's modifiers are words, not «of the»: «2 of the guards» is not a sample", () => {
|
|
54
|
+
expect(reported("It held for 2 of the guards.")).toEqual([
|
|
55
|
+
"It held for 2 of the guards.",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("🔴 a name before a number is skipped only mid-sentence: a sentence-initial word is not a name", () => {
|
|
60
|
+
expect(
|
|
61
|
+
reported(
|
|
62
|
+
"Only 2 of the guards hold. It runs on Claude 3 and fails 40\\%.",
|
|
63
|
+
),
|
|
64
|
+
).toEqual([
|
|
65
|
+
"Only 2 of the guards hold.",
|
|
66
|
+
"It runs on Claude 3 and fails 40\\%.",
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("the numbers the rule skips do not hide their neighbours", () => {
|
|
72
|
+
it("🔴 a number after an enumerator, or outside the quotation, is still read", () => {
|
|
73
|
+
expect(
|
|
74
|
+
reported(
|
|
75
|
+
"It rose to 2. The ``safe'' share fell by 30\\%. A high share (81.15\\%) holds.",
|
|
76
|
+
),
|
|
77
|
+
).toEqual([
|
|
78
|
+
"It rose to 2.",
|
|
79
|
+
"The ``safe'' share fell by 30\\%.",
|
|
80
|
+
"A high share (81.15\\%) holds.",
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("🔴 «US» is a country, not the authors", () => {
|
|
85
|
+
expect(reported("US agencies report 40\\% growth.")).toEqual([
|
|
86
|
+
"US agencies report 40\\% growth.",
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("a number inside \\emph is still the sentence's", () => {
|
|
91
|
+
expect(
|
|
92
|
+
reported("A model changes its mind on \\emph{21.5\\%} of rows."),
|
|
93
|
+
).toEqual(["A model changes its mind on \\emph{21.5\\%} of rows."]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("where the reported sentence's text comes from", () => {
|
|
98
|
+
it("the text of a formatting macro the parser has no signature for, and of a bare group", () => {
|
|
99
|
+
expect(
|
|
100
|
+
reported("\\enquote{It fails 40\\%.} {\\bfseries It holds 60\\%.}"),
|
|
101
|
+
).toEqual(["It fails 40\\%.", "It holds 60\\%."]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("a source with no document environment is read whole", () => {
|
|
105
|
+
const src = "It fails 40\\%.";
|
|
106
|
+
expect(
|
|
107
|
+
judgeClaimProvenance(latexReader.bodyProse(src)).map((f) => f.at),
|
|
108
|
+
).toEqual([{ start: 0, end: src.length }]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("the abstract is body prose", () => {
|
|
112
|
+
expect(
|
|
113
|
+
reported(
|
|
114
|
+
"\\begin{abstract}A frontier model flips on 21.5\\% of rows.\\end{abstract}",
|
|
115
|
+
),
|
|
116
|
+
).toEqual(["A frontier model flips on 21.5\\% of rows."]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("a footnote is its own passage", () => {
|
|
120
|
+
expect(
|
|
121
|
+
reported("Text.\\footnote{The cache hit 93\\% of lookups.}"),
|
|
122
|
+
).toEqual(["The cache hit 93\\% of lookups."]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("🔴 a sample named in math owns only its own sentence", () => {
|
|
126
|
+
expect(reported("We set $n=3$. It fails 12\\%. Then $n=5$ held.")).toEqual([
|
|
127
|
+
"It fails 12\\%.",
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("the sentence ends at a paragraph break", () => {
|
|
132
|
+
expect(reported("Rates rose by 12\\%\n\nand we then stopped.")).toEqual([
|
|
133
|
+
"Rates rose by 12\\%",
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("an owner silences the sentence", () => {
|
|
139
|
+
it.each([
|
|
140
|
+
["a citation", "Hooks block 2 of 7 commands~\\cite{x}."],
|
|
141
|
+
[
|
|
142
|
+
"a citation after the full stop",
|
|
143
|
+
"It grew 900\\%.~\\cite{x} Then it fell.",
|
|
144
|
+
],
|
|
145
|
+
["a citation opening the passage", "\\cite{x} report 40\\%."],
|
|
146
|
+
[
|
|
147
|
+
"a citation's note is still a citation",
|
|
148
|
+
"It fails 30\\%~\\citep[p.~4]{x}.",
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
"a link",
|
|
152
|
+
"The registry lists 900\\% growth (\\url{https://example.org}).",
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
"a link with its text",
|
|
156
|
+
"The registry grew 900\\% (\\href{https://e.org}{see})..",
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
"a footnote whose own footnote links",
|
|
160
|
+
"The registry grew 900\\%.\\footnote{Seen\\footnote{\\url{https://e.org}} there.}",
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"a footnote that links",
|
|
164
|
+
"The registry grew 900\\%.\\footnote{\\url{https://e.org}}",
|
|
165
|
+
],
|
|
166
|
+
])("%s", (_, body) => {
|
|
167
|
+
expect(reported(body)).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("an owner in words silences the sentence", () => {
|
|
172
|
+
it.each([
|
|
173
|
+
["we", "We find a 6\\% reduction."],
|
|
174
|
+
["our", "Our census found 12\\% of files empty."],
|
|
175
|
+
["a \\ref", "The rate is 12\\% (Table~\\ref{t:rates})."],
|
|
176
|
+
["a \\cref", "\\Cref{s:x} puts it at 12\\%."],
|
|
177
|
+
["Section N", "Section 4 puts the rate at 12\\%."],
|
|
178
|
+
["Table N", "The rate is 12\\% (see Table 2)."],
|
|
179
|
+
["Fig. N, across its full stop", "As Fig. 3 shows, the rate is 12\\%."],
|
|
180
|
+
["Appendix X", "Appendix B gives the rate as 12\\%."],
|
|
181
|
+
["§N", "The rate is 12\\% (\\S 4)."],
|
|
182
|
+
["n = 134", "The rate is 12\\% (n = 134)."],
|
|
183
|
+
["$n=134$ in math", "The rate is 12\\% ($n=134$)."],
|
|
184
|
+
["$n{=}7$ in math, braces and all", "The rate is 12\\% ($n{=}7$)."],
|
|
185
|
+
[
|
|
186
|
+
"a claimant named as one",
|
|
187
|
+
"The skill advertises a 65\\% token reduction.",
|
|
188
|
+
],
|
|
189
|
+
["a claim in brackets", "The second tool (claim 63\\%) runs slower."],
|
|
190
|
+
["a count of things", "The rule held in 48 runs."],
|
|
191
|
+
["a count of times", "It is 3 times faster."],
|
|
192
|
+
["a count with an adjective", "The rule held in 48 independent runs."],
|
|
193
|
+
[
|
|
194
|
+
"a count with a capitalised modifier",
|
|
195
|
+
"Each dataset contains 466 Boolean questions.",
|
|
196
|
+
],
|
|
197
|
+
[
|
|
198
|
+
"a count with two modifiers",
|
|
199
|
+
"GPT was evaluated on 900 systematically collected questions.",
|
|
200
|
+
],
|
|
201
|
+
["of N things", "Of 1,836 repositories, 12\\% ship hooks."],
|
|
202
|
+
[
|
|
203
|
+
"e.g. does not end the sentence",
|
|
204
|
+
"Some fail, e.g. Table 2 has 12\\% of them.",
|
|
205
|
+
],
|
|
206
|
+
])("%s", (_, body) => {
|
|
207
|
+
expect(reported(body)).toEqual([]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("a number that is not the body's claim is not read", () => {
|
|
212
|
+
it.each([
|
|
213
|
+
["a year", "In 2024 the first agents shipped."],
|
|
214
|
+
[
|
|
215
|
+
"a digit that belongs to a product or model name",
|
|
216
|
+
"It runs on Claude 3 and Python 3.12, beside GPT-4o and Llama-3-8B.",
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
"an inline list's enumerators",
|
|
220
|
+
"The weaknesses are: 1. keeping up with guidance, 2. judging tools, and (3) time.",
|
|
221
|
+
],
|
|
222
|
+
[
|
|
223
|
+
"a number inside a quotation",
|
|
224
|
+
"Such as ``Will 2 factor authentication protect me?''.",
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
"the level of an interval",
|
|
228
|
+
"It is the 95\\% CI of the median, and the 99\\% confidence band.",
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
"a name with a digit",
|
|
232
|
+
"GPT-4 and COVID-19 and v1.2 and 10k and 2FA and 0x1F appear.",
|
|
233
|
+
],
|
|
234
|
+
])("%s", (_, body) => {
|
|
235
|
+
expect(reported(body)).toEqual([]);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("markup that is not the body's prose is not read", () => {
|
|
240
|
+
it.each([
|
|
241
|
+
["math", "The bound $x \\le 3$ holds."],
|
|
242
|
+
["an equation", "\\begin{equation} x = 40 \\end{equation}"],
|
|
243
|
+
[
|
|
244
|
+
"a bibliography environment",
|
|
245
|
+
"Text.\n\\begin{thebibliography}{9}\\bibitem{a} It fails 40\\%.\\end{thebibliography}",
|
|
246
|
+
],
|
|
247
|
+
["the ACM classification", "\\ccsdesc[500]{Security~Software}Text."],
|
|
248
|
+
[
|
|
249
|
+
"its XML",
|
|
250
|
+
"\\begin{CCSXML}<concept_significance>500</concept_significance>\\end{CCSXML}",
|
|
251
|
+
],
|
|
252
|
+
[
|
|
253
|
+
"a table",
|
|
254
|
+
"\\begin{table}\\caption{Rates: 12\\%}\\begin{tabular}{l}40\\%\\end{tabular}\\end{table}",
|
|
255
|
+
],
|
|
256
|
+
["a figure", "\\begin{figure}\\caption{The 12\\% gap.}\\end{figure}"],
|
|
257
|
+
["a heading", "\\section{Results for 12 models}\nText."],
|
|
258
|
+
["a caption outside a float", "\\captionof{table}{Rates of 12\\%}"],
|
|
259
|
+
["code", "\\begin{lstlisting}x = 42\\end{lstlisting}"],
|
|
260
|
+
["a comment", "% 42 percent\nText."],
|
|
261
|
+
["the appendix", "Text.\n\\appendix\n\\section{More}\nIt fails 40\\%."],
|
|
262
|
+
["the bibliography", "Text.\n\\bibliography{refs}\nIt fails 40\\%."],
|
|
263
|
+
[
|
|
264
|
+
"the title block",
|
|
265
|
+
"\\title{Rules 2026: 12 findings}\\author{A}\\maketitle Text.",
|
|
266
|
+
],
|
|
267
|
+
[
|
|
268
|
+
"an unknown macro's arguments",
|
|
269
|
+
"\\affiliation{\\institution{Lab 42}}Text.",
|
|
270
|
+
],
|
|
271
|
+
["a macro definition", "\\newcommand{\\rate}{42\\%}Text."],
|
|
272
|
+
[
|
|
273
|
+
"a TeX conditional's argument glued to its macro",
|
|
274
|
+
"\\def\\conference{2}\n\\if\\conference1\n\\fi\n\\if\\conference2\nText.\n\\fi",
|
|
275
|
+
],
|
|
276
|
+
])("%s", (_, body) => {
|
|
277
|
+
expect(reported(body)).toEqual([]);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
/** What a rule reported, as ESLint would receive it: the line, and the id or the whole message. */
|
|
282
|
+
type Report =
|
|
283
|
+
| {
|
|
284
|
+
readonly line: number;
|
|
285
|
+
readonly messageId: string;
|
|
286
|
+
readonly data?: unknown;
|
|
287
|
+
}
|
|
288
|
+
| { readonly line: number; readonly message: string };
|
|
289
|
+
|
|
290
|
+
/** Run `rule` over `/p/paper.tex` holding `src`, the way ESLint runs it: `create`, then `root`. */
|
|
291
|
+
function run(
|
|
292
|
+
rule: ReturnType<typeof claimProvenanceRule>,
|
|
293
|
+
src: string,
|
|
294
|
+
raw = true,
|
|
295
|
+
): Report[] {
|
|
296
|
+
const reports: Report[] = [];
|
|
297
|
+
rule
|
|
298
|
+
.create({
|
|
299
|
+
filename: "/p/paper.tex",
|
|
300
|
+
sourceCode: {
|
|
301
|
+
text: src,
|
|
302
|
+
...(raw ? { raw: src } : {}),
|
|
303
|
+
getLocFromIndex: (i) => ({
|
|
304
|
+
line: src.slice(0, i).split("\n").length,
|
|
305
|
+
column: 0,
|
|
306
|
+
}),
|
|
307
|
+
},
|
|
308
|
+
report: (d) =>
|
|
309
|
+
reports.push(
|
|
310
|
+
"messageId" in d
|
|
311
|
+
? { line: d.loc.start.line, messageId: d.messageId, data: d.data }
|
|
312
|
+
: { line: d.loc.start.line, message: d.message },
|
|
313
|
+
),
|
|
314
|
+
})
|
|
315
|
+
.root?.();
|
|
316
|
+
return reports;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const onDisk = (files: Record<string, string> = {}) =>
|
|
320
|
+
claimProvenanceRule({ files: memoryFiles(files), latex: latexReader });
|
|
321
|
+
|
|
322
|
+
describe("the rule, as ESLint runs it", () => {
|
|
323
|
+
it("reports each finding at its lines, with the message naming the owners that would do", () => {
|
|
324
|
+
const rule = onDisk();
|
|
325
|
+
expect(run(rule, doc("Agents ignore 42\\% of rules."))).toEqual([
|
|
326
|
+
{ line: 3, messageId: "noOwner", data: { number: "42%" } },
|
|
327
|
+
]);
|
|
328
|
+
expect(rule.meta.messages["noOwner"]).toContain("{{number}}");
|
|
329
|
+
expect(rule.meta.docs.url).toBe(
|
|
330
|
+
"https://github.com/zernie/paperlint/blob/main/docs/rules/tex/claim-provenance.md",
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("reads `text` when the source code carries no `raw`", () => {
|
|
335
|
+
expect(run(onDisk(), doc("It fails 40\\%."), false)).toEqual([
|
|
336
|
+
{ line: 3, messageId: "noOwner", data: { number: "40%" } },
|
|
337
|
+
]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("🔴 reads the files the paper \\inputs: a finding there is reported at the \\input, naming its file and line", () => {
|
|
341
|
+
const rule = onDisk({
|
|
342
|
+
"/p/sections/results.tex":
|
|
343
|
+
"\\section{Results}\nIt holds.\nAgents ignore 42\\% of rules.\n",
|
|
344
|
+
});
|
|
345
|
+
expect(run(rule, doc("Intro.\n\\input{sections/results}"))).toEqual([
|
|
346
|
+
{
|
|
347
|
+
line: 4,
|
|
348
|
+
message: `sections/results.tex:3:1: ${String(rule.meta.messages["noOwner"]).replace("{{number}}", "42%")}`,
|
|
349
|
+
},
|
|
350
|
+
]);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tex/claim-provenance` — a sentence of the body that states a number says whose the number is.
|
|
3
|
+
*
|
|
4
|
+
* warn a sentence with a number and none of: a citation or a link, the authors (we, our, us), a
|
|
5
|
+
* place in the paper (`\ref` and its kin, "Section 3", "Table 2", "Appendix B", "§4"), a
|
|
6
|
+
* claimant named as one ("advertises 65%", "(claim 63%)"), or the sample it counts
|
|
7
|
+
* ("n = 134", "48 runs", "of 1,836 repositories")
|
|
8
|
+
*
|
|
9
|
+
* ── WHY ──────────────────────────────────────────────────────────────────────────
|
|
10
|
+
* A reviewer who cannot tell a property of the world from a cited finding from the authors' own
|
|
11
|
+
* measurement writes exactly that, and it costs the paper its soundness score. Standard academic
|
|
12
|
+
* prose carries the owner by default ("we found", "[12] report"); prose pushed toward a punchy
|
|
13
|
+
* register drops it first. The owner is a fact about the sentence, so a rule can ask for it.
|
|
14
|
+
*
|
|
15
|
+
* ── WHAT IT READS ────────────────────────────────────────────────────────────────
|
|
16
|
+
* The body's prose through the `LatexReader` port (`bodyProse`): the parse tree of `paper.tex`,
|
|
17
|
+
* never a search of its source. Citations, links and cross-references arrive as marks, math as math.
|
|
18
|
+
* The only text matched here is the rendered prose of one sentence — one lexeme at a time: a number,
|
|
19
|
+
* a pronoun, a word such as "Table" followed by its number, a count followed by what it counts.
|
|
20
|
+
*
|
|
21
|
+
* ── WHAT IT CANNOT KNOW ──────────────────────────────────────────────────────────
|
|
22
|
+
* Whether an owner is the RIGHT one (a citation that does not say the number, a "we" in a sentence
|
|
23
|
+
* about someone else) or what a custom macro prints. A year alone (1900–2099) is read as a date; a
|
|
24
|
+
* digit glued to a letter or a hyphen ("GPT-4", "10k"), or right after a capitalised word inside the
|
|
25
|
+
* sentence ("Claude 3", "Python 3.12"), as part of a name. Warn, never error: it asks the question; the author answers it.
|
|
26
|
+
*/
|
|
27
|
+
import {
|
|
28
|
+
runText,
|
|
29
|
+
spanIn,
|
|
30
|
+
type Passage,
|
|
31
|
+
type ProsePiece,
|
|
32
|
+
} from "./domain/tex-document.ts";
|
|
33
|
+
import {
|
|
34
|
+
CITATION,
|
|
35
|
+
REFERENCE,
|
|
36
|
+
runOf,
|
|
37
|
+
segmentOf,
|
|
38
|
+
sentences,
|
|
39
|
+
} from "./domain/sentences.ts";
|
|
40
|
+
import { readPaper, reportInPaper, type PaperDeps } from "./tex-paper.ts";
|
|
41
|
+
import {
|
|
42
|
+
ruleDocsUrl,
|
|
43
|
+
type Located,
|
|
44
|
+
type TexRuleModule,
|
|
45
|
+
} from "./tex-venue-rules.ts";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A number a reader sees: digits not glued to a letter, a hyphen or a point before them (a name, a
|
|
49
|
+
* version, a decimal without its zero), with an optional decimal part and a `%` or `×` after them,
|
|
50
|
+
* and no letter or digit after that.
|
|
51
|
+
*/
|
|
52
|
+
const NUMBER =
|
|
53
|
+
/(?<![\p{L}\p{N}_.,-])(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?(?:\s?%|×|x)?(?![\p{L}\p{N}])/gu;
|
|
54
|
+
|
|
55
|
+
/** A plain year, read as a date rather than a quantity. */
|
|
56
|
+
const YEAR = /^(?:19|20)\d\d$/u;
|
|
57
|
+
|
|
58
|
+
const FIRST_PERSON = /(?<!\p{L})(?:we|our|ours|us|ourselves)(?!\p{L})/giu;
|
|
59
|
+
|
|
60
|
+
/** A place in the paper named in words: "Section 4", "Table 2", "Fig. 3", "Appendix B", "§4". */
|
|
61
|
+
const PLACE =
|
|
62
|
+
/(?<!\p{L})(?:Sections?|Sec\.|§|Tables?|Tab\.|Figures?|Figs?\.|Appendix|Appendices|App\.|Listings?|Algorithms?|Equations?|Eqs?\.|Chapters?)\s*(?:\d|[A-Z](?!\p{L}))/u;
|
|
63
|
+
|
|
64
|
+
/** "n = 134", in prose or in math. */
|
|
65
|
+
const SAMPLE_SIZE = /(?<![\p{L}\\])[nN]\s*=\s*\d/u;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A count followed by what it counts, with up to two modifiers that are not function words: "48
|
|
69
|
+
* runs", "1,836 repositories", "466 Boolean questions", "900 systematically collected questions" —
|
|
70
|
+
* and not "2 of the guards".
|
|
71
|
+
*/
|
|
72
|
+
const COUNT =
|
|
73
|
+
/(?<![\p{L}\p{N}.,])(?:\d{1,3}(?:,\d{3})+|\d+)(?![.,]?\d)\s+(?:(?!(?:of|the|and|or|to|in|for|on|at|by|a|an|with|from|than|as)\s)[\p{L}-]+\s+){0,2}\p{Ll}+s(?!\p{L})/gu;
|
|
74
|
+
|
|
75
|
+
/** A quotation (`` … '' or “ … ”): an example the paper quotes, not a claim it makes. */
|
|
76
|
+
const QUOTED = /``[^']*?''|“[^”]*?”/gu;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* An inline list's enumerator: a whole number standing after the start, `:`, `,`, `;` or `(`, and
|
|
80
|
+
* closed by `.` or `)` — "are: 1. the challenge …, 2. …", "(3)"; not "(81.15%)".
|
|
81
|
+
*/
|
|
82
|
+
const isEnumerator = (s: string, at: number, n: string): boolean =>
|
|
83
|
+
/^\d+$/u.test(n) &&
|
|
84
|
+
/^[.)](?:\s|$)/u.test(s.slice(at + n.length)) &&
|
|
85
|
+
/(?:^|[:,;(])\s*$/u.test(s.slice(0, at));
|
|
86
|
+
|
|
87
|
+
/** The first number in `s` that is a quantity, or undefined. */
|
|
88
|
+
const quantityIn = (s: string): string | undefined => {
|
|
89
|
+
const unquoted = s.replace(QUOTED, (q) => " ".repeat(q.length));
|
|
90
|
+
return [...unquoted.matchAll(NUMBER)]
|
|
91
|
+
.filter(
|
|
92
|
+
(m) =>
|
|
93
|
+
!CONFIDENCE.test(unquoted.slice(m.index + m[0].length)) &&
|
|
94
|
+
!isEnumerator(unquoted, m.index, m[0]) &&
|
|
95
|
+
!(isNamed(unquoted, m.index) && !m[0].endsWith("%")),
|
|
96
|
+
)
|
|
97
|
+
.map((m) => m[0])
|
|
98
|
+
.find((n) => !YEAR.test(n));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A capitalised word right before the number, and not the sentence's first word: the number is part
|
|
103
|
+
* of a name — "Claude 3", "Python 3.12", "Node 22" — not a quantity. A sentence-initial word ("Only 2
|
|
104
|
+
* of 46") is capitalised because it opens the sentence, and says nothing.
|
|
105
|
+
*/
|
|
106
|
+
const NAMED_BY = /\S\s+\p{Lu}[\p{L}\p{N}.+-]*\s$/u;
|
|
107
|
+
|
|
108
|
+
/** Whether the number at `at` in the sentence `s` is part of a name (`NAMED_BY`). */
|
|
109
|
+
const isNamed = (s: string, at: number): boolean =>
|
|
110
|
+
NAMED_BY.test(s.slice(s.length - s.trimStart().length, at));
|
|
111
|
+
|
|
112
|
+
/** What follows the level of an interval ("95% CI"): the level is not a result. */
|
|
113
|
+
const CONFIDENCE = /^\s*(?:CI|confidence)(?!\p{L})/u;
|
|
114
|
+
|
|
115
|
+
/** A claimant named as such: "advertises a 65% reduction", "(claim 63%)". */
|
|
116
|
+
const ATTRIBUTED =
|
|
117
|
+
/(?<!\p{L})(?:advertis(?:e|es|ed|ing)|claim(?:s|ed)?)(?!\p{L})/iu;
|
|
118
|
+
|
|
119
|
+
/** Whether a sentence says whose its number is. `maths` is the source of its math. */
|
|
120
|
+
const isOwned = (s: string, maths: readonly string[]): boolean =>
|
|
121
|
+
s.includes(CITATION) ||
|
|
122
|
+
s.includes(REFERENCE) ||
|
|
123
|
+
[...s.matchAll(FIRST_PERSON)].some((m) => m[0] !== "US") ||
|
|
124
|
+
ATTRIBUTED.test(s) ||
|
|
125
|
+
PLACE.test(s) ||
|
|
126
|
+
SAMPLE_SIZE.test(s) ||
|
|
127
|
+
// "Claude 3 and fails" is a name and a verb, not a count.
|
|
128
|
+
[...s.matchAll(COUNT)].some((m) => !isNamed(s, m.index)) ||
|
|
129
|
+
// `$n{=}7$` and `$n = 7$` alike: braces are grouping in math, not text.
|
|
130
|
+
maths.some((m) => SAMPLE_SIZE.test(m.replace(/[{}]/gu, "")));
|
|
131
|
+
|
|
132
|
+
/** Each piece of a passage with the offset where it starts in the passage's text. */
|
|
133
|
+
const laidOut = (
|
|
134
|
+
p: Passage,
|
|
135
|
+
): readonly { readonly from: number; readonly piece: ProsePiece }[] =>
|
|
136
|
+
p.pieces.reduce<{
|
|
137
|
+
readonly laid: readonly {
|
|
138
|
+
readonly from: number;
|
|
139
|
+
readonly piece: ProsePiece;
|
|
140
|
+
}[];
|
|
141
|
+
readonly next: number;
|
|
142
|
+
}>(
|
|
143
|
+
(acc, piece) => ({
|
|
144
|
+
laid: [...acc.laid, { from: acc.next, piece }],
|
|
145
|
+
next: acc.next + segmentOf(piece).text.length,
|
|
146
|
+
}),
|
|
147
|
+
{ laid: [], next: 0 },
|
|
148
|
+
).laid;
|
|
149
|
+
|
|
150
|
+
/** The source of the math that starts inside `[from, to)` of the passage's text. */
|
|
151
|
+
const mathsIn = (
|
|
152
|
+
laid: ReturnType<typeof laidOut>,
|
|
153
|
+
from: number,
|
|
154
|
+
to: number,
|
|
155
|
+
): readonly string[] =>
|
|
156
|
+
laid.flatMap(({ from: at, piece }) =>
|
|
157
|
+
piece.kind === "math" && at >= from && at < to ? [piece.tex] : [],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
/** Every unowned sentence of one passage that states a number, spanning its source. */
|
|
161
|
+
function judgePassage(p: Passage): readonly Located[] {
|
|
162
|
+
const run = runOf(p);
|
|
163
|
+
const text = runText(run);
|
|
164
|
+
const laid = laidOut(p);
|
|
165
|
+
return sentences(text).flatMap(([from, to]) => {
|
|
166
|
+
const s = text.slice(from, to);
|
|
167
|
+
const number = quantityIn(s);
|
|
168
|
+
if (number === undefined || isOwned(s, mathsIn(laid, from, to))) return [];
|
|
169
|
+
const lead = s.length - s.trimStart().length;
|
|
170
|
+
const trail = s.length - s.trimEnd().length;
|
|
171
|
+
return [
|
|
172
|
+
{
|
|
173
|
+
messageId: "noOwner",
|
|
174
|
+
data: { number: number.replace(/\s/gu, "") },
|
|
175
|
+
at: spanIn(run, from + lead, to - trail),
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Every sentence of the body that states a number and does not say whose it is. */
|
|
182
|
+
export const judgeClaimProvenance = (
|
|
183
|
+
passages: readonly Passage[],
|
|
184
|
+
): readonly Located[] => passages.flatMap(judgePassage);
|
|
185
|
+
|
|
186
|
+
const META: TexRuleModule["meta"] = {
|
|
187
|
+
type: "suggestion",
|
|
188
|
+
docs: {
|
|
189
|
+
description:
|
|
190
|
+
"a sentence of the body that states a number says whose it is: a citation, the authors, a place in the paper, or the sample it counts",
|
|
191
|
+
url: ruleDocsUrl("claim-provenance"),
|
|
192
|
+
},
|
|
193
|
+
schema: [],
|
|
194
|
+
messages: {
|
|
195
|
+
noOwner:
|
|
196
|
+
"«{{number}}» — whose number is this? A reader cannot tell a cited finding from your own measurement from a general fact. Say it in the sentence: cite the work (\\cite), claim it (we measured, our census), point to where it is shown (\\ref, Section N, Table N), or name the sample it counts (n = 134, 48 runs)",
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** The rule, over the body's prose of the whole paper, its includes spliced (`readPaper`). */
|
|
201
|
+
export const claimProvenanceRule = (deps: PaperDeps): TexRuleModule => ({
|
|
202
|
+
meta: META,
|
|
203
|
+
create(context) {
|
|
204
|
+
return {
|
|
205
|
+
root() {
|
|
206
|
+
const sc = context.sourceCode;
|
|
207
|
+
const paper = readPaper(context.filename, sc.raw ?? sc.text, deps);
|
|
208
|
+
reportInPaper(
|
|
209
|
+
context,
|
|
210
|
+
META.messages,
|
|
211
|
+
paper,
|
|
212
|
+
judgeClaimProvenance(deps.latex.bodyProse(paper.text)),
|
|
213
|
+
);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
});
|
package/src/cli.harness.mjs
CHANGED
|
@@ -1148,7 +1148,7 @@ check(
|
|
|
1148
1148
|
// pnpm: node_modules/vigiles/dist/cli.js DOES NOT
|
|
1149
1149
|
// The old wiring addressed the runtime from the project root and did not resolve under pnpm,
|
|
1150
1150
|
// and `|| exit 2` on PreToolUse(Bash) turned that into a block on ANY command. The end-to-end
|
|
1151
|
-
// half (both installs, real processes) lives in `test/e2e/install.ts`; here — the verdicts.
|
|
1151
|
+
// half (both installs, real processes) lives in `test/e2e/install/install.e2e.ts`; here — the verdicts.
|
|
1152
1152
|
{
|
|
1153
1153
|
const calls = [];
|
|
1154
1154
|
const fake = (code) => (bin, args, opts) => {
|
package/src/cli.test.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
silentOptionalRules,
|
|
24
24
|
toolchainTex,
|
|
25
25
|
} from "./cli.ts";
|
|
26
|
+
import { texInputsDir } from "./package-dirs.ts";
|
|
26
27
|
|
|
27
28
|
const root = useTempDir("cli-test-");
|
|
28
29
|
|
|
@@ -248,14 +249,7 @@ writeTree(noTexDir, {
|
|
|
248
249
|
"papers/tex/paper.tex":
|
|
249
250
|
"\\documentclass{article}\\begin{document}x\\end{document}\n",
|
|
250
251
|
});
|
|
251
|
-
const venues =
|
|
252
|
-
import.meta.dirname,
|
|
253
|
-
"..",
|
|
254
|
-
"skills",
|
|
255
|
-
"submit-paper",
|
|
256
|
-
"references",
|
|
257
|
-
"venues",
|
|
258
|
-
);
|
|
252
|
+
const venues = texInputsDir();
|
|
259
253
|
|
|
260
254
|
test("build: a markdown-only paper is refused", async () => {
|
|
261
255
|
const md = await withoutTex(noTexDir, () =>
|