paperlint 2.1.0 → 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/README.md +233 -224
- 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.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +38 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +28 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +277 -265
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +40 -72
- package/dist/doctor.js.map +1 -1
- package/dist/facts-file.d.ts +2 -2
- package/dist/facts-file.js +2 -2
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +39 -29
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +143 -172
- 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.map +1 -1
- package/dist/new-paper.js +2 -13
- 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 +13 -41
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +71 -71
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -6
- package/dist/presets.js.map +1 -1
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +5 -3
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +15 -6
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +3 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.js +2 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +20 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +119 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +60 -23
- 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 -31
- package/eslint-rules/pdf-last-page-balance.mjs +4 -7
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -25
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +83 -105
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +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/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build.harness.mjs +8 -4
- package/src/build.ts +62 -6
- package/src/cli.harness.mjs +147 -292
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +344 -304
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/facts-file.test.ts +0 -7
- package/src/facts-file.ts +2 -2
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +186 -230
- 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.test.ts +1 -13
- package/src/new-paper.ts +2 -17
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +84 -121
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/ports/check-references.ts +28 -0
- package/src/presets.ts +5 -15
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +19 -7
- package/src/structure.harness.mjs +3 -1
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +20 -11
- package/src/venue-rules.ts +11 -13
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paper/section-word · paper/leading-zero · paper/figure-ref-style · bib/reachable-entry — each
|
|
3
|
+
* FIRES on a bad input, stays SILENT on a good one, reports every occurrence at its own place,
|
|
4
|
+
* and (the first three) fixes to an exact output. Plus the escape hatch: a disable directive in a
|
|
5
|
+
* `%` comment, including inside the `filecontents` bibliography, silences exactly one finding,
|
|
6
|
+
* and a directive that silences nothing is itself reported.
|
|
7
|
+
*
|
|
8
|
+
* The leading-zero cases are the ones paperlint#44 measured: a regex over the raw source found 22
|
|
9
|
+
* decimals on a real corpus and none was a defect, so the SILENT list is as load-bearing as the
|
|
10
|
+
* CAUGHT one.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { ESLint } from "eslint";
|
|
14
|
+
import markdown from "@eslint/markdown";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { texLanguage } from "./latex-language.mjs";
|
|
19
|
+
import typography from "./paper-typography.mjs";
|
|
20
|
+
import bib from "./bib-reachable-entry.mjs";
|
|
21
|
+
|
|
22
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const FIX = join(HERE, "..", "fixtures", "paper-typography");
|
|
24
|
+
|
|
25
|
+
const RULES = {
|
|
26
|
+
"paper/section-word": "warn",
|
|
27
|
+
"paper/leading-zero": "warn",
|
|
28
|
+
"paper/figure-ref-style": "warn",
|
|
29
|
+
"bib/reachable-entry": "warn",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const eslint = (fix = false) =>
|
|
33
|
+
new ESLint({
|
|
34
|
+
cwd: FIX,
|
|
35
|
+
overrideConfigFile: true,
|
|
36
|
+
fix,
|
|
37
|
+
overrideConfig: [
|
|
38
|
+
{
|
|
39
|
+
files: ["**/*.tex"],
|
|
40
|
+
plugins: {
|
|
41
|
+
tex: { languages: { latex: texLanguage } },
|
|
42
|
+
paper: typography,
|
|
43
|
+
bib,
|
|
44
|
+
},
|
|
45
|
+
language: "tex/latex",
|
|
46
|
+
linterOptions: { reportUnusedDisableDirectives: "error" },
|
|
47
|
+
rules: RULES,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
files: ["**/*.md"],
|
|
51
|
+
plugins: { markdown, paper: typography },
|
|
52
|
+
language: "markdown/gfm",
|
|
53
|
+
languageOptions: { frontmatter: "yaml" },
|
|
54
|
+
rules: {
|
|
55
|
+
"paper/section-word": "warn",
|
|
56
|
+
"paper/leading-zero": "warn",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Lint `text` as `file`; no crash allowed, only findings. */
|
|
63
|
+
async function lint(text, file = "paper.tex", fix = false) {
|
|
64
|
+
const [res] = await eslint(fix).lintText(text, {
|
|
65
|
+
filePath: join(FIX, "inline", file),
|
|
66
|
+
});
|
|
67
|
+
expect(res.messages.filter((m) => m.fatal)).toEqual([]);
|
|
68
|
+
return res;
|
|
69
|
+
}
|
|
70
|
+
const ids = (res, id) => res.messages.filter((m) => !id || m.ruleId === id);
|
|
71
|
+
const fixed = async (text, file) =>
|
|
72
|
+
(await lint(text, file, true)).output ?? text;
|
|
73
|
+
const doc = (body) =>
|
|
74
|
+
`\\documentclass{acmart}\n\\begin{document}\n${body}\n\\end{document}\n`;
|
|
75
|
+
|
|
76
|
+
describe("the fixtures: messy fires on each rule, clean is silent", () => {
|
|
77
|
+
it("messy-paper — every occurrence, at its own line", async () => {
|
|
78
|
+
const res = await lint(
|
|
79
|
+
readFileSync(join(FIX, "messy-paper", "paper.tex"), "utf8"),
|
|
80
|
+
);
|
|
81
|
+
const count = (id) => ids(res, id).length;
|
|
82
|
+
expect(count("paper/section-word")).toBe(3);
|
|
83
|
+
expect(count("paper/leading-zero")).toBe(2);
|
|
84
|
+
expect(count("paper/figure-ref-style")).toBe(1);
|
|
85
|
+
expect(count("bib/reachable-entry")).toBe(2);
|
|
86
|
+
expect(ids(res, "bib/reachable-entry").map((m) => m.line)).toEqual([3, 8]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("clean-paper — nothing", async () => {
|
|
90
|
+
const res = await lint(
|
|
91
|
+
readFileSync(join(FIX, "clean-paper", "paper.tex"), "utf8"),
|
|
92
|
+
);
|
|
93
|
+
expect(res.messages).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("paper/section-word", () => {
|
|
98
|
+
it("🔴 the MACRO form counts, not only the glyph — and each is fixed exactly", async () => {
|
|
99
|
+
const src = doc("See §~\\ref{a}, \\S\\ref{b}, \\S~\\ref{c}, \\S 4 and §5.");
|
|
100
|
+
expect(ids(await lint(src), "paper/section-word")).toHaveLength(5);
|
|
101
|
+
expect(await fixed(src)).toBe(
|
|
102
|
+
doc(
|
|
103
|
+
"See Section~\\ref{a}, Section~\\ref{b}, Section~\\ref{c}, Section 4 and Section 5.",
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("a glyph before prose is reported but not rewritten", async () => {
|
|
109
|
+
const src = doc("As §§ above.");
|
|
110
|
+
expect(ids(await lint(src), "paper/section-word")).toHaveLength(2);
|
|
111
|
+
expect(await fixed(src)).toBe(src);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("silent on Section written out, on comments, on the bibliography", async () => {
|
|
115
|
+
const src =
|
|
116
|
+
"\\documentclass{acmart}\n\\begin{filecontents*}{refs.bib}\n@misc{k, note = {see §5}, url = {https://x.org}}\n\\end{filecontents*}\n" +
|
|
117
|
+
"\\begin{document}\nSection~\\ref{a}.\n% §5 in an old draft\n\\end{document}\n";
|
|
118
|
+
expect(ids(await lint(src), "paper/section-word")).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("markdown: `§5` → `Section 5`, code untouched", async () => {
|
|
122
|
+
const src = "See §5 and `§3` here.\n";
|
|
123
|
+
expect(ids(await lint(src, "paper.md"), "paper/section-word")).toHaveLength(
|
|
124
|
+
1,
|
|
125
|
+
);
|
|
126
|
+
expect(await fixed(src, "paper.md")).toBe("See Section 5 and `§3` here.\n");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("paper/leading-zero", () => {
|
|
131
|
+
const caught = [
|
|
132
|
+
["a p-value in inline math", "We call it significant at $p < .05$."],
|
|
133
|
+
["a p-value after other math", "(bugfix $-31\\%$, $p=.002$)"],
|
|
134
|
+
[
|
|
135
|
+
"a table cell",
|
|
136
|
+
"\\begin{tabular}{ll}\ntask & p \\\\\nbugfix & .037 \\\\\n\\end{tabular}",
|
|
137
|
+
],
|
|
138
|
+
["plain prose", "We use a threshold of .05 throughout."],
|
|
139
|
+
];
|
|
140
|
+
it.each(caught)("CAUGHT, exactly one: %s", async (_, body) => {
|
|
141
|
+
expect(ids(await lint(doc(body)), "paper/leading-zero")).toHaveLength(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const silent = [
|
|
145
|
+
["a figure width option", "\\includegraphics[width=.48\\columnwidth]{f}"],
|
|
146
|
+
[
|
|
147
|
+
"a tabular column spec",
|
|
148
|
+
"\\begin{tabular}{p{.25\\linewidth}l}\na & b \\\\\n\\end{tabular}",
|
|
149
|
+
],
|
|
150
|
+
["a comment", "Text.\n% p<.01 in the old draft\nMore text."],
|
|
151
|
+
["a listing", "\\begin{lstlisting}\nx = .25\n\\end{lstlisting}"],
|
|
152
|
+
["inline listing", "Call \\lstinline{.25} here."],
|
|
153
|
+
["a length argument", "A\\hspace{.3em}B\\hspace{.35em}C"],
|
|
154
|
+
["a macro definition body", "\\def\\x{.85}"],
|
|
155
|
+
[
|
|
156
|
+
"tikz coordinates",
|
|
157
|
+
"\\begin{tikzpicture}\\draw (.35,.65);\\end{tikzpicture}",
|
|
158
|
+
],
|
|
159
|
+
["an arXiv id", "See arXiv 2310.05736 for details."],
|
|
160
|
+
[
|
|
161
|
+
"an unsigned environment's width",
|
|
162
|
+
"\\begin{subfigure}[b]{.48\\textwidth}\nPanel.\n\\end{subfigure}",
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"an unsigned table's column spec",
|
|
166
|
+
"\\begin{longtable}{p{.25\\linewidth}l}\na & b \\\\\n\\end{longtable}",
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
"an unknown macro's options",
|
|
170
|
+
"\\adjustbox{width=.48\\linewidth}{x} \\foo[scale=.75]{y}",
|
|
171
|
+
],
|
|
172
|
+
];
|
|
173
|
+
it.each(silent)("SILENT: %s", async (_, body) => {
|
|
174
|
+
expect(ids(await lint(doc(body)), "paper/leading-zero")).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("the preamble and the inline bibliography are not the reader's text", async () => {
|
|
178
|
+
const src =
|
|
179
|
+
"\\documentclass{acmart}\n\\renewcommand{\\arraystretch}{.85}\n" +
|
|
180
|
+
"\\begin{filecontents*}{refs.bib}\n@misc{k, note = {p = .05}, url = {https://x.org}}\n" +
|
|
181
|
+
"\\end{filecontents*}\n\\begin{document}\nNothing here.\n\\end{document}\n";
|
|
182
|
+
expect(ids(await lint(src), "paper/leading-zero")).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("the fix inserts the zero at the dot — in math, in a table, in prose", async () => {
|
|
186
|
+
const src = doc(
|
|
187
|
+
"At $p < .05$ and $d=.21$; cell & .037 \\\\ and .5 or .25.",
|
|
188
|
+
);
|
|
189
|
+
expect(await fixed(src)).toBe(
|
|
190
|
+
doc("At $p < 0.05$ and $d=0.21$; cell & 0.037 \\\\ and .5 or 0.25."),
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("markdown: prose and a table cell are fixed, code is not", async () => {
|
|
195
|
+
const src =
|
|
196
|
+
"Significant at p<.05.\n\n| task | p |\n| --- | --- |\n| bugfix | .002 |\n\nRun `.25`.\n";
|
|
197
|
+
expect(ids(await lint(src, "paper.md"), "paper/leading-zero")).toHaveLength(
|
|
198
|
+
2,
|
|
199
|
+
);
|
|
200
|
+
expect(await fixed(src, "paper.md")).toBe(
|
|
201
|
+
"Significant at p<0.05.\n\n| task | p |\n| --- | --- |\n| bugfix | 0.002 |\n\nRun `.25`.\n",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("paper/figure-ref-style", () => {
|
|
207
|
+
it("the minority form is reported and rewritten to the majority", async () => {
|
|
208
|
+
const src = doc("Figure~\\ref{a}, Figure~\\ref{b} and Fig.~\\ref{c}.");
|
|
209
|
+
const found = ids(await lint(src), "paper/figure-ref-style");
|
|
210
|
+
expect(found).toHaveLength(1);
|
|
211
|
+
expect(found[0].column).toBe(src.split("\n")[2].indexOf("Fig.") + 1);
|
|
212
|
+
expect(await fixed(src)).toBe(
|
|
213
|
+
doc("Figure~\\ref{a}, Figure~\\ref{b} and Figure~\\ref{c}."),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("the majority can be the short form", async () => {
|
|
218
|
+
const src = doc("Fig.~\\ref{a}, Fig.~\\ref{b} and Figure~\\ref{c}.");
|
|
219
|
+
expect(await fixed(src)).toBe(
|
|
220
|
+
doc("Fig.~\\ref{a}, Fig.~\\ref{b} and Fig.~\\ref{c}."),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("silent when one form is used throughout", async () => {
|
|
225
|
+
expect(
|
|
226
|
+
ids(
|
|
227
|
+
await lint(doc("Fig.~\\ref{a} and Fig.~\\ref{b}.")),
|
|
228
|
+
"paper/figure-ref-style",
|
|
229
|
+
),
|
|
230
|
+
).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("bib/reachable-entry", () => {
|
|
235
|
+
const bibDoc = (entries) =>
|
|
236
|
+
`\\documentclass{acmart}\n\\begin{filecontents*}{refs.bib}\n${entries}\n\\end{filecontents*}\n\\begin{document}\nx\n\\end{document}\n`;
|
|
237
|
+
|
|
238
|
+
it("a doi, a url or an arXiv id each make an entry reachable", async () => {
|
|
239
|
+
const res = await lint(
|
|
240
|
+
bibDoc(
|
|
241
|
+
"@article{a, doi = {10.1/x}}\n@misc{b, url = {https://x.org}}\n@misc{c, note = {arXiv:2310.05736}}\n@string{v = {x}}",
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
expect(ids(res, "bib/reachable-entry")).toEqual([]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("reports on the entry itself, naming its key", async () => {
|
|
248
|
+
const res = await lint(
|
|
249
|
+
bibDoc("@misc{ok, url = {https://x.org}}\n@article{lost, title = {x}}"),
|
|
250
|
+
);
|
|
251
|
+
const [m] = ids(res, "bib/reachable-entry");
|
|
252
|
+
expect(m.line).toBe(4);
|
|
253
|
+
expect(m.column).toBe(1);
|
|
254
|
+
expect(m.message).toMatch(/`lost`/);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("the escape hatch — a disable directive in a `%` comment", () => {
|
|
259
|
+
it("🔴 inside the filecontents bibliography, it silences exactly the next entry", async () => {
|
|
260
|
+
const res = await lint(
|
|
261
|
+
`\\documentclass{acmart}\n\\begin{filecontents*}{refs.bib}\n` +
|
|
262
|
+
"% eslint-disable-next-line bib/reachable-entry -- an invited talk, no recording exists\n" +
|
|
263
|
+
"@misc{talk, title = {x}}\n@misc{lost, title = {y}}\n" +
|
|
264
|
+
"\\end{filecontents*}\n\\begin{document}\nx\n\\end{document}\n",
|
|
265
|
+
);
|
|
266
|
+
const found = ids(res, "bib/reachable-entry");
|
|
267
|
+
expect(found).toHaveLength(1);
|
|
268
|
+
expect(found[0].message).toMatch(/`lost`/);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("in the body: next-line, same-line, and a disable/enable region", async () => {
|
|
272
|
+
const src = doc(
|
|
273
|
+
[
|
|
274
|
+
"% eslint-disable-next-line paper/leading-zero -- quoted from the reviewer",
|
|
275
|
+
"They wrote .05 here.",
|
|
276
|
+
"And .06 here. % eslint-disable-line paper/leading-zero -- also quoted",
|
|
277
|
+
"% eslint-disable paper/leading-zero -- a quoted table",
|
|
278
|
+
"Region .07 and .08.",
|
|
279
|
+
"% eslint-enable paper/leading-zero",
|
|
280
|
+
"But .09 counts.",
|
|
281
|
+
].join("\n"),
|
|
282
|
+
);
|
|
283
|
+
const found = ids(await lint(src), "paper/leading-zero");
|
|
284
|
+
expect(found.map((m) => m.line)).toEqual([9]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("a directive that silences nothing is itself reported", async () => {
|
|
288
|
+
const res = await lint(
|
|
289
|
+
doc(
|
|
290
|
+
"% eslint-disable-next-line paper/leading-zero -- stale\nNo decimals here.",
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
expect(
|
|
294
|
+
res.messages.filter((m) => /Unused eslint-disable/.test(m.message)),
|
|
295
|
+
).toHaveLength(1);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -47,9 +47,8 @@ const repoWith = (...dirs) => {
|
|
|
47
47
|
for (const d of dirs) mkdirSync(join(base, d), { recursive: true });
|
|
48
48
|
return base;
|
|
49
49
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
50
|
+
/** The root `paperlint.json`, parsed, declaring `papers`. */
|
|
51
|
+
const declaring = (papers) => ({ [PAPERS_DIR_FIELD]: papers });
|
|
53
52
|
|
|
54
53
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
55
54
|
// I. THE DEFAULT AND THE DECLARATION
|
|
@@ -57,10 +56,9 @@ const declaring = (papers) => ({
|
|
|
57
56
|
{
|
|
58
57
|
const repo = repoWith(DEFAULT_PAPERS_ROOT);
|
|
59
58
|
for (const [label, pkg] of [
|
|
60
|
-
["no
|
|
61
|
-
["an empty
|
|
62
|
-
["a
|
|
63
|
-
["our key present but empty", { paperlint: {} }],
|
|
59
|
+
["no paperlint.json at all", undefined],
|
|
60
|
+
["an empty paperlint.json", {}],
|
|
61
|
+
["a paperlint.json with only other keys", { kind: "short", rules: {} }],
|
|
64
62
|
])
|
|
65
63
|
assert.equal(
|
|
66
64
|
papersRoot(pkg, repo),
|
|
@@ -97,8 +95,8 @@ for (const declared of ["papers", "docs/papers", "writing/drafts", "a/b/c/d"]) {
|
|
|
97
95
|
e.message.includes('"writing/drafts"') &&
|
|
98
96
|
// names WHERE it looked,
|
|
99
97
|
e.message.includes(repo) &&
|
|
100
|
-
// tells the reader it came from
|
|
101
|
-
/declared in
|
|
98
|
+
// tells the reader it came from paperlint.json rather than from a default,
|
|
99
|
+
/declared in paperlint\.json/.test(e.message) &&
|
|
102
100
|
// and says WHY this is fatal instead of ignorable.
|
|
103
101
|
/indistinguishable/.test(e.message),
|
|
104
102
|
"a declared-but-missing root must throw, and the message must name the value, the place " +
|
|
@@ -113,9 +111,7 @@ for (const declared of ["papers", "docs/papers", "writing/drafts", "a/b/c/d"]) {
|
|
|
113
111
|
(e) =>
|
|
114
112
|
/does not exist/.test(e.message) &&
|
|
115
113
|
/Nothing was declared/.test(e.message) &&
|
|
116
|
-
e.message.includes(
|
|
117
|
-
`"paperlint": { "${PAPERS_DIR_FIELD}": "path/to/papers" }`,
|
|
118
|
-
),
|
|
114
|
+
e.message.includes(`{ "${PAPERS_DIR_FIELD}": "path/to/papers" }`),
|
|
119
115
|
"a missing root reached by DEFAULTING must say so and show the declaration to add",
|
|
120
116
|
);
|
|
121
117
|
}
|
package/eslint-rules/papers.mjs
CHANGED
|
@@ -10,25 +10,21 @@
|
|
|
10
10
|
* that forbids imports), the PROSE of a skill (read by a model, not resolved by code), and
|
|
11
11
|
* LaTeX (`TEXINPUTS`, kpathsea).
|
|
12
12
|
*
|
|
13
|
-
* ── THE DECISION
|
|
14
|
-
* ONE declaration, in the
|
|
13
|
+
* ── THE DECISION ────────────────────────────────────────────────────────────
|
|
14
|
+
* ONE declaration, in the project's root `paperlint.json`, with a default:
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* { "papersDir": "docs/papers" }
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* No file, or no key → `papers`. Every carrier then reads that one value with its OWN standard
|
|
19
|
+
* mechanism:
|
|
20
20
|
*
|
|
21
|
-
* ESLint → `
|
|
22
|
-
* hook → `needs: [provide("
|
|
21
|
+
* ESLint → `settingsOf(root)` (lib/paper-config.mjs) + this module
|
|
22
|
+
* hook → `needs: [provide("config", …)]` cats the file, JSON.parse inside `decide`
|
|
23
23
|
* prose → the skill names a COMMAND, not a path:
|
|
24
|
-
* `node -p "require('./
|
|
24
|
+
* `node -p "(() => { try { return require('./paperlint.json').papersDir } catch {} })() ?? 'papers'"`
|
|
25
25
|
* LaTeX → `TEXINPUTS` built FROM THE SCRIPT (`$(dirname "$0")/../tex//:`), so the
|
|
26
26
|
* consumer declares nothing at all for this carrier
|
|
27
27
|
*
|
|
28
|
-
* `package.json` is the right home rather than a new dotfile for one measured reason: it is
|
|
29
|
-
* the file Node already opens to resolve this package, so it cannot be absent, cannot be
|
|
30
|
-
* gitignored, and needs no discovery rules of its own.
|
|
31
|
-
*
|
|
32
28
|
* ── WHAT WAS MEASURED AND KILLED, so it is not proposed again ───────────────
|
|
33
29
|
* (full measurements: consumer repo, `idei/paper-pipeline-extraction/`, 2026-09-11)
|
|
34
30
|
*
|
|
@@ -85,17 +81,14 @@ import {
|
|
|
85
81
|
CONFIG_KEY,
|
|
86
82
|
DEFAULT_PAPERS_ROOT,
|
|
87
83
|
PAPERS_DIR_FIELD,
|
|
88
|
-
|
|
89
|
-
declaredSettings,
|
|
90
|
-
renamedFieldMessage,
|
|
91
|
-
settingsOf,
|
|
84
|
+
CONFIG_FILE,
|
|
92
85
|
} from "../lib/paper-config.mjs";
|
|
93
86
|
export { DEFAULT_PAPERS_ROOT };
|
|
94
87
|
|
|
95
88
|
/**
|
|
96
89
|
* The declared papers root, verified to be on disk.
|
|
97
90
|
*
|
|
98
|
-
* @param
|
|
91
|
+
* @param settings the project's root `paperlint.json`, parsed (`settingsOf(root)`), or undefined
|
|
99
92
|
* @param baseDir the directory the root is relative to. PASS `import.meta.dirname` FROM THE
|
|
100
93
|
* CONFIG rather than relying on the default: `process.cwd()` is wherever the
|
|
101
94
|
* editor, the hook runtime or the CI step happened to start, and in a git
|
|
@@ -105,12 +98,8 @@ export { DEFAULT_PAPERS_ROOT };
|
|
|
105
98
|
* globs, which ESLint interprets relative to the config, and an absolute path there
|
|
106
99
|
* would change their meaning.
|
|
107
100
|
*/
|
|
108
|
-
export function papersRoot(
|
|
109
|
-
const
|
|
110
|
-
if (found.conflict !== null) throw new TypeError(found.conflict);
|
|
111
|
-
const renamed = renamedFieldMessage(found.settings);
|
|
112
|
-
if (renamed) throw new TypeError(renamed);
|
|
113
|
-
const declared = settingsOf(pkg)?.[PAPERS_DIR_FIELD];
|
|
101
|
+
export function papersRoot(settings, baseDir = process.cwd()) {
|
|
102
|
+
const declared = settings?.[PAPERS_DIR_FIELD];
|
|
114
103
|
// 🔴 `declared === undefined`, NOT `declared ?? DEFAULT`. The two differ on exactly one
|
|
115
104
|
// input — `"papersDir": null` — and the difference is the whole point: `??` reads an explicit
|
|
116
105
|
// `null` as "nothing was declared" and silently uses the default, which is a typed keystroke
|
|
@@ -127,9 +116,9 @@ export function papersRoot(pkg, baseDir = process.cwd()) {
|
|
|
127
116
|
`${CONFIG_KEY}: the papers root "${root}" does not exist under ${baseDir}.\n` +
|
|
128
117
|
(declared === undefined
|
|
129
118
|
? `Nothing was declared, so the default "${DEFAULT_PAPERS_ROOT}" was used. Declare the ` +
|
|
130
|
-
`real location in
|
|
131
|
-
`
|
|
132
|
-
: `It is declared in
|
|
119
|
+
`real location in ${CONFIG_FILE}:\n` +
|
|
120
|
+
` { "${PAPERS_DIR_FIELD}": "path/to/papers" }`
|
|
121
|
+
: `It is declared in ${CONFIG_FILE} as "${declared}". Fix it there, or create the ` +
|
|
133
122
|
`directory.`) +
|
|
134
123
|
`\nThis is thrown rather than ignored on purpose: a papers root that matches nothing ` +
|
|
135
124
|
`makes every rule lint zero files, and a run with zero findings is indistinguishable ` +
|
|
@@ -155,11 +144,8 @@ export function paperFiles(root) {
|
|
|
155
144
|
tex: [`${root}/*/paper.tex`],
|
|
156
145
|
/** The per-paper stage ledger. */
|
|
157
146
|
status: [`${root}/*/PIPELINE-STATUS.md`],
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
* old name so a consumer's config that lints these files keeps matching them.
|
|
161
|
-
*/
|
|
162
|
-
venue: [`${root}/*/${PAPER_SETTINGS_FILE}`],
|
|
147
|
+
/** The per-paper settings file, `paperlint.json`. */
|
|
148
|
+
venue: [`${root}/*/${CONFIG_FILE}`],
|
|
163
149
|
/** Facts extracted from the built PDF. */
|
|
164
150
|
pdfFacts: [`${root}/*/_build/paper.facts.json`],
|
|
165
151
|
/** Facts extracted from the bibliography. */
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
* ICSE/ASE/SIGSOFT/SIGPLAN proceedings; Sheridan for ACM SIG conferences such as CCS), IEEEtran
|
|
7
7
|
* advises it for camera-ready work, and ACL, USENIX, AAAI and ICML say nothing. No standard format
|
|
8
8
|
* checker (HotCRP's banal, aclpubcheck, IEEE PDF eXpress) tests it. So a consumer turns it on for
|
|
9
|
-
* the papers whose venue asks, in
|
|
9
|
+
* the papers whose venue asks, in that paper's paperlint.json:
|
|
10
10
|
*
|
|
11
|
-
* "
|
|
12
|
-
* "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] } } ] }
|
|
11
|
+
* { "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] } }
|
|
13
12
|
*
|
|
14
13
|
* (That is a line comment's worth of JSON in a block comment only because it has no asterisk
|
|
15
14
|
* followed by a slash; see rule 8 of CLAUDE.md before editing it.)
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
import { createHash } from "node:crypto";
|
|
37
36
|
import { existsSync, readFileSync } from "node:fs";
|
|
38
37
|
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
39
|
-
import {
|
|
38
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
40
39
|
|
|
41
40
|
/** The difference, in points, that two columns may end apart. See the harness for why 120. */
|
|
42
41
|
export const DEFAULT_TOLERANCE_PT = 120;
|
|
@@ -143,9 +142,7 @@ function reportLine(file) {
|
|
|
143
142
|
*/
|
|
144
143
|
function extendsPreset(paperDir) {
|
|
145
144
|
try {
|
|
146
|
-
const s = JSON.parse(
|
|
147
|
-
readFileSync(join(paperDir, PAPER_SETTINGS_FILE), "utf8"),
|
|
148
|
-
);
|
|
145
|
+
const s = JSON.parse(readFileSync(join(paperDir, CONFIG_FILE), "utf8"));
|
|
149
146
|
return typeof s?.extends === "string" && s.extends !== "";
|
|
150
147
|
} catch {
|
|
151
148
|
return false;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `review/frontmatter` — a review report's frontmatter is a RECORD, validated by a JSON Schema.
|
|
3
|
+
*
|
|
4
|
+
* ── WHAT IT CHECKS ───────────────────────────────────────────────────────────────
|
|
5
|
+
* A review under `reviews/` declares what it found in its YAML frontmatter:
|
|
6
|
+
*
|
|
7
|
+
* findings:
|
|
8
|
+
* - id: 1
|
|
9
|
+
* status: open # open | fixed | wontfix
|
|
10
|
+
* cause: missing-skill # skill-defect | missing-skill | hook | rule — required when open
|
|
11
|
+
*
|
|
12
|
+
* The pipeline convention behind it: a review does not end with a list of findings, it ends with
|
|
13
|
+
* what in the PIPELINE let each one through. Otherwise the text gets fixed while the tool that
|
|
14
|
+
* missed it stays the same. The schema is `review-frontmatter.schema.json` beside this file, and
|
|
15
|
+
* "an open finding names its cause" is its `if`/`then`, not code here.
|
|
16
|
+
*
|
|
17
|
+
* The same mechanism, with a schema of its own, is `sibling/frontmatter`
|
|
18
|
+
* (`sibling-frontmatter.mjs`): `frontmatterRule` below is the one implementation.
|
|
19
|
+
*
|
|
20
|
+
* ── WHY THIS REPLACED TWO RULES IN 3.0.0 ────────────────────────────────────────────
|
|
21
|
+
* `review/findings-cause` GUESSED the findings from the prose: it counted numbered table rows and
|
|
22
|
+
* list items starting in bold, looked for a magic word (`causeMarker`, "Cause:") anywhere in the
|
|
23
|
+
* text, spoke only above a threshold (`minFindings`), and skipped reports older than a date
|
|
24
|
+
* (`sinceCreated`) — a date ratchet its own header said belonged in `ignores`. Every one of
|
|
25
|
+
* those was a convention in prose standing in for a field nobody had declared.
|
|
26
|
+
* `doc/fields` was a hand-rolled schema language in the config (`{ field: { values: [...] } }`),
|
|
27
|
+
* and its one real use was the `read:` field of sibling cards — which this package's own
|
|
28
|
+
* `analyze-sibling-paper` skill writes, so the package now ships that schema too. The record is
|
|
29
|
+
* data, the schema is JSON Schema, the validator is ajv.
|
|
30
|
+
*
|
|
31
|
+
* A review with no `findings` key is not a findings record and is not checked for findings — so
|
|
32
|
+
* there is no date to compare: old reports simply have no records. A file with no frontmatter is
|
|
33
|
+
* validated as `{}`, so a field a schema requires is still reported on it: a missing header must
|
|
34
|
+
* not be a way around the check.
|
|
35
|
+
*
|
|
36
|
+
* YAML is read with js-yaml's CORE schema, which leaves `2026-09-20` a string. The default schema
|
|
37
|
+
* turns it into a Date, and a schema asking for `"type": "string"` would then reject a date the
|
|
38
|
+
* author wrote correctly.
|
|
39
|
+
*/
|
|
40
|
+
import Ajv from "ajv";
|
|
41
|
+
import { CORE_SCHEMA, load } from "js-yaml";
|
|
42
|
+
import { readFileSync } from "node:fs";
|
|
43
|
+
|
|
44
|
+
/** One ajv error as a line a person can act on. `if` errors only repeat their `then`. */
|
|
45
|
+
function describe(e) {
|
|
46
|
+
const where = e.dataPath
|
|
47
|
+
? `\`${e.dataPath.replace(/^\./, "")}\``
|
|
48
|
+
: "the frontmatter";
|
|
49
|
+
if (e.keyword === "enum")
|
|
50
|
+
return `${where} must be one of: ${e.params.allowedValues.join(", ")}`;
|
|
51
|
+
if (e.keyword === "additionalProperties")
|
|
52
|
+
return `${where} has an unknown field \`${e.params.additionalProperty}\``;
|
|
53
|
+
return `${where} ${e.message}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const errorsOf = (validate, data) =>
|
|
57
|
+
validate(data)
|
|
58
|
+
? []
|
|
59
|
+
: (validate.errors ?? []).filter((e) => e.keyword !== "if").map(describe);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A rule that validates a markdown file's YAML frontmatter against the JSON Schema in `schemaUrl`
|
|
63
|
+
* (a file shipped beside the rule). A file with no frontmatter is validated as `{}`.
|
|
64
|
+
*/
|
|
65
|
+
export function frontmatterRule(schemaUrl, description) {
|
|
66
|
+
const validate = new Ajv({ allErrors: true }).compile(
|
|
67
|
+
JSON.parse(readFileSync(schemaUrl, "utf8")),
|
|
68
|
+
);
|
|
69
|
+
return {
|
|
70
|
+
meta: {
|
|
71
|
+
type: "problem",
|
|
72
|
+
docs: { description },
|
|
73
|
+
schema: [],
|
|
74
|
+
messages: {
|
|
75
|
+
malformed:
|
|
76
|
+
"the frontmatter does not parse as YAML ({{why}}) — there is no record to read",
|
|
77
|
+
invalid: "{{problem}}",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
create(context) {
|
|
81
|
+
let frontmatter = null;
|
|
82
|
+
let data = {};
|
|
83
|
+
return {
|
|
84
|
+
yaml(node) {
|
|
85
|
+
frontmatter = node;
|
|
86
|
+
try {
|
|
87
|
+
data = load(node.value ?? "", { schema: CORE_SCHEMA }) ?? {};
|
|
88
|
+
} catch (e) {
|
|
89
|
+
data = null;
|
|
90
|
+
context.report({
|
|
91
|
+
node,
|
|
92
|
+
messageId: "malformed",
|
|
93
|
+
data: { why: e.reason ?? e.message ?? "unparseable" },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"root:exit"(root) {
|
|
98
|
+
if (data === null) return;
|
|
99
|
+
for (const problem of errorsOf(validate, data))
|
|
100
|
+
context.report({
|
|
101
|
+
node: frontmatter ?? root,
|
|
102
|
+
messageId: "invalid",
|
|
103
|
+
data: { problem },
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default {
|
|
112
|
+
rules: {
|
|
113
|
+
frontmatter: frontmatterRule(
|
|
114
|
+
new URL("./review-frontmatter.schema.json", import.meta.url),
|
|
115
|
+
"a review's frontmatter is a record: its findings, each with a status and, when open, the pipeline cause — validated by JSON Schema",
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "urn:paperlint:review-frontmatter",
|
|
4
|
+
"title": "paperlint review frontmatter",
|
|
5
|
+
"description": "The YAML frontmatter of a review report under reviews/. A review with no `findings` key is not a findings record and is not checked for findings. A project adds its own required fields with the `reviewSchema` setting; its schema is applied together with this one.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"findings": {
|
|
9
|
+
"description": "What the review found, one record per finding.",
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": { "$ref": "#/definitions/finding" }
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"definitions": {
|
|
15
|
+
"finding": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": ["id", "status"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"id": {
|
|
20
|
+
"description": "How the review body refers to this finding.",
|
|
21
|
+
"type": ["integer", "string"]
|
|
22
|
+
},
|
|
23
|
+
"status": { "enum": ["open", "fixed", "wontfix"] },
|
|
24
|
+
"cause": {
|
|
25
|
+
"description": "What in the PIPELINE let this through: a defective skill, a missing skill, a hook, or a rule. Fix that, and the text edit falls out of running the fixed tool.",
|
|
26
|
+
"enum": ["skill-defect", "missing-skill", "hook", "rule"]
|
|
27
|
+
},
|
|
28
|
+
"title": { "type": "string" },
|
|
29
|
+
"note": { "type": "string" }
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"if": { "properties": { "status": { "const": "open" } } },
|
|
33
|
+
"then": { "required": ["cause"] }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|