paperlint 7.1.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/.github/workflows/ci.yml +3 -0
- package/.github/workflows/deadlines.yml +100 -0
- package/README.md +6 -0
- package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
- package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.js +26 -0
- package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
- package/dist/adapters/hotcrp/deadlines.js +134 -0
- package/dist/adapters/hotcrp/deadlines.js.map +1 -0
- package/dist/adapters/hotcrp/index.d.ts +2 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -1
- package/dist/adapters/hotcrp/index.js +2 -0
- package/dist/adapters/hotcrp/index.js.map +1 -1
- 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 +53 -47
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts +24 -0
- package/dist/cycle-rules.d.ts.map +1 -0
- package/dist/cycle-rules.js +212 -0
- package/dist/cycle-rules.js.map +1 -0
- package/dist/domain/cycle.d.ts +148 -0
- package/dist/domain/cycle.d.ts.map +1 -0
- package/dist/domain/cycle.js +281 -0
- package/dist/domain/cycle.js.map +1 -0
- package/dist/domain/deadline.d.ts +128 -0
- package/dist/domain/deadline.d.ts.map +1 -0
- package/dist/domain/deadline.js +281 -0
- package/dist/domain/deadline.js.map +1 -0
- package/dist/eslint-rules/cycle-stage.d.ts +39 -0
- package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
- package/dist/eslint-rules/cycle-stage.js +119 -0
- package/dist/eslint-rules/cycle-stage.js.map +1 -0
- 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 +2 -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/paper-settings.d.ts +7 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +61 -8
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/venue-deadlines.d.ts +14 -0
- package/dist/ports/venue-deadlines.d.ts.map +1 -0
- package/dist/ports/venue-deadlines.js +2 -0
- package/dist/ports/venue-deadlines.js.map +1 -0
- package/dist/presets.d.ts +12 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -11
- package/dist/presets.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.js.map +1 -1
- package/dist/submission.d.ts +8 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +41 -10
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +3 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +19 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +19 -0
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +69 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +10 -0
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +26 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +146 -20
- package/docs/e2e.md +4 -6
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- package/docs/rules/cycle/evidence.md +87 -0
- package/docs/rules/cycle/port-done.md +75 -0
- package/docs/rules/cycle/record.md +152 -0
- package/docs/rules/cycle/stage.md +85 -0
- package/docs/rules/tex/claim-provenance.md +0 -1
- package/docs/rules.md +48 -47
- package/docs/submission.md +5 -0
- package/eslint-rules/cycle-stage.test.mjs +131 -0
- package/eslint-rules/cycle-stage.ts +141 -0
- 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 +41 -2
- 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 +3 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +1 -3
- package/presets/aidc.jsonc +24 -3
- package/presets/aidc.md +6 -3
- package/presets/msr.jsonc +25 -1
- package/presets/msr.md +5 -1
- package/presets/realm.jsonc +1 -1
- package/presets/venue-profile.schema.json +39 -0
- package/scripts/check.ts +4 -0
- package/scripts/harness-api.frozen.json +0 -5
- package/scripts/refresh-deadlines.test.ts +431 -0
- package/scripts/refresh-deadlines.ts +387 -0
- 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/adapters/hotcrp/cassette/README.md +22 -0
- package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
- package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
- package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
- package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
- package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
- package/src/adapters/hotcrp/deadlines.io.ts +27 -0
- package/src/adapters/hotcrp/deadlines.test.ts +170 -0
- package/src/adapters/hotcrp/deadlines.ts +161 -0
- package/src/adapters/hotcrp/index.ts +2 -0
- 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 +41 -40
- package/src/cli.ts +57 -58
- package/src/cycle-rules.test.ts +278 -0
- package/src/cycle-rules.ts +273 -0
- package/src/domain/cycle.test.ts +456 -0
- package/src/domain/cycle.ts +432 -0
- package/src/domain/deadline.test.ts +386 -0
- package/src/domain/deadline.ts +472 -0
- 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 +7 -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/paper-settings.test.ts +167 -0
- package/src/paper-settings.ts +85 -11
- package/src/parse-args.test.ts +0 -1
- package/src/ports/venue-deadlines.ts +14 -0
- package/src/presets.test.ts +105 -0
- package/src/presets.ts +43 -10
- package/src/rule-cases.test.ts +144 -0
- package/src/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- package/src/submission.test.ts +105 -1
- package/src/submission.ts +73 -12
- package/src/tex-requirements.ts +31 -0
- package/src/tex-venue-rules.test.ts +76 -0
- package/src/tex-venue-rules.ts +85 -0
- package/src/types.ts +0 -2
- package/src/venue-rules.test.ts +82 -0
- package/src/venue-rules.ts +44 -12
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* artifact-coverage.mjs — does the released bundle actually contain what the paper points at?
|
|
4
|
-
*
|
|
5
|
-
* node artifact-coverage.mjs <paper-dir> report
|
|
6
|
-
* node artifact-coverage.mjs <paper-dir> --flags-only for hooks and pre-commit
|
|
7
|
-
*
|
|
8
|
-
* 🔴 WHY THIS EXISTS. On 2026-08-06, hours before a submission, the author asked a plain question —
|
|
9
|
-
* "did we ever actually run the artifact?" — and the answer exposed that the released bundle held NO
|
|
10
|
-
* DATA for the paper's §5.2: the agent experiment behind "stopped and asked a human in 34 of 48
|
|
11
|
-
* gated runs". That result is in the ABSTRACT, it is the single reason an agent workshop is the
|
|
12
|
-
* right venue for the paper, and the artifact reference promises "every data file behind the
|
|
13
|
-
* numbers in this paper".
|
|
14
|
-
*
|
|
15
|
-
* Six `harden-paper` passes and five review panels had not caught it, and none of them was broken.
|
|
16
|
-
* They were all asking a different question:
|
|
17
|
-
*
|
|
18
|
-
* the anonymity gate asks — does anything in the bundle leak an identity?
|
|
19
|
-
* the numbers gate asks — does a printed number match its data file?
|
|
20
|
-
* the artifact reviewer asks— does the code in the bundle run?
|
|
21
|
-
* NOBODY asked — is the data for this experiment IN THE BUNDLE AT ALL?
|
|
22
|
-
*
|
|
23
|
-
* A missing directory passes every one of those: nothing leaks, nothing mismatches, and everything
|
|
24
|
-
* present still runs. Absence is invisible to a checker that only inspects what is there.
|
|
25
|
-
*
|
|
26
|
-
* The two checks below are deliberately dumb, because dumb is what survives:
|
|
27
|
-
* 1. every body section that REPORTS A NUMBER is named somewhere in the artifact's index;
|
|
28
|
-
* 2. every path the index names EXISTS in the bundle.
|
|
29
|
-
*
|
|
30
|
-
* ── 2026-08-10: THE THIRD CHECK RUNS THE OTHER WAY ────────────────────────────────────────────
|
|
31
|
-
*
|
|
32
|
-
* 🔴 Both checks above run FORWARD, from the paper outward, and neither can see the opposite
|
|
33
|
-
* failure: a result that EXISTS ON DISK and that the paper never mentions. Ported from
|
|
34
|
-
* `check_artifact_coverage.py` in MedSci Skills (Nam, Jeong & Kim, arXiv:2606.09500v4, tag
|
|
35
|
-
* v3.8.0), where it is a Major verdict named `DISK_UNREPORTED` and defined as
|
|
36
|
-
*
|
|
37
|
-
* "an analysis output that exists on disk … is never mentioned in the manuscript. The work was
|
|
38
|
-
* done and run but its result, which may contradict the headline, is silently absent."
|
|
39
|
-
*
|
|
40
|
-
* That is a CHERRY-PICKING DETECTOR, and it is the one direction no other gate in this pipeline
|
|
41
|
-
* covers. `check-anon` reads the bundle. `paper_numbers` reads printed quantities. `arm_permutation`
|
|
42
|
-
* reads a quantity against its arm label. Every one of them starts from something the paper says.
|
|
43
|
-
* An experiment that was run and quietly dropped says nothing, so nothing looks at it.
|
|
44
|
-
*
|
|
45
|
-
* ⚠️ WHAT WE PORTED IS THE MECHANISM, NOT THE PAPER'S HEADLINE. That paper's 27/27-versus-11/27
|
|
46
|
-
* result does not survive its own artifact: seven of the 27 defects were in files never shown to
|
|
47
|
-
* the model, and one of its eleven "hits" is a substring match on the word `random` in a sentence
|
|
48
|
-
* about random forests. The honest number is 10/27, and 10/20 on what the model could see. Read
|
|
49
|
-
* `the author's private research notes` §2.4 before citing
|
|
50
|
-
* anything from it. The DETECTOR is a good idea regardless of how its evaluation was scored.
|
|
51
|
-
*
|
|
52
|
-
* 🔴 FALSE POSITIVES ARE THE WHOLE DESIGN PROBLEM, AND THE ANSWER IS AN IGNORE SET YOU CAN COUNT.
|
|
53
|
-
* `repro/` is mostly things nobody should ever report: fixtures, fetch caches, working clones,
|
|
54
|
-
* harness scratch, per-repository dumps. So this check needs an ignore set — and an ignore set that
|
|
55
|
-
* is applied silently would turn a cherry-picking detector into the very thing it exists to catch.
|
|
56
|
-
* So it is PRINTED IN FULL ON EVERY RUN, findings or none, exactly as `check_grandfather` in
|
|
57
|
-
* `repro/paper_numbers.py` prints the quantities the numbers gate does not cover. Two halves:
|
|
58
|
-
* — structural rules, in the code below, each one printed with the names it swallowed;
|
|
59
|
-
* — `repro/unreported-grandfathered.txt`, one row per knowingly-unreported analysis, and rows of
|
|
60
|
-
* the form `ships-as:<path>` are RE-VERIFIED against the bundle so that reason cannot rot.
|
|
61
|
-
*/
|
|
62
|
-
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
63
|
-
import { join } from "node:path";
|
|
64
|
-
import {
|
|
65
|
-
headings as mdHeadings,
|
|
66
|
-
splitSections,
|
|
67
|
-
requireMarkdown,
|
|
68
|
-
} from "#lib/markdown";
|
|
69
|
-
|
|
70
|
-
// Markup is parsed with a parser (`CLAUDE.md`, 2026-08-11). We fail rather than degrade: without
|
|
71
|
-
// the parser NOT A SINGLE section would be found in the paper, and the check "every section with a
|
|
72
|
-
// number is named in the artifact index" would pass clean — that is exactly the failure this file
|
|
73
|
-
// was written for (absence is invisible to a checker that looks only at what is present).
|
|
74
|
-
requireMarkdown();
|
|
75
|
-
|
|
76
|
-
const args = process.argv.slice(2);
|
|
77
|
-
const dir = args.find((a) => !a.startsWith("--"));
|
|
78
|
-
const flagsOnly = args.includes("--flags-only");
|
|
79
|
-
if (!dir) {
|
|
80
|
-
console.error("usage: node artifact-coverage.mjs <paper-dir> [--flags-only]");
|
|
81
|
-
process.exit(0);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const paperPath = ["paper.md", "draft.md"]
|
|
85
|
-
.map((f) => join(dir, f))
|
|
86
|
-
.find(existsSync);
|
|
87
|
-
// 🔴 A SILENT exit(0) here read as «clean» for four of five papers. Measured 2026-08-25: only
|
|
88
|
-
// <paper-a> has a markdown source; agenticdev/aisec/<paper-e>/scored ship
|
|
89
|
-
// `paper.tex`, so this checker printed NOTHING and returned 0 on the camera-ready — which has
|
|
90
|
-
// an artifact (reproduce.py, data/) it was built to inspect. Nothing anywhere said so, and the
|
|
91
|
-
// CI step mirrored the same condition, so both layers agreed to say nothing. A skip that cannot
|
|
92
|
-
// be told apart from a pass is the failure this whole file exists to prevent.
|
|
93
|
-
if (!paperPath) {
|
|
94
|
-
console.error(
|
|
95
|
-
`⏭️ artifact coverage SKIPPED for ${dir} — no paper.md/draft.md. This checker\n reads markdown only; a .tex paper is NOT covered by it. Absence of findings here is\n absence of checking, not a clean bill.`,
|
|
96
|
-
);
|
|
97
|
-
process.exit(0);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// The bundle is whatever directory the paper releases. `artifact-anon` is the anonymised one that
|
|
101
|
-
// actually ships; a working `repro/` is NOT the release and must never be treated as one.
|
|
102
|
-
const bundle = [join(dir, "repro/artifact-anon"), join(dir, "artifact")].find(
|
|
103
|
-
existsSync,
|
|
104
|
-
);
|
|
105
|
-
if (!bundle) {
|
|
106
|
-
console.error(
|
|
107
|
-
`⏭️ artifact coverage SKIPPED for ${dir} — no repro/artifact-anon and no artifact/ directory.`,
|
|
108
|
-
);
|
|
109
|
-
process.exit(0);
|
|
110
|
-
}
|
|
111
|
-
const indexPath = ["NUMBERS.md", "README.md"]
|
|
112
|
-
.map((f) => join(bundle, f))
|
|
113
|
-
.find(existsSync);
|
|
114
|
-
if (!indexPath) {
|
|
115
|
-
const msg = `${bundle} has no NUMBERS.md and no README.md — nothing maps the paper's numbers to its data`;
|
|
116
|
-
console.error(`📦 artifact — ${msg}`);
|
|
117
|
-
process.exit(1);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const paper = readFileSync(paperPath, "utf8").replace(/<!--[\s\S]*?-->/g, "");
|
|
121
|
-
const index = readFileSync(indexPath, "utf8");
|
|
122
|
-
const findings = [];
|
|
123
|
-
|
|
124
|
-
// ---- 1. every number-reporting section is represented in the index --------------------------
|
|
125
|
-
// "Reports a number" means a BOLD span containing digits — the paper's own convention for a figure
|
|
126
|
-
// it is asserting, as opposed to a section number or a citation. A section that merely mentions
|
|
127
|
-
// §4.3 in passing does not trip this.
|
|
128
|
-
// 2026-08-11: both the body boundary and the split into sections come from the parser. The former
|
|
129
|
-
// `^## ` / `^### ` counted any line with hashes as a section, including hashes inside a ```-block:
|
|
130
|
-
// a paper QUOTING somebody else's `## 4.2 …` in an example got an extra "section", and its bold
|
|
131
|
-
// numbers got a requirement to be named in the artifact index. The `-1 / >>> 0` convention is kept:
|
|
132
|
-
// the former expression did not treat a heading on the very first line of the file as a boundary
|
|
133
|
-
// either.
|
|
134
|
-
const freeH = mdHeadings(paper).find(
|
|
135
|
-
(h) => h.depth === 2 && /^(Limitations|References|Ethical)/u.test(h.text),
|
|
136
|
-
);
|
|
137
|
-
const body = paper.slice(0, (freeH ? freeH.offset : -1) >>> 0 || paper.length);
|
|
138
|
-
const SECTION_NUM = /^(\d+(?:\.\d+)?)\.?\s+(.+)$/u;
|
|
139
|
-
const SUBSECTION_NUM = /^(\d+\.\d+)\s+(.+)$/u;
|
|
140
|
-
for (const chunk of splitSections(body, { min: 2, max: 2 })) {
|
|
141
|
-
const h = chunk.heading && SECTION_NUM.exec(chunk.heading.text);
|
|
142
|
-
if (!h) continue;
|
|
143
|
-
const subs = splitSections(chunk.raw, { min: 3, max: 3 });
|
|
144
|
-
for (const subSec of subs) {
|
|
145
|
-
// `subs.length === 1` means "there are no subsections at all", exactly what used to be checked
|
|
146
|
-
// as `sub === chunk`: the numbers are then counted against the section itself. If subsections DO
|
|
147
|
-
// exist, the section's leading chunk (before the first `###`) is not checked — as it was before.
|
|
148
|
-
const sh = subSec.heading
|
|
149
|
-
? SUBSECTION_NUM.exec(subSec.heading.text)
|
|
150
|
-
: subs.length === 1
|
|
151
|
-
? [null, h[1], h[2]]
|
|
152
|
-
: null;
|
|
153
|
-
if (!sh) continue;
|
|
154
|
-
const sub = subSec.raw;
|
|
155
|
-
const num = sh[1];
|
|
156
|
-
// Bold spans with digits that are not section pointers or bracketed citations.
|
|
157
|
-
const reported = (
|
|
158
|
-
sub
|
|
159
|
-
.replace(/§\s*\d+(\.\d+)*/g, "")
|
|
160
|
-
.replace(/\[[\d,\s–-]+\]/g, "")
|
|
161
|
-
.match(/\*\*[^*]*\d[^*]*\*\*/g) || []
|
|
162
|
-
).length;
|
|
163
|
-
if (!reported) continue;
|
|
164
|
-
const named = new RegExp(
|
|
165
|
-
`§\\s*${num.replace(".", "\\.")}\\b|\\b${num.replace(".", "\\.")}\\b`,
|
|
166
|
-
).test(index);
|
|
167
|
-
if (!named)
|
|
168
|
-
findings.push(
|
|
169
|
-
`§${num} reports ${reported} bolded figure(s) and is named NOWHERE in ` +
|
|
170
|
-
`${indexPath.replace(dir + "/", "")} — the released bundle may hold no data for it`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ---- 2. every path the index names actually exists -------------------------------------------
|
|
176
|
-
// Backtick-quoted things that look like a path into the bundle. A promise to a file that is not
|
|
177
|
-
// there is worse than no promise: it reads as released.
|
|
178
|
-
const seen = new Set();
|
|
179
|
-
for (const m of index.matchAll(/`([A-Za-z0-9_./-]+\/[A-Za-z0-9_.*/-]+)`/g)) {
|
|
180
|
-
let p = m[1];
|
|
181
|
-
if (seen.has(p) || p.includes("*") || p.startsWith("http")) continue;
|
|
182
|
-
seen.add(p);
|
|
183
|
-
// A paper about repositories names a LOT of `owner/repo`, and those are not paths. Require a file
|
|
184
|
-
// extension: it is the one signal that separates `census/rows.tsv` from `saleor/apps`. Caught on
|
|
185
|
-
// the first run, when three GitHub repositories were reported as missing directories.
|
|
186
|
-
if (!/\.[a-z0-9]{1,5}$/i.test(p)) continue;
|
|
187
|
-
// `repro/` is the WORKING tree, deliberately not in the anonymised bundle — the whole reason the
|
|
188
|
-
// bundle exists separately is that repro holds real names in fixtures.
|
|
189
|
-
if (p.startsWith("repro/")) continue;
|
|
190
|
-
// A directory prefix is enough: the index names files inside runs that may be regenerated.
|
|
191
|
-
const head = p.split("/")[0];
|
|
192
|
-
if (!existsSync(join(bundle, head)) && !readdirSync(bundle).includes(head))
|
|
193
|
-
findings.push(
|
|
194
|
-
`the index names \`${p}\` and \`${head}\` is not in the bundle`,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ---- 3. reverse: a result on disk that neither the paper nor the index mentions ---------------
|
|
199
|
-
// The unit is an EXPERIMENT DIRECTORY under `repro/` that holds a headline write-up — this
|
|
200
|
-
// project's own convention, stated in `repro/README.md`: "you go into the folder, and there is a
|
|
201
|
-
// RESULTS.md with the full write-up". A directory with a RESULTS.md is a run somebody finished and
|
|
202
|
-
// wrote up.
|
|
203
|
-
//
|
|
204
|
-
// "Mentioned" is: the directory's name, or its name with a trailing -YYYY-MM-DD stripped, appears
|
|
205
|
-
// in the paper OR in the released index. Matching is LITERAL on purpose. Prefix or fuzzy matching
|
|
206
|
-
// would quietly absolve `gate-effect-spike-2026-08-05` on the strength of the shipped
|
|
207
|
-
// `gate-effect/`, and a false NEGATIVE in a cherry-picking detector is the failure that costs
|
|
208
|
-
// something. A rename is therefore a row in the ignore file, where it is visible and re-verified,
|
|
209
|
-
// rather than an inference nobody can see.
|
|
210
|
-
//
|
|
211
|
-
// 🔴 THIS LEG READS EVERY INDEX FILE, NOT JUST THE FIRST. Check 1 above resolves `indexPath` to
|
|
212
|
-
// NUMBERS.md *or* README.md and stops, which is right for it: it asks whether the map from numbers
|
|
213
|
-
// to data exists. Reused here it produced three false positives on the first run —
|
|
214
|
-
// `enforcement-surface`, `ladder-prototype` and `tier-a-prime` are all named in the bundle's
|
|
215
|
-
// README.md and in no other file, so a reverse check reading only NUMBERS.md accused the bundle of
|
|
216
|
-
// hiding three analyses it ships and documents. A cherry-picking detector that cries wolf gets
|
|
217
|
-
// muted, and a muted detector is how the failure happens twice.
|
|
218
|
-
const ledger = [];
|
|
219
|
-
const HEADLINE = /^(RESULTS|SUMMARY|FINDINGS|METRICS)\.(md|json|tsv|csv|txt)$/i;
|
|
220
|
-
const repro = join(dir, "repro");
|
|
221
|
-
const bundleName = bundle
|
|
222
|
-
.slice(dir.length + 1)
|
|
223
|
-
.split("/")
|
|
224
|
-
.pop();
|
|
225
|
-
const indexAll = ["NUMBERS.md", "README.md"]
|
|
226
|
-
.map((f) => join(bundle, f))
|
|
227
|
-
.filter(existsSync)
|
|
228
|
-
.map((f) => readFileSync(f, "utf8"))
|
|
229
|
-
.join("\n");
|
|
230
|
-
|
|
231
|
-
if (!existsSync(repro)) {
|
|
232
|
-
ledger.push(
|
|
233
|
-
" reverse check (results on disk nobody reports): SKIPPED — no repro/ directory",
|
|
234
|
-
);
|
|
235
|
-
} else {
|
|
236
|
-
// Structural ignores. Each is a RULE, and each prints what it swallowed.
|
|
237
|
-
const swallowed = new Map([
|
|
238
|
-
[
|
|
239
|
-
`the released bundle itself (${bundleName}/) — it is the shipped copy of these same analyses, ` +
|
|
240
|
-
"so counting it would report every one of them a second time",
|
|
241
|
-
[],
|
|
242
|
-
],
|
|
243
|
-
[
|
|
244
|
-
"dot-directories and build/dependency trees (node_modules, __pycache__)",
|
|
245
|
-
[],
|
|
246
|
-
],
|
|
247
|
-
]);
|
|
248
|
-
const allowPath = join(repro, "unreported-grandfathered.txt");
|
|
249
|
-
const allowed = new Map();
|
|
250
|
-
if (existsSync(allowPath)) {
|
|
251
|
-
for (const line of readFileSync(allowPath, "utf8").split("\n")) {
|
|
252
|
-
const row = line.split("#")[0].trimEnd();
|
|
253
|
-
if (!row.trim()) continue;
|
|
254
|
-
const [name, reason, ...rest] = row.split("\t").map((c) => c.trim());
|
|
255
|
-
if (name)
|
|
256
|
-
allowed.set(
|
|
257
|
-
name,
|
|
258
|
-
[reason, ...rest].filter(Boolean).join(" — ") || "(NO REASON GIVEN)",
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const candidates = [];
|
|
264
|
-
for (const name of readdirSync(repro)) {
|
|
265
|
-
const p = join(repro, name);
|
|
266
|
-
if (!statSync(p).isDirectory()) continue;
|
|
267
|
-
if (name === bundleName) {
|
|
268
|
-
swallowed.get([...swallowed.keys()][0]).push(name);
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
if (
|
|
272
|
-
name.startsWith(".") ||
|
|
273
|
-
name === "node_modules" ||
|
|
274
|
-
name === "__pycache__"
|
|
275
|
-
) {
|
|
276
|
-
swallowed.get([...swallowed.keys()][1]).push(name);
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
const heads = readdirSync(p).filter((f) => HEADLINE.test(f));
|
|
280
|
-
if (heads.length) candidates.push({ name, heads });
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// 🔴 WHOLE-TOKEN, AND NEVER ON A SHORT SLUG. `includes()` was the first version and it acquitted
|
|
284
|
-
// a directory called `a-2026-01-01` on the strength of the letter "a" appearing in the prose —
|
|
285
|
-
// caught by the harness case that asserts the stated finding count. Substring matching in a
|
|
286
|
-
// detector like this fails in the dangerous direction: it invents evidence that a result was
|
|
287
|
-
// mentioned. Hyphens count as part of the token, so `gate-effect` does not match inside
|
|
288
|
-
// `gate-effect-spike`, and a slug shorter than four characters is not used for matching at all.
|
|
289
|
-
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
290
|
-
const mentions = (t, s) =>
|
|
291
|
-
s.length >= 4 && new RegExp(`(?<![\\w-])${esc(s)}(?![\\w-])`).test(t);
|
|
292
|
-
|
|
293
|
-
const reported = [],
|
|
294
|
-
ignored = [],
|
|
295
|
-
unreported = [];
|
|
296
|
-
for (const c of candidates) {
|
|
297
|
-
const slug = c.name.replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
298
|
-
const named = (t) => mentions(t, c.name) || mentions(t, slug);
|
|
299
|
-
if (named(paper)) {
|
|
300
|
-
reported.push(`${c.name} (named in the paper)`);
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if (named(indexAll)) {
|
|
304
|
-
reported.push(`${c.name} (named in the index)`);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (allowed.has(c.name)) {
|
|
308
|
-
ignored.push([c.name, allowed.get(c.name)]);
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
unreported.push(c);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// A `ships-as:` reason carries a witness, so it is checked rather than believed. Everything else
|
|
315
|
-
// is prose and is reprinted verbatim for a human to disbelieve.
|
|
316
|
-
const stale = [];
|
|
317
|
-
for (const [name, reason] of ignored) {
|
|
318
|
-
const m = /^ships-as:(\S+)/.exec(reason);
|
|
319
|
-
if (m && !existsSync(join(bundle, m[1])))
|
|
320
|
-
stale.push(
|
|
321
|
-
`the ignore row for \`${name}\` claims it ships as \`${m[1]}\`, and that path is ` +
|
|
322
|
-
"not in the bundle — the allowance has rotted and is now hiding a real absence",
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
for (const name of allowed.keys())
|
|
326
|
-
if (!candidates.some((c) => c.name === name))
|
|
327
|
-
stale.push(
|
|
328
|
-
`the ignore row for \`${name}\` names no directory under repro/ that holds a ` +
|
|
329
|
-
"headline result — retire the row rather than leaving it to grow stale",
|
|
330
|
-
);
|
|
331
|
-
findings.push(...stale);
|
|
332
|
-
|
|
333
|
-
for (const c of unreported)
|
|
334
|
-
findings.push(
|
|
335
|
-
`DISK_UNREPORTED repro/${c.name}/ holds ${c.heads.join(", ")} and is named neither in the ` +
|
|
336
|
-
"paper nor in the released index — an analysis that ran and whose result a reader cannot reach",
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
ledger.push(
|
|
340
|
-
"",
|
|
341
|
-
` ── what the reverse check ignored, in full (${candidates.length} candidate analyses under repro/) ──`,
|
|
342
|
-
" An ignore set applied in silence would make this a cherry-picking detector that cherry-picks,",
|
|
343
|
-
" so it is printed whether or not anything was found. Same discipline as check_grandfather in",
|
|
344
|
-
" repro/paper_numbers.py: the uncovered set is a list you can count, not an absence of warnings.",
|
|
345
|
-
);
|
|
346
|
-
for (const [rule, names] of swallowed)
|
|
347
|
-
ledger.push(
|
|
348
|
-
` • ${names.length} before they were counted — ${rule}`,
|
|
349
|
-
...(names.length ? [` ${names.join(", ")}`] : []),
|
|
350
|
-
);
|
|
351
|
-
ledger.push(
|
|
352
|
-
` • ${reported.length} reported: ${reported.join(" · ") || "(none)"}`,
|
|
353
|
-
);
|
|
354
|
-
ledger.push(
|
|
355
|
-
` • ${ignored.length} in repro/unreported-grandfathered.txt — every row, every reason:`,
|
|
356
|
-
);
|
|
357
|
-
for (const [name, reason] of ignored)
|
|
358
|
-
ledger.push(` ${name} → ${reason}`);
|
|
359
|
-
if (!ignored.length)
|
|
360
|
-
ledger.push(" (the file is empty — nothing is being waved through)");
|
|
361
|
-
ledger.push(
|
|
362
|
-
` • ${unreported.length} left over, and those are the findings above.`,
|
|
363
|
-
` Ratio: ${unreported.length} reported as unreported, ${ignored.length} deliberately ignored, ` +
|
|
364
|
-
`${reported.length} reachable. If the ignored column ever dwarfs the other two, this check has`,
|
|
365
|
-
" become a formality and the honest move is to say so rather than to keep running it.",
|
|
366
|
-
"",
|
|
367
|
-
" 🔴 WHAT THIS LEG IS SILENT ABOUT, stated so nobody reads its silence as coverage:",
|
|
368
|
-
" — an analysis SHIPPED in the bundle but never discussed in prose is counted as reported here,",
|
|
369
|
-
" because the index is treated as the paper's delegate. MedSci's DISK_UNREPORTED is scoped to",
|
|
370
|
-
" the manuscript alone and would flag those; ours would not.",
|
|
371
|
-
" — a result that was never written up (no RESULTS.md) is invisible. Deleting the write-up is a",
|
|
372
|
-
" one-command way past this check, and nothing here can tell that from an experiment nobody",
|
|
373
|
-
" finished.",
|
|
374
|
-
' — nothing reads the CONTENT of a result, so "mentioned" and "reported honestly" are different',
|
|
375
|
-
" things and only the first is checked.",
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const header = `📦 artifact coverage — ${findings.length} finding(s) in ${dir}`;
|
|
380
|
-
if (findings.length) {
|
|
381
|
-
const out = flagsOnly ? console.error : console.log;
|
|
382
|
-
out(header);
|
|
383
|
-
findings.forEach((f) => out(" " + f));
|
|
384
|
-
out(
|
|
385
|
-
" absence is invisible to the leak gate, the numbers gate and the artifact runner:",
|
|
386
|
-
);
|
|
387
|
-
out(" nothing leaks, nothing mismatches, and what IS there still runs.");
|
|
388
|
-
ledger.forEach((l) => out(l));
|
|
389
|
-
process.exit(1);
|
|
390
|
-
}
|
|
391
|
-
if (!flagsOnly) {
|
|
392
|
-
console.log(header);
|
|
393
|
-
console.log(
|
|
394
|
-
" every number-reporting section is indexed, every named path exists",
|
|
395
|
-
);
|
|
396
|
-
ledger.forEach((l) => console.log(l));
|
|
397
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* artifact-coverage.mjs on the inputs its harness does not build: no argument, no markdown paper,
|
|
3
|
-
* a free-section heading on the paper's very first line, a numbered section without subsections,
|
|
4
|
-
* an index naming the same path twice / a glob / a URL / a repro path, no repro/ at all, a
|
|
5
|
-
* grandfather row without a reason, and a plain file among the repro/ entries.
|
|
6
|
-
*/
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { dirname, join } from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { test } from "vitest";
|
|
11
|
-
import { runNode, useTempDir, writeTree } from "../../../test/support.ts";
|
|
12
|
-
|
|
13
|
-
const SCRIPT = join(
|
|
14
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
15
|
-
"artifact-coverage.mjs",
|
|
16
|
-
);
|
|
17
|
-
const root = useTempDir("artifact-coverage-");
|
|
18
|
-
const run = (...args: string[]) => runNode(SCRIPT, args);
|
|
19
|
-
|
|
20
|
-
test("no argument prints the usage; a paper with no markdown source is a named skip", () => {
|
|
21
|
-
assert.deepEqual(run(), {
|
|
22
|
-
status: 0,
|
|
23
|
-
stdout: "",
|
|
24
|
-
stderr: "usage: node artifact-coverage.mjs <paper-dir> [--flags-only]\n",
|
|
25
|
-
});
|
|
26
|
-
const tex = writeTree(join(root, "tex"), { "paper.tex": "x" });
|
|
27
|
-
const r = run(tex);
|
|
28
|
-
assert.equal(r.status, 0);
|
|
29
|
-
assert.match(
|
|
30
|
-
r.stderr,
|
|
31
|
-
/artifact coverage SKIPPED for .* — no paper\.md\/draft\.md/,
|
|
32
|
-
);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("a section without subsections is checked against its own number; the index's odd paths are passed over", () => {
|
|
36
|
-
const dir = writeTree(join(root, "odd"), {
|
|
37
|
-
"paper.md": "## 3. Results\n\nWe measured **42 %** of runs.\n",
|
|
38
|
-
"artifact/README.md": [
|
|
39
|
-
"Section 7 only.",
|
|
40
|
-
"`data/rows.tsv` and again `data/rows.tsv`",
|
|
41
|
-
"`runs/*/out.json`",
|
|
42
|
-
"`https://example.org/x.json`",
|
|
43
|
-
"`repro/work/tmp.json`",
|
|
44
|
-
].join("\n"),
|
|
45
|
-
});
|
|
46
|
-
const r = run(dir);
|
|
47
|
-
assert.match(
|
|
48
|
-
r.stdout,
|
|
49
|
-
/§3 reports 1 bolded figure\(s\) and is named NOWHERE/,
|
|
50
|
-
);
|
|
51
|
-
assert.match(
|
|
52
|
-
r.stdout,
|
|
53
|
-
/the index names `data\/rows\.tsv` and `data` is not in the bundle/,
|
|
54
|
-
);
|
|
55
|
-
assert.equal(r.stdout.match(/rows\.tsv/g)?.length, 1);
|
|
56
|
-
assert.doesNotMatch(r.stdout, /out\.json|example\.org|tmp\.json/);
|
|
57
|
-
assert.match(
|
|
58
|
-
r.stdout,
|
|
59
|
-
/reverse check \(results on disk nobody reports\): SKIPPED — no repro\/ directory/,
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("a free-section heading on the first line does not end the body at offset 0", () => {
|
|
64
|
-
const dir = writeTree(join(root, "first-line"), {
|
|
65
|
-
"paper.md": "## Limitations\n\nNone.\n\n## 2. Method\n\nIt took **9 s**.\n",
|
|
66
|
-
"artifact/README.md": "nothing\n",
|
|
67
|
-
});
|
|
68
|
-
assert.match(run(dir).stdout, /§2 reports 1 bolded figure\(s\)/);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("repro/: a plain file is not a candidate, a dot-directory is swallowed and named; a grandfather row with no reason says so", () => {
|
|
72
|
-
const dir = writeTree(join(root, "grandfathered"), {
|
|
73
|
-
"paper.md": "## 1. Intro\n\nText.\n",
|
|
74
|
-
"artifact/README.md": "§1\n",
|
|
75
|
-
"repro/notes.txt": "a file, not an analysis",
|
|
76
|
-
"repro/.cache/RESULTS.md": "a dot-directory, swallowed before counting",
|
|
77
|
-
"repro/old-run/RESULTS.md": "done\n",
|
|
78
|
-
"repro/unreported-grandfathered.txt": "old-run\n",
|
|
79
|
-
});
|
|
80
|
-
const out = run(dir).stdout;
|
|
81
|
-
assert.match(out, / {7}old-run {2}→ {2}\(NO REASON GIVEN\)/);
|
|
82
|
-
assert.match(
|
|
83
|
-
out,
|
|
84
|
-
/• 1 before they were counted — dot-directories and build\/dependency trees[^\n]*\n {7}\.cache/,
|
|
85
|
-
);
|
|
86
|
-
});
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* check-provenance — does the paper have a build-time numbers gate, and what does it still NOT
|
|
4
|
-
* cover.
|
|
5
|
-
*
|
|
6
|
-
* 🔴 THREE OF THE FIVE CHECKS MOVED INTO ESLint RULES ON 2026-08-26
|
|
7
|
-
* (`eslint-rules/paper-registry.mjs`):
|
|
8
|
-
*
|
|
9
|
-
* arm-mismatch → `paper/number-arm-mismatch`
|
|
10
|
-
* untraced-number → `paper/number-untraced`
|
|
11
|
-
* arm-unlabelled → `paper/number-arm-unlabelled`
|
|
12
|
-
*
|
|
13
|
-
* All three spoke about a SENTENCE IN THE PAPER, while the provenance files (`NOTES.md`,
|
|
14
|
-
* `CLAIMS.md`, `repro/artifact-anon/NUMBERS.md`, `NUMBER-ARMS.tsv`) were CONFIGURATION for them —
|
|
15
|
-
* what sets which sentence counts as defective. Exactly like a venue profile for `pdf/profile`.
|
|
16
|
-
* Parity was proved BEFORE the deletion: the real paper (5 `arm-mismatch` + 5 `untraced` numbers,
|
|
17
|
-
* the sets matched) plus 17 fixtures. The write-up is in
|
|
18
|
-
* `the author's private research notes`.
|
|
19
|
-
*
|
|
20
|
-
* WHAT IS LEFT HERE, AND WHY THAT IS NOT LAZINESS. The two remaining checks **do not read the body
|
|
21
|
-
* of the paper at all** — all they are given is the directory:
|
|
22
|
-
*
|
|
23
|
-
* no-numbers-gate the paper has no `repro/paper_numbers.py`, that is, every printed quantity
|
|
24
|
-
* here is a hand-typed digit whose only link to its data file is somebody's
|
|
25
|
-
* memory.
|
|
26
|
-
* numbers-coverage how many quantities are sourced and how many are still raw (the
|
|
27
|
-
* `numbers-grandfathered.txt` ratchet). This is NOT A FINDING AT ALL, it is a
|
|
28
|
-
* coverage measurement — ESLint has no "for information" severity, and throwing
|
|
29
|
-
* it away would mean losing the only line that answers "does the gate cover what
|
|
30
|
-
* a reviewer will check?".
|
|
31
|
-
*
|
|
32
|
-
* A check that does not read the file being linted cannot be a rule about it — that discriminator
|
|
33
|
-
* decided both.
|
|
34
|
-
*
|
|
35
|
-
* ═══ THE FAILURE STORY that made this file exist at all (kept verbatim) ═══
|
|
36
|
-
* 2026-08-05, 29 hours before the deadline. The paper's headline read "147 real rules files exactly
|
|
37
|
-
* as their projects committed them, 22 fail admission". In the artifact's aggregate
|
|
38
|
-
* `.all.annotated.failedContradicted = 22` — on the files AS COMMITTED nothing fails. 22 is the
|
|
39
|
-
* counterfactual arm, where the extractor's guesses are written in as if the author had written
|
|
40
|
-
* them.
|
|
41
|
-
*
|
|
42
|
-
* Not one check in the pipeline saw this, and the reason is structural: each looks at ONE side.
|
|
43
|
-
* verify-citations checks that a reference is real. The claims-preservation diff checks that a
|
|
44
|
-
* claim did not grow (and it did not grow, 22 was always 22). The artifact reviewer recomputes the
|
|
45
|
-
* number from the data, and it agrees, because the number is CORRECT. The panel reads the prose and
|
|
46
|
-
* takes "unmodified" as a fact. The defect lives only in the JOINT between the sentence and the
|
|
47
|
-
* path to the field, and nobody held both sides at once. `paper/number-arm-mismatch` holds them
|
|
48
|
-
* now.
|
|
49
|
-
*
|
|
50
|
-
* Advisory. Exit 0 always; findings on stdout.
|
|
51
|
-
*/
|
|
52
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
53
|
-
import { join, resolve } from "node:path";
|
|
54
|
-
import { isMain } from "./consumer.mjs";
|
|
55
|
-
|
|
56
|
-
// 🔴 THERE IS NO `markdown.mjs` IMPORT ANY MORE, AND THAT IS A CONSEQUENCE OF THE MOVE, NOT A
|
|
57
|
-
// SIMPLIFICATION. There is nothing left here to parse markup with and no reason to: both remaining
|
|
58
|
-
// checks look at the DIRECTORY, not at the text. The bibliography boundaries (`## References`) that
|
|
59
|
-
// `requireMarkdown()` stood for are now needed by the rules in `eslint-rules/paper-registry.mjs`,
|
|
60
|
-
// and there ESLint itself provides them.
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Has this paper adopted the build-time numbers gate, and how far?
|
|
64
|
-
*
|
|
65
|
-
* SUPERSEDED, 2026-08-05, same day it was written. The registry below (`NUMBER-ARMS.tsv`) keyed its
|
|
66
|
-
* rules on the NUMBER — "wherever 22 appears, one of these phrases must be near it". That is wrong
|
|
67
|
-
* in a way that shows up immediately in a real paper: `<paper-a>` prints two unrelated 22s
|
|
68
|
-
* (22 files that fail admission in the counterfactual arm, and 8 of 22 abstentions in a paired
|
|
69
|
-
* sample), and a number-keyed rule cannot tell them apart. It fires on the innocent one and, worse,
|
|
70
|
-
* would go on passing if the guilty one were reworded.
|
|
71
|
-
*
|
|
72
|
-
* The replacement keys on the OCCURRENCE: the paper writes `{{annotated.failedContradicted}}`, not
|
|
73
|
-
* `22`, so each printed quantity carries its own identity and its own guard, and the build refuses
|
|
74
|
-
* a PDF where the value has gone stale or the surrounding prose claims the wrong arm. See
|
|
75
|
-
* `<paper>/repro/paper_numbers.py`. That gate is per-paper because the data layout is per-paper;
|
|
76
|
-
* what belongs HERE is the generic question: does this paper have one, and what does it still leave
|
|
77
|
-
* uncovered?
|
|
78
|
-
*
|
|
79
|
-
* ⚠️ Reading `NUMBER-ARMS.tsv` (the `arm-unlabelled` finding) moved on 2026-08-26 into the rule
|
|
80
|
-
* `paper/number-arm-unlabelled`; the registry still works for a paper that has one.
|
|
81
|
-
*/
|
|
82
|
-
function checkNumbersGate(dir) {
|
|
83
|
-
const out = [];
|
|
84
|
-
const gate = join(dir, "repro", "paper_numbers.py");
|
|
85
|
-
const registry = join(dir, "repro", "numbers.tsv");
|
|
86
|
-
if (!existsSync(gate)) {
|
|
87
|
-
out.push({
|
|
88
|
-
kind: "no-numbers-gate",
|
|
89
|
-
msg:
|
|
90
|
-
"no repro/paper_numbers.py — every printed figure here is a hand-typed digit whose only " +
|
|
91
|
-
"link to its data file is somebody's memory. Copy the gate from a paper that already has one; the " +
|
|
92
|
-
"defect it catches (right number, wrong experimental arm) is invisible to every other check",
|
|
93
|
-
});
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
if (!existsSync(registry)) return out;
|
|
97
|
-
const rows = readFileSync(registry, "utf8")
|
|
98
|
-
.split("\n")
|
|
99
|
-
.filter((l) => l.trim() && !l.startsWith("#")).length;
|
|
100
|
-
const allow = join(dir, "repro", "numbers-grandfathered.txt");
|
|
101
|
-
const raw = existsSync(allow)
|
|
102
|
-
? readFileSync(allow, "utf8")
|
|
103
|
-
.split("\n")
|
|
104
|
-
.filter((l) => l.split("#")[0].trim()).length
|
|
105
|
-
: 0;
|
|
106
|
-
// Not a finding — a fact the scorecard should carry, because "we have a gate" and "the gate
|
|
107
|
-
// covers the numbers a reviewer will check" are different claims and only the second matters.
|
|
108
|
-
out.push({
|
|
109
|
-
kind: "numbers-coverage",
|
|
110
|
-
msg:
|
|
111
|
-
`${rows} quantities are sourced and guarded, ${raw} are still raw and grandfathered — ` +
|
|
112
|
-
`the grandfathered ones are an admission, not a clearance`,
|
|
113
|
-
});
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Why the guards were pulled out INTO A SEPARATE function instead of being left as early
|
|
119
|
-
* `return`s inside `checkProvenance` — measured 2026-08-26.
|
|
120
|
-
*
|
|
121
|
-
* 🔴 The script printed `🔢 check-provenance — clean` for `<paper-b>` and `<paper-c>` without
|
|
122
|
-
* checking anything at all: both papers are in LaTeX, they have no `paper.md`, `find(existsSync)`
|
|
123
|
-
* returned `undefined`, and the function exited with an empty array. That is, **"you have no
|
|
124
|
-
* numbers gate" was muted on exactly those papers that do not have one** — `repro/paper_numbers.py`
|
|
125
|
-
* exists only in `<paper-a>`. A confident green on three papers out of four.
|
|
126
|
-
*
|
|
127
|
-
* A skip indistinguishable from a pass is the same failure as "0 checks" versus "checks passed" in
|
|
128
|
-
* the PR interface. The neighbours in this directory (`artifact-coverage.mjs`,
|
|
129
|
-
* `population-map.mjs`) have long printed `⏭️ SKIPPED … absence of findings here is absence of
|
|
130
|
-
* checking, not a clean bill`; this file was the only one that stayed silent.
|
|
131
|
-
*
|
|
132
|
-
* The guard is ONE for both consumers (the findings list and the CLI output), so that the reason
|
|
133
|
-
* for the skip cannot drift apart from the skip itself.
|
|
134
|
-
*/
|
|
135
|
-
export function provenanceScope(dir) {
|
|
136
|
-
const paper = ["paper.md", "draft.md"]
|
|
137
|
-
.map((f) => join(dir, f))
|
|
138
|
-
.find(existsSync);
|
|
139
|
-
if (!paper) {
|
|
140
|
-
return {
|
|
141
|
-
covered: false,
|
|
142
|
-
reason:
|
|
143
|
-
"no paper.md/draft.md — this checker reads markdown only, a .tex paper is NOT covered",
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
const prov = [
|
|
147
|
-
"NOTES.md",
|
|
148
|
-
"CLAIMS.md",
|
|
149
|
-
join("repro", "artifact-anon", "NUMBERS.md"),
|
|
150
|
-
]
|
|
151
|
-
.map((f) => join(dir, f))
|
|
152
|
-
.filter(existsSync)
|
|
153
|
-
.map((f) => readFileSync(f, "utf8"))
|
|
154
|
-
.join("\n");
|
|
155
|
-
if (!prov) {
|
|
156
|
-
return {
|
|
157
|
-
covered: false,
|
|
158
|
-
reason:
|
|
159
|
-
"no non-empty NOTES.md / CLAIMS.md / repro/artifact-anon/NUMBERS.md",
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
return { covered: true, reason: null };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function checkProvenance(dir) {
|
|
166
|
-
if (!provenanceScope(dir).covered) return [];
|
|
167
|
-
return [...checkNumbersGate(dir)];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (isMain(import.meta.url)) {
|
|
171
|
-
const dir = resolve(process.argv[2] ?? ".");
|
|
172
|
-
const f = checkProvenance(dir);
|
|
173
|
-
if (f.length) {
|
|
174
|
-
console.log(`🔢 check-provenance — ${f.length} finding(s):`);
|
|
175
|
-
for (const x of f) console.log(` [${x.kind}] ${x.msg}`);
|
|
176
|
-
} else {
|
|
177
|
-
// A skip and a pass print as DIFFERENT lines — see the comment on `provenanceScope`.
|
|
178
|
-
const scope = provenanceScope(dir);
|
|
179
|
-
if (scope.covered) console.log("🔢 check-provenance — clean");
|
|
180
|
-
else
|
|
181
|
-
console.log(`⏭️ check-provenance SKIPPED for ${dir} — ${scope.reason}`);
|
|
182
|
-
}
|
|
183
|
-
process.exit(0);
|
|
184
|
-
}
|