paperlint 2.0.0 → 2.1.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 +4 -4
- package/CLAUDE.md +3 -3
- package/CONTRIBUTING.md +21 -0
- package/LICENSE +1 -1
- package/README.md +81 -24
- package/action.harness.mjs +10 -10
- package/action.mutations.mjs +3 -3
- package/action.yml +11 -11
- package/bin/{rpp.mjs → paperlint.mjs} +2 -2
- package/dist/adapters/banal/invocation.js +3 -3
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +1 -1
- package/dist/adapters/banal/run.js +1 -1
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +4 -4
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +4 -4
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/banal/xml.js +1 -1
- package/dist/adapters/banal/xml.js.map +1 -1
- package/dist/adapters/curl/download.io.d.ts.map +1 -1
- package/dist/adapters/curl/download.io.js +2 -2
- package/dist/adapters/curl/download.io.js.map +1 -1
- package/dist/build-engine.d.ts +1 -1
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -3
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +29 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +174 -98
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +21 -6
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts +2 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +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 -18
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +70 -42
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +13 -10
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +74 -42
- package/dist/init.js.map +1 -1
- package/dist/link-skills.js +2 -2
- 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 +17 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +68 -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/presets.d.ts +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +186 -0
- package/dist/presets.js.map +1 -0
- package/dist/rules-config.d.ts +8 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +13 -8
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +2 -1
- 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/toolchain.d.ts +2 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +8 -8
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +4 -4
- 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 +387 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +73 -17
- package/docs/e2e.md +3 -4
- package/docs/install.md +11 -3
- package/docs/optional-rules.md +26 -25
- package/docs/rules.md +132 -3
- package/docs/toolchain.md +5 -5
- package/eslint-rules/paper-typography.mjs +1 -1
- package/eslint-rules/paper-typography.mutations.mjs +1 -1
- package/eslint-rules/papers.mjs +6 -2
- package/eslint-rules/pdf-last-page-balance.harness.mjs +5 -3
- package/eslint-rules/pdf-last-page-balance.mjs +23 -2
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/build-e2e/guards/paper.tex +1 -1
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.json +1 -1
- package/fixtures/real-markdown-paper/baseline.mjs +2 -2
- package/fixtures/toolchain-mirror/install-tl +2 -2
- package/fixtures/toolchain-mirror/stub-pdflatex +1 -1
- package/fixtures/toolchain-mirror/stub-tlmgr +1 -1
- package/hooks/hooks.harness.mjs +1 -1
- package/lib/paper-config.d.mts +7 -0
- package/lib/paper-config.harness.mjs +3 -3
- package/lib/paper-config.mjs +34 -3
- package/lib/skill-trigger-cases.harness.mjs +1 -1
- package/package.json +4 -4
- package/plugin/hooks/hooks.json +3 -3
- package/scripts/check.mjs +1 -1
- package/scripts/harness-api.frozen.json +1 -1
- package/scripts/harness-api.test.ts +2 -2
- package/scripts/layer-legacy.frozen.json +1 -1
- package/scripts/mutation-batteries-frozen.harness.mjs +1 -1
- package/scripts/mutation-batteries.frozen.json +2 -2
- package/scripts/release-config.test.ts +1 -1
- package/skills/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -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/description-language.eval.mjs +1 -1
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
- package/skills/paper-pipeline/scripts/consumer.d.mts +1 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +10 -4
- package/skills/paper-pipeline/scripts/consumer.mjs +8 -1
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +1 -1
- package/skills/render-paper/SKILL.md +3 -3
- package/skills/render-paper/SKILL.md.spec.ts +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +6 -6
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- 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 +120 -30
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.test.ts +3 -3
- package/src/adapters/banal/invocation.ts +3 -3
- package/src/adapters/banal/locate.test.ts +4 -4
- package/src/adapters/banal/run.ts +2 -2
- package/src/adapters/banal/settings.test.ts +12 -6
- package/src/adapters/banal/settings.ts +9 -5
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/banal/xml.ts +1 -1
- package/src/adapters/curl/download.io.ts +4 -2
- package/src/adapters/curl/download.test.ts +3 -1
- package/src/adapters/node/files.test.ts +1 -1
- package/src/adapters/node/process.test.ts +2 -2
- package/src/adapters/node/workspace.test.ts +4 -4
- package/src/build-engine.harness.mjs +2 -2
- package/src/build-engine.ts +11 -3
- package/src/build.harness.mjs +27 -22
- package/src/build.mutations.mjs +3 -2
- package/src/build.ts +6 -5
- package/src/cli.harness.mjs +72 -131
- package/src/cli.mutations.mjs +7 -32
- package/src/cli.ts +204 -121
- package/src/doctor.harness.mjs +9 -25
- package/src/doctor.ts +28 -5
- package/src/engine.harness.mjs +4 -2
- package/src/engine.ts +2 -2
- package/src/facts-file.test.ts +54 -7
- package/src/facts-file.ts +145 -24
- package/src/hooks-settings.harness.mjs +24 -15
- package/src/hooks-settings.mutations.mjs +2 -4
- package/src/hooks-settings.ts +92 -49
- package/src/init.ts +88 -52
- package/src/latex-log.harness.mjs +1 -1
- package/src/link-skills.harness.mjs +3 -1
- package/src/link-skills.mutations.mjs +1 -1
- package/src/link-skills.ts +2 -2
- package/src/new-paper.harness.mjs +10 -7
- package/src/new-paper.test.ts +145 -0
- package/src/new-paper.ts +22 -2
- package/src/paper-settings-commands.test.ts +311 -0
- package/src/paper-settings.test.ts +220 -0
- package/src/paper-settings.ts +206 -0
- package/src/pdf-facts.harness.mjs +1 -1
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +307 -0
- package/src/rules-config.ts +14 -9
- package/src/structure.harness.mjs +4 -4
- package/src/structure.ts +2 -1
- package/src/tex-requirements.harness.mjs +12 -18
- package/src/tex-requirements.ts +179 -29
- package/src/toolchain.harness.mjs +34 -25
- package/src/toolchain.ts +14 -8
- package/src/types.ts +4 -4
- package/src/venue-rules.test.ts +545 -0
- package/src/venue-rules.ts +573 -0
- package/templates/paper/paperlint.json +4 -0
- package/fixtures/build-e2e/acmart/venue.json +0 -1
- package/plugin/.claude-plugin/plugin.json +0 -8
|
@@ -1,43 +1,121 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://github.com/zernie/
|
|
4
|
-
"title": "Venue
|
|
5
|
-
"description": "One venue
|
|
3
|
+
"$id": "https://github.com/zernie/paperlint/venue-profile.schema.json",
|
|
4
|
+
"title": "Venue preset",
|
|
5
|
+
"description": "One venue preset or template family (venues/<name>.jsonc), and the TeX base set (venues/tex-base.jsonc). A paper's paperlint.json names one with \"extends\"; a preset may extend another. `format` and `rules` are read by paperlint's pdf/* venue rules and its lint config (src/venue-rules.ts, src/presets.ts); `tex` by `paperlint toolchain` and `paperlint build`.",
|
|
6
6
|
"type": "object",
|
|
7
|
-
"required": ["tex"],
|
|
8
7
|
"additionalProperties": false,
|
|
9
8
|
"properties": {
|
|
10
|
-
"
|
|
9
|
+
"extends": {
|
|
10
|
+
"description": "The preset this one builds on: `paperlint:<name>` (shipped) or a path starting with ./ or ../, relative to this file.",
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1
|
|
13
|
+
},
|
|
14
|
+
"name": {
|
|
15
|
+
"description": "A display name for messages; the file name otherwise.",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1
|
|
18
|
+
},
|
|
19
|
+
"tex": {
|
|
20
|
+
"$ref": "#/definitions/tex"
|
|
21
|
+
},
|
|
11
22
|
"template": {
|
|
12
23
|
"description": "The \\documentclass the venue's template uses.",
|
|
13
24
|
"type": "string",
|
|
14
25
|
"minLength": 1
|
|
15
26
|
},
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"page_h_in": { "type": "number", "exclusiveMinimum": 0 },
|
|
19
|
-
"textblock_w_in": { "type": "number", "exclusiveMinimum": 0 },
|
|
20
|
-
"textblock_h_in": { "type": "number", "exclusiveMinimum": 0 },
|
|
21
|
-
"columns": { "type": "integer", "minimum": 1 },
|
|
22
|
-
"body_pt": { "type": "number", "exclusiveMinimum": 0 },
|
|
23
|
-
"body_pt_tol": { "type": "number", "minimum": 0 },
|
|
24
|
-
"ref_pt_min": { "type": "number", "exclusiveMinimum": 0 },
|
|
25
|
-
"ref_pt_max": { "type": "number", "exclusiveMinimum": 0 },
|
|
26
|
-
"fonts_text": { "type": "string", "minLength": 1 },
|
|
27
|
-
"fonts_title": { "type": "string", "minLength": 1 },
|
|
28
|
-
"page_limits_not_gated": { "type": "object" },
|
|
29
|
-
"kinds": {
|
|
27
|
+
"format": {
|
|
28
|
+
"description": "What the venue's call for papers sets. Every key is optional; a child preset's key replaces its parent's, and a child's kind replaces that kind.",
|
|
30
29
|
"type": "object",
|
|
31
|
-
"additionalProperties":
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"properties": {
|
|
32
|
+
"page_size": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"enum": ["letter", "a4"]
|
|
35
|
+
},
|
|
36
|
+
"page_w_in": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"exclusiveMinimum": 0
|
|
39
|
+
},
|
|
40
|
+
"page_h_in": {
|
|
41
|
+
"type": "number",
|
|
42
|
+
"exclusiveMinimum": 0
|
|
43
|
+
},
|
|
44
|
+
"textblock_w_in": {
|
|
45
|
+
"type": "number",
|
|
46
|
+
"exclusiveMinimum": 0
|
|
47
|
+
},
|
|
48
|
+
"textblock_h_in": {
|
|
49
|
+
"type": "number",
|
|
50
|
+
"exclusiveMinimum": 0
|
|
51
|
+
},
|
|
52
|
+
"columns": {
|
|
53
|
+
"type": "integer",
|
|
54
|
+
"minimum": 1
|
|
55
|
+
},
|
|
56
|
+
"body_pt": {
|
|
57
|
+
"type": "number",
|
|
58
|
+
"exclusiveMinimum": 0
|
|
59
|
+
},
|
|
60
|
+
"body_pt_tol": {
|
|
61
|
+
"type": "number",
|
|
62
|
+
"minimum": 0
|
|
63
|
+
},
|
|
64
|
+
"ref_pt_min": {
|
|
65
|
+
"type": "number",
|
|
66
|
+
"exclusiveMinimum": 0
|
|
67
|
+
},
|
|
68
|
+
"ref_pt_max": {
|
|
69
|
+
"type": "number",
|
|
70
|
+
"exclusiveMinimum": 0
|
|
71
|
+
},
|
|
72
|
+
"fonts_text": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"minLength": 1
|
|
75
|
+
},
|
|
76
|
+
"fonts_title": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"minLength": 1
|
|
79
|
+
},
|
|
80
|
+
"page_limits_not_gated": {
|
|
81
|
+
"type": "object"
|
|
82
|
+
},
|
|
83
|
+
"kinds": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"additionalProperties": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"additionalProperties": false,
|
|
88
|
+
"properties": {
|
|
89
|
+
"body_pages_max": {
|
|
90
|
+
"type": "integer",
|
|
91
|
+
"minimum": 1
|
|
92
|
+
},
|
|
93
|
+
"ref_pages_max": {
|
|
94
|
+
"type": "integer",
|
|
95
|
+
"minimum": 0
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
37
99
|
}
|
|
38
100
|
}
|
|
101
|
+
},
|
|
102
|
+
"rules": {
|
|
103
|
+
"description": "Rules the venue implies, rule id \u2192 ESLint entry (a severity, or [severity, options]). Applied to every paper that extends this preset, before the paper's own rules.",
|
|
104
|
+
"type": "object"
|
|
39
105
|
}
|
|
40
106
|
},
|
|
107
|
+
"allOf": [
|
|
108
|
+
{
|
|
109
|
+
"if": {
|
|
110
|
+
"not": {
|
|
111
|
+
"required": ["extends"]
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"then": {
|
|
115
|
+
"required": ["tex"]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
],
|
|
41
119
|
"definitions": {
|
|
42
120
|
"ctanName": {
|
|
43
121
|
"description": "A package name as tlmgr knows it (CTAN / TeX Live), not a distribution package name.",
|
|
@@ -48,7 +126,11 @@
|
|
|
48
126
|
"type": "array",
|
|
49
127
|
"minItems": 1,
|
|
50
128
|
"uniqueItems": true,
|
|
51
|
-
"items": {
|
|
129
|
+
"items": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"minLength": 1,
|
|
132
|
+
"pattern": "^[^/\\s]+$"
|
|
133
|
+
}
|
|
52
134
|
},
|
|
53
135
|
"tex": {
|
|
54
136
|
"description": "The TeX Live packages this template needs. Each package maps to the files that PROVE it is present: `kpsewhich` must find every one after an install, or the install failed and says which package.",
|
|
@@ -59,14 +141,22 @@
|
|
|
59
141
|
"packages": {
|
|
60
142
|
"type": "object",
|
|
61
143
|
"minProperties": 1,
|
|
62
|
-
"propertyNames": {
|
|
63
|
-
|
|
144
|
+
"propertyNames": {
|
|
145
|
+
"$ref": "#/definitions/ctanName"
|
|
146
|
+
},
|
|
147
|
+
"additionalProperties": {
|
|
148
|
+
"$ref": "#/definitions/proofs"
|
|
149
|
+
}
|
|
64
150
|
},
|
|
65
151
|
"tools": {
|
|
66
152
|
"description": "Packages that ship a PROGRAM rather than a file LaTeX loads. Proved by an executable of that name in TeX Live's bin directory, not by kpsewhich.",
|
|
67
153
|
"type": "object",
|
|
68
|
-
"propertyNames": {
|
|
69
|
-
|
|
154
|
+
"propertyNames": {
|
|
155
|
+
"$ref": "#/definitions/ctanName"
|
|
156
|
+
},
|
|
157
|
+
"additionalProperties": {
|
|
158
|
+
"$ref": "#/definitions/proofs"
|
|
159
|
+
}
|
|
70
160
|
}
|
|
71
161
|
}
|
|
72
162
|
}
|
|
@@ -54,7 +54,7 @@ test("with $BANAL: banal gets the .xml and a quoted $PDFTOHTML, and the geometry
|
|
|
54
54
|
[2, 1],
|
|
55
55
|
);
|
|
56
56
|
const c = run.calls[0];
|
|
57
|
-
// Guards: the poppler-free path — banal is handed
|
|
57
|
+
// Guards: the poppler-free path — banal is handed paperlint's XML, never the PDF.
|
|
58
58
|
assert.match(c?.args.at(-1) ?? "", /\.xml$/);
|
|
59
59
|
assert.match(c?.env["PDFTOHTML"] ?? "", /^'.*pdftohtml'$/);
|
|
60
60
|
});
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import type { LocatedBanal } from "./locate.ts";
|
|
23
23
|
import { XML_DIALECT } from "./xml.ts";
|
|
24
24
|
|
|
25
|
-
test("the staging holds BOTH files: the XML and the -v stub answering the dialect
|
|
25
|
+
test("the staging holds BOTH files: the XML and the -v stub answering the dialect paperlint writes", () => {
|
|
26
26
|
const [xml, stub] = stageBanalInput([]).files;
|
|
27
27
|
assert.equal(xml.name, "paper.xml");
|
|
28
28
|
assert.match(xml.content, /^<\?xml/);
|
|
@@ -50,14 +50,14 @@ test("shQuote survives a single quote", () => {
|
|
|
50
50
|
assert.equal(shQuote("it's"), `'it'"'"'s'`);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
53
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-stage-test-")));
|
|
54
54
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
55
55
|
|
|
56
56
|
test("🔴 stage: on a real scratch directory the stub answers `-v` through the shell, from a path with a space and a quote", () => {
|
|
57
57
|
const awkward = join(root, "it's a dir");
|
|
58
58
|
mkdirSync(awkward);
|
|
59
59
|
const { workspace } = nodeAdapters({ tmpDir: awkward });
|
|
60
|
-
workspace.within("
|
|
60
|
+
workspace.within("paperlint-banal-", (s) => {
|
|
61
61
|
const staged = stage(s, stageBanalInput([]));
|
|
62
62
|
assert.ok(existsSync(staged.xml));
|
|
63
63
|
// Guards: banal runs `$PDFTOHTML -v 2>&1 |` through /bin/sh, unquoted.
|
|
@@ -30,13 +30,13 @@ export const shQuote = (s: string): ShQuoted =>
|
|
|
30
30
|
/** The stub standing in for `pdftohtml`: it answers `-v` and refuses to convert anything. */
|
|
31
31
|
export const PDFTOHTML_STUB = [
|
|
32
32
|
"#!/bin/sh",
|
|
33
|
-
"#
|
|
34
|
-
"# asks `pdftohtml -v` which dialect to expect; this answers with the one
|
|
33
|
+
"# paperlint: banal reads the banal input XML it writes, never a PDF. banal still",
|
|
34
|
+
"# asks `pdftohtml -v` which dialect to expect; this answers with the one paperlint writes.",
|
|
35
35
|
'if [ "$1" = "-v" ]; then',
|
|
36
36
|
` echo "pdftohtml version ${XML_DIALECT.version}"`,
|
|
37
37
|
" exit 0",
|
|
38
38
|
"fi",
|
|
39
|
-
'echo "
|
|
39
|
+
'echo "paperlint: this pdftohtml only answers -v; banal was given a PDF instead of the banal input XML" >&2',
|
|
40
40
|
"exit 1",
|
|
41
41
|
"",
|
|
42
42
|
].join("\n");
|
|
@@ -8,7 +8,7 @@ import { parseBanalSettings } from "./settings.ts";
|
|
|
8
8
|
|
|
9
9
|
const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
|
|
10
10
|
const project = "/p" as AbsolutePath;
|
|
11
|
-
const cache = `/h/.cache/
|
|
11
|
+
const cache = `/h/.cache/paperlint/banal/${BANAL_PIN.commit.slice(0, 12)}/banal`;
|
|
12
12
|
const on =
|
|
13
13
|
(...files: string[]) =>
|
|
14
14
|
(p: string) =>
|
|
@@ -22,11 +22,11 @@ test("installedBanal: one directory per HotCRP commit", () => {
|
|
|
22
22
|
assert.equal(installedBanal(parseBanalSettings({}, dirs)), cache);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test("
|
|
25
|
+
test("paperlint's own copy when nothing else is named", () => {
|
|
26
26
|
assert.equal(kind(pick({}, on(cache))), "cache");
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
test("a project's vendor/banal before
|
|
29
|
+
test("a project's vendor/banal before paperlint's copy", () => {
|
|
30
30
|
// Guards: a project that vendors banal keeps using its own copy.
|
|
31
31
|
assert.equal(kind(pick({}, on(cache, "/p/vendor/banal"))), "vendor");
|
|
32
32
|
});
|
|
@@ -44,7 +44,7 @@ test("🔴 $BANAL naming a missing file finds NOTHING — no fallback to another
|
|
|
44
44
|
);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
test("nothing anywhere: not installed, naming where
|
|
47
|
+
test("nothing anywhere: not installed, naming where paperlint's copy would be", () => {
|
|
48
48
|
assert.deepEqual(
|
|
49
49
|
pick({}, on()),
|
|
50
50
|
err({ kind: "not-installed", installed: cache }),
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ── THE LICENCE BOUNDARY ────────────────────────────────────────────────────────
|
|
10
10
|
* banal is GPL-2.0-or-later; paperlint is MIT. paperlint therefore never contains banal: `paperlint toolchain`
|
|
11
|
-
* DOWNLOADS it from HotCRP at a pinned commit, checks its sha256, and stores it in
|
|
11
|
+
* DOWNLOADS it from HotCRP at a pinned commit, checks its sha256, and stores it in paperlint's cache, and
|
|
12
12
|
* paperlint EXECUTES it as a separate program (`perl banal …`), reading its JSON output. Nothing of banal
|
|
13
13
|
* is copied, vendored, linked or translated into this package.
|
|
14
14
|
*
|
|
@@ -96,7 +96,7 @@ function runBanal(
|
|
|
96
96
|
banal: LocatedBanal,
|
|
97
97
|
pages: readonly PageLayout[],
|
|
98
98
|
): Result<BanalMeasurement, BanalFailure> {
|
|
99
|
-
return d.workspace.within("
|
|
99
|
+
return d.workspace.within("paperlint-banal-", (scratch) =>
|
|
100
100
|
parseBanalOutput(
|
|
101
101
|
d.run.run(
|
|
102
102
|
banalCommand(
|
|
@@ -4,22 +4,28 @@ import { parseBanalSettings } from "./settings.ts";
|
|
|
4
4
|
|
|
5
5
|
const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
|
|
6
6
|
|
|
7
|
-
test("cacheDir: $
|
|
7
|
+
test("cacheDir: $PAPERLINT_BANAL_DIR, else $XDG_CACHE_HOME/paperlint/banal, else ~/.cache/paperlint/banal", () => {
|
|
8
8
|
assert.equal(
|
|
9
|
-
parseBanalSettings({
|
|
9
|
+
parseBanalSettings({ PAPERLINT_BANAL_DIR: "/x" }, dirs).cacheDir,
|
|
10
10
|
"/x",
|
|
11
11
|
);
|
|
12
12
|
assert.equal(
|
|
13
13
|
parseBanalSettings({ XDG_CACHE_HOME: "/c" }, dirs).cacheDir,
|
|
14
|
-
"/c/
|
|
14
|
+
"/c/paperlint/banal",
|
|
15
|
+
);
|
|
16
|
+
assert.equal(
|
|
17
|
+
parseBanalSettings({}, dirs).cacheDir,
|
|
18
|
+
"/h/.cache/paperlint/banal",
|
|
15
19
|
);
|
|
16
|
-
assert.equal(parseBanalSettings({}, dirs).cacheDir, "/h/.cache/rpp/banal");
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
test("an empty variable is unset, and a relative one is relative to the cwd the root read", () => {
|
|
20
|
-
const s = parseBanalSettings(
|
|
23
|
+
const s = parseBanalSettings(
|
|
24
|
+
{ BANAL: "b/banal", PAPERLINT_BANAL_DIR: "" },
|
|
25
|
+
dirs,
|
|
26
|
+
);
|
|
21
27
|
assert.equal(s.explicit, "/w/b/banal");
|
|
22
|
-
assert.equal(s.cacheDir, "/h/.cache/
|
|
28
|
+
assert.equal(s.cacheDir, "/h/.cache/paperlint/banal");
|
|
23
29
|
assert.equal(parseBanalSettings({}, dirs).explicit, null);
|
|
24
30
|
});
|
|
25
31
|
|
|
@@ -11,15 +11,15 @@ import {
|
|
|
11
11
|
} from "../../domain/host.ts";
|
|
12
12
|
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
13
13
|
|
|
14
|
-
/** An explicit banal to use instead of
|
|
14
|
+
/** An explicit banal to use instead of paperlint's (a path to the script). */
|
|
15
15
|
export const BANAL_ENV = "BANAL";
|
|
16
|
-
/** Where `paperlint toolchain` stores banal. Default: `$XDG_CACHE_HOME/
|
|
17
|
-
export const BANAL_DIR_ENV = "
|
|
16
|
+
/** Where `paperlint toolchain` stores banal. Default: `$XDG_CACHE_HOME/paperlint/banal`, else `~/.cache/paperlint/banal`. */
|
|
17
|
+
export const BANAL_DIR_ENV = "PAPERLINT_BANAL_DIR";
|
|
18
18
|
|
|
19
19
|
export interface BanalSettings {
|
|
20
20
|
/** `$BANAL`: the user's explicit choice; null when unset. */
|
|
21
21
|
readonly explicit: AbsolutePath | null;
|
|
22
|
-
/** `$
|
|
22
|
+
/** `$PAPERLINT_BANAL_DIR` › `$XDG_CACHE_HOME/paperlint/banal` › `<home>/.cache/paperlint/banal`. */
|
|
23
23
|
readonly cacheDir: AbsolutePath;
|
|
24
24
|
/** Where scratch directories go. */
|
|
25
25
|
readonly tmpDir: AbsolutePath;
|
|
@@ -37,7 +37,11 @@ const set = (v: string | undefined): string | null => (v ? v : null);
|
|
|
37
37
|
function cacheDirOf(env: Environment, d: HostDirs): string {
|
|
38
38
|
return (
|
|
39
39
|
set(env[BANAL_DIR_ENV]) ??
|
|
40
|
-
join(
|
|
40
|
+
join(
|
|
41
|
+
set(env["XDG_CACHE_HOME"]) ?? join(d.home, ".cache"),
|
|
42
|
+
"paperlint",
|
|
43
|
+
"banal",
|
|
44
|
+
)
|
|
41
45
|
);
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -47,7 +47,7 @@ const page = (boxes: TextBox[], widthPt = 612, heightPt = 792): PageLayout => ({
|
|
|
47
47
|
const expected = [
|
|
48
48
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
49
49
|
'<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
|
|
50
|
-
'<pdf2xml producer="
|
|
50
|
+
'<pdf2xml producer="paperlint (pdf.js)" version="24.02.0">',
|
|
51
51
|
'<page number="1" position="absolute" top="0" left="0" height="2376" width="1836">',
|
|
52
52
|
'\t<fontspec id="0" size="30" family="LinLibertineT" color="#000000"/>',
|
|
53
53
|
'<text top="30.000000" left="60.000000" width="90.000000" height="36.000000" font="0">Hello</text>',
|
|
@@ -104,7 +104,7 @@ export function pdf2xml(pages: readonly PageLayout[]): string {
|
|
|
104
104
|
const lines = [
|
|
105
105
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
106
106
|
'<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
|
|
107
|
-
`<pdf2xml producer="
|
|
107
|
+
`<pdf2xml producer="paperlint (pdf.js)" version="${XML_DIALECT.version}">`,
|
|
108
108
|
...pages.flatMap((p, i) => pageXml(p, i + 1, fonts)),
|
|
109
109
|
"</pdf2xml>",
|
|
110
110
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Download` through `curl -fsSL`. Not Node's `fetch`: undici's `fetch` ignores `HTTPS_PROXY` unless
|
|
3
|
-
* a proxy agent is installed, and
|
|
3
|
+
* a proxy agent is installed, and paperlint's CI and the containers it runs in reach the network only
|
|
4
4
|
* through a proxy. curl reads the proxy variables itself. (A claim from documentation, not measured
|
|
5
5
|
* here — the port is what makes trying the other adapter cheap.)
|
|
6
6
|
*/
|
|
@@ -41,7 +41,9 @@ export function curlDownload(o: {
|
|
|
41
41
|
}): Download {
|
|
42
42
|
return {
|
|
43
43
|
fetch(url: string, timeoutMs: number) {
|
|
44
|
-
const dir = realpathSync(
|
|
44
|
+
const dir = realpathSync(
|
|
45
|
+
mkdtempSync(join(o.tmpDir, "paperlint-download-")),
|
|
46
|
+
);
|
|
45
47
|
const out = join(dir, "body");
|
|
46
48
|
try {
|
|
47
49
|
const seconds = String(Math.ceil(timeoutMs / 1000));
|
|
@@ -19,7 +19,9 @@ import type { AbsolutePath } from "../../domain/paths.ts";
|
|
|
19
19
|
import { curlDownload } from "./download.io.ts";
|
|
20
20
|
import { spawnProcess } from "../node/index.ts";
|
|
21
21
|
|
|
22
|
-
const root = realpathSync(
|
|
22
|
+
const root = realpathSync(
|
|
23
|
+
mkdtempSync(join(tmpdir(), "paperlint-download-test-")),
|
|
24
|
+
);
|
|
23
25
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
24
26
|
const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
|
|
25
27
|
|
|
@@ -7,7 +7,7 @@ import { afterAll as after, test } from "vitest";
|
|
|
7
7
|
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
8
8
|
import { nodeFiles } from "./files.io.ts";
|
|
9
9
|
|
|
10
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
10
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-files-test-")));
|
|
11
11
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
12
12
|
const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
|
|
13
13
|
|
|
@@ -19,8 +19,8 @@ const sh = (script: string, timeoutMs = 10_000): Command => ({
|
|
|
19
19
|
test("a program that is not there is `not-found`, and names the program", () => {
|
|
20
20
|
// Guards: ENOENT is the executable itself — "perl is missing" rather than "banal failed".
|
|
21
21
|
assert.deepEqual(
|
|
22
|
-
run.run({ ...sh(""), file: "
|
|
23
|
-
{ kind: "not-found", file: "
|
|
22
|
+
run.run({ ...sh(""), file: "paperlint-no-such-program-xyz", args: [] }),
|
|
23
|
+
{ kind: "not-found", file: "paperlint-no-such-program-xyz" },
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -19,12 +19,12 @@ import { join } from "node:path";
|
|
|
19
19
|
import { afterAll as after, test } from "vitest";
|
|
20
20
|
import { tmpWorkspace } from "./workspace.io.ts";
|
|
21
21
|
|
|
22
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
22
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-ws-test-")));
|
|
23
23
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
24
24
|
|
|
25
25
|
test("the directory exists inside the scope and is gone after it", () => {
|
|
26
26
|
const ws = tmpWorkspace(root);
|
|
27
|
-
const dir = ws.within("
|
|
27
|
+
const dir = ws.within("paperlint-banal-", (s) => {
|
|
28
28
|
assert.ok(existsSync(s.dir));
|
|
29
29
|
return s.dir;
|
|
30
30
|
});
|
|
@@ -35,7 +35,7 @@ test("the directory exists inside the scope and is gone after it", () => {
|
|
|
35
35
|
test("the directory is gone after a throw too", () => {
|
|
36
36
|
let seen = "";
|
|
37
37
|
assert.throws(() =>
|
|
38
|
-
tmpWorkspace(root).within("
|
|
38
|
+
tmpWorkspace(root).within("paperlint-banal-", (s) => {
|
|
39
39
|
seen = s.dir;
|
|
40
40
|
throw new Error("boom");
|
|
41
41
|
}),
|
|
@@ -46,7 +46,7 @@ test("the directory is gone after a throw too", () => {
|
|
|
46
46
|
test("🔴 write: the file lands in the scope's directory, and `exec` makes it runnable — from a path with a space and a quote", () => {
|
|
47
47
|
const awkward = join(root, "it's a dir");
|
|
48
48
|
mkdirSync(awkward);
|
|
49
|
-
tmpWorkspace(awkward).within("
|
|
49
|
+
tmpWorkspace(awkward).within("paperlint-ws-", (s) => {
|
|
50
50
|
const plain = s.write("data.txt", "hello", "read");
|
|
51
51
|
assert.equal(readFileSync(plain, "utf8"), "hello");
|
|
52
52
|
const script = s.write("run", "#!/bin/sh\necho ran\n", "exec");
|
|
@@ -89,7 +89,7 @@ for (const [a, yes] of [
|
|
|
89
89
|
check(`isYes(${JSON.stringify(a)}) is ${yes}`, E.isYes(a) === yes);
|
|
90
90
|
|
|
91
91
|
// ── 2. prepareEngine ────────────────────────────────────────────────────────────────────
|
|
92
|
-
const work = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
92
|
+
const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-engine-h-")));
|
|
93
93
|
const TEX = {
|
|
94
94
|
packages: { acmart: ["acmart.cls"], libertine: ["libertine.sty"] },
|
|
95
95
|
tools: {},
|
|
@@ -144,7 +144,7 @@ const go = async (o) => {
|
|
|
144
144
|
};
|
|
145
145
|
};
|
|
146
146
|
const envWith = (cacheRoot, path) => ({
|
|
147
|
-
|
|
147
|
+
PAPERLINT_TEXLIVE_DIR: cacheRoot,
|
|
148
148
|
PATH: path,
|
|
149
149
|
});
|
|
150
150
|
|
package/src/build-engine.ts
CHANGED
|
@@ -29,7 +29,11 @@ import {
|
|
|
29
29
|
usableTree,
|
|
30
30
|
type InstallResult,
|
|
31
31
|
} from "./toolchain.ts";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
declaredUnion,
|
|
34
|
+
mergeRequirements,
|
|
35
|
+
type TexRequirements,
|
|
36
|
+
} from "./tex-requirements.ts";
|
|
33
37
|
|
|
34
38
|
export interface EngineOptions {
|
|
35
39
|
/** What the targeted papers need: the base set plus each paper's venue. */
|
|
@@ -81,12 +85,16 @@ function withDefaults(o: EngineOptions): Resolved {
|
|
|
81
85
|
};
|
|
82
86
|
return {
|
|
83
87
|
...r,
|
|
84
|
-
|
|
88
|
+
// The shipped union AND what the targeted papers need: a project's own preset lives outside
|
|
89
|
+
// the package, and its packages are only in `r.tex`.
|
|
90
|
+
install:
|
|
91
|
+
o.install ??
|
|
92
|
+
(() => ensureTexLive(mergeRequirements(declaredUnion().tex, r.tex), r)),
|
|
85
93
|
};
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
/**
|
|
89
|
-
* The facts `resolveEngine` decides on:
|
|
97
|
+
* The facts `resolveEngine` decides on: paperlint's cache and the PATH's TeX Live, each probed. The cache
|
|
90
98
|
* may hold one tree per TeX Live year; the one reported is the newest COMPLETE one, else the newest
|
|
91
99
|
* (so a new year whose install was interrupted never wins over a complete older year).
|
|
92
100
|
*/
|
package/src/build.harness.mjs
CHANGED
|
@@ -50,10 +50,10 @@ const check = (label, cond) => {
|
|
|
50
50
|
n++;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
53
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-build-")));
|
|
54
54
|
// 🔴 banal is looked for under the temp root, never in the developer's cache: with the real
|
|
55
|
-
// `~/.cache/
|
|
56
|
-
const ENV = {
|
|
55
|
+
// `~/.cache/paperlint/banal` present, the measure step would run the real banal on the fake reader's pages.
|
|
56
|
+
const ENV = { PAPERLINT_BANAL_DIR: join(root, "no-banal") };
|
|
57
57
|
const paper = (name, files) => {
|
|
58
58
|
const dir = join(root, "papers", name);
|
|
59
59
|
mkdirSync(dir, { recursive: true });
|
|
@@ -137,6 +137,24 @@ const fakeRead = async (pdf) => {
|
|
|
137
137
|
const CLEAN_TEX = "\\documentclass{article}\\begin{document}x\\end{document}";
|
|
138
138
|
|
|
139
139
|
try {
|
|
140
|
+
// ── inputs: paperlint's own venues directory, no configuration ───────────────────────────────
|
|
141
|
+
// First: the facts below resolve a paper's venue preset in this same directory, so a wrong one
|
|
142
|
+
// must be named HERE, not as a missing venue label two checks later.
|
|
143
|
+
const venues = packageVenuesDir();
|
|
144
|
+
check(
|
|
145
|
+
"the venues directory is paperlint's own and holds paper-guards.tex",
|
|
146
|
+
existsSync(join(venues, "paper-guards.tex")),
|
|
147
|
+
);
|
|
148
|
+
check(
|
|
149
|
+
"🔴 with no TEXINPUTS set, the value ENDS in the separator — otherwise the system tree stops resolving",
|
|
150
|
+
withTexInputs({}, [venues]).TEXINPUTS === `${venues}${delimiter}`,
|
|
151
|
+
);
|
|
152
|
+
check(
|
|
153
|
+
"an existing TEXINPUTS is kept, after ours",
|
|
154
|
+
withTexInputs({ TEXINPUTS: `/mine${delimiter}` }, [venues]).TEXINPUTS ===
|
|
155
|
+
`${venues}${delimiter}/mine${delimiter}`,
|
|
156
|
+
);
|
|
157
|
+
|
|
140
158
|
// ── facts: parsed from the paper, not configured ──────────────────────────────────────
|
|
141
159
|
check(
|
|
142
160
|
"documentclass and options come from the parser",
|
|
@@ -157,33 +175,20 @@ try {
|
|
|
157
175
|
);
|
|
158
176
|
const venued = paper("venued", {
|
|
159
177
|
"paper.tex": CLEAN_TEX,
|
|
160
|
-
"
|
|
178
|
+
"paperlint.json": JSON.stringify({
|
|
179
|
+
extends: "paperlint:agenticdev",
|
|
180
|
+
kind: "short",
|
|
181
|
+
}),
|
|
161
182
|
"build.sh": "exit 0\n",
|
|
162
183
|
});
|
|
163
184
|
const facts = readFacts(venued);
|
|
164
|
-
check("the venue comes from
|
|
185
|
+
check("the venue comes from paperlint.json", facts.venue === "agenticdev");
|
|
165
186
|
check(
|
|
166
187
|
"a leftover build.sh is a FACT (reported), not a step",
|
|
167
188
|
JSON.stringify(facts.ignoredScripts) === JSON.stringify(["build.sh"]) &&
|
|
168
189
|
IGNORED_SCRIPTS.includes("repro/build-submission.sh"),
|
|
169
190
|
);
|
|
170
191
|
|
|
171
|
-
// ── inputs: rpp's own venues directory, no configuration ───────────────────────────────
|
|
172
|
-
const venues = packageVenuesDir();
|
|
173
|
-
check(
|
|
174
|
-
"the venues directory is rpp's own and holds paper-guards.tex",
|
|
175
|
-
existsSync(join(venues, "paper-guards.tex")),
|
|
176
|
-
);
|
|
177
|
-
check(
|
|
178
|
-
"🔴 with no TEXINPUTS set, the value ENDS in the separator — otherwise the system tree stops resolving",
|
|
179
|
-
withTexInputs({}, [venues]).TEXINPUTS === `${venues}${delimiter}`,
|
|
180
|
-
);
|
|
181
|
-
check(
|
|
182
|
-
"an existing TEXINPUTS is kept, after ours",
|
|
183
|
-
withTexInputs({ TEXINPUTS: `/mine${delimiter}` }, [venues]).TEXINPUTS ===
|
|
184
|
-
`${venues}${delimiter}/mine${delimiter}`,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
192
|
// ── the plan ──────────────────────────────────────────────────────────────────────────
|
|
188
193
|
const plan = planFor(facts);
|
|
189
194
|
check(
|
|
@@ -261,7 +266,7 @@ try {
|
|
|
261
266
|
tex.calls.every((c) => c.cwd === clean),
|
|
262
267
|
);
|
|
263
268
|
check(
|
|
264
|
-
"🔴 pdflatex got
|
|
269
|
+
"🔴 pdflatex got paperlint's venues directory on TEXINPUTS, trailing separator kept",
|
|
265
270
|
tex.calls.every((c) => c.texinputs === `${venues}${delimiter}`),
|
|
266
271
|
);
|
|
267
272
|
|
package/src/build.mutations.mjs
CHANGED
|
@@ -132,9 +132,10 @@ process.exit(
|
|
|
132
132
|
{
|
|
133
133
|
name: "the port resolves the venues directory one level off",
|
|
134
134
|
harness: HARNESS,
|
|
135
|
-
expect:
|
|
135
|
+
expect:
|
|
136
|
+
"the venues directory is paperlint's own and holds paper-guards.tex",
|
|
136
137
|
disables:
|
|
137
|
-
"the one answer to 'where are
|
|
138
|
+
"the one answer to 'where are paperlint's venue files'. A wrong directory does not fail " +
|
|
138
139
|
"pdflatex by itself — \\input{paper-guards} would fail later, in a paper, far from here",
|
|
139
140
|
edits: [
|
|
140
141
|
[
|