paperlint 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -1
- package/CONTRIBUTING.md +21 -0
- package/README.md +231 -180
- 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 +5 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +16 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +40 -5
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +37 -20
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +377 -251
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +37 -52
- package/dist/doctor.js.map +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 -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 +40 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +144 -109
- 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 +2 -2
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +6 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +63 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +69 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +40 -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/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 +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +184 -0
- package/dist/presets.js.map +1 -0
- 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 +11 -2
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +25 -11
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +3 -2
- 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/types.d.ts +20 -18
- 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 +389 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +152 -77
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +26 -24
- package/docs/rules.md +181 -15
- 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 +267 -192
- package/eslint-rules/paper-typography.test.mjs +297 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -27
- package/eslint-rules/pdf-last-page-balance.mjs +23 -5
- 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/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- 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 -18
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +90 -81
- 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 +4 -13
- 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/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -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/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/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/extract-pdf-facts.harness.mjs +5 -5
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- 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 +119 -29
- 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 +10 -2
- package/src/build.harness.mjs +31 -22
- package/src/build.ts +65 -8
- package/src/cli.harness.mjs +151 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +457 -288
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +41 -61
- package/src/facts-file.test.ts +47 -7
- package/src/facts-file.ts +145 -24
- 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 +193 -159
- 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/new-paper.harness.mjs +9 -6
- package/src/new-paper.test.ts +133 -0
- package/src/new-paper.ts +7 -2
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +274 -0
- package/src/paper-settings.test.ts +249 -0
- package/src/paper-settings.ts +208 -0
- package/src/ports/check-references.ts +28 -0
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +297 -0
- 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 +30 -13
- package/src/structure.harness.mjs +6 -4
- package/src/structure.ts +3 -2
- package/src/tex-requirements.harness.mjs +11 -17
- package/src/tex-requirements.ts +179 -29
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +554 -0
- package/src/venue-rules.ts +571 -0
- package/templates/paper/paperlint.json +4 -0
- 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/build-e2e/acmart/venue.json +0 -1
- 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/cli.harness.mjs
CHANGED
|
@@ -24,18 +24,16 @@ import {
|
|
|
24
24
|
realpathSync,
|
|
25
25
|
symlinkSync,
|
|
26
26
|
readdirSync,
|
|
27
|
+
renameSync,
|
|
27
28
|
} from "node:fs";
|
|
28
29
|
import { spawnSync } from "node:child_process";
|
|
29
30
|
import { tmpdir } from "node:os";
|
|
30
31
|
import { join, dirname } from "node:path";
|
|
31
32
|
import { fileURLToPath } from "node:url";
|
|
32
|
-
import {
|
|
33
|
-
OLD_PAPERS_DIR_FIELD,
|
|
34
|
-
PAPERS_DIR_FIELD,
|
|
35
|
-
} from "../lib/paper-config.mjs";
|
|
33
|
+
import { PAPERS_DIR_FIELD, findProjectRoot } from "../lib/paper-config.mjs";
|
|
36
34
|
|
|
37
35
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
38
|
-
const { run, parseArgs, buildConfig, nextSteps,
|
|
36
|
+
const { run, parseArgs, buildConfig, nextSteps, toPaths, runHook } =
|
|
39
37
|
await import(join(HERE, "cli.ts"));
|
|
40
38
|
const { init, choosePapers, offerWorkflow, missingPrograms, WORKFLOW_PATH } =
|
|
41
39
|
await import(join(HERE, "init.ts"));
|
|
@@ -133,19 +131,16 @@ check(
|
|
|
133
131
|
|
|
134
132
|
// ── config is assembled, and consumer data gets through ────────────────────────────────────
|
|
135
133
|
{
|
|
136
|
-
const cfg = buildConfig(
|
|
137
|
-
{ typographyDebt: { x: { sectionSign: 3 } }, authorListCommand: "run-me" },
|
|
138
|
-
null,
|
|
139
|
-
);
|
|
134
|
+
const cfg = buildConfig({}, null);
|
|
140
135
|
// Blocks with `files` — the global `ignores` block below is not a rule block.
|
|
141
136
|
const ruleBlocks = (c) => c.filter((b) => Array.isArray(b.files));
|
|
142
137
|
check(
|
|
143
138
|
"without a LaTeX language the config still gets built — a corpus with no .tex is not a reason to refuse",
|
|
144
|
-
Array.isArray(cfg) && ruleBlocks(cfg).length ===
|
|
139
|
+
Array.isArray(cfg) && ruleBlocks(cfg).length === 4,
|
|
145
140
|
);
|
|
146
141
|
check(
|
|
147
|
-
"with a LaTeX language a
|
|
148
|
-
ruleBlocks(buildConfig({}, {})).length ===
|
|
142
|
+
"with a LaTeX language a fifth block is added",
|
|
143
|
+
ruleBlocks(buildConfig({}, {})).length === 5,
|
|
149
144
|
);
|
|
150
145
|
check(
|
|
151
146
|
"🔴 the project's paper TEMPLATE directory is ignored — flat config does not skip dot-directories",
|
|
@@ -156,12 +151,13 @@ check(
|
|
|
156
151
|
b.ignores.includes("**/.template/"),
|
|
157
152
|
),
|
|
158
153
|
);
|
|
159
|
-
const
|
|
160
|
-
c.files.some((f) => f.includes("
|
|
154
|
+
const siblings = ruleBlocks(cfg).find((c) =>
|
|
155
|
+
c.files.some((f) => f.includes("siblings")),
|
|
161
156
|
);
|
|
162
157
|
check(
|
|
163
|
-
"
|
|
164
|
-
|
|
158
|
+
"sibling cards are checked, and the siblings index is not a card",
|
|
159
|
+
siblings.rules["sibling/frontmatter"] === "warn" &&
|
|
160
|
+
siblings.ignores.some((g) => g.endsWith("siblings/README.md")),
|
|
165
161
|
);
|
|
166
162
|
}
|
|
167
163
|
|
|
@@ -175,12 +171,10 @@ check(
|
|
|
175
171
|
}
|
|
176
172
|
// ── ONE DECLARATION, READ BY THE SAME THING THAT WRITES IT ─────────────────────────────
|
|
177
173
|
//
|
|
178
|
-
// 🔴 WITHOUT THIS BLOCK
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// "nothing to lint". I.e. the install command and the check command would be looking at
|
|
183
|
-
// different files — exactly the defect it exists to close, just from the other side.
|
|
174
|
+
// 🔴 WITHOUT THIS BLOCK `init` COULD PRODUCE A BROKEN INSTALL. It writes one declaration, into
|
|
175
|
+
// the root `paperlint.json` — the file the hooks read at `$CLAUDE_PROJECT_DIR`. If the utility
|
|
176
|
+
// read another file, the install command and the check command would be looking at different
|
|
177
|
+
// files — exactly the defect it exists to close, just from the other side.
|
|
184
178
|
{
|
|
185
179
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-carrier-")));
|
|
186
180
|
try {
|
|
@@ -198,129 +192,47 @@ check(
|
|
|
198
192
|
);
|
|
199
193
|
writeFileSync(
|
|
200
194
|
join(root, "package.json"),
|
|
201
|
-
JSON.stringify(
|
|
202
|
-
{
|
|
203
|
-
name: "c",
|
|
204
|
-
version: "1.0.0",
|
|
205
|
-
paperlint: { [PAPERS_DIR_FIELD]: "papers" },
|
|
206
|
-
},
|
|
207
|
-
null,
|
|
208
|
-
2,
|
|
209
|
-
),
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
const r = await cli(["lint"], root);
|
|
213
|
-
check(
|
|
214
|
-
"🔴 THE UTILITY READS THE DECLARATION FROM package.json — otherwise `paperlint init` sets up something `paperlint lint` cannot see",
|
|
215
|
-
r.code === 0 && /no findings/.test(r.out),
|
|
216
|
-
);
|
|
217
|
-
check(
|
|
218
|
-
"and NAMES the carrier out loud — swapping settings is never silent",
|
|
219
|
-
/config: package\.json/.test(r.out),
|
|
220
|
-
);
|
|
221
|
-
check(
|
|
222
|
-
"package.json carries no deprecation notice — it is not the one that is deprecated",
|
|
223
|
-
!/is deprecated/.test(r.out),
|
|
195
|
+
JSON.stringify({ name: "c", version: "1.0.0" }),
|
|
224
196
|
);
|
|
225
197
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
join(root, "package.json"),
|
|
229
|
-
JSON.stringify({
|
|
230
|
-
name: "c",
|
|
231
|
-
version: "1.0.0",
|
|
232
|
-
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "papers" },
|
|
233
|
-
}),
|
|
234
|
-
);
|
|
235
|
-
const legacy = await cli(["lint"], root);
|
|
198
|
+
// No paperlint.json at all: every default, papers in `papers/`, and no config line.
|
|
199
|
+
const bare = await cli(["lint"], root);
|
|
236
200
|
check(
|
|
237
|
-
"🔴
|
|
238
|
-
|
|
239
|
-
/no findings/.test(legacy.out) &&
|
|
240
|
-
/"research-paper-pipeline" in package\.json is the old name .* rename it to "paperlint"/.test(
|
|
241
|
-
legacy.out,
|
|
242
|
-
),
|
|
243
|
-
);
|
|
244
|
-
// Guards: both keys, different contents — refused, since there is no telling which is meant.
|
|
245
|
-
writeFileSync(
|
|
246
|
-
join(root, "package.json"),
|
|
247
|
-
JSON.stringify({
|
|
248
|
-
name: "c",
|
|
249
|
-
version: "1.0.0",
|
|
250
|
-
paperlint: { [PAPERS_DIR_FIELD]: "papers" },
|
|
251
|
-
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "elsewhere" },
|
|
252
|
-
}),
|
|
201
|
+
"🔴 NO paperlint.json IS A VALID PROJECT — the default papers directory is linted",
|
|
202
|
+
bare.code === 0 && /no findings/.test(bare.out),
|
|
253
203
|
);
|
|
254
|
-
const both = await cli(["lint"], root);
|
|
255
204
|
check(
|
|
256
|
-
"
|
|
257
|
-
|
|
258
|
-
/has both "paperlint" and "research-paper-pipeline", and they differ/.test(
|
|
259
|
-
both.out,
|
|
260
|
-
),
|
|
205
|
+
"and no config line is printed when there is no file to name",
|
|
206
|
+
!/config:/.test(bare.out),
|
|
261
207
|
);
|
|
262
208
|
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
join(root, "package.json"),
|
|
267
|
-
JSON.stringify({ name: "c", version: "1.0.0", paperlint: {} }, null, 2),
|
|
268
|
-
);
|
|
269
|
-
const noPapers = await cli(["lint"], root);
|
|
209
|
+
// An empty root file is a file with every default, not an unfinished one.
|
|
210
|
+
writeFileSync(join(root, "paperlint.json"), "{}");
|
|
211
|
+
const empty = await cli(["lint"], root);
|
|
270
212
|
check(
|
|
271
|
-
`
|
|
272
|
-
|
|
273
|
-
noPapers.out.includes(`must declare \`${PAPERS_DIR_FIELD}\``) &&
|
|
274
|
-
noPapers.out.includes(
|
|
275
|
-
`"paperlint": { "${PAPERS_DIR_FIELD}": "papers" }`,
|
|
276
|
-
),
|
|
213
|
+
`an empty paperlint.json — no \`${PAPERS_DIR_FIELD}\`, so the default`,
|
|
214
|
+
empty.code === 0 && /no findings/.test(empty.out),
|
|
277
215
|
);
|
|
278
216
|
|
|
279
|
-
//
|
|
280
|
-
//
|
|
217
|
+
// 🔴 THE DECLARED DIRECTORY IS NOT THE DEFAULT: with `papers` declared, a CLI that ignored
|
|
218
|
+
// the file would lint the same tree and pass for the wrong reason.
|
|
219
|
+
renameSync(join(root, "papers"), join(root, "writing"));
|
|
281
220
|
writeFileSync(
|
|
282
|
-
join(root, "
|
|
283
|
-
JSON.stringify({
|
|
284
|
-
name: "c",
|
|
285
|
-
version: "1.0.0",
|
|
286
|
-
paperlint: {
|
|
287
|
-
[OLD_PAPERS_DIR_FIELD]: "papers",
|
|
288
|
-
[PAPERS_DIR_FIELD]: "papers",
|
|
289
|
-
},
|
|
290
|
-
}),
|
|
221
|
+
join(root, "paperlint.json"),
|
|
222
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: "writing" }),
|
|
291
223
|
);
|
|
292
|
-
const
|
|
224
|
+
const r = await cli(["lint"], root);
|
|
293
225
|
check(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
oldName.out.includes(
|
|
297
|
-
`"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "paperlint"`,
|
|
298
|
-
),
|
|
299
|
-
);
|
|
300
|
-
writeFileSync(
|
|
301
|
-
join(root, "package.json"),
|
|
302
|
-
JSON.stringify({
|
|
303
|
-
name: "c",
|
|
304
|
-
version: "1.0.0",
|
|
305
|
-
paperlint: { [OLD_PAPERS_DIR_FIELD]: "papers" },
|
|
306
|
-
}),
|
|
226
|
+
"🔴 THE UTILITY READS THE DECLARATION FROM paperlint.json — the file `paperlint init` writes and the hooks read",
|
|
227
|
+
r.code === 0 && /no findings/.test(r.out),
|
|
307
228
|
);
|
|
308
229
|
check(
|
|
309
|
-
"
|
|
310
|
-
|
|
230
|
+
"and NAMES the carrier out loud — swapping settings is never silent",
|
|
231
|
+
/config: paperlint\.json/.test(r.out),
|
|
311
232
|
);
|
|
312
233
|
|
|
313
|
-
// 🔴 A package.json
|
|
314
|
-
//
|
|
315
|
-
// and would never find anything, ever.
|
|
316
|
-
writeFileSync(
|
|
317
|
-
join(root, "package.json"),
|
|
318
|
-
JSON.stringify({
|
|
319
|
-
name: "c",
|
|
320
|
-
version: "1.0.0",
|
|
321
|
-
paperlint: { [PAPERS_DIR_FIELD]: "papers" },
|
|
322
|
-
}),
|
|
323
|
-
);
|
|
234
|
+
// 🔴 A package.json does not stop the walk upward while a root paperlint.json sits above:
|
|
235
|
+
// a nested package (a tools/ workspace) still belongs to the project.
|
|
324
236
|
const inner = join(root, "tools");
|
|
325
237
|
mkdirSync(inner, { recursive: true });
|
|
326
238
|
writeFileSync(
|
|
@@ -328,13 +240,13 @@ check(
|
|
|
328
240
|
JSON.stringify({ name: "tools", version: "1.0.0" }),
|
|
329
241
|
);
|
|
330
242
|
check(
|
|
331
|
-
"a
|
|
332
|
-
|
|
243
|
+
"a nested package.json does not intercept the search — the root paperlint.json above it is found",
|
|
244
|
+
findProjectRoot(inner) === root,
|
|
333
245
|
);
|
|
334
246
|
const viaInner = await cli(["lint"], inner);
|
|
335
247
|
check(
|
|
336
248
|
"and `lint` run from under it uses that declaration",
|
|
337
|
-
viaInner.code === 0 && /config: \.\.\/
|
|
249
|
+
viaInner.code === 0 && /config: \.\.\/paperlint\.json/.test(viaInner.out),
|
|
338
250
|
);
|
|
339
251
|
} finally {
|
|
340
252
|
rmSync(root, { recursive: true, force: true });
|
|
@@ -356,7 +268,11 @@ check(
|
|
|
356
268
|
const workRoot = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-init-")));
|
|
357
269
|
const project = (
|
|
358
270
|
name,
|
|
359
|
-
{
|
|
271
|
+
{
|
|
272
|
+
pkg = { name: "consumer", version: "1.0.0" },
|
|
273
|
+
papers = [],
|
|
274
|
+
settings = null,
|
|
275
|
+
} = {},
|
|
360
276
|
) => {
|
|
361
277
|
const dir = join(workRoot, name);
|
|
362
278
|
mkdirSync(dir, { recursive: true });
|
|
@@ -372,6 +288,11 @@ check(
|
|
|
372
288
|
join(dir, "package.json"),
|
|
373
289
|
JSON.stringify(pkg, null, 2) + "\n",
|
|
374
290
|
);
|
|
291
|
+
if (settings)
|
|
292
|
+
writeFileSync(
|
|
293
|
+
join(dir, "paperlint.json"),
|
|
294
|
+
JSON.stringify(settings, null, 2) + "\n",
|
|
295
|
+
);
|
|
375
296
|
return dir;
|
|
376
297
|
};
|
|
377
298
|
const say = () => {
|
|
@@ -382,8 +303,11 @@ check(
|
|
|
382
303
|
text: () => lines.join("\n"),
|
|
383
304
|
};
|
|
384
305
|
};
|
|
306
|
+
/** The root paperlint.json init left behind, or undefined when it wrote none. */
|
|
385
307
|
const declared = (dir) =>
|
|
386
|
-
|
|
308
|
+
existsSync(join(dir, "paperlint.json"))
|
|
309
|
+
? JSON.parse(readFileSync(join(dir, "paperlint.json"), "utf8"))
|
|
310
|
+
: undefined;
|
|
387
311
|
// No check should depend on what is installed ON THIS MACHINE: `command -v` is faked,
|
|
388
312
|
// otherwise "no external programs" would read as a finding about init.
|
|
389
313
|
const haveAll = () => ({ status: 0 });
|
|
@@ -400,8 +324,15 @@ check(
|
|
|
400
324
|
interactive: false,
|
|
401
325
|
run: haveAll,
|
|
402
326
|
});
|
|
327
|
+
// 🔴 THE REPORT IS CHECKED FIRST: a directory that was GUESSED lands on the default,
|
|
328
|
+
// and the default is not written — so the file check below would fail for a guess too,
|
|
329
|
+
// and could not tell "not measured" from "not written".
|
|
330
|
+
check(
|
|
331
|
+
"and it says HOW it was measured — otherwise a guess reads as a fact",
|
|
332
|
+
/measured: its subdirectories carry/.test(out.text()),
|
|
333
|
+
);
|
|
403
334
|
check(
|
|
404
|
-
"🔴 THE DECLARATION SHOWS UP IN
|
|
335
|
+
"🔴 THE DECLARATION SHOWS UP IN paperlint.json — the file the hooks read",
|
|
405
336
|
declared(dir) !== undefined &&
|
|
406
337
|
typeof declared(dir)[PAPERS_DIR_FIELD] === "string",
|
|
407
338
|
);
|
|
@@ -409,10 +340,6 @@ check(
|
|
|
409
340
|
"🔴 and its value is MEASURED, not taken from the `papers` default",
|
|
410
341
|
declared(dir)[PAPERS_DIR_FIELD] === "writing/drafts",
|
|
411
342
|
);
|
|
412
|
-
check(
|
|
413
|
-
"and it says HOW it was measured — otherwise a guess reads as a fact",
|
|
414
|
-
/measured: its subdirectories carry/.test(out.text()),
|
|
415
|
-
);
|
|
416
343
|
check(
|
|
417
344
|
"init ends with doctor's report: the install vouches for its OWN state",
|
|
418
345
|
/paperlint doctor — what is wired/.test(out.text()),
|
|
@@ -434,13 +361,14 @@ check(
|
|
|
434
361
|
run: haveAll,
|
|
435
362
|
});
|
|
436
363
|
check(
|
|
437
|
-
"nothing to measure — the
|
|
438
|
-
declared(dir)
|
|
364
|
+
"🔴 nothing to measure — the default is taken, and NO paperlint.json is written for it",
|
|
365
|
+
declared(dir) === undefined,
|
|
439
366
|
);
|
|
440
367
|
check(
|
|
441
|
-
"
|
|
442
|
-
/
|
|
443
|
-
|
|
368
|
+
"and it is called the default, not presented as a measurement",
|
|
369
|
+
/papers — the default\. Nothing here looks like a papers directory/.test(
|
|
370
|
+
out.text(),
|
|
371
|
+
),
|
|
444
372
|
);
|
|
445
373
|
// Guards: a fresh project with no papers anywhere is not a broken install. The guard has
|
|
446
374
|
// nothing to protect yet, and `init --yes` (an agent, CI) must finish green with a next step.
|
|
@@ -453,80 +381,13 @@ check(
|
|
|
453
381
|
);
|
|
454
382
|
}
|
|
455
383
|
|
|
456
|
-
// ── 2½. SETTINGS UNDER THE OLD KEY ARE MOVED TO THE NEW ONE ───────────────────────
|
|
457
|
-
{
|
|
458
|
-
const dir = project("old-key", {
|
|
459
|
-
pkg: {
|
|
460
|
-
name: "consumer",
|
|
461
|
-
"research-paper-pipeline": {
|
|
462
|
-
[PAPERS_DIR_FIELD]: "writing",
|
|
463
|
-
minFindings: 3,
|
|
464
|
-
},
|
|
465
|
-
version: "1.0.0",
|
|
466
|
-
},
|
|
467
|
-
papers: ["writing"],
|
|
468
|
-
});
|
|
469
|
-
const out = say();
|
|
470
|
-
await init(dir, {
|
|
471
|
-
log: out.log,
|
|
472
|
-
err: out.log,
|
|
473
|
-
interactive: false,
|
|
474
|
-
run: haveAll,
|
|
475
|
-
});
|
|
476
|
-
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
477
|
-
check(
|
|
478
|
-
"🔴 init moves the old key's settings to the new key, whole, and removes the old key",
|
|
479
|
-
JSON.stringify(pkg.paperlint) ===
|
|
480
|
-
JSON.stringify({ [PAPERS_DIR_FIELD]: "writing", minFindings: 3 }) &&
|
|
481
|
-
!("research-paper-pipeline" in pkg),
|
|
482
|
-
);
|
|
483
|
-
check(
|
|
484
|
-
"in the same position in the file, so the diff is a one-word rename",
|
|
485
|
-
Object.keys(pkg).join() === "name,paperlint,version",
|
|
486
|
-
);
|
|
487
|
-
check(
|
|
488
|
-
"and it says so",
|
|
489
|
-
/moved the settings from "research-paper-pipeline" \(the old key\) to "paperlint"/.test(
|
|
490
|
-
out.text(),
|
|
491
|
-
),
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
{
|
|
495
|
-
const dir = project("both-keys", {
|
|
496
|
-
pkg: {
|
|
497
|
-
name: "consumer",
|
|
498
|
-
paperlint: { [PAPERS_DIR_FIELD]: "writing" },
|
|
499
|
-
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "other" },
|
|
500
|
-
},
|
|
501
|
-
papers: ["writing"],
|
|
502
|
-
});
|
|
503
|
-
const out = say();
|
|
504
|
-
const code = await init(dir, {
|
|
505
|
-
log: out.log,
|
|
506
|
-
err: out.log,
|
|
507
|
-
interactive: false,
|
|
508
|
-
run: haveAll,
|
|
509
|
-
});
|
|
510
|
-
check(
|
|
511
|
-
"both keys with different contents — init refuses (exit 2) and writes nothing",
|
|
512
|
-
code === 2 &&
|
|
513
|
-
/they differ/.test(out.text()) &&
|
|
514
|
-
"research-paper-pipeline" in
|
|
515
|
-
JSON.parse(readFileSync(join(dir, "package.json"), "utf8")),
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
384
|
// ── 3. SOMEONE ELSE'S VALUE DOES NOT GET OVERWRITTEN ─────────────────────────────
|
|
520
385
|
{
|
|
521
386
|
const dir = project("mine", {
|
|
522
|
-
|
|
523
|
-
name: "c",
|
|
524
|
-
version: "1.0.0",
|
|
525
|
-
paperlint: { [PAPERS_DIR_FIELD]: "mine" },
|
|
526
|
-
},
|
|
387
|
+
settings: { [PAPERS_DIR_FIELD]: "mine" },
|
|
527
388
|
papers: ["writing"],
|
|
528
389
|
});
|
|
529
|
-
const before = readFileSync(join(dir, "
|
|
390
|
+
const before = readFileSync(join(dir, "paperlint.json"), "utf8");
|
|
530
391
|
const out = say();
|
|
531
392
|
await init(dir, {
|
|
532
393
|
log: out.log,
|
|
@@ -536,7 +397,7 @@ check(
|
|
|
536
397
|
});
|
|
537
398
|
check(
|
|
538
399
|
"🔴 an already-declared value stays intact byte for byte — silently replacing a setting is worse than doing nothing",
|
|
539
|
-
readFileSync(join(dir, "
|
|
400
|
+
readFileSync(join(dir, "paperlint.json"), "utf8") === before,
|
|
540
401
|
);
|
|
541
402
|
check(
|
|
542
403
|
"and it says so out loud, rather than skipping it",
|
|
@@ -546,24 +407,22 @@ check(
|
|
|
546
407
|
);
|
|
547
408
|
}
|
|
548
409
|
|
|
549
|
-
// ── 4.
|
|
410
|
+
// ── 4. NO package.json — the settings have their own file, so nothing is missing ─
|
|
550
411
|
{
|
|
551
412
|
const dir = project("nopkg", { pkg: null, papers: ["writing"] });
|
|
552
413
|
const out = say();
|
|
553
|
-
|
|
414
|
+
await init(dir, {
|
|
554
415
|
log: out.log,
|
|
555
416
|
err: out.log,
|
|
556
417
|
interactive: false,
|
|
557
418
|
run: haveAll,
|
|
558
419
|
});
|
|
559
420
|
check(
|
|
560
|
-
"without package.json init
|
|
561
|
-
|
|
562
|
-
/npm init -y/.test(out.text()) &&
|
|
563
|
-
/nowhere to put the declaration/.test(out.text()),
|
|
421
|
+
"without package.json init still declares the measured directory, in paperlint.json",
|
|
422
|
+
declared(dir)?.[PAPERS_DIR_FIELD] === "writing",
|
|
564
423
|
);
|
|
565
424
|
check(
|
|
566
|
-
"and creates
|
|
425
|
+
"and creates no package.json",
|
|
567
426
|
!existsSync(join(dir, "package.json")),
|
|
568
427
|
);
|
|
569
428
|
}
|
|
@@ -748,9 +607,15 @@ check(
|
|
|
748
607
|
"every absence is NAMED, and the count matches the names",
|
|
749
608
|
/✗ 6 of 6 missing: pdflatex, bibtex/.test(own),
|
|
750
609
|
);
|
|
610
|
+
// The programs section alone: the TeX Live step above it and the "next:" lines below it
|
|
611
|
+
// name `npx paperlint toolchain` too, and would answer for a section that lost its remedy.
|
|
612
|
+
const programs = own.slice(
|
|
613
|
+
own.indexOf("external programs"),
|
|
614
|
+
own.indexOf("next:"),
|
|
615
|
+
);
|
|
751
616
|
check(
|
|
752
617
|
"🔴 and it carries the INSTALL COMMAND — a remedy, not just a diagnosis",
|
|
753
|
-
/npx paperlint toolchain/.test(
|
|
618
|
+
/npx paperlint toolchain/.test(programs),
|
|
754
619
|
);
|
|
755
620
|
check(
|
|
756
621
|
"and it says outright that nothing is installed on the user's behalf",
|
|
@@ -847,18 +712,17 @@ check(
|
|
|
847
712
|
);
|
|
848
713
|
}
|
|
849
714
|
{
|
|
850
|
-
// A "." default would give a green run over whatever happens to be lying around
|
|
851
|
-
//
|
|
852
|
-
// that case.
|
|
715
|
+
// A "." default would give a green run over whatever happens to be lying around. The default
|
|
716
|
+
// is `papers/`, and an empty directory with no config has none: a refusal naming BOTH ways out.
|
|
853
717
|
const bare = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-bare-")));
|
|
854
718
|
try {
|
|
855
719
|
const r = await cli(["lint"], bare);
|
|
856
720
|
check(
|
|
857
|
-
"`lint` with NO path
|
|
721
|
+
"`lint` with NO path, no config and no papers/ refuses and names BOTH ways out",
|
|
858
722
|
r.code === 2 &&
|
|
859
|
-
/
|
|
860
|
-
/paperlint
|
|
861
|
-
/
|
|
723
|
+
/no papers in papers\//.test(r.out) &&
|
|
724
|
+
/paperlint new <name>/.test(r.out) &&
|
|
725
|
+
/set "papersDir" in paperlint\.json/.test(r.out),
|
|
862
726
|
);
|
|
863
727
|
} finally {
|
|
864
728
|
rmSync(bare, { recursive: true, force: true });
|
|
@@ -968,14 +832,14 @@ check(
|
|
|
968
832
|
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\nresearchQuestion: "does it hold?"\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`;
|
|
969
833
|
writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(100));
|
|
970
834
|
|
|
971
|
-
//
|
|
835
|
+
// findProjectRoot — kept separate from the run so the failure is distinguishable
|
|
972
836
|
writeFileSync(
|
|
973
|
-
join(root, "
|
|
974
|
-
JSON.stringify({
|
|
837
|
+
join(root, "paperlint.json"),
|
|
838
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
|
|
975
839
|
);
|
|
976
840
|
check(
|
|
977
|
-
"
|
|
978
|
-
|
|
841
|
+
"findProjectRoot walks up from inside a paper and finds the root file, not the paper's",
|
|
842
|
+
findProjectRoot(paper) === root,
|
|
979
843
|
);
|
|
980
844
|
// There can be no tautology here: a neighboring tree must NOT pick up our file. Asserting
|
|
981
845
|
// `=== null` against a live filesystem is not safe — someone else's package.json may sit further
|
|
@@ -986,7 +850,7 @@ check(
|
|
|
986
850
|
);
|
|
987
851
|
check(
|
|
988
852
|
"the config does NOT leak into a neighboring tree — the search goes up, not sideways",
|
|
989
|
-
|
|
853
|
+
findProjectRoot(sibling) !== root,
|
|
990
854
|
);
|
|
991
855
|
rmSync(sibling, { recursive: true, force: true });
|
|
992
856
|
|
|
@@ -997,7 +861,7 @@ check(
|
|
|
997
861
|
);
|
|
998
862
|
check(
|
|
999
863
|
"and the found file is NAMED out loud — swapping settings silently is not acceptable",
|
|
1000
|
-
/config:
|
|
864
|
+
/config: paperlint\.json/.test(found.out),
|
|
1001
865
|
);
|
|
1002
866
|
|
|
1003
867
|
// 🔴 THE RESOLUTION DISCRIMINATOR. Run FROM the paper's directory: same config, and
|
|
@@ -1028,26 +892,23 @@ check(
|
|
|
1028
892
|
/nothing was linted under elsewhere/.test(override.out),
|
|
1029
893
|
);
|
|
1030
894
|
|
|
1031
|
-
//
|
|
1032
|
-
// unfinished one: silently falling back to "." means running the rules over the whole checkout.
|
|
895
|
+
// The papers directory is OPTIONAL: a config without it takes `papers`, never ".".
|
|
1033
896
|
writeFileSync(
|
|
1034
|
-
join(root, "
|
|
1035
|
-
JSON.stringify({
|
|
897
|
+
join(root, "paperlint.json"),
|
|
898
|
+
JSON.stringify({ structure: false }),
|
|
1036
899
|
);
|
|
1037
900
|
const noPapers = await cli(["lint"], root);
|
|
1038
901
|
check(
|
|
1039
|
-
"a config WITHOUT the papers-directory field
|
|
1040
|
-
noPapers.code ===
|
|
1041
|
-
noPapers.out.includes(`must declare \`${PAPERS_DIR_FIELD}\``) &&
|
|
1042
|
-
/cannot guess/.test(noPapers.out),
|
|
902
|
+
"a config WITHOUT the papers-directory field lints the default `papers/`",
|
|
903
|
+
noPapers.code === 0 && /no findings/.test(noPapers.out),
|
|
1043
904
|
);
|
|
1044
905
|
check(
|
|
1045
|
-
'an empty string as the papers directory
|
|
906
|
+
'an empty string as the papers directory is refused, not read as the directory ""',
|
|
1046
907
|
(
|
|
1047
908
|
await (async () => {
|
|
1048
909
|
writeFileSync(
|
|
1049
|
-
join(root, "
|
|
1050
|
-
JSON.stringify({
|
|
910
|
+
join(root, "paperlint.json"),
|
|
911
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: "" }),
|
|
1051
912
|
);
|
|
1052
913
|
return await cli(["lint"], root);
|
|
1053
914
|
})()
|
|
@@ -1073,7 +934,7 @@ check(
|
|
|
1073
934
|
join(paper, "versions", "2026-07-22-submitted.pdf"),
|
|
1074
935
|
"x".repeat(100),
|
|
1075
936
|
);
|
|
1076
|
-
// Three § signs —
|
|
937
|
+
// Three § signs — `paper/section-word`, warn level and only warn.
|
|
1077
938
|
writeFileSync(
|
|
1078
939
|
join(paper, "paper.md"),
|
|
1079
940
|
"# Intro\n\nRQ1: does it hold?\n\nSee \u00a7 5 and \u00a7 6 and \u00a7 7.\n",
|
|
@@ -1083,8 +944,8 @@ check(
|
|
|
1083
944
|
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\nresearchQuestion: "does it hold?"\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`,
|
|
1084
945
|
);
|
|
1085
946
|
writeFileSync(
|
|
1086
|
-
join(root, "
|
|
1087
|
-
JSON.stringify({
|
|
947
|
+
join(root, "paperlint.json"),
|
|
948
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
|
|
1088
949
|
);
|
|
1089
950
|
|
|
1090
951
|
const lax = await cli(["lint"], root);
|
|
@@ -1099,7 +960,7 @@ check(
|
|
|
1099
960
|
);
|
|
1100
961
|
check(
|
|
1101
962
|
'and the failure names the NUMBER and the THRESHOLD, not just "too many"',
|
|
1102
|
-
/
|
|
963
|
+
/3 warning\(s\) exceed the --max-warnings limit of 0/.test(strict.out),
|
|
1103
964
|
);
|
|
1104
965
|
const generous = await cli(["lint", "--max-warnings", "5"], root);
|
|
1105
966
|
check(
|
|
@@ -1124,8 +985,8 @@ check(
|
|
|
1124
985
|
// The marker is present, the scorecard is not: NOT ONE pipeline rule runs over this directory.
|
|
1125
986
|
writeFileSync(join(paper, "paper.tex"), "\\documentclass{article}\n");
|
|
1126
987
|
writeFileSync(
|
|
1127
|
-
join(root, "
|
|
1128
|
-
JSON.stringify({
|
|
988
|
+
join(root, "paperlint.json"),
|
|
989
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
|
|
1129
990
|
);
|
|
1130
991
|
|
|
1131
992
|
const r = await cli(["lint"], root);
|
|
@@ -1158,7 +1019,7 @@ check(
|
|
|
1158
1019
|
);
|
|
1159
1020
|
check(
|
|
1160
1021
|
"and the line about the found config is not lost — it went to stderr",
|
|
1161
|
-
/config:
|
|
1022
|
+
/config: paperlint\.json/.test(j.stderr) && !/config:/.test(j.stdout),
|
|
1162
1023
|
);
|
|
1163
1024
|
check(
|
|
1164
1025
|
"`--json` gives back ONE array, where the missing-file finding sits next to the rule findings",
|
|
@@ -1233,7 +1094,7 @@ check(
|
|
|
1233
1094
|
check(
|
|
1234
1095
|
"the inside run is the reference: it has both a structure and a rule finding",
|
|
1235
1096
|
findings(inside).some((f) => f.startsWith("structure/required-file")) &&
|
|
1236
|
-
findings(inside).some((f) => f.startsWith("paper/
|
|
1097
|
+
findings(inside).some((f) => f.startsWith("paper/section-word")),
|
|
1237
1098
|
);
|
|
1238
1099
|
check(
|
|
1239
1100
|
"and from outside it reports the SAME findings with the SAME exit code",
|
|
@@ -1241,44 +1102,44 @@ check(
|
|
|
1241
1102
|
outside.code === inside.code,
|
|
1242
1103
|
);
|
|
1243
1104
|
|
|
1244
|
-
//
|
|
1105
|
+
// A `rules` block, its `files` relative to the config's directory, turns the `§` rule off
|
|
1106
|
+
// for this paper — and it must reach the paper from wherever the command runs.
|
|
1245
1107
|
writeFileSync(
|
|
1246
|
-
join(tree, "
|
|
1108
|
+
join(tree, "paperlint.json"),
|
|
1247
1109
|
JSON.stringify({
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
[
|
|
1251
|
-
|
|
1252
|
-
},
|
|
1110
|
+
[PAPERS_DIR_FIELD]: "papers",
|
|
1111
|
+
rules: [
|
|
1112
|
+
{ files: ["papers/p/**"], rules: { "paper/section-word": "off" } },
|
|
1113
|
+
],
|
|
1253
1114
|
}),
|
|
1254
1115
|
);
|
|
1255
1116
|
const debtHonoured = (r) =>
|
|
1256
1117
|
r.code !== 99 &&
|
|
1257
|
-
!findings(r).some((f) => f.startsWith("paper/
|
|
1118
|
+
!findings(r).some((f) => f.startsWith("paper/section-word"));
|
|
1258
1119
|
check(
|
|
1259
|
-
"from the config's own directory the
|
|
1120
|
+
"from the config's own directory the block turns the `§` finding off",
|
|
1260
1121
|
debtHonoured(await cli(["lint", "--json"], tree)),
|
|
1261
1122
|
);
|
|
1262
1123
|
check(
|
|
1263
|
-
"from a SUBDIRECTORY (config found by walking up) the same
|
|
1124
|
+
"from a SUBDIRECTORY (config found by walking up) the same block still applies",
|
|
1264
1125
|
debtHonoured(await cli(["lint", "--json"], join(tree, "papers"))),
|
|
1265
1126
|
);
|
|
1266
1127
|
check(
|
|
1267
1128
|
"and with `--config` from an unrelated directory it applies too",
|
|
1268
1129
|
debtHonoured(
|
|
1269
1130
|
await cli(
|
|
1270
|
-
["lint", "--json", "--config", join(tree, "
|
|
1131
|
+
["lint", "--json", "--config", join(tree, "paperlint.json")],
|
|
1271
1132
|
elsewhere,
|
|
1272
1133
|
),
|
|
1273
1134
|
),
|
|
1274
1135
|
);
|
|
1275
1136
|
// A RELATIVE `--config` stays relative in `configPath`, while every paper path is absolute:
|
|
1276
|
-
// without resolving it first, the common root of `.` and `/tmp/…` is `/`,
|
|
1277
|
-
//
|
|
1137
|
+
// without resolving it first, the common root of `.` and `/tmp/…` is `/`, and globs written
|
|
1138
|
+
// relative to the config no longer match (Codex on #45).
|
|
1278
1139
|
check(
|
|
1279
|
-
"and with a RELATIVE `--config
|
|
1140
|
+
"and with a RELATIVE `--config paperlint.json` it applies too",
|
|
1280
1141
|
debtHonoured(
|
|
1281
|
-
await cli(["lint", "--json", "--config", "
|
|
1142
|
+
await cli(["lint", "--json", "--config", "paperlint.json"], tree),
|
|
1282
1143
|
),
|
|
1283
1144
|
);
|
|
1284
1145
|
} finally {
|
|
@@ -1678,8 +1539,8 @@ console.log(
|
|
|
1678
1539
|
// step, and every later step must use the kept value, not the directory init guessed.
|
|
1679
1540
|
const dir = bare("declared");
|
|
1680
1541
|
writeFileSync(
|
|
1681
|
-
join(dir, "
|
|
1682
|
-
`{"
|
|
1542
|
+
join(dir, "paperlint.json"),
|
|
1543
|
+
`{"${PAPERS_DIR_FIELD}":"writing"}\n`,
|
|
1683
1544
|
);
|
|
1684
1545
|
const made = [];
|
|
1685
1546
|
await init(dir, {
|
|
@@ -1743,8 +1604,8 @@ console.log(
|
|
|
1743
1604
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-template-")));
|
|
1744
1605
|
try {
|
|
1745
1606
|
writeFileSync(
|
|
1746
|
-
join(root, "
|
|
1747
|
-
`{"
|
|
1607
|
+
join(root, "paperlint.json"),
|
|
1608
|
+
`{"${PAPERS_DIR_FIELD}":"papers"}\n`,
|
|
1748
1609
|
);
|
|
1749
1610
|
mkdirSync(join(root, "papers", "p1"), { recursive: true });
|
|
1750
1611
|
writeFileSync(join(root, "papers", "p1", "paper.md"), "# P\n");
|
|
@@ -1784,22 +1645,24 @@ console.log(
|
|
|
1784
1645
|
{
|
|
1785
1646
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-new-cli-")));
|
|
1786
1647
|
try {
|
|
1787
|
-
const none = await cli(["new", "
|
|
1648
|
+
const none = await cli(["new", "first"], root);
|
|
1788
1649
|
check(
|
|
1789
|
-
"without a declaration
|
|
1790
|
-
none.code ===
|
|
1650
|
+
"without a declaration the paper goes into the default `papers/`",
|
|
1651
|
+
none.code === 0 && existsSync(join(root, "papers", "first", "paper.tex")),
|
|
1791
1652
|
);
|
|
1792
1653
|
writeFileSync(
|
|
1793
|
-
join(root, "
|
|
1794
|
-
`{"
|
|
1654
|
+
join(root, "paperlint.json"),
|
|
1655
|
+
`{"${PAPERS_DIR_FIELD}":"writing"}\n`,
|
|
1795
1656
|
);
|
|
1796
1657
|
const r = await cli(["new", "demo"], root);
|
|
1797
1658
|
check(
|
|
1798
|
-
"🔴 `paperlint new demo` creates writing/demo/ and its lint is the verdict — exit 0, no
|
|
1659
|
+
"🔴 `paperlint new demo` creates writing/demo/ and its lint is the verdict — exit 0, and only the warning that no venue is chosen yet",
|
|
1799
1660
|
r.code === 0 &&
|
|
1800
1661
|
existsSync(join(root, "writing", "demo", "PIPELINE-STATUS.md")) &&
|
|
1801
1662
|
existsSync(join(root, "writing", "demo", "paper.tex")) &&
|
|
1802
|
-
|
|
1663
|
+
existsSync(join(root, "writing", "demo", "paperlint.json")) &&
|
|
1664
|
+
/names no venue preset yet/.test(r.out) &&
|
|
1665
|
+
/\(0 errors, 1 warning\)/.test(r.out),
|
|
1803
1666
|
);
|
|
1804
1667
|
const again = await cli(["new", "demo", "--format", "md"], root);
|
|
1805
1668
|
check(
|
|
@@ -1840,12 +1703,8 @@ console.log(
|
|
|
1840
1703
|
);
|
|
1841
1704
|
const settings = (extra) =>
|
|
1842
1705
|
writeFileSync(
|
|
1843
|
-
join(root, "
|
|
1844
|
-
JSON.stringify({
|
|
1845
|
-
name: "c",
|
|
1846
|
-
version: "1.0.0",
|
|
1847
|
-
paperlint: { [PAPERS_DIR_FIELD]: "papers", ...extra },
|
|
1848
|
-
}),
|
|
1706
|
+
join(root, "paperlint.json"),
|
|
1707
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: "papers", ...extra }),
|
|
1849
1708
|
);
|
|
1850
1709
|
const balanceOn = (files) => ({
|
|
1851
1710
|
rules: [{ files, rules: { "pdf/last-page-balance": "error" } }],
|
|
@@ -1856,9 +1715,7 @@ console.log(
|
|
|
1856
1715
|
check(
|
|
1857
1716
|
"🔴 an unknown key is REFUSED by name — a typo must not read as 'not set'",
|
|
1858
1717
|
typo.code === 2 &&
|
|
1859
|
-
typo.out.includes(
|
|
1860
|
-
'package.json → "paperlint": unknown key "typographyDept"',
|
|
1861
|
-
),
|
|
1718
|
+
typo.out.includes('paperlint.json: unknown key "typographyDept"'),
|
|
1862
1719
|
);
|
|
1863
1720
|
settings({
|
|
1864
1721
|
ledger: "x.jsonl",
|
|
@@ -1872,10 +1729,10 @@ console.log(
|
|
|
1872
1729
|
"the keys the skill scripts read are known keys, not typos",
|
|
1873
1730
|
(await cli(["lint"], root)).code !== 2,
|
|
1874
1731
|
);
|
|
1875
|
-
settings({ rules:
|
|
1732
|
+
settings({ rules: "pdf/last-page-balance" });
|
|
1876
1733
|
check(
|
|
1877
|
-
"`rules` that is
|
|
1878
|
-
|
|
1734
|
+
"`rules` that is neither { id: severity } nor a list is refused, naming the key",
|
|
1735
|
+
/paperlint\.json → "rules" must be/.test((await cli(["lint"], root)).out),
|
|
1879
1736
|
);
|
|
1880
1737
|
settings({
|
|
1881
1738
|
rules: [{ files: ["papers/**"], rules: { "pdf/nope": "error" } }],
|
|
@@ -1885,7 +1742,7 @@ console.log(
|
|
|
1885
1742
|
"a rule paperlint does not ship is refused, naming the block and the rule",
|
|
1886
1743
|
unknownRule.code === 2 &&
|
|
1887
1744
|
unknownRule.out.includes(
|
|
1888
|
-
'
|
|
1745
|
+
'rules[0].rules: "pdf/nope" is not a rule paperlint ships',
|
|
1889
1746
|
),
|
|
1890
1747
|
);
|
|
1891
1748
|
settings({
|
|
@@ -1930,7 +1787,7 @@ console.log(
|
|
|
1930
1787
|
),
|
|
1931
1788
|
);
|
|
1932
1789
|
settings({
|
|
1933
|
-
rules: [{ files: ["papers/**"], rules: { "paper/
|
|
1790
|
+
rules: [{ files: ["papers/**"], rules: { "paper/section-word": "off" } }],
|
|
1934
1791
|
});
|
|
1935
1792
|
check(
|
|
1936
1793
|
"a built-in rule's severity can be changed the same way",
|