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
|
@@ -15,7 +15,7 @@ const dir = hookConsumer(useTempDir("paper-edit-guard-"), {
|
|
|
15
15
|
});
|
|
16
16
|
/** `>` built rather than typed: a literal one here would be a redirection to the guard's own scan. */
|
|
17
17
|
const GT = String.fromCharCode(62);
|
|
18
|
-
const P = "papers/alpha/paper.
|
|
18
|
+
const P = "papers/alpha/paper.tex";
|
|
19
19
|
|
|
20
20
|
/** The guard's exit code for one Bash command: 2 denies, 0 lets it through. */
|
|
21
21
|
const guard = (command: string): number =>
|
|
@@ -35,8 +35,8 @@ test("a `>` inside quotes, after an escaped quote, or escaped itself is not a re
|
|
|
35
35
|
test("a redirection target is read through backslashes, quoted or not, and ends at a delimiter", () => {
|
|
36
36
|
assert.deepEqual(
|
|
37
37
|
[
|
|
38
|
-
guard(`echo x ${GT} "papers/alpha/pa\\per.
|
|
39
|
-
guard(`echo x ${GT} papers/alpha/pa\\per.
|
|
38
|
+
guard(`echo x ${GT} "papers/alpha/pa\\per.tex"`),
|
|
39
|
+
guard(`echo x ${GT} papers/alpha/pa\\per.tex`),
|
|
40
40
|
guard(`echo x ${GT}${P}; true`),
|
|
41
41
|
],
|
|
42
42
|
[2, 2, 2],
|
|
@@ -55,3 +55,12 @@ test("a command cut off right after a backslash still names its target, quoted o
|
|
|
55
55
|
[2, 2],
|
|
56
56
|
);
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
test("only paper.tex is a paper source: copying over another file of the paper is let through", () => {
|
|
60
|
+
// Guards: the extension filter of the argv leg. (A redirection is refused for ANY file under the
|
|
61
|
+
// papers root, so the argv leg is the one that says which files are sources.)
|
|
62
|
+
assert.deepEqual(
|
|
63
|
+
[guard("cp /tmp/x papers/alpha/notes.txt"), guard(`cp /tmp/x ${P}`)],
|
|
64
|
+
[0, 2],
|
|
65
|
+
);
|
|
66
|
+
});
|
|
@@ -96,9 +96,8 @@ const papersRoot = (rawConfig) => {
|
|
|
96
96
|
return root;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
/** A paper SOURCE — a `.tex
|
|
100
|
-
const isPaperSource = (p) =>
|
|
101
|
-
p.endsWith(".tex") || /\/(paper|draft)\.md$/.test(p);
|
|
99
|
+
/** A paper SOURCE — a `.tex`. Not every note or README. */
|
|
100
|
+
const isPaperSource = (p) => p.endsWith(".tex");
|
|
102
101
|
|
|
103
102
|
const CHECKLIST = `📄 Editing a paper — drive it with the \`paper-pipeline\` skill, don't wing the review.
|
|
104
103
|
→ Update <paper-dir>/PIPELINE-STATUS.md: mark the row for any stage you (re)ran, and read its
|
|
@@ -10,11 +10,11 @@ import { useTempDir } from "../test/support.ts";
|
|
|
10
10
|
|
|
11
11
|
const root = useTempDir("paper-skills-nudge-");
|
|
12
12
|
|
|
13
|
-
test("no paperlint.json: an edit to papers/<p>/paper.
|
|
13
|
+
test("no paperlint.json: an edit to papers/<p>/paper.tex gets the checklist", () => {
|
|
14
14
|
const dir = hookConsumer(join(root, "default"));
|
|
15
15
|
const r = runShippedHook(
|
|
16
16
|
"paper-skills-nudge",
|
|
17
|
-
onEdit("papers/alpha/paper.
|
|
17
|
+
onEdit("papers/alpha/paper.tex"),
|
|
18
18
|
dir,
|
|
19
19
|
);
|
|
20
20
|
assert.equal(r.exitCode, 0);
|
|
@@ -27,7 +27,17 @@ test("a paperlint.json that does not parse: silent, exit 0", () => {
|
|
|
27
27
|
});
|
|
28
28
|
const r = runShippedHook(
|
|
29
29
|
"paper-skills-nudge",
|
|
30
|
-
onEdit("papers/alpha/paper.
|
|
30
|
+
onEdit("papers/alpha/paper.tex"),
|
|
31
|
+
dir,
|
|
32
|
+
);
|
|
33
|
+
assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("an edit to a file of the paper other than its source: silent", () => {
|
|
37
|
+
const dir = hookConsumer(join(root, "not-a-source"));
|
|
38
|
+
const r = runShippedHook(
|
|
39
|
+
"paper-skills-nudge",
|
|
40
|
+
onEdit("papers/alpha/notes.txt"),
|
|
31
41
|
dir,
|
|
32
42
|
);
|
|
33
43
|
assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* harness's own edit helper builds a RELATIVE path. The live harness sends an ABSOLUTE one.
|
|
32
32
|
* Measured against the real runtime:
|
|
33
33
|
*
|
|
34
|
-
* file_path "<root>/<paper>/paper.
|
|
34
|
+
* file_path "<root>/<paper>/paper.tex" → fires
|
|
35
35
|
* file_path "/abs/path/to/repo/<root>/<paper>/…" → SILENT
|
|
36
36
|
*
|
|
37
37
|
* So the hook would have been dead in production and green in the tests: precisely the
|
|
@@ -120,7 +120,7 @@ const papersRoot = (rawConfig) => {
|
|
|
120
120
|
*/
|
|
121
121
|
const paperSourceRe = (root) =>
|
|
122
122
|
new RegExp(
|
|
123
|
-
`(?:^|/)${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([A-Za-z0-9._-]+)/
|
|
123
|
+
`(?:^|/)${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([A-Za-z0-9._-]+)/[^/]*\\.tex$`,
|
|
124
124
|
);
|
|
125
125
|
|
|
126
126
|
/**
|
|
@@ -17,7 +17,7 @@ test("no paperlint.json: an edit under papers/ runs the gates for that paper", (
|
|
|
17
17
|
});
|
|
18
18
|
const r = runShippedHook(
|
|
19
19
|
"paper-status-gates",
|
|
20
|
-
onEdit("papers/alpha/paper.
|
|
20
|
+
onEdit("papers/alpha/paper.tex"),
|
|
21
21
|
dir,
|
|
22
22
|
);
|
|
23
23
|
assert.equal(r.exitCode, 0);
|
|
@@ -31,7 +31,19 @@ test("a paperlint.json that does not parse: silent, exit 0", () => {
|
|
|
31
31
|
});
|
|
32
32
|
const r = runShippedHook(
|
|
33
33
|
"paper-status-gates",
|
|
34
|
-
onEdit("papers/alpha/paper.
|
|
34
|
+
onEdit("papers/alpha/paper.tex"),
|
|
35
|
+
dir,
|
|
36
|
+
);
|
|
37
|
+
assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("an edit to a file of the paper other than its source: no gates run", () => {
|
|
41
|
+
const dir = hookConsumer(join(root, "not-a-source"), {
|
|
42
|
+
"papers/alpha/PIPELINE-STATUS.md": STATUS,
|
|
43
|
+
});
|
|
44
|
+
const r = runShippedHook(
|
|
45
|
+
"paper-status-gates",
|
|
46
|
+
onEdit("papers/alpha/notes.txt"),
|
|
35
47
|
dir,
|
|
36
48
|
);
|
|
37
49
|
assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
|
package/lib/markdown.test.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* markdown.mjs — the markup readers every script shares, on the shapes their callers rely on:
|
|
3
3
|
* frontmatter blanked (offsets kept) or cut, headings from the parser (not from a `#` inside a
|
|
4
|
-
* fence),
|
|
4
|
+
* fence), and fences. And the one thing a test run never
|
|
5
5
|
* sees by accident: markdown-it failing to resolve, where every reader must degrade to its
|
|
6
6
|
* documented empty value and `requireMarkdown` must refuse with exit 2.
|
|
7
7
|
*/
|
|
@@ -17,11 +17,8 @@ import {
|
|
|
17
17
|
frontmatterBlock,
|
|
18
18
|
headings,
|
|
19
19
|
requireMarkdown,
|
|
20
|
-
paragraphs,
|
|
21
|
-
splitSections,
|
|
22
20
|
stripFences,
|
|
23
21
|
stripFrontmatter,
|
|
24
|
-
tables,
|
|
25
22
|
} from "./markdown.ts";
|
|
26
23
|
|
|
27
24
|
const DOC = [
|
|
@@ -93,27 +90,6 @@ test("an empty ATX heading has empty text", () => {
|
|
|
93
90
|
);
|
|
94
91
|
});
|
|
95
92
|
|
|
96
|
-
test("splitSections: a preamble always first, then each heading of the level range", () => {
|
|
97
|
-
assert.deepEqual(
|
|
98
|
-
splitSections(DOC, { min: 2, max: 2 }).map((s) => [
|
|
99
|
-
s.heading?.text ?? null,
|
|
100
|
-
s.body.split("\n")[0],
|
|
101
|
-
]),
|
|
102
|
-
[
|
|
103
|
-
[null, "---"],
|
|
104
|
-
["One", "Body one."],
|
|
105
|
-
["Two", "last"],
|
|
106
|
-
],
|
|
107
|
-
);
|
|
108
|
-
assert.deepEqual(
|
|
109
|
-
splitSections("## A").map((s) => [s.heading?.text ?? null, s.body]),
|
|
110
|
-
[
|
|
111
|
-
[null, ""],
|
|
112
|
-
["A", ""],
|
|
113
|
-
],
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
93
|
test("fences and indented code, as content; stripped as lines, or blanked in place", () => {
|
|
118
94
|
const src = "a\n```\nx\n```\n\n y\nb";
|
|
119
95
|
assert.deepEqual(
|
|
@@ -126,30 +102,6 @@ test("fences and indented code, as content; stripped as lines, or blanked in pla
|
|
|
126
102
|
);
|
|
127
103
|
});
|
|
128
104
|
|
|
129
|
-
test("paragraphs are line ranges; tables are rows of cells, the alignment row skipped", () => {
|
|
130
|
-
assert.deepEqual(
|
|
131
|
-
{ paragraphs: paragraphs(DOC), tables: tables(DOC) },
|
|
132
|
-
{
|
|
133
|
-
paragraphs: [
|
|
134
|
-
{ line: 6, endLine: 7 },
|
|
135
|
-
{ line: 9, endLine: 10 },
|
|
136
|
-
{ line: 13, endLine: 14 },
|
|
137
|
-
{ line: 27, endLine: 28 },
|
|
138
|
-
],
|
|
139
|
-
tables: [
|
|
140
|
-
{
|
|
141
|
-
line: 22,
|
|
142
|
-
endLine: 25,
|
|
143
|
-
rows: [
|
|
144
|
-
["a", "b"],
|
|
145
|
-
["1", "2"],
|
|
146
|
-
],
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
105
|
const root = useTempDir("markdown-no-parser-");
|
|
154
106
|
writeTree(root, {
|
|
155
107
|
// Registered with --import: resolving `markdown-it` fails, as it does with no node_modules.
|
|
@@ -159,7 +111,7 @@ writeTree(root, {
|
|
|
159
111
|
"probe.mjs":
|
|
160
112
|
`const m = await import(${JSON.stringify(join(dirname(fileURLToPath(import.meta.url)), "markdown.ts"))});\n` +
|
|
161
113
|
"const t = '# H\\n\\n```\\nx\\n```\\n';\n" +
|
|
162
|
-
"console.log(JSON.stringify([m.MD_AVAILABLE(), m.headings(t), m.fences(t), m.stripFences(t)
|
|
114
|
+
"console.log(JSON.stringify([m.MD_AVAILABLE(), m.headings(t), m.fences(t), m.stripFences(t)]));\n" +
|
|
163
115
|
"m.requireMarkdown();\n" +
|
|
164
116
|
"console.log('not reached');\n",
|
|
165
117
|
"probe-plugin.mjs":
|
|
@@ -188,22 +140,15 @@ test("with no markdown-it, every reader degrades to its empty value and requireM
|
|
|
188
140
|
});
|
|
189
141
|
assert.deepEqual(r, {
|
|
190
142
|
status: 2,
|
|
191
|
-
stdout:
|
|
192
|
-
JSON.stringify([false, [], [], "# H\n\n```\nx\n```\n", [], []]) + "\n",
|
|
143
|
+
stdout: JSON.stringify([false, [], [], "# H\n\n```\nx\n```\n"]) + "\n",
|
|
193
144
|
stderr:
|
|
194
145
|
"markdown-it does not resolve — this script parses markup with a parser and without it would produce a confident zero instead of an error.\n" +
|
|
195
146
|
" Cured by `npm i` at the repo root.\n",
|
|
196
147
|
});
|
|
197
148
|
});
|
|
198
149
|
|
|
199
|
-
test("
|
|
200
|
-
assert.
|
|
201
|
-
[
|
|
202
|
-
splitSections("just text").map((s) => [s.heading, s.body]),
|
|
203
|
-
requireMarkdown(),
|
|
204
|
-
],
|
|
205
|
-
[[[null, "just text"]], undefined],
|
|
206
|
-
);
|
|
150
|
+
test("requireMarkdown is silent with a parser", () => {
|
|
151
|
+
assert.equal(requireMarkdown(), undefined);
|
|
207
152
|
});
|
|
208
153
|
|
|
209
154
|
test("headings read a token stream that breaks markdown-it's shape as empty text and offset 0", () => {
|
package/lib/markdown.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* markdown.mjs — markup parsing for EVERYTHING in this repo that reads markdown.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* would be a lie about the direction of the dependency.
|
|
4
|
+
* Its consumers read the package's own markdown — skills' `SKILL.md` frontmatter
|
|
5
|
+
* (`skill-corpus.mjs`), fenced commands — and a consumer's linters of its own files.
|
|
6
|
+
* Hence the home is here and not in a linter core: importing "the linter core" from a
|
|
7
|
+
* skill script would be a lie about the direction of the dependency.
|
|
9
8
|
*
|
|
10
9
|
* CHECKS NOTHING AND PRINTS NOTHING: text in, data out. What to do with a missing
|
|
11
10
|
* parser is the caller's decision, and the decisions DIFFER:
|
|
@@ -43,13 +42,6 @@ export interface Heading {
|
|
|
43
42
|
readonly offset: number;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
/** A chunk of the document: the heading it starts with (`null` for the preamble), with and without it. */
|
|
47
|
-
export interface Section {
|
|
48
|
-
readonly heading: Heading | null;
|
|
49
|
-
readonly raw: string;
|
|
50
|
-
readonly body: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
45
|
/** A block tokenizer with markdown-it's shape — the default, or one a test passes. */
|
|
54
46
|
export type Parse = (text: string) => readonly Token[];
|
|
55
47
|
|
|
@@ -169,56 +161,6 @@ export function headings(
|
|
|
169
161
|
return out;
|
|
170
162
|
}
|
|
171
163
|
|
|
172
|
-
/**
|
|
173
|
-
* Slicing the document into chunks "a heading + everything up to the next heading FROM
|
|
174
|
-
* THE SAME RANGE of levels". → `[{ heading, raw, body }]`.
|
|
175
|
-
*
|
|
176
|
-
* This is exactly what four scripts did as `split(/^(?=## )/m)` and
|
|
177
|
-
* `split(/^(?=#{2,3} )/m)`. The shape of the expression lied about the intent: "two or
|
|
178
|
-
* three hashes" is about CHARACTERS, while what is needed is the LEVEL, and `#{2,3}`
|
|
179
|
-
* also caught a hash inside a ```-block, that is, it cut a quotation from someone
|
|
180
|
-
* else's paper as a section of its own.
|
|
181
|
-
*
|
|
182
|
-
* `heading` — `null` on the zeroth element (the preamble before the first heading). It
|
|
183
|
-
* is ALWAYS there, including empty: the former `split` also always returned the chunk
|
|
184
|
-
* before the first separator as its zeroth element, and callers rely on that
|
|
185
|
-
* (`sub === chunk` in `artifact-coverage` meant "there are no subsections").
|
|
186
|
-
*
|
|
187
|
-
* `raw` — WITH the heading line (it is used to count `§` references and bold numbers,
|
|
188
|
-
* which occur in the heading itself too), `body` — without it (it is used to count words).
|
|
189
|
-
*
|
|
190
|
-
* 2026-08-11: moved here rather than copied into every script, because the chunk
|
|
191
|
-
* boundary is the one place where an off-by-one is easy, and such a mistake has already
|
|
192
|
-
* happened (`recordBlock` in `skill-corpus.mjs` returned a single "#" character for all
|
|
193
|
-
* 21 skills).
|
|
194
|
-
*/
|
|
195
|
-
export function splitSections(
|
|
196
|
-
text: string,
|
|
197
|
-
{ min = 2, max = 6 }: { min?: number; max?: number } = {},
|
|
198
|
-
): Section[] {
|
|
199
|
-
const hs = headings(text).filter((h) => h.depth >= min && h.depth <= max);
|
|
200
|
-
const out: Section[] = [];
|
|
201
|
-
const [first] = hs;
|
|
202
|
-
const preEnd = first ? first.offset : text.length;
|
|
203
|
-
out.push({
|
|
204
|
-
heading: null,
|
|
205
|
-
raw: text.slice(0, preEnd),
|
|
206
|
-
body: text.slice(0, preEnd),
|
|
207
|
-
});
|
|
208
|
-
for (const [i, h] of hs.entries()) {
|
|
209
|
-
const next = hs[i + 1];
|
|
210
|
-
const end = next ? next.offset : text.length;
|
|
211
|
-
const nl = text.indexOf("\n", h.offset);
|
|
212
|
-
const bodyStart = nl === -1 || nl + 1 > end ? end : nl + 1;
|
|
213
|
-
out.push({
|
|
214
|
-
heading: h,
|
|
215
|
-
raw: text.slice(h.offset, end),
|
|
216
|
-
body: text.slice(bodyStart, end),
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
return out;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
164
|
/**
|
|
223
165
|
* The text without the contents of ```-blocks (the fence lines themselves go too).
|
|
224
166
|
*
|
|
@@ -229,14 +171,11 @@ export function splitSections(
|
|
|
229
171
|
* `blank: true` — the block's lines are NOT deleted but REPLACED with empty ones. The
|
|
230
172
|
* difference is not cosmetic: deleting the lines GLUES the paragraph before the block to
|
|
231
173
|
* the paragraph after it, and a code block is a block boundary — on the other side a
|
|
232
|
-
* different paragraph starts. Measured 2026-08-11 on
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* claims-per-sentence counter — the threshold at which the file emits findings. The
|
|
238
|
-
* former regex preserved the boundary (it left one newline on each side), so `blank` is
|
|
239
|
-
* also a preservation of the former behaviour where that behaviour was right.
|
|
174
|
+
* different paragraph starts. Measured 2026-08-11 on a README: a sentence counter counted
|
|
175
|
+
* 82 sentences there, and after the lines were deleted it counted 81 — the lead paragraph
|
|
176
|
+
* "**Reproduction commands:**" merged with the text AFTER the block into one sentence.
|
|
177
|
+
* The former regex preserved the boundary (it left one newline on each side), so `blank`
|
|
178
|
+
* is also a preservation of the former behaviour where that behaviour was right.
|
|
240
179
|
*/
|
|
241
180
|
/**
|
|
242
181
|
* The CONTENTS of fenced blocks — the other side of `stripFences`.
|
|
@@ -304,69 +243,3 @@ export function requireMarkdown({
|
|
|
304
243
|
);
|
|
305
244
|
process.exit(2);
|
|
306
245
|
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* GFM tables — rows, cells and the line range, WITH THE PARSER.
|
|
310
|
-
*
|
|
311
|
-
* The only consumer as of 2026-08-20 is `crosspost-article`, which needs to re-lay a
|
|
312
|
-
* table into a monospace block for a platform without tables (Medium). It lives here
|
|
313
|
-
* and not in the skill, by the rule "markup is parsed with a parser, and in one place":
|
|
314
|
-
* with a regex a table is told apart from a line of text with pipes only by its
|
|
315
|
-
* neighbours, and that is exactly the class that has already cost this repo 27 places.
|
|
316
|
-
*
|
|
317
|
-
* Returns `{ line, endLine, rows }`, where `rows[0]` is the header; the alignment row
|
|
318
|
-
* (`| --- |`) is thrown away, because in monospace output it has nothing to express.
|
|
319
|
-
*/
|
|
320
|
-
/**
|
|
321
|
-
* The document's paragraphs — `paragraph_open` line ranges, WITH THE PARSER.
|
|
322
|
-
* `[{ line, endLine }]`.
|
|
323
|
-
*
|
|
324
|
-
* The only consumer as of 2026-08-21 is `crosspost-article`: the blog's sources are
|
|
325
|
-
* hard-wrapped at a column (an editorial habit, not markdown), and the platforms render
|
|
326
|
-
* a single `\n` inside a paragraph DIFFERENTLY — dev.to (Redcarpet) keeps it as a
|
|
327
|
-
* literal line break, and the paragraph arrives torn into short lines exactly at the
|
|
328
|
-
* source file's boundaries. Medium has no such defect not because the platform is more
|
|
329
|
-
* honest, but because the paste goes through "Import a story" — the importer takes the
|
|
330
|
-
* ALREADY RENDERED page of the site (where the breaks were collapsed by the browser long
|
|
331
|
-
* ago), not the raw markdown.
|
|
332
|
-
*
|
|
333
|
-
* The range is EXACTLY the one `t.map` gives on `paragraph_open` — that is, a code
|
|
334
|
-
* fence, a heading, a table, a list (in a TIGHT list the item is not wrapped in a
|
|
335
|
-
* `paragraph_open` at all, it has no paragraph of its own) fall outside this function
|
|
336
|
-
* automatically, by the construction of the parser, and not by a separate line-type
|
|
337
|
-
* check.
|
|
338
|
-
*/
|
|
339
|
-
export function paragraphs(text: string): { line: number; endLine: number }[] {
|
|
340
|
-
if (md === null) return [];
|
|
341
|
-
return md
|
|
342
|
-
.parse(blankFrontmatter(text), {})
|
|
343
|
-
.flatMap((t) =>
|
|
344
|
-
t.type === "paragraph_open" && t.map
|
|
345
|
-
? [{ line: t.map[0], endLine: t.map[1] }]
|
|
346
|
-
: [],
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
export function tables(
|
|
351
|
-
text: string,
|
|
352
|
-
): { line: number; endLine: number; rows: string[][] }[] {
|
|
353
|
-
if (md === null) return [];
|
|
354
|
-
const lines = blankFrontmatter(text).split("\n");
|
|
355
|
-
const out: { line: number; endLine: number; rows: string[][] }[] = [];
|
|
356
|
-
for (const t of md.parse(blankFrontmatter(text), {})) {
|
|
357
|
-
if (t.type !== "table_open" || !t.map) continue;
|
|
358
|
-
const rows: string[][] = [];
|
|
359
|
-
for (const raw of lines.slice(t.map[0], t.map[1])) {
|
|
360
|
-
if (/^\s*\|?[\s:|-]+\|?\s*$/.test(raw) && raw.includes("-")) continue; // alignment
|
|
361
|
-
rows.push(
|
|
362
|
-
raw
|
|
363
|
-
.replace(/^\s*\|/, "")
|
|
364
|
-
.replace(/\|\s*$/, "")
|
|
365
|
-
.split("|")
|
|
366
|
-
.map((c) => c.trim()),
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
out.push({ line: t.map[0], endLine: t.map[1], rows });
|
|
370
|
-
}
|
|
371
|
-
return out;
|
|
372
|
-
}
|
package/lib/paper-config.ts
CHANGED
|
@@ -236,7 +236,10 @@ test("a Bash entry narrowed to the instructed script permits it, and one narrowe
|
|
|
236
236
|
await brokenBy("verify-citations", narrowTo("extract-ref-facts.mjs")),
|
|
237
237
|
null,
|
|
238
238
|
);
|
|
239
|
-
const lines = await brokenBy(
|
|
239
|
+
const lines = await brokenBy(
|
|
240
|
+
"verify-citations",
|
|
241
|
+
narrowTo("generated-code.mjs"),
|
|
242
|
+
);
|
|
240
243
|
assert.equal(
|
|
241
244
|
lines?.[0],
|
|
242
245
|
'2 assertion(s) failed for skill "verify-citations":',
|
|
@@ -10,54 +10,61 @@
|
|
|
10
10
|
* and the zero was an artifact of the empty directory, not a fact about the skill.
|
|
11
11
|
*
|
|
12
12
|
* Extracted from `paper-pipeline/pipeline-firing.eval.mjs`, which had it right
|
|
13
|
-
* from the start
|
|
13
|
+
* from the start; the `paper-pipeline/*.eval.mjs` experiments carry the same
|
|
14
|
+
* block, byte for byte.
|
|
14
15
|
*
|
|
15
16
|
* ⚠️ AND THE PRICE, so this file is not a one-sided story (measured 2026-08-12).
|
|
16
17
|
* With a fixture present the model OPENS THE PAPER and looks around, so the number
|
|
17
18
|
* stops being "did the DESCRIPTION fire" and becomes "did the model go investigate
|
|
18
19
|
* and then fire". A different quantity, and a costlier one: ~590k → ~780k tokens
|
|
19
|
-
* per skill (+190k) and 67s → 84s, while
|
|
20
|
-
* ≈ 384 tokens. The growth is reading, not input size (`cache`
|
|
20
|
+
* per skill (+190k) and 67s → 84s, while the fixture measured then was 1,537
|
|
21
|
+
* characters ≈ 384 tokens. The growth is reading, not input size (`cache`
|
|
22
|
+
* 470k → 652k).
|
|
21
23
|
* Cite the trigger numbers with that caveat attached — it is written out in
|
|
22
24
|
* `vigiles/repro/skill-trigger-2026-08-11/README.md` under "How to read".
|
|
23
25
|
*/
|
|
24
26
|
export const FIXTURE = {
|
|
25
|
-
"paper/paper.
|
|
26
|
-
"
|
|
27
|
+
"paper/paper.tex": [
|
|
28
|
+
"\\documentclass{article}",
|
|
29
|
+
"\\title{Prose Isn't Policy: Measuring Whether Agent-Config Rules Are Enforceable}",
|
|
30
|
+
"\\begin{document}",
|
|
31
|
+
"\\maketitle",
|
|
27
32
|
"",
|
|
28
|
-
"
|
|
33
|
+
"\\begin{abstract}",
|
|
29
34
|
"Agent configuration files state rules in prose and assume the model obeys them. We compile a",
|
|
30
|
-
"corpus of real rules and measure what fraction can be mechanically enforced. We find that 84
|
|
35
|
+
"corpus of real rules and measure what fraction can be mechanically enforced. We find that 84\\%",
|
|
31
36
|
"of rules in our corpus are enforceable, and that LLM-authored checkers for the remainder leak",
|
|
32
|
-
"silently in 84
|
|
37
|
+
"silently in 84--96\\% of adversarial cases~\\cite{greshake2023}.",
|
|
38
|
+
"\\end{abstract}",
|
|
33
39
|
"",
|
|
34
|
-
"
|
|
40
|
+
"\\section{Introduction}",
|
|
35
41
|
"Every agent harness ships a natural-language rulebook. Nothing checks it. This is the same",
|
|
36
|
-
"mistake as a code comment that claims an invariant no test enforces
|
|
42
|
+
"mistake as a code comment that claims an invariant no test enforces~\\cite{thompson1984}.",
|
|
37
43
|
"",
|
|
38
|
-
"
|
|
44
|
+
"\\section{Method}",
|
|
39
45
|
"We gather rules from public repositories, classify each by enforceability, and build a",
|
|
40
46
|
"two-stage adversarial gate that validates a synthesized rule against a blind gold set.",
|
|
41
47
|
"",
|
|
42
|
-
"
|
|
43
|
-
"See Table
|
|
48
|
+
"\\section{Results}",
|
|
49
|
+
"See Table~1. The headline number is 84\\%.",
|
|
44
50
|
"",
|
|
45
|
-
"
|
|
51
|
+
"\\section{Discussion}",
|
|
46
52
|
"The result generalizes beyond our corpus in the sense that the mechanism is not corpus-specific,",
|
|
47
53
|
"though of course the specific percentages are, and it is important to note in this context that",
|
|
48
54
|
"the framing itself may be what carries, rather than the measurement.",
|
|
49
55
|
"",
|
|
50
|
-
"
|
|
56
|
+
"\\section{Threats to Validity}",
|
|
51
57
|
"Our corpus is drawn from public repositories and may not represent private configurations.",
|
|
52
58
|
"",
|
|
53
|
-
"
|
|
59
|
+
"\\section{Related Work}",
|
|
54
60
|
"TODO",
|
|
55
61
|
"",
|
|
56
|
-
"
|
|
62
|
+
"\\section{Conclusion}",
|
|
57
63
|
"Prose is not policy.",
|
|
64
|
+
"\\end{document}",
|
|
58
65
|
].join("\n"),
|
|
59
66
|
"paper/repro/README.md":
|
|
60
|
-
"# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.
|
|
67
|
+
"# Reproduction artifact\n\n`python3 paper_numbers.py` recomputes every bolded figure in paper.tex.\n",
|
|
61
68
|
"paper/PIPELINE-STATUS.md":
|
|
62
69
|
"# Pipeline status\n\n| gate | state |\n|---|---|\n| numbers | pass |\n| structure | not run |\n| citations | not run |\n",
|
|
63
70
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperlint",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -114,8 +114,6 @@
|
|
|
114
114
|
"eslint.config.mjs",
|
|
115
115
|
"fixtures",
|
|
116
116
|
"!fixtures/accepted-papers",
|
|
117
|
-
"!fixtures/real-markdown-paper",
|
|
118
|
-
"fixtures/real-markdown-paper/baseline.*",
|
|
119
117
|
"hooks",
|
|
120
118
|
"lib",
|
|
121
119
|
"presets",
|
package/presets/realm.jsonc
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
//
|
|
73
73
|
// A gate on banal's number would give 13 against a limit of 8 and would redden on a paper that
|
|
74
74
|
// is within the limit. For an `error`-level rule that is the worst outcome. The limit stays
|
|
75
|
-
// owned by the counter inside `build-submission.sh`: it reads the
|
|
75
|
+
// owned by the counter inside `build-submission.sh`: it reads the paper's source and therefore
|
|
76
76
|
// KNOWS where the body ends, which the PDF does not.
|
|
77
77
|
//
|
|
78
78
|
// The numbers below are DATA for that counter and for humans, not an input to the rule. Source:
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"eslint-rules/pdf-last-page-balance.harness.mjs",
|
|
11
11
|
"eslint-rules/temp-root-realpath.harness.mjs",
|
|
12
12
|
"eslint-rules/tex-build.harness.mjs",
|
|
13
|
-
"fixtures/real-markdown-paper/real-markdown-paper.harness.ts",
|
|
14
13
|
"lib/agent-cli-version.harness.mjs",
|
|
15
14
|
"lib/paper-config.harness.mjs",
|
|
16
15
|
"lib/skill-trigger-cases.harness.mjs",
|
|
@@ -30,13 +29,9 @@
|
|
|
30
29
|
"skills/osf-artifact-upload/osf-artifact-upload.harness.ts",
|
|
31
30
|
"skills/paper-adversarial-review/paper-adversarial-review.harness.mjs",
|
|
32
31
|
"skills/paper-pipeline/paper-pipeline.harness.mjs",
|
|
33
|
-
"skills/paper-pipeline/scripts/artifact-coverage.harness.mjs",
|
|
34
32
|
"skills/paper-pipeline/scripts/consumer.harness.mjs",
|
|
35
33
|
"skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs",
|
|
36
34
|
"skills/paper-pipeline/scripts/generated-code.harness.mjs",
|
|
37
|
-
"skills/paper-pipeline/scripts/population-map.harness.mjs",
|
|
38
|
-
"skills/paper-pipeline/scripts/provenance.harness.mjs",
|
|
39
|
-
"skills/paper-pipeline/scripts/round-diff.harness.ts",
|
|
40
35
|
"skills/paper-status/paper-status.harness.mjs",
|
|
41
36
|
"skills/pc-panel-review/pc-panel-review.harness.mjs",
|
|
42
37
|
"skills/plan-paper-timeline/plan-paper-timeline.harness.mjs",
|
|
@@ -5,7 +5,7 @@ context: fork
|
|
|
5
5
|
allowed-tools: [Bash, Read, Agent]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
<!-- vigiles:sha256:
|
|
8
|
+
<!-- vigiles:sha256:c3a1f2644781eb6f compiled from skills/cold-read-diff/SKILL.md.spec.ts -->
|
|
9
9
|
|
|
10
10
|
# cold-read-diff — the reader who cannot fake understanding
|
|
11
11
|
|
|
@@ -55,7 +55,7 @@ one, and its confusion is the finding.
|
|
|
55
55
|
|
|
56
56
|
## How to run it
|
|
57
57
|
|
|
58
|
-
1. **Take the diff.** `git diff -U0 -- <paper.
|
|
58
|
+
1. **Take the diff.** `git diff -U0 -- <paper.tex>` since the last commit, or the `new_string` of the
|
|
59
59
|
edits just applied. Prose only — skip comments, tables, bibliography.
|
|
60
60
|
2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
|
|
61
61
|
changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
|
|
@@ -105,7 +105,7 @@ Two more things the same run proved:
|
|
|
105
105
|
- **Expand build-time macros first.** The reader saw seven raw `{{placeholders}}` and correctly
|
|
106
106
|
reported that every headline number was missing. That is a true statement about the extract and a
|
|
107
107
|
false one about the paper.
|
|
108
|
-
- **Strip working comments.** He graded
|
|
108
|
+
- **Strip working comments.** He graded `% JUSTIFY …` comment notes as prose because he could not tell
|
|
109
109
|
them apart — and said so, which is itself a finding about the extract.
|
|
110
110
|
|
|
111
111
|
**So:** take the changed lines, then widen each to its enclosing paragraph, expand macros, drop
|
|
@@ -60,7 +60,7 @@ one, and its confusion is the finding.
|
|
|
60
60
|
|
|
61
61
|
## How to run it
|
|
62
62
|
|
|
63
|
-
1. **Take the diff.** \`git diff -U0 -- <paper.
|
|
63
|
+
1. **Take the diff.** \`git diff -U0 -- <paper.tex>\` since the last commit, or the \`new_string\` of the
|
|
64
64
|
edits just applied. Prose only — skip comments, tables, bibliography.
|
|
65
65
|
2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
|
|
66
66
|
changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
|
|
@@ -110,7 +110,7 @@ Two more things the same run proved:
|
|
|
110
110
|
- **Expand build-time macros first.** The reader saw seven raw \`{{placeholders}}\` and correctly
|
|
111
111
|
reported that every headline number was missing. That is a true statement about the extract and a
|
|
112
112
|
false one about the paper.
|
|
113
|
-
- **Strip working comments.** He graded
|
|
113
|
+
- **Strip working comments.** He graded \`% JUSTIFY …\` comment notes as prose because he could not tell
|
|
114
114
|
them apart — and said so, which is itself a finding about the extract.
|
|
115
115
|
|
|
116
116
|
**So:** take the changed lines, then widen each to its enclosing paragraph, expand macros, drop
|