paperlint 4.1.0 → 5.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 +26 -2
- package/CONTRIBUTING.md +7 -0
- package/README.md +15 -4
- package/action.harness.mjs +54 -36
- package/action.yml +12 -27
- package/dist/adapters/latex/document-class.d.ts +6 -5
- package/dist/adapters/latex/document-class.d.ts.map +1 -1
- package/dist/adapters/latex/document-class.js +18 -10
- package/dist/adapters/latex/document-class.js.map +1 -1
- package/dist/adapters/latex/includes.d.ts +16 -0
- package/dist/adapters/latex/includes.d.ts.map +1 -0
- package/dist/adapters/latex/includes.js +48 -0
- package/dist/adapters/latex/includes.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +9 -5
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +8 -1
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/nodes.d.ts +5 -1
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +6 -2
- 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 +1 -0
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/latex/prose.d.ts +8 -0
- package/dist/adapters/latex/prose.d.ts.map +1 -0
- package/dist/adapters/latex/prose.js +353 -0
- package/dist/adapters/latex/prose.js.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -3
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +34 -0
- package/dist/claim-provenance.d.ts.map +1 -0
- package/dist/claim-provenance.js +146 -0
- package/dist/claim-provenance.js.map +1 -0
- package/dist/cli.d.ts +2 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +81 -31
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +58 -0
- package/dist/domain/paper-source.d.ts.map +1 -0
- package/dist/domain/paper-source.js +109 -0
- package/dist/domain/paper-source.js.map +1 -0
- package/dist/domain/sentences.d.ts +26 -0
- package/dist/domain/sentences.d.ts.map +1 -0
- package/dist/domain/sentences.js +48 -0
- package/dist/domain/sentences.js.map +1 -0
- package/dist/domain/tex-document.d.ts +39 -4
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js.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 +8 -1
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +85 -19
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- 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/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/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/ports/latex.d.ts +12 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/register.d.ts +40 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +134 -0
- package/dist/register.js.map +1 -0
- 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 +33 -0
- package/dist/tex-paper.d.ts.map +1 -0
- package/dist/tex-paper.js +103 -0
- package/dist/tex-paper.js.map +1 -0
- package/dist/tex-venue-rules.d.ts +7 -2
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +31 -17
- package/dist/tex-venue-rules.js.map +1 -1
- package/docs/configuration.md +5 -4
- package/docs/install.md +8 -8
- package/docs/prior-art/paper-folder-scaffolding.md +4 -7
- package/docs/rules/tex/claim-provenance.md +83 -0
- package/docs/rules/tex/register.md +77 -0
- package/docs/rules/tex/required-section.md +3 -1
- package/docs/rules/tex/template.md +12 -0
- package/docs/rules/tex/venue-leftover.md +4 -2
- package/docs/rules.md +17 -10
- 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.test.mjs +48 -1
- package/eslint-rules/paper-typography.ts +139 -24
- package/eslint-rules/tex-build.ts +8 -1
- package/eslint.config.mjs +1 -0
- package/fixtures/real-markdown-paper/baseline.json +1 -2
- package/fixtures/real-markdown-paper/baseline.ts +8 -7
- package/package.json +2 -1
- package/scripts/check.ts +2 -2
- package/scripts/eslint-report-guard.mjs +5 -2
- package/scripts/harness-api.frozen.json +0 -1
- package/skills/cold-read-diff/SKILL.md +31 -10
- package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
- package/skills/draft-paper/SKILL.md +2 -2
- package/skills/draft-paper/SKILL.md.spec.ts +1 -1
- package/skills/grade-paper-writing/SKILL.md +109 -44
- package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
- package/skills/harden-paper/SKILL.md +35 -36
- package/skills/harden-paper/SKILL.md.spec.ts +34 -35
- 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 +3 -3
- package/skills/paper-pipeline/SKILL.md.spec.ts +2 -2
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
- package/skills/paper-pipeline/references/pipeline-status-template.md +2 -16
- package/skills/paper-pipeline/references/writing-craft.md +53 -18
- package/skills/pc-panel-review/SKILL.md +36 -11
- package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
- package/src/adapters/latex/document-class.ts +20 -12
- package/src/adapters/latex/includes.ts +74 -0
- package/src/adapters/latex/index.test.ts +183 -1
- package/src/adapters/latex/index.ts +14 -4
- package/src/adapters/latex/nodes.ts +11 -2
- package/src/adapters/latex/parse.ts +1 -0
- package/src/adapters/latex/prose.ts +484 -0
- package/src/build.harness.mjs +4 -2
- package/src/build.test.ts +18 -1
- package/src/build.ts +7 -3
- package/src/claim-provenance.test.ts +352 -0
- package/src/claim-provenance.ts +217 -0
- package/src/cli.harness.mjs +22 -54
- package/src/cli.test.ts +2 -18
- package/src/cli.ts +92 -36
- package/src/domain/paper-source.test.ts +156 -0
- package/src/domain/paper-source.ts +233 -0
- package/src/domain/sentences.ts +67 -0
- package/src/domain/tex-document.ts +36 -3
- 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/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/ports/latex.ts +13 -0
- package/src/register.test.ts +165 -0
- package/src/register.ts +177 -0
- package/src/rule-cases.test.ts +43 -23
- package/src/rule-docs.test.ts +0 -1
- 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 +148 -0
- package/src/tex-paper.ts +161 -0
- package/src/tex-venue-rules.test.ts +87 -5
- package/src/tex-venue-rules.ts +62 -30
- 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/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/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
- package/src/action-ref.test.ts +0 -26
- package/src/action-ref.ts +0 -15
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A paper as one text: `paper.tex` with every `\input`, `\include` and `\subfile` replaced by the
|
|
3
|
+
* file it names — what TeX itself does — and a map from every character of that text back to the
|
|
4
|
+
* file and offset it came from. The rules read the whole paper; a finding still points at its file.
|
|
5
|
+
*
|
|
6
|
+
* ── WHY SPLICE THE TEXT ──────────────────────────────────────────────────────────
|
|
7
|
+
* `\input` is textual inclusion: TeX reads the named file where the macro stands. Replacing the
|
|
8
|
+
* macro by the file's text gives the parser exactly what TeX reads, so every question the reader
|
|
9
|
+
* answers about one file (the class, the outline, the rendered text, the body's passages) is
|
|
10
|
+
* answered about the paper, unchanged. A subfile is a document of its own; only its body is spliced.
|
|
11
|
+
*
|
|
12
|
+
* ── WHAT IS NOT DONE ─────────────────────────────────────────────────────────────
|
|
13
|
+
* No macro is expanded, so a path built from a macro (`\input{\dir/intro}`) is read as written. A
|
|
14
|
+
* file that cannot be found is left out and named in `missing`; a file that includes itself,
|
|
15
|
+
* directly or through others, is spliced once.
|
|
16
|
+
*/
|
|
17
|
+
import { extname, normalize } from "node:path";
|
|
18
|
+
import type { Span } from "./tex-document.ts";
|
|
19
|
+
|
|
20
|
+
/** One `\input`, `\include` or `\subfile` in a source: the path as written and where the macro stands. */
|
|
21
|
+
export interface Include {
|
|
22
|
+
readonly macro: "input" | "include" | "subfile";
|
|
23
|
+
readonly target: string;
|
|
24
|
+
readonly span: Span;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What assembling needs from the LaTeX reader and from the disk, relative to the paper directory. */
|
|
28
|
+
export interface AssembleDeps {
|
|
29
|
+
/** The includes of a source, in source order. */
|
|
30
|
+
readonly includes: (src: string) => readonly Include[];
|
|
31
|
+
/** The span of a source's `document` environment body, or null when it has none. */
|
|
32
|
+
readonly documentBody: (src: string) => Span | null;
|
|
33
|
+
/** A file's text by its path relative to the paper directory, or null when there is none. */
|
|
34
|
+
readonly read: (path: string) => string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A stretch of the assembled text copied from one file: `[start, end)` here is `from…` there. */
|
|
38
|
+
export interface Segment {
|
|
39
|
+
readonly start: number;
|
|
40
|
+
readonly end: number;
|
|
41
|
+
readonly file: string;
|
|
42
|
+
/** The whole text of `file`, so a place in it can be told as a line and a column. */
|
|
43
|
+
readonly source: string;
|
|
44
|
+
readonly from: number;
|
|
45
|
+
/** Where, in the main file, the outermost include that brought this text in stands. */
|
|
46
|
+
readonly via: Span | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** An include whose file was not found, where it stands. */
|
|
50
|
+
export interface MissingInclude {
|
|
51
|
+
readonly file: string;
|
|
52
|
+
readonly target: string;
|
|
53
|
+
readonly span: Span;
|
|
54
|
+
/** Where it stands in the main file (the include itself, or the one that brought its file in). */
|
|
55
|
+
readonly via: Span;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The paper as one text, the map back to its files, and the includes that were not found. */
|
|
59
|
+
export interface PaperSource {
|
|
60
|
+
readonly main: string;
|
|
61
|
+
readonly text: string;
|
|
62
|
+
readonly segments: readonly Segment[];
|
|
63
|
+
readonly missing: readonly MissingInclude[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The paths TeX tries for a target: `name.tex`, then `name` — or the name alone if it has one. */
|
|
67
|
+
const candidates = (target: string): readonly string[] => {
|
|
68
|
+
const t = normalize(target.trim());
|
|
69
|
+
return extname(t) === "" ? [`${t}.tex`, t] : [t];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The first candidate path that exists, with its text. */
|
|
73
|
+
function found(
|
|
74
|
+
target: string,
|
|
75
|
+
read: AssembleDeps["read"],
|
|
76
|
+
): { readonly path: string; readonly text: string } | null {
|
|
77
|
+
const hits = candidates(target).flatMap((path) => {
|
|
78
|
+
const text = read(path);
|
|
79
|
+
return text === null ? [] : [{ path, text }];
|
|
80
|
+
});
|
|
81
|
+
return hits[0] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** An assembled piece of text: the text, its segments from 0, and the includes not found. */
|
|
85
|
+
interface Piece {
|
|
86
|
+
readonly text: string;
|
|
87
|
+
readonly segments: readonly Segment[];
|
|
88
|
+
readonly missing: readonly MissingInclude[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `segments` moved by `by` characters. */
|
|
92
|
+
const shifted = (
|
|
93
|
+
segments: readonly Segment[],
|
|
94
|
+
by: number,
|
|
95
|
+
): readonly Segment[] =>
|
|
96
|
+
segments.map((s) => ({ ...s, start: s.start + by, end: s.end + by }));
|
|
97
|
+
|
|
98
|
+
/** `[from, to)` of `file`'s text, copied as it is, brought in via `via`. */
|
|
99
|
+
interface Stretch {
|
|
100
|
+
readonly file: string;
|
|
101
|
+
readonly text: string;
|
|
102
|
+
readonly from: number;
|
|
103
|
+
readonly to: number;
|
|
104
|
+
readonly via: Span | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A stretch as a piece: its text, and one segment mapping it back. */
|
|
108
|
+
const copied = ({ file, text, from, to, via }: Stretch): Piece => ({
|
|
109
|
+
text: text.slice(from, to),
|
|
110
|
+
segments:
|
|
111
|
+
to > from
|
|
112
|
+
? [{ start: 0, end: to - from, file, source: text, from, via }]
|
|
113
|
+
: [],
|
|
114
|
+
missing: [],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** Nothing: an include left out. */
|
|
118
|
+
const NOTHING: Piece = { text: "", segments: [], missing: [] };
|
|
119
|
+
|
|
120
|
+
/** Pieces joined in order. */
|
|
121
|
+
const joined = (pieces: readonly Piece[]): Piece =>
|
|
122
|
+
pieces.reduce<Piece>(
|
|
123
|
+
(acc, p) => ({
|
|
124
|
+
text: acc.text + p.text,
|
|
125
|
+
segments: [...acc.segments, ...shifted(p.segments, acc.text.length)],
|
|
126
|
+
missing: [...acc.missing, ...p.missing],
|
|
127
|
+
}),
|
|
128
|
+
NOTHING,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `[from, to)` of `file`, its includes spliced. `stack` is the chain of files being spliced, so a
|
|
133
|
+
* file that includes itself is not entered again; `via` is where the chain began in the main file.
|
|
134
|
+
*/
|
|
135
|
+
/** What splicing carries down the chain of files. */
|
|
136
|
+
interface Context {
|
|
137
|
+
readonly deps: AssembleDeps;
|
|
138
|
+
readonly stack: readonly string[];
|
|
139
|
+
readonly via: Span | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function spliced(file: string, text: string, range: Span, ctx: Context): Piece {
|
|
143
|
+
const incs = ctx.deps
|
|
144
|
+
.includes(text)
|
|
145
|
+
.filter((i) => i.span.start >= range.start && i.span.end <= range.end);
|
|
146
|
+
const at = (from: number, to: number): Piece =>
|
|
147
|
+
copied({ file, text, from, to, via: ctx.via });
|
|
148
|
+
const { pieces, cursor } = incs.reduce<{
|
|
149
|
+
readonly pieces: readonly Piece[];
|
|
150
|
+
readonly cursor: number;
|
|
151
|
+
}>(
|
|
152
|
+
(acc, inc) => ({
|
|
153
|
+
pieces: [
|
|
154
|
+
...acc.pieces,
|
|
155
|
+
at(acc.cursor, inc.span.start),
|
|
156
|
+
included(file, inc, ctx),
|
|
157
|
+
],
|
|
158
|
+
cursor: inc.span.end,
|
|
159
|
+
}),
|
|
160
|
+
{ pieces: [], cursor: range.start },
|
|
161
|
+
);
|
|
162
|
+
return joined([...pieces, at(cursor, range.end)]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** One include: the file it names, spliced, or — when not found — nothing, and a report. */
|
|
166
|
+
function included(file: string, inc: Include, ctx: Context): Piece {
|
|
167
|
+
const via = ctx.via ?? inc.span;
|
|
168
|
+
const hit = found(inc.target, ctx.deps.read);
|
|
169
|
+
if (hit === null)
|
|
170
|
+
return {
|
|
171
|
+
...NOTHING,
|
|
172
|
+
missing: [{ file, target: inc.target, span: inc.span, via }],
|
|
173
|
+
};
|
|
174
|
+
if (ctx.stack.includes(hit.path)) return NOTHING;
|
|
175
|
+
const body = inc.macro === "subfile" ? ctx.deps.documentBody(hit.text) : null;
|
|
176
|
+
return spliced(
|
|
177
|
+
hit.path,
|
|
178
|
+
hit.text,
|
|
179
|
+
body ?? { start: 0, end: hit.text.length },
|
|
180
|
+
{ deps: ctx.deps, stack: [...ctx.stack, hit.path], via },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The paper whose main file is `main` (a path relative to the paper directory) with `text`. */
|
|
185
|
+
export function assemblePaper(
|
|
186
|
+
main: string,
|
|
187
|
+
text: string,
|
|
188
|
+
deps: AssembleDeps,
|
|
189
|
+
): PaperSource {
|
|
190
|
+
const p = spliced(
|
|
191
|
+
main,
|
|
192
|
+
text,
|
|
193
|
+
{ start: 0, end: text.length },
|
|
194
|
+
{ deps, stack: [main], via: null },
|
|
195
|
+
);
|
|
196
|
+
return {
|
|
197
|
+
main,
|
|
198
|
+
text: p.text,
|
|
199
|
+
segments: p.segments,
|
|
200
|
+
missing: p.missing,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Where a span of the assembled text stands: in which file, at which offsets, and via which include. */
|
|
205
|
+
export interface Origin {
|
|
206
|
+
readonly file: string;
|
|
207
|
+
/** The whole text of `file`. */
|
|
208
|
+
readonly source: string;
|
|
209
|
+
readonly span: Span;
|
|
210
|
+
readonly via: Span | null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The origin of `span` of the assembled text: the file of its first character. A span that runs
|
|
215
|
+
* past that file's stretch is cut at its end — a sentence cannot point into two files.
|
|
216
|
+
*/
|
|
217
|
+
export function originOf(p: PaperSource, span: Span): Origin | null {
|
|
218
|
+
// The character at `start`; an empty span at the very end belongs to the last stretch.
|
|
219
|
+
const seg =
|
|
220
|
+
p.segments.find((s) => span.start >= s.start && span.start < s.end) ??
|
|
221
|
+
p.segments.find((s) => span.start === s.end);
|
|
222
|
+
if (seg === undefined) return null;
|
|
223
|
+
const end = Math.min(span.end, seg.end);
|
|
224
|
+
return {
|
|
225
|
+
file: seg.file,
|
|
226
|
+
source: seg.source,
|
|
227
|
+
span: {
|
|
228
|
+
start: seg.from + span.start - seg.start,
|
|
229
|
+
end: seg.from + end - seg.start,
|
|
230
|
+
},
|
|
231
|
+
via: seg.via,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sentences of body prose — one splitter, so every rule that counts or judges sentences cuts
|
|
3
|
+
* them the same way (`tex/claim-provenance` judges each; `tex/register` counts them).
|
|
4
|
+
*
|
|
5
|
+
* A passage becomes one text: its prose as itself, and each mark (a citation, a cross-reference) or
|
|
6
|
+
* math as one character no prose contains, where it stands. `Intl.Segmenter` cuts that text; a break
|
|
7
|
+
* after an abbreviation is undone, and marks set right after a full stop belong to the sentence the
|
|
8
|
+
* full stop ends.
|
|
9
|
+
*/
|
|
10
|
+
import type { Passage, ProsePiece, Segment, TextRun } from "./tex-document.ts";
|
|
11
|
+
|
|
12
|
+
/** What stands in a sentence's text for a mark: characters no prose contains (private use). */
|
|
13
|
+
export const CITATION = "";
|
|
14
|
+
export const REFERENCE = "";
|
|
15
|
+
export const MATH = "";
|
|
16
|
+
|
|
17
|
+
/** A piece as the text of its sentence sees it: text as itself, a mark or math as one character. */
|
|
18
|
+
export function segmentOf(p: ProsePiece): Segment {
|
|
19
|
+
switch (p.kind) {
|
|
20
|
+
case "text":
|
|
21
|
+
return p.segment;
|
|
22
|
+
case "owner":
|
|
23
|
+
return {
|
|
24
|
+
text: p.owner === "citation" ? CITATION : REFERENCE,
|
|
25
|
+
at: p.span.start,
|
|
26
|
+
};
|
|
27
|
+
case "math":
|
|
28
|
+
return { text: MATH, at: p.span.start };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A passage as one run of text, each mark a character where it stands. */
|
|
33
|
+
export const runOf = (p: Passage): TextRun => {
|
|
34
|
+
const [first, ...rest] = p.pieces;
|
|
35
|
+
return { segments: [segmentOf(first), ...rest.map(segmentOf)] };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** An abbreviation whose full stop does not end a sentence, at the end of a segment. */
|
|
39
|
+
const ABBREVIATION =
|
|
40
|
+
/(?:^|[^\p{L}])(?:e\.g|i\.e|cf|vs|et al|Figs?|Secs?|Tab|Eqs?|No|App|approx|resp)\.\s*$/u;
|
|
41
|
+
|
|
42
|
+
const SEGMENTER = new Intl.Segmenter("en", { granularity: "sentence" });
|
|
43
|
+
|
|
44
|
+
/** Marks, and the spaces among them, that open a segment: a `\cite` or a footnote set after the full stop. */
|
|
45
|
+
const LEADING_MARKS = /^[\s]*[]/u;
|
|
46
|
+
|
|
47
|
+
/** `[from, to)` of a text. */
|
|
48
|
+
export type Range = readonly [number, number];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The sentences of `text`, as `[from, to)` ranges. A break after an abbreviation is undone, and
|
|
52
|
+
* marks standing right after a full stop belong to the sentence the full stop ends.
|
|
53
|
+
*/
|
|
54
|
+
export function sentences(text: string): readonly Range[] {
|
|
55
|
+
return [...SEGMENTER.segment(text)].reduce<readonly Range[]>((acc, s) => {
|
|
56
|
+
const prev = acc.at(-1);
|
|
57
|
+
const end = s.index + s.segment.length;
|
|
58
|
+
if (prev === undefined) return [[s.index, end]];
|
|
59
|
+
const before = acc.slice(0, -1);
|
|
60
|
+
if (ABBREVIATION.test(text.slice(...prev)))
|
|
61
|
+
return [...before, [prev[0], end]];
|
|
62
|
+
const split = s.index + (LEADING_MARKS.exec(s.segment)?.[0].length ?? 0);
|
|
63
|
+
return split < end
|
|
64
|
+
? [...before, [prev[0], split], [split, end]]
|
|
65
|
+
: [...before, [prev[0], end]];
|
|
66
|
+
}, []);
|
|
67
|
+
}
|
|
@@ -24,12 +24,28 @@ export interface DocumentClass {
|
|
|
24
24
|
readonly options: readonly string[];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
export type
|
|
29
|
-
| { readonly kind: "missing" }
|
|
27
|
+
/** One `\documentclass` line: one that names no class, or a class with its options. */
|
|
28
|
+
export type ClassCandidate =
|
|
30
29
|
| { readonly kind: "empty"; readonly place: Place }
|
|
31
30
|
| ({ readonly kind: "class"; readonly place: Place } & DocumentClass);
|
|
32
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A paper's `\documentclass`: none, exactly one, or several — a source that picks its class behind
|
|
34
|
+
* a TeX switch (`\if\venue1 \documentclass{article} \fi …`). Which branch builds is not
|
|
35
|
+
* evaluated, so several lines are candidates, in source order, and none of them is THE class.
|
|
36
|
+
*/
|
|
37
|
+
export type ClassLine =
|
|
38
|
+
| { readonly kind: "missing" }
|
|
39
|
+
| ClassCandidate
|
|
40
|
+
| {
|
|
41
|
+
readonly kind: "ambiguous";
|
|
42
|
+
readonly candidates: readonly [
|
|
43
|
+
ClassCandidate,
|
|
44
|
+
ClassCandidate,
|
|
45
|
+
...ClassCandidate[],
|
|
46
|
+
];
|
|
47
|
+
};
|
|
48
|
+
|
|
33
49
|
/** The options of `want` that `got` does not carry — every one, in `want`'s order. */
|
|
34
50
|
export const missingOptions = (
|
|
35
51
|
got: DocumentClass,
|
|
@@ -108,3 +124,20 @@ export function spanIn(run: TextRun, from: number, to: number): Span | null {
|
|
|
108
124
|
const last = to > from ? offsetIn(run.segments, to - 1) : null;
|
|
109
125
|
return start === null || last === null ? null : { start, end: last + 1 };
|
|
110
126
|
}
|
|
127
|
+
|
|
128
|
+
/** What a mark in prose says about where a claim comes from: a work outside the paper, or a place in it. */
|
|
129
|
+
export type Owner = "citation" | "reference";
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* One piece of body prose, in source order: text a reader sees, a mark the markup leaves in the
|
|
133
|
+
* sentence (a `\cite`, a link, a `\ref`), or math, with its source.
|
|
134
|
+
*/
|
|
135
|
+
export type ProsePiece =
|
|
136
|
+
| { readonly kind: "text"; readonly segment: Segment }
|
|
137
|
+
| { readonly kind: "owner"; readonly owner: Owner; readonly span: Span }
|
|
138
|
+
| { readonly kind: "math"; readonly tex: string; readonly span: Span };
|
|
139
|
+
|
|
140
|
+
/** A stretch of body prose no sentence crosses — a paragraph, a list item, a footnote. Never empty. */
|
|
141
|
+
export interface Passage {
|
|
142
|
+
readonly pieces: readonly [ProsePiece, ...ProsePiece[]];
|
|
143
|
+
}
|
package/src/init.test.ts
CHANGED
|
@@ -21,8 +21,9 @@ import { tmpdir } from "node:os";
|
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { afterEach, describe, expect, it } from "vitest";
|
|
23
23
|
import { FRESH_CLONE_NOTE } from "./hooks-settings.ts";
|
|
24
|
+
import { load as yamlLoad } from "js-yaml";
|
|
25
|
+
import { z } from "zod";
|
|
24
26
|
import {
|
|
25
|
-
UNPINNED_REF,
|
|
26
27
|
WORKFLOW_PATH,
|
|
27
28
|
choosePapers,
|
|
28
29
|
declarePapers,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
reportSkillLinks,
|
|
34
35
|
reportTexLive,
|
|
35
36
|
reportWorkflow,
|
|
37
|
+
workflowYaml,
|
|
36
38
|
} from "./init.ts";
|
|
37
39
|
import { runNode } from "../test/support.ts";
|
|
38
40
|
|
|
@@ -353,30 +355,48 @@ describe("paperlint init — how several candidate directories were decided, sai
|
|
|
353
355
|
const here = (p: string) => `<${p}>`;
|
|
354
356
|
|
|
355
357
|
describe("paperlint init — the workflow section, whole", () => {
|
|
356
|
-
it("the workflow
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
it("the written workflow installs the package, then runs the action from node_modules", () => {
|
|
359
|
+
// Guards: one revision. The action comes from the lockfile-pinned install, so there is no
|
|
360
|
+
// `zernie/paperlint@<ref>` to drift from package.json, and no `paths` to repeat papersDir.
|
|
361
|
+
const Workflow = z.object({
|
|
362
|
+
jobs: z.object({
|
|
363
|
+
papers: z.object({
|
|
364
|
+
steps: z.array(z.record(z.string(), z.unknown())),
|
|
365
|
+
}),
|
|
366
|
+
}),
|
|
367
|
+
});
|
|
368
|
+
expect(Workflow.parse(yamlLoad(workflowYaml())).jobs.papers.steps).toEqual([
|
|
369
|
+
{ uses: "actions/checkout@v4" },
|
|
370
|
+
{ uses: "actions/setup-node@v4", with: { "node-version": 22 } },
|
|
371
|
+
{ run: "npm ci" },
|
|
372
|
+
{ uses: "./node_modules/paperlint" },
|
|
360
373
|
]);
|
|
361
|
-
|
|
362
|
-
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("the workflow report: written, kept, declined, not asked", () => {
|
|
377
|
+
const ctx = { why: "stdin is not a terminal" };
|
|
378
|
+
expect(reportWorkflow("written", ctx)).toEqual([
|
|
379
|
+
` ✓ wrote ${WORKFLOW_PATH}`,
|
|
363
380
|
]);
|
|
364
381
|
expect(reportWorkflow("kept", ctx)).toEqual([
|
|
365
382
|
` ✓ ${WORKFLOW_PATH} is already there — kept, nothing overwritten`,
|
|
366
383
|
]);
|
|
367
|
-
const
|
|
368
|
-
" to run the same checks in CI, add
|
|
369
|
-
|
|
384
|
+
const steps = [
|
|
385
|
+
" to run the same checks in CI, add these steps to a workflow:",
|
|
386
|
+
" - uses: actions/checkout@v4",
|
|
387
|
+
" - uses: actions/setup-node@v4",
|
|
370
388
|
" with:",
|
|
371
|
-
"
|
|
389
|
+
" node-version: 22",
|
|
390
|
+
" - run: npm ci",
|
|
391
|
+
" - uses: ./node_modules/paperlint",
|
|
372
392
|
];
|
|
373
393
|
expect(reportWorkflow("declined", ctx)).toEqual([
|
|
374
394
|
" · declined — nothing written",
|
|
375
|
-
...
|
|
395
|
+
...steps,
|
|
376
396
|
]);
|
|
377
397
|
expect(reportWorkflow("not-asked", ctx)).toEqual([
|
|
378
398
|
" · stdin is not a terminal, so nothing was asked. Default taken: NO file written.",
|
|
379
|
-
...
|
|
399
|
+
...steps,
|
|
380
400
|
]);
|
|
381
401
|
});
|
|
382
402
|
});
|
package/src/init.ts
CHANGED
|
@@ -55,7 +55,6 @@ import {
|
|
|
55
55
|
} from "./doctor.ts";
|
|
56
56
|
import { PAPER_MARKERS, papersIn } from "./build.ts";
|
|
57
57
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
58
|
-
import { actionRef } from "./action-ref.ts";
|
|
59
58
|
import {
|
|
60
59
|
FRESH_CLONE_NOTE,
|
|
61
60
|
SETTINGS_PATH,
|
|
@@ -270,29 +269,33 @@ export async function declarePapers(
|
|
|
270
269
|
|
|
271
270
|
export const WORKFLOW_PATH = join(".github", "workflows", "papers.yml");
|
|
272
271
|
|
|
273
|
-
/** Where the action is pinned when no release tag is known — obviously a placeholder. */
|
|
274
|
-
export const UNPINNED_REF = "<commit-sha>";
|
|
275
|
-
|
|
276
272
|
/**
|
|
277
|
-
* The CI
|
|
278
|
-
*
|
|
279
|
-
*
|
|
273
|
+
* The CI steps, one list for the file `init` writes and for the steps it prints. The action is
|
|
274
|
+
* called from the installed package, after `npm ci`: it and the CLI it runs are then one revision,
|
|
275
|
+
* set by the lockfile, and the action finds papersDir in paperlint.json itself.
|
|
280
276
|
*/
|
|
281
|
-
|
|
277
|
+
const CI_STEPS = [
|
|
278
|
+
"- uses: actions/checkout@v4",
|
|
279
|
+
"- uses: actions/setup-node@v4",
|
|
280
|
+
" with:",
|
|
281
|
+
" node-version: 22",
|
|
282
|
+
"- run: npm ci",
|
|
283
|
+
"- uses: ./node_modules/paperlint",
|
|
284
|
+
] as const;
|
|
285
|
+
|
|
286
|
+
const indent = (by: number) => (line: string) => `${" ".repeat(by)}${line}`;
|
|
287
|
+
|
|
288
|
+
/** The CI workflow `init` offers to write. */
|
|
289
|
+
export function workflowYaml(): string {
|
|
282
290
|
return [
|
|
283
|
-
|
|
284
|
-
? `# Written by \`paperlint init\`. Replace ${UNPINNED_REF} with a commit or release tag of the action.`
|
|
285
|
-
: `# Written by \`paperlint init\`, pinned to ${ref} — the release you installed.`,
|
|
291
|
+
`# Written by \`paperlint init\`.`,
|
|
286
292
|
`name: papers`,
|
|
287
293
|
`on: [push, pull_request]`,
|
|
288
294
|
`jobs:`,
|
|
289
295
|
` papers:`,
|
|
290
296
|
` runs-on: ubuntu-latest`,
|
|
291
297
|
` steps:`,
|
|
292
|
-
|
|
293
|
-
` - uses: zernie/paperlint@${ref ?? UNPINNED_REF}`,
|
|
294
|
-
` with:`,
|
|
295
|
-
` paths: ${papers}`,
|
|
298
|
+
...CI_STEPS.map(indent(6)),
|
|
296
299
|
``,
|
|
297
300
|
].join("\n");
|
|
298
301
|
}
|
|
@@ -301,16 +304,12 @@ export type WorkflowResult = "written" | "kept" | "declined" | "not-asked";
|
|
|
301
304
|
|
|
302
305
|
export async function offerWorkflow(
|
|
303
306
|
root: string,
|
|
304
|
-
papers: string,
|
|
305
307
|
{
|
|
306
308
|
ask,
|
|
307
309
|
interactive,
|
|
308
|
-
version,
|
|
309
310
|
}: {
|
|
310
311
|
ask?: (q: string) => Promise<string>;
|
|
311
312
|
interactive: boolean;
|
|
312
|
-
/** The running package's version; see `InitOptions.version`. */
|
|
313
|
-
version?: string | undefined;
|
|
314
313
|
},
|
|
315
314
|
): Promise<WorkflowResult> {
|
|
316
315
|
const path = join(root, WORKFLOW_PATH);
|
|
@@ -327,27 +326,17 @@ export async function offerWorkflow(
|
|
|
327
326
|
// No answer — an empty line, or a stream that ended — is the safe default, which is "no file".
|
|
328
327
|
if (answer !== "y" && answer !== "yes") return "declined";
|
|
329
328
|
mkdirSync(dirname(path), { recursive: true });
|
|
330
|
-
writeFileSync(path, workflowYaml(
|
|
329
|
+
writeFileSync(path, workflowYaml(), "utf8");
|
|
331
330
|
return "written";
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
/** What `init` says about the CI workflow — and, when none was written, the step to paste. */
|
|
335
334
|
export function reportWorkflow(
|
|
336
335
|
wf: WorkflowResult,
|
|
337
|
-
{
|
|
338
|
-
version,
|
|
339
|
-
papersDir,
|
|
340
|
-
why,
|
|
341
|
-
}: { version?: string | undefined; papersDir: string; why: string },
|
|
336
|
+
{ why }: { why: string },
|
|
342
337
|
): string[] {
|
|
343
|
-
const ref = actionRef(version);
|
|
344
338
|
const out: string[] = [];
|
|
345
|
-
if (wf === "written")
|
|
346
|
-
out.push(
|
|
347
|
-
ref === null
|
|
348
|
-
? ` ✓ wrote ${WORKFLOW_PATH} — pin ${UNPINNED_REF} before pushing it`
|
|
349
|
-
: ` ✓ wrote ${WORKFLOW_PATH}, pinned to ${ref}`,
|
|
350
|
-
);
|
|
339
|
+
if (wf === "written") out.push(` ✓ wrote ${WORKFLOW_PATH}`);
|
|
351
340
|
else if (wf === "kept")
|
|
352
341
|
out.push(
|
|
353
342
|
` ✓ ${WORKFLOW_PATH} is already there — kept, nothing overwritten`,
|
|
@@ -359,10 +348,8 @@ export function reportWorkflow(
|
|
|
359
348
|
);
|
|
360
349
|
if (wf !== "written" && wf !== "kept")
|
|
361
350
|
out.push(
|
|
362
|
-
` to run the same checks in CI, add
|
|
363
|
-
|
|
364
|
-
` with:`,
|
|
365
|
-
` paths: ${papersDir}`,
|
|
351
|
+
` to run the same checks in CI, add these steps to a workflow:`,
|
|
352
|
+
...CI_STEPS.map(indent(8)),
|
|
366
353
|
);
|
|
367
354
|
return out;
|
|
368
355
|
}
|
|
@@ -603,11 +590,6 @@ export interface InitOptions {
|
|
|
603
590
|
resolveCliPapers?: (root: string) => string | null;
|
|
604
591
|
/** Links the skills. Injected only so a test can stand in for the installed package. */
|
|
605
592
|
link?: (root: string) => LinkReport;
|
|
606
|
-
/**
|
|
607
|
-
* The version of the running package, read by the CLI from its own `package.json`. The CI
|
|
608
|
-
* workflow is pinned to its release tag (`actionRef`); absent or unreleased, the placeholder.
|
|
609
|
-
*/
|
|
610
|
-
version?: string | undefined;
|
|
611
593
|
/**
|
|
612
594
|
* TeX Live for `paperlint build`: whether paperlint's own tree is installed, and how to install
|
|
613
595
|
* it (`paperlint toolchain`). Passed in by the CLI; without them the step only names the command.
|
|
@@ -734,7 +716,6 @@ export async function init(
|
|
|
734
716
|
run = spawnSync,
|
|
735
717
|
resolveCliPapers,
|
|
736
718
|
link = (r: string) => linkSkills(r),
|
|
737
|
-
version,
|
|
738
719
|
} = opts;
|
|
739
720
|
// An injected `interactive` is a test standing in for a terminal; its reason is the classic one.
|
|
740
721
|
const { interactive, why } =
|
|
@@ -816,12 +797,8 @@ export async function init(
|
|
|
816
797
|
// ── 5. the one expensive, unguessable thing ───────────────────────────────────────────
|
|
817
798
|
log(``);
|
|
818
799
|
log(`CI`);
|
|
819
|
-
const wf = await offerWorkflow(root,
|
|
820
|
-
|
|
821
|
-
interactive,
|
|
822
|
-
version,
|
|
823
|
-
});
|
|
824
|
-
for (const line of reportWorkflow(wf, { version, papersDir, why })) log(line);
|
|
800
|
+
const wf = await offerWorkflow(root, { ask, interactive });
|
|
801
|
+
for (const line of reportWorkflow(wf, { why })) log(line);
|
|
825
802
|
|
|
826
803
|
// ── 6. a first paper — offered only where there is none, and only to a human ──────────
|
|
827
804
|
log(``);
|
package/src/linted-set.test.ts
CHANGED
|
@@ -26,7 +26,11 @@ afterEach(() => {
|
|
|
26
26
|
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// The body is partly in `sections/intro.tex`, brought in by `\input` in the document; the preamble
|
|
30
|
+
// `\input`s a macro file and paperlint's own `paper-guards`. Only the body file is the paper.
|
|
31
|
+
const TEX =
|
|
32
|
+
"\\documentclass{article}\n\\input{macros}\n\\input{paper-guards}\n" +
|
|
33
|
+
"\\begin{document}x\n\\input{sections/intro}\n\\end{document}\n";
|
|
30
34
|
const JS = "export const x = 1;\n";
|
|
31
35
|
|
|
32
36
|
/** The files paperlint owns in the fixture paper. */
|
|
@@ -35,6 +39,7 @@ const OWNED = [
|
|
|
35
39
|
"papers/a/PIPELINE-STATUS.md",
|
|
36
40
|
"papers/a/reviews/r1.md",
|
|
37
41
|
"papers/a/siblings/smith2025.md",
|
|
42
|
+
"papers/a/sections/intro.tex",
|
|
38
43
|
];
|
|
39
44
|
|
|
40
45
|
/** Everything else a real paper folder and project carry. None of it may be linted. */
|
|
@@ -46,6 +51,10 @@ const NEIGHBOURS = {
|
|
|
46
51
|
"papers/a/repro/lib/y.mjs": JS,
|
|
47
52
|
"papers/a/repro/z.cjs": "module.exports = 1;\n",
|
|
48
53
|
"papers/a/repro/t.ts": "const t: number = 1;\n",
|
|
54
|
+
// A preamble include, a frozen version and a .tex nothing includes: none is the paper's body.
|
|
55
|
+
"papers/a/macros.tex": "\\newcommand{\\x}{y}\n",
|
|
56
|
+
"papers/a/versions/2026-01-01-submitted.tex": TEX,
|
|
57
|
+
"papers/a/notes/scratch.tex": "Scratch.\n",
|
|
49
58
|
"script.js": JS,
|
|
50
59
|
"node_modules/pkg/index.js": JS,
|
|
51
60
|
};
|
|
@@ -75,6 +84,7 @@ function project(): string {
|
|
|
75
84
|
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
76
85
|
"papers/a/reviews/r1.md": "# review\n",
|
|
77
86
|
"papers/a/siblings/smith2025.md": "---\nread: full\n---\n# Smith 2025\n",
|
|
87
|
+
"papers/a/sections/intro.tex": "Intro.\n",
|
|
78
88
|
...NEIGHBOURS,
|
|
79
89
|
};
|
|
80
90
|
for (const [p, text] of Object.entries(all)) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - the scaffold is what `paperlint lint` ACCEPTS: the old first run opened with "missing
|
|
6
6
|
* PIPELINE-STATUS.md", and a scaffold that still produced a finding would move that error
|
|
7
7
|
* from a missing file into a created one;
|
|
8
|
-
* - the scorecard carries
|
|
8
|
+
* - the scorecard carries NO `stages` — a new paper has shipped nothing,
|
|
9
9
|
* and a placeholder stage would be a red `paper/stages` on the very first run;
|
|
10
10
|
* - the scorecard carries the `**Readiness verdict:**` line the status hook surfaces on every
|
|
11
11
|
* paper edit — without it the hook reports that the paper cannot say whether it is ready;
|
|
@@ -104,10 +104,6 @@ try {
|
|
|
104
104
|
"🔴 the scorecard has NO `stages` — a new paper has shipped nothing and owes nothing",
|
|
105
105
|
!("stages" in fm),
|
|
106
106
|
);
|
|
107
|
-
check(
|
|
108
|
-
"and carries `researchQuestion`, present and empty",
|
|
109
|
-
"researchQuestion" in fm && fm.researchQuestion === "",
|
|
110
|
-
);
|
|
111
107
|
check(
|
|
112
108
|
"the name is filled in, and no placeholder survives in any file",
|
|
113
109
|
/PIPELINE-STATUS — demo/.test(status) &&
|
package/src/package-dirs.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* presets/ the venue presets (`paperlint:<name>`) and their JSON Schema
|
|
7
7
|
* presets/tex/ the LaTeX inputs `paperlint build` puts on TEXINPUTS
|
|
8
|
+
*
|
|
9
|
+
* `texSearchPath` is where an `\input` is looked for, in order. The build hands it to TeX and the
|
|
10
|
+
* lint rules read includes through it, so the two cannot disagree about which file a paper means.
|
|
8
11
|
*/
|
|
9
12
|
import { fileURLToPath } from "node:url";
|
|
10
13
|
|
|
@@ -15,3 +18,9 @@ export const presetsDir = (): string =>
|
|
|
15
18
|
/** The LaTeX inputs a paper `\input`s by name (`paper-guards`, a venue's numbers). */
|
|
16
19
|
export const texInputsDir = (): string =>
|
|
17
20
|
fileURLToPath(new URL("../presets/tex", import.meta.url));
|
|
21
|
+
|
|
22
|
+
/** Where an `\input` in the paper in `paperDir` is looked for, first to last. */
|
|
23
|
+
export const texSearchPath = (paperDir: string): readonly string[] => [
|
|
24
|
+
paperDir,
|
|
25
|
+
texInputsDir(),
|
|
26
|
+
];
|
package/src/paper-files.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*
|
|
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
|
-
* `stages`/`source`/`author-list` beside `PIPELINE-STATUS.md` and `
|
|
20
|
-
* `
|
|
19
|
+
* `stages`/`source`/`author-list` beside `PIPELINE-STATUS.md` and `typography` beside
|
|
20
|
+
* `paper.md`/`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
|