paperlint 3.0.0 → 3.1.1
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 +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +51 -56
- package/CONTRIBUTING.md +30 -18
- package/README.md +179 -180
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +14 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.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 +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +29 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +93 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -80
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.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/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -4
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- 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 +50 -15
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +30 -8
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +59 -30
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +8 -3
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +26 -10
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +6 -0
- 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 +48 -4
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/rules.md +29 -25
- package/docs/skills.md +80 -0
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +56 -9
- package/eslint-rules/paper-typography.test.mjs +186 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.schema.json +1 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- 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 +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +51 -11
- package/src/build.test.ts +362 -0
- package/src/build.ts +40 -14
- package/src/cli.harness.mjs +12 -19
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +409 -94
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +77 -7
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +4 -4
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +5 -7
- package/src/new-paper.test.ts +203 -2
- package/src/new-paper.ts +84 -17
- package/src/paper-files.test.ts +122 -5
- package/src/paper-files.ts +99 -39
- package/src/paper-settings-commands.test.ts +41 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/parse-args.test.ts +48 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +5 -9
- package/src/structure.ts +1 -1
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +44 -13
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +37 -12
- package/src/types.ts +6 -0
- package/src/venue-rules.test.ts +63 -0
- package/src/venue-rules.ts +14 -12
- package/templates/paper/paperlint.json +1 -1
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -79
- package/src/structure.mutations.mjs +0 -83
package/src/new-paper.ts
CHANGED
|
@@ -98,12 +98,85 @@ function scorecardAndSource(dir: string, format: PaperFormat): string[] {
|
|
|
98
98
|
return hasSource ? [STATUS_FILE] : [STATUS_FILE, SOURCE_FILE[format]];
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
/** The venue `paperlint new --venue` chose: what goes into the new paper's `paperlint.json`. */
|
|
102
|
+
export interface VenueSetting {
|
|
103
|
+
/** `paperlint:<name>`, or a path relative to the paper's `paperlint.json`. */
|
|
104
|
+
readonly extends: string;
|
|
105
|
+
readonly kind: string | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The template's `paperlint.json` with the chosen venue written in. The template's `$comment`
|
|
110
|
+
* explains how to choose one, so it goes once one is chosen. A template that is not plain JSON (a
|
|
111
|
+
* project's own, with comments) cannot be edited safely and is refused by name.
|
|
112
|
+
*/
|
|
113
|
+
function withVenue(
|
|
114
|
+
text: string,
|
|
115
|
+
src: string,
|
|
116
|
+
venue: VenueSetting,
|
|
117
|
+
): { ok: true; text: string } | { ok: false; reason: string } {
|
|
118
|
+
let obj: Record<string, unknown>;
|
|
119
|
+
try {
|
|
120
|
+
obj = JSON.parse(text) as Record<string, unknown>;
|
|
121
|
+
} catch {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: `--venue cannot be written into ${src}: it is not plain JSON — set "extends" by hand`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const out: Record<string, unknown> = { ...obj, extends: venue.extends };
|
|
128
|
+
delete out["$comment"];
|
|
129
|
+
if (venue.kind !== null) out["kind"] = venue.kind;
|
|
130
|
+
return { ok: true, text: `${JSON.stringify(out, null, 2)}\n` };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A file `newPaper` will write: its text, and which template it came from. */
|
|
134
|
+
interface Planned {
|
|
135
|
+
readonly file: string;
|
|
136
|
+
readonly text: string;
|
|
137
|
+
readonly from: "project" | "package";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** One file's text from its template — the project's override first, the package's second. */
|
|
141
|
+
function fromTemplate(
|
|
142
|
+
file: string,
|
|
143
|
+
{
|
|
144
|
+
papersRoot,
|
|
145
|
+
packageTemplates,
|
|
146
|
+
name,
|
|
147
|
+
venue,
|
|
148
|
+
}: {
|
|
149
|
+
papersRoot: string;
|
|
150
|
+
packageTemplates: string;
|
|
151
|
+
name: string;
|
|
152
|
+
venue: VenueSetting | null;
|
|
153
|
+
},
|
|
154
|
+
): { ok: true; value: Planned } | { ok: false; reason: string } {
|
|
155
|
+
const project = join(papersRoot, OVERRIDE_DIR, file);
|
|
156
|
+
const [src, from] = existsSync(project)
|
|
157
|
+
? [project, "project" as const]
|
|
158
|
+
: [join(packageTemplates, file), "package" as const];
|
|
159
|
+
// A missing package template is a broken install, not a reason to write an empty file.
|
|
160
|
+
if (!existsSync(src))
|
|
161
|
+
return { ok: false, reason: `no template for ${file}: ${src} is missing` };
|
|
162
|
+
const text = readFileSync(src, "utf8").split("{{name}}").join(name);
|
|
163
|
+
if (file !== CONFIG_FILE || venue === null)
|
|
164
|
+
return { ok: true, value: { file, text, from } };
|
|
165
|
+
const edited = withVenue(text, src, venue);
|
|
166
|
+
return edited.ok
|
|
167
|
+
? { ok: true, value: { file, text: edited.text, from } }
|
|
168
|
+
: edited;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Documented in README.md#getting-started — update it when this changes.
|
|
102
172
|
export function newPaper(
|
|
103
173
|
papersRoot: string,
|
|
104
174
|
name: string,
|
|
105
175
|
format: PaperFormat,
|
|
106
|
-
{
|
|
176
|
+
{
|
|
177
|
+
packageTemplates = PACKAGE_TEMPLATES,
|
|
178
|
+
venue = null,
|
|
179
|
+
}: { packageTemplates?: string; venue?: VenueSetting | null } = {},
|
|
107
180
|
): NewPaperResult {
|
|
108
181
|
const problem = nameProblem(name);
|
|
109
182
|
if (problem) return { ok: false, reason: problem };
|
|
@@ -113,26 +186,20 @@ export function newPaper(
|
|
|
113
186
|
return { ok: false, reason: `${dir} exists and is not a directory` };
|
|
114
187
|
|
|
115
188
|
const files: FileOutcome[] = [];
|
|
116
|
-
const toWrite:
|
|
117
|
-
[];
|
|
189
|
+
const toWrite: Planned[] = [];
|
|
118
190
|
for (const file of wantedFiles(dir, format)) {
|
|
119
191
|
if (existsSync(join(dir, file))) {
|
|
120
192
|
files.push({ file, status: "kept" });
|
|
121
193
|
continue;
|
|
122
194
|
}
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (!
|
|
130
|
-
|
|
131
|
-
ok: false,
|
|
132
|
-
reason: `no template for ${file}: ${src} is missing`,
|
|
133
|
-
};
|
|
134
|
-
const text = readFileSync(src, "utf8").split("{{name}}").join(name);
|
|
135
|
-
toWrite.push({ file, text, from });
|
|
195
|
+
const planned = fromTemplate(file, {
|
|
196
|
+
papersRoot,
|
|
197
|
+
packageTemplates,
|
|
198
|
+
name,
|
|
199
|
+
venue,
|
|
200
|
+
});
|
|
201
|
+
if (!planned.ok) return planned;
|
|
202
|
+
toWrite.push(planned.value);
|
|
136
203
|
}
|
|
137
204
|
// Read every template before writing any file, so a missing one leaves no half-made folder.
|
|
138
205
|
mkdirSync(dir, { recursive: true });
|
package/src/paper-files.test.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { buildConfig, run } from "./cli.ts";
|
|
|
23
23
|
import {
|
|
24
24
|
narrowToOwners,
|
|
25
25
|
ownedPatterns,
|
|
26
|
+
ownedScopes,
|
|
26
27
|
ruleOwners,
|
|
27
28
|
scopeToOwned,
|
|
28
29
|
} from "./paper-files.ts";
|
|
@@ -160,6 +161,73 @@ describe("`rules` blocks reach only the files each rule is written for", () => {
|
|
|
160
161
|
});
|
|
161
162
|
});
|
|
162
163
|
|
|
164
|
+
describe("🔴 a generated block never reaches a file paperlint does not lint (#101)", () => {
|
|
165
|
+
// A venue preset with `rules` (agenticdev turns on pdf/last-page-balance) became a block over
|
|
166
|
+
// every paper file. `pdf` is registered for every file, so the block was not narrowed, and the
|
|
167
|
+
// siblings index — which the sibling-card block excludes — matched a block with no language:
|
|
168
|
+
// ESLint parsed it as JavaScript ("Parsing error: Assigning to rvalue").
|
|
169
|
+
const SIBLINGS = {
|
|
170
|
+
"papers/a/siblings/README.md": "# Siblings\n\n- [x](smith2025.md)\n",
|
|
171
|
+
"papers/a/siblings/smith2025.md": "---\nread: full\n---\n# Smith 2025\n",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
it.each([
|
|
175
|
+
[
|
|
176
|
+
"a venue preset's rules",
|
|
177
|
+
{
|
|
178
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
179
|
+
extends: "paperlint:agenticdev",
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
[
|
|
184
|
+
// `papers/**` alone would not show it: ESLint does not lint a file on a pattern ending in
|
|
185
|
+
// `/**`, it only applies such a block to files something else claims.
|
|
186
|
+
"a root block over every markdown file",
|
|
187
|
+
{
|
|
188
|
+
"paperlint.json": JSON.stringify({
|
|
189
|
+
rules: [
|
|
190
|
+
{
|
|
191
|
+
files: ["papers/**/*.md", "papers/**/*.tex"],
|
|
192
|
+
rules: { "pdf/last-page-balance": "warn" },
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
[
|
|
199
|
+
"a paper's own { id: severity }",
|
|
200
|
+
{
|
|
201
|
+
"papers/a/paperlint.json": JSON.stringify({
|
|
202
|
+
rules: { "pdf/last-page-balance": "warn" },
|
|
203
|
+
}),
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
])(
|
|
207
|
+
"%s: the siblings index is not parsed, the cards still are",
|
|
208
|
+
async (_, settings) => {
|
|
209
|
+
const root = project({ ...SIBLINGS, ...settings });
|
|
210
|
+
const r = await lint(root);
|
|
211
|
+
const results = JSON.parse(r.out) as {
|
|
212
|
+
filePath: string;
|
|
213
|
+
messages: { ruleId: string | null; message: string }[];
|
|
214
|
+
}[];
|
|
215
|
+
// Guards: the class — no file reaches ESLint's default JavaScript parser.
|
|
216
|
+
expect(
|
|
217
|
+
results.flatMap((x) => x.messages).filter((m) => m.ruleId === null),
|
|
218
|
+
).toEqual([]);
|
|
219
|
+
expect(linted(r.out)).not.toContain(
|
|
220
|
+
join(root, "papers/a/siblings/README.md"),
|
|
221
|
+
);
|
|
222
|
+
// Guards: the other half — narrowing did not drop the files the block is for.
|
|
223
|
+
expect(linted(r.out)).toContain(
|
|
224
|
+
join(root, "papers/a/siblings/smith2025.md"),
|
|
225
|
+
);
|
|
226
|
+
expect(linted(r.out)).toContain(join(root, "papers/a/paper.tex"));
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
163
231
|
describe("`rules` blocks — where the rule lands", () => {
|
|
164
232
|
it("on its own files and on no other", async () => {
|
|
165
233
|
const root = project();
|
|
@@ -206,11 +274,60 @@ describe("paper-files — the pieces", () => {
|
|
|
206
274
|
expect(ownedPatterns(own)).toEqual(["**/a.md", "**/b.md", "**/c.md"]);
|
|
207
275
|
});
|
|
208
276
|
|
|
209
|
-
it("a rule's owner is where its plugin is registered; a global plugin owns every file", () => {
|
|
277
|
+
it("a rule's owner is where its plugin is registered; a global plugin owns every OWNED file", () => {
|
|
210
278
|
const owners = ruleOwners(own);
|
|
211
|
-
expect(owners.get("g/any")).
|
|
212
|
-
|
|
213
|
-
|
|
279
|
+
expect(owners.get("g/any")).toEqual([
|
|
280
|
+
{ files: ["**/a.md", "**/b.md", "**/c.md"], ignores: [] },
|
|
281
|
+
]);
|
|
282
|
+
expect(owners.get("p/one")).toEqual([{ files: ["**/a.md"], ignores: [] }]);
|
|
283
|
+
expect(owners.get("p/two")).toEqual([
|
|
284
|
+
{ files: ["**/b.md", "**/c.md"], ignores: [] },
|
|
285
|
+
]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("🔴 a scope keeps its block's exceptions, and a global plugin inherits them (#101)", () => {
|
|
289
|
+
const withIndex = [
|
|
290
|
+
...own,
|
|
291
|
+
{
|
|
292
|
+
files: ["**/s/*.md"],
|
|
293
|
+
ignores: ["**/s/README.md"],
|
|
294
|
+
plugins: { s: { rules: { card: {} } } },
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
expect(ownedScopes(withIndex)).toEqual([
|
|
298
|
+
{ files: ["**/a.md", "**/b.md", "**/c.md"], ignores: [] },
|
|
299
|
+
{ files: ["**/s/*.md"], ignores: ["**/s/README.md"] },
|
|
300
|
+
]);
|
|
301
|
+
// Guards: the defect — the plugin registered for every file used to keep any glob, and so
|
|
302
|
+
// reached the index the card block excludes.
|
|
303
|
+
expect(ruleOwners(withIndex).get("g/any")).toEqual(ownedScopes(withIndex));
|
|
304
|
+
expect(
|
|
305
|
+
narrowToOwners(
|
|
306
|
+
{
|
|
307
|
+
basePath: "/x",
|
|
308
|
+
files: ["papers/**/*.md"],
|
|
309
|
+
rules: { "g/any": "warn" },
|
|
310
|
+
},
|
|
311
|
+
ruleOwners(withIndex),
|
|
312
|
+
).at(-1),
|
|
313
|
+
).toEqual({
|
|
314
|
+
basePath: "/x",
|
|
315
|
+
files: [["papers/**/*.md", "**/s/*.md"]],
|
|
316
|
+
ignores: ["**/s/README.md"],
|
|
317
|
+
rules: { "g/any": "warn" },
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("paper-files — blocks that carry no rules", () => {
|
|
323
|
+
it("a plugin with only a language registers no rule; a block with no files owns no pattern of its own", () => {
|
|
324
|
+
const own = [
|
|
325
|
+
{ plugins: { tex: { languages: {} } } },
|
|
326
|
+
{ ignores: ["**/x.md"], plugins: { tex: { languages: {} } } },
|
|
327
|
+
{ files: ["**/a.md"], plugins: { p: { rules: { one: {} } } } },
|
|
328
|
+
];
|
|
329
|
+
expect([...ruleOwners(own).keys()]).toEqual(["p/one"]);
|
|
330
|
+
expect(ownedPatterns(own)).toEqual(["**/a.md"]);
|
|
214
331
|
});
|
|
215
332
|
});
|
|
216
333
|
|
|
@@ -249,7 +366,7 @@ describe("paper-files — narrowing a block", () => {
|
|
|
249
366
|
},
|
|
250
367
|
{
|
|
251
368
|
basePath: "/x",
|
|
252
|
-
files: ["papers/**"],
|
|
369
|
+
files: ["**/a.md", "**/b.md", "**/c.md"].map((o) => ["papers/**", o]),
|
|
253
370
|
ignores: ["papers/old/**"],
|
|
254
371
|
rules: { "g/any": "off" },
|
|
255
372
|
},
|
package/src/paper-files.ts
CHANGED
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
* per owner and its `files` ANDed with the owner's (ESLint's nested-array form), so a rule can
|
|
25
25
|
* only land where its plugin is registered.
|
|
26
26
|
*
|
|
27
|
+
* Both read ONE definition of "the files paperlint lints": `ownedScopes`, each own block's `files`
|
|
28
|
+
* together with its `ignores`. The globs alone are not that definition. The sibling-card block
|
|
29
|
+
* claims every markdown file under `siblings/` EXCEPT the index, and a rule whose plugin is
|
|
30
|
+
* registered for every file (`pdf`) used to keep the consumer block's `files` as they were — so a
|
|
31
|
+
* venue preset's rules reached `siblings/README.md`, a block with no `language` matched it, and
|
|
32
|
+
* ESLint parsed the markdown as JavaScript (#101). Now every generated block narrows to scopes,
|
|
33
|
+
* exceptions included, and a plugin registered everywhere owns exactly what paperlint lints.
|
|
34
|
+
*
|
|
27
35
|
* ESLint's semantics that this leans on, each measured against ESLint 10 before writing it:
|
|
28
36
|
* - a global-ignore pattern that matches a DIRECTORY hides everything inside it, so directories
|
|
29
37
|
* must be un-ignored (`!` + a pattern ending in a slash) before owned files can be;
|
|
@@ -37,11 +45,21 @@ import type { RuleBlock, RuleEntry } from "./rules-config.ts";
|
|
|
37
45
|
/** What this module reads of a config block. */
|
|
38
46
|
interface Block {
|
|
39
47
|
readonly files?: readonly unknown[];
|
|
48
|
+
readonly ignores?: readonly unknown[];
|
|
40
49
|
readonly plugins?: Readonly<
|
|
41
50
|
Record<string, { readonly rules?: Readonly<Record<string, unknown>> }>
|
|
42
51
|
>;
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
/**
|
|
55
|
+
* A set of files paperlint lints: an own block's globs and the exceptions that block makes. A file is
|
|
56
|
+
* in it when it matches one of `files` and none of `ignores`.
|
|
57
|
+
*/
|
|
58
|
+
export interface OwnedScope {
|
|
59
|
+
readonly files: readonly string[];
|
|
60
|
+
readonly ignores: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
/** A `files` entry: one glob, or several that must ALL match. */
|
|
46
64
|
export type FilesEntry = string | readonly string[];
|
|
47
65
|
|
|
@@ -53,12 +71,49 @@ export interface ScopedBlock {
|
|
|
53
71
|
readonly rules: Readonly<Record<string, RuleEntry>>;
|
|
54
72
|
}
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
/** A block's `files`, the string globs only. */
|
|
75
|
+
const globsOf = (files: readonly unknown[]): string[] =>
|
|
76
|
+
files.filter((f): f is string => typeof f === "string");
|
|
77
|
+
|
|
78
|
+
const stringsOf = (xs: readonly unknown[] | undefined): string[] =>
|
|
79
|
+
(xs ?? []).filter((x): x is string => typeof x === "string");
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Scopes with the same exceptions, merged into one: their globs united, once, in order. Keeps a
|
|
83
|
+
* generated block per distinct set of exceptions rather than one per own block.
|
|
84
|
+
*/
|
|
85
|
+
const merged = (scopes: readonly OwnedScope[]): OwnedScope[] => {
|
|
86
|
+
const byIgnores = new Map<string, OwnedScope>();
|
|
87
|
+
for (const s of scopes) {
|
|
88
|
+
const key = JSON.stringify(s.ignores);
|
|
89
|
+
const had = byIgnores.get(key);
|
|
90
|
+
byIgnores.set(key, {
|
|
91
|
+
files: [...new Set([...(had?.files ?? []), ...s.files])],
|
|
92
|
+
ignores: s.ignores,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return [...byIgnores.values()];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* THE FILES PAPERLINT LINTS: every own block that names `files`, with its `ignores`. A block without
|
|
100
|
+
* `files` (a global ignore, or the block registering `pdf` for every file) claims nothing.
|
|
101
|
+
*/
|
|
102
|
+
export function ownedScopes(own: readonly unknown[]): OwnedScope[] {
|
|
103
|
+
return merged(
|
|
104
|
+
own
|
|
105
|
+
.map((raw) => raw as Block)
|
|
106
|
+
.flatMap((b) =>
|
|
107
|
+
b.files === undefined
|
|
108
|
+
? []
|
|
109
|
+
: [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }],
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
58
113
|
|
|
59
114
|
/** Every glob paperlint's own blocks lint, once, in config order. */
|
|
60
115
|
export function ownedPatterns(own: readonly unknown[]): string[] {
|
|
61
|
-
return [...new Set(own.flatMap((
|
|
116
|
+
return [...new Set(ownedScopes(own).flatMap((s) => s.files))];
|
|
62
117
|
}
|
|
63
118
|
|
|
64
119
|
/** ESLint's default ignores, re-applied after directories are un-ignored. */
|
|
@@ -84,40 +139,37 @@ const ruleIdsOf = (b: Block): string[] =>
|
|
|
84
139
|
Object.keys(plugin.rules ?? {}).map((rule) => `${name}/${rule}`),
|
|
85
140
|
);
|
|
86
141
|
|
|
87
|
-
/** An owner seen so far, widened by one more block's globs; `null` (every file) absorbs all. */
|
|
88
|
-
const widen = (
|
|
89
|
-
had: readonly string[] | null | undefined,
|
|
90
|
-
files: readonly string[] | null,
|
|
91
|
-
): readonly string[] | null =>
|
|
92
|
-
had === null || files === null
|
|
93
|
-
? null
|
|
94
|
-
: [...new Set([...(had ?? []), ...files])];
|
|
95
|
-
|
|
96
142
|
/**
|
|
97
|
-
* Rule id → the
|
|
98
|
-
*
|
|
143
|
+
* Rule id → the scopes of the blocks that register its plugin with that rule. A plugin registered
|
|
144
|
+
* for every file (a block without `files`) owns every scope — everything paperlint lints, and not
|
|
145
|
+
* one file more.
|
|
99
146
|
*/
|
|
100
147
|
export function ruleOwners(
|
|
101
148
|
own: readonly unknown[],
|
|
102
|
-
): Map<string, readonly
|
|
103
|
-
const
|
|
149
|
+
): Map<string, readonly OwnedScope[]> {
|
|
150
|
+
const all = ownedScopes(own);
|
|
151
|
+
const found = new Map<string, OwnedScope[]>();
|
|
104
152
|
for (const raw of own) {
|
|
105
153
|
const b = raw as Block;
|
|
106
|
-
const
|
|
107
|
-
|
|
154
|
+
const scopes =
|
|
155
|
+
b.files === undefined
|
|
156
|
+
? all
|
|
157
|
+
: [{ files: globsOf(b.files), ignores: stringsOf(b.ignores) }];
|
|
158
|
+
for (const id of ruleIdsOf(b))
|
|
159
|
+
found.set(id, [...(found.get(id) ?? []), ...scopes]);
|
|
108
160
|
}
|
|
109
|
-
return
|
|
161
|
+
return new Map([...found].map(([id, scopes]) => [id, merged(scopes)]));
|
|
110
162
|
}
|
|
111
163
|
|
|
112
164
|
/**
|
|
113
|
-
* One consumer block → one block per owner, each carrying only that owner's rules, its `files`
|
|
114
|
-
* ANDed with the
|
|
115
|
-
* absent when its parser did not load) gets no block:
|
|
116
|
-
* naming it to ESLint would only crash.
|
|
165
|
+
* One consumer block → one block per owner scope, each carrying only that owner's rules, its `files`
|
|
166
|
+
* ANDed with the scope's globs and its `ignores` joined by the scope's exceptions. A rule no block
|
|
167
|
+
* of this config registers (the LaTeX block is absent when its parser did not load) gets no block:
|
|
168
|
+
* it has no file to reach in this run, and naming it to ESLint would only crash.
|
|
117
169
|
*/
|
|
118
170
|
export function narrowToOwners(
|
|
119
171
|
block: RuleBlock,
|
|
120
|
-
owners: ReadonlyMap<string, readonly
|
|
172
|
+
owners: ReadonlyMap<string, readonly OwnedScope[]>,
|
|
121
173
|
): ScopedBlock[] {
|
|
122
174
|
const groups = new Map<string, Record<string, RuleEntry>>();
|
|
123
175
|
for (const [id, entry] of Object.entries(block.rules)) {
|
|
@@ -126,19 +178,27 @@ export function narrowToOwners(
|
|
|
126
178
|
const key = JSON.stringify(owner);
|
|
127
179
|
groups.set(key, { ...groups.get(key), [id]: entry });
|
|
128
180
|
}
|
|
129
|
-
return [...groups].
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
181
|
+
return [...groups].flatMap(([key, rules]) =>
|
|
182
|
+
(JSON.parse(key) as OwnedScope[]).map((scope) =>
|
|
183
|
+
within(block, scope, rules),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** `block`'s rules over the files both it and `scope` claim, minus both sets of exceptions. */
|
|
189
|
+
function within(
|
|
190
|
+
block: RuleBlock,
|
|
191
|
+
scope: OwnedScope,
|
|
192
|
+
rules: Readonly<Record<string, RuleEntry>>,
|
|
193
|
+
): ScopedBlock {
|
|
194
|
+
const files: readonly FilesEntry[] = block.files
|
|
195
|
+
? block.files.flatMap((f) => scope.files.map((o) => [f, o]))
|
|
196
|
+
: scope.files;
|
|
197
|
+
const ignores = [...(block.ignores ?? []), ...scope.ignores];
|
|
198
|
+
return {
|
|
199
|
+
basePath: block.basePath,
|
|
200
|
+
files,
|
|
201
|
+
...(ignores.length ? { ignores } : {}),
|
|
202
|
+
rules,
|
|
203
|
+
};
|
|
144
204
|
}
|
|
@@ -42,10 +42,11 @@ function project(files: Record<string, string> = {}): string {
|
|
|
42
42
|
|
|
43
43
|
async function lint(
|
|
44
44
|
root: string,
|
|
45
|
+
args: string[] = [],
|
|
45
46
|
): Promise<{ code: number; out: string; err: string }> {
|
|
46
47
|
const out: string[] = [];
|
|
47
48
|
const err: string[] = [];
|
|
48
|
-
const code = await run(["lint", "--json"], {
|
|
49
|
+
const code = await run(["lint", "--json", ...args], {
|
|
49
50
|
cwd: root,
|
|
50
51
|
log: (s: string) => out.push(s),
|
|
51
52
|
err: (s: string) => err.push(s),
|
|
@@ -58,6 +59,43 @@ const rulesIn = (stdout: string, paper: string): string[] =>
|
|
|
58
59
|
.filter((r) => r.filePath.endsWith(join(paper, "paper.tex")))
|
|
59
60
|
.flatMap((r) => r.messages.map((m) => m.ruleId));
|
|
60
61
|
|
|
62
|
+
describe("🔴 an optional rule that reaches no paper — judged against the PROJECT, not the run (#103)", () => {
|
|
63
|
+
const THIRD = {
|
|
64
|
+
"papers/c/paper.tex":
|
|
65
|
+
"\\documentclass{article}\n\\begin{document}x\\end{document}\n",
|
|
66
|
+
"papers/c/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
67
|
+
};
|
|
68
|
+
const rootRules = (files: string[]) => ({
|
|
69
|
+
"paperlint.json": JSON.stringify({
|
|
70
|
+
rules: [{ files, rules: { "pdf/last-page-balance": "error" } }],
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("linting paper c alone passes when the block reaches papers a and b", async () => {
|
|
75
|
+
const root = project({
|
|
76
|
+
...THIRD,
|
|
77
|
+
...rootRules(["papers/a/**", "papers/b/**"]),
|
|
78
|
+
});
|
|
79
|
+
const r = await lint(root, ["papers/c"]);
|
|
80
|
+
// Guards: the defect — the guard counted only the paper.tex files this run linted.
|
|
81
|
+
expect(r.err).not.toMatch(/no linted paper\.tex gets it/);
|
|
82
|
+
expect(r.code).toBe(0);
|
|
83
|
+
expect(rulesIn(r.out, "c")).not.toContain("pdf/last-page-balance");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("…and a glob that reaches no paper of the project still fails, from a subset too", async () => {
|
|
87
|
+
const root = project({ ...THIRD, ...rootRules(["papers/typo/**"]) });
|
|
88
|
+
for (const args of [["papers/c"], []]) {
|
|
89
|
+
const r = await lint(root, args);
|
|
90
|
+
// Guards: the other half — widening the judged set did not turn the guard off.
|
|
91
|
+
expect(r.err).toMatch(
|
|
92
|
+
/pdf\/last-page-balance is turned on in "rules", but no paper\.tex/,
|
|
93
|
+
);
|
|
94
|
+
expect(r.code).toBe(1);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
61
99
|
describe("paperlint lint — `rules` in a paper's paperlint.json", () => {
|
|
62
100
|
it("turns an optional rule on for that paper alone", async () => {
|
|
63
101
|
const root = project({
|
|
@@ -177,11 +215,9 @@ describe("paperlint lint — the venue preset's rules", () => {
|
|
|
177
215
|
expect(cfgOf.ok && cfgOf.value).toEqual({
|
|
178
216
|
preset: [
|
|
179
217
|
{
|
|
218
|
+
// Scoped to the paper by `basePath` alone: which of its files the rule reaches is decided
|
|
219
|
+
// once, by narrowing to paperlint's owned scopes (#101), not by a copy of their globs.
|
|
180
220
|
basePath: join(root, "papers/a"),
|
|
181
|
-
files: expect.arrayContaining([
|
|
182
|
-
"**/paper.tex",
|
|
183
|
-
"**/PIPELINE-STATUS.md",
|
|
184
|
-
]),
|
|
185
221
|
rules: { "pdf/last-page-balance": ["error", { tolerancePt: 120 }] },
|
|
186
222
|
},
|
|
187
223
|
],
|
|
@@ -118,6 +118,26 @@ describe("readPaperSettings", () => {
|
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
describe("readPaperSettings — a root paperlint.json that cannot be used", () => {
|
|
122
|
+
const ROOT = "/work";
|
|
123
|
+
const withFiles = (f: Record<string, string>) =>
|
|
124
|
+
memoryFiles({ [`${ROOT}/package.json`]: "{}", ...f });
|
|
125
|
+
|
|
126
|
+
it("a root file that does not parse, or names a field of the wrong type, is the paper's problem too", () => {
|
|
127
|
+
const broken = (root: string) =>
|
|
128
|
+
readPaperSettings(withFiles({ [`${ROOT}/paperlint.json`]: root }), PAPER);
|
|
129
|
+
const notJson = broken("{ nope");
|
|
130
|
+
expect(!notJson.ok && notJson.error.kind).toBe("broken");
|
|
131
|
+
expect(broken('{"kind":3}')).toEqual({
|
|
132
|
+
ok: false,
|
|
133
|
+
error: {
|
|
134
|
+
kind: "broken",
|
|
135
|
+
why: expect.stringMatching(/^the root paperlint\.json: .*kind/),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
121
141
|
describe("readPaperSettings — the root paperlint.json's defaults, the paper's file over them", () => {
|
|
122
142
|
const ROOT = "/work";
|
|
123
143
|
const withFiles = (f: Record<string, string>) =>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--flag=value` and unknown flags — the command line the CI action actually writes.
|
|
3
|
+
*
|
|
4
|
+
* action.yml runs `npx paperlint lint $PATHS --max-warnings="$MAXWARN" --json`. The parser knew
|
|
5
|
+
* only `--max-warnings <n>`, so `--max-warnings=-1` fell through to the list of PATHS: ESLint was
|
|
6
|
+
* handed a file named `--max-warnings=-1`, found nothing for it, and the run reported "nothing was
|
|
7
|
+
* linted under papers, --max-warnings=-1". Any flag the parser did not know became a path the same
|
|
8
|
+
* way, silently.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { parseArgs, run } from "./cli.ts";
|
|
12
|
+
|
|
13
|
+
describe("parseArgs — `--flag=value` is the same as `--flag value`", () => {
|
|
14
|
+
it("🔴 the action's own command line: `--max-warnings=-1` is a threshold, not a path", () => {
|
|
15
|
+
const a = parseArgs(["lint", "papers", "--max-warnings=-1", "--json"]);
|
|
16
|
+
// Guards: the defect — the flag landed in `paths`.
|
|
17
|
+
expect(a.paths).toEqual(["papers"]);
|
|
18
|
+
expect(a.maxWarnings).toBe(-1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
["--config=o.json", "config", "o.json"],
|
|
23
|
+
["--venue=agenticdev", "venue", "agenticdev"],
|
|
24
|
+
["--kind=short", "kind", "short"],
|
|
25
|
+
["--format=md", "format", "md"],
|
|
26
|
+
["--paper=p", "paper", "p"],
|
|
27
|
+
] as const)("%s", (flag, key, value) => {
|
|
28
|
+
const a = parseArgs(["new", "x", flag]);
|
|
29
|
+
expect(a[key]).toBe(value);
|
|
30
|
+
expect(a.paths).toEqual(["x"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("an empty value after `=` is a missing value, refused like `--config` with none", () => {
|
|
34
|
+
expect(parseArgs(["lint", "--config="]).missingValue).toBe("--config");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("an unknown flag is refused, not read as a path", () => {
|
|
39
|
+
it("exits 2 and names the flag", async () => {
|
|
40
|
+
const err: string[] = [];
|
|
41
|
+
const code = await run(["lint", "--frobnicate"], {
|
|
42
|
+
log: () => {},
|
|
43
|
+
err: (s: string) => err.push(s),
|
|
44
|
+
});
|
|
45
|
+
expect(code).toBe(2);
|
|
46
|
+
expect(err.join("\n")).toMatch(/unknown flag `--frobnicate`/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -11,17 +11,14 @@ import { writeFileSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { createChecker } from "../lib/check.mjs";
|
|
14
15
|
|
|
15
16
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const FIX = resolve(HERE, "..", "fixtures", "pdf-facts");
|
|
17
18
|
const { readPdf } = await import(join(HERE, "pdf-facts.ts"));
|
|
18
19
|
const { classifyLastPage } = await import(join(HERE, "pdf-geometry.ts"));
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
const check = (label, cond, detail = "") => {
|
|
22
|
-
assert.ok(cond, detail ? `${label} — ${detail}` : label);
|
|
23
|
-
n++;
|
|
24
|
-
};
|
|
21
|
+
const check = createChecker();
|
|
25
22
|
|
|
26
23
|
const read = async (name) => {
|
|
27
24
|
const r = await readPdf(join(FIX, name));
|
|
@@ -184,5 +181,5 @@ try {
|
|
|
184
181
|
}
|
|
185
182
|
|
|
186
183
|
console.log(
|
|
187
|
-
`✓ ${String(
|
|
184
|
+
`✓ ${String(check.count)} assertions passed — pdf-facts: pdf.js on real PDFs agrees with poppler, and says what it cannot read`,
|
|
188
185
|
);
|