paperlint 4.2.0 β 6.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 +15 -2
- package/CONTRIBUTING.md +3 -0
- package/README.md +23 -12
- package/action.harness.mjs +54 -36
- package/action.yml +12 -27
- package/bin/paperlint.mjs +5 -1
- package/dist/adapters/latex/includes.d.ts +5 -1
- package/dist/adapters/latex/includes.d.ts.map +1 -1
- package/dist/adapters/latex/includes.js +8 -2
- package/dist/adapters/latex/includes.js.map +1 -1
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/layout.d.ts +27 -0
- package/dist/adapters/latex/layout.d.ts.map +1 -0
- package/dist/adapters/latex/layout.js +62 -0
- package/dist/adapters/latex/layout.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +11 -1
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +5 -1
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -3
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +2 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +92 -42
- package/dist/cli.js.map +1 -1
- package/dist/domain/anonymity.d.ts +68 -0
- package/dist/domain/anonymity.d.ts.map +1 -0
- package/dist/domain/anonymity.js +94 -0
- package/dist/domain/anonymity.js.map +1 -0
- package/dist/domain/body-pages.d.ts +69 -0
- package/dist/domain/body-pages.d.ts.map +1 -0
- package/dist/domain/body-pages.js +141 -0
- package/dist/domain/body-pages.js.map +1 -0
- package/dist/domain/paper-source.js +2 -2
- package/dist/domain/tex-document.d.ts +9 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.d.ts +5 -1
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +6 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.d.ts +20 -0
- package/dist/eslint-rules/paper-context.d.ts.map +1 -0
- package/dist/eslint-rules/paper-context.js +44 -0
- package/dist/eslint-rules/paper-context.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +1 -1
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +32 -18
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +7 -1
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/facts-file.d.ts +28 -2
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +54 -9
- package/dist/facts-file.js.map +1 -1
- package/dist/init.d.ts +4 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +24 -29
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/package-dirs.d.ts +2 -0
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +8 -0
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-files.d.ts +2 -2
- package/dist/paper-includes.d.ts +17 -0
- package/dist/paper-includes.d.ts.map +1 -0
- package/dist/paper-includes.js +51 -0
- package/dist/paper-includes.js.map +1 -0
- package/dist/paper-settings.d.ts +11 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +38 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-content.d.ts +40 -0
- package/dist/pdf-content.d.ts.map +1 -0
- package/dist/pdf-content.js +97 -0
- package/dist/pdf-content.js.map +1 -0
- package/dist/pdf-facts.d.ts +30 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +37 -1
- package/dist/pdf-facts.js.map +1 -1
- package/dist/ports/latex.d.ts +3 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -0
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.d.ts +1 -0
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-paper.d.ts +17 -3
- package/dist/tex-paper.d.ts.map +1 -1
- package/dist/tex-paper.js +36 -38
- package/dist/tex-paper.js.map +1 -1
- package/dist/tex-requirements.d.ts +4 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +3 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +7 -1
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +40 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +38 -8
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +179 -41
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +23 -20
- package/docs/e2e.md +32 -11
- package/docs/install.md +8 -8
- package/docs/prior-art/paper-folder-scaffolding.md +4 -7
- package/docs/rules/anonymity/identity.md +111 -0
- package/docs/rules/format/layout-override.md +61 -0
- package/docs/rules/format/page-limit.md +108 -0
- package/docs/rules/pdf/body-size.md +87 -0
- package/docs/rules.md +55 -46
- package/eslint-rules/latex-language.ts +10 -4
- package/eslint-rules/paper-context.test.ts +47 -0
- package/eslint-rules/paper-context.ts +57 -0
- package/eslint-rules/paper-typography.ts +67 -23
- package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
- package/eslint-rules/pdf-last-page-balance.ts +2 -2
- package/eslint-rules/tex-build.ts +8 -1
- package/fixtures/build-e2e/aidc/expect.json +1 -1
- package/fixtures/build-e2e/aidc/paperlint.json +1 -1
- package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-ack/expect.json +19 -0
- package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
- package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-author/expect.json +19 -0
- package/fixtures/build-e2e/anon-author/paper.tex +29 -0
- package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-bib/expect.json +20 -0
- package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
- package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-href/expect.json +19 -0
- package/fixtures/build-e2e/anon-href/paper.tex +30 -0
- package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-macro/expect.json +19 -0
- package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
- package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
- package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
- package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
- package/fixtures/build-e2e/unbalanced/expect.json +2 -2
- package/fixtures/real-markdown-paper/baseline.json +1 -2
- package/lib/paper-config.ts +3 -1
- package/package.json +1 -1
- package/presets/acm-sigconf.jsonc +3 -1
- package/presets/aidc.jsonc +16 -17
- package/presets/ieee-conference.jsonc +8 -0
- package/presets/venue-profile.schema.json +9 -0
- package/scripts/check.ts +2 -2
- package/scripts/eslint-report-guard.mjs +5 -2
- package/scripts/harness-api.frozen.json +0 -1
- package/skills/paper-pipeline/references/pipeline-status-template.md +2 -16
- package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
- package/src/adapters/latex/includes.ts +10 -2
- package/src/adapters/latex/index.test.ts +8 -0
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/layout.ts +106 -0
- package/src/adapters/latex/nodes.ts +11 -3
- package/src/adapters/latex/parse.ts +5 -1
- package/src/build.harness.mjs +6 -4
- package/src/build.test.ts +23 -1
- package/src/build.ts +7 -3
- package/src/cli.harness.mjs +22 -54
- package/src/cli.test.ts +15 -18
- package/src/cli.ts +114 -46
- package/src/domain/anonymity.test.ts +163 -0
- package/src/domain/anonymity.ts +129 -0
- package/src/domain/body-pages.test.ts +300 -0
- package/src/domain/body-pages.ts +223 -0
- package/src/domain/paper-source.ts +2 -2
- package/src/domain/tex-document.ts +10 -0
- package/src/facts-file.test.ts +100 -4
- package/src/facts-file.ts +91 -9
- package/src/init.test.ts +33 -13
- package/src/init.ts +25 -48
- package/src/linted-set.test.ts +11 -1
- package/src/new-paper.harness.mjs +1 -5
- package/src/new-paper.test.ts +7 -1
- package/src/package-dirs.ts +9 -0
- package/src/paper-files.ts +2 -2
- package/src/paper-includes.test.ts +161 -0
- package/src/paper-includes.ts +77 -0
- package/src/paper-settings.test.ts +63 -6
- package/src/paper-settings.ts +53 -5
- package/src/pdf-content.test.ts +71 -0
- package/src/pdf-content.ts +125 -0
- package/src/pdf-facts.test.ts +110 -2
- package/src/pdf-facts.ts +106 -8
- package/src/ports/latex.ts +3 -0
- package/src/presets.ts +8 -8
- package/src/rule-cases.test.ts +29 -33
- package/src/rule-docs.test.ts +0 -3
- package/src/rules-config.ts +1 -0
- package/src/structure.harness.ts +1 -3
- package/src/structure.ts +1 -1
- package/src/tex-paper.test.ts +78 -61
- package/src/tex-paper.ts +57 -48
- package/src/tex-requirements.ts +9 -0
- package/src/tex-venue-rules.test.ts +111 -6
- package/src/tex-venue-rules.ts +57 -2
- package/src/types.ts +2 -0
- package/src/venue-rules.test.ts +351 -39
- package/src/venue-rules.ts +247 -47
- package/templates/paper/PIPELINE-STATUS.md +0 -4
- package/dist/action-ref.d.ts +0 -12
- package/dist/action-ref.d.ts.map +0 -1
- package/dist/action-ref.js +0 -16
- package/dist/action-ref.js.map +0 -1
- package/dist/eslint-rules/paper-research-question.d.ts +0 -31
- package/dist/eslint-rules/paper-research-question.d.ts.map +0 -1
- package/dist/eslint-rules/paper-research-question.js +0 -188
- package/dist/eslint-rules/paper-research-question.js.map +0 -1
- package/docs/rules/tex/missing-input.md +0 -58
- package/eslint-rules/paper-research-question.harness.mjs +0 -212
- package/eslint-rules/paper-research-question.ts +0 -199
- package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/comment-only/paper.tex +0 -7
- package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +0 -6
- package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +0 -6
- package/fixtures/paper-research-question/draft/paper.tex +0 -2
- package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/markdown-no-rq/paper.md +0 -4
- package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +0 -12
- package/fixtures/paper-research-question/shipped-no-rq/paper.tex +0 -3
- package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/shipped-with-rq/paper.tex +0 -2
- package/src/action-ref.test.ts +0 -26
- package/src/action-ref.ts +0 -15
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `paper/research-question` β a shipped paper states its research question EXPLICITLY.
|
|
3
|
-
*
|
|
4
|
-
* Reviewer A's verbatim point on agenticdev (#20), as it was parsed: "State the goal as a
|
|
5
|
-
* research question." This is a venue expectation, not taste: an empirical paper that never
|
|
6
|
-
* names the question it answers forces the reviewer to reconstruct it β and they say so.
|
|
7
|
-
*
|
|
8
|
-
* ββ WHY THE SCOPE IS SHIPPED PAPERS ONLY βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
9
|
-
* A draft never asked anyone to read it. The scope deliberately matches `paper/author-list`:
|
|
10
|
-
* both are about the DEBT of a shipped paper, and an unshipped one has no debt. Measured on the
|
|
11
|
-
* live corpus before the move:
|
|
12
|
-
* agenticdev-2026 has RQ, shipped -> silent (added because of this very review)
|
|
13
|
-
* aisec-2026 no RQ, shipped -> a finding
|
|
14
|
-
* compile-rules no RQ, shipped -> a finding
|
|
15
|
-
* scored-2026 no RQ, NOT shipped -> silent
|
|
16
|
-
*
|
|
17
|
-
* ββ WHAT THE MOVE CHANGED βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
18
|
-
* The predecessor read the stage with a REGEX OVER THE SCORECARD'S PROSE. Remeasured 2026-09-17:
|
|
19
|
-
* for `agenticdev-2026` the prose reads `submitted`, while the `stages` field declares
|
|
20
|
-
* `submitted, camera-ready`. Here the stage comes from the FIELD β the same one `paper/stages`
|
|
21
|
-
* checks against the bytes in both directions.
|
|
22
|
-
*
|
|
23
|
-
* β οΈ WHAT THE MOVE DOES NOT FIX, AND THIS IS MEASURED, NOT ASSUMED. The rule reads RAW text, not
|
|
24
|
-
* the parsed tree, so a mention of RQ inside a LaTeX comment (`% add an RQ`) will put it to
|
|
25
|
-
* sleep. Measured 2026-09-17 across all four papers: ZERO such cases β not a single match inside
|
|
26
|
-
* a comment. So the hole is LATENT, not observed, and closing it with a tree walk would
|
|
27
|
-
* complicate the rule without a measured difference. The neighboring `paper/typography` is built
|
|
28
|
-
* the same way and for the same reason. Once a real case shows up, there will be something to
|
|
29
|
-
* cite here.
|
|
30
|
-
*
|
|
31
|
-
* The rule is ADVISORY by decision, not by coincidence: a position paper may legitimately have
|
|
32
|
-
* no question. Then its absence is the author's decision, and the rule says exactly that,
|
|
33
|
-
* rather than declaring the absence a defect on its own.
|
|
34
|
-
*/
|
|
35
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
36
|
-
import { join, dirname } from "node:path";
|
|
37
|
-
import { load } from "js-yaml";
|
|
38
|
-
import { z } from "zod";
|
|
39
|
-
/**
|
|
40
|
-
* The two scorecard fields this rule reads, parsed rather than trusted: a field of the wrong shape
|
|
41
|
-
* reads as absent, the way `data?.stages` on a string or a number always did.
|
|
42
|
-
*/
|
|
43
|
-
const Scorecard = z
|
|
44
|
-
.looseObject({
|
|
45
|
-
stages: z.array(z.unknown()).catch([]),
|
|
46
|
-
researchQuestion: z.string().catch(""),
|
|
47
|
-
})
|
|
48
|
-
.catch({ stages: [], researchQuestion: "" });
|
|
49
|
-
const StageEntry = z.looseObject({ stage: z.unknown().optional() }).catch({});
|
|
50
|
-
const RuleOptions = z
|
|
51
|
-
.object({ statusFile: z.string().default("PIPELINE-STATUS.md") })
|
|
52
|
-
.default({ statusFile: "PIPELINE-STATUS.md" });
|
|
53
|
-
/**
|
|
54
|
-
* π΄ THE REGEX IS GONE, AND THE REASON IS NOT "regexes are bad" β IT IS THAT IT ANSWERED THE
|
|
55
|
-
* WRONG QUESTION (2026-09-19).
|
|
56
|
-
*
|
|
57
|
-
* What stood here was `/\\textbf\{RQ|\bRQ[0-9]?\b|research question/i`, defended in this very
|
|
58
|
-
* file with: "a regex is LEGITIMATE here: the subject is human prose, which has no structure by
|
|
59
|
-
* definition, and no parser can tell a question's statement apart from a paragraph about it."
|
|
60
|
-
*
|
|
61
|
-
* The premise is true. The conclusion does not follow. From "the text cannot decide this" the
|
|
62
|
-
* answer is not "so pattern-match harder" β it is "so the HUMAN declares it, and the check
|
|
63
|
-
* compares the declaration against the text." The three spellings were an undeclared schema
|
|
64
|
-
* field: someone added a fact and invented a convention for how it would look in prose, which is
|
|
65
|
-
* the design running backwards.
|
|
66
|
-
*
|
|
67
|
-
* What the pattern actually decided, in both directions:
|
|
68
|
-
* β’ a paper that says "we leave the research question to future work" β MATCHED, and the rule
|
|
69
|
-
* went silent on a paper that states no question at all;
|
|
70
|
-
* β’ a paper whose question is written plainly ("Does pruning reduce review cost?") with no
|
|
71
|
-
* "RQ" anywhere β NOT matched, and the rule reported a paper that does exactly what is asked.
|
|
72
|
-
* An open list of spellings cannot be completed, so both failures are structural, not bugs.
|
|
73
|
-
*
|
|
74
|
-
* ββ WHAT REPLACES IT: ONE UNDECIDABLE QUESTION BECOMES TWO DECIDABLE ONES ββββββββββββββββββ
|
|
75
|
-
* 1. Is the question WRITTEN DOWN? β `researchQuestion` in the scorecard's front matter. A
|
|
76
|
-
* field, parsed by js-yaml, not a phrase hunted for in prose.
|
|
77
|
-
* 2. Does the paper CONTAIN what was written down? β the declared sentence, whitespace
|
|
78
|
-
* collapsed, compared against the source. Bytes, not spelling.
|
|
79
|
-
*
|
|
80
|
-
* This is the shape this package already uses twice, and for the same reason: `bytes`/
|
|
81
|
-
* `sourceBytes` (a recorded number compared against the file on disk) and `paper/author-list`
|
|
82
|
-
* (a recorded marker, because whether a cross-check ran is not visible in the paper). Neither
|
|
83
|
-
* asks the prose a question the prose cannot answer.
|
|
84
|
-
*
|
|
85
|
-
* β οΈ WHAT THIS STILL CANNOT DO, said plainly rather than implied: nothing here decides that the
|
|
86
|
-
* declared sentence IS a research question. A declaration of "banana" passes step 1 and, if the
|
|
87
|
-
* word appears in the paper, step 2. The rule guarantees that the author wrote a question down
|
|
88
|
-
* and that the paper carries it β it does not grade the question. That is a human's job, and
|
|
89
|
-
* pretending otherwise is what the regex was doing.
|
|
90
|
-
*/
|
|
91
|
-
/** Whitespace is the only thing normalised: a sentence wrapped across lines is the same sentence. */
|
|
92
|
-
const flatten = (t) => t.replace(/\s+/g, " ").trim().toLowerCase();
|
|
93
|
-
/**
|
|
94
|
-
* Both facts come out of ONE parse of the scorecard's front matter: the stages (has this paper
|
|
95
|
-
* shipped?) and the declared question. Reading them separately would invite the two to be taken
|
|
96
|
-
* from different revisions of the same file.
|
|
97
|
-
*/
|
|
98
|
-
function scorecard(dir, statusName) {
|
|
99
|
-
const p = join(dir, statusName);
|
|
100
|
-
const none = { stages: [], question: "" };
|
|
101
|
-
if (!existsSync(p))
|
|
102
|
-
return none;
|
|
103
|
-
let text;
|
|
104
|
-
try {
|
|
105
|
-
text = readFileSync(p, "utf-8");
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return none;
|
|
109
|
-
}
|
|
110
|
-
const m = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
111
|
-
if (!m)
|
|
112
|
-
return none;
|
|
113
|
-
const [, front = ""] = m;
|
|
114
|
-
let data;
|
|
115
|
-
try {
|
|
116
|
-
data = load(front);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return none; // the unreadable YAML is already reported by `paper/stages`, on its own file
|
|
120
|
-
}
|
|
121
|
-
const { stages: raw, researchQuestion: question } = Scorecard.parse(data);
|
|
122
|
-
const stages = raw.map((r) => StageEntry.parse(r).stage).filter(Boolean);
|
|
123
|
-
return { stages, question };
|
|
124
|
-
}
|
|
125
|
-
export default {
|
|
126
|
-
rules: {
|
|
127
|
-
"research-question": {
|
|
128
|
-
meta: {
|
|
129
|
-
type: "suggestion",
|
|
130
|
-
docs: {
|
|
131
|
-
description: "a paper that declares a stage states its research question explicitly β otherwise the reviewer reconstructs it themselves, and says so",
|
|
132
|
-
},
|
|
133
|
-
schema: [
|
|
134
|
-
{
|
|
135
|
-
type: "object",
|
|
136
|
-
properties: {
|
|
137
|
-
// The scorecard's name is a CONSUMER convention. The rule is declared on the
|
|
138
|
-
// paper's source, so it has to name its neighbor itself rather than get it via a
|
|
139
|
-
// config glob.
|
|
140
|
-
statusFile: { type: "string" },
|
|
141
|
-
},
|
|
142
|
-
additionalProperties: false,
|
|
143
|
-
},
|
|
144
|
-
],
|
|
145
|
-
messages: {
|
|
146
|
-
notDeclared: "the paper shipped (stage Β«{{stages}}Β») and its scorecard declares no `researchQuestion`. This is reviewer A's verbatim point on agenticdev (#20), not the linter's taste. Write the question down as a field, in your own words β the check cannot infer it from the prose, and the pattern that used to try matched Β«we leave the research question to future workΒ» while missing a question stated plainly. Advisory: a position paper may legitimately have none, and then leaving the field out is a DECISION β record it as one",
|
|
147
|
-
notInPaper: "the scorecard declares a research question the paper does not contain. Looked for Β«{{needle}}Β» with whitespace collapsed, and the source has no such run of text. Either the paper dropped it or the declaration drifted from what was written β and which of the two it is, only you know",
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
create(context) {
|
|
151
|
-
const { statusFile: statusName } = RuleOptions.parse(context.options[0]);
|
|
152
|
-
return {
|
|
153
|
-
// `root:exit` exists for both markdown and the `tex/latex` language β the same place
|
|
154
|
-
// `paper/typography` lives, and for the same reason: a paper in this corpus can be
|
|
155
|
-
// either.
|
|
156
|
-
"root:exit"(node) {
|
|
157
|
-
const raw = context.sourceCode.raw ?? context.sourceCode.text;
|
|
158
|
-
if (typeof raw !== "string")
|
|
159
|
-
return;
|
|
160
|
-
const { stages, question } = scorecard(dirname(context.filename), statusName);
|
|
161
|
-
if (stages.length === 0)
|
|
162
|
-
return; // not shipped β owes nothing
|
|
163
|
-
// Step 1 β is it written down? A field, not a phrase hunted for in prose.
|
|
164
|
-
if (question.trim() === "") {
|
|
165
|
-
context.report({
|
|
166
|
-
node,
|
|
167
|
-
messageId: "notDeclared",
|
|
168
|
-
data: { stages: stages.join("/") },
|
|
169
|
-
});
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
// Step 2 β does the paper carry what was written down? Bytes, not spelling. Only
|
|
173
|
-
// whitespace is normalised, because a sentence wrapped across source lines is the
|
|
174
|
-
// same sentence; everything else stays the author's.
|
|
175
|
-
if (flatten(raw).includes(flatten(question)))
|
|
176
|
-
return;
|
|
177
|
-
context.report({
|
|
178
|
-
node,
|
|
179
|
-
messageId: "notInPaper",
|
|
180
|
-
data: { needle: flatten(question) },
|
|
181
|
-
});
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
//# sourceMappingURL=paper-research-question.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"paper-research-question.js","sourceRoot":"","sources":["../../eslint-rules/paper-research-question.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# tex/missing-input
|
|
2
|
-
|
|
3
|
-
**Level:** warn Β· **Reads:** `paper.tex`, and every file it includes
|
|
4
|
-
|
|
5
|
-
## What it catches
|
|
6
|
-
|
|
7
|
-
An `\input`, `\include` or `\subfile` that names a file which is not there. The path is resolved the
|
|
8
|
-
way TeX resolves it, from the paper's directory: `name.tex` first, then `name` as written. An
|
|
9
|
-
include inside an included file is resolved from the paper's directory too.
|
|
10
|
-
|
|
11
|
-
## Why
|
|
12
|
-
|
|
13
|
-
The rules that read the parse tree β `tex/template`, `tex/required-section`, `tex/venue-leftover`,
|
|
14
|
-
`tex/claim-provenance` β read the whole paper: `paper.tex` with every included file spliced in where
|
|
15
|
-
it is included, as TeX reads it. A file that is not there cannot be read, and without this rule the
|
|
16
|
-
others would report a clean body for text they never saw. TeX stops on it too.
|
|
17
|
-
|
|
18
|
-
## Examples
|
|
19
|
-
|
|
20
|
-
```latex
|
|
21
|
-
\input{sections/results} % warns when neither sections/results.tex nor sections/results exists
|
|
22
|
-
\input{sections/intro} % silent: sections/intro.tex is there
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
The first line reports:
|
|
26
|
-
|
|
27
|
-
> `sections/results` is not there (tried `sections/results.tex` and `sections/results`, from the
|
|
28
|
-
> paper's directory): TeX stops on it, and no rule read what it should hold
|
|
29
|
-
|
|
30
|
-
A missing file included by another included file is reported at the include in `paper.tex` that
|
|
31
|
-
brought that file in, and the message names the file that holds it.
|
|
32
|
-
|
|
33
|
-
## Options / preset fields
|
|
34
|
-
|
|
35
|
-
None.
|
|
36
|
-
|
|
37
|
-
## What it does not check
|
|
38
|
-
|
|
39
|
-
- A path built from a macro (`\input{\dir/intro}`): macros are not expanded, so the path is looked
|
|
40
|
-
up as written.
|
|
41
|
-
- `\import{dir}{file}` and other packages' include macros.
|
|
42
|
-
|
|
43
|
-
## How to fix
|
|
44
|
-
|
|
45
|
-
Correct the path, or add the file. A file that a build step writes (a table of generated numbers)
|
|
46
|
-
is absent before that step runs β keep the include with a disable directive and the reason:
|
|
47
|
-
|
|
48
|
-
```latex
|
|
49
|
-
% eslint-disable-next-line tex/missing-input -- written by repro/numbers.sh before the build
|
|
50
|
-
\input{generated/numbers}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Where findings in included files are reported
|
|
54
|
-
|
|
55
|
-
ESLint reports a finding in the file it lints, and paperlint lints `paper.tex`. A finding of any of
|
|
56
|
-
the rules above whose text came from an included file is reported at the include in `paper.tex`,
|
|
57
|
-
with the included file, line and column at the front of its message
|
|
58
|
-
(`sections/results.tex:12:3: β¦`). A disable directive for it goes above that include.
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Both halves for `paper/research-question`, plus two that a test forgets most often: SCOPE
|
|
3
|
-
* and LANGUAGE.
|
|
4
|
-
*
|
|
5
|
-
* The scope is load-bearing here. The rule does not ask "is there a question", it asks "is there
|
|
6
|
-
* a question FOR SOMETHING THAT HAS ALREADY SHIPPED". Remove the stage gate and the rule starts
|
|
7
|
-
* scolding every draft in the corpus, and gets turned off within a week. So "silent on a draft"
|
|
8
|
-
* is checked by its own assert, not treated as a special case of "silent".
|
|
9
|
-
*
|
|
10
|
-
* Language is the second forgotten half: a paper in this corpus can be either LaTeX or markdown.
|
|
11
|
-
* A rule pinned to `.tex` only would LOSE `compile-rules-2026`, where the paper is written in
|
|
12
|
-
* markdown and states no question β one of the two real findings on the live corpus.
|
|
13
|
-
*/
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { join, dirname } from "node:path";
|
|
16
|
-
import {
|
|
17
|
-
mkdirSync,
|
|
18
|
-
mkdtempSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
realpathSync,
|
|
21
|
-
rmSync,
|
|
22
|
-
writeFileSync,
|
|
23
|
-
} from "node:fs";
|
|
24
|
-
import { tmpdir } from "node:os";
|
|
25
|
-
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { Linter } from "eslint";
|
|
27
|
-
import markdown from "@eslint/markdown";
|
|
28
|
-
import { createChecker } from "../lib/check.ts";
|
|
29
|
-
|
|
30
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
|
-
const FIX = join(HERE, "..", "fixtures", "paper-research-question");
|
|
32
|
-
|
|
33
|
-
const { texLanguage } = await import(join(HERE, "latex-language.ts"));
|
|
34
|
-
const rq = (await import(join(HERE, "paper-research-question.ts"))).default;
|
|
35
|
-
|
|
36
|
-
const check = createChecker();
|
|
37
|
-
|
|
38
|
-
const linter = new Linter();
|
|
39
|
-
|
|
40
|
-
/** Run against one fixture. The language is picked by extension β exactly like the consumer's config. */
|
|
41
|
-
function lint(dir, file, opts = {}) {
|
|
42
|
-
const path = join(FIX, dir, file);
|
|
43
|
-
const tex = file.endsWith(".tex");
|
|
44
|
-
const cfg = tex
|
|
45
|
-
? {
|
|
46
|
-
files: ["**/*.tex"],
|
|
47
|
-
plugins: { tex: { languages: { latex: texLanguage } }, paper: rq },
|
|
48
|
-
language: "tex/latex",
|
|
49
|
-
}
|
|
50
|
-
: {
|
|
51
|
-
// `files` is required, not cosmetic: without it flat config falls back to JS
|
|
52
|
-
// extensions, and the fixture comes back as "No matching configuration found" β a
|
|
53
|
-
// message that reads exactly like a clean run.
|
|
54
|
-
files: ["**/*.md"],
|
|
55
|
-
plugins: { markdown, paper: rq },
|
|
56
|
-
language: "markdown/gfm",
|
|
57
|
-
languageOptions: { frontmatter: "yaml" },
|
|
58
|
-
};
|
|
59
|
-
const msgs = linter.verify(
|
|
60
|
-
readFileSync(path, "utf-8"),
|
|
61
|
-
[{ ...cfg, rules: { "paper/research-question": ["error", opts] } }],
|
|
62
|
-
path,
|
|
63
|
-
);
|
|
64
|
-
assert.deepEqual(
|
|
65
|
-
msgs.filter((m) => m.fatal),
|
|
66
|
-
[],
|
|
67
|
-
`${dir}/${file}: the rule threw`,
|
|
68
|
-
);
|
|
69
|
-
return msgs.map((m) => m.message);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ββ fires on a planted defect βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
73
|
-
const fires = lint("shipped-no-rq", "paper.tex");
|
|
74
|
-
check("shipped and no question β a finding", fires.length === 1);
|
|
75
|
-
// π΄ The message must name WHERE the requirement comes from. Without this it reads as the
|
|
76
|
-
// linter's taste, but the requirement came from a real venue reviewer.
|
|
77
|
-
check(
|
|
78
|
-
"and it cites the reviewer, rather than passing this off as the linter's taste",
|
|
79
|
-
/reviewer/i.test(fires[0]),
|
|
80
|
-
);
|
|
81
|
-
// The stages in the text come from the FIELD. The predecessor derived them with a regex over
|
|
82
|
-
// the scorecard's prose and on agenticdev printed `submitted` where `submitted, camera-ready`
|
|
83
|
-
// was declared.
|
|
84
|
-
check(
|
|
85
|
-
"the stage list in the message comes from the field and carries BOTH",
|
|
86
|
-
/submitted\/camera-ready/.test(fires[0]),
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// markdown β the second half of LANGUAGE, without it one of the two real findings is lost
|
|
90
|
-
check(
|
|
91
|
-
"a paper in markdown is checked the same way",
|
|
92
|
-
lint("markdown-no-rq", "paper.md").length === 1,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
// ββ THE CASE THE PREDECESSOR COULD NOT EXPRESS ββββββββββββββββββββββββββββββββββββββββββ
|
|
96
|
-
// π΄ The point of the 2026-09-19 redesign, in one fixture. The scorecard declares a question,
|
|
97
|
-
// the paper does not carry it β AND the paper contains the words "research question", inside
|
|
98
|
-
// "We leave the research question to future work". The old pattern matched that phrase and went
|
|
99
|
-
// SILENT on a paper stating no question at all. Matching a sentence ABOUT the thing is not
|
|
100
|
-
// finding the thing, and no list of spellings fixes that.
|
|
101
|
-
const drifted = lint("declared-not-in-paper", "paper.tex");
|
|
102
|
-
check(
|
|
103
|
-
"declared but absent from the paper β a finding, where the old pattern was silent",
|
|
104
|
-
drifted.length === 1,
|
|
105
|
-
);
|
|
106
|
-
check(
|
|
107
|
-
"and the message QUOTES what was looked for, so the author can see why it missed",
|
|
108
|
-
/does pruning reduce review cost\?/i.test(drifted[0]),
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
// ββ stays silent where it must ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
112
|
-
check(
|
|
113
|
-
"the question is stated β silent",
|
|
114
|
-
lint("shipped-with-rq", "paper.tex").length === 0,
|
|
115
|
-
);
|
|
116
|
-
// The stage gate: a draft owes nothing, because it never asked anyone to read it.
|
|
117
|
-
check(
|
|
118
|
-
"a draft (no stages) β silent, even though it has no question either",
|
|
119
|
-
lint("draft", "paper.tex").length === 0,
|
|
120
|
-
);
|
|
121
|
-
// And the distinguisher against the previous case: the same draft with a SWAPPED scorecard
|
|
122
|
-
// name is also silent β so the silence there is not because the file went unfound, it's
|
|
123
|
-
// because the stage list is empty.
|
|
124
|
-
check(
|
|
125
|
-
"and with a nonexistent scorecard a shipped paper is also silent β the stage gate is load-bearing",
|
|
126
|
-
lint("shipped-no-rq", "paper.tex", { statusFile: "NO-SUCH-FILE.md" })
|
|
127
|
-
.length === 0,
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
// ββ a named hole, pinned down by an assert, and RESHAPED by the redesign ββββββββββββββββ
|
|
131
|
-
// β οΈ The hole changed form on 2026-09-19 and did not close. Before, ANY mention of "RQ" in a
|
|
132
|
-
// LaTeX comment silenced the rule. Now the rule no longer hunts for spellings, so this fixture
|
|
133
|
-
// β a comment saying "TODO: state the research question", with no `researchQuestion` field β
|
|
134
|
-
// is a FINDING, which it never was before. What survives is narrower: if the DECLARED sentence
|
|
135
|
-
// appears only inside a comment, the raw-text comparison still accepts it. That is a smaller
|
|
136
|
-
// hole with a named owner, and closing it means walking the parsed tree.
|
|
137
|
-
check(
|
|
138
|
-
"a paper whose only mention is a comment, with nothing declared, is now a finding",
|
|
139
|
-
lint("comment-only", "paper.tex").length === 1,
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
// ββ the scorecard's name is consumer data βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
143
|
-
check(
|
|
144
|
-
"the scorecard's name comes in as an option",
|
|
145
|
-
lint("shipped-no-rq", "paper.tex", { statusFile: "PIPELINE-STATUS.md" })
|
|
146
|
-
.length === 1,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// ββ a scorecard that cannot be read is no scorecard: silence, never a crash ββββββββββββ
|
|
150
|
-
// A directory where the file should be (EISDIR), and front matter that is not YAML β which
|
|
151
|
-
// `paper/stages` reports on the scorecard itself, so this rule does not report it twice.
|
|
152
|
-
{
|
|
153
|
-
const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-rq-")));
|
|
154
|
-
try {
|
|
155
|
-
const paperIn = (name, status) => {
|
|
156
|
-
const dir = join(work, name);
|
|
157
|
-
mkdirSync(dir);
|
|
158
|
-
status(join(dir, "PIPELINE-STATUS.md"));
|
|
159
|
-
const path = join(dir, "paper.md");
|
|
160
|
-
writeFileSync(path, "# A paper\n");
|
|
161
|
-
// A linter rooted at the scratch directory: files outside `cwd` match no config.
|
|
162
|
-
return new Linter({ cwd: work }).verify(
|
|
163
|
-
"# A paper\n",
|
|
164
|
-
[
|
|
165
|
-
{
|
|
166
|
-
files: ["**/*.md"],
|
|
167
|
-
plugins: { markdown, paper: rq },
|
|
168
|
-
language: "markdown/gfm",
|
|
169
|
-
rules: { "paper/research-question": "error" },
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
path,
|
|
173
|
-
);
|
|
174
|
-
};
|
|
175
|
-
check(
|
|
176
|
-
"a scorecard path that is a directory: no finding, no crash",
|
|
177
|
-
JSON.stringify(paperIn("dir", (p) => mkdirSync(p))) === "[]",
|
|
178
|
-
() => paperIn("dir2", (p) => mkdirSync(p)),
|
|
179
|
-
);
|
|
180
|
-
check(
|
|
181
|
-
"a scorecard whose front matter is not YAML: no finding, no crash",
|
|
182
|
-
JSON.stringify(
|
|
183
|
-
paperIn("yaml", (p) =>
|
|
184
|
-
writeFileSync(p, "---\nstages: [submitted\n---\n"),
|
|
185
|
-
),
|
|
186
|
-
) === "[]",
|
|
187
|
-
);
|
|
188
|
-
} finally {
|
|
189
|
-
rmSync(work, { recursive: true, force: true });
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ββ a language whose source code carries no text at all: nothing to compare, silence βββ
|
|
194
|
-
{
|
|
195
|
-
const reports = [];
|
|
196
|
-
rq.rules["research-question"]
|
|
197
|
-
.create({
|
|
198
|
-
options: [],
|
|
199
|
-
filename: join(FIX, "shipped-no-rq", "paper.tex"),
|
|
200
|
-
sourceCode: {},
|
|
201
|
-
report: (r) => reports.push(r),
|
|
202
|
-
})
|
|
203
|
-
["root:exit"]({});
|
|
204
|
-
check(
|
|
205
|
-
"a source code with neither `raw` nor `text` is skipped, even for a shipped paper",
|
|
206
|
-
reports.length === 0,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
console.log(
|
|
211
|
-
`β ${String(check.count)} assertions passed β paper/research-question, the debt of a shipped paper`,
|
|
212
|
-
);
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `paper/research-question` β a shipped paper states its research question EXPLICITLY.
|
|
3
|
-
*
|
|
4
|
-
* Reviewer A's verbatim point on agenticdev (#20), as it was parsed: "State the goal as a
|
|
5
|
-
* research question." This is a venue expectation, not taste: an empirical paper that never
|
|
6
|
-
* names the question it answers forces the reviewer to reconstruct it β and they say so.
|
|
7
|
-
*
|
|
8
|
-
* ββ WHY THE SCOPE IS SHIPPED PAPERS ONLY βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
9
|
-
* A draft never asked anyone to read it. The scope deliberately matches `paper/author-list`:
|
|
10
|
-
* both are about the DEBT of a shipped paper, and an unshipped one has no debt. Measured on the
|
|
11
|
-
* live corpus before the move:
|
|
12
|
-
* agenticdev-2026 has RQ, shipped -> silent (added because of this very review)
|
|
13
|
-
* aisec-2026 no RQ, shipped -> a finding
|
|
14
|
-
* compile-rules no RQ, shipped -> a finding
|
|
15
|
-
* scored-2026 no RQ, NOT shipped -> silent
|
|
16
|
-
*
|
|
17
|
-
* ββ WHAT THE MOVE CHANGED βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
18
|
-
* The predecessor read the stage with a REGEX OVER THE SCORECARD'S PROSE. Remeasured 2026-09-17:
|
|
19
|
-
* for `agenticdev-2026` the prose reads `submitted`, while the `stages` field declares
|
|
20
|
-
* `submitted, camera-ready`. Here the stage comes from the FIELD β the same one `paper/stages`
|
|
21
|
-
* checks against the bytes in both directions.
|
|
22
|
-
*
|
|
23
|
-
* β οΈ WHAT THE MOVE DOES NOT FIX, AND THIS IS MEASURED, NOT ASSUMED. The rule reads RAW text, not
|
|
24
|
-
* the parsed tree, so a mention of RQ inside a LaTeX comment (`% add an RQ`) will put it to
|
|
25
|
-
* sleep. Measured 2026-09-17 across all four papers: ZERO such cases β not a single match inside
|
|
26
|
-
* a comment. So the hole is LATENT, not observed, and closing it with a tree walk would
|
|
27
|
-
* complicate the rule without a measured difference. The neighboring `paper/typography` is built
|
|
28
|
-
* the same way and for the same reason. Once a real case shows up, there will be something to
|
|
29
|
-
* cite here.
|
|
30
|
-
*
|
|
31
|
-
* The rule is ADVISORY by decision, not by coincidence: a position paper may legitimately have
|
|
32
|
-
* no question. Then its absence is the author's decision, and the rule says exactly that,
|
|
33
|
-
* rather than declaring the absence a defect on its own.
|
|
34
|
-
*/
|
|
35
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
36
|
-
import { join, dirname } from "node:path";
|
|
37
|
-
import { load } from "js-yaml";
|
|
38
|
-
import { z } from "zod";
|
|
39
|
-
import type { RuleContext } from "./rule-context.ts";
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The two scorecard fields this rule reads, parsed rather than trusted: a field of the wrong shape
|
|
43
|
-
* reads as absent, the way `data?.stages` on a string or a number always did.
|
|
44
|
-
*/
|
|
45
|
-
const Scorecard = z
|
|
46
|
-
.looseObject({
|
|
47
|
-
stages: z.array(z.unknown()).catch([]),
|
|
48
|
-
researchQuestion: z.string().catch(""),
|
|
49
|
-
})
|
|
50
|
-
.catch({ stages: [], researchQuestion: "" });
|
|
51
|
-
const StageEntry = z.looseObject({ stage: z.unknown().optional() }).catch({});
|
|
52
|
-
const RuleOptions = z
|
|
53
|
-
.object({ statusFile: z.string().default("PIPELINE-STATUS.md") })
|
|
54
|
-
.default({ statusFile: "PIPELINE-STATUS.md" });
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* π΄ THE REGEX IS GONE, AND THE REASON IS NOT "regexes are bad" β IT IS THAT IT ANSWERED THE
|
|
58
|
-
* WRONG QUESTION (2026-09-19).
|
|
59
|
-
*
|
|
60
|
-
* What stood here was `/\\textbf\{RQ|\bRQ[0-9]?\b|research question/i`, defended in this very
|
|
61
|
-
* file with: "a regex is LEGITIMATE here: the subject is human prose, which has no structure by
|
|
62
|
-
* definition, and no parser can tell a question's statement apart from a paragraph about it."
|
|
63
|
-
*
|
|
64
|
-
* The premise is true. The conclusion does not follow. From "the text cannot decide this" the
|
|
65
|
-
* answer is not "so pattern-match harder" β it is "so the HUMAN declares it, and the check
|
|
66
|
-
* compares the declaration against the text." The three spellings were an undeclared schema
|
|
67
|
-
* field: someone added a fact and invented a convention for how it would look in prose, which is
|
|
68
|
-
* the design running backwards.
|
|
69
|
-
*
|
|
70
|
-
* What the pattern actually decided, in both directions:
|
|
71
|
-
* β’ a paper that says "we leave the research question to future work" β MATCHED, and the rule
|
|
72
|
-
* went silent on a paper that states no question at all;
|
|
73
|
-
* β’ a paper whose question is written plainly ("Does pruning reduce review cost?") with no
|
|
74
|
-
* "RQ" anywhere β NOT matched, and the rule reported a paper that does exactly what is asked.
|
|
75
|
-
* An open list of spellings cannot be completed, so both failures are structural, not bugs.
|
|
76
|
-
*
|
|
77
|
-
* ββ WHAT REPLACES IT: ONE UNDECIDABLE QUESTION BECOMES TWO DECIDABLE ONES ββββββββββββββββββ
|
|
78
|
-
* 1. Is the question WRITTEN DOWN? β `researchQuestion` in the scorecard's front matter. A
|
|
79
|
-
* field, parsed by js-yaml, not a phrase hunted for in prose.
|
|
80
|
-
* 2. Does the paper CONTAIN what was written down? β the declared sentence, whitespace
|
|
81
|
-
* collapsed, compared against the source. Bytes, not spelling.
|
|
82
|
-
*
|
|
83
|
-
* This is the shape this package already uses twice, and for the same reason: `bytes`/
|
|
84
|
-
* `sourceBytes` (a recorded number compared against the file on disk) and `paper/author-list`
|
|
85
|
-
* (a recorded marker, because whether a cross-check ran is not visible in the paper). Neither
|
|
86
|
-
* asks the prose a question the prose cannot answer.
|
|
87
|
-
*
|
|
88
|
-
* β οΈ WHAT THIS STILL CANNOT DO, said plainly rather than implied: nothing here decides that the
|
|
89
|
-
* declared sentence IS a research question. A declaration of "banana" passes step 1 and, if the
|
|
90
|
-
* word appears in the paper, step 2. The rule guarantees that the author wrote a question down
|
|
91
|
-
* and that the paper carries it β it does not grade the question. That is a human's job, and
|
|
92
|
-
* pretending otherwise is what the regex was doing.
|
|
93
|
-
*/
|
|
94
|
-
|
|
95
|
-
/** Whitespace is the only thing normalised: a sentence wrapped across lines is the same sentence. */
|
|
96
|
-
const flatten = (t: string) => t.replace(/\s+/g, " ").trim().toLowerCase();
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Both facts come out of ONE parse of the scorecard's front matter: the stages (has this paper
|
|
100
|
-
* shipped?) and the declared question. Reading them separately would invite the two to be taken
|
|
101
|
-
* from different revisions of the same file.
|
|
102
|
-
*/
|
|
103
|
-
function scorecard(
|
|
104
|
-
dir: string,
|
|
105
|
-
statusName: string,
|
|
106
|
-
): { stages: unknown[]; question: string } {
|
|
107
|
-
const p = join(dir, statusName);
|
|
108
|
-
const none = { stages: [], question: "" };
|
|
109
|
-
if (!existsSync(p)) return none;
|
|
110
|
-
let text: string;
|
|
111
|
-
try {
|
|
112
|
-
text = readFileSync(p, "utf-8");
|
|
113
|
-
} catch {
|
|
114
|
-
return none;
|
|
115
|
-
}
|
|
116
|
-
const m = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
117
|
-
if (!m) return none;
|
|
118
|
-
const [, front = ""] = m;
|
|
119
|
-
let data: unknown;
|
|
120
|
-
try {
|
|
121
|
-
data = load(front);
|
|
122
|
-
} catch {
|
|
123
|
-
return none; // the unreadable YAML is already reported by `paper/stages`, on its own file
|
|
124
|
-
}
|
|
125
|
-
const { stages: raw, researchQuestion: question } = Scorecard.parse(data);
|
|
126
|
-
const stages = raw.map((r) => StageEntry.parse(r).stage).filter(Boolean);
|
|
127
|
-
return { stages, question };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export default {
|
|
131
|
-
rules: {
|
|
132
|
-
"research-question": {
|
|
133
|
-
meta: {
|
|
134
|
-
type: "suggestion" as const,
|
|
135
|
-
docs: {
|
|
136
|
-
description:
|
|
137
|
-
"a paper that declares a stage states its research question explicitly β otherwise the reviewer reconstructs it themselves, and says so",
|
|
138
|
-
},
|
|
139
|
-
schema: [
|
|
140
|
-
{
|
|
141
|
-
type: "object",
|
|
142
|
-
properties: {
|
|
143
|
-
// The scorecard's name is a CONSUMER convention. The rule is declared on the
|
|
144
|
-
// paper's source, so it has to name its neighbor itself rather than get it via a
|
|
145
|
-
// config glob.
|
|
146
|
-
statusFile: { type: "string" },
|
|
147
|
-
},
|
|
148
|
-
additionalProperties: false,
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
|
-
messages: {
|
|
152
|
-
notDeclared:
|
|
153
|
-
"the paper shipped (stage Β«{{stages}}Β») and its scorecard declares no `researchQuestion`. This is reviewer A's verbatim point on agenticdev (#20), not the linter's taste. Write the question down as a field, in your own words β the check cannot infer it from the prose, and the pattern that used to try matched Β«we leave the research question to future workΒ» while missing a question stated plainly. Advisory: a position paper may legitimately have none, and then leaving the field out is a DECISION β record it as one",
|
|
154
|
-
notInPaper:
|
|
155
|
-
"the scorecard declares a research question the paper does not contain. Looked for Β«{{needle}}Β» with whitespace collapsed, and the source has no such run of text. Either the paper dropped it or the declaration drifted from what was written β and which of the two it is, only you know",
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
create(context: RuleContext) {
|
|
159
|
-
const { statusFile: statusName } = RuleOptions.parse(
|
|
160
|
-
context.options[0],
|
|
161
|
-
);
|
|
162
|
-
return {
|
|
163
|
-
// `root:exit` exists for both markdown and the `tex/latex` language β the same place
|
|
164
|
-
// `paper/typography` lives, and for the same reason: a paper in this corpus can be
|
|
165
|
-
// either.
|
|
166
|
-
"root:exit"(node: object) {
|
|
167
|
-
const raw = context.sourceCode.raw ?? context.sourceCode.text;
|
|
168
|
-
if (typeof raw !== "string") return;
|
|
169
|
-
const { stages, question } = scorecard(
|
|
170
|
-
dirname(context.filename),
|
|
171
|
-
statusName,
|
|
172
|
-
);
|
|
173
|
-
if (stages.length === 0) return; // not shipped β owes nothing
|
|
174
|
-
|
|
175
|
-
// Step 1 β is it written down? A field, not a phrase hunted for in prose.
|
|
176
|
-
if (question.trim() === "") {
|
|
177
|
-
context.report({
|
|
178
|
-
node,
|
|
179
|
-
messageId: "notDeclared",
|
|
180
|
-
data: { stages: stages.join("/") },
|
|
181
|
-
});
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Step 2 β does the paper carry what was written down? Bytes, not spelling. Only
|
|
186
|
-
// whitespace is normalised, because a sentence wrapped across source lines is the
|
|
187
|
-
// same sentence; everything else stays the author's.
|
|
188
|
-
if (flatten(raw).includes(flatten(question))) return;
|
|
189
|
-
context.report({
|
|
190
|
-
node,
|
|
191
|
-
messageId: "notInPaper",
|
|
192
|
-
data: { needle: flatten(question) },
|
|
193
|
-
});
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
};
|