paperlint 3.0.0 → 3.1.1
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/.github/workflows/ci.yml +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +51 -56
- package/CONTRIBUTING.md +30 -18
- package/README.md +179 -180
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +14 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +29 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +93 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -80
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -4
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +50 -15
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +30 -8
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +59 -30
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +8 -3
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +26 -10
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +48 -4
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/rules.md +29 -25
- package/docs/skills.md +80 -0
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +56 -9
- package/eslint-rules/paper-typography.test.mjs +186 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.schema.json +1 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +51 -11
- package/src/build.test.ts +362 -0
- package/src/build.ts +40 -14
- package/src/cli.harness.mjs +12 -19
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +409 -94
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +77 -7
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +4 -4
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +5 -7
- package/src/new-paper.test.ts +203 -2
- package/src/new-paper.ts +84 -17
- package/src/paper-files.test.ts +122 -5
- package/src/paper-files.ts +99 -39
- package/src/paper-settings-commands.test.ts +41 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/parse-args.test.ts +48 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +5 -9
- package/src/structure.ts +1 -1
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +44 -13
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +37 -12
- package/src/types.ts +6 -0
- package/src/venue-rules.test.ts +63 -0
- package/src/venue-rules.ts +14 -12
- package/templates/paper/paperlint.json +1 -1
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -79
- package/src/structure.mutations.mjs +0 -83
|
@@ -4,8 +4,18 @@
|
|
|
4
4
|
* tally is tested on what the linter reports, not on source text.
|
|
5
5
|
*/
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
7
9
|
import { test } from "vitest";
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
checkFrozen,
|
|
12
|
+
FROZEN_FILE,
|
|
13
|
+
judge,
|
|
14
|
+
main,
|
|
15
|
+
tally,
|
|
16
|
+
type Suppressed,
|
|
17
|
+
} from "./layer-legacy-frozen.mjs";
|
|
18
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
9
19
|
|
|
10
20
|
const IO = "legacy I/O, moves behind a port in #76";
|
|
11
21
|
const LAYER = "legacy layer, moves behind a port in #76";
|
|
@@ -113,3 +123,135 @@ test("a layer finding silenced without naming #76 is reported; other rules' supp
|
|
|
113
123
|
/src\/build\.ts:4: boundaries\/dependencies is silenced with "temporary"/,
|
|
114
124
|
);
|
|
115
125
|
});
|
|
126
|
+
|
|
127
|
+
test("main: problems are listed and fail; a clean count passes", async () => {
|
|
128
|
+
const run = async (r: {
|
|
129
|
+
problems: string[];
|
|
130
|
+
counts: Record<string, Record<string, number>>;
|
|
131
|
+
}) => {
|
|
132
|
+
const out: string[] = [];
|
|
133
|
+
const code = await main({
|
|
134
|
+
check: async () => ({ ...r, frozen: {} }),
|
|
135
|
+
log: (s: string) => out.push(s),
|
|
136
|
+
err: (s: string) => out.push(`E ${s}`),
|
|
137
|
+
});
|
|
138
|
+
return { code, out };
|
|
139
|
+
};
|
|
140
|
+
assert.deepEqual(
|
|
141
|
+
[
|
|
142
|
+
await run({ problems: ["a.ts: grew", "b.ts: new"], counts: {} }),
|
|
143
|
+
await run({
|
|
144
|
+
problems: [],
|
|
145
|
+
counts: { "a.ts": { r: 2 }, "b.ts": { r: 1, s: 1 } },
|
|
146
|
+
}),
|
|
147
|
+
],
|
|
148
|
+
[
|
|
149
|
+
{
|
|
150
|
+
code: 1,
|
|
151
|
+
out: [
|
|
152
|
+
"E 🔴 legacy layer exemptions are frozen and may only shrink (#76) — 2 problem(s):",
|
|
153
|
+
"E a.ts: grew",
|
|
154
|
+
"E b.ts: new",
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
code: 0,
|
|
159
|
+
out: [
|
|
160
|
+
"✓ 2 legacy files, 4 frozen layer exemptions, none new, none grown (#76)",
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Lints the whole of src/ with the real config: ~3 s alone, 5.6 s measured under c8 with the
|
|
168
|
+
// suite running in parallel — past vitest's 5 s default, so the budget is stated here.
|
|
169
|
+
test(
|
|
170
|
+
"checkFrozen on this repository: nothing new, nothing grown — and on a tree with no src/, ESLint refuses loudly",
|
|
171
|
+
{
|
|
172
|
+
timeout: 30_000,
|
|
173
|
+
},
|
|
174
|
+
async () => {
|
|
175
|
+
const root = useTempDir("layer-frozen-");
|
|
176
|
+
writeTree(root, {
|
|
177
|
+
[FROZEN_FILE]: JSON.stringify({ files: {} }),
|
|
178
|
+
"eslint.config.mjs": "export default [];\n",
|
|
179
|
+
});
|
|
180
|
+
const here = await checkFrozen();
|
|
181
|
+
await assert.rejects(checkFrozen(root), {
|
|
182
|
+
message: "No files matching 'src/**/*.ts' were found.",
|
|
183
|
+
});
|
|
184
|
+
assert.deepEqual(here.problems, []);
|
|
185
|
+
assert.deepEqual(here.counts, here.frozen);
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
test("checkFrozen: a lint that returns no result reports that nothing was counted", async () => {
|
|
190
|
+
const root = useTempDir("layer-frozen-empty-");
|
|
191
|
+
writeTree(root, {
|
|
192
|
+
[FROZEN_FILE]: JSON.stringify({ files: { "src/a.ts": { x: 1 } } }),
|
|
193
|
+
});
|
|
194
|
+
assert.deepEqual(await checkFrozen(root, { lint: async () => [] }), {
|
|
195
|
+
problems: ["src/**/*.ts matched no file — nothing was counted."],
|
|
196
|
+
frozen: { "src/a.ts": { x: 1 } },
|
|
197
|
+
counts: {},
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("tally: optional parts of ESLint's result shape are read as absent", () => {
|
|
202
|
+
const t = tally([
|
|
203
|
+
{ filePath: "src/a.ts" },
|
|
204
|
+
{
|
|
205
|
+
filePath: "src/b.ts",
|
|
206
|
+
suppressedMessages: [
|
|
207
|
+
{ ruleId: null, line: 1, suppressions: [{ justification: IO }] },
|
|
208
|
+
{ ruleId: "boundaries/dependencies", line: 2 },
|
|
209
|
+
{ ruleId: "boundaries/dependencies", line: 3, suppressions: [{}] },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
]);
|
|
213
|
+
assert.deepEqual(t.counts, {});
|
|
214
|
+
assert.deepEqual(t.unexplained, [
|
|
215
|
+
'src/b.ts:2: boundaries/dependencies is silenced with "" — a layer exemption must say "legacy I/O|layer, moves behind a port in #76", or the site must be fixed.',
|
|
216
|
+
'src/b.ts:3: boundaries/dependencies is silenced with "" — a layer exemption must say "legacy I/O|layer, moves behind a port in #76", or the site must be fixed.',
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("checkFrozen: a frozen file with no `files` freezes nothing", async () => {
|
|
221
|
+
const root = useTempDir("layer-frozen-nofiles-");
|
|
222
|
+
writeTree(root, { [FROZEN_FILE]: "{}\n" });
|
|
223
|
+
const r = await checkFrozen(root, {
|
|
224
|
+
lint: async () => [
|
|
225
|
+
{ filePath: `${root}/src/a.ts`, suppressedMessages: [] },
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
assert.deepEqual(r, { problems: [], frozen: {}, counts: {} });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("run as a program: the gate's verdict is the exit code", () => {
|
|
232
|
+
// ESLint is replaced by one whose lint answers nothing, so the real script runs end to end in
|
|
233
|
+
// well under a second and takes its refusal path.
|
|
234
|
+
const fake = encodeURIComponent(
|
|
235
|
+
"export class ESLint { async lintFiles() { return []; } }",
|
|
236
|
+
);
|
|
237
|
+
const hook = encodeURIComponent(
|
|
238
|
+
`export async function resolve(s, c, next) { return s === "eslint" ? { url: "data:text/javascript,${fake}", shortCircuit: true } : next(s, c); }`,
|
|
239
|
+
);
|
|
240
|
+
const preload = `data:text/javascript,${encodeURIComponent(
|
|
241
|
+
`import { register } from "node:module"; register("data:text/javascript,${hook}");`,
|
|
242
|
+
)}`;
|
|
243
|
+
const r = spawnSync(
|
|
244
|
+
process.execPath,
|
|
245
|
+
[
|
|
246
|
+
"--import",
|
|
247
|
+
preload,
|
|
248
|
+
fileURLToPath(new URL("./layer-legacy-frozen.mjs", import.meta.url)),
|
|
249
|
+
],
|
|
250
|
+
{ encoding: "utf8" },
|
|
251
|
+
);
|
|
252
|
+
assert.equal(r.status, 1, r.stderr);
|
|
253
|
+
assert.match(
|
|
254
|
+
r.stderr,
|
|
255
|
+
/1 problem\(s\):\n {3}src\/\*\*\/\*\.ts matched no file — nothing was counted\./,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
@@ -12,20 +12,16 @@
|
|
|
12
12
|
* III. QUIET on sources that merely TALK about the banned module — a comment and a string
|
|
13
13
|
* literal. This is the half a grep cannot have, and the reason the check parses.
|
|
14
14
|
*/
|
|
15
|
-
import assert from "node:assert/strict";
|
|
16
15
|
import { dirname, join } from "node:path";
|
|
17
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { createChecker } from "../lib/check.mjs";
|
|
18
18
|
|
|
19
19
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const { rulesAreContentOnly, processImports } = await import(
|
|
21
21
|
join(HERE, "rules-are-content-only.mjs")
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
const check = (label, ok) => {
|
|
26
|
-
n++;
|
|
27
|
-
assert.equal(ok, true, label);
|
|
28
|
-
};
|
|
24
|
+
const check = createChecker();
|
|
29
25
|
|
|
30
26
|
// ── I. quiet on the real corpus, and it actually looked ───────────────────────
|
|
31
27
|
{
|
|
@@ -38,10 +34,6 @@ const check = (label, ok) => {
|
|
|
38
34
|
`quiet on the real corpus (${findings.length} findings)`,
|
|
39
35
|
findings.length === 0,
|
|
40
36
|
);
|
|
41
|
-
check(
|
|
42
|
-
"mutation files are not scanned as rules",
|
|
43
|
-
!checked.some((f) => f.endsWith(".mutations.mjs")),
|
|
44
|
-
);
|
|
45
37
|
}
|
|
46
38
|
|
|
47
39
|
// ── II. fires on every form the defect has taken ────────────────────────────
|
|
@@ -110,4 +102,4 @@ for (const [label, src] of [
|
|
|
110
102
|
check(`quiet on ${label}`, processImports(src).length === 0);
|
|
111
103
|
}
|
|
112
104
|
|
|
113
|
-
console.log(`rules-are-content-only.harness: ${
|
|
105
|
+
console.log(`rules-are-content-only.harness: ${check.count} assertions passed`);
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* Tested by: `scripts/rules-are-content-only.harness.mjs` — quiet on the real corpus, firing on
|
|
29
29
|
* a planted rule.
|
|
30
30
|
*/
|
|
31
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
31
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
32
32
|
import { join } from "node:path";
|
|
33
33
|
import { parse } from "espree";
|
|
34
34
|
import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
@@ -59,11 +59,8 @@ const SPAWNERS = new Set([
|
|
|
59
59
|
const isGit = (v) =>
|
|
60
60
|
typeof v === "string" && (v === "git" || v.startsWith("git "));
|
|
61
61
|
|
|
62
|
-
/** Rule sources only: a
|
|
63
|
-
const isRuleSource = (f) =>
|
|
64
|
-
f.endsWith(".mjs") &&
|
|
65
|
-
!f.endsWith(".mutations.mjs") &&
|
|
66
|
-
!f.endsWith(".harness.mjs");
|
|
62
|
+
/** Rule sources only: a harness plants defects on purpose and asserts about them. */
|
|
63
|
+
const isRuleSource = (f) => f.endsWith(".mjs") && !f.endsWith(".harness.mjs");
|
|
67
64
|
|
|
68
65
|
/** @param {string} src @returns {string[]} the git invocations this source makes */
|
|
69
66
|
export function processImports(src) {
|
|
@@ -98,7 +95,10 @@ export function rulesAreContentOnly({ cwd }) {
|
|
|
98
95
|
const dir = join(cwd, "eslint-rules");
|
|
99
96
|
const checked = [];
|
|
100
97
|
const findings = [];
|
|
101
|
-
|
|
98
|
+
// A repository with no `eslint-rules/` has no rule sources: `checked` stays empty and the gate
|
|
99
|
+
// says so by name. `readdirSync` would throw ENOENT instead — a stack trace, not the refusal.
|
|
100
|
+
const names = existsSync(dir) ? readdirSync(dir) : [];
|
|
101
|
+
for (const f of names.filter(isRuleSource)) {
|
|
102
102
|
const hits = processImports(readFileSync(join(dir, f), "utf8"));
|
|
103
103
|
// 🔴 The counter increments WITH the verdict, not before it: a `checked` that keeps
|
|
104
104
|
// counting while the verdict stops being reached is the failure recorded in the consumer's
|
|
@@ -109,30 +109,40 @@ export function rulesAreContentOnly({ cwd }) {
|
|
|
109
109
|
return { checked, findings };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
/**
|
|
113
|
+
* The gate over `cwd`'s `eslint-rules/`: exit 1 on no rule sources (a silent zero would read as
|
|
114
|
+
* clean) or on any rule asking git; 0 otherwise.
|
|
115
|
+
*
|
|
116
|
+
* 🔴 `cwd` is `process.cwd()`, NOT the package root. A consumer installs this package and runs the
|
|
117
|
+
* script from its own repository, where the rules that matter are ITS `eslint-rules/` — the
|
|
118
|
+
* package's own are already checked by the package's own gate. Anchored to the package root the
|
|
119
|
+
* consumer's gate would re-check the same eight files and report a confident zero about rules it
|
|
120
|
+
* never opened: a counter that counts what it ignores.
|
|
121
|
+
*/
|
|
122
|
+
export function main({
|
|
123
|
+
cwd = process.cwd(),
|
|
124
|
+
log = console.log,
|
|
125
|
+
err = console.error,
|
|
126
|
+
} = {}) {
|
|
119
127
|
const { checked, findings } = rulesAreContentOnly({ cwd });
|
|
120
128
|
if (checked.length === 0) {
|
|
121
|
-
|
|
129
|
+
err(
|
|
122
130
|
`${join(cwd, "eslint-rules")}: no rule sources. Either this is not a repository with ` +
|
|
123
131
|
`ESLint rules, or the directory moved — a silent zero here would read as a clean run.`,
|
|
124
132
|
);
|
|
125
|
-
|
|
133
|
+
return 1;
|
|
126
134
|
}
|
|
127
135
|
for (const { file, specifier } of findings) {
|
|
128
|
-
|
|
136
|
+
err(
|
|
129
137
|
`${file}: imports \`${specifier}\`. A rule may not ask git: history is rewritten by ` +
|
|
130
138
|
`routine maintenance and CI clones one commit deep, so the fact does not survive where ` +
|
|
131
139
|
`the rule has to hold. Put the fact on disk and check it by bytes.`,
|
|
132
140
|
);
|
|
133
141
|
}
|
|
134
|
-
|
|
142
|
+
log(
|
|
135
143
|
`rules-are-content-only: ${checked.length} rule sources under ${cwd}, ${findings.length} findings`,
|
|
136
144
|
);
|
|
137
|
-
|
|
145
|
+
return findings.length === 0 ? 0 : 1;
|
|
138
146
|
}
|
|
147
|
+
|
|
148
|
+
if (isMain(import.meta.url)) process.exit(main());
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rules-are-content-only's gate over a consumer's `eslint-rules/`: temp trees with planted rules.
|
|
3
|
+
*/
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { test } from "vitest";
|
|
8
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
9
|
+
import { main as contentMain } from "./rules-are-content-only.mjs";
|
|
10
|
+
|
|
11
|
+
const capture = () => {
|
|
12
|
+
const out = [];
|
|
13
|
+
const io = { log: (s) => out.push(s), err: (s) => out.push(`E ${s}`) };
|
|
14
|
+
return { out, io };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const root = useTempDir("content-only-");
|
|
18
|
+
|
|
19
|
+
test("rules-are-content-only: no rules is a failure, a rule asking git is a finding, clean rules pass", () => {
|
|
20
|
+
writeTree(root, {
|
|
21
|
+
"empty/.keep": "",
|
|
22
|
+
"dirty/eslint-rules/asks-git.mjs":
|
|
23
|
+
'import { execFileSync } from "node:child_process";\nexecFileSync("git", ["log"]);\n',
|
|
24
|
+
"dirty/eslint-rules/clean.mjs": "export default {};\n",
|
|
25
|
+
"clean/eslint-rules/clean.mjs": "export default {};\n",
|
|
26
|
+
});
|
|
27
|
+
const runs = ["empty", "dirty", "clean"].map((d) => {
|
|
28
|
+
const { out, io } = capture();
|
|
29
|
+
return [
|
|
30
|
+
contentMain({ cwd: `${root}/${d}`, ...io }),
|
|
31
|
+
out.map((l) => l.replace(root, "<root>").split(". ")[0]),
|
|
32
|
+
];
|
|
33
|
+
});
|
|
34
|
+
assert.deepEqual(runs, [
|
|
35
|
+
[1, ["E <root>/empty/eslint-rules: no rule sources"]],
|
|
36
|
+
[
|
|
37
|
+
1,
|
|
38
|
+
[
|
|
39
|
+
"E asks-git.mjs: imports `git`",
|
|
40
|
+
"rules-are-content-only: 2 rule sources under <root>/dirty, 1 findings",
|
|
41
|
+
],
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
0,
|
|
45
|
+
["rules-are-content-only: 1 rule sources under <root>/clean, 0 findings"],
|
|
46
|
+
],
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("run as a program over this repository: every rule source is content-only, exit 0", () => {
|
|
51
|
+
const r = spawnSync(
|
|
52
|
+
process.execPath,
|
|
53
|
+
[fileURLToPath(new URL("./rules-are-content-only.mjs", import.meta.url))],
|
|
54
|
+
{ cwd: fileURLToPath(new URL("..", import.meta.url)), encoding: "utf8" },
|
|
55
|
+
);
|
|
56
|
+
assert.equal(r.status, 0, r.stderr);
|
|
57
|
+
assert.match(
|
|
58
|
+
r.stdout,
|
|
59
|
+
/^rules-are-content-only: \d+ rule sources under .*, 0 findings\n$/,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
@@ -79,21 +79,23 @@ export async function rulesSeeFiles({ cwd }) {
|
|
|
79
79
|
// a symlink the two are not equal and the CLI silently does not run — the process exits 0 having
|
|
80
80
|
// done nothing. The consumer reaches these scripts through exactly such a symlink. Observed 14.09
|
|
81
81
|
// on run 34784079821: `extract-pdf-facts.mjs --strict` returned RC=0 and created no facts file.
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
/** The gate: every declared rule listed with the files it saw; exit 1 when one saw none. */
|
|
83
|
+
export async function main({ cwd = process.cwd(), log = console.log } = {}) {
|
|
84
|
+
const { rules, linted, blind } = await rulesSeeFiles({ cwd });
|
|
84
85
|
for (const { rule, files } of rules)
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
log(`${String(files).padStart(4)} ${rule}`);
|
|
87
|
+
log(`\n${rules.length} rule(s) declared, ${linted} file(s) linted.`);
|
|
87
88
|
if (blind.length) {
|
|
88
|
-
|
|
89
|
+
log(
|
|
89
90
|
`\n✗ ${blind.length} rule(s) saw NO file and were never invoked:\n` +
|
|
90
91
|
blind.map((r) => ` ${r}`).join("\n") +
|
|
91
92
|
"\n\n A rule that is never invoked reports exactly what a rule that passed reports.\n" +
|
|
92
93
|
" Fix the glob it is declared on, or delete the declaration — do not leave it green.",
|
|
93
94
|
);
|
|
94
|
-
|
|
95
|
+
return 1;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
97
|
+
log("✓ every declared rule was enabled for at least one file on disk.");
|
|
98
|
+
return 0;
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
if (isMain(import.meta.url)) process.exit(await main());
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rules-see-files' gate: a temp repository whose config declares a rule on a glob matching nothing.
|
|
3
|
+
*/
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { test } from "vitest";
|
|
8
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
9
|
+
import { main as seeMain } from "./rules-see-files.mjs";
|
|
10
|
+
|
|
11
|
+
const capture = () => {
|
|
12
|
+
const out = [];
|
|
13
|
+
const io = { log: (s) => out.push(s), err: (s) => out.push(`E ${s}`) };
|
|
14
|
+
return { out, io };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const root = useTempDir("rules-see-files-");
|
|
18
|
+
|
|
19
|
+
test("rules-see-files: a rule declared on a glob that matches nothing is named and fails", async () => {
|
|
20
|
+
writeTree(root, {
|
|
21
|
+
"see/eslint.config.mjs":
|
|
22
|
+
'export default [{ files: ["**/*.js"], rules: { "no-debugger": "error" } }, { files: ["**/*.nothing"], rules: { "no-undef": "error", "eqeqeq": "off" } }];\n',
|
|
23
|
+
"see/a.js": "export const a = 1;\n",
|
|
24
|
+
"see/package.json": '{ "type": "module" }\n',
|
|
25
|
+
});
|
|
26
|
+
const { out, io } = capture();
|
|
27
|
+
const code = await seeMain({ cwd: `${root}/see`, ...io });
|
|
28
|
+
assert.deepEqual(
|
|
29
|
+
{ code, out },
|
|
30
|
+
{
|
|
31
|
+
code: 1,
|
|
32
|
+
out: [
|
|
33
|
+
" 1 no-debugger",
|
|
34
|
+
" 0 no-undef",
|
|
35
|
+
"\n2 rule(s) declared, 2 file(s) linted.",
|
|
36
|
+
"\n✗ 1 rule(s) saw NO file and were never invoked:\n no-undef\n\n" +
|
|
37
|
+
" A rule that is never invoked reports exactly what a rule that passed reports.\n" +
|
|
38
|
+
" Fix the glob it is declared on, or delete the declaration — do not leave it green.",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("rules-see-files: every rule seeing a file passes — asked with a trailing slash, and as a program", async () => {
|
|
45
|
+
writeTree(root, {
|
|
46
|
+
"ok/eslint.config.mjs":
|
|
47
|
+
'export default [{ files: ["**/*.js"], rules: { "no-debugger": ["error"] } }];\n',
|
|
48
|
+
"ok/a.js": "export const a = 1;\n",
|
|
49
|
+
"ok/package.json": '{ "type": "module" }\n',
|
|
50
|
+
});
|
|
51
|
+
const { out, io } = capture();
|
|
52
|
+
const code = await seeMain({ cwd: `${root}/ok/`, ...io });
|
|
53
|
+
assert.deepEqual(
|
|
54
|
+
{ code, out },
|
|
55
|
+
{
|
|
56
|
+
code: 0,
|
|
57
|
+
out: [
|
|
58
|
+
" 1 no-debugger",
|
|
59
|
+
"\n1 rule(s) declared, 2 file(s) linted.",
|
|
60
|
+
"✓ every declared rule was enabled for at least one file on disk.",
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
const run = spawnSync(
|
|
65
|
+
process.execPath,
|
|
66
|
+
[fileURLToPath(new URL("./rules-see-files.mjs", import.meta.url))],
|
|
67
|
+
{ cwd: `${root}/ok`, encoding: "utf8" },
|
|
68
|
+
);
|
|
69
|
+
assert.equal(run.status, 0, run.stderr);
|
|
70
|
+
assert.match(run.stdout, /✓ every declared rule was enabled/);
|
|
71
|
+
});
|
package/scripts/test.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm test`: the unit tests (vitest), then the harnesses (`vigiles test`). BOTH always run.
|
|
3
|
+
*
|
|
4
|
+
* Until #52 this was `vitest run && vigiles test --min=1`. Under `npm run coverage` a single
|
|
5
|
+
* failing vitest test then skipped every harness, and what the run printed last was c8's summary:
|
|
6
|
+
* lines, branches and functions each some six points below the floor. The one failing test sat
|
|
7
|
+
* above that, looking like a consequence rather than the cause. Running both suites keeps the
|
|
8
|
+
* coverage report whole, so a failing test is reported as itself and the coverage figures still
|
|
9
|
+
* mean what they say.
|
|
10
|
+
*
|
|
11
|
+
* Arguments are the harness runner's (`npm test -- --no-skip`), as they were before.
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
15
|
+
|
|
16
|
+
/** The two suites, in order: what a failure is named as, and the command. */
|
|
17
|
+
export const SUITES = [
|
|
18
|
+
{ name: "vitest", command: ["vitest", "run"] },
|
|
19
|
+
{ name: "harnesses", command: ["vigiles", "test", "--min=1"] },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run every suite, whatever the one before it returned, and name the ones that failed.
|
|
24
|
+
* @returns 0 when every suite passed; otherwise the first failing suite's exit code (1 for a signal).
|
|
25
|
+
*/
|
|
26
|
+
export function runSuites(
|
|
27
|
+
args,
|
|
28
|
+
{ spawn = spawnSync, err = console.error, suites = SUITES } = {},
|
|
29
|
+
) {
|
|
30
|
+
const failed = suites.flatMap(({ name, command }) => {
|
|
31
|
+
const [file, ...rest] = command;
|
|
32
|
+
const r = spawn(file, [...rest, ...(name === "harnesses" ? args : [])], {
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
});
|
|
35
|
+
return r.status === 0 ? [] : [{ name, code: r.status ?? 1 }];
|
|
36
|
+
});
|
|
37
|
+
for (const f of failed)
|
|
38
|
+
err(
|
|
39
|
+
`✗ ${f.name} failed (exit ${String(f.code)}) — its own report is above`,
|
|
40
|
+
);
|
|
41
|
+
return failed.length === 0 ? 0 : failed[0].code;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isMain(import.meta.url)) process.exit(runSuites(process.argv.slice(2)));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm test`'s runner: both suites run whatever the first returned, each failure is named, and
|
|
3
|
+
* the harness arguments reach the harness runner only.
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { test } from "vitest";
|
|
7
|
+
import { runSuites, SUITES } from "./test.mjs";
|
|
8
|
+
|
|
9
|
+
const fake = (status) => {
|
|
10
|
+
const calls = [];
|
|
11
|
+
return {
|
|
12
|
+
calls,
|
|
13
|
+
spawn: (file, args) => {
|
|
14
|
+
calls.push([file, ...args]);
|
|
15
|
+
return { status: status[file] };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
test("a failing vitest does not skip the harnesses, and is named as the failure", () => {
|
|
21
|
+
const { calls, spawn } = fake({ vitest: 1, vigiles: 0 });
|
|
22
|
+
const said = [];
|
|
23
|
+
assert.equal(
|
|
24
|
+
runSuites(["--no-skip"], { spawn, err: (s) => said.push(s) }),
|
|
25
|
+
1,
|
|
26
|
+
);
|
|
27
|
+
assert.deepEqual(calls, [
|
|
28
|
+
["vitest", "run"],
|
|
29
|
+
["vigiles", "test", "--min=1", "--no-skip"],
|
|
30
|
+
]);
|
|
31
|
+
assert.deepEqual(said, [
|
|
32
|
+
"✗ vitest failed (exit 1) — its own report is above",
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("both passing is 0 and silent; a harness killed by a signal is exit 1, named", () => {
|
|
37
|
+
const ok = fake({ vitest: 0, vigiles: 0 });
|
|
38
|
+
assert.equal(runSuites([], { spawn: ok.spawn, err: assert.fail }), 0);
|
|
39
|
+
const killed = fake({ vitest: 0, vigiles: null });
|
|
40
|
+
const said = [];
|
|
41
|
+
assert.equal(
|
|
42
|
+
runSuites([], { spawn: killed.spawn, err: (s) => said.push(s) }),
|
|
43
|
+
1,
|
|
44
|
+
);
|
|
45
|
+
assert.deepEqual(said, [
|
|
46
|
+
"✗ harnesses failed (exit 1) — its own report is above",
|
|
47
|
+
]);
|
|
48
|
+
assert.deepEqual(
|
|
49
|
+
SUITES.map((s) => s.name),
|
|
50
|
+
["vitest", "harnesses"],
|
|
51
|
+
);
|
|
52
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`--flags-only: an over-long caption flags and exits 1 1`] = `
|
|
4
|
+
"✍️ prose-lint — paper.md — 2 finding(s):
|
|
5
|
+
figures/fig1.tex: caption sentence of 46 words (warn >40) — "Big. word0 word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 word13 word14 word15 wor…"
|
|
6
|
+
figures/fig1.tex: caption is 106 words (warn >100) — a caption this long is a section under a picture
|
|
7
|
+
run \`node .claude/skills/grade-paper-writing/prose-lint.mjs <file>\` for the sentences
|
|
8
|
+
"
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
exports[`--headings lists every heading from level 2 to 4, in order 1`] = `
|
|
12
|
+
"
|
|
13
|
+
=== paper.md — every heading, in order ===
|
|
14
|
+
1. Introduction
|
|
15
|
+
2. Method
|
|
16
|
+
|
|
17
|
+
Read these as a stranger who will read nothing else. Each should name a thing:
|
|
18
|
+
a linter, a configuration, a repository, a rule — not "answers", "kinds", "moves".
|
|
19
|
+
|
|
20
|
+
(thresholds and their sources are in THRESHOLDS at the top of this file)
|
|
21
|
+
"
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
exports[`a .tex file is refused with exit 2, and nothing is measured 1`] = `
|
|
25
|
+
"prose-lint reads markdown, not LaTeX: <root>/paper.tex
|
|
26
|
+
There is no .tex stripping here — the word count would include the preamble and the .bib,
|
|
27
|
+
and every metric (they are all fractions of the word count) would be understated by roughly half.
|
|
28
|
+
What to cover prose in .tex with — papers/research/2026-08-26-proza-v-latex-zanyatost.md
|
|
29
|
+
"
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
exports[`the full report over markdown and rendered text 1`] = `
|
|
33
|
+
"
|
|
34
|
+
=== paper.md — 82 words, 9 sentences ===
|
|
35
|
+
sentence lengths [7,10,8,6,19,6,8,8,6] mean 8.7 sd 3.9 CV 0.45
|
|
36
|
+
FLAG all metadiscourse/1000w 195.1 (Hyland RA baseline 64.9, warn >90)
|
|
37
|
+
nominalisations/1000w 12.2
|
|
38
|
+
FLAG "rather than"/instead-of/10,000w 243.9 (warn >25; not epanorthosis, no published baseline)
|
|
39
|
+
FLAG sentences ending on a cross-ref/citation/hedge (wasted stress position): 3
|
|
40
|
+
> …Clearly the first result matters (Section 3).
|
|
41
|
+
> …Prior work agrees [1, 2].
|
|
42
|
+
> … full suite of papers, which suggests the method might hold generally.
|
|
43
|
+
ok sentences whose main claim is a negation: 22%
|
|
44
|
+
topic positions (whose story is this?): "in this paper we present" | "it might possibly help, but" | "clearly the first result matters" | "prior work agrees [1, 2]." | "the speedup was 40% on" | "- the first item is" | "- the second item is" | "we believe that rather than" | "instead of prose, the numbers"
|
|
45
|
+
|
|
46
|
+
=== page.txt — 13 words, 3 sentences ===
|
|
47
|
+
sentence lengths [3,6,4] mean 4.3 sd 1.2 CV 0.29
|
|
48
|
+
ok all metadiscourse/1000w 0 (Hyland RA baseline 64.9, warn >90)
|
|
49
|
+
nominalisations/1000w 0
|
|
50
|
+
ok "rather than"/instead-of/10,000w 0 (warn >25; not epanorthosis, no published baseline)
|
|
51
|
+
ok sentences ending on a cross-ref/citation/hedge (wasted stress position): 0
|
|
52
|
+
ok sentences whose main claim is a negation: 0%
|
|
53
|
+
topic positions (whose story is this?): "a rendered page." | "it was hyphenated across a" | "next page text here."
|
|
54
|
+
|
|
55
|
+
(thresholds and their sources are in THRESHOLDS at the top of this file)
|
|
56
|
+
"
|
|
57
|
+
`;
|