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
|
@@ -14,7 +14,7 @@ poppler's numbers, so a pass means agreement with the tool pdf.js replaced.
|
|
|
14
14
|
| `hidden-text.pdf` | `hidden-text.tex`, pdflatex | two-column body text beside near-white, rotated and render-mode-3 text; page 2 holds only hidden text, so banal calls it blank |
|
|
15
15
|
|
|
16
16
|
`test/e2e/banal.mjs` also reads these files, with the real banal: it pins what banal 1.2 printed for
|
|
17
|
-
each one on poppler's `pdftohtml` 24.02.0 (2026-09-25), so a pass there means
|
|
17
|
+
each one on poppler's `pdftohtml` 24.02.0 (2026-09-25), so a pass there means paperlint's pdf.js-written
|
|
18
18
|
XML measures the same as poppler's. `hidden-text.pdf` exists for that test: each kind of hidden text
|
|
19
19
|
changes banal's answer if it is counted.
|
|
20
20
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"",
|
|
10
10
|
"Recorded 2026-09-19. Three of the five are about the scorecard being deliberately thin",
|
|
11
11
|
"(no pdf, no frozen source) rather than about the prose; paper/typography's single entry is",
|
|
12
|
-
"the 18 p-values of
|
|
12
|
+
"the 18 p-values of paperlint#44. They are real findings against IEEE / ISO 80000-1 style, which",
|
|
13
13
|
"requires the leading zero, in a blog post written for a general audience rather than for an",
|
|
14
14
|
"IEEE venue. All 18 sit in prose and table cells, so the count from the parsed tree keeps them.",
|
|
15
15
|
"The count is expected to stay. A drop is never a failure here, only growth is."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ONE reader of `baseline.json`, shared by the two runs that compare against it: the harness
|
|
3
|
-
* beside this file (the repository's own `bin/
|
|
3
|
+
* beside this file (the repository's own `bin/paperlint.mjs`) and `test/e2e/install.mjs` (the binary
|
|
4
4
|
* a consumer actually got). Two copies of "growth fails, a drop never does" would drift the first
|
|
5
5
|
* time one of them is tightened, and the two runs would then disagree about the same article.
|
|
6
6
|
*/
|
|
@@ -34,7 +34,7 @@ export function countByRule(stdout) {
|
|
|
34
34
|
* otherwise. Always a failure.
|
|
35
35
|
* vanished — a recorded rule went fully quiet without the recording being updated: how a check
|
|
36
36
|
* dies unnoticed. Also a failure.
|
|
37
|
-
* A partial drop is neither: it is what a fix looks like (
|
|
37
|
+
* A partial drop is neither: it is what a fix looks like (paperlint#44 is expected to take typography
|
|
38
38
|
* to zero, and then the recording is updated in the same change).
|
|
39
39
|
*/
|
|
40
40
|
export function compareToBaseline(found, recorded = recordedFindings()) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# STANDS IN FOR install-tl in src/toolchain.harness.mjs. It reads TEXDIR from the profile
|
|
2
|
+
# STANDS IN FOR install-tl in src/toolchain.harness.mjs. It reads TEXDIR from the profile paperlint
|
|
3
3
|
# wrote and lays out a tree whose kpsewhich and tlmgr are the stubs shipped beside it, so the
|
|
4
|
-
# harness drives
|
|
4
|
+
# harness drives paperlint's real download, unpack, install and verify logic without the network.
|
|
5
5
|
set -eu
|
|
6
6
|
here=$(cd "$(dirname "$0")" && pwd)
|
|
7
7
|
profile=""
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# Answers like tlmgr install: each package is looked up in the repository's catalog.txt
|
|
3
3
|
# ("name file…", a "bin:NAME" item creates an executable). An unknown name gets tlmgr's own
|
|
4
|
-
# wording, which is what
|
|
4
|
+
# wording, which is what paperlint parses. Every call is appended to tlmgr.log at the tree root.
|
|
5
5
|
bin=$(cd "$(dirname "$0")" && pwd)
|
|
6
6
|
tree=$(cd "$bin/../.." && pwd)
|
|
7
7
|
repo=""
|
package/hooks/hooks.harness.mjs
CHANGED
|
@@ -72,7 +72,7 @@ const check = (label, cond) => {
|
|
|
72
72
|
* checkout, which is what `npm install` of a local package does anyway.
|
|
73
73
|
*/
|
|
74
74
|
const consumer = (block, { papers = "docs/papers", paper = "alpha" } = {}) => {
|
|
75
|
-
const dir = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
75
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-hooks-")));
|
|
76
76
|
const nm = join(dir, "node_modules");
|
|
77
77
|
mkdirSync(nm, { recursive: true });
|
|
78
78
|
symlinkSync(join(ROOT, "node_modules", "vigiles"), join(nm, "vigiles"));
|
package/lib/paper-config.d.mts
CHANGED
|
@@ -32,3 +32,10 @@ export declare function renamedFieldMessage(
|
|
|
32
32
|
|
|
33
33
|
/** Every key the settings object may hold, mapped to who reads it. */
|
|
34
34
|
export declare const SETTINGS_KEYS: Readonly<Record<string, string>>;
|
|
35
|
+
|
|
36
|
+
/** The per-paper settings file, `paperlint.json`, and its name before 2.1.0. */
|
|
37
|
+
export declare const PAPER_SETTINGS_FILE: string;
|
|
38
|
+
export declare const LEGACY_PAPER_SETTINGS_FILE: string;
|
|
39
|
+
/** Every key `paperlint.json` may hold, mapped to who reads it. */
|
|
40
|
+
export declare const PAPER_SETTINGS_KEYS: Readonly<Record<string, string>>;
|
|
41
|
+
export declare const LEGACY_PAPER_SETTINGS_MESSAGE: string;
|
|
@@ -219,7 +219,7 @@ const loaded = await Promise.all(
|
|
|
219
219
|
// refuses any key not in SETTINGS_KEYS. So the list must hold every key some reader reads — or a
|
|
220
220
|
// consumer's valid key is refused — and nothing no one reads. The readers are FOUND, by the AST:
|
|
221
221
|
// every `[CONFIG_KEY]?.<key>` (or the literal package name in the brackets), plus every field of
|
|
222
|
-
// `
|
|
222
|
+
// `PaperlintConfig`, the CLI's typed view of the same object.
|
|
223
223
|
{
|
|
224
224
|
const { SETTINGS_KEYS } = await import(join(HERE, "paper-config.mjs"));
|
|
225
225
|
const ts = (await import("typescript")).default;
|
|
@@ -239,7 +239,7 @@ const loaded = await Promise.all(
|
|
|
239
239
|
const visit = (node) => {
|
|
240
240
|
if (ts.isPropertyAccessExpression(node) && isSettings(node.expression))
|
|
241
241
|
readKeys.add(node.name.text);
|
|
242
|
-
if (ts.isInterfaceDeclaration(node) && node.name.text === "
|
|
242
|
+
if (ts.isInterfaceDeclaration(node) && node.name.text === "PaperlintConfig")
|
|
243
243
|
for (const m of node.members)
|
|
244
244
|
if (m.name && ts.isIdentifier(m.name)) readKeys.add(m.name.text);
|
|
245
245
|
ts.forEachChild(node, visit);
|
|
@@ -274,7 +274,7 @@ const loaded = await Promise.all(
|
|
|
274
274
|
unread.length === 0,
|
|
275
275
|
);
|
|
276
276
|
check(
|
|
277
|
-
"and the scan found the readers it must find — the skill scripts' keys and
|
|
277
|
+
"and the scan found the readers it must find — the skill scripts' keys and PaperlintConfig's",
|
|
278
278
|
["ledger", "citeChecks", "triggerCases", "papersDir", "rules"].every((k) =>
|
|
279
279
|
readKeys.has(k),
|
|
280
280
|
),
|
package/lib/paper-config.mjs
CHANGED
|
@@ -30,6 +30,37 @@ export const CONFIG_KEY = "paperlint";
|
|
|
30
30
|
*/
|
|
31
31
|
export const LEGACY_CONFIG_KEY = "research-paper-pipeline";
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* THE THREE LEVELS OF SETTINGS, each named after the tool:
|
|
35
|
+
*
|
|
36
|
+
* package.json → "paperlint" the project: where the papers are, what every paper gets
|
|
37
|
+
* <paper>/paperlint.json one paper: the venue preset it extends, its kind, its own rules
|
|
38
|
+
* a venue preset one venue: its format, TeX packages and rules — shipped with
|
|
39
|
+
* paperlint (`paperlint:<name>`) or the project's own (`./x.jsonc`)
|
|
40
|
+
*
|
|
41
|
+
* The per-paper file was `venue.json` before 2.1.0. It is NOT read any more — a fallback would keep
|
|
42
|
+
* it working forever — and `paperlint init` moves it.
|
|
43
|
+
*/
|
|
44
|
+
export const PAPER_SETTINGS_FILE = "paperlint.json";
|
|
45
|
+
export const LEGACY_PAPER_SETTINGS_FILE = "venue.json";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every key `paperlint.json` may hold. Any other key is REFUSED with its name: a typo would
|
|
49
|
+
* otherwise read as "not set". `$comment` is JSON Schema's own keyword for a comment, the one way
|
|
50
|
+
* to leave a note in a JSON file; paperlint ignores its value.
|
|
51
|
+
*/
|
|
52
|
+
export const PAPER_SETTINGS_KEYS = Object.freeze({
|
|
53
|
+
extends:
|
|
54
|
+
"the venue preset: paperlint:<name> or ./path.jsonc — the pdf/ venue rules, paperlint build",
|
|
55
|
+
kind: "the kind of paper, whose page limit applies — pdf/limits",
|
|
56
|
+
pdf: "where the built PDF is, relative to the paper, when it is not paper.pdf — the facts",
|
|
57
|
+
rules: "rule overrides for this paper alone — paperlint lint",
|
|
58
|
+
$comment: "a note for humans; ignored",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** What a leftover pre-2.1.0 file is told. */
|
|
62
|
+
export const LEGACY_PAPER_SETTINGS_MESSAGE = `${LEGACY_PAPER_SETTINGS_FILE} is no longer read: it was renamed ${PAPER_SETTINGS_FILE} in paperlint 2.1.0. \`npx paperlint init\` moves it.`;
|
|
63
|
+
|
|
33
64
|
/**
|
|
34
65
|
* Where a parsed `package.json` keeps this package's settings — the new key, else the old one.
|
|
35
66
|
*
|
|
@@ -96,7 +127,7 @@ export const OLD_PAPERS_DIR_FIELD = "papers";
|
|
|
96
127
|
/**
|
|
97
128
|
* The error text for a config that still uses the old field name, or `null` when it does not.
|
|
98
129
|
*
|
|
99
|
-
* @param settings the object under CONFIG_KEY
|
|
130
|
+
* @param settings the object under CONFIG_KEY
|
|
100
131
|
* @param where where that object lives, as the reader should see it in the message
|
|
101
132
|
*/
|
|
102
133
|
export function renamedFieldMessage(
|
|
@@ -117,7 +148,7 @@ export function renamedFieldMessage(
|
|
|
117
148
|
* 🔴 ONE LIST, AND IT IS CHECKED AGAINST THE READERS, NOT TRUSTED. The settings object has readers
|
|
118
149
|
* all over the package (the CLI, the skill scripts, the trigger-case loader), each reading its own
|
|
119
150
|
* keys. `lib/paper-config.harness.mjs` walks the package's source, finds every
|
|
120
|
-
* `[CONFIG_KEY]?.<key>` read and every field of `
|
|
151
|
+
* `[CONFIG_KEY]?.<key>` read and every field of `PaperlintConfig`, and requires each to be here — so a
|
|
121
152
|
* new reader that forgets this list turns the harness red before a consumer's config does.
|
|
122
153
|
*/
|
|
123
154
|
export const SETTINGS_KEYS = Object.freeze({
|
|
@@ -130,7 +161,7 @@ export const SETTINGS_KEYS = Object.freeze({
|
|
|
130
161
|
minFindings: "review/findings-cause — paperlint lint",
|
|
131
162
|
causeMarker: "review/findings-cause — paperlint lint",
|
|
132
163
|
rules:
|
|
133
|
-
"extra ESLint config blocks, appended after
|
|
164
|
+
"extra ESLint config blocks, appended after paperlint's own — paperlint lint",
|
|
134
165
|
buildScripts: "REMOVED; still named so paperlint build can say it is ignored",
|
|
135
166
|
ledger: "the run ledger — skills/paper-pipeline/scripts/consumer.mjs",
|
|
136
167
|
scripts: "how skill prose names the scripts — consumer.mjs",
|
|
@@ -54,7 +54,7 @@ function loadAt(root) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function fixture(pkgJson, casesFile) {
|
|
57
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
57
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-cases-")));
|
|
58
58
|
writeFileSync(join(root, "package.json"), JSON.stringify(pkgJson));
|
|
59
59
|
if (casesFile) {
|
|
60
60
|
mkdirSync(join(root, ".claude", "lib"), { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperlint",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"scripts": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"repository": {
|
|
53
53
|
"type": "git",
|
|
54
|
-
"url": "git+https://github.com/zernie/
|
|
54
|
+
"url": "git+https://github.com/zernie/paperlint.git"
|
|
55
55
|
},
|
|
56
56
|
"keywords": [
|
|
57
57
|
"eslint",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"node": ">=22.13"
|
|
69
69
|
},
|
|
70
70
|
"bin": {
|
|
71
|
-
"paperlint": "bin/
|
|
71
|
+
"paperlint": "bin/paperlint.mjs"
|
|
72
72
|
},
|
|
73
73
|
"files": [
|
|
74
74
|
".github",
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/
|
|
9
|
+
"command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/paperlint.mjs\" hook paper-edit-guard"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"hooks": [
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/
|
|
20
|
+
"command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/paperlint.mjs\" hook paper-skills-nudge"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"type": "command",
|
|
24
|
-
"command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/
|
|
24
|
+
"command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/paperlint.mjs\" hook paper-status-gates"
|
|
25
25
|
}
|
|
26
26
|
]
|
|
27
27
|
}
|
package/scripts/check.mjs
CHANGED
|
@@ -140,7 +140,7 @@ export const GATES = [
|
|
|
140
140
|
run: locked("node", "test/e2e/banal.mjs"),
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
|
-
name: "toolchain e2e — real TeX Live into $
|
|
143
|
+
name: "toolchain e2e — real TeX Live into $PAPERLINT_TEXLIVE_DIR, then a build with only it on PATH",
|
|
144
144
|
job: "build-e2e",
|
|
145
145
|
run: locked("node", "test/e2e/toolchain.mjs"),
|
|
146
146
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"issue": "https://github.com/zernie/
|
|
2
|
+
"issue": "https://github.com/zernie/paperlint/issues/77",
|
|
3
3
|
"why": "A *.harness.* file tests the agent surface and imports runHook, runHarnessTest or runEval from vigiles. Plain unit tests are *.test.ts, run by vitest. These are the harness files that existed on main on 2026-09-25 and import none of the three. The list only shrinks: rename a file to *.test.ts (issue #77), or make it a real harness, then delete its entry. Checked by scripts/harness-api.test.ts, part of npm test.",
|
|
4
4
|
"harnesses": [
|
|
5
5
|
"action.harness.mjs",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The rule is read from the file's IMPORTS, parsed with the TypeScript compiler — not from its
|
|
7
7
|
* folder and not from a text search: a comment or a string that mentions `runHook` is not an import,
|
|
8
|
-
* and `src/cli.harness.mjs` calls a function of
|
|
8
|
+
* and `src/cli.harness.mjs` calls a function of paperlint's own that happens to be called `runHook`.
|
|
9
9
|
*
|
|
10
10
|
* Harnesses that predate the rule and import none of the three are frozen in
|
|
11
11
|
* `harness-api.frozen.json` (issue #77). That list only shrinks: an entry that now imports the API,
|
|
@@ -156,7 +156,7 @@ test("an import is what counts — not a mention, not a same-named local functio
|
|
|
156
156
|
const mention =
|
|
157
157
|
'// runHook\nconst s = "runHarnessTest";\nimport { x } from "vigiles";\n';
|
|
158
158
|
assert.equal(importsAgentApi("a.harness.mjs", mention), false);
|
|
159
|
-
// Guards:
|
|
159
|
+
// Guards: paperlint's own `runHook` (src/cli.ts) is not vigiles'.
|
|
160
160
|
assert.equal(
|
|
161
161
|
importsAgentApi(
|
|
162
162
|
"b.harness.mjs",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"issue": "https://github.com/zernie/
|
|
2
|
+
"issue": "https://github.com/zernie/paperlint/issues/76",
|
|
3
3
|
"why": "Legacy src/*.ts files that still do I/O or import an adapter or a tool package directly. May only shrink: fix a site, delete its directive, lower the count here (delete the file entry at zero). A new file may not appear. Counted from ESLint suppressedMessages by scripts/layer-legacy-frozen.mjs, part of npm run check.",
|
|
4
4
|
"files": {
|
|
5
5
|
"src/build-engine.ts": {
|
|
@@ -60,7 +60,7 @@ const driverBattery = (k) =>
|
|
|
60
60
|
`import { runMutations } from "./lib/mutation-driver.mjs";\n` +
|
|
61
61
|
`process.exit(runMutations({ root: ".", cases: [${Array.from({ length: k }, (_, i) => `{ name: "c${String(i)}" }`).join(", ")}] }));\n`;
|
|
62
62
|
|
|
63
|
-
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
63
|
+
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-frozen-h-")));
|
|
64
64
|
process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
65
65
|
mkdirSync(join(TMP, "scripts"), { recursive: true });
|
|
66
66
|
mkdirSync(join(TMP, "src"), { recursive: true });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"issue": "https://github.com/zernie/
|
|
2
|
+
"issue": "https://github.com/zernie/paperlint/issues/52",
|
|
3
3
|
"why": "Hand-written *.mutations.mjs batteries are being removed (#52). This list may only shrink: no new batteries, no new cases in existing ones. Delete a battery or a case, then delete its entry or lower its count here. Checked by scripts/mutation-batteries-frozen.mjs, part of npm run check.",
|
|
4
4
|
"batteries": {
|
|
5
5
|
"action.mutations.mjs": {
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"cases": 11
|
|
97
97
|
},
|
|
98
98
|
"src/cli.mutations.mjs": {
|
|
99
|
-
"cases":
|
|
99
|
+
"cases": 26
|
|
100
100
|
},
|
|
101
101
|
"src/doctor.mutations.mjs": {
|
|
102
102
|
"cases": 10
|
|
@@ -53,7 +53,7 @@ const fix = {
|
|
|
53
53
|
const context = {
|
|
54
54
|
cwd: process.cwd(),
|
|
55
55
|
options: {
|
|
56
|
-
repositoryUrl: "https://github.com/zernie/
|
|
56
|
+
repositoryUrl: "https://github.com/zernie/paperlint",
|
|
57
57
|
},
|
|
58
58
|
lastRelease: { gitTag: "v1.0.0", version: "1.0.0" },
|
|
59
59
|
nextRelease: { gitTag: "v2.0.0", version: "2.0.0", type: "major" },
|
|
@@ -4,7 +4,7 @@ description: "Discover and rank real venues for a given paper, scored by authors
|
|
|
4
4
|
allowed-tools: [Read, Write, Grep, Glob, WebSearch, WebFetch, Agent, Bash(node .claude/skills/paper-pipeline/scripts/announce.mjs:*), Bash(node .claude/skills/paper-pipeline/scripts/ledger.mjs:*)]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:5efdcc0e1b178c1a compiled from skills/find-venue/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# find-venue — rank real venues by what earns the credit, then keep or switch
|
|
10
10
|
|
|
@@ -113,6 +113,10 @@ dropped and why, and a verdict with no table behind it cannot be re-checked when
|
|
|
113
113
|
## Compose with
|
|
114
114
|
- `research-ideate` (upstream) — consumes its candidate venue *types* and sharpest framing.
|
|
115
115
|
- `plan-paper-timeline` — feed it the chosen deadline to back-plan the work.
|
|
116
|
+
- once a venue is chosen, the paper declares it in `<paper>/paperlint.json`:
|
|
117
|
+
`{ "extends": "paperlint:<venue>", "kind": "<kind>" }` — a shipped preset, else a family
|
|
118
|
+
(`paperlint:acm-sigconf`) or the project's own `./venues/<name>.jsonc`; the `pdf/*` rules then
|
|
119
|
+
check the built PDF against its page limit and format.
|
|
116
120
|
- `submit-paper` venue data cards (`submit-paper/references/venues/<venue>.md`, e.g. `agenticdev.md`,
|
|
117
121
|
`aisec.md`) — the winner gets a venue-specific data card with its HotCRP quirks; save a new one per
|
|
118
122
|
venue as plain data, not a new skill.
|
|
@@ -129,6 +129,10 @@ dropped and why, and a verdict with no table behind it cannot be re-checked when
|
|
|
129
129
|
## Compose with
|
|
130
130
|
- \`research-ideate\` (upstream) — consumes its candidate venue *types* and sharpest framing.
|
|
131
131
|
- \`plan-paper-timeline\` — feed it the chosen deadline to back-plan the work.
|
|
132
|
+
- once a venue is chosen, the paper declares it in \`<paper>/paperlint.json\`:
|
|
133
|
+
\`{ "extends": "paperlint:<venue>", "kind": "<kind>" }\` — a shipped preset, else a family
|
|
134
|
+
(\`paperlint:acm-sigconf\`) or the project's own \`./venues/<name>.jsonc\`; the \`pdf/*\` rules then
|
|
135
|
+
check the built PDF against its page limit and format.
|
|
132
136
|
- \`submit-paper\` venue data cards (\`submit-paper/references/venues/<venue>.md\`, e.g. \`agenticdev.md\`,
|
|
133
137
|
\`aisec.md\`) — the winner gets a venue-specific data card with its HotCRP quirks; save a new one per
|
|
134
138
|
venue as plain data, not a new skill.
|
|
@@ -199,6 +199,14 @@ format.
|
|
|
199
199
|
|
|
200
200
|
## 3. Venue constants belong in a card, not in code
|
|
201
201
|
|
|
202
|
+
> **Done (#79, 2.1.0), in a different shape than planned below.** The constants live in **venue
|
|
203
|
+
> presets**, JSONC files in `submit-paper/references/venues/`: `acm-sigconf.jsonc` is the ACM
|
|
204
|
+
> family (the `format` block — page size, columns, fonts, font sizes — plus the `tex` packages),
|
|
205
|
+
> `agenticdev.jsonc` and `aisec.jsonc` extend it and add their `kinds` (page limits) and `rules`,
|
|
206
|
+
> `realm.jsonc` stands alone. A paper picks one in `<paper>/paperlint.json`
|
|
207
|
+
> (`{ "extends": "paperlint:<venue>", "kind": "…" }`); the `pdf/*` lint rules judge the built PDF
|
|
208
|
+
> against it. The `.md` cards stay as prose. What follows is the plan as it was written.
|
|
209
|
+
|
|
202
210
|
Right now `LinLibertine`, `LinBiolinum`, `acmart` are hardcoded into `check-render.sh`. This is
|
|
203
211
|
**venue data**, and it belongs in
|
|
204
212
|
`.claude/skills/submit-paper/references/venues/<venue>.md`:
|
|
@@ -292,7 +300,7 @@ imports, paths inside regexes).
|
|
|
292
300
|
## 2. One build entry point for all papers, not one per paper
|
|
293
301
|
|
|
294
302
|
> **Done (#59, 2026-09-24).** `paperlint build <paper>` is the entry point, and `paperlint toolchain` installs
|
|
295
|
-
> TeX Live with the packages the venue
|
|
303
|
+
> TeX Live with the packages the venue presets declare; `ensure-toolchain.sh` and
|
|
296
304
|
> `ci-install-texlive.sh` are deleted. What follows is the plan as it was written.
|
|
297
305
|
|
|
298
306
|
**The hole:** `compile-rules` has `repro/build-submission.sh`, which calls `ensure-toolchain.sh`.
|
|
@@ -324,9 +332,11 @@ point. Nothing to check.
|
|
|
324
332
|
| page limit | **4** | `venues/realm.md` 8 · `build-submission.sh` 8 · `PIPELINE-STATUS` "8/8" · the build prints **9** |
|
|
325
333
|
| the TeX package list | **5** | `ensure-toolchain.sh` (executable) · `SKILL.md` · `render-paper.harness.mjs` (pins it ✅) · `build-submission.sh` · `PIPELINE-STATUS` |
|
|
326
334
|
|
|
327
|
-
The canonical source is the venue
|
|
328
|
-
format
|
|
329
|
-
|
|
335
|
+
The canonical source is the venue preset (`venues/<name>.jsonc`, resolved through its `extends`
|
|
336
|
+
chain): its `format` block for the numbers (page size, columns, fonts, font sizes, and `kinds` for
|
|
337
|
+
page limits), its `tex` block (plus `tex-base.jsonc`) for packages — since 2026-09-24;
|
|
338
|
+
`ensure-toolchain.sh` held them until then — and its `rules` block for the checks the venue
|
|
339
|
+
implies. The `.md` card is prose about the venue. Everything else is a pointer.
|
|
330
340
|
|
|
331
341
|
## 3-ter. Drop the font gate from `check-render.sh` once the rules run in CI
|
|
332
342
|
|
|
@@ -4,7 +4,7 @@ description: The orchestrator for writing a research paper end-to-end, from idea
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent, Skill]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:291d2266e29874be compiled from skills/paper-pipeline/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# paper-pipeline — the conductor for the whole organism
|
|
10
10
|
|
|
@@ -28,8 +28,9 @@ of that — who is filing what, and when — stays in the author's own private n
|
|
|
28
28
|
- `references/acl-venue-rules.md` — what holds at **every** ACL-family venue and workshop: the
|
|
29
29
|
page-limit map, the appendix rule and the trap inside it, Limitations/Ethics as free-but-restricted
|
|
30
30
|
space, anonymity, OpenReview mechanics. Fetch once, not per paper. Venue-specific facts stay in
|
|
31
|
-
`submit-paper/references/venues/<venue>.md`. (No equivalent yet for ACM or IEEE — write one
|
|
32
|
-
the next paper goes there.
|
|
31
|
+
`submit-paper/references/venues/<venue>.md`. (No prose equivalent yet for ACM or IEEE — write one
|
|
32
|
+
when the next paper goes there. ACM's machine-checked format already exists: the
|
|
33
|
+
`paperlint:acm-sigconf` preset, `submit-paper/references/venues/acm-sigconf.jsonc`.)
|
|
33
34
|
|
|
34
35
|
## The readiness scorecard (which boxes are checked)
|
|
35
36
|
Stages run in subagents and report into chat, which evaporates — so "did everything run?" becomes
|
|
@@ -45,8 +45,9 @@ of that — who is filing what, and when — stays in the author's own private n
|
|
|
45
45
|
- \`references/acl-venue-rules.md\` — what holds at **every** ACL-family venue and workshop: the
|
|
46
46
|
page-limit map, the appendix rule and the trap inside it, Limitations/Ethics as free-but-restricted
|
|
47
47
|
space, anonymity, OpenReview mechanics. Fetch once, not per paper. Venue-specific facts stay in
|
|
48
|
-
\`submit-paper/references/venues/<venue>.md\`. (No equivalent yet for ACM or IEEE — write one
|
|
49
|
-
the next paper goes there.
|
|
48
|
+
\`submit-paper/references/venues/<venue>.md\`. (No prose equivalent yet for ACM or IEEE — write one
|
|
49
|
+
when the next paper goes there. ACM's machine-checked format already exists: the
|
|
50
|
+
\`paperlint:acm-sigconf\` preset, \`submit-paper/references/venues/acm-sigconf.jsonc\`.)
|
|
50
51
|
|
|
51
52
|
## The readiness scorecard (which boxes are checked)
|
|
52
53
|
Stages run in subagents and report into chat, which evaporates — so "did everything run?" becomes
|
|
@@ -236,7 +236,7 @@ const CASES = [
|
|
|
236
236
|
|
|
237
237
|
// Preflight: a misspelled skill or a changed namespace makes every `fired` predicate permanently
|
|
238
238
|
// false, and the run then reports a wall of confident 0.00s as though the descriptions were dead.
|
|
239
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
239
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
240
240
|
const installed = new Set(installedSkills(SKILLS_DIR));
|
|
241
241
|
for (const c of CASES) {
|
|
242
242
|
if (!installed.has(c.skill))
|
|
@@ -433,7 +433,7 @@ const CELL_LABEL = {
|
|
|
433
433
|
// as though the descriptions were dead. That is the failure mode this block exists to prevent.
|
|
434
434
|
|
|
435
435
|
// 1. the skills exist and declare the names we test against
|
|
436
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
436
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
437
437
|
const installed = installedSkills(SKILLS_DIR);
|
|
438
438
|
const installedSet = new Set(installed);
|
|
439
439
|
for (const c of CASES) {
|
|
@@ -586,7 +586,7 @@ if (ONLY.length && RUN_ARMS.length !== ONLY.length)
|
|
|
586
586
|
// a wall of confident 0.00s that read like a finding.
|
|
587
587
|
|
|
588
588
|
// 1. the roster is what the parent measured
|
|
589
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
589
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
590
590
|
const installed = installedSkills(SKILLS_DIR);
|
|
591
591
|
const installedSet = new Set(installed);
|
|
592
592
|
if (installedSet.has(ARMS[5].rename))
|
|
@@ -737,7 +737,7 @@ function buildArm(a) {
|
|
|
737
737
|
rmSync(dir, { recursive: true, force: true });
|
|
738
738
|
mkdirSync(dir, { recursive: true });
|
|
739
739
|
// 🔴 ONE SKILL AT A TIME, FROM ITS REALPATH — not `cpSync(SKILLS_DIR, dir, { recursive })`.
|
|
740
|
-
// In a consumer every entry of SKILLS_DIR is a link `paperlint init` made (
|
|
740
|
+
// In a consumer every entry of SKILLS_DIR is a link `paperlint init` made (paperlint#62), and `cpSync` copies
|
|
741
741
|
// a link as a link, rewritten to an ABSOLUTE path into the original; `dereference: true` does not
|
|
742
742
|
// change that for nested entries (measured, Node 22.22). The arm's `writeFileSync` below would
|
|
743
743
|
// then land in the real SKILL.md — "the real one is never written" would be false in exactly the
|
|
@@ -397,7 +397,7 @@ const irrelevantFor = (c) => {
|
|
|
397
397
|
function assertSkillIdsExist() {
|
|
398
398
|
if (!existsSync(SKILLS_DIR))
|
|
399
399
|
throw new Error(`no skills dir at ${SKILLS_DIR}`);
|
|
400
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
400
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
401
401
|
const installed = new Set(installedSkills(SKILLS_DIR));
|
|
402
402
|
const missing = CASES.map((c) => c.skill).filter((s) => !installed.has(s));
|
|
403
403
|
if (missing.length)
|
|
@@ -449,7 +449,7 @@ const CASES = [
|
|
|
449
449
|
function assertSkillIdsExist() {
|
|
450
450
|
if (!existsSync(SKILLS_DIR))
|
|
451
451
|
throw new Error(`no skills dir at ${SKILLS_DIR}`);
|
|
452
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
452
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
453
453
|
const installed = new Set(installedSkills(SKILLS_DIR));
|
|
454
454
|
const missing = CASES.map((c) => c.skill).filter((s) => !installed.has(s));
|
|
455
455
|
if (missing.length)
|
|
@@ -30,3 +30,4 @@ export declare function packageVenuesDir(): string;
|
|
|
30
30
|
export declare const SHIPPED_SKILLS_DIR: string;
|
|
31
31
|
export declare const PACKAGE_NAME: "paperlint";
|
|
32
32
|
export declare const LEGACY_PACKAGE_NAME: "research-paper-pipeline";
|
|
33
|
+
export declare const BIN_FILE: "bin/paperlint.mjs";
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
pipelineScripts,
|
|
44
44
|
scriptsRoot,
|
|
45
45
|
PACKAGE_NAME,
|
|
46
|
+
BIN_FILE,
|
|
46
47
|
} from "./consumer.mjs";
|
|
47
48
|
|
|
48
49
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -63,6 +64,11 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
63
64
|
[PACKAGE_NAME],
|
|
64
65
|
"the package exposes exactly one command, named like the package",
|
|
65
66
|
);
|
|
67
|
+
assert.equal(
|
|
68
|
+
manifest.bin?.[PACKAGE_NAME],
|
|
69
|
+
BIN_FILE,
|
|
70
|
+
"package.json `bin` points at BIN_FILE from consumer.mjs — the file every hook command runs",
|
|
71
|
+
);
|
|
66
72
|
const wiring = JSON.parse(
|
|
67
73
|
readFileSync(join(root, "plugin", "hooks", "hooks.json"), "utf8"),
|
|
68
74
|
);
|
|
@@ -73,9 +79,9 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
73
79
|
assert.ok(
|
|
74
80
|
commands.length > 0 &&
|
|
75
81
|
commands.every((c) =>
|
|
76
|
-
c.includes(`/node_modules/${PACKAGE_NAME}
|
|
82
|
+
c.includes(`/node_modules/${PACKAGE_NAME}/${BIN_FILE}`),
|
|
77
83
|
),
|
|
78
|
-
`every command in plugin/hooks/hooks.json runs node_modules/${PACKAGE_NAME}
|
|
84
|
+
`every command in plugin/hooks/hooks.json runs node_modules/${PACKAGE_NAME}/${BIN_FILE}`,
|
|
79
85
|
);
|
|
80
86
|
}
|
|
81
87
|
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "consumer-harness-")));
|
|
@@ -472,7 +478,7 @@ assert.equal(
|
|
|
472
478
|
);
|
|
473
479
|
}
|
|
474
480
|
|
|
475
|
-
// ── XI. WHICH SKILLS ARE INSTALLED — a link counts, a dangling link is REFUSED (
|
|
481
|
+
// ── XI. WHICH SKILLS ARE INSTALLED — a link counts, a dangling link is REFUSED (paperlint#62) ────────
|
|
476
482
|
// `paperlint init` installs every skill as a SYMLINK `.claude/skills/<name> -> …/skills/<name>`. A
|
|
477
483
|
// `Dirent` from `readdirSync(…, { withFileTypes: true })` describes the entry ITSELF and does not
|
|
478
484
|
// follow links, so `e.isDirectory()` is false for every one of them: the eval preflights that
|
|
@@ -506,7 +512,7 @@ assert.equal(
|
|
|
506
512
|
const names = installedSkills(home);
|
|
507
513
|
assert.ok(
|
|
508
514
|
names.includes("linked"),
|
|
509
|
-
"a SYMLINKED skill was not counted as installed. This is
|
|
515
|
+
"a SYMLINKED skill was not counted as installed. This is paperlint#62: `paperlint init` installs every " +
|
|
510
516
|
"skill as a link, so a reader that does not follow links sees none of them in any consumer.",
|
|
511
517
|
);
|
|
512
518
|
assert.deepEqual(
|
|
@@ -151,6 +151,13 @@ export const PACKAGE_NAME = "paperlint";
|
|
|
151
151
|
*/
|
|
152
152
|
export const LEGACY_PACKAGE_NAME = "research-paper-pipeline";
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The package's one executable, relative to the package root — what `bin` in package.json names
|
|
156
|
+
* and what every hook command runs. Named like the package; `consumer.harness.mjs` checks it
|
|
157
|
+
* against the manifest, so the file and the hook commands cannot drift apart.
|
|
158
|
+
*/
|
|
159
|
+
export const BIN_FILE = `bin/${PACKAGE_NAME}.mjs`;
|
|
160
|
+
|
|
154
161
|
/**
|
|
155
162
|
* Where the package keeps the skills it ships, relative to the package root. One constant, read by
|
|
156
163
|
* the linker (`src/link-skills.ts`) and the install e2e, so the two cannot disagree; `files` in
|
|
@@ -168,7 +175,7 @@ export function consumerSkillsDir(opts) {
|
|
|
168
175
|
* sorted by name. The one answer to "which skills are here" — for the consumer's
|
|
169
176
|
* `.claude/skills/`, for this package's own declared skills directory, and for a copy of either.
|
|
170
177
|
*
|
|
171
|
-
* 🔴 IT FOLLOWS SYMLINKS, AND THAT IS THE WHOLE POINT (
|
|
178
|
+
* 🔴 IT FOLLOWS SYMLINKS, AND THAT IS THE WHOLE POINT (paperlint#62). `paperlint init` (`src/link-skills.ts`)
|
|
172
179
|
* installs every skill as a link `.claude/skills/<name> -> …/skills/<name>`. A `Dirent` from
|
|
173
180
|
* `readdirSync(dir, { withFileTypes: true })` describes the entry itself, so `isDirectory()` is
|
|
174
181
|
* false for every link: five eval preflights asked the question that way, saw zero skills in
|
|
@@ -117,7 +117,7 @@ const MUTATIONS = [
|
|
|
117
117
|
"the boundary between the pipeline's scripts and a directory merely named like them",
|
|
118
118
|
],
|
|
119
119
|
|
|
120
|
-
// ── `installedSkills` (
|
|
120
|
+
// ── `installedSkills` (paperlint#62). Its defects fail toward a WRONG ROSTER, not an error: a reader
|
|
121
121
|
// that does not follow links sees zero skills in every consumer; one that skips a dangling link
|
|
122
122
|
// reports a broken install as "not installed"; one that counts it hands out an unreadable skill.
|
|
123
123
|
[
|
|
@@ -4,7 +4,7 @@ description: Compile a LaTeX paper (ACM/IEEE/arXiv) to PDF and render its pages
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, SendUserFile]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:0943963cae95f8f1 compiled from skills/render-paper/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# render-paper — .tex → PDF → readable page PNGs
|
|
10
10
|
|
|
@@ -38,7 +38,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
|
|
|
38
38
|
## Toolchain (one command, not a checklist)
|
|
39
39
|
- **Compiler:** TeX Live, installed by paperlint with exactly the packages the venue profiles declare:
|
|
40
40
|
```
|
|
41
|
-
npx paperlint toolchain # into ~/.cache/
|
|
41
|
+
npx paperlint toolchain # into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); idempotent
|
|
42
42
|
npx paperlint toolchain --check # report what is missing, change nothing
|
|
43
43
|
```
|
|
44
44
|
Measured 2026-09-24: 3 min from an empty directory, 269 MB, TeX Live 2026, 47 packages verified
|
|
@@ -50,7 +50,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
|
|
|
50
50
|
package gets it there — never in a script, never here. A TeX Live without `libertine` builds an
|
|
51
51
|
acmart paper GREEN in Computer Modern; that is why the files are checked, not the exit code.
|
|
52
52
|
- **Reading the PDF** needs nothing installed: `paperlint build` and `extract-pdf-facts.mjs` use pdf.js,
|
|
53
|
-
which comes with
|
|
53
|
+
which comes with paperlint. Page size, columns and font sizes come from banal (HotCRP's page-geometry
|
|
54
54
|
script), which `npx paperlint toolchain` installs and which runs on pdf.js output — it needs `perl`,
|
|
55
55
|
and no poppler. Without banal those facts are `null` and the build says so.
|
|
56
56
|
- **Renderer:** `pip install --quiet pymupdf` (system rasterizers and ghostscript are often missing or
|