paperlint 7.2.0 → 8.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/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -6
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +3 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -42
- package/dist/cli.js.map +1 -1
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.js +2 -2
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +22 -91
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.js.map +1 -1
- package/dist/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +0 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-prose.d.ts +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +7 -9
- package/docs/e2e.md +4 -6
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- package/docs/rules/tex/claim-provenance.md +0 -1
- package/docs/rules.md +2 -6
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +2 -2
- package/eslint-rules/paper-typography.test.mjs +17 -85
- package/eslint-rules/paper-typography.ts +22 -98
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +0 -1
- package/hooks/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +0 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +1 -3
- package/presets/realm.jsonc +1 -1
- package/scripts/harness-api.frozen.json +0 -5
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +14 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +108 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
- package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +1 -1
- package/src/build.ts +1 -6
- package/src/cli.harness.mjs +26 -46
- package/src/cli.test.ts +32 -40
- package/src/cli.ts +14 -54
- package/src/folder-venue-rule.test.ts +1 -1
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/lint-fix.test.ts +10 -4
- package/src/new-paper.harness.mjs +9 -28
- package/src/new-paper.test.ts +6 -6
- package/src/new-paper.ts +11 -30
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-prose.test.ts +49 -0
- package/src/paper-prose.ts +67 -0
- package/src/parse-args.test.ts +0 -1
- package/src/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- package/src/types.ts +0 -2
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
package/src/init.ts
CHANGED
|
@@ -63,12 +63,7 @@ import {
|
|
|
63
63
|
type Merge,
|
|
64
64
|
type WireResult,
|
|
65
65
|
} from "./hooks-settings.ts";
|
|
66
|
-
import {
|
|
67
|
-
DEFAULT_FORMAT,
|
|
68
|
-
isFormat,
|
|
69
|
-
nameProblem,
|
|
70
|
-
type PaperFormat,
|
|
71
|
-
} from "./new-paper.ts";
|
|
66
|
+
import { nameProblem } from "./new-paper.ts";
|
|
72
67
|
// The one source for the consumer's config key lives in lib/ (the ESLint rules and the skill scripts
|
|
73
68
|
// import it too). It is imported compiled, from dist/, the one path that resolves the same from src/
|
|
74
69
|
// and from dist/ — see CONTRIBUTING.md.
|
|
@@ -572,17 +567,11 @@ export interface InitOptions {
|
|
|
572
567
|
merge?: Merge | undefined;
|
|
573
568
|
/** `--paper <name>`: create this paper, even without a terminal. */
|
|
574
569
|
paper?: string | null;
|
|
575
|
-
/** `--format tex|md` for that paper. */
|
|
576
|
-
format?: PaperFormat | null;
|
|
577
570
|
/**
|
|
578
571
|
* Creates one paper and lints it — `paperlint new`'s own routine, passed in by the CLI so `init` and
|
|
579
572
|
* `new` cannot drift into two implementations.
|
|
580
573
|
*/
|
|
581
|
-
createPaper?: (
|
|
582
|
-
papersRoot: string,
|
|
583
|
-
name: string,
|
|
584
|
-
format: PaperFormat,
|
|
585
|
-
) => Promise<number>;
|
|
574
|
+
createPaper?: (papersRoot: string, name: string) => Promise<number>;
|
|
586
575
|
/**
|
|
587
576
|
* The venues a paper name may not name (`shippedVenueNames`), so `--paper aisec-2026` is refused
|
|
588
577
|
* before anything is asked. `createPaper` refuses it again whatever this holds.
|
|
@@ -717,7 +706,6 @@ export async function init(
|
|
|
717
706
|
hooks = true,
|
|
718
707
|
merge,
|
|
719
708
|
paper = null,
|
|
720
|
-
format = null,
|
|
721
709
|
createPaper,
|
|
722
710
|
venues = [],
|
|
723
711
|
run = spawnSync,
|
|
@@ -824,18 +812,11 @@ export async function init(
|
|
|
824
812
|
}
|
|
825
813
|
if (wanted !== null && createPaper) {
|
|
826
814
|
const problem = nameProblem(wanted, venues);
|
|
827
|
-
let fmt: PaperFormat = format ?? DEFAULT_FORMAT;
|
|
828
|
-
if (!problem && format === null && interactive) {
|
|
829
|
-
const f = (
|
|
830
|
-
await askOrDefault(ask, ` format: tex / md [${DEFAULT_FORMAT}] `)
|
|
831
|
-
)?.trim();
|
|
832
|
-
if (isFormat(f)) fmt = f;
|
|
833
|
-
}
|
|
834
815
|
if (problem) {
|
|
835
816
|
log(` ✗ no paper created — ${problem}`);
|
|
836
817
|
paperCode = 2;
|
|
837
818
|
} else {
|
|
838
|
-
const code = await createPaper(papersAbs, wanted
|
|
819
|
+
const code = await createPaper(papersAbs, wanted);
|
|
839
820
|
if (code !== 0) {
|
|
840
821
|
log(` ⚠ the new paper's lint exited ${String(code)} — see above`);
|
|
841
822
|
paperCode = code;
|
package/src/lint-fix.test.ts
CHANGED
|
@@ -20,6 +20,10 @@ afterEach(() => {
|
|
|
20
20
|
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
/** A paper.tex whose whole body is `body`. */
|
|
24
|
+
const PAPER = (body: string): string =>
|
|
25
|
+
`\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
|
|
26
|
+
|
|
23
27
|
function project(
|
|
24
28
|
settings: Record<string, unknown>,
|
|
25
29
|
files: Record<string, string> = {},
|
|
@@ -30,7 +34,7 @@ function project(
|
|
|
30
34
|
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
31
35
|
"paperlint.json": JSON.stringify({ papersDir: "papers", ...settings }),
|
|
32
36
|
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
33
|
-
"papers/a/paper.
|
|
37
|
+
"papers/a/paper.tex": PAPER("See §5, §6 and §7; $p < .05$."),
|
|
34
38
|
...files,
|
|
35
39
|
};
|
|
36
40
|
for (const [p, text] of Object.entries(all)) {
|
|
@@ -64,15 +68,17 @@ describe("paperlint lint --fix", () => {
|
|
|
64
68
|
const fixed = await lint(root, ["--fix"]);
|
|
65
69
|
expect(ruleIds(fixed.out)).not.toContain("paper/section-word");
|
|
66
70
|
expect(ruleIds(fixed.out)).not.toContain("paper/leading-zero");
|
|
67
|
-
expect(readFileSync(join(root, "papers/a/paper.
|
|
68
|
-
"
|
|
71
|
+
expect(readFileSync(join(root, "papers/a/paper.tex"), "utf8")).toBe(
|
|
72
|
+
PAPER("See Section 5, Section 6 and Section 7; $p < 0.05$."),
|
|
69
73
|
);
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
it("without --fix nothing is written", async () => {
|
|
73
77
|
const root = project({});
|
|
74
78
|
await lint(root);
|
|
75
|
-
expect(readFileSync(join(root, "papers/a/paper.
|
|
79
|
+
expect(readFileSync(join(root, "papers/a/paper.tex"), "utf8")).toMatch(
|
|
80
|
+
/§5/,
|
|
81
|
+
);
|
|
76
82
|
});
|
|
77
83
|
});
|
|
78
84
|
|
|
@@ -36,7 +36,6 @@ const ROOT = dirname(HERE);
|
|
|
36
36
|
const {
|
|
37
37
|
newPaper,
|
|
38
38
|
nameProblem,
|
|
39
|
-
wantedFiles,
|
|
40
39
|
PACKAGE_TEMPLATES,
|
|
41
40
|
OVERRIDE_DIR,
|
|
42
41
|
STATUS_FILE,
|
|
@@ -63,7 +62,7 @@ async function lint(dir) {
|
|
|
63
62
|
|
|
64
63
|
try {
|
|
65
64
|
// ── the templates are FILES in the package, one per required file ──────────────────────
|
|
66
|
-
for (const f of [STATUS_FILE, SOURCE_FILE
|
|
65
|
+
for (const f of [STATUS_FILE, SOURCE_FILE])
|
|
67
66
|
check(
|
|
68
67
|
`the package ships templates/paper/${f}`,
|
|
69
68
|
existsSync(join(PACKAGE_TEMPLATES, f)),
|
|
@@ -85,13 +84,13 @@ try {
|
|
|
85
84
|
);
|
|
86
85
|
check(
|
|
87
86
|
"a refused name creates nothing",
|
|
88
|
-
newPaper(work, "Bad",
|
|
87
|
+
newPaper(work, "Bad", { venues: [] }).ok === false &&
|
|
89
88
|
!existsSync(join(work, "Bad")),
|
|
90
89
|
);
|
|
91
90
|
|
|
92
91
|
// ── a fresh LaTeX paper ─────────────────────────────────────────────────────────────
|
|
93
92
|
const papers = join(work, "papers");
|
|
94
|
-
const r = newPaper(papers, "demo",
|
|
93
|
+
const r = newPaper(papers, "demo", { venues: [] });
|
|
95
94
|
check(
|
|
96
95
|
"a fresh folder gets the scorecard, a .tex source and paperlint.json, all from the package",
|
|
97
96
|
r.ok &&
|
|
@@ -129,36 +128,18 @@ try {
|
|
|
129
128
|
|
|
130
129
|
// ── never overwrite; on an existing folder only the missing files ─────────────────────
|
|
131
130
|
writeFileSync(join(papers, "demo", STATUS_FILE), "mine\n");
|
|
132
|
-
const again = newPaper(papers, "demo",
|
|
131
|
+
const again = newPaper(papers, "demo", { venues: [] });
|
|
133
132
|
check(
|
|
134
|
-
"🔴 a second run overwrites nothing
|
|
133
|
+
"🔴 a second run overwrites nothing",
|
|
135
134
|
again.ok &&
|
|
136
135
|
!again.fresh &&
|
|
137
136
|
read(papers, "demo", STATUS_FILE) === "mine\n" &&
|
|
138
|
-
!existsSync(join(papers, "demo", "paper.md")) &&
|
|
139
137
|
again.files.every((f) => f.status === "kept"),
|
|
140
138
|
);
|
|
141
|
-
mkdirSync(join(papers, "old"), { recursive: true });
|
|
142
|
-
writeFileSync(join(papers, "old", "paper.md"), "# Old\n");
|
|
143
|
-
const migrated = newPaper(papers, "old", "tex", { venues: [] });
|
|
144
|
-
check(
|
|
145
|
-
"an old folder with only paper.md gets ONLY the scorecard — that is the migration",
|
|
146
|
-
migrated.ok &&
|
|
147
|
-
existsSync(join(papers, "old", STATUS_FILE)) &&
|
|
148
|
-
!existsSync(join(papers, "old", "paper.tex")) &&
|
|
149
|
-
read(papers, "old", "paper.md") === "# Old\n",
|
|
150
|
-
);
|
|
151
|
-
check(
|
|
152
|
-
"wantedFiles asks for a source only when neither format is there",
|
|
153
|
-
wantedFiles(join(papers, "old"), "tex").join() ===
|
|
154
|
-
`${STATUS_FILE},paperlint.json` &&
|
|
155
|
-
wantedFiles(join(papers, "nowhere"), "md").join() ===
|
|
156
|
-
`${STATUS_FILE},paper.md,paperlint.json`,
|
|
157
|
-
);
|
|
158
139
|
writeFileSync(join(papers, "afile"), "x");
|
|
159
140
|
check(
|
|
160
141
|
"a name taken by a FILE is refused, not written through",
|
|
161
|
-
newPaper(papers, "afile",
|
|
142
|
+
newPaper(papers, "afile", { venues: [] }).ok === false,
|
|
162
143
|
);
|
|
163
144
|
|
|
164
145
|
// ── the project's template wins, file by file ─────────────────────────────────────────
|
|
@@ -167,7 +148,7 @@ try {
|
|
|
167
148
|
join(papers, OVERRIDE_DIR, STATUS_FILE),
|
|
168
149
|
"---\n---\n# House scorecard for {{name}}\n",
|
|
169
150
|
);
|
|
170
|
-
const own = newPaper(papers, "house",
|
|
151
|
+
const own = newPaper(papers, "house", { venues: [] });
|
|
171
152
|
// Guards: the override slot — `fromTemplate` in new-paper.ts reads `<papers>/.template/<file>`
|
|
172
153
|
// before the package's copy. Replace that lookup with the package's alone and this goes red.
|
|
173
154
|
check(
|
|
@@ -176,11 +157,11 @@ try {
|
|
|
176
157
|
read(papers, "house", STATUS_FILE) ===
|
|
177
158
|
"---\n---\n# House scorecard for house\n" &&
|
|
178
159
|
own.files.find((f) => f.file === STATUS_FILE)?.from === "project" &&
|
|
179
|
-
own.files.find((f) => f.file ===
|
|
160
|
+
own.files.find((f) => f.file === SOURCE_FILE)?.from === "package",
|
|
180
161
|
);
|
|
181
162
|
|
|
182
163
|
// ── a missing template is a broken install, not an empty file ─────────────────────────
|
|
183
|
-
const broken = newPaper(join(work, "b"), "x",
|
|
164
|
+
const broken = newPaper(join(work, "b"), "x", {
|
|
184
165
|
venues: [],
|
|
185
166
|
packageTemplates: join(work, "no-such-dir"),
|
|
186
167
|
});
|
package/src/new-paper.test.ts
CHANGED
|
@@ -41,7 +41,7 @@ const settingsOf = (dir: string) =>
|
|
|
41
41
|
describe("paperlint new — paperlint.json", () => {
|
|
42
42
|
it("is always written, from the package template: no venue chosen yet, and valid", () => {
|
|
43
43
|
const papers = join(tmp(), "papers");
|
|
44
|
-
const r = newPaper(papers, "demo",
|
|
44
|
+
const r = newPaper(papers, "demo", { venues: [] });
|
|
45
45
|
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")).toEqual({
|
|
46
46
|
file: "paperlint.json",
|
|
47
47
|
status: "created",
|
|
@@ -72,7 +72,7 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
72
72
|
join(papers, OVERRIDE_DIR, "paperlint.json"),
|
|
73
73
|
'{ "extends": "paperlint:aisec", "kind": "research" }\n',
|
|
74
74
|
);
|
|
75
|
-
const r = newPaper(papers, "house",
|
|
75
|
+
const r = newPaper(papers, "house", { venues: [] });
|
|
76
76
|
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")?.from).toBe(
|
|
77
77
|
"project",
|
|
78
78
|
);
|
|
@@ -89,7 +89,7 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
89
89
|
join(papers, "p", "paperlint.json"),
|
|
90
90
|
'{"extends":"paperlint:aisec"}',
|
|
91
91
|
);
|
|
92
|
-
const r = newPaper(papers, "p",
|
|
92
|
+
const r = newPaper(papers, "p", { venues: [] });
|
|
93
93
|
expect(
|
|
94
94
|
r.ok && r.files.find((f) => f.file === "paperlint.json")?.status,
|
|
95
95
|
).toBe("kept");
|
|
@@ -106,7 +106,7 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
|
106
106
|
join(root, "package.json"),
|
|
107
107
|
JSON.stringify({ name: "c", private: true }),
|
|
108
108
|
);
|
|
109
|
-
newPaper(join(root, "papers"), "demo",
|
|
109
|
+
newPaper(join(root, "papers"), "demo", { venues: [] });
|
|
110
110
|
if (extendsValue !== null)
|
|
111
111
|
writeFileSync(
|
|
112
112
|
join(root, "papers", "demo", "paperlint.json"),
|
|
@@ -488,7 +488,7 @@ describe("a project's own template", () => {
|
|
|
488
488
|
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
489
489
|
const src = join(papers, OVERRIDE_DIR, "paperlint.json");
|
|
490
490
|
writeFileSync(src, '{\n // our house venue\n "extends": null\n}\n');
|
|
491
|
-
const r = newPaper(papers, "p",
|
|
491
|
+
const r = newPaper(papers, "p", {
|
|
492
492
|
venues: [],
|
|
493
493
|
venue: { extends: "paperlint:aisec", kind: null },
|
|
494
494
|
});
|
|
@@ -504,7 +504,7 @@ describe("a project's own template", () => {
|
|
|
504
504
|
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
505
505
|
writeFileSync(join(papers, OVERRIDE_DIR, "paperlint.json"), "{}\n");
|
|
506
506
|
const lines = reportNewPaper(
|
|
507
|
-
newPaper(papers, "p",
|
|
507
|
+
newPaper(papers, "p", { venues: [] }),
|
|
508
508
|
(p) => p,
|
|
509
509
|
);
|
|
510
510
|
expect(lines).toContain(
|
package/src/new-paper.ts
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
* Every rule below is taken from a tool that already solved it (docs/prior-art/paper-folder-scaffolding.md):
|
|
9
9
|
* - the template is a FILE with a lookup order — the project's `<papers>/.template/<file>` first,
|
|
10
10
|
* the package's `templates/paper/<file>` second (Hugo's archetypes);
|
|
11
|
-
* - it NEVER overwrites: on an existing folder it adds only the missing required files
|
|
12
|
-
* also the migration for an old folder that has only `paper.md` (`cargo init`);
|
|
11
|
+
* - it NEVER overwrites: on an existing folder it adds only the missing required files (`cargo init`);
|
|
13
12
|
* - the name is validated, because it becomes a path and, through the hooks, a shell argument.
|
|
14
13
|
*
|
|
15
14
|
* The only substitution is `{{name}}` → the folder name. Nothing else in a template is touched.
|
|
@@ -33,10 +32,6 @@ import {
|
|
|
33
32
|
type NamedVenue,
|
|
34
33
|
} from "./domain/venue-name.ts";
|
|
35
34
|
|
|
36
|
-
export type PaperFormat = "tex" | "md";
|
|
37
|
-
export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
|
|
38
|
-
export const DEFAULT_FORMAT: PaperFormat = "tex";
|
|
39
|
-
|
|
40
35
|
/** The package's default templates. Read from disk, so they ship in the tarball (`files`). */
|
|
41
36
|
export const PACKAGE_TEMPLATES = fileURLToPath(
|
|
42
37
|
new URL("../templates/paper/", import.meta.url),
|
|
@@ -50,17 +45,12 @@ export const PACKAGE_TEMPLATES = fileURLToPath(
|
|
|
50
45
|
export const OVERRIDE_DIR = ".template";
|
|
51
46
|
|
|
52
47
|
export const STATUS_FILE = "PIPELINE-STATUS.md";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
md: "paper.md",
|
|
56
|
-
};
|
|
48
|
+
/** A paper's source: LaTeX, one file at the paper's root. */
|
|
49
|
+
export const SOURCE_FILE = "paper.tex";
|
|
57
50
|
|
|
58
51
|
/** The flag that lets `paperlint new` create a folder whose name names a venue. */
|
|
59
52
|
export const ALLOW_VENUE_NAME = "--allow-venue-name";
|
|
60
53
|
|
|
61
|
-
export const isFormat = (v: unknown): v is PaperFormat =>
|
|
62
|
-
FORMATS.some((f) => f === v);
|
|
63
|
-
|
|
64
54
|
/**
|
|
65
55
|
* Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
|
|
66
56
|
* of it, because discovery skips dot-directories — a paper named `.x` would be created and then
|
|
@@ -109,20 +99,12 @@ export type NewPaperResult =
|
|
|
109
99
|
}
|
|
110
100
|
| { readonly ok: false; readonly reason: string };
|
|
111
101
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function scorecardAndSource(dir: string, format: PaperFormat): string[] {
|
|
121
|
-
const hasSource = Object.values(SOURCE_FILE).some((f) =>
|
|
122
|
-
existsSync(join(dir, f)),
|
|
123
|
-
);
|
|
124
|
-
return hasSource ? [STATUS_FILE] : [STATUS_FILE, SOURCE_FILE[format]];
|
|
125
|
-
}
|
|
102
|
+
/** The files a paper folder must end up with: the scorecard, the source and `paperlint.json`. */
|
|
103
|
+
export const WANTED_FILES: readonly string[] = [
|
|
104
|
+
STATUS_FILE,
|
|
105
|
+
SOURCE_FILE,
|
|
106
|
+
CONFIG_FILE,
|
|
107
|
+
];
|
|
126
108
|
|
|
127
109
|
/** The venue `paperlint new --venue` chose: what goes into the new paper's `paperlint.json`. */
|
|
128
110
|
export interface VenueSetting {
|
|
@@ -195,7 +177,7 @@ function fromTemplate(
|
|
|
195
177
|
return { ok: false, reason: `no template for ${file}: ${src} is missing` };
|
|
196
178
|
const text = readFileSync(src, "utf8").split("{{name}}").join(name);
|
|
197
179
|
const setClass = venue?.setClass ?? null;
|
|
198
|
-
if (file === SOURCE_FILE
|
|
180
|
+
if (file === SOURCE_FILE && setClass !== null)
|
|
199
181
|
return { ok: true, value: { file, text: setClass(text), from } };
|
|
200
182
|
if (file !== CONFIG_FILE || venue === null)
|
|
201
183
|
return { ok: true, value: { file, text, from } };
|
|
@@ -217,7 +199,6 @@ export interface NewPaperOptions {
|
|
|
217
199
|
export function newPaper(
|
|
218
200
|
papersRoot: string,
|
|
219
201
|
name: string,
|
|
220
|
-
format: PaperFormat,
|
|
221
202
|
opts: NewPaperOptions,
|
|
222
203
|
): NewPaperResult {
|
|
223
204
|
const { packageTemplates = PACKAGE_TEMPLATES, venue = null } = opts;
|
|
@@ -230,7 +211,7 @@ export function newPaper(
|
|
|
230
211
|
|
|
231
212
|
const files: FileOutcome[] = [];
|
|
232
213
|
const toWrite: Planned[] = [];
|
|
233
|
-
for (const file of
|
|
214
|
+
for (const file of WANTED_FILES) {
|
|
234
215
|
if (existsSync(join(dir, file))) {
|
|
235
216
|
files.push({ file, status: "kept" });
|
|
236
217
|
continue;
|
package/src/paper-files.test.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { tmpdir } from "node:os";
|
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { afterEach, describe, expect, it } from "vitest";
|
|
22
22
|
import { buildConfig, eslintConfig, run } from "./cli.ts";
|
|
23
|
+
import { texLanguage } from "#eslint-rules/latex-language";
|
|
23
24
|
import {
|
|
24
25
|
narrowToOwners,
|
|
25
26
|
ownedPatterns,
|
|
@@ -247,7 +248,7 @@ describe("`rules` blocks — where the rule lands", () => {
|
|
|
247
248
|
},
|
|
248
249
|
],
|
|
249
250
|
},
|
|
250
|
-
|
|
251
|
+
texLanguage,
|
|
251
252
|
),
|
|
252
253
|
),
|
|
253
254
|
});
|
|
@@ -257,9 +258,9 @@ describe("`rules` blocks — where the rule lands", () => {
|
|
|
257
258
|
const status = await on("papers/a/PIPELINE-STATUS.md");
|
|
258
259
|
expect(status["paper/source"]).toEqual([1]);
|
|
259
260
|
expect(status["paper/section-word"]).toBeUndefined();
|
|
260
|
-
const
|
|
261
|
-
expect(
|
|
262
|
-
expect(
|
|
261
|
+
const source = await on("papers/a/paper.tex");
|
|
262
|
+
expect(source["paper/section-word"]).toEqual([0]);
|
|
263
|
+
expect(source["paper/source"]).toBeUndefined();
|
|
263
264
|
});
|
|
264
265
|
});
|
|
265
266
|
|
package/src/paper-files.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THE FILES PAPERLINT OWNS — read off its own config, and made the only thing ESLint may reach.
|
|
3
3
|
*
|
|
4
|
-
* paperlint's blocks name the files they lint (`PIPELINE-STATUS.md`, `paper.
|
|
5
|
-
*
|
|
4
|
+
* paperlint's blocks name the files they lint (`PIPELINE-STATUS.md`, `paper.tex`, `reviews/*.md`,
|
|
5
|
+
* …). That list is the whole scope of `paperlint lint`, and this module turns it
|
|
6
6
|
* into two things ESLint understands:
|
|
7
7
|
*
|
|
8
8
|
* 1. **A global ignore of everything else** (`scopeToOwned`). Handing ESLint a directory is not
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 2. **Each rule scoped to the files it was written for** (`ruleOwners` + `narrowToOwners`).
|
|
18
18
|
* One plugin NAME is bound to different plugin objects in different blocks: `paper` holds
|
|
19
19
|
* `stages`/`source`/`author-list` beside `PIPELINE-STATUS.md` and `typography` beside
|
|
20
|
-
* `paper.
|
|
20
|
+
* `paper.tex`. So a consumer block that named `paper/source`
|
|
21
21
|
* for every file under the papers directory reached `paper.tex`, where `paper` has no `source`
|
|
22
22
|
* — and ESLint threw `Could not find "source" in plugin "paper"` from inside `lintFiles`,
|
|
23
23
|
* after paperlint's own validation had (correctly) accepted the id. A block is therefore split
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* - a global-ignore pattern that matches a DIRECTORY hides everything inside it, so directories
|
|
37
37
|
* must be un-ignored (`!` + a pattern ending in a slash) before owned files can be;
|
|
38
38
|
* - that un-ignores `node_modules/` and `.git/` too — ESLint's own defaults — so they are
|
|
39
|
-
* re-ignored after, or a `paper.
|
|
39
|
+
* re-ignored after, or a `paper.tex` inside a dependency would be linted;
|
|
40
40
|
* - later global-ignore patterns win, across blocks, in config order;
|
|
41
41
|
* - `files: [["a", "b"]]` matches a file only when BOTH patterns match.
|
|
42
42
|
*/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { latexReader } from "./adapters/latex/index.ts";
|
|
3
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
4
|
+
import { paperProse } from "./paper-prose.ts";
|
|
5
|
+
|
|
6
|
+
const SRC = [
|
|
7
|
+
"\\documentclass{article}",
|
|
8
|
+
"\\usepackage{amsmath} % a preamble word count of its own",
|
|
9
|
+
"\\title{A Title}",
|
|
10
|
+
"\\begin{document}",
|
|
11
|
+
"\\maketitle",
|
|
12
|
+
"\\section{Introduction}",
|
|
13
|
+
"We measure it~\\cite{a}. % a comment",
|
|
14
|
+
"",
|
|
15
|
+
"A second paragraph \\input{body}",
|
|
16
|
+
"\\begin{filecontents*}{refs.bib}",
|
|
17
|
+
"@misc{a, author = {Ada Lovelace}, title = {Never Prose}}",
|
|
18
|
+
"\\end{filecontents*}",
|
|
19
|
+
"\\end{document}",
|
|
20
|
+
].join("\n");
|
|
21
|
+
|
|
22
|
+
it("the body's prose, a blank line between passages, includes spliced; the preamble, comments, the inline .bib and the title stay out", () => {
|
|
23
|
+
const files = memoryFiles({
|
|
24
|
+
"/p/paper.tex": SRC,
|
|
25
|
+
"/p/body.tex": "\\subsection{Method}\nends here.",
|
|
26
|
+
});
|
|
27
|
+
expect(
|
|
28
|
+
paperProse("/p/paper.tex", SRC, { files, latex: latexReader }),
|
|
29
|
+
).toEqual({
|
|
30
|
+
// A citation stands as `[1]`, where it is set; a heading — here an included one — ends a passage.
|
|
31
|
+
body: "We measure it [1].\n\nA second paragraph\n\nends here.",
|
|
32
|
+
headings: ["Introduction", "Method"],
|
|
33
|
+
files: [
|
|
34
|
+
{ file: "paper.tex", text: SRC },
|
|
35
|
+
{ file: "body.tex", text: "\\subsection{Method}\nends here." },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("a citation stands as `[1]` and a cross-reference as `1`, so the sentence still ends on them", () => {
|
|
41
|
+
const src =
|
|
42
|
+
"\\documentclass{article}\n\\begin{document}\nIt holds~\\cite{a}. It is shown (Section~\\ref{s}).\n\\end{document}\n";
|
|
43
|
+
expect(
|
|
44
|
+
paperProse("/p/paper.tex", src, {
|
|
45
|
+
files: memoryFiles({ "/p/paper.tex": src }),
|
|
46
|
+
latex: latexReader,
|
|
47
|
+
}).body,
|
|
48
|
+
).toBe("It holds [1]. It is shown (Section 1).");
|
|
49
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PROSE OF A paper.tex AS PLAIN TEXT — what `grade-paper-writing/prose-lint.mjs` measures.
|
|
3
|
+
*
|
|
4
|
+
* prose-lint counts sentences and words, and every metric it prints is a fraction of the word
|
|
5
|
+
* count. Read as raw text, a `.tex` puts the preamble, the `%` comments and an inline `filecontents`
|
|
6
|
+
* bibliography into that count (8127 "words" against about 4636 real ones on one paper), so every
|
|
7
|
+
* rate comes out roughly halved. This reads the paper the way the `tex/register` rule does: the
|
|
8
|
+
* includes spliced in (`readPaper`), then the body's prose as passages (`bodyProse` — the abstract
|
|
9
|
+
* to the back matter, without headings, captions, floats or code), each passage's sentences
|
|
10
|
+
* (`bodySentences`). A blank line separates two passages, so no sentence runs across them.
|
|
11
|
+
*
|
|
12
|
+
* A citation stands as `[1]` and a cross-reference as `1`, where they are set — not left out:
|
|
13
|
+
* prose-lint judges a sentence that ENDS on one (a wasted stress position) and counts references
|
|
14
|
+
* to other parts of the paper (`(Section 1)`), and both are invisible in a sentence the marks
|
|
15
|
+
* were cut out of. Math is left out; prose-lint counts no formula.
|
|
16
|
+
*/
|
|
17
|
+
import { bodySentences } from "./domain/register.ts";
|
|
18
|
+
import { CITATION, MATH, REFERENCE } from "./domain/sentences.ts";
|
|
19
|
+
import { runText } from "./domain/tex-document.ts";
|
|
20
|
+
import { readPaper, type PaperDeps } from "./tex-paper.ts";
|
|
21
|
+
|
|
22
|
+
/** A sentence's text with each mark as a reader would see it set, and math left out. */
|
|
23
|
+
const rendered = (raw: string): string =>
|
|
24
|
+
raw
|
|
25
|
+
.replaceAll(CITATION, "[1]")
|
|
26
|
+
.replaceAll(REFERENCE, "1")
|
|
27
|
+
.replaceAll(MATH, "")
|
|
28
|
+
.replace(/\s+/gu, " ")
|
|
29
|
+
.trim();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A paper's body prose, one passage per paragraph, its section titles in document order, and the
|
|
33
|
+
* text of each file it is made of — `paper.tex` and every file it includes, once, named as the
|
|
34
|
+
* include names it — for what the body leaves out, such as captions.
|
|
35
|
+
*/
|
|
36
|
+
export interface PaperProse {
|
|
37
|
+
readonly body: string;
|
|
38
|
+
readonly headings: readonly string[];
|
|
39
|
+
readonly files: readonly { readonly file: string; readonly text: string }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The prose and the headings of the paper whose main file is `filename` with the text `src`. */
|
|
43
|
+
export function paperProse(
|
|
44
|
+
filename: string,
|
|
45
|
+
src: string,
|
|
46
|
+
deps: PaperDeps,
|
|
47
|
+
): PaperProse {
|
|
48
|
+
const { text, segments } = readPaper(filename, src, deps);
|
|
49
|
+
const passages = deps.latex
|
|
50
|
+
.bodyProse(text)
|
|
51
|
+
.map((p) =>
|
|
52
|
+
bodySentences([p])
|
|
53
|
+
.map((s) => rendered(s.raw))
|
|
54
|
+
.join(" "),
|
|
55
|
+
)
|
|
56
|
+
.filter((p) => p !== "");
|
|
57
|
+
return {
|
|
58
|
+
body: passages.join("\n\n"),
|
|
59
|
+
headings: deps.latex
|
|
60
|
+
.headings(text)
|
|
61
|
+
.filter((h) => h.command.kind === "heading" && h.argument === "title")
|
|
62
|
+
.map((h) => runText(h.title).replace(/\s+/g, " ").trim()),
|
|
63
|
+
files: [...new Map(segments.map((s) => [s.file, s.source])).entries()].map(
|
|
64
|
+
([file, text]) => ({ file, text }),
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
}
|
package/src/parse-args.test.ts
CHANGED
|
@@ -22,7 +22,6 @@ describe("parseArgs — `--flag=value` is the same as `--flag value`", () => {
|
|
|
22
22
|
["--config=o.json", "config", "o.json"],
|
|
23
23
|
["--venue=agenticdev", "venue", "agenticdev"],
|
|
24
24
|
["--kind=short", "kind", "short"],
|
|
25
|
-
["--format=md", "format", "md"],
|
|
26
25
|
["--paper=p", "paper", "p"],
|
|
27
26
|
] as const)("%s", (flag, key, value) => {
|
|
28
27
|
const a = parseArgs(["new", "x", flag]);
|
package/src/structure.harness.ts
CHANGED
|
@@ -47,7 +47,6 @@ const paper = (name: string, files: readonly string[]) => {
|
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
49
|
paper("complete", ["PIPELINE-STATUS.md", "paper.tex", "refs.bib"]);
|
|
50
|
-
paper("complete-md", ["PIPELINE-STATUS.md", "paper.md"]);
|
|
51
50
|
// the marker is present (paper.tex), the scorecard is not — the directory is linted by ZERO
|
|
52
51
|
// rules and reports clean
|
|
53
52
|
paper("no-scorecard", ["paper.tex", "refs.bib"]);
|
|
@@ -65,10 +64,6 @@ try {
|
|
|
65
64
|
"a complete paper directory — NOT A SINGLE finding",
|
|
66
65
|
at("complete").length === 0,
|
|
67
66
|
);
|
|
68
|
-
check(
|
|
69
|
-
"and `paper.md` counts on equal footing with `paper.tex` — the corpus holds both forms",
|
|
70
|
-
at("complete-md").length === 0,
|
|
71
|
-
);
|
|
72
67
|
check(
|
|
73
68
|
"a missing scorecard — a finding",
|
|
74
69
|
at("no-scorecard").length === 1 &&
|
|
@@ -86,10 +81,11 @@ try {
|
|
|
86
81
|
/no-scorecard/.test(at("no-scorecard")[0]?.message ?? ""),
|
|
87
82
|
);
|
|
88
83
|
check(
|
|
89
|
-
"a directory with no source — a finding
|
|
84
|
+
"a directory with no source — a finding naming `paper.tex`",
|
|
90
85
|
at("no-source").length === 1 &&
|
|
91
|
-
/`paper\.tex
|
|
92
|
-
|
|
86
|
+
/`paper\.tex` — a paper directory with no source/.test(
|
|
87
|
+
at("no-source")[0]?.message ?? "",
|
|
88
|
+
),
|
|
93
89
|
);
|
|
94
90
|
|
|
95
91
|
// 🔴 THE PAIRED HALF: detection is GENEROUS. Without this the check would scream about every
|
|
@@ -124,9 +120,7 @@ try {
|
|
|
124
120
|
{
|
|
125
121
|
cwd: root,
|
|
126
122
|
},
|
|
127
|
-
).some(
|
|
128
|
-
(x) => x.file.endsWith("complete-md") && /refs\.bib/.test(x.message),
|
|
129
|
-
),
|
|
123
|
+
).some((x) => x.file.endsWith("no-source") && /refs\.bib/.test(x.message)),
|
|
130
124
|
);
|
|
131
125
|
check(
|
|
132
126
|
"a nonexistent root does not crash it — that's the empty-set guard talking",
|
package/src/structure.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { readdirSync, existsSync } from "node:fs";
|
|
|
26
26
|
import { join, relative, basename, isAbsolute, sep } from "node:path";
|
|
27
27
|
import type { StructureConfig, StructureFinding } from "./types.ts";
|
|
28
28
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
29
|
+
import { SOURCE_FILE } from "./new-paper.ts";
|
|
29
30
|
|
|
30
31
|
/** The requirements after the consumer's config is laid over the defaults. */
|
|
31
32
|
type Rules = Required<StructureConfig>;
|
|
@@ -38,9 +39,9 @@ type Rules = Required<StructureConfig>;
|
|
|
38
39
|
* byte-compared by the `paper/stages` rule anyway.
|
|
39
40
|
*/
|
|
40
41
|
export const STRUCTURE_DEFAULTS = {
|
|
41
|
-
markers: ["PIPELINE-STATUS.md",
|
|
42
|
+
markers: ["PIPELINE-STATUS.md", SOURCE_FILE, CONFIG_FILE],
|
|
42
43
|
require: ["PIPELINE-STATUS.md"],
|
|
43
|
-
requireOneOf: [[
|
|
44
|
+
requireOneOf: [[SOURCE_FILE]],
|
|
44
45
|
ignore: [],
|
|
45
46
|
};
|
|
46
47
|
|
|
@@ -118,7 +119,7 @@ export function checkStructure(
|
|
|
118
119
|
* The message NAMES THE CONSEQUENCE rather than restating the condition. "missing
|
|
119
120
|
* PIPELINE-STATUS.md" without the second half reads as nitpicking about formatting; with it you
|
|
120
121
|
* can see which checks the directory silently loses — the rules that read the scorecard; the rules
|
|
121
|
-
* over `paper.
|
|
122
|
+
* over `paper.tex` still run.
|
|
122
123
|
*/
|
|
123
124
|
function whyMissingMatters(file: string, dirName: string): string {
|
|
124
125
|
if (file === "PIPELINE-STATUS.md")
|
package/src/types.ts
CHANGED
|
@@ -62,8 +62,6 @@ export interface Args {
|
|
|
62
62
|
noHooks: boolean;
|
|
63
63
|
/** `--paper <name>`: `init` creates this paper. */
|
|
64
64
|
paper: string | null;
|
|
65
|
-
/** `--format tex|md` for `new` and `init --paper`. Validated by the command, not here. */
|
|
66
|
-
format: string | null;
|
|
67
65
|
/** `--venue <preset>` for `new`: a shipped preset name, or a path to the project's own. */
|
|
68
66
|
venue: string | null;
|
|
69
67
|
/** `--kind <kind>` for `new`: one of the venue preset's kinds. */
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$comment": [
|
|
3
|
-
"What the rules say about this article TODAY, as data rather than as a sentence in a docblock.",
|
|
4
|
-
"It is not a target and not a clean bill of health — the fixture's README says findings here",
|
|
5
|
-
"are measurements. Its job is the SILENCE half: a rule that starts reporting something new on",
|
|
6
|
-
"225 lines of real prose has almost certainly gained a false positive, and the harness beside",
|
|
7
|
-
"this file names the rule. That is not expressible on an eleven-line stub, where every line was",
|
|
8
|
-
"written by someone who already knew which rule would read it.",
|
|
9
|
-
"",
|
|
10
|
-
"Recorded 2026-09-19, re-recorded 2026-09-26 for 3.0.0. Two of the three rules are about the",
|
|
11
|
-
"scorecard being deliberately thin (no pdf, no frozen source) rather than about the prose.",
|
|
12
|
-
"paper/leading-zero reports each of the 18 p-values of paperlint#44 where it is (one finding per",
|
|
13
|
-
"occurrence since 3.0.0; before, paper/typography reported them as one count). They are real",
|
|
14
|
-
"findings against IEEE / ISO 80000-1 style, in a blog post written for a general audience.",
|
|
15
|
-
"The count is expected to stay. A drop is never a failure here, only growth is."
|
|
16
|
-
],
|
|
17
|
-
"findings": {
|
|
18
|
-
"paper/stages": 2,
|
|
19
|
-
"paper/source": 1,
|
|
20
|
-
"paper/leading-zero": 18
|
|
21
|
-
}
|
|
22
|
-
}
|