paperlint 13.3.0 → 13.3.2
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 +33 -1
- package/CLAUDE.md +3 -3
- package/dist/domain/repro-layout.d.ts.map +1 -1
- package/dist/domain/repro-layout.js +9 -1
- package/dist/domain/repro-layout.js.map +1 -1
- package/docs/layout.md +2 -0
- package/docs/rules/paper/allowed-files-only.md +2 -0
- package/docs/rules/repro/allowed-files-only.md +2 -0
- package/docs/testing.md +5 -2
- package/docs/website.md +10 -0
- package/package.json +1 -1
- package/scripts/check.ts +4 -1
- package/skills/assess-novelty/scripts/check_novelty.py +47 -2
- package/skills/assess-novelty/scripts/check_novelty.test.ts +177 -43
- package/src/domain/repro-layout.test.ts +2 -0
- package/src/domain/repro-layout.ts +9 -1
- package/src/repro-layout-rule.test.ts +10 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -204,6 +204,28 @@ jobs:
|
|
|
204
204
|
run: npm run test:e2e:studies
|
|
205
205
|
timeout-minutes: 5
|
|
206
206
|
|
|
207
|
+
# ── DOES THIS PULL REQUEST CHANGE ANYTHING BUT DOCS? ────────────────────────────────────────────
|
|
208
|
+
# dorny/paths-filter lists a PR's files from the API (no checkout). With `every`, a file counts as
|
|
209
|
+
# code only if it matches all three patterns: anything, but not root or docs/ Markdown.
|
|
210
|
+
changes:
|
|
211
|
+
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false
|
|
212
|
+
runs-on: ubuntu-latest
|
|
213
|
+
timeout-minutes: 3
|
|
214
|
+
permissions:
|
|
215
|
+
pull-requests: read
|
|
216
|
+
outputs:
|
|
217
|
+
code: ${{ steps.filter.outputs.code }}
|
|
218
|
+
steps:
|
|
219
|
+
- uses: dorny/paths-filter@v3
|
|
220
|
+
id: filter
|
|
221
|
+
with:
|
|
222
|
+
predicate-quantifier: every
|
|
223
|
+
filters: |
|
|
224
|
+
code:
|
|
225
|
+
- "**"
|
|
226
|
+
- "!*.md"
|
|
227
|
+
- "!docs/**/*.md"
|
|
228
|
+
|
|
207
229
|
# ── BUILDING THE PAPER WITH REAL pdflatex, ON LINUX AND macOS ──────────────────────────────────
|
|
208
230
|
#
|
|
209
231
|
# 🔴 ONE JOB, TWO OPERATING SYSTEMS. TeX Live comes from `paperlint toolchain` — the same command a user
|
|
@@ -220,8 +242,18 @@ jobs:
|
|
|
220
242
|
# paper typeset in the wrong font. This repository is public, so the macOS minutes are free.
|
|
221
243
|
#
|
|
222
244
|
# `CI` is set: absence of TeX locally is a legitimate skip; here it is an environment failure.
|
|
245
|
+
#
|
|
246
|
+
# 🔴 EXCEPT A PULL REQUEST THAT CHANGES ONLY DOCS. A README or docs/*.md edit cannot change what
|
|
247
|
+
# this job builds, and it used to wait for TeX Live on two systems anyway (#343: one new
|
|
248
|
+
# docs/website.md). `changes` below decides; `gates` still runs on such a PR, because tests read
|
|
249
|
+
# the docs. On a push to main or a manual run `changes` does not run and this job always does.
|
|
223
250
|
build-e2e:
|
|
224
|
-
|
|
251
|
+
needs: changes
|
|
252
|
+
# `!cancelled()`: without it a skipped `changes` (every push to main) would skip this job too.
|
|
253
|
+
if: >-
|
|
254
|
+
!cancelled() && (github.event_name != 'pull_request' ||
|
|
255
|
+
(github.event.pull_request.draft == false && needs.changes.result == 'success' &&
|
|
256
|
+
needs.changes.outputs.code == 'true'))
|
|
225
257
|
strategy:
|
|
226
258
|
fail-fast: false
|
|
227
259
|
matrix:
|
package/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- vigiles:sha256:
|
|
1
|
+
<!-- vigiles:sha256:58c66e374c084cc5 compiled from CLAUDE.md.spec.ts -->
|
|
2
2
|
|
|
3
3
|
# CLAUDE.md
|
|
4
4
|
|
|
@@ -37,9 +37,9 @@ This file loads on every turn, so it holds only what applies to **any** change h
|
|
|
37
37
|
| changing CI, Dependabot or the release | [`.github/CLAUDE.md`](.github/CLAUDE.md) | cost, Dependabot cadence, release from the PR title |
|
|
38
38
|
| writing a test | [`docs/testing.md`](docs/testing.md), [`docs/e2e.md`](docs/e2e.md) | which kind of test, fakes, the e2e triggers |
|
|
39
39
|
|
|
40
|
-
## Before
|
|
40
|
+
## Before leaving draft and before merge
|
|
41
41
|
|
|
42
|
-
`npm run check`, exit code read without a pipe. Red first · assert the whole value · no test touches the real network · 100% coverage, no `c8 ignore`. In a container without TeX Live, only the TeX e2e gate fails, and it says so; `npx paperlint toolchain` installs it.
|
|
42
|
+
`npm run check`, exit code read without a pipe; a `wip:` push to a DRAFT pull request needs only the tests of what it touched (`npx vitest run <dir>`), and is never reported as "the gates", because CI does not run the gates on a draft. Red first · assert the whole value · no test touches the real network · 100% coverage, no `c8 ignore`. In a container without TeX Live, only the TeX e2e gate fails, and it says so; `npx paperlint toolchain` installs it.
|
|
43
43
|
|
|
44
44
|
## Commits
|
|
45
45
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repro-layout.d.ts","sourceRoot":"","sources":["../../src/domain/repro-layout.ts"],"names":[],"mappings":"AAmCA,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEnE,kEAAkE;AAClE,eAAO,MAAM,SAAS,UAAU,CAAC;AAEjC,iFAAiF;AACjF,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,+EAA+E;AAC/E,eAAO,MAAM,MAAM,cAAc,CAAC;AAElC,6DAA6D;AAC7D,eAAO,MAAM,gBAAgB,EAAE,SAAS;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,
|
|
1
|
+
{"version":3,"file":"repro-layout.d.ts","sourceRoot":"","sources":["../../src/domain/repro-layout.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* ├── tools/ scripts used across the paper (numbers, figures, anonymity check, harnesses)
|
|
14
14
|
* └── data/ shared data (tables, caches)
|
|
15
15
|
*
|
|
16
|
-
* plus `.gitignore`. A loose note belongs in its study's folder or in the paper's `research/`.
|
|
16
|
+
* plus `.gitignore`, and optionally the agent instruction files `CLAUDE.md` and `AGENTS.md`. A loose note belongs in its study's folder or in the paper's `research/`.
|
|
17
17
|
*
|
|
18
18
|
* ── A STUDY'S NAME ───────────────────────────────────────────────────────────────
|
|
19
19
|
* `<id>-<slug>` (`P1-S03-baseline`): the id of the study's row in `repro/studies.json` — the paper's
|
|
@@ -51,6 +51,14 @@ export const REPRO_FILE_NOTES = [
|
|
|
51
51
|
note: "the study registry: id, title, status and result of each study",
|
|
52
52
|
},
|
|
53
53
|
{ name: ".gitignore", note: "keeps the scripts' byproducts out of git" },
|
|
54
|
+
{
|
|
55
|
+
name: "CLAUDE.md",
|
|
56
|
+
note: "optional: instructions an agent loads when it works in repro/",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "AGENTS.md",
|
|
60
|
+
note: "optional: the same, for agents that read AGENTS.md",
|
|
61
|
+
},
|
|
54
62
|
];
|
|
55
63
|
export const REPRO_FILES = REPRO_FILE_NOTES.map((f) => f.name);
|
|
56
64
|
/** The shared folders `repro/` may hold besides its study folders. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repro-layout.js","sourceRoot":"","sources":["../../src/domain/repro-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAiB,MAAM,kBAAkB,CAAC;AAEnE,kEAAkE;AAClE,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC;AAEjC,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C,+EAA+E;AAC/E,MAAM,CAAC,MAAM,MAAM,GAAG,WAAW,CAAC;AAElC,6DAA6D;AAC7D,MAAM,CAAC,MAAM,gBAAgB,GAGvB;IACJ;QACE,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,4DAA4D;KACnE;IACD;QACE,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,gEAAgE;KACvE;IACD,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,0CAA0C,EAAE;
|
|
1
|
+
{"version":3,"file":"repro-layout.js","sourceRoot":"","sources":["../../src/domain/repro-layout.ts"],"names":[],"mappings":"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"}
|
package/docs/layout.md
CHANGED
|
@@ -72,6 +72,8 @@ creates one.
|
|
|
72
72
|
│ ├── README.md ← the index: one line per study, generated from studies.json
|
|
73
73
|
│ ├── studies.json ← the study registry: id, title, status and result of each study
|
|
74
74
|
│ ├── .gitignore ← keeps the scripts' byproducts out of git
|
|
75
|
+
│ ├── CLAUDE.md ← optional: instructions an agent loads when it works in repro/
|
|
76
|
+
│ ├── AGENTS.md ← optional: the same, for agents that read AGENTS.md
|
|
75
77
|
│ ├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)
|
|
76
78
|
│ ├── data/ ← shared data (tables, caches)
|
|
77
79
|
│ └── <id>-<slug>/ ← one folder per study or spike, named by its row in studies.json (P1-S03-baseline)
|
|
@@ -37,6 +37,8 @@ it: [the map](../../layout.md).
|
|
|
37
37
|
│ ├── README.md ← the index: one line per study, generated from studies.json
|
|
38
38
|
│ ├── studies.json ← the study registry: id, title, status and result of each study
|
|
39
39
|
│ ├── .gitignore ← keeps the scripts' byproducts out of git
|
|
40
|
+
│ ├── CLAUDE.md ← optional: instructions an agent loads when it works in repro/
|
|
41
|
+
│ ├── AGENTS.md ← optional: the same, for agents that read AGENTS.md
|
|
40
42
|
│ ├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)
|
|
41
43
|
│ ├── data/ ← shared data (tables, caches)
|
|
42
44
|
│ └── <id>-<slug>/ ← one folder per study or spike, named by its row in studies.json (P1-S03-baseline)
|
|
@@ -19,6 +19,8 @@ repro/
|
|
|
19
19
|
├── README.md ← the index: one line per study, generated from studies.json
|
|
20
20
|
├── studies.json ← the study registry: id, title, status and result of each study
|
|
21
21
|
├── .gitignore ← keeps the scripts' byproducts out of git
|
|
22
|
+
├── CLAUDE.md ← optional: instructions an agent loads when it works in repro/
|
|
23
|
+
├── AGENTS.md ← optional: the same, for agents that read AGENTS.md
|
|
22
24
|
├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)
|
|
23
25
|
├── data/ ← shared data (tables, caches)
|
|
24
26
|
└── <id>-<slug>/ ← one folder per study or spike, named by its row in studies.json (P1-S03-baseline)
|
package/docs/testing.md
CHANGED
|
@@ -105,8 +105,11 @@ npm run check # every gate CI runs, and the list of what it
|
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
While editing, run the tests you touched, `npx tsc --noEmit`, `npx eslint <files>` and
|
|
108
|
-
`npm run fmt:check`.
|
|
109
|
-
|
|
108
|
+
`npm run fmt:check`. A `wip:` push to a draft pull request needs only that: CI skips the gates on a
|
|
109
|
+
draft, and the push is not "the gates passed". **Before taking a pull request out of draft, and
|
|
110
|
+
before merging, run `npm run check` and read its exit code** — the coverage gate cannot be judged
|
|
111
|
+
from one file. Waiting ten minutes for the whole suite on every wip push bought nothing the
|
|
112
|
+
pre-ready run does not (2026-11-03).
|
|
110
113
|
|
|
111
114
|
⚠️ **Not `vigiles test .`**, and never drop `--min=1` from `npm test`. Measured 2026-09-11:
|
|
112
115
|
|
package/docs/website.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Website
|
|
2
|
+
|
|
3
|
+
The project's domain is **[paperlint.dev](https://paperlint.dev)**, registered in November 2026
|
|
4
|
+
through Vercel. No site is deployed on it yet: the first version is
|
|
5
|
+
[#342](https://github.com/zernie/paperlint/issues/342).
|
|
6
|
+
|
|
7
|
+
- `.dev` is on the HSTS preload list, so browsers only open it over HTTPS: the site needs a
|
|
8
|
+
certificate from its first deploy.
|
|
9
|
+
- Until the site exists, the npm page links to the GitHub README: `package.json` sets no
|
|
10
|
+
`homepage`, only `repository`.
|
package/package.json
CHANGED
package/scripts/check.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* `npm run check` — ONE command you run on yourself before
|
|
3
|
+
* `npm run check` — ONE command you run on yourself before a pull request leaves draft, and before merge.
|
|
4
4
|
*
|
|
5
5
|
* ── WHY IT EXISTS, AND IT IS A MEASURED FAILURE, NOT A TIDINESS IDEA ───────────────────────
|
|
6
6
|
* On 2026-09-19 a change to a lint rule was pushed that broke the test suite.
|
|
@@ -201,6 +201,9 @@ export function commandOf(gate: Gate): readonly [string, ...string[]] {
|
|
|
201
201
|
export const NOT_COVERED: Readonly<Record<string, string>> = {
|
|
202
202
|
// build-e2e's macOS cell is the one part of a covered job no local command can stand in for:
|
|
203
203
|
// it exists because issue #9 appeared on macOS alone (/var is a symlink to /private/var).
|
|
204
|
+
changes:
|
|
205
|
+
"ci.yml asks dorny/paths-filter whether a pull request changes anything but docs, so " +
|
|
206
|
+
"build-e2e can skip; it checks nothing itself, and a local run always builds.",
|
|
204
207
|
merge:
|
|
205
208
|
"dependabot-automerge.yml merges the bot's own pull requests once CI is green. It checks " +
|
|
206
209
|
"nothing itself; there is no local equivalent because it acts on GitHub, not on the tree.",
|
|
@@ -12,7 +12,11 @@ The verdict is computed from the cells, never stored, over neighbours read in fu
|
|
|
12
12
|
Errors: a 'yes' cell without a quote or with a quote not in the text · a 'no' cell on an abstract-only
|
|
13
13
|
neighbour · a missing search source · a priority sentence in paper.tex (first / none of the studies /
|
|
14
14
|
to our knowledge / ...) not mapped to a 🟢 claim · CLAIMS.md disagreeing with the computed verdict ·
|
|
15
|
-
a 🟢 claim without a refutation file · a claim without a known type or an objection_answer
|
|
15
|
+
a 🟢 claim without a refutation file · a claim without a known type or an objection_answer · a saved text
|
|
16
|
+
with more than 30 % glued lines (two page columns merged by `pdftotext -layout`).
|
|
17
|
+
The ledger may be MORE cautious than the computed verdict (🟢 > 🟡 > 🔴), never bolder: a claim then carries
|
|
18
|
+
"ledger_narrower_because", a reason naming an existing file in the paper folder. A bolder ledger, a ⚪ on either
|
|
19
|
+
side, or the field beside equal statuses (a stale reason) is an error.
|
|
16
20
|
A quote is a presence check (quote_match.py's matcher), not a proof that it supports the cell.
|
|
17
21
|
A neighbour read 'full' proves it with `coverage`, one quote per chunk of its text (novelty_coverage.py).
|
|
18
22
|
A work a refutation or a search surfaced is a candidate until the author decides on it (novelty_candidates.py):
|
|
@@ -29,6 +33,9 @@ from novelty_coverage import coverage_errors # noqa: E402
|
|
|
29
33
|
import novelty_candidates # noqa: E402
|
|
30
34
|
import novelty_cells # noqa: E402
|
|
31
35
|
|
|
36
|
+
# Boldness of a ledger status; ⚪ is not on the scale (unchecked is neither more nor less cautious).
|
|
37
|
+
BOLDNESS = {"🟢": 2, "🟡": 1, "🔴": 0}
|
|
38
|
+
FILE_NAME = re.compile(r"[\w./-]+\.(?:md|txt|json|tex|tsv|csv|py|ts|mjs)\b")
|
|
32
39
|
SOURCES = ("venue-pc", "prev-edition", "arxiv-recent")
|
|
33
40
|
TYPES = {"new-object", "transfer", "installed-base", "instrument", "refutation", "replication"}
|
|
34
41
|
# The result itself: what is examined and what number comes out. A neighbour with both already reported the
|
|
@@ -53,6 +60,17 @@ PRIORITY = re.compile(
|
|
|
53
60
|
)
|
|
54
61
|
|
|
55
62
|
|
|
63
|
+
# `pdftotext -layout` prints a two-column page as one line per row, the columns joined by a run of spaces, so a
|
|
64
|
+
# sentence is split across two lines and a quote is not contiguous.
|
|
65
|
+
GLUED = re.compile(r"\S {6,}\S")
|
|
66
|
+
GLUED_LIMIT = 0.30
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def glued_share(text):
|
|
70
|
+
lines = [l.strip() for l in text.splitlines() if l.strip()]
|
|
71
|
+
return sum(1 for l in lines if GLUED.search(l)) / len(lines) if lines else 0.0
|
|
72
|
+
|
|
73
|
+
|
|
56
74
|
def tex_sentences(tex):
|
|
57
75
|
tex = re.sub(r"(?<!\\)%.*", "", tex)
|
|
58
76
|
tex = re.sub(r"\\(?:cite|ref|label|eqref|autoref|cref|documentclass|usepackage|begin|end|input|include)\*?"
|
|
@@ -74,6 +92,17 @@ def ledger_status(claims_md, row):
|
|
|
74
92
|
return None
|
|
75
93
|
|
|
76
94
|
|
|
95
|
+
def narrower_reason_errors(cid, reason, paper):
|
|
96
|
+
"""Problems with a claim's ledger_narrower_because: empty, naming no file, or naming a missing one."""
|
|
97
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
98
|
+
return [f"{cid}: ledger_narrower_because is empty (a reason naming a file in the paper folder)"]
|
|
99
|
+
names = FILE_NAME.findall(reason)
|
|
100
|
+
if not names:
|
|
101
|
+
return [f"{cid}: ledger_narrower_because names no file (give the path of the file holding the reason)"]
|
|
102
|
+
return [f"{cid}: ledger_narrower_because names {n}, which does not exist"
|
|
103
|
+
for n in names if not os.path.isfile(os.path.join(paper, n))]
|
|
104
|
+
|
|
105
|
+
|
|
77
106
|
def verdict(claim, neighbours):
|
|
78
107
|
pieces = list(claim.get("pieces", {}))
|
|
79
108
|
if not pieces or not neighbours:
|
|
@@ -138,6 +167,10 @@ def main():
|
|
|
138
167
|
errors.append(f"neighbour {n['id']}: read 'full' but no text at {n.get('text')}")
|
|
139
168
|
if os.path.isfile(path):
|
|
140
169
|
t = open(path, encoding="utf-8", errors="replace").read()
|
|
170
|
+
share = glued_share(t)
|
|
171
|
+
if share > GLUED_LIMIT:
|
|
172
|
+
errors.append(f"neighbour {n['id']}: {n.get('text')} has {share:.0%} glued lines (two page columns"
|
|
173
|
+
" merged into one line); re-extract with plain pdftotext (no -layout)")
|
|
141
174
|
texts[n["id"]] = (norm(t), letters(t))
|
|
142
175
|
if n.get("read") == "full":
|
|
143
176
|
errors += coverage_errors(n["id"], t, n.get("coverage"))
|
|
@@ -176,7 +209,19 @@ def main():
|
|
|
176
209
|
elif status is None:
|
|
177
210
|
errors.append(f"{cid}: ledger row {row!r} not found in CLAIMS.md")
|
|
178
211
|
elif status != v:
|
|
179
|
-
|
|
212
|
+
narrower = status in BOLDNESS and v in BOLDNESS and BOLDNESS[status] < BOLDNESS[v]
|
|
213
|
+
reason = c.get("ledger_narrower_because")
|
|
214
|
+
if narrower and "ledger_narrower_because" in c:
|
|
215
|
+
errors += narrower_reason_errors(cid, reason, paper)
|
|
216
|
+
else:
|
|
217
|
+
hint = ("; a more cautious ledger needs ledger_narrower_because naming the file with the reason"
|
|
218
|
+
if narrower else
|
|
219
|
+
"; ledger_narrower_because excuses only a MORE cautious ledger" if "ledger_narrower_because" in c
|
|
220
|
+
else "")
|
|
221
|
+
errors.append(f"{cid}: CLAIMS.md row {row} says {status}, the cells give {v} ({why}){hint}")
|
|
222
|
+
elif "ledger_narrower_because" in c:
|
|
223
|
+
errors.append(f"{cid}: ledger_narrower_because is set but the ledger equals the computed verdict {v}"
|
|
224
|
+
" (stale reason; remove the field)")
|
|
180
225
|
if v == "🟢":
|
|
181
226
|
ref = c.get("refutation") or {}
|
|
182
227
|
if not ref.get("file"):
|
|
@@ -21,50 +21,51 @@ import { fileURLToPath } from "node:url";
|
|
|
21
21
|
import assert from "node:assert/strict";
|
|
22
22
|
import { test } from "vitest";
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const CHECKER = join(here, "check_novelty.py");
|
|
24
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const FIXTURE = join(here, "fixtures", "novelty");
|
|
26
|
+
const CHECKER = join(here, "check_novelty.py");
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
28
|
+
// Copy the fixture, apply one change, run the checker; returns {code, out}.
|
|
29
|
+
type Files = { json: any; tex: string; claims: string; dir: string };
|
|
30
|
+
type Change = (f: Files) => void;
|
|
31
|
+
const run = (change: Change = () => undefined) => {
|
|
32
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "novelty-")));
|
|
33
|
+
try {
|
|
34
|
+
cpSync(FIXTURE, dir, { recursive: true });
|
|
35
|
+
const jsonPath = join(dir, "siblings", "novelty.json");
|
|
36
|
+
const files = {
|
|
37
|
+
json: JSON.parse(readFileSync(jsonPath, "utf8")),
|
|
38
|
+
tex: readFileSync(join(dir, "paper.tex"), "utf8"),
|
|
39
|
+
claims: readFileSync(join(dir, "CLAIMS.md"), "utf8"),
|
|
40
|
+
dir,
|
|
41
|
+
};
|
|
42
|
+
change(files);
|
|
43
|
+
writeFileSync(jsonPath, JSON.stringify(files.json, null, 2));
|
|
44
|
+
writeFileSync(join(dir, "paper.tex"), files.tex);
|
|
45
|
+
writeFileSync(join(dir, "CLAIMS.md"), files.claims);
|
|
46
|
+
const r = spawnSync("python3", ["-I", CHECKER, dir], {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
});
|
|
49
|
+
return { code: r.status, out: `${r.stdout}\n${r.stderr}` };
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const passes = (change: Change, what: string) => {
|
|
55
|
+
const r = run(change);
|
|
56
|
+
assert.equal(r.code, 0, `must pass: ${what}\n${r.out}`);
|
|
57
|
+
return r.out;
|
|
58
|
+
};
|
|
59
|
+
const fails = (change: Change, re: RegExp, what: string) => {
|
|
60
|
+
const r = run(change);
|
|
61
|
+
assert.equal(r.code, 1, `must fail: ${what}\n${r.out}`);
|
|
62
|
+
assert.match(r.out, re, `wrong reason for: ${what}\n${r.out}`);
|
|
63
|
+
};
|
|
64
|
+
const claim = (j: any, id: string) => j.claims.find((c: any) => c.id === id);
|
|
65
|
+
const FIRST =
|
|
66
|
+
"This is the first rule finder recall reported in this literature.";
|
|
67
67
|
|
|
68
|
+
test("check_novelty", () => {
|
|
68
69
|
// ── clean fixture: passes, verdicts computed from the cells ────────────────────
|
|
69
70
|
{
|
|
70
71
|
const out = passes(() => undefined, "the fixture as committed");
|
|
@@ -485,6 +486,139 @@ test("check_novelty", () => {
|
|
|
485
486
|
"abstract-only neighbour whose text is saved",
|
|
486
487
|
);
|
|
487
488
|
}
|
|
489
|
+
}, 300_000);
|
|
488
490
|
|
|
489
|
-
|
|
491
|
+
test("check_novelty: a ledger more cautious than the cells", () => {
|
|
492
|
+
// ── 4b. a ledger MORE cautious than the cells, with a written reason ──────────
|
|
493
|
+
// A claim may carry "ledger_narrower_because": a reason naming an existing file. With it a ledger status
|
|
494
|
+
// strictly more cautious than the computed one (🟢 > 🟡 > 🔴) is accepted; a bolder one never is, and the
|
|
495
|
+
// field beside equal statuses is a stale reason. P5-1 computes 🟢 in the fixture (refutation file exists).
|
|
496
|
+
{
|
|
497
|
+
const REASON =
|
|
498
|
+
"siblings/2026-10-27-novelty-refutation.md: the refutation pass kept the claim narrower than the cells";
|
|
499
|
+
const P51_GREEN =
|
|
500
|
+
"| P5-1 | The repository's linter does not make 58.3 % of lint-checkable requirements an error | 🟢 ours |";
|
|
501
|
+
const P51_AMBER =
|
|
502
|
+
"| P5-1 | The repository's linter does not make 58.3 % of lint-checkable requirements an error | 🟡 narrowed |";
|
|
503
|
+
const amber = (f: Files, reason?: string) => {
|
|
504
|
+
assert.ok(f.claims.includes(P51_GREEN), "fixture row P5-1 changed");
|
|
505
|
+
f.claims = f.claims.replace(P51_GREEN, P51_AMBER);
|
|
506
|
+
if (reason !== undefined)
|
|
507
|
+
claim(f.json, "P5-1").ledger_narrower_because = reason;
|
|
508
|
+
};
|
|
509
|
+
// (a) ledger 🟡, cells 🟢, field naming an existing file: accepted, the printed verdict stays computed
|
|
510
|
+
{
|
|
511
|
+
const out = passes(
|
|
512
|
+
(f) => amber(f, REASON),
|
|
513
|
+
"(a) ledger 🟡, cells 🟢, reason names an existing file",
|
|
514
|
+
);
|
|
515
|
+
assert.match(
|
|
516
|
+
out,
|
|
517
|
+
/P5-1\s+🟢/,
|
|
518
|
+
"the printed verdict stays the computed one",
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
// (b) the same without the field: the error ends with the hint
|
|
522
|
+
fails(
|
|
523
|
+
(f) => amber(f),
|
|
524
|
+
/P5-1: CLAIMS\.md row P5-1 says 🟡, the cells give 🟢.*; a more cautious ledger needs ledger_narrower_because naming the file with the reason/s,
|
|
525
|
+
"(b) ledger 🟡, cells 🟢, no field",
|
|
526
|
+
);
|
|
527
|
+
// (c) ledger bolder than the cells: an error with or without the field
|
|
528
|
+
const oneNo = (f: Files) => {
|
|
529
|
+
claim(f.json, "R1").cells.doe26.measure = { has: "no", quote: null };
|
|
530
|
+
};
|
|
531
|
+
const bolder = (f: Files, reason?: string) => {
|
|
532
|
+
oneNo(f); // R1 computes 🟡
|
|
533
|
+
assert.ok(
|
|
534
|
+
f.claims.includes("| 🔴 taken (Doe 2026, recall 66.3 %) |"),
|
|
535
|
+
"fixture row R1 changed",
|
|
536
|
+
);
|
|
537
|
+
f.claims = f.claims.replace(
|
|
538
|
+
"| 🔴 taken (Doe 2026, recall 66.3 %) |",
|
|
539
|
+
"| 🟢 ours |",
|
|
540
|
+
);
|
|
541
|
+
if (reason !== undefined)
|
|
542
|
+
claim(f.json, "R1").ledger_narrower_because = reason;
|
|
543
|
+
};
|
|
544
|
+
fails(
|
|
545
|
+
(f) => bolder(f, REASON),
|
|
546
|
+
/R1: CLAIMS\.md row R1 says 🟢, the cells give 🟡/,
|
|
547
|
+
"(c) ledger 🟢, cells 🟡, with the field",
|
|
548
|
+
);
|
|
549
|
+
fails(
|
|
550
|
+
(f) => bolder(f),
|
|
551
|
+
/R1: CLAIMS\.md row R1 says 🟢, the cells give 🟡/,
|
|
552
|
+
"(c) ledger 🟢, cells 🟡, no field",
|
|
553
|
+
);
|
|
554
|
+
// (d) the named file is missing, no file is named, or the field is empty
|
|
555
|
+
fails(
|
|
556
|
+
(f) => amber(f, "siblings/no-such-refutation.md: kept narrower"),
|
|
557
|
+
/P5-1: ledger_narrower_because names siblings\/no-such-refutation\.md.*does not exist/,
|
|
558
|
+
"(d) the named file is missing",
|
|
559
|
+
);
|
|
560
|
+
fails(
|
|
561
|
+
(f) => amber(f, "kept narrower by hand"),
|
|
562
|
+
/P5-1: ledger_narrower_because names no file/,
|
|
563
|
+
"(d) a reason naming no file",
|
|
564
|
+
);
|
|
565
|
+
fails(
|
|
566
|
+
(f) => amber(f, " "),
|
|
567
|
+
/P5-1: ledger_narrower_because is empty/,
|
|
568
|
+
"(d) an empty reason",
|
|
569
|
+
);
|
|
570
|
+
// (e) equal statuses with the field: a stale reason
|
|
571
|
+
fails(
|
|
572
|
+
(f) => {
|
|
573
|
+
claim(f.json, "P5-1").ledger_narrower_because = REASON;
|
|
574
|
+
},
|
|
575
|
+
/P5-1: ledger_narrower_because is set but the ledger equals the computed verdict 🟢/,
|
|
576
|
+
"(e) equal statuses with the field",
|
|
577
|
+
);
|
|
578
|
+
// ⚪ is not on the scale: a ledger ⚪ over a computed 🟢 is unchecked, not more cautious
|
|
579
|
+
fails(
|
|
580
|
+
(f) => {
|
|
581
|
+
f.claims = f.claims.replace(
|
|
582
|
+
P51_GREEN,
|
|
583
|
+
P51_GREEN.replace("🟢 ours", "⚪ unchecked"),
|
|
584
|
+
);
|
|
585
|
+
claim(f.json, "P5-1").ledger_narrower_because = REASON;
|
|
586
|
+
},
|
|
587
|
+
/P5-1: CLAIMS\.md row P5-1 says ⚪, the cells give 🟢/,
|
|
588
|
+
"ledger ⚪ over computed 🟢, with the field",
|
|
589
|
+
);
|
|
590
|
+
// the more cautious ledger on a 🔴: ledger 🔴 (as committed), cells 🟡, accepted with the field
|
|
591
|
+
passes((f) => {
|
|
592
|
+
oneNo(f);
|
|
593
|
+
claim(f.json, "R1").ledger_narrower_because = REASON;
|
|
594
|
+
}, "ledger 🔴, cells 🟡, reason names an existing file");
|
|
595
|
+
}
|
|
596
|
+
}, 300_000);
|
|
597
|
+
|
|
598
|
+
test("check_novelty: a text with its columns glued", () => {
|
|
599
|
+
// ── 10. a text extracted with `pdftotext -layout` has its two columns glued into one line ──
|
|
600
|
+
// A sentence is then split across two lines and a verbatim quote is not contiguous. The glued copy pairs
|
|
601
|
+
// the fixture's own lines with an 8-space gap; the clean twin is the fixture as committed.
|
|
602
|
+
{
|
|
603
|
+
const DOE = "doe-preprint.txt";
|
|
604
|
+
const glue = (f: Files) => {
|
|
605
|
+
const p = join(f.dir, "siblings", "data", DOE);
|
|
606
|
+
const lines = readFileSync(p, "utf8")
|
|
607
|
+
.split("\n")
|
|
608
|
+
.filter((l) => l.trim());
|
|
609
|
+
const pairs: string[] = [];
|
|
610
|
+
for (let i = 0; i < lines.length; i += 2)
|
|
611
|
+
pairs.push(`${lines[i]} ${lines[i + 1] ?? ""}`);
|
|
612
|
+
writeFileSync(p, pairs.join("\n") + "\n");
|
|
613
|
+
};
|
|
614
|
+
fails(
|
|
615
|
+
glue,
|
|
616
|
+
/neighbour doe26: siblings\/data\/doe-preprint\.txt has \d+% glued lines.*re-extract with plain pdftotext \(no -layout\)/,
|
|
617
|
+
"a neighbour text with its columns glued",
|
|
618
|
+
);
|
|
619
|
+
passes(
|
|
620
|
+
() => undefined,
|
|
621
|
+
"the same text extracted without -layout (the fixture as committed)",
|
|
622
|
+
);
|
|
623
|
+
}
|
|
490
624
|
}, 300_000);
|
|
@@ -208,6 +208,8 @@ describe("reproMessage — stands alone", () => {
|
|
|
208
208
|
"├── README.md ← the index: one line per study, generated from studies.json",
|
|
209
209
|
"├── studies.json ← the study registry: id, title, status and result of each study",
|
|
210
210
|
"├── .gitignore ← keeps the scripts' byproducts out of git",
|
|
211
|
+
"├── CLAUDE.md ← optional: instructions an agent loads when it works in repro/",
|
|
212
|
+
"├── AGENTS.md ← optional: the same, for agents that read AGENTS.md",
|
|
211
213
|
"├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)",
|
|
212
214
|
"├── data/ ← shared data (tables, caches)",
|
|
213
215
|
"└── <id>-<slug>/ ← one folder per study or spike, named by its row in studies.json (P1-S03-baseline)",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* ├── tools/ scripts used across the paper (numbers, figures, anonymity check, harnesses)
|
|
14
14
|
* └── data/ shared data (tables, caches)
|
|
15
15
|
*
|
|
16
|
-
* plus `.gitignore`. A loose note belongs in its study's folder or in the paper's `research/`.
|
|
16
|
+
* plus `.gitignore`, and optionally the agent instruction files `CLAUDE.md` and `AGENTS.md`. A loose note belongs in its study's folder or in the paper's `research/`.
|
|
17
17
|
*
|
|
18
18
|
* ── A STUDY'S NAME ───────────────────────────────────────────────────────────────
|
|
19
19
|
* `<id>-<slug>` (`P1-S03-baseline`): the id of the study's row in `repro/studies.json` — the paper's
|
|
@@ -58,6 +58,14 @@ export const REPRO_FILE_NOTES: readonly {
|
|
|
58
58
|
note: "the study registry: id, title, status and result of each study",
|
|
59
59
|
},
|
|
60
60
|
{ name: ".gitignore", note: "keeps the scripts' byproducts out of git" },
|
|
61
|
+
{
|
|
62
|
+
name: "CLAUDE.md",
|
|
63
|
+
note: "optional: instructions an agent loads when it works in repro/",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "AGENTS.md",
|
|
67
|
+
note: "optional: the same, for agents that read AGENTS.md",
|
|
68
|
+
},
|
|
61
69
|
];
|
|
62
70
|
export const REPRO_FILES: readonly string[] = REPRO_FILE_NOTES.map(
|
|
63
71
|
(f) => f.name,
|
|
@@ -94,6 +94,16 @@ describe("repro/allowed-files-only — silent", () => {
|
|
|
94
94
|
).toEqual([]);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
it("on agent instruction files at the top (CLAUDE.md, AGENTS.md)", () => {
|
|
98
|
+
expect(
|
|
99
|
+
lint({
|
|
100
|
+
...SCAFFOLD,
|
|
101
|
+
[`${DIR}/repro/CLAUDE.md`]: "x",
|
|
102
|
+
[`${DIR}/repro/AGENTS.md`]: "x",
|
|
103
|
+
}),
|
|
104
|
+
).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
|
|
97
107
|
it("on a paper with no repro/", () => {
|
|
98
108
|
expect(lint(SCAFFOLD)).toEqual([]);
|
|
99
109
|
});
|