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
|
@@ -124,11 +124,11 @@ export const UNEXPRESSED = [
|
|
|
124
124
|
],
|
|
125
125
|
[
|
|
126
126
|
"draft ← study",
|
|
127
|
-
'draft-paper/SKILL.md: "**
|
|
127
|
+
'draft-paper/SKILL.md: "**FINDING** — it declined to draft because a required input was missing: no numbers from `build-benchmark`"',
|
|
128
128
|
],
|
|
129
129
|
[
|
|
130
130
|
"priorwork ← idea",
|
|
131
|
-
'
|
|
131
|
+
'map-prior-work/SKILL.md, the `priorwork` row: "SETUP, right after `research-ideate` says go"',
|
|
132
132
|
],
|
|
133
133
|
];
|
|
134
134
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pipeline-edges.mjs admits each edge on a SENTENCE from a skill's own SKILL.md, quoted in the
|
|
3
|
+
* table so the judgement stays auditable. A quote is only evidence while the skill still says it,
|
|
4
|
+
* so every quote is looked up in the SKILL.md it names. A skill reworded since — or a quote that
|
|
5
|
+
* was never verbatim — fails here, naming the edge.
|
|
6
|
+
*
|
|
7
|
+
* Matching tolerates what quoting legitimately changes: runs of whitespace, `**` emphasis, and an
|
|
8
|
+
* elision `…`, which splits a quote into fragments that must each appear. The values are prose,
|
|
9
|
+
* so reading them with a pattern is reading prose, not parsing a format.
|
|
10
|
+
*/
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { test } from "vitest";
|
|
16
|
+
import { CANONICAL_INPUTS, NOT_EDGES, UNEXPRESSED } from "./pipeline-edges.mjs";
|
|
17
|
+
|
|
18
|
+
const SKILLS = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
19
|
+
const norm = (s) => s.replace(/\*\*/g, "").replace(/\s+/g, " ");
|
|
20
|
+
|
|
21
|
+
/** [edge, skill, quote] for every `<skill>/SKILL.md…: "<quote>"` in the three tables. */
|
|
22
|
+
function quotes() {
|
|
23
|
+
const cells = [
|
|
24
|
+
...Object.entries(CANONICAL_INPUTS).flatMap(([row, { requires }]) =>
|
|
25
|
+
Object.entries(requires).map(([input, why]) => [
|
|
26
|
+
`${row} ← ${input}`,
|
|
27
|
+
why,
|
|
28
|
+
]),
|
|
29
|
+
),
|
|
30
|
+
...UNEXPRESSED,
|
|
31
|
+
...NOT_EDGES,
|
|
32
|
+
];
|
|
33
|
+
return cells.flatMap(([edge, why]) =>
|
|
34
|
+
quotesIn(why).map(([skill, quote]) => [edge, skill, quote]),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** [skill, quote] for every `<skill>/SKILL.md…: "<quote>"` in one cell. */
|
|
39
|
+
function quotesIn(why) {
|
|
40
|
+
return [...why.matchAll(/([a-z-]+)\/SKILL\.md[^"]*"((?:[^"\\]|\\.)*)"/g)].map(
|
|
41
|
+
(m) => [m[1], m[2]],
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Guards: the pattern below is not vacuous — a cell that names a SKILL.md yields its quote.
|
|
46
|
+
test("every cell that names a SKILL.md yields a quote to check", () => {
|
|
47
|
+
const cells = [
|
|
48
|
+
...Object.values(CANONICAL_INPUTS).flatMap((v) =>
|
|
49
|
+
Object.values(v.requires),
|
|
50
|
+
),
|
|
51
|
+
...UNEXPRESSED.map(([, why]) => why),
|
|
52
|
+
...NOT_EDGES.map(([, why]) => why),
|
|
53
|
+
];
|
|
54
|
+
const unparsed = cells.filter(
|
|
55
|
+
(why) => why.includes("/SKILL.md") && quotesIn(why).length === 0,
|
|
56
|
+
);
|
|
57
|
+
assert.deepEqual(unparsed, []);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("every quoted sentence is still in the SKILL.md it is attributed to", () => {
|
|
61
|
+
const stale = quotes().flatMap(([edge, skill, quote]) => {
|
|
62
|
+
const file = join(SKILLS, skill, "SKILL.md");
|
|
63
|
+
if (!existsSync(file))
|
|
64
|
+
return [{ edge, skill, missing: "the skill itself" }];
|
|
65
|
+
const text = norm(readFileSync(file, "utf8"));
|
|
66
|
+
return norm(quote)
|
|
67
|
+
.split("…")
|
|
68
|
+
.map((f) => f.trim())
|
|
69
|
+
.filter((f) => f.length > 0 && !text.includes(f))
|
|
70
|
+
.map((missing) => ({ edge, skill, missing }));
|
|
71
|
+
});
|
|
72
|
+
assert.deepEqual(stale, []);
|
|
73
|
+
});
|
|
@@ -8,29 +8,10 @@
|
|
|
8
8
|
* THE REGISTRY ITSELF (`repro/populations.tsv`), both have no address in the paper, and so cannot be expressed by ESLint,
|
|
9
9
|
* which reports only to the linted file.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 1. IT IS INVISIBLE TO THE MEASUREMENT. `vigiles audit` scores this repo `Tested: 0`, and the
|
|
16
|
-
* metric counts `*.harness.mjs` / `*.eval.mjs`. Verified 2026-08-07: bare `npx vigiles test`
|
|
17
|
-
* reports no harness files found — its glob does not descend into dot-dirs AND the self-test
|
|
18
|
-
* does not carry a name the collector looks for. Given an explicit path it runs
|
|
19
|
-
* fine, which is the trap: it is green, correct, and uncounted, so the repo reads
|
|
20
|
-
* as having no test for its own registry hygiene.
|
|
21
|
-
*
|
|
22
|
-
* 2. IT ONLY EXERCISES `findings()`. Everything between the command line and that function —
|
|
23
|
-
* finding paper.md, finding repro/populations.tsv, `--flags-only` staying silent on a clean
|
|
24
|
-
* paper — is what the CI step and `run-mechanical.mjs` actually invoke, and none of it was
|
|
25
|
-
* under test. This repository's three dead hooks all had correct logic; what was broken was
|
|
26
|
-
* the wiring around it.
|
|
27
|
-
*
|
|
28
|
-
* 🔴 THE SELF-TEST IS RUN AS A SUBPROCESS, NEVER IMPORTED. It ends in `process.exit(fail ? 1 : 0)`.
|
|
29
|
-
* Import it and a PASSING self-test terminates this process at that line — every assertion below it
|
|
30
|
-
* never runs, and `vigiles test` reports ✓ because nothing threw. That is trap #1 from
|
|
31
|
-
* hooks.harness.mjs wearing a different hat, and it would have been undetectable from the output.
|
|
32
|
-
*
|
|
33
|
-
* Every fixture is a throwaway in a temp dir; nothing here reads or writes a real paper.
|
|
11
|
+
* The findings themselves are planted in `population-map.test.mjs` (vitest; until #52 a
|
|
12
|
+
* self-test script this harness spawned). This file keeps what only the command shows: finding
|
|
13
|
+
* paper.md and repro/populations.tsv, and `--flags-only` staying silent on a clean paper — the
|
|
14
|
+
* wiring the CI step and `run-mechanical.mjs` actually invoke.
|
|
34
15
|
*/
|
|
35
16
|
import assert from "node:assert/strict";
|
|
36
17
|
import { execFileSync } from "node:child_process";
|
|
@@ -49,7 +30,6 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
49
30
|
|
|
50
31
|
const ROOT = consumerRoot();
|
|
51
32
|
const SCRIPT = join(HERE, "population-map.mjs");
|
|
52
|
-
const SELFTEST = join(HERE, "population-map.selftest.mjs");
|
|
53
33
|
const tmp = realpathSync(mkdtempSync(join(tmpdir(), "popmap-harness-")));
|
|
54
34
|
|
|
55
35
|
/** Run a node script; never throws on a non-zero exit. */
|
|
@@ -87,29 +67,6 @@ const REG = [
|
|
|
87
67
|
"census\t134\tdisjoint\tbroad\ta separate sample",
|
|
88
68
|
].join("\n");
|
|
89
69
|
|
|
90
|
-
// ── 1. the self-test still kills every mutant it claims to ─────────────────────────────
|
|
91
|
-
// Owning it from a *.harness.mjs is the whole point: the metric now counts it, and a regression in
|
|
92
|
-
// the self-test itself fails this file rather than passing unnoticed in a step nobody reads.
|
|
93
|
-
{
|
|
94
|
-
const r = run([SELFTEST]);
|
|
95
|
-
assert.equal(r.code, 0, "population-map.selftest.mjs failed:\n" + r.out);
|
|
96
|
-
const m = r.out.match(/(\d+) passed, (\d+) failed/);
|
|
97
|
-
assert.ok(
|
|
98
|
-
m,
|
|
99
|
-
"the self-test printed no result line — it may have exited before running:\n" +
|
|
100
|
-
r.out,
|
|
101
|
-
);
|
|
102
|
-
// A self-test that runs ZERO cases also prints "0 failed" and exits 0. That shape is exactly how
|
|
103
|
-
// this repo's earlier harness reported ✓ on `assert.equal(1, 2)`, so the count is asserted too.
|
|
104
|
-
// Threshold lowered 13 → 7 along with the move of two findings to ESLint. The assertion on COUNT is kept:
|
|
105
|
-
// a self-test that ran zero cases also prints «0 failed» and exits zero.
|
|
106
|
-
assert.ok(
|
|
107
|
-
Number(m[1]) >= 7,
|
|
108
|
-
`the self-test ran only ${m[1]} case(s) — cases have gone missing`,
|
|
109
|
-
);
|
|
110
|
-
assert.equal(Number(m[2]), 0, "the self-test reported failures:\n" + r.out);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
70
|
// ── 2. the CLI finds the paper and the registry, and fires on a stale registry row ─────
|
|
114
71
|
// 🔴 FIXTURE REWRITTEN 2026-08-26. Before that it planted an UNLINKED POPULATION, and that
|
|
115
72
|
// finding moved to `paper/population-untied` — meaning the old «dirty» input became clean, and
|
|
@@ -94,7 +94,7 @@ export function parseRegistry(tsv) {
|
|
|
94
94
|
if (!line.trim() || line.startsWith("#")) continue;
|
|
95
95
|
const [id, print, relation, relatedTo, gloss] = line
|
|
96
96
|
.split("\t")
|
|
97
|
-
.map((c) =>
|
|
97
|
+
.map((c) => c.trim()); // split yields strings; a missing column is `undefined` in the destructuring
|
|
98
98
|
if (!id || id === "id") continue;
|
|
99
99
|
rows.push({ id, print, relation, relatedTo, gloss });
|
|
100
100
|
}
|
|
@@ -108,6 +108,12 @@ export function findings(md, tsv) {
|
|
|
108
108
|
const out = [];
|
|
109
109
|
|
|
110
110
|
for (const row of reg) {
|
|
111
|
+
// A row with no printed quantity has nothing to look for in the body. It used to reach
|
|
112
|
+
// `printed(undefined)` and crash the checker; it is a defect of the TSV, reported as one.
|
|
113
|
+
if (!row.print) {
|
|
114
|
+
out.push({ kind: "incomplete", row });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
111
117
|
const appears = printed(row.print).test(body);
|
|
112
118
|
if (!appears) {
|
|
113
119
|
// A registry row for a quantity the body no longer prints. Same discipline as the
|
|
@@ -124,7 +130,7 @@ export function findings(md, tsv) {
|
|
|
124
130
|
return out;
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
/* Importable above this line; the CLI only runs when this file is the entry point, so the
|
|
133
|
+
/* Importable above this line; the CLI only runs when this file is the entry point, so the tests
|
|
128
134
|
* can exercise findings() without the argv handling firing. */
|
|
129
135
|
if (!isMain(import.meta.url)) {
|
|
130
136
|
// imported — nothing to do
|
|
@@ -175,6 +181,10 @@ function main() {
|
|
|
175
181
|
console.log(
|
|
176
182
|
` 🔴 ${f.row.id} says it relates to "${f.row.relatedTo}", which is not a row in populations.tsv`,
|
|
177
183
|
);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(
|
|
186
|
+
` 🔴 ${f.row.id} has no printed quantity — the row cannot be checked against the body`,
|
|
187
|
+
);
|
|
178
188
|
}
|
|
179
189
|
}
|
|
180
190
|
process.exit(0);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* population-map.mjs — the registry's own findings (`stale`, `badref`), planted in both directions,
|
|
3
|
+
* and the command around them. Until #52 the seven cases below were `population-map.selftest.mjs`,
|
|
4
|
+
* a script the harness spawned and whose "N passed" line it parsed; they are vitest cases now.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 ONE CASE WAS DELETED BEFORE THE MOVE, NOT MOVED, and that is worth remembering: it ran a
|
|
7
|
+
* HAND-COPIED COPY of the checker's regex over a string. A change to the real `printed()` cannot
|
|
8
|
+
* touch such a test by construction. The property is pinned by behaviour instead (case 6).
|
|
9
|
+
*/
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { test } from "vitest";
|
|
14
|
+
import { runNode, useTempDir, writeTree } from "../../../test/support.mjs";
|
|
15
|
+
import { bodyOf, findings } from "./population-map.mjs";
|
|
16
|
+
|
|
17
|
+
const SCRIPT = join(
|
|
18
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
19
|
+
"population-map.mjs",
|
|
20
|
+
);
|
|
21
|
+
const doc = (bodyLines) =>
|
|
22
|
+
`## Abstract\n\n${bodyLines}\n\n## References\n\n[1] x.\n`;
|
|
23
|
+
const kinds = (md, reg) =>
|
|
24
|
+
findings(md, reg).map((f) => `${f.kind}:${f.row.print}`);
|
|
25
|
+
const REG = [
|
|
26
|
+
"id\tprinted\trelation\trelated_to\tgloss",
|
|
27
|
+
"broad\t1,921\troot\t\tthe corpus",
|
|
28
|
+
"sub\t1,836\tsubset\tbroad\tthose it could read",
|
|
29
|
+
].join("\n");
|
|
30
|
+
const HEAD = "id\tprinted\trelation\trelated_to\tgloss\n";
|
|
31
|
+
|
|
32
|
+
// 1. The direction that matters more: a checker firing on a paper that already obeys gets muted.
|
|
33
|
+
test("a registry that matches the body is silent", () => {
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
kinds(
|
|
36
|
+
doc(
|
|
37
|
+
"The resolver ran over all 1,921 repositories, and read 1,836 of them.",
|
|
38
|
+
),
|
|
39
|
+
REG,
|
|
40
|
+
),
|
|
41
|
+
[],
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 2. A row for a quantity the paper no longer prints is a claim nobody can check.
|
|
46
|
+
test("a stale registry row fires", () => {
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
kinds(doc("The resolver ran over all 1,921 repositories."), REG),
|
|
49
|
+
["stale:1,836"],
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 3. `retired` is what keeps a set declared after it was cut from the paper.
|
|
54
|
+
test("a retired row does not fire", () => {
|
|
55
|
+
assert.deepEqual(
|
|
56
|
+
kinds(
|
|
57
|
+
doc("The resolver ran over all 1,921 repositories, and read 1,836."),
|
|
58
|
+
`${REG}\ngold\t7,310\tretired\t\ta public gold set`,
|
|
59
|
+
),
|
|
60
|
+
[],
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 4. A relation pointing at an id that is not a row — a defect of the TSV alone.
|
|
65
|
+
test("a dangling related_to fires", () => {
|
|
66
|
+
assert.deepEqual(
|
|
67
|
+
kinds(
|
|
68
|
+
doc("We read 1,836 repositories."),
|
|
69
|
+
`${HEAD}sub\t1,836\tsubset\tnosuch\tx`,
|
|
70
|
+
),
|
|
71
|
+
["badref:1,836"],
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 5. A row whose number is absent is `stale`, never also `badref`.
|
|
76
|
+
test("a row that is absent is stale, not badref", () => {
|
|
77
|
+
assert.deepEqual(
|
|
78
|
+
kinds(doc("Nothing numeric here."), `${HEAD}sub\t1,836\tsubset\tnosuch\tx`),
|
|
79
|
+
["stale:1,836"],
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 6. `printed()` boundaries through behaviour: 189 is not printed inside 1,189.
|
|
84
|
+
test('189 is not "printed" inside 1,189', () => {
|
|
85
|
+
assert.deepEqual(
|
|
86
|
+
kinds(
|
|
87
|
+
doc("A follow-up read covered 1,189 records, of all 1,921."),
|
|
88
|
+
`${HEAD}broad\t1,921\troot\t\tthe corpus\nspread\t189\tsubset\tbroad\tenumerable`,
|
|
89
|
+
),
|
|
90
|
+
["stale:189"],
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// 7. The body stops at the bibliography; with no Abstract it starts at the top, with no
|
|
95
|
+
// References it runs to the end.
|
|
96
|
+
test("bodyOf: from Abstract to References, or the whole text when either is missing", () => {
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
[
|
|
99
|
+
bodyOf(doc("nothing here")).includes("[1] x."),
|
|
100
|
+
bodyOf("pre\n\n## References\n\n[1] x.\n"),
|
|
101
|
+
bodyOf("## Abstract\n\nall of it\n"),
|
|
102
|
+
],
|
|
103
|
+
[false, "pre\n\n", "## Abstract\n\nall of it\n"],
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a registry row with no printed quantity is reported, not a crash", () => {
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
findings(doc("x"), `${HEAD}short\n`).map((f) => [f.kind, f.row.id]),
|
|
110
|
+
[["incomplete", "short"]],
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── the command ─────────────────────────────────────────────────────────────────────────
|
|
115
|
+
const root = useTempDir("population-map-");
|
|
116
|
+
const paper = (name, body, tsv) =>
|
|
117
|
+
writeTree(join(root, name), {
|
|
118
|
+
"paper.md": doc(body),
|
|
119
|
+
...(tsv === undefined ? {} : { "repro/populations.tsv": tsv }),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("the command: usage, a paper without markdown, a clean registry, and every finding kind", () => {
|
|
123
|
+
assert.deepEqual(runNode(SCRIPT, []), {
|
|
124
|
+
status: 0,
|
|
125
|
+
stdout: "",
|
|
126
|
+
stderr: "usage: node population-map.mjs <paper-dir> [--flags-only]\n",
|
|
127
|
+
});
|
|
128
|
+
const tex = writeTree(join(root, "tex"), { "paper.tex": "x" });
|
|
129
|
+
assert.match(
|
|
130
|
+
runNode(SCRIPT, [tex]).stderr,
|
|
131
|
+
/population map SKIPPED for .* — no paper\.md\/draft\.md — this checker reads markdown only/,
|
|
132
|
+
);
|
|
133
|
+
const clean = paper(
|
|
134
|
+
"clean",
|
|
135
|
+
"All 1,921 of them.",
|
|
136
|
+
`${HEAD}broad\t1,921\troot\t\tx`,
|
|
137
|
+
);
|
|
138
|
+
assert.equal(
|
|
139
|
+
runNode(SCRIPT, [clean]).stdout,
|
|
140
|
+
"population map: the registry matches the body — no stale rows, no dangling related_to.\n",
|
|
141
|
+
);
|
|
142
|
+
assert.equal(runNode(SCRIPT, [clean, "--flags-only"]).stdout, "");
|
|
143
|
+
const bad = paper(
|
|
144
|
+
"bad",
|
|
145
|
+
"We read 1,836.",
|
|
146
|
+
`${HEAD}sub\t1,836\tsubset\tnosuch\tx\nold\t7,310\tsubset\tsub\tx\nshort\n`,
|
|
147
|
+
);
|
|
148
|
+
assert.deepEqual(runNode(SCRIPT, [bad]).stdout.split("\n").slice(1), [
|
|
149
|
+
' 🔴 sub says it relates to "nosuch", which is not a row in populations.tsv',
|
|
150
|
+
" · 7,310 (old) is declared and the body no longer prints it — drop the row or mark it retired",
|
|
151
|
+
" 🔴 short has no printed quantity — the row cannot be checked against the body",
|
|
152
|
+
"",
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
@@ -234,6 +234,32 @@ const NOTES = [
|
|
|
234
234
|
assert.match(out, /clean/, "clean run did not print a verdict:\n" + out);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// ── 6. a registry with no ratchet file: every registered quantity sourced, nothing still raw ──
|
|
238
|
+
{
|
|
239
|
+
const d = paper("no-ratchet", {
|
|
240
|
+
body: "Nothing bolded here.",
|
|
241
|
+
notes: NOTES,
|
|
242
|
+
numbers: "# id\tvalue\n a\t1\n",
|
|
243
|
+
});
|
|
244
|
+
const { out } = kinds(d);
|
|
245
|
+
assert.match(
|
|
246
|
+
out,
|
|
247
|
+
/1 quantities are sourced and guarded, 0 are still raw/,
|
|
248
|
+
"no numbers-grandfathered.txt must count as zero still raw:\n" + out,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── 7. no argument: the directory it is run from ──────────────────────────────────────────
|
|
253
|
+
{
|
|
254
|
+
const d = paper("from-cwd", { body: "Nothing bolded here.", notes: NOTES });
|
|
255
|
+
const out = execFileSync("node", [SCRIPT], { cwd: d, encoding: "utf8" });
|
|
256
|
+
assert.match(
|
|
257
|
+
out,
|
|
258
|
+
/clean/,
|
|
259
|
+
"run with no argument must judge its cwd:\n" + out,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
237
263
|
rmSync(tmp, { recursive: true, force: true });
|
|
238
264
|
console.log(
|
|
239
265
|
"✓ check-provenance: no-numbers-gate fires, coverage is measured, both guards are pinned, clean run speaks",
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
* on the mutation-testing principle that a test which kills no mutant is not a test. On the live
|
|
15
15
|
* paper this check reports one finding (`no-round-ledger` — there are no rounds on file yet), so the
|
|
16
16
|
* live corpus cannot distinguish "the other twelve checks work" from "the other twelve checks are
|
|
17
|
-
* dead code". Its evidence of life is here
|
|
18
|
-
* `round-diff.mutations.mjs`.
|
|
17
|
+
* dead code". Its evidence of life is here: every case below was seen failing before it passed.
|
|
19
18
|
*
|
|
20
19
|
* 🔴 ASSERTIONS RUN AT MODULE TOP LEVEL. `vigiles test` imports the file and treats "did not throw"
|
|
21
20
|
* as a pass; an earlier harness in this directory exported a `tests` object, nothing ran, and the
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
*/
|
|
66
66
|
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
67
67
|
import { spawnSync } from "node:child_process";
|
|
68
|
-
import { join, resolve, relative } from "node:path";
|
|
68
|
+
import { basename, join, resolve, relative } from "node:path";
|
|
69
69
|
import { stripFrontmatter, frontmatterBlock } from "../../../lib/markdown.mjs";
|
|
70
70
|
import { isMain } from "./consumer.mjs";
|
|
71
71
|
|
|
@@ -274,7 +274,7 @@ const unquote = (s) => s.replace(/^["']|["']$/g, "");
|
|
|
274
274
|
* The first version fell through to the substring rule for numeric entries too, so `touches: ["2"]`
|
|
275
275
|
* matched "Section 12", "2026", and every heading containing the character 2 — a one-character
|
|
276
276
|
* declaration authorising most of the paper, with `overbroad-scope` silent because the LIST was
|
|
277
|
-
* short.
|
|
277
|
+
* short. Measured on 2026-08-10, not read off the code: disabling the nesting rule changed no
|
|
278
278
|
* verdict, because the substring fallback was quietly doing its job for it.
|
|
279
279
|
*/
|
|
280
280
|
export function covers(entry, heading) {
|
|
@@ -318,6 +318,8 @@ export function check(dir, { since } = {}) {
|
|
|
318
318
|
const at = (rev) => {
|
|
319
319
|
if (!repo) return { ok: false, err: "not a git repository" };
|
|
320
320
|
const r = git(repo, "show", `${rev}:${rel}`);
|
|
321
|
+
// A git that fails without a word on stderr (killed, or a wrapper that swallows it) still
|
|
322
|
+
// leaves the reader the revision and path it could not read.
|
|
321
323
|
return r.ok
|
|
322
324
|
? { ok: true, src: r.out }
|
|
323
325
|
: { ok: false, err: r.err || `git show ${rev}:${rel} failed` };
|
|
@@ -342,6 +344,10 @@ export function check(dir, { since } = {}) {
|
|
|
342
344
|
manifests.push(man);
|
|
343
345
|
}
|
|
344
346
|
manifests.sort((a, b) => Number(a.round ?? 0) - Number(b.round ?? 0));
|
|
347
|
+
// A manifest names its round in `round:`; one that does not is named by its file — printing
|
|
348
|
+
// `round undefined` would point the reader at nothing.
|
|
349
|
+
const label = (m) =>
|
|
350
|
+
m.round === undefined ? relOf(dir, m.path) : `round ${m.round}`;
|
|
345
351
|
const open = manifests.filter((m) => !m.closed);
|
|
346
352
|
|
|
347
353
|
if (!since && manifests.length === 0) {
|
|
@@ -353,7 +359,7 @@ export function check(dir, { since } = {}) {
|
|
|
353
359
|
if (open.length > 1) {
|
|
354
360
|
add(
|
|
355
361
|
"two-open-rounds",
|
|
356
|
-
`${open.length} manifests have no \`closed:\` (${open.map((m) =>
|
|
362
|
+
`${open.length} manifests have no \`closed:\` (${open.map((m) => label(m)).join(", ")}) — with two open rounds every budget below is checked against the wrong base.`,
|
|
357
363
|
);
|
|
358
364
|
}
|
|
359
365
|
if (!since && manifests.length > 0 && open.length === 0) {
|
|
@@ -410,13 +416,13 @@ export function check(dir, { since } = {}) {
|
|
|
410
416
|
if (cum > total) {
|
|
411
417
|
add(
|
|
412
418
|
"ratchet-cumulative",
|
|
413
|
-
`across ${manifests.length} rounds the body grew ${sign(cum)} words against ${sign(total)} of declared budget (${c0.bodyWords} → ${nowC.bodyWords}, from
|
|
419
|
+
`across ${manifests.length} rounds the body grew ${sign(cum)} words against ${sign(total)} of declared budget (${c0.bodyWords} → ${nowC.bodyWords}, from ${label(first)}'s base \`${first.base}\`). Every individual round may have been inside its own budget; this is the drift they compound into.`,
|
|
414
420
|
);
|
|
415
421
|
}
|
|
416
422
|
} else {
|
|
417
423
|
add(
|
|
418
424
|
"unresolvable-base",
|
|
419
|
-
|
|
425
|
+
`${label(first)}'s base \`${first.base}\` does not resolve (${g0.err}), so cumulative drift across ${manifests.length} rounds is UNMEASURED — not zero.`,
|
|
420
426
|
);
|
|
421
427
|
}
|
|
422
428
|
}
|
|
@@ -522,7 +528,8 @@ export function check(dir, { since } = {}) {
|
|
|
522
528
|
const num = (v, d) =>
|
|
523
529
|
v === undefined || v === null || v === "" || isNaN(Number(v)) ? d : Number(v);
|
|
524
530
|
const sign = (n) => (n > 0 ? `+${n}` : String(n));
|
|
525
|
-
|
|
531
|
+
/** `p`, which lies inside the paper directory `dir`, named from the paper's own name down. */
|
|
532
|
+
const relOf = (dir, p) => join(basename(dir), relative(dir, p));
|
|
526
533
|
|
|
527
534
|
// ── CLI ──────────────────────────────────────────────────────────────────────────────────────
|
|
528
535
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* round-diff.mjs beyond its harness: the manifest reader's edge lines, `*` coverage, the census of
|
|
3
|
+
* an empty text, and — in real git repositories — manifests without a round number, a first base
|
|
4
|
+
* that does not resolve, a removed section that is declared, more than eight new numbers, and the
|
|
5
|
+
* command with no git on PATH, no paper.md, and no directory argument.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { chmodSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { test } from "vitest";
|
|
13
|
+
import { runNode, useTempDir, writeTree } from "../../../test/support.mjs";
|
|
14
|
+
import { census, check, covers, parseManifest } from "./round-diff.mjs";
|
|
15
|
+
|
|
16
|
+
const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "round-diff.mjs");
|
|
17
|
+
const root = useTempDir("round-diff-");
|
|
18
|
+
let n = 0;
|
|
19
|
+
|
|
20
|
+
/** A git repo whose HEAD holds `base` as paper.md; the working tree then holds `now`. */
|
|
21
|
+
function repo(base, now = base, rounds = {}) {
|
|
22
|
+
const dir = join(root, `r${String(n++)}`, "paper");
|
|
23
|
+
writeTree(dir, { "paper.md": base });
|
|
24
|
+
const g = (...a) => spawnSync("git", a, { cwd: dir, encoding: "utf8" });
|
|
25
|
+
g("init", "-q");
|
|
26
|
+
g(
|
|
27
|
+
"-c",
|
|
28
|
+
"user.name=t",
|
|
29
|
+
"-c",
|
|
30
|
+
"user.email=t@t",
|
|
31
|
+
"commit",
|
|
32
|
+
"-qm",
|
|
33
|
+
"base",
|
|
34
|
+
"--allow-empty",
|
|
35
|
+
);
|
|
36
|
+
g("add", "paper.md");
|
|
37
|
+
g("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "paper");
|
|
38
|
+
const head = g("rev-parse", "HEAD").stdout.trim();
|
|
39
|
+
writeTree(dir, {
|
|
40
|
+
"paper.md": now,
|
|
41
|
+
...Object.fromEntries(
|
|
42
|
+
Object.entries(rounds).map(([f, body]) => [
|
|
43
|
+
`rounds/${f}`,
|
|
44
|
+
body.replaceAll("HEAD_SHA", head),
|
|
45
|
+
]),
|
|
46
|
+
),
|
|
47
|
+
});
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
const kinds = (r) => r.findings.map((f) => f.kind);
|
|
51
|
+
|
|
52
|
+
test("manifest: no frontmatter, comments and blanks, a list item before any key, lists both ways", () => {
|
|
53
|
+
assert.deepEqual(parseManifest("no frontmatter", "m.md"), {
|
|
54
|
+
error: "no frontmatter block",
|
|
55
|
+
path: "m.md",
|
|
56
|
+
});
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
parseManifest(
|
|
59
|
+
[
|
|
60
|
+
"---",
|
|
61
|
+
"- stray item",
|
|
62
|
+
"# a comment",
|
|
63
|
+
"",
|
|
64
|
+
"base: abc # the base",
|
|
65
|
+
"touches:",
|
|
66
|
+
" - 1",
|
|
67
|
+
" - Intro",
|
|
68
|
+
"allows: [new-number, 'new-citation']",
|
|
69
|
+
"not a key line",
|
|
70
|
+
"notes:",
|
|
71
|
+
"---",
|
|
72
|
+
"",
|
|
73
|
+
].join("\n"),
|
|
74
|
+
"m.md",
|
|
75
|
+
),
|
|
76
|
+
{
|
|
77
|
+
path: "m.md",
|
|
78
|
+
touches: ["1", "Intro"],
|
|
79
|
+
allows: ["new-number", "new-citation"],
|
|
80
|
+
base: "abc",
|
|
81
|
+
notes: [],
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("`*` covers every heading; an empty text has no hedge density", () => {
|
|
87
|
+
assert.equal(covers("*", "7. Anything"), true);
|
|
88
|
+
assert.equal(census("").hedgeDensity, 0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const P = (...secs) => secs.map(([h, t]) => `## ${h}\n\n${t}\n`).join("\n");
|
|
92
|
+
// Untouched sections, so a round that touches one or two is not an over-broad scope.
|
|
93
|
+
const REST = ["3. Three", "4. Four", "5. Five", "6. Six"].map((h) => [
|
|
94
|
+
h,
|
|
95
|
+
"Same.",
|
|
96
|
+
]);
|
|
97
|
+
const round = (r, base, touches, extra = "") =>
|
|
98
|
+
`---\n${r === null ? "" : `round: ${r}\n`}base: ${base}\ntouches: [${touches}]\nbudget: 1000\n${extra}---\n`;
|
|
99
|
+
|
|
100
|
+
test("a manifest without a round number is named by its file, not as `round undefined`; its unresolvable base is named", () => {
|
|
101
|
+
const text = P(["1. Intro", "Words here."], ...REST);
|
|
102
|
+
const dir = repo(text, text, {
|
|
103
|
+
"a.md": round(null, "deadbeef", "1", "closed: true\n"),
|
|
104
|
+
"b.md": round(null, "HEAD_SHA", "1"),
|
|
105
|
+
});
|
|
106
|
+
const r = check(dir);
|
|
107
|
+
assert.deepEqual(kinds(r), ["unresolvable-base"]);
|
|
108
|
+
assert.match(
|
|
109
|
+
r.findings[0].msg,
|
|
110
|
+
/^paper\/rounds\/a\.md's base `deadbeef` does not resolve/,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a removed section the round declares is not a finding; more than eight new numbers are elided", () => {
|
|
115
|
+
const base = P(["1. Intro", "Words."], ["2. Gone", "Old words."], ...REST);
|
|
116
|
+
const now = P(["1. Intro", "Words 1 2 3 4 5 6 7 8 9 10."], ...REST);
|
|
117
|
+
const dir = repo(base, now, { "a.md": round(1, "HEAD_SHA", "1, 2") });
|
|
118
|
+
const r = check(dir);
|
|
119
|
+
assert.deepEqual(kinds(r), ["unauthorised-number"]);
|
|
120
|
+
assert.match(
|
|
121
|
+
r.findings[0].msg,
|
|
122
|
+
/^10 numeric literal\(s\) new to the body this round \(1, 2, 3, 4, 5, 6, 7, 8, …\)/,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("the command: no paper.md is a note; no git on PATH is 'not a git repository'; no argument is the cwd", () => {
|
|
127
|
+
const empty = writeTree(join(root, "empty"), { "x.txt": "" });
|
|
128
|
+
const none = runNode(SCRIPT, [], { cwd: empty });
|
|
129
|
+
assert.equal(none.status, 0);
|
|
130
|
+
assert.match(none.stdout, new RegExp(`^ {3}no paper\\.md in ${empty}$`, "m"));
|
|
131
|
+
const dir = writeTree(join(root, "nogit"), { "paper.md": P(["1. A", "x"]) });
|
|
132
|
+
const r = runNode(SCRIPT, [dir, "--json", "--since=HEAD"], {
|
|
133
|
+
env: { PATH: dirname(process.execPath) },
|
|
134
|
+
});
|
|
135
|
+
assert.deepEqual(JSON.parse(r.stdout), [
|
|
136
|
+
{
|
|
137
|
+
kind: "unresolvable-base",
|
|
138
|
+
msg: "cannot read paper.md at `HEAD`: not a git repository. A round whose base does not resolve is a round with no gate, and it must not read as a clean run.",
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a git that fails without a word still names the revision it could not read", () => {
|
|
144
|
+
// A stand-in `git` first on PATH: it knows the repository root, and `show` exits 1 silently.
|
|
145
|
+
const bin = writeTree(join(root, "quiet-git-bin"), {
|
|
146
|
+
git: '#!/bin/sh\ncase "$1" in rev-parse) pwd ;; show) exit 1 ;; *) exit 0 ;; esac\n',
|
|
147
|
+
});
|
|
148
|
+
chmodSync(join(bin, "git"), 0o755);
|
|
149
|
+
const dir = writeTree(join(root, "quiet"), { "paper.md": P(["1. A", "x"]) });
|
|
150
|
+
const r = runNode(SCRIPT, [dir, "--json", "--since=abc"], {
|
|
151
|
+
env: { PATH: `${bin}:${dirname(process.execPath)}` },
|
|
152
|
+
});
|
|
153
|
+
assert.deepEqual(JSON.parse(r.stdout), [
|
|
154
|
+
{
|
|
155
|
+
kind: "unresolvable-base",
|
|
156
|
+
msg: "cannot read paper.md at `abc`: git show abc:paper.md failed. A round whose base does not resolve is a round with no gate, and it must not read as a clean run.",
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
});
|