paperlint 2.1.0 → 3.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/CLAUDE.md +6 -1
- package/README.md +243 -235
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +60 -7
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +94 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +500 -288
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +40 -72
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -1
- package/dist/facts-file.d.ts +2 -2
- package/dist/facts-file.js +2 -2
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +39 -29
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +144 -173
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +49 -25
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +85 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +96 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +13 -41
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +71 -71
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -6
- package/dist/presets.js.map +1 -1
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +5 -3
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +15 -6
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +3 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.js +2 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +5 -1
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +22 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +26 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +163 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +65 -24
- package/docs/skills.md +80 -0
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +306 -191
- package/eslint-rules/paper-typography.test.mjs +337 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -31
- package/eslint-rules/pdf-last-page-balance.mjs +4 -7
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -25
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +83 -105
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +5 -14
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +54 -6
- package/src/build.ts +89 -11
- package/src/cli.harness.mjs +149 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +637 -328
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/engine.harness.mjs +50 -0
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +0 -7
- package/src/facts-file.ts +2 -2
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +187 -231
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +2 -0
- package/src/new-paper.mutations.mjs +0 -14
- package/src/new-paper.test.ts +173 -14
- package/src/new-paper.ts +82 -30
- package/src/paper-files.test.ts +395 -0
- package/src/paper-files.ts +200 -0
- package/src/paper-settings-commands.test.ts +121 -122
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/parse-args.test.ts +48 -0
- package/src/ports/check-references.ts +28 -0
- package/src/presets.ts +5 -15
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +19 -7
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +4 -2
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/toolchain.harness.mjs +41 -6
- package/src/toolchain.ts +29 -5
- package/src/types.ts +36 -18
- package/src/venue-rules.test.ts +23 -11
- package/src/venue-rules.ts +12 -14
- package/templates/paper/paperlint.json +1 -1
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
package/src/doctor.harness.mjs
CHANGED
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
* Killed by: src/doctor.mutations.mjs
|
|
13
13
|
*/
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
|
-
import {
|
|
16
|
-
OLD_PAPERS_DIR_FIELD,
|
|
17
|
-
PAPERS_DIR_FIELD,
|
|
18
|
-
} from "../lib/paper-config.mjs";
|
|
15
|
+
import { PAPERS_DIR_FIELD } from "../lib/paper-config.mjs";
|
|
19
16
|
import {
|
|
20
17
|
mkdtempSync,
|
|
21
18
|
mkdirSync,
|
|
@@ -41,8 +38,8 @@ const check = (label, cond) => {
|
|
|
41
38
|
assert.ok(cond, label);
|
|
42
39
|
};
|
|
43
40
|
|
|
44
|
-
/** A consumer on disk: a papers directory, and maybe a declaration in
|
|
45
|
-
function consumer({ papersDir,
|
|
41
|
+
/** A consumer on disk: a papers directory, and maybe a declaration in the root paperlint.json. */
|
|
42
|
+
function consumer({ papersDir, declared, makeDir = true }) {
|
|
46
43
|
const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-doctor-")));
|
|
47
44
|
if (makeDir && papersDir) {
|
|
48
45
|
mkdirSync(join(dir, papersDir, "some-paper"), { recursive: true });
|
|
@@ -51,9 +48,15 @@ function consumer({ papersDir, pkgKey, makeDir = true }) {
|
|
|
51
48
|
"# s\n",
|
|
52
49
|
);
|
|
53
50
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
writeFileSync(
|
|
52
|
+
join(dir, "package.json"),
|
|
53
|
+
JSON.stringify({ name: "consumer", version: "1.0.0" }, null, 2),
|
|
54
|
+
);
|
|
55
|
+
if (declared !== undefined)
|
|
56
|
+
writeFileSync(
|
|
57
|
+
join(dir, "paperlint.json"),
|
|
58
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: declared }, null, 2),
|
|
59
|
+
);
|
|
57
60
|
return dir;
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -76,7 +79,7 @@ const runDoctor = (
|
|
|
76
79
|
|
|
77
80
|
// ── I. A CONSISTENT INSTALL IS SILENT ───────────────────────────────────────────────────────
|
|
78
81
|
{
|
|
79
|
-
const dir = consumer({ papersDir: "papers",
|
|
82
|
+
const dir = consumer({ papersDir: "papers", declared: "papers" });
|
|
80
83
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
81
84
|
check("a consistent install — exit ZERO", r.code === 0);
|
|
82
85
|
check(
|
|
@@ -86,30 +89,9 @@ const runDoctor = (
|
|
|
86
89
|
rmSync(dir, { recursive: true, force: true });
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
// ── I½. THE OLD FIELD NAME IS A FAILURE, NAMED ─────────────────────────────────────────────
|
|
90
|
-
{
|
|
91
|
-
const dir = consumer({ papersDir: "papers", pkgKey: "papers" });
|
|
92
|
-
writeFileSync(
|
|
93
|
-
join(dir, "package.json"),
|
|
94
|
-
JSON.stringify({
|
|
95
|
-
name: "consumer",
|
|
96
|
-
paperlint: { [OLD_PAPERS_DIR_FIELD]: "papers" },
|
|
97
|
-
}),
|
|
98
|
-
);
|
|
99
|
-
const r = runDoctor(dir, { cliPapers: null });
|
|
100
|
-
check("the old field name — exit NON-zero", r.code !== 0);
|
|
101
|
-
check(
|
|
102
|
-
"and doctor says what the field was renamed to",
|
|
103
|
-
r.out.includes(
|
|
104
|
-
`"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "paperlint"`,
|
|
105
|
-
),
|
|
106
|
-
);
|
|
107
|
-
rmSync(dir, { recursive: true, force: true });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
92
|
// ── II. THE VERY DEFECT: AN INSTALL FOLLOWING THE DOCS ─────────────────────────────────────
|
|
111
|
-
// The
|
|
112
|
-
//
|
|
93
|
+
// The CLI lints one directory and the hook guards another: the install that motivated doctor
|
|
94
|
+
// (issue #33). Here the CLI was pointed elsewhere while the hook took the default.
|
|
113
95
|
{
|
|
114
96
|
const dir = consumer({ papersDir: "writing/drafts" });
|
|
115
97
|
const r = runDoctor(dir, { cliPapers: "writing/drafts" });
|
|
@@ -137,21 +119,31 @@ const runDoctor = (
|
|
|
137
119
|
rmSync(dir, { recursive: true, force: true });
|
|
138
120
|
}
|
|
139
121
|
|
|
140
|
-
// ── II-bis.
|
|
141
|
-
// The
|
|
142
|
-
// It must not fail here (an error-level false positive costs more than a miss), but it must
|
|
143
|
-
// not stay silent either.
|
|
122
|
+
// ── II-bis. NO paperlint.json — EVERY DEFAULT, AND THAT IS A VALID PROJECT ─────────────────
|
|
123
|
+
// The CLI and the hooks both take `papers` when nothing is declared, so they cannot disagree.
|
|
144
124
|
{
|
|
145
125
|
const dir = consumer({ papersDir: "papers" });
|
|
146
126
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
147
|
-
check("
|
|
127
|
+
check("no paperlint.json — exit ZERO", r.code === 0);
|
|
148
128
|
check(
|
|
149
|
-
"
|
|
150
|
-
|
|
129
|
+
"and it says the defaults are in play, naming the directory",
|
|
130
|
+
/✓ no paperlint\.json — every setting has its default; papers are in "papers"/.test(
|
|
131
|
+
r.out,
|
|
132
|
+
),
|
|
151
133
|
);
|
|
134
|
+
rmSync(dir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── II-quater. A paperlint.json THAT DOES NOT PARSE — the guard refuses every command ─────
|
|
138
|
+
{
|
|
139
|
+
const dir = consumer({ papersDir: "papers" });
|
|
140
|
+
writeFileSync(join(dir, "paperlint.json"), '{ "papersDir": <<<<<<< HEAD\n');
|
|
141
|
+
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
142
|
+
check("an unparsable paperlint.json — a FAILURE", r.code === 2);
|
|
152
143
|
check(
|
|
153
|
-
"
|
|
154
|
-
|
|
144
|
+
"named, with the way out (a file edit, not a command)",
|
|
145
|
+
/✗ paperlint\.json is not valid JSON/.test(r.out) &&
|
|
146
|
+
/Edit or Write/.test(r.out),
|
|
155
147
|
);
|
|
156
148
|
rmSync(dir, { recursive: true, force: true });
|
|
157
149
|
}
|
|
@@ -163,7 +155,7 @@ const runDoctor = (
|
|
|
163
155
|
{
|
|
164
156
|
const dir = consumer({
|
|
165
157
|
papersDir: "papers",
|
|
166
|
-
|
|
158
|
+
declared: "papers",
|
|
167
159
|
makeDir: false,
|
|
168
160
|
});
|
|
169
161
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
@@ -177,42 +169,9 @@ const runDoctor = (
|
|
|
177
169
|
rmSync(dir, { recursive: true, force: true });
|
|
178
170
|
}
|
|
179
171
|
|
|
180
|
-
// ── II-quater. THE KEY'S OLD NAME IS READ AND NAMED; TWO DIFFERENT KEYS ARE A FAILURE ──────
|
|
181
|
-
{
|
|
182
|
-
const dir = consumer({ papersDir: "papers" });
|
|
183
|
-
writeFileSync(
|
|
184
|
-
join(dir, "package.json"),
|
|
185
|
-
JSON.stringify({
|
|
186
|
-
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "papers" },
|
|
187
|
-
}),
|
|
188
|
-
);
|
|
189
|
-
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
190
|
-
check(
|
|
191
|
-
"the old key — read (not a failure), and named with the new one",
|
|
192
|
-
r.code === 0 &&
|
|
193
|
-
/⚠ "research-paper-pipeline" in package\.json is the old name/.test(
|
|
194
|
-
r.out,
|
|
195
|
-
) &&
|
|
196
|
-
/the hooks will guard papers/.test(r.out),
|
|
197
|
-
);
|
|
198
|
-
writeFileSync(
|
|
199
|
-
join(dir, "package.json"),
|
|
200
|
-
JSON.stringify({
|
|
201
|
-
paperlint: { [PAPERS_DIR_FIELD]: "papers" },
|
|
202
|
-
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "elsewhere" },
|
|
203
|
-
}),
|
|
204
|
-
);
|
|
205
|
-
const both = runDoctor(dir, { cliPapers: "papers" });
|
|
206
|
-
check(
|
|
207
|
-
"both keys with different contents — a failure",
|
|
208
|
-
both.code === 2 && /✗ package\.json has both/.test(both.out),
|
|
209
|
-
);
|
|
210
|
-
rmSync(dir, { recursive: true, force: true });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
172
|
// ── III. TWO DECLARATIONS HAVE DRIFTED APART ────────────────────────────────────────────────
|
|
214
173
|
{
|
|
215
|
-
const dir = consumer({ papersDir: "writing/drafts",
|
|
174
|
+
const dir = consumer({ papersDir: "writing/drafts", declared: "papers" });
|
|
216
175
|
mkdirSync(join(dir, "papers"), { recursive: true });
|
|
217
176
|
const r = runDoctor(dir, { cliPapers: "writing/drafts" });
|
|
218
177
|
check("both declarations exist, but differ — a FAILURE", r.code === 2);
|
|
@@ -223,12 +182,10 @@ const runDoctor = (
|
|
|
223
182
|
// This is load-bearing: a copy of the logic would drift silently and print a confident wrong
|
|
224
183
|
// answer.
|
|
225
184
|
{
|
|
226
|
-
const dir = consumer({ papersDir: "docs/papers",
|
|
185
|
+
const dir = consumer({ papersDir: "docs/papers", declared: "docs/papers/" });
|
|
227
186
|
const r = runDoctor(dir, { cliPapers: "docs/papers" });
|
|
228
187
|
const fromHook = papersRoot(
|
|
229
|
-
JSON.stringify({
|
|
230
|
-
paperlint: { [PAPERS_DIR_FIELD]: "docs/papers/" },
|
|
231
|
-
}),
|
|
188
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: "docs/papers/" }),
|
|
232
189
|
);
|
|
233
190
|
check("the hook itself trims the trailing slash", fromHook === "docs/papers");
|
|
234
191
|
check(
|
|
@@ -244,7 +201,7 @@ const runDoctor = (
|
|
|
244
201
|
|
|
245
202
|
// ── V. THE HOOK'S REFUSAL IS PASSED ALONG, NOT TURNED INTO A DIRECTORY ─────────────────────
|
|
246
203
|
{
|
|
247
|
-
const dir = consumer({ papersDir: "papers",
|
|
204
|
+
const dir = consumer({ papersDir: "papers", declared: "" });
|
|
248
205
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
249
206
|
check(
|
|
250
207
|
"an empty string in the declaration — the hook refuses, and doctor NAMES it",
|
|
@@ -255,7 +212,7 @@ const runDoctor = (
|
|
|
255
212
|
|
|
256
213
|
// ── VI. EXTERNAL PROGRAMS ARE A FACT, NOT A VERDICT ─────────────────────────────────────────
|
|
257
214
|
{
|
|
258
|
-
const dir = consumer({ papersDir: "papers",
|
|
215
|
+
const dir = consumer({ papersDir: "papers", declared: "papers" });
|
|
259
216
|
const none = runDoctor(dir, { cliPapers: "papers", have: () => 1 });
|
|
260
217
|
check(
|
|
261
218
|
"🔴 a missing tex install does NOT fail the run — a gate on advice would mute the whole thing",
|
|
@@ -330,7 +287,7 @@ const runDoctor = (
|
|
|
330
287
|
// nothing anywhere said so. The link state itself is `link-skills.harness.mjs`'s subject; here
|
|
331
288
|
// only doctor's REPORTING of it is judged, so the state is handed in rather than built on disk.
|
|
332
289
|
{
|
|
333
|
-
const dir = consumer({ papersDir: "papers",
|
|
290
|
+
const dir = consumer({ papersDir: "papers", declared: "papers" });
|
|
334
291
|
const state = {
|
|
335
292
|
ok: true,
|
|
336
293
|
home: join(dir, ".claude", "skills"),
|
package/src/doctor.mutations.mjs
CHANGED
|
@@ -46,8 +46,8 @@ process.exit(
|
|
|
46
46
|
edits: [
|
|
47
47
|
[
|
|
48
48
|
SRC,
|
|
49
|
-
|
|
50
|
-
' const hookRoot =
|
|
49
|
+
' const hookRoot = papersRoot(raw ?? "{}");',
|
|
50
|
+
' const hookRoot = raw === null ? "papers" : (() => { try { return JSON.parse(raw)?.papersDir ?? "papers"; } catch { return "papers"; } })();',
|
|
51
51
|
],
|
|
52
52
|
],
|
|
53
53
|
},
|
|
@@ -115,18 +115,18 @@ process.exit(
|
|
|
115
115
|
],
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
|
-
name: "
|
|
118
|
+
name: "an unparsable paperlint.json is reported as all-clear",
|
|
119
119
|
harness: HARNESS,
|
|
120
|
-
expect: "
|
|
120
|
+
expect: "an unparsable paperlint.json — a FAILURE",
|
|
121
121
|
disables:
|
|
122
|
-
"the
|
|
123
|
-
"
|
|
124
|
-
"
|
|
122
|
+
"the one failure left in the settings file: while it does not parse, the edit guard " +
|
|
123
|
+
"refuses every Bash command. A doctor that counts it as fine sends the reader looking " +
|
|
124
|
+
"everywhere but the file",
|
|
125
125
|
edits: [
|
|
126
126
|
[
|
|
127
127
|
SRC,
|
|
128
|
-
|
|
129
|
-
"
|
|
128
|
+
" ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 1,",
|
|
129
|
+
" ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 0,",
|
|
130
130
|
],
|
|
131
131
|
],
|
|
132
132
|
},
|
package/src/doctor.ts
CHANGED
|
@@ -29,19 +29,11 @@ import { spawnSync } from "node:child_process";
|
|
|
29
29
|
// the hook itself rather than at a shared module.
|
|
30
30
|
import {
|
|
31
31
|
papersRoot,
|
|
32
|
-
CONFIG_KEY,
|
|
33
32
|
DEFAULT_PAPERS_ROOT,
|
|
34
33
|
PAPERS_DIR_FIELD,
|
|
35
|
-
|
|
34
|
+
CONFIG_FILE,
|
|
36
35
|
} from "../hooks/paper-edit-guard.hook.mjs";
|
|
37
|
-
import {
|
|
38
|
-
LEGACY_CONFIG_KEY,
|
|
39
|
-
LEGACY_KEY_MESSAGE,
|
|
40
|
-
LEGACY_PAPER_SETTINGS_FILE,
|
|
41
|
-
PAPER_SETTINGS_FILE,
|
|
42
|
-
declaredSettings,
|
|
43
|
-
} from "../lib/paper-config.mjs";
|
|
44
|
-
import { PAPER_MARKERS, papersIn } from "./build.ts";
|
|
36
|
+
import { PAPER_MARKERS } from "./build.ts";
|
|
45
37
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
46
38
|
import { doctorHooks } from "./hooks-settings.ts";
|
|
47
39
|
|
|
@@ -148,51 +140,44 @@ export function detectPapers(cwd: string, depth = 2): string[] {
|
|
|
148
140
|
}
|
|
149
141
|
|
|
150
142
|
/**
|
|
151
|
-
* The
|
|
152
|
-
*
|
|
143
|
+
* The root `paperlint.json`'s verdict. Absent, or silent on `papersDir`, is fine: the default is
|
|
144
|
+
* `papers`, and the CLI and the hooks take it alike. Only a file that does not parse is a failure —
|
|
145
|
+
* the edit guard refuses every Bash command while it cannot read it.
|
|
153
146
|
*/
|
|
154
|
-
function declarationVerdict(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (found.legacy) out.push(` ⚠ ${LEGACY_KEY_MESSAGE}`);
|
|
170
|
-
// The old field name is a failure, not a warning: every reader refuses it.
|
|
171
|
-
if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD))
|
|
147
|
+
function declarationVerdict(raw: string | null): {
|
|
148
|
+
lines: string[];
|
|
149
|
+
bad: number;
|
|
150
|
+
} {
|
|
151
|
+
if (raw === null)
|
|
152
|
+
return {
|
|
153
|
+
lines: [
|
|
154
|
+
` ✓ no ${CONFIG_FILE} — every setting has its default; papers are in "${DEFAULT_PAPERS_ROOT}"`,
|
|
155
|
+
],
|
|
156
|
+
bad: 0,
|
|
157
|
+
};
|
|
158
|
+
let settings: unknown;
|
|
159
|
+
try {
|
|
160
|
+
settings = JSON.parse(raw);
|
|
161
|
+
} catch (e) {
|
|
172
162
|
return {
|
|
173
163
|
lines: [
|
|
174
|
-
|
|
175
|
-
`
|
|
164
|
+
` ✗ ${CONFIG_FILE} is not valid JSON: ${(e as Error).message}`,
|
|
165
|
+
` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,
|
|
176
166
|
],
|
|
177
167
|
bad: 1,
|
|
178
168
|
};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
else
|
|
192
|
-
out.push(
|
|
193
|
-
` ✓ package.json → ${key}.${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
|
|
194
|
-
);
|
|
195
|
-
return { lines: out, bad: 0 };
|
|
169
|
+
}
|
|
170
|
+
const declared = (settings as Record<string, unknown> | null)?.[
|
|
171
|
+
PAPERS_DIR_FIELD
|
|
172
|
+
];
|
|
173
|
+
return {
|
|
174
|
+
lines: [
|
|
175
|
+
declared === undefined
|
|
176
|
+
? ` ✓ ${CONFIG_FILE} names no ${PAPERS_DIR_FIELD} — the default "${DEFAULT_PAPERS_ROOT}"`
|
|
177
|
+
: ` ✓ ${CONFIG_FILE} → ${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
|
|
178
|
+
],
|
|
179
|
+
bad: 0,
|
|
180
|
+
};
|
|
196
181
|
}
|
|
197
182
|
|
|
198
183
|
/**
|
|
@@ -235,28 +220,6 @@ function papersVerdict(
|
|
|
235
220
|
return { lines: out, bad };
|
|
236
221
|
}
|
|
237
222
|
|
|
238
|
-
/**
|
|
239
|
-
* A pre-2.1.0 `venue.json` left in a paper: it is no longer read, so that paper's venue checks
|
|
240
|
-
* silently would not run if nothing named it. `init` moves it.
|
|
241
|
-
*/
|
|
242
|
-
function leftoverSettingsVerdict(
|
|
243
|
-
root: string,
|
|
244
|
-
papersDir: string | null,
|
|
245
|
-
): { lines: string[]; bad: number } {
|
|
246
|
-
if (papersDir === null) return { lines: [], bad: 0 };
|
|
247
|
-
const lines = papersIn(resolve(root, papersDir), [
|
|
248
|
-
...PAPER_MARKERS,
|
|
249
|
-
LEGACY_PAPER_SETTINGS_FILE,
|
|
250
|
-
])
|
|
251
|
-
.map((dir) => join(dir, LEGACY_PAPER_SETTINGS_FILE))
|
|
252
|
-
.filter((f) => existsSync(f))
|
|
253
|
-
.map(
|
|
254
|
-
(f) =>
|
|
255
|
-
` ✗ ${relative(root, f)} is no longer read — paperlint 2.1.0 renamed it ${PAPER_SETTINGS_FILE}, so this paper's venue checks do not run. \`npx paperlint init\` moves it`,
|
|
256
|
-
);
|
|
257
|
-
return { lines, bad: lines.length };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
223
|
export interface DoctorOptions {
|
|
261
224
|
log?: typeof console.log;
|
|
262
225
|
cwd?: string;
|
|
@@ -285,7 +248,7 @@ export function doctor({
|
|
|
285
248
|
skillLinks = (r: string) => linkSkills(r, { write: false }),
|
|
286
249
|
}: DoctorOptions = {}): number {
|
|
287
250
|
const root = projectDir ?? cwd;
|
|
288
|
-
const
|
|
251
|
+
const configPath = join(root, CONFIG_FILE);
|
|
289
252
|
const out: string[] = [
|
|
290
253
|
"",
|
|
291
254
|
"paperlint doctor — what is wired, and what only looks wired",
|
|
@@ -293,22 +256,15 @@ export function doctor({
|
|
|
293
256
|
];
|
|
294
257
|
let bad = 0;
|
|
295
258
|
|
|
296
|
-
out.push("
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
` the hooks read their papers directory from there and refuse without it`,
|
|
302
|
-
);
|
|
303
|
-
bad++;
|
|
304
|
-
} else {
|
|
305
|
-
const declaration = declarationVerdict(rawPkg);
|
|
306
|
-
out.push(...declaration.lines);
|
|
307
|
-
bad += declaration.bad;
|
|
308
|
-
}
|
|
259
|
+
out.push("settings");
|
|
260
|
+
const raw = existsSync(configPath) ? readFileSync(configPath, "utf8") : null;
|
|
261
|
+
const declaration = declarationVerdict(raw);
|
|
262
|
+
out.push(...declaration.lines);
|
|
263
|
+
bad += declaration.bad;
|
|
309
264
|
|
|
310
265
|
out.push("", "papers directory");
|
|
311
|
-
|
|
266
|
+
// The hook is given what its provider gives it: the file, or `{}` when there is none.
|
|
267
|
+
const hookRoot = papersRoot(raw ?? "{}");
|
|
312
268
|
const hookSays = typeof hookRoot === "string" ? hookRoot : null;
|
|
313
269
|
out.push(
|
|
314
270
|
` the CLI will lint ${cliPapers ?? "(nothing — no declaration found)"}`,
|
|
@@ -317,9 +273,8 @@ export function doctor({
|
|
|
317
273
|
` the hooks will guard ${hookSays ?? "(nothing — the guard refuses and says why on first use)"}`,
|
|
318
274
|
);
|
|
319
275
|
const verdict = papersVerdict(root, cliPapers, hookSays);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
bad += verdict.bad + leftover.bad;
|
|
276
|
+
out.push(...verdict.lines);
|
|
277
|
+
bad += verdict.bad;
|
|
323
278
|
|
|
324
279
|
// A skill that is not linked is ADVISORY, like a missing program: `paperlint lint`, the hooks and CI
|
|
325
280
|
// work without it, and an entry of the same name that `init` refused to replace is the
|
package/src/engine.harness.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
whichOnPath,
|
|
29
29
|
isExecutable,
|
|
30
30
|
probeTree,
|
|
31
|
+
missingDependencies,
|
|
31
32
|
supportedPlatform,
|
|
32
33
|
} = await import(join(HERE, "engine.ts"));
|
|
33
34
|
|
|
@@ -251,4 +252,53 @@ check(
|
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
// ── missingDependencies: what an install that "continued anyway" left out ──────────────────
|
|
256
|
+
// Real stdout of `tlmgr check depends` on a tree whose install-tl lost unicode-data to a mirror
|
|
257
|
+
// (2026-09-26): the pdflatex format could not be built, yet every venue .sty was present.
|
|
258
|
+
// Byte for byte: tlmgr opens every section header with a FORM FEED, then a space.
|
|
259
|
+
const BROKEN_DEPENDS =
|
|
260
|
+
"\f DEPENDS WITHOUT PACKAGES:\n" +
|
|
261
|
+
"unicode-data in: collection-basic latex-bin luahbtex luatex\n" +
|
|
262
|
+
"\f PACKAGES NOT IN ANY COLLECTION: acmart booktabs caption\n";
|
|
263
|
+
{
|
|
264
|
+
const calls = [];
|
|
265
|
+
const run = (cmd, args) => {
|
|
266
|
+
calls.push([cmd, args]);
|
|
267
|
+
return {
|
|
268
|
+
stdout: BROKEN_DEPENDS,
|
|
269
|
+
stderr: "tlmgr: An error has occurred.",
|
|
270
|
+
status: 2,
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
const missing = missingDependencies("/tl/bin", run);
|
|
274
|
+
check(
|
|
275
|
+
"missingDependencies: asks THAT tree's tlmgr `check depends`",
|
|
276
|
+
calls.length === 1 &&
|
|
277
|
+
calls[0][0] === "/tl/bin/tlmgr" &&
|
|
278
|
+
JSON.stringify(calls[0][1]) === '["check","depends"]',
|
|
279
|
+
JSON.stringify(calls),
|
|
280
|
+
);
|
|
281
|
+
// Guards: a package the tree's own database requires but never installed is a gap.
|
|
282
|
+
check(
|
|
283
|
+
"🔴 missingDependencies: names the dependency the install dropped",
|
|
284
|
+
JSON.stringify(missing) === '["unicode-data"]',
|
|
285
|
+
JSON.stringify(missing),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
check(
|
|
289
|
+
"missingDependencies: packages outside any collection are not gaps",
|
|
290
|
+
JSON.stringify(
|
|
291
|
+
missingDependencies("/tl/bin", () => ({
|
|
292
|
+
stdout: "\f PACKAGES NOT IN ANY COLLECTION: acmart booktabs\n",
|
|
293
|
+
status: 2,
|
|
294
|
+
})),
|
|
295
|
+
) === "[]",
|
|
296
|
+
);
|
|
297
|
+
check(
|
|
298
|
+
"missingDependencies: a clean tree has none",
|
|
299
|
+
JSON.stringify(
|
|
300
|
+
missingDependencies("/tl/bin", () => ({ stdout: "", status: 0 })),
|
|
301
|
+
) === "[]",
|
|
302
|
+
);
|
|
303
|
+
|
|
254
304
|
console.log(`engine: ${n} checks passed`);
|
package/src/engine.ts
CHANGED
|
@@ -164,6 +164,41 @@ export function probeTree(
|
|
|
164
164
|
return missingPackages(packages, String(r.stdout ?? ""));
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Packages the tree's OWN database lists as dependencies but never installed, by its `tlmgr check
|
|
169
|
+
* depends`. 🔴 The venue's `.sty` files are not enough evidence of a working tree: install-tl that
|
|
170
|
+
* cannot download a package prints "continuing anyway", and a missing `unicode-data` leaves every
|
|
171
|
+
* `.sty` in place while the pdflatex format cannot be built — pdflatex then dies before writing a
|
|
172
|
+
* log. Only paperlint's cache trees are asked: their tlmgr writes to a directory we own. A tlmgr
|
|
173
|
+
* that cannot start returns nothing here; the tree is then judged by `probeTree` alone.
|
|
174
|
+
*/
|
|
175
|
+
export function missingDependencies(
|
|
176
|
+
bin: string,
|
|
177
|
+
run: Runner = spawnSync,
|
|
178
|
+
): string[] {
|
|
179
|
+
const r = run(join(bin, "tlmgr"), ["check", "depends"], {
|
|
180
|
+
encoding: "utf8",
|
|
181
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
182
|
+
});
|
|
183
|
+
if (r.error) return [];
|
|
184
|
+
const lines = String(r.stdout ?? "").split("\n");
|
|
185
|
+
const start = lines.findIndex(
|
|
186
|
+
(l) => l.trim() === "DEPENDS WITHOUT PACKAGES:",
|
|
187
|
+
);
|
|
188
|
+
if (start < 0) return [];
|
|
189
|
+
// The section runs until tlmgr's next header. Headers open with a form feed and a space, entries
|
|
190
|
+
// with the package name, so a header is any line with leading whitespace.
|
|
191
|
+
const section = lines.slice(start + 1);
|
|
192
|
+
const end = section.findIndex((l) => l !== l.trimStart() || !l.trim());
|
|
193
|
+
return [
|
|
194
|
+
...new Set(
|
|
195
|
+
(end < 0 ? section : section.slice(0, end)).map((l) =>
|
|
196
|
+
l.split(" in: ")[0]!.trim(),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
].sort();
|
|
200
|
+
}
|
|
201
|
+
|
|
167
202
|
/** The first directory on `path` holding a runnable `name` — what a shell would run. */
|
|
168
203
|
export function whichOnPath(
|
|
169
204
|
name: string,
|
package/src/facts-file.test.ts
CHANGED
|
@@ -272,13 +272,6 @@ test("declaredVenue: the label is the preset's, and an extends that resolves now
|
|
|
272
272
|
assert.throws(() => declaredVenue(typo, PAPER), /shipped presets/);
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
test("declaredVenue: a venue.json alone is refused with the command that moves it, not read", () => {
|
|
276
|
-
const files = memoryFiles({
|
|
277
|
-
[`${PAPER}/venue.json`]: '{"venue":"agenticdev"}',
|
|
278
|
-
});
|
|
279
|
-
assert.throws(() => declaredVenue(files, PAPER), /npx paperlint init/);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
275
|
test("declaredVenue: a paperlint.json with an unknown key is refused, naming the key", () => {
|
|
283
276
|
const files = memoryFiles({
|
|
284
277
|
[`${PAPER}/paperlint.json`]: '{"venu":"agenticdev"}',
|
package/src/facts-file.ts
CHANGED
|
@@ -75,8 +75,8 @@ export interface VenueDecl {
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* What a paper's `paperlint.json` declares, or null when it has none. THROWS with a one-line reason
|
|
78
|
-
* when the file does not parse
|
|
79
|
-
*
|
|
78
|
+
* when the file does not parse or its `extends` does not resolve: the build reports that as its
|
|
79
|
+
* `facts` failure rather than building against no venue.
|
|
80
80
|
*/
|
|
81
81
|
export function declaredVenue(
|
|
82
82
|
files: Files,
|