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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AN ESLint LANGUAGE FOR `.tex` — so that
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* AN ESLint LANGUAGE FOR `.tex` — so that prose rules read a paper's LaTeX source. The spike it
|
|
3
|
+
* grew out of lives in the source corpus as a coverage measurement plus an executable prototype
|
|
4
|
+
* kept as history.
|
|
5
5
|
*
|
|
6
6
|
* ── CONSTRUCTION: two legs ──────────────────────────────────────────────────
|
|
7
7
|
* 1. AST — a flat mdast-like root (heading / code / html / strong) with REAL positions taken
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
* 2. TEXT — a projection of `.tex` into a markdown-like view THAT PRESERVES LENGTHS AND
|
|
10
10
|
* OFFSETS: comments, the preamble, math, service macros and code environments are blanked
|
|
11
11
|
* with spaces; `\section{X}` projects to `## X `, `\texttt{X}` to `` `X` ``.
|
|
12
|
-
* The point: a rule
|
|
13
|
-
*
|
|
14
|
-
* rewritten for LaTeX.
|
|
12
|
+
* The point: a rule reads a plain, heading-marked text and KNOWS NOTHING about LaTeX, while
|
|
13
|
+
* the position of a finding points into the real `.tex`.
|
|
15
14
|
*
|
|
16
15
|
* ── HEADING SYNTHESIS: not decoration, but the repair of seven silent rules ──
|
|
17
16
|
* Measured 2026-08-26 across three real papers: NONE of them has a `Limitations` /
|
|
@@ -47,10 +46,8 @@
|
|
|
47
46
|
* `95 of it, costing 5 and A B`. This matters: a word splitter counts `%` as part of
|
|
48
47
|
* a word, and a plain-language rule counts NUMBERS — so the percent sign detaches from
|
|
49
48
|
* its number. Not fixed: three exceptions would have to be written and pinned.
|
|
50
|
-
* 3.
|
|
51
|
-
*
|
|
52
|
-
* blanking and 1208 after — a 2.5% discrepancy. The discrepancy is ACCEPTED deliberately;
|
|
53
|
-
* the thresholds were not recalibrated.
|
|
49
|
+
* 3. MACRO BLANKING SHIFTS WORD COUNTS. The same place gave 1239 words before macro blanking
|
|
50
|
+
* and 1208 after — a 2.5% discrepancy, ACCEPTED deliberately.
|
|
54
51
|
* 4. ONE FILE AT A TIME. `\input` / `\include` are in OPAQUE: the projection of `paper.tex`
|
|
55
52
|
* does not hold what they pull in. `paperlint lint` hands ESLint each file the paper includes
|
|
56
53
|
* from its body as a file of its own (src/paper-includes.ts), and a file with no `document`
|
|
@@ -546,9 +543,9 @@ export function texToMdast(
|
|
|
546
543
|
// 🔴 AND SYMMETRICALLY ON THE LEFT (found by a run on 2026-08-27, not by reading). A
|
|
547
544
|
// comment that follows a line of text directly, rather than a blank line, comes back from
|
|
548
545
|
// unified-latex with a position starting at the PREVIOUS newline instead of at `%`.
|
|
549
|
-
// Consumers recognise a comment by its first character (
|
|
550
|
-
//
|
|
551
|
-
//
|
|
546
|
+
// Consumers recognise a comment by its first character (`%`), so such a node was not a
|
|
547
|
+
// comment to them: notes above bold appendix lead-ins never bound, and the block-note /
|
|
548
|
+
// block-verdict rules could NEVER fire. Measured:
|
|
552
549
|
// 2 out of 2 blocks were reported as "no note AT ALL" while the notes were right there.
|
|
553
550
|
let cs = pos.start.offset;
|
|
554
551
|
while (cs < ce && src[cs] !== "%") cs++;
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* them under `settings.paperlint.paper`
|
|
9
9
|
* for `paper.tex` and for every file it includes. A rule reads the other files from disk.
|
|
10
10
|
*
|
|
11
|
-
* No setting — a file linted outside `paperlint lint
|
|
12
|
-
*
|
|
11
|
+
* No setting — a file linted outside `paperlint lint` — means a paper of one file: every rule
|
|
12
|
+
* decides on the file it was handed, as it did before.
|
|
13
13
|
*/
|
|
14
14
|
import { readFileSync } from "node:fs";
|
|
15
15
|
import { z } from "zod";
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { describe, expect, it } from "vitest";
|
|
13
13
|
import { ESLint } from "eslint";
|
|
14
|
-
import markdown from "@eslint/markdown";
|
|
15
14
|
import { readFileSync } from "node:fs";
|
|
16
15
|
import { dirname, join } from "node:path";
|
|
17
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -49,16 +48,6 @@ const eslint = (fix = false) =>
|
|
|
49
48
|
linterOptions: { reportUnusedDisableDirectives: "error" },
|
|
50
49
|
rules: RULES,
|
|
51
50
|
},
|
|
52
|
-
{
|
|
53
|
-
files: ["**/*.md"],
|
|
54
|
-
plugins: { markdown, paper: typography },
|
|
55
|
-
language: "markdown/gfm",
|
|
56
|
-
languageOptions: { frontmatter: "yaml" },
|
|
57
|
-
rules: {
|
|
58
|
-
"paper/section-word": "warn",
|
|
59
|
-
"paper/leading-zero": "warn",
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
51
|
],
|
|
63
52
|
});
|
|
64
53
|
|
|
@@ -164,14 +153,6 @@ describe("paper/section-word", () => {
|
|
|
164
153
|
"\\documentclass{article}\n\\crefname{section}{§}{§§}\n\\begin{document}\nSee §5.\n\\end{document}\n";
|
|
165
154
|
expect(ids(await lint(src), "paper/section-word")).toHaveLength(1);
|
|
166
155
|
});
|
|
167
|
-
|
|
168
|
-
it("markdown: `§5` → `Section 5`, code untouched", async () => {
|
|
169
|
-
const src = "See §5 and `§3` here.\n";
|
|
170
|
-
expect(ids(await lint(src, "paper.md"), "paper/section-word")).toHaveLength(
|
|
171
|
-
1,
|
|
172
|
-
);
|
|
173
|
-
expect(await fixed(src, "paper.md")).toBe("See Section 5 and `§3` here.\n");
|
|
174
|
-
});
|
|
175
156
|
});
|
|
176
157
|
|
|
177
158
|
describe("paper/leading-zero", () => {
|
|
@@ -237,17 +218,6 @@ describe("paper/leading-zero", () => {
|
|
|
237
218
|
doc("At $p < 0.05$ and $d=0.21$; cell & 0.037 \\\\ and .5 or 0.25."),
|
|
238
219
|
);
|
|
239
220
|
});
|
|
240
|
-
|
|
241
|
-
it("markdown: prose and a table cell are fixed, code is not", async () => {
|
|
242
|
-
const src =
|
|
243
|
-
"Significant at p<.05.\n\n| task | p |\n| --- | --- |\n| bugfix | .002 |\n\nRun `.25`.\n";
|
|
244
|
-
expect(ids(await lint(src, "paper.md"), "paper/leading-zero")).toHaveLength(
|
|
245
|
-
2,
|
|
246
|
-
);
|
|
247
|
-
expect(await fixed(src, "paper.md")).toBe(
|
|
248
|
-
"Significant at p<0.05.\n\n| task | p |\n| --- | --- |\n| bugfix | 0.002 |\n\nRun `.25`.\n",
|
|
249
|
-
);
|
|
250
|
-
});
|
|
251
221
|
});
|
|
252
222
|
|
|
253
223
|
describe("a finding after a heading written over several lines", () => {
|
|
@@ -290,15 +260,11 @@ describe("paper/leading-zero — a number after ¶ or § is not a decimal (#102)
|
|
|
290
260
|
expect(await fixed(doc(CITE))).toBe(doc(CITE));
|
|
291
261
|
});
|
|
292
262
|
|
|
293
|
-
it("…while a real decimal still fixes,
|
|
263
|
+
it("…while a real decimal still fixes, even beside a cited paragraph", async () => {
|
|
294
264
|
const body = `${CITE}\n\nWe use a threshold of .05 throughout.`;
|
|
295
265
|
expect(await fixed(doc(body))).toBe(
|
|
296
266
|
doc(`${CITE}\n\nWe use a threshold of 0.05 throughout.`),
|
|
297
267
|
);
|
|
298
|
-
const md = "See ¶¶.42 and .44.\n\nSignificant at p<.05.\n";
|
|
299
|
-
expect(await fixed(md, "paper.md")).toBe(
|
|
300
|
-
"See ¶¶.42 and .44.\n\nSignificant at p<0.05.\n",
|
|
301
|
-
);
|
|
302
268
|
});
|
|
303
269
|
});
|
|
304
270
|
|
|
@@ -467,48 +433,6 @@ describe("where visible text comes from — the walk's other paths", () => {
|
|
|
467
433
|
});
|
|
468
434
|
});
|
|
469
435
|
|
|
470
|
-
describe("markdown: offsets that cannot be followed, and rules that are LaTeX-only", () => {
|
|
471
|
-
const mdLint = async (text, rules) => {
|
|
472
|
-
const [res] = await new ESLint({
|
|
473
|
-
cwd: FIX,
|
|
474
|
-
overrideConfigFile: true,
|
|
475
|
-
overrideConfig: [
|
|
476
|
-
{
|
|
477
|
-
files: ["**/*.md"],
|
|
478
|
-
plugins: { markdown, paper: typography },
|
|
479
|
-
language: "markdown/gfm",
|
|
480
|
-
rules,
|
|
481
|
-
},
|
|
482
|
-
],
|
|
483
|
-
}).lintText(text, { filePath: join(FIX, "inline", "paper.md") });
|
|
484
|
-
expect(res.messages.filter((m) => m.fatal)).toEqual([]);
|
|
485
|
-
return res;
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
it("`§` before prose is reported without a fix", async () => {
|
|
489
|
-
const src = "As § above.\n";
|
|
490
|
-
const res = await mdLint(src, { "paper/section-word": "warn" });
|
|
491
|
-
expect(res.messages.map((m) => [m.ruleId, m.column, m.fix])).toEqual([
|
|
492
|
-
["paper/section-word", 4, undefined],
|
|
493
|
-
]);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
it("a sign or a dot written as a character reference has no source offset: nothing is reported", async () => {
|
|
497
|
-
const res = await mdLint("See § 5 and .25.\n", {
|
|
498
|
-
"paper/section-word": "warn",
|
|
499
|
-
"paper/leading-zero": "warn",
|
|
500
|
-
});
|
|
501
|
-
expect(res.messages).toEqual([]);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
it("figure-ref-style is LaTeX-only: silent on markdown", async () => {
|
|
505
|
-
const res = await mdLint("Figure~\\ref{a} and Fig.~\\ref{b}.\n", {
|
|
506
|
-
"paper/figure-ref-style": "warn",
|
|
507
|
-
});
|
|
508
|
-
expect(res.messages).toEqual([]);
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
|
|
512
436
|
describe("a source code the rules did not expect — the defaults hold", () => {
|
|
513
437
|
const reports = (ruleId, sourceCode) => {
|
|
514
438
|
const out = [];
|
|
@@ -530,12 +454,20 @@ describe("a source code the rules did not expect — the defaults hold", () => {
|
|
|
530
454
|
]);
|
|
531
455
|
});
|
|
532
456
|
|
|
533
|
-
it("
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
457
|
+
it.each(["section-word", "leading-zero", "figure-ref-style"])(
|
|
458
|
+
"%s reads LaTeX only: a source without `raw` (any other language) is not reported",
|
|
459
|
+
(ruleId) => {
|
|
460
|
+
const ast = {
|
|
461
|
+
type: "root",
|
|
462
|
+
children: [
|
|
463
|
+
{
|
|
464
|
+
type: "text",
|
|
465
|
+
value: "§5 and .25",
|
|
466
|
+
position: { start: { offset: 0 }, end: { offset: 10 } },
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
};
|
|
470
|
+
expect(reports(ruleId, { text: "§5 and .25", ast })).toEqual([]);
|
|
471
|
+
},
|
|
472
|
+
);
|
|
541
473
|
});
|
|
@@ -41,11 +41,9 @@
|
|
|
41
41
|
* IEEE Editorial Style Manual ("0.25, not .25") require the zero. A regex over the raw source
|
|
42
42
|
* found 22 decimals on a real corpus and none was a defect: every one sat in markup (an option,
|
|
43
43
|
* a column spec, a comment, a listing, a tikz coordinate). So this walks the TREE and keeps only
|
|
44
|
-
* what the reader sees as a number:
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* comments, verbatim/listings/`\lstinline`, tikz, the preamble, the bibliography.
|
|
48
|
-
* Markdown text nodes, table cells included. Not: code, inline code, html, front matter.
|
|
44
|
+
* what the reader sees as a number: prose, inline and display math, table cells, and the text
|
|
45
|
+
* arguments of a known set of text macros. Not: any other macro's arguments, environment
|
|
46
|
+
* arguments, comments, verbatim/listings/`\lstinline`, tikz, the preamble, the bibliography.
|
|
49
47
|
* Each character of a run keeps its source offset, so the finding points at the dot and the fix
|
|
50
48
|
* inserts the zero there. The arXiv lookbehind stays: in `2310.05736` the dot follows a digit.
|
|
51
49
|
* A text macro outside the known set (`\hl{.05}`) is read as a parameter and NOT reported — a
|
|
@@ -62,7 +60,6 @@
|
|
|
62
60
|
import assert from "node:assert/strict";
|
|
63
61
|
import { getParser } from "@unified-latex/unified-latex-util-parse";
|
|
64
62
|
import { takeWhile } from "remeda";
|
|
65
|
-
import type { Nodes } from "mdast";
|
|
66
63
|
import {
|
|
67
64
|
texToMdast,
|
|
68
65
|
type TexArg,
|
|
@@ -108,7 +105,7 @@ export function bibRange(text: string): BibRange | null {
|
|
|
108
105
|
return { start: m.index, end: m.index + whole.length, bodyStart, body };
|
|
109
106
|
}
|
|
110
107
|
|
|
111
|
-
/** A node of
|
|
108
|
+
/** A node of the `.tex` language's projection, as far as its place goes. */
|
|
112
109
|
interface ProjectedNode {
|
|
113
110
|
readonly type: string;
|
|
114
111
|
readonly position?:
|
|
@@ -372,66 +369,6 @@ export function texVisibleRuns(
|
|
|
372
369
|
return texRuns(document ? document.content : root, false, raw, []);
|
|
373
370
|
}
|
|
374
371
|
|
|
375
|
-
// Markdown node types that are not prose.
|
|
376
|
-
const MD_HIDDEN = new Set([
|
|
377
|
-
"code",
|
|
378
|
-
"inlineCode",
|
|
379
|
-
"html",
|
|
380
|
-
"yaml",
|
|
381
|
-
"toml",
|
|
382
|
-
"math",
|
|
383
|
-
"inlineMath",
|
|
384
|
-
]);
|
|
385
|
-
/**
|
|
386
|
-
* The source offset of each character of a text node's value. The value is the source with
|
|
387
|
-
* markdown's escapes (`\*`) and character references (`<`) resolved, so the two are walked
|
|
388
|
-
* side by side; a character whose source cannot be followed gets null (reported, not fixed).
|
|
389
|
-
*/
|
|
390
|
-
function mdOffsets(value: string, src: string, start: number) {
|
|
391
|
-
const offs: (number | null)[] = [];
|
|
392
|
-
let j = start;
|
|
393
|
-
for (const ch of value) {
|
|
394
|
-
if (src[j] === ch) offs.push(j++);
|
|
395
|
-
else if (src[j] === "\\" && src[j + 1] === ch) {
|
|
396
|
-
offs.push(j + 1);
|
|
397
|
-
j += 2;
|
|
398
|
-
} else if (src[j] === "&" && /^&#?\w{1,8};/.test(src.slice(j, j + 10))) {
|
|
399
|
-
offs.push(null);
|
|
400
|
-
j = src.indexOf(";", j) + 1;
|
|
401
|
-
} else {
|
|
402
|
-
offs.push(null);
|
|
403
|
-
j++;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return offs;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function mdVisibleRuns(
|
|
410
|
-
node: Nodes | undefined,
|
|
411
|
-
src: string,
|
|
412
|
-
out: Run[] = [],
|
|
413
|
-
): Run[] {
|
|
414
|
-
if (!node || MD_HIDDEN.has(node.type)) return out;
|
|
415
|
-
if (node.type === "text") {
|
|
416
|
-
const start = node.position?.start.offset;
|
|
417
|
-
out.push({
|
|
418
|
-
text: node.value,
|
|
419
|
-
offs:
|
|
420
|
-
typeof start === "number"
|
|
421
|
-
? mdOffsets(node.value, src, start)
|
|
422
|
-
: Array.from(node.value, () => null),
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
for (const c of "children" in node ? node.children : [])
|
|
426
|
-
mdVisibleRuns(c, src, out);
|
|
427
|
-
return out;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const visibleRuns = (sourceCode: RuleSourceCode): Run[] =>
|
|
431
|
-
isTex(sourceCode)
|
|
432
|
-
? texVisibleRuns(sourceCode.raw)
|
|
433
|
-
: mdVisibleRuns(sourceCode.ast, sourceCode.text);
|
|
434
|
-
|
|
435
372
|
/** A report at `[from, to)` of the file. */
|
|
436
373
|
const at = (
|
|
437
374
|
context: RuleContext,
|
|
@@ -455,36 +392,22 @@ const SECTION_GLYPH = /§/g;
|
|
|
455
392
|
|
|
456
393
|
function sectionWord(context: RuleContext) {
|
|
457
394
|
const sc = context.sourceCode;
|
|
458
|
-
if (isTex(sc))
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
// A glyph before neither a reference nor a number: reported, not rewritten — there is no
|
|
470
|
-
// single right word for a doubled sign or a sign before prose.
|
|
471
|
-
for (const m of markupMatches(sc, SECTION_GLYPH))
|
|
472
|
-
if (!seen.has(m.index))
|
|
473
|
-
at(context, m.index, m.index + 1, { messageId: "sign" });
|
|
474
|
-
return;
|
|
395
|
+
if (!isTex(sc)) return;
|
|
396
|
+
const seen = new Set();
|
|
397
|
+
for (const m of markupMatches(sc, SECTION_TEX)) {
|
|
398
|
+
seen.add(m.index);
|
|
399
|
+
const word = m[3] ? "Section~" : "Section ";
|
|
400
|
+
const range: [number, number] = [m.index, m.index + m[0].length];
|
|
401
|
+
at(context, range[0], range[1], {
|
|
402
|
+
messageId: "sign",
|
|
403
|
+
fix: (f) => f.replaceTextRange(range, word),
|
|
404
|
+
});
|
|
475
405
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const [, gap = ""] = m;
|
|
482
|
-
const range: [number, number] = [from, from + 1 + gap.length];
|
|
483
|
-
at(context, range[0], range[1], {
|
|
484
|
-
messageId: "sign",
|
|
485
|
-
...(m[2] ? { fix: (f) => f.replaceTextRange(range, "Section ") } : {}),
|
|
486
|
-
});
|
|
487
|
-
}
|
|
406
|
+
// A glyph before neither a reference nor a number: reported, not rewritten — there is no
|
|
407
|
+
// single right word for a doubled sign or a sign before prose.
|
|
408
|
+
for (const m of markupMatches(sc, SECTION_GLYPH))
|
|
409
|
+
if (!seen.has(m.index))
|
|
410
|
+
at(context, m.index, m.index + 1, { messageId: "sign" });
|
|
488
411
|
}
|
|
489
412
|
|
|
490
413
|
// ── paper/leading-zero ─────────────────────────────────────────────────────────────────
|
|
@@ -508,8 +431,9 @@ const paragraphBefore = (src: string, i: number) => {
|
|
|
508
431
|
|
|
509
432
|
function leadingZero(context: RuleContext) {
|
|
510
433
|
const sc = context.sourceCode;
|
|
511
|
-
|
|
512
|
-
|
|
434
|
+
if (!isTex(sc)) return;
|
|
435
|
+
const src = sc.raw;
|
|
436
|
+
for (const r of texVisibleRuns(src))
|
|
513
437
|
for (const m of r.text.matchAll(BARE_DECIMAL)) {
|
|
514
438
|
const dot = r.offs[m.index];
|
|
515
439
|
if (dot === null || dot === undefined) continue;
|
|
@@ -170,7 +170,7 @@ for (const bad of ["", null, 0, false, [], {}, 42])
|
|
|
170
170
|
const names = Object.keys(sets).sort();
|
|
171
171
|
assert.deepEqual(
|
|
172
172
|
names,
|
|
173
|
-
["all", "
|
|
173
|
+
["all", "pdfFacts", "refFacts", "status", "tex", "venue"],
|
|
174
174
|
"the glob set changed — a consumer's config enumerates these by name, and a renamed or " +
|
|
175
175
|
"dropped key is a block that silently lints nothing",
|
|
176
176
|
);
|
package/eslint-rules/papers.ts
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
*
|
|
42
42
|
* A FILENAME MARKER, no root at all (`**\/paper.tex`).
|
|
43
43
|
* Measured on the first consumer: `paper.tex` ×4 with one outside the corpus,
|
|
44
|
-
* `PIPELINE-STATUS.md` ×6 with two of them test fixtures
|
|
44
|
+
* `PIPELINE-STATUS.md` ×6 with two of them test fixtures.
|
|
45
45
|
* The guard that would consume this is a `deny` at `error` severity; a denial on writing
|
|
46
46
|
* a fixture gets the hook switched off the same day.
|
|
47
47
|
*
|
|
@@ -134,15 +134,13 @@ export function papersRoot(
|
|
|
134
134
|
* The glob sets a consumer declares its blocks on, derived from one root.
|
|
135
135
|
*
|
|
136
136
|
* 🔴 THE SHAPES LIVE HERE, NOT IN THE CONSUMER, and that is the half that keeps a rule from
|
|
137
|
-
* going blind later. `paper.
|
|
137
|
+
* going blind later. `paper.tex`, `_build/*.facts.json`, `PIPELINE-STATUS.md` are
|
|
138
138
|
* this pipeline's CONVENTION; if a rule starts reading a new file, the glob for it must arrive
|
|
139
139
|
* with the rule, in the same package, rather than as a line every consumer has to add by hand
|
|
140
140
|
* to keep up. A hand-maintained list of paths is the thing that rots, and rots silently.
|
|
141
141
|
*/
|
|
142
142
|
export function paperFiles(root: string) {
|
|
143
143
|
return {
|
|
144
|
-
/** Markdown drafts — the two names a paper's prose is allowed to have. */
|
|
145
|
-
md: [`${root}/*/paper.md`, `${root}/*/draft.md`],
|
|
146
144
|
/** The LaTeX source of a paper (not its figures, not its vendored drafts). */
|
|
147
145
|
tex: [`${root}/*/paper.tex`],
|
|
148
146
|
/** The per-paper stage ledger. */
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
* (`284:will be released`, `284:Upon acceptance` + `285:will be made publicly available`).
|
|
36
36
|
*
|
|
37
37
|
* 🔴 AND ONE DISAGREEMENT, WHICH IS NOT IN THE LOGIC BUT IN THE INPUT — and it is the whole
|
|
38
|
-
* gain. The old implementation picked its file by
|
|
39
|
-
*
|
|
38
|
+
* gain. The old implementation picked its file by taking the FIRST `.tex` in `readdirSync`
|
|
39
|
+
* order. Measured 2026-09-07:
|
|
40
40
|
* paper A → a 536-BYTE file of nine `\def`s with venue numbers. The check, added
|
|
41
41
|
* specifically BECAUSE of paper A, was not reading paper A;
|
|
42
42
|
* paper B → a four-month-old draft, one of seven `.tex` files in that directory;
|
|
@@ -3,27 +3,11 @@
|
|
|
3
3
|
* how the prose is written.
|
|
4
4
|
*
|
|
5
5
|
* ── WHY ITS OWN NAMESPACE, AND NOT `paper/*` ────────────────────────────────
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `paper/future-promise: "off"` on markdown would be an exemption justified by "there is no
|
|
12
|
-
* subject", and that same parity assertion requires the rule to be ALIVE on the other side —
|
|
13
|
-
* i.e. the machinery exists but is built for the opposite direction (off on `.tex`, alive on
|
|
14
|
-
* `.md`).
|
|
15
|
-
*
|
|
16
|
-
* Hence a namespace of its own, the same way an external word-counter gets one: it has a word
|
|
17
|
-
* count markdown does not have, we have a preamble markdown does not have. Where the config
|
|
18
|
-
* enumerates non-`paper/*` rules on `.tex`, the list is CLOSED and `tex/future-promise` is
|
|
19
|
-
* named explicitly, so that a new namespace arrives by decision rather than as a side effect
|
|
20
|
-
* of editing a config.
|
|
21
|
-
*
|
|
22
|
-
* ⚠️ THE MEASUREMENT THIS DECISION RESTS ON (source corpus, 2026-09-06 → 2026-09-07): had the
|
|
23
|
-
* rule been declared on `paper.md` it would produce ZERO there TODAY — not one match of
|
|
24
|
-
* `FUTURE_PROMISE_RE` in the markdown draft of the one paper that has one. So the choice is
|
|
25
|
-
* about semantics, not noise: markdown has no input distinguishing a draft from a shipped
|
|
26
|
-
* build, and the rule would fire on a draft's honest promise.
|
|
6
|
+
* The subject here is the BUILD MODE (`\documentclass[review]`, `printacmref=false`), not the
|
|
7
|
+
* prose the `paper/*` rules read. Hence a namespace of its own, the same way an external
|
|
8
|
+
* word-counter gets one. Where the config enumerates non-`paper/*` rules on `.tex`, the list is
|
|
9
|
+
* CLOSED and `tex/future-promise` is named explicitly, so that a new namespace arrives by
|
|
10
|
+
* decision rather than as a side effect of editing a config.
|
|
27
11
|
*
|
|
28
12
|
* ── THE RULE READS THE FILE FROM DISK, NOT THE PROJECTION. This is load-bearing ──
|
|
29
13
|
* The LaTeX language in `latex-language.mjs` hands rules a PROJECTION of `.tex` into a
|
|
@@ -37,9 +21,8 @@
|
|
|
37
21
|
* run (and in CI) there is no such difference.
|
|
38
22
|
*
|
|
39
23
|
* ── WHAT THE MOVE TO A LINT RULE FIXED BESIDES THE FORM (measured 2026-09-07) ──
|
|
40
|
-
* 🔴 The previous implementation, a script, picked its input like this:
|
|
41
|
-
* `
|
|
42
|
-
* FIRST `.tex` in directory order. Measured across the three papers of the source corpus:
|
|
24
|
+
* 🔴 The previous implementation, a script, picked its input like this:
|
|
25
|
+
* `readdirSync(dir).find((f) => f.endsWith(".tex"))` — the FIRST `.tex` in directory order. Measured across the three papers of the source corpus:
|
|
43
26
|
* paper A → a 536-BYTE file holding nine `\def`s with venue numbers. The check, added
|
|
44
27
|
* specifically BECAUSE of paper A, was not reading paper A;
|
|
45
28
|
* paper B → a four-month-old draft, one of SEVEN `.tex` files in that directory (the real
|
|
@@ -121,7 +104,7 @@ const FRONTMATTER_OVERRIDES = [
|
|
|
121
104
|
// is looking.
|
|
122
105
|
//
|
|
123
106
|
// The scope is DELIBERATELY NARROW, because a noisy gate is a silenced gate:
|
|
124
|
-
// - `.tex` only
|
|
107
|
+
// - `.tex` only: the build mode is read from the preamble;
|
|
125
108
|
// - a review-mode build is EXEMPT, and that is the whole point: during peer review
|
|
126
109
|
// "at camera-ready" is a true and ordinary promise. The defect is only a promise that
|
|
127
110
|
// outlived its own delivery;
|
package/eslint.config.mjs
CHANGED
|
@@ -520,7 +520,6 @@ const TYPESCRIPT_ONLY = [
|
|
|
520
520
|
"test/e2e/**/*.{js,mjs,cjs}",
|
|
521
521
|
"test/fixtures/layers/src/**/*.{js,mjs,cjs}",
|
|
522
522
|
"fixtures/accepted-papers/**/*.{js,mjs,cjs}",
|
|
523
|
-
"fixtures/real-markdown-paper/**/*.{js,mjs,cjs}",
|
|
524
523
|
"skills/paper-pipeline/scripts/fixtures/**/*.{js,mjs,cjs}",
|
|
525
524
|
"skills/plan-paper-timeline/fixtures/**/*.{js,mjs,cjs}",
|
|
526
525
|
];
|
package/hooks/hooks.harness.ts
CHANGED
|
@@ -206,7 +206,7 @@ try {
|
|
|
206
206
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
207
207
|
{
|
|
208
208
|
const dir = fixture({ [PAPERS_DIR_FIELD]: "docs/papers" });
|
|
209
|
-
const P = "docs/papers/alpha/paper.
|
|
209
|
+
const P = "docs/papers/alpha/paper.tex";
|
|
210
210
|
const T = "docs/papers/alpha/paper.tex";
|
|
211
211
|
const deny = (label: string, cmd: string) => {
|
|
212
212
|
const r = at(dir, "paper-edit-guard", onBash(cmd));
|
|
@@ -228,7 +228,7 @@ try {
|
|
|
228
228
|
};
|
|
229
229
|
|
|
230
230
|
// The two cases `touches` alone MISSES: a redirection target is not an argv token.
|
|
231
|
-
deny("a redirect write to paper.
|
|
231
|
+
deny("a redirect write to paper.tex", `echo x ${GT} ${P}`);
|
|
232
232
|
deny("an append write to a .tex", `cat /tmp/a ${GT}${GT} ${T}`);
|
|
233
233
|
deny("sed -i on a paper", `sed -i s/a/b/ ${P}`);
|
|
234
234
|
deny("cp onto a paper", `cp /tmp/a ${P}`);
|
|
@@ -358,8 +358,8 @@ try {
|
|
|
358
358
|
// for a gate that had never read the declaration. Caught by its own negative control,
|
|
359
359
|
// 2026-09-12. A declared root only demonstrates anything when the default would MISS it.
|
|
360
360
|
const DECLARED = "writing/drafts";
|
|
361
|
-
const declaredWrite = onBash(`echo x ${GT} ${DECLARED}/alpha/paper.
|
|
362
|
-
const defaultWrite = onBash(`echo x ${GT} papers/alpha/paper.
|
|
361
|
+
const declaredWrite = onBash(`echo x ${GT} ${DECLARED}/alpha/paper.tex`);
|
|
362
|
+
const defaultWrite = onBash(`echo x ${GT} papers/alpha/paper.tex`);
|
|
363
363
|
|
|
364
364
|
// (a) a declared root is used AS DECLARED — and the default is NOT.
|
|
365
365
|
{
|
|
@@ -479,11 +479,11 @@ try {
|
|
|
479
479
|
r.stdout.length === 0 && r.stderr.length === 0 && r.exitCode === 0,
|
|
480
480
|
);
|
|
481
481
|
};
|
|
482
|
-
lands("a relative paper.
|
|
482
|
+
lands("a relative paper.tex", "docs/papers/alpha/paper.tex");
|
|
483
483
|
lands("a .tex", "docs/papers/alpha/paper.tex");
|
|
484
484
|
// 🔴 THE ABSOLUTE SPELLING. The live harness sends absolute paths; a helper that only builds
|
|
485
485
|
// relative ones let an anchored predecessor pass every test while being dead in production.
|
|
486
|
-
lands("an ABSOLUTE paper.
|
|
486
|
+
lands("an ABSOLUTE paper.tex", join(dir, "docs/papers/alpha/paper.tex"));
|
|
487
487
|
silent("a README under the papers root", "docs/papers/README.md");
|
|
488
488
|
silent("a file outside the papers root", "README.md");
|
|
489
489
|
// The advisory's own asymmetry: it goes quiet where the gate refuses.
|
|
@@ -495,7 +495,7 @@ try {
|
|
|
495
495
|
const r = at(
|
|
496
496
|
broken,
|
|
497
497
|
"paper-skills-nudge",
|
|
498
|
-
onEdit("docs/papers/alpha/paper.
|
|
498
|
+
onEdit("docs/papers/alpha/paper.tex"),
|
|
499
499
|
);
|
|
500
500
|
check(
|
|
501
501
|
"nudge is SILENT (not blocking) on an unusable declaration",
|
|
@@ -508,7 +508,7 @@ try {
|
|
|
508
508
|
const d = at(
|
|
509
509
|
broken,
|
|
510
510
|
"paper-skills-nudge",
|
|
511
|
-
onEdit("papers/alpha/paper.
|
|
511
|
+
onEdit("papers/alpha/paper.tex"),
|
|
512
512
|
);
|
|
513
513
|
check(
|
|
514
514
|
"nudge: an explicit null is NOT read as an absence (no fallback to the default root)",
|
|
@@ -537,15 +537,15 @@ try {
|
|
|
537
537
|
r.stdout.length === 0 && r.stderr.length === 0 && r.exitCode === 0,
|
|
538
538
|
);
|
|
539
539
|
};
|
|
540
|
-
fires("a relative paper.
|
|
541
|
-
fires("an ABSOLUTE paper.
|
|
540
|
+
fires("a relative paper.tex", "docs/papers/alpha/paper.tex");
|
|
541
|
+
fires("an ABSOLUTE paper.tex", join(dir, "docs/papers/alpha/paper.tex"));
|
|
542
542
|
silent("a README under the papers root", "docs/papers/README.md");
|
|
543
543
|
silent("a file outside the papers root", "README.md");
|
|
544
544
|
// 🔴 THE BOUNDARY IN `(?:^|/)`: a directory merely ENDING in the root's last segment must
|
|
545
545
|
// not smuggle a match. Without the boundary this is a hit.
|
|
546
546
|
silent(
|
|
547
547
|
"a look-alike root (`notdocs/papers/…`)",
|
|
548
|
-
"notdocs/papers/alpha/paper.
|
|
548
|
+
"notdocs/papers/alpha/paper.tex",
|
|
549
549
|
);
|
|
550
550
|
// A `.tex` whose directory name carries a shell metacharacter cannot reach a command.
|
|
551
551
|
silent(
|
|
@@ -560,8 +560,8 @@ try {
|
|
|
560
560
|
);
|
|
561
561
|
check(
|
|
562
562
|
"gates: an explicit null is NOT read as an absence (no fallback to the default root)",
|
|
563
|
-
at(broken, "paper-status-gates", onEdit("papers/alpha/paper.
|
|
564
|
-
.length === 0,
|
|
563
|
+
at(broken, "paper-status-gates", onEdit("papers/alpha/paper.tex"))
|
|
564
|
+
.stderr.length === 0,
|
|
565
565
|
);
|
|
566
566
|
}
|
|
567
567
|
// The declared root is regex-ESCAPED: `.` in a root must not act as a wildcard.
|
|
@@ -576,7 +576,7 @@ try {
|
|
|
576
576
|
at(
|
|
577
577
|
dotted,
|
|
578
578
|
"paper-status-gates",
|
|
579
|
-
onEdit("docs.v2/papers/alpha/paper.
|
|
579
|
+
onEdit("docs.v2/papers/alpha/paper.tex"),
|
|
580
580
|
).stderr,
|
|
581
581
|
),
|
|
582
582
|
);
|
|
@@ -585,7 +585,7 @@ try {
|
|
|
585
585
|
at(
|
|
586
586
|
dotted,
|
|
587
587
|
"paper-status-gates",
|
|
588
|
-
onEdit("docsXv2/papers/alpha/paper.
|
|
588
|
+
onEdit("docsXv2/papers/alpha/paper.tex"),
|
|
589
589
|
).stderr.length === 0,
|
|
590
590
|
);
|
|
591
591
|
}
|
|
@@ -186,7 +186,7 @@ export const papersRoot = (rawConfig) => {
|
|
|
186
186
|
/**
|
|
187
187
|
* Redirection targets (`> f`, `>> f`, `1> f`, `&> f`), which `touches` cannot see.
|
|
188
188
|
*
|
|
189
|
-
* MEASURED, not assumed: `leafCommandsNormalized("echo x > p/x/paper.
|
|
189
|
+
* MEASURED, not assumed: `leafCommandsNormalized("echo x > p/x/paper.tex")` returns
|
|
190
190
|
* `[{head:"echo", argv:["echo","x"], args:["x"], …}]` — the redirection and its target are
|
|
191
191
|
* DROPPED by the parser, so they appear in no field of any leaf. The closed vocabulary
|
|
192
192
|
* (`runs` / `touches` / `pipesToShell` / `isSideEffecting`) has no way to ask what a command
|
|
@@ -196,9 +196,9 @@ export const papersRoot = (rawConfig) => {
|
|
|
196
196
|
* 🔴 THE SCAN IS QUOTE-AWARE, and the version before it was not. It stripped quoted spans
|
|
197
197
|
* wholesale, so a quoted redirection target vanished with them. MEASURED on this guard:
|
|
198
198
|
*
|
|
199
|
-
* echo x > <root>/x/paper.
|
|
200
|
-
* echo x > "<root>/x/paper.
|
|
201
|
-
* echo x > '<root>/x/paper.
|
|
199
|
+
* echo x > <root>/x/paper.tex → blocked
|
|
200
|
+
* echo x > "<root>/x/paper.tex" → ALLOWED
|
|
201
|
+
* echo x > '<root>/x/paper.tex' → ALLOWED
|
|
202
202
|
*
|
|
203
203
|
* Stripping was not merely careless: a `>` INSIDE quotes is not a redirection at all, so the
|
|
204
204
|
* guard has to know about quoting or it fires on any command that merely QUOTES a paper write —
|
|
@@ -376,10 +376,7 @@ const runsMutator = (cmd) => MUTATORS.some((m) => cmd.runs(m));
|
|
|
376
376
|
*/
|
|
377
377
|
const isPaperSource = (p) => !isFrozenSnapshot(p) && hasSourceExtension(p);
|
|
378
378
|
|
|
379
|
-
const hasSourceExtension = (p) =>
|
|
380
|
-
p.endsWith(".tex") ||
|
|
381
|
-
/\/(paper|draft)\.md$/.test(p) ||
|
|
382
|
-
/^(paper|draft)\.md$/.test(p);
|
|
379
|
+
const hasSourceExtension = (p) => p.endsWith(".tex");
|
|
383
380
|
|
|
384
381
|
/**
|
|
385
382
|
* A FROZEN SNAPSHOT — a file under a paper's `versions/`, which is the same carve-out as the
|