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/link-skills.ts
CHANGED
|
@@ -34,15 +34,13 @@ import {
|
|
|
34
34
|
readlinkSync,
|
|
35
35
|
realpathSync,
|
|
36
36
|
symlinkSync,
|
|
37
|
-
unlinkSync,
|
|
38
37
|
} from "node:fs";
|
|
39
38
|
/* eslint-enable boundaries/dependencies */
|
|
40
39
|
import { createRequire } from "node:module";
|
|
41
|
-
import { dirname, join, relative, resolve
|
|
40
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
42
41
|
import { pathToFileURL } from "node:url";
|
|
43
42
|
import {
|
|
44
43
|
installedSkills,
|
|
45
|
-
LEGACY_PACKAGE_NAME,
|
|
46
44
|
PACKAGE_NAME,
|
|
47
45
|
SHIPPED_SKILLS_DIR,
|
|
48
46
|
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
@@ -109,8 +107,7 @@ export function shippedSkills(
|
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
export type LinkStatus =
|
|
113
|
-
"created" | "replaced" | "present" | "missing" | "foreign";
|
|
110
|
+
export type LinkStatus = "created" | "present" | "missing" | "foreign";
|
|
114
111
|
|
|
115
112
|
export interface SkillLink {
|
|
116
113
|
readonly name: string;
|
|
@@ -129,27 +126,17 @@ export type LinkReport =
|
|
|
129
126
|
}
|
|
130
127
|
| { readonly ok: false; readonly error: string };
|
|
131
128
|
|
|
132
|
-
/**
|
|
133
|
-
* What occupies `entry`, judged against the directory it should lead to. A link spelled exactly as
|
|
134
|
-
* an install under the package's old name wrote it (`legacyTarget`) is `stale`: ours, from before
|
|
135
|
-
* the rename, and replaced on the next `init`.
|
|
136
|
-
*/
|
|
129
|
+
/** What occupies `entry`, judged against the directory it should lead to. */
|
|
137
130
|
function inspect(
|
|
138
131
|
entry: string,
|
|
139
132
|
want: string,
|
|
140
|
-
|
|
141
|
-
): { status: "present" | "missing" | "foreign" | "stale"; reason?: string } {
|
|
133
|
+
): { status: "present" | "missing" | "foreign"; reason?: string } {
|
|
142
134
|
let st;
|
|
143
135
|
try {
|
|
144
136
|
st = lstatSync(entry);
|
|
145
137
|
} catch {
|
|
146
138
|
return { status: "missing" };
|
|
147
139
|
}
|
|
148
|
-
if (st.isSymbolicLink() && readlinkSync(entry) === legacyTarget)
|
|
149
|
-
return {
|
|
150
|
-
status: "stale",
|
|
151
|
-
reason: `a link into ${LEGACY_PACKAGE_NAME}, the package's old name — \`npx ${PACKAGE_NAME} init\` replaces it`,
|
|
152
|
-
};
|
|
153
140
|
try {
|
|
154
141
|
if (realpathSync(entry) === want) return { status: "present" };
|
|
155
142
|
} catch {
|
|
@@ -168,10 +155,7 @@ function inspect(
|
|
|
168
155
|
};
|
|
169
156
|
}
|
|
170
157
|
|
|
171
|
-
/**
|
|
172
|
-
* One skill: inspect its entry and, when writing, create the link — or replace one an install
|
|
173
|
-
* under the package's old name made.
|
|
174
|
-
*/
|
|
158
|
+
/** One skill: inspect its entry and, when writing, create the link. */
|
|
175
159
|
function linkOne(
|
|
176
160
|
name: string,
|
|
177
161
|
{
|
|
@@ -181,22 +165,14 @@ function linkOne(
|
|
|
181
165
|
write,
|
|
182
166
|
}: { entry: string; target: string; want: string; write: boolean },
|
|
183
167
|
): SkillLink {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
`node_modules${sep}${LEGACY_PACKAGE_NAME}${sep}`,
|
|
187
|
-
);
|
|
188
|
-
const seen = inspect(entry, want, legacyTarget);
|
|
189
|
-
const fixable = seen.status === "missing" || seen.status === "stale";
|
|
190
|
-
if (!write || !fixable) {
|
|
191
|
-
const status = seen.status === "stale" ? "foreign" : seen.status;
|
|
168
|
+
const seen = inspect(entry, want);
|
|
169
|
+
if (!write || seen.status !== "missing")
|
|
192
170
|
return seen.reason
|
|
193
|
-
? { name, status, reason: seen.reason }
|
|
194
|
-
: { name, status };
|
|
195
|
-
}
|
|
171
|
+
? { name, status: seen.status, reason: seen.reason }
|
|
172
|
+
: { name, status: seen.status };
|
|
196
173
|
try {
|
|
197
|
-
if (seen.status === "stale") unlinkSync(entry);
|
|
198
174
|
symlinkSync(target, entry, "dir");
|
|
199
|
-
return { name, status:
|
|
175
|
+
return { name, status: "created" };
|
|
200
176
|
} catch (e) {
|
|
201
177
|
return {
|
|
202
178
|
name,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperlint lint --fix`, and the settings that replaced the ratchets, through the command.
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
15
|
+
import { run } from "./cli.ts";
|
|
16
|
+
|
|
17
|
+
const dirs: string[] = [];
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function project(
|
|
23
|
+
settings: Record<string, unknown>,
|
|
24
|
+
files: Record<string, string> = {},
|
|
25
|
+
): string {
|
|
26
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-fix-")));
|
|
27
|
+
dirs.push(root);
|
|
28
|
+
const all: Record<string, string> = {
|
|
29
|
+
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
30
|
+
"paperlint.json": JSON.stringify({ papersDir: "papers", ...settings }),
|
|
31
|
+
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
32
|
+
"papers/a/paper.md": "# Intro\n\nSee §5, §6 and §7; p < .05.\n",
|
|
33
|
+
...files,
|
|
34
|
+
};
|
|
35
|
+
for (const [p, text] of Object.entries(all)) {
|
|
36
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
37
|
+
writeFileSync(join(root, p), text);
|
|
38
|
+
}
|
|
39
|
+
return root;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function lint(root: string, args: string[] = []) {
|
|
43
|
+
const out: string[] = [];
|
|
44
|
+
const err: string[] = [];
|
|
45
|
+
const code = await run(["lint", "--json", ...args], {
|
|
46
|
+
cwd: root,
|
|
47
|
+
log: (s: string) => out.push(s),
|
|
48
|
+
err: (s: string) => err.push(s),
|
|
49
|
+
});
|
|
50
|
+
return { code, out: out.join("\n"), err: err.join("\n") };
|
|
51
|
+
}
|
|
52
|
+
const ruleIds = (stdout: string): string[] =>
|
|
53
|
+
(JSON.parse(stdout) as { messages: { ruleId: string }[] }[]).flatMap((r) =>
|
|
54
|
+
r.messages.map((m) => m.ruleId),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
describe("paperlint lint --fix", () => {
|
|
58
|
+
it("🔴 three `§` and a bare decimal → --fix → zero findings, and the file says Section", async () => {
|
|
59
|
+
const root = project({});
|
|
60
|
+
const before = ruleIds((await lint(root)).out);
|
|
61
|
+
expect(before.filter((r) => r === "paper/section-word")).toHaveLength(3);
|
|
62
|
+
expect(before).toContain("paper/leading-zero");
|
|
63
|
+
const fixed = await lint(root, ["--fix"]);
|
|
64
|
+
expect(ruleIds(fixed.out)).not.toContain("paper/section-word");
|
|
65
|
+
expect(ruleIds(fixed.out)).not.toContain("paper/leading-zero");
|
|
66
|
+
expect(readFileSync(join(root, "papers/a/paper.md"), "utf8")).toBe(
|
|
67
|
+
"# Intro\n\nSee Section 5, Section 6 and Section 7; p < 0.05.\n",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("without --fix nothing is written", async () => {
|
|
72
|
+
const root = project({});
|
|
73
|
+
await lint(root);
|
|
74
|
+
expect(readFileSync(join(root, "papers/a/paper.md"), "utf8")).toMatch(/§5/);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("sibling cards: `read:` is required, from the package's own schema", () => {
|
|
79
|
+
it("a card without it is a warning; with it, silent; the index is not a card", async () => {
|
|
80
|
+
const root = project(
|
|
81
|
+
{},
|
|
82
|
+
{
|
|
83
|
+
"papers/a/siblings/README.md": "# Siblings\n",
|
|
84
|
+
"papers/a/siblings/smith2025.md": "# Smith 2025\n",
|
|
85
|
+
"papers/a/siblings/jones2024.md": "---\nread: abstract\n---\n# Jones\n",
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
const results = JSON.parse((await lint(root)).out) as {
|
|
89
|
+
filePath: string;
|
|
90
|
+
messages: { ruleId: string; severity: number }[];
|
|
91
|
+
}[];
|
|
92
|
+
const of = (name: string) =>
|
|
93
|
+
results.find((r) => r.filePath.endsWith(name))?.messages ?? null;
|
|
94
|
+
expect(of("smith2025.md")?.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
95
|
+
["sibling/frontmatter", 1],
|
|
96
|
+
]);
|
|
97
|
+
expect(of("jones2024.md")).toEqual([]);
|
|
98
|
+
expect(of("siblings/README.md")).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("settings: what 3.0.0 removed is an ordinary unknown key or rule", () => {
|
|
103
|
+
it.each([
|
|
104
|
+
"typographyDebt",
|
|
105
|
+
"authorListCommand",
|
|
106
|
+
"docFields",
|
|
107
|
+
"causeMarker",
|
|
108
|
+
"reviewSchema",
|
|
109
|
+
])("%s → the generic unknown-key error", async (key) => {
|
|
110
|
+
const r = await lint(project({ [key]: 1 }));
|
|
111
|
+
expect(r.code).toBe(2);
|
|
112
|
+
expect(r.err).toMatch(new RegExp(`unknown key "${key}"`));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("paper/typography → not a rule paperlint ships", async () => {
|
|
116
|
+
const r = await lint(project({ rules: { "paper/typography": "off" } }));
|
|
117
|
+
expect(r.code).toBe(2);
|
|
118
|
+
expect(r.err).toMatch(/"paper\/typography" is not a rule paperlint ships/);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -98,11 +98,11 @@ try {
|
|
|
98
98
|
const papers = join(work, "papers");
|
|
99
99
|
const r = newPaper(papers, "demo", "tex");
|
|
100
100
|
check(
|
|
101
|
-
"a fresh folder gets the scorecard
|
|
101
|
+
"a fresh folder gets the scorecard, a .tex source and paperlint.json, all from the package",
|
|
102
102
|
r.ok &&
|
|
103
103
|
r.fresh &&
|
|
104
104
|
r.files.map((f) => `${f.file}:${f.status}:${f.from}`).join(" ") ===
|
|
105
|
-
"PIPELINE-STATUS.md:created:package paper.tex:created:package",
|
|
105
|
+
"PIPELINE-STATUS.md:created:package paper.tex:created:package paperlint.json:created:package",
|
|
106
106
|
);
|
|
107
107
|
const status = read(papers, "demo", STATUS_FILE);
|
|
108
108
|
const fm = front(status);
|
|
@@ -122,8 +122,10 @@ try {
|
|
|
122
122
|
);
|
|
123
123
|
const l = await lint(join(papers, "demo"));
|
|
124
124
|
check(
|
|
125
|
-
"🔴 `paperlint lint` passes the scaffold
|
|
126
|
-
l.code === 0 &&
|
|
125
|
+
"🔴 `paperlint lint` passes the scaffold — exit 0, not a missing-file error — with the one warning that no venue is chosen yet",
|
|
126
|
+
l.code === 0 &&
|
|
127
|
+
/names no venue preset yet/.test(l.text) &&
|
|
128
|
+
/1 problem \(0 errors, 1 warning\)/.test(l.text),
|
|
127
129
|
);
|
|
128
130
|
|
|
129
131
|
// The format contract of the skills' checker: four sections, parsed.
|
|
@@ -171,9 +173,10 @@ try {
|
|
|
171
173
|
);
|
|
172
174
|
check(
|
|
173
175
|
"wantedFiles asks for a source only when neither format is there",
|
|
174
|
-
wantedFiles(join(papers, "old"), "tex").join() ===
|
|
176
|
+
wantedFiles(join(papers, "old"), "tex").join() ===
|
|
177
|
+
`${STATUS_FILE},paperlint.json` &&
|
|
175
178
|
wantedFiles(join(papers, "nowhere"), "md").join() ===
|
|
176
|
-
`${STATUS_FILE},paper.md`,
|
|
179
|
+
`${STATUS_FILE},paper.md,paperlint.json`,
|
|
177
180
|
);
|
|
178
181
|
writeFileSync(join(papers, "afile"), "x");
|
|
179
182
|
check(
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperlint new` writes `<paper>/paperlint.json` — from the package's template, or from the
|
|
3
|
+
* project's `<papers>/.template/` when it has one — with no venue chosen yet, which lint then names
|
|
4
|
+
* in one warning instead of staying silent.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
mkdirSync,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
17
|
+
import { newPaper, OVERRIDE_DIR } from "./new-paper.ts";
|
|
18
|
+
import { parsePaperSettings } from "./paper-settings.ts";
|
|
19
|
+
import { run } from "./cli.ts";
|
|
20
|
+
|
|
21
|
+
const dirs: string[] = [];
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
const tmp = () => {
|
|
26
|
+
const d = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-new-")));
|
|
27
|
+
dirs.push(d);
|
|
28
|
+
return d;
|
|
29
|
+
};
|
|
30
|
+
const settingsOf = (dir: string) =>
|
|
31
|
+
JSON.parse(readFileSync(join(dir, "paperlint.json"), "utf8")) as Record<
|
|
32
|
+
string,
|
|
33
|
+
unknown
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
describe("paperlint new — paperlint.json", () => {
|
|
37
|
+
it("is always written, from the package template: no venue chosen yet, and valid", () => {
|
|
38
|
+
const papers = join(tmp(), "papers");
|
|
39
|
+
const r = newPaper(papers, "demo", "tex");
|
|
40
|
+
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")).toEqual({
|
|
41
|
+
file: "paperlint.json",
|
|
42
|
+
status: "created",
|
|
43
|
+
from: "package",
|
|
44
|
+
});
|
|
45
|
+
const s = settingsOf(join(papers, "demo"));
|
|
46
|
+
expect(s["extends"]).toBeNull();
|
|
47
|
+
expect(String(s["$comment"])).toMatch(/paperlint:agenticdev/);
|
|
48
|
+
expect(parsePaperSettings(s)).toEqual({
|
|
49
|
+
ok: true,
|
|
50
|
+
value: { extends: null, kind: null, pdf: null, rules: null },
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("comes from the project's <papers>/.template/ when it has one", () => {
|
|
55
|
+
const papers = join(tmp(), "papers");
|
|
56
|
+
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
57
|
+
writeFileSync(
|
|
58
|
+
join(papers, OVERRIDE_DIR, "paperlint.json"),
|
|
59
|
+
'{ "extends": "paperlint:aisec", "kind": "research" }\n',
|
|
60
|
+
);
|
|
61
|
+
const r = newPaper(papers, "house", "tex");
|
|
62
|
+
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")?.from).toBe(
|
|
63
|
+
"project",
|
|
64
|
+
);
|
|
65
|
+
expect(settingsOf(join(papers, "house"))).toEqual({
|
|
66
|
+
extends: "paperlint:aisec",
|
|
67
|
+
kind: "research",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("is never overwritten", () => {
|
|
72
|
+
const papers = join(tmp(), "papers");
|
|
73
|
+
mkdirSync(join(papers, "p"), { recursive: true });
|
|
74
|
+
writeFileSync(
|
|
75
|
+
join(papers, "p", "paperlint.json"),
|
|
76
|
+
'{"extends":"paperlint:aisec"}',
|
|
77
|
+
);
|
|
78
|
+
const r = newPaper(papers, "p", "tex");
|
|
79
|
+
expect(
|
|
80
|
+
r.ok && r.files.find((f) => f.file === "paperlint.json")?.status,
|
|
81
|
+
).toBe("kept");
|
|
82
|
+
expect(settingsOf(join(papers, "p"))).toEqual({
|
|
83
|
+
extends: "paperlint:aisec",
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
89
|
+
async function lintNew(extendsValue: string | null) {
|
|
90
|
+
const root = tmp();
|
|
91
|
+
writeFileSync(
|
|
92
|
+
join(root, "package.json"),
|
|
93
|
+
JSON.stringify({ name: "c", private: true }),
|
|
94
|
+
);
|
|
95
|
+
newPaper(join(root, "papers"), "demo", "tex");
|
|
96
|
+
if (extendsValue !== null)
|
|
97
|
+
writeFileSync(
|
|
98
|
+
join(root, "papers", "demo", "paperlint.json"),
|
|
99
|
+
JSON.stringify({ extends: extendsValue, kind: "short" }),
|
|
100
|
+
);
|
|
101
|
+
const out: string[] = [];
|
|
102
|
+
const code = await run(["lint", "--json"], {
|
|
103
|
+
cwd: root,
|
|
104
|
+
log: (s: string) => out.push(s),
|
|
105
|
+
err: () => {},
|
|
106
|
+
});
|
|
107
|
+
const messages = (
|
|
108
|
+
JSON.parse(out.join("\n")) as {
|
|
109
|
+
messages: { ruleId: string; severity: number; message: string }[];
|
|
110
|
+
}[]
|
|
111
|
+
).flatMap((r) => r.messages);
|
|
112
|
+
return { code, messages };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
it("gets exactly one warning naming the file to set, and exits 0", async () => {
|
|
116
|
+
const { code, messages } = await lintNew(null);
|
|
117
|
+
expect(code).toBe(0);
|
|
118
|
+
expect(messages).toHaveLength(1);
|
|
119
|
+
expect(messages[0]?.ruleId).toBe("pdf/measured");
|
|
120
|
+
expect(messages[0]?.severity).toBe(1);
|
|
121
|
+
expect(messages[0]?.message).toMatch(/names no venue preset yet/);
|
|
122
|
+
expect(messages[0]?.message).toMatch(
|
|
123
|
+
/set "extends" in .*papers\/demo\/paperlint\.json/,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("a paper with a real extends does not get it", async () => {
|
|
128
|
+
const { messages } = await lintNew("paperlint:agenticdev");
|
|
129
|
+
expect(messages.map((m) => m.message).join("\n")).not.toMatch(
|
|
130
|
+
/names no venue preset yet/,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
package/src/new-paper.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
/* eslint-enable boundaries/dependencies */
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
28
29
|
|
|
29
30
|
export type PaperFormat = "tex" | "md";
|
|
30
31
|
export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
|
|
@@ -83,10 +84,14 @@ export type NewPaperResult =
|
|
|
83
84
|
| { readonly ok: false; readonly reason: string };
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
|
-
* The files a paper folder must end up with: the scorecard,
|
|
87
|
-
*
|
|
87
|
+
* The files a paper folder must end up with: the scorecard, a source in `format` — unless it
|
|
88
|
+
* already has a source in EITHER format, in which case that one stands — and `paperlint.json`.
|
|
88
89
|
*/
|
|
89
90
|
export function wantedFiles(dir: string, format: PaperFormat): string[] {
|
|
91
|
+
return [...scorecardAndSource(dir, format), CONFIG_FILE];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function scorecardAndSource(dir: string, format: PaperFormat): string[] {
|
|
90
95
|
const hasSource = Object.values(SOURCE_FILE).some((f) =>
|
|
91
96
|
existsSync(join(dir, f)),
|
|
92
97
|
);
|