paperlint 12.1.0 → 13.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/memory/index.d.ts +5 -0
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +16 -0
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/node/index.d.ts +1 -0
- package/dist/adapters/node/index.d.ts.map +1 -1
- package/dist/adapters/node/index.js +1 -0
- package/dist/adapters/node/index.js.map +1 -1
- package/dist/adapters/node/move-file.io.d.ts +4 -0
- package/dist/adapters/node/move-file.io.d.ts.map +1 -0
- package/dist/adapters/node/move-file.io.js +12 -0
- package/dist/adapters/node/move-file.io.js.map +1 -0
- package/dist/adapters/yaml/index.d.ts +3 -0
- package/dist/adapters/yaml/index.d.ts.map +1 -0
- package/dist/adapters/yaml/index.js +17 -0
- package/dist/adapters/yaml/index.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +48 -7
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts +3 -3
- package/dist/cycle-rules.d.ts.map +1 -1
- package/dist/cycle-rules.js +1 -2
- package/dist/cycle-rules.js.map +1 -1
- package/dist/domain/cycle.d.ts +16 -2
- package/dist/domain/cycle.d.ts.map +1 -1
- package/dist/domain/cycle.js +21 -42
- package/dist/domain/cycle.js.map +1 -1
- package/dist/domain/paper-layout.d.ts.map +1 -1
- package/dist/domain/paper-layout.js +5 -2
- package/dist/domain/paper-layout.js.map +1 -1
- package/dist/domain/paths.d.ts +12 -0
- package/dist/domain/paths.d.ts.map +1 -1
- package/dist/domain/paths.js +22 -1
- package/dist/domain/paths.js.map +1 -1
- package/dist/domain/scaffold-layout.js +1 -1
- package/dist/domain/versions-migration.d.ts +51 -0
- package/dist/domain/versions-migration.d.ts.map +1 -0
- package/dist/domain/versions-migration.js +252 -0
- package/dist/domain/versions-migration.js.map +1 -0
- package/dist/domain/versions.d.ts +167 -0
- package/dist/domain/versions.d.ts.map +1 -0
- package/dist/domain/versions.js +457 -0
- package/dist/domain/versions.js.map +1 -0
- package/dist/eslint-rules/paper-stages.d.ts +7 -0
- package/dist/eslint-rules/paper-stages.d.ts.map +1 -1
- package/dist/eslint-rules/paper-stages.js +62 -7
- package/dist/eslint-rules/paper-stages.js.map +1 -1
- package/dist/eslint-rules/rule-context.d.ts +2 -0
- package/dist/eslint-rules/rule-context.d.ts.map +1 -1
- package/dist/ports/move-file.d.ts +7 -0
- package/dist/ports/move-file.d.ts.map +1 -0
- package/dist/ports/move-file.js +2 -0
- package/dist/ports/move-file.js.map +1 -0
- package/dist/ports/yaml.d.ts +5 -0
- package/dist/ports/yaml.d.ts.map +1 -0
- package/dist/ports/yaml.js +2 -0
- package/dist/ports/yaml.js.map +1 -0
- package/dist/submission.d.ts +2 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +49 -9
- package/dist/submission.js.map +1 -1
- package/dist/version-rules.d.ts +16 -0
- package/dist/version-rules.d.ts.map +1 -0
- package/dist/version-rules.js +201 -0
- package/dist/version-rules.js.map +1 -0
- package/dist/versions-command.d.ts +37 -0
- package/dist/versions-command.d.ts.map +1 -0
- package/dist/versions-command.js +209 -0
- package/dist/versions-command.js.map +1 -0
- package/dist/versions.d.ts +76 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +164 -0
- package/dist/versions.js.map +1 -0
- package/docs/configuration.md +76 -2
- package/docs/layout.md +5 -5
- package/docs/rules/cycle/camera-ready-source.md +62 -0
- package/docs/rules/cycle/draft-drift.md +47 -0
- package/docs/rules/cycle/port-done.md +2 -2
- package/docs/rules/cycle/record.md +2 -1
- package/docs/rules/cycle/submitted-frozen.md +65 -0
- package/docs/rules/paper/allowed-files-only.md +1 -1
- package/docs/rules/versions/layout.md +88 -0
- package/docs/rules.md +25 -11
- package/docs/submission.md +21 -3
- package/eslint-rules/paper-stages.test.mjs +62 -0
- package/eslint-rules/paper-stages.ts +67 -8
- package/eslint-rules/prereg-coverage.test.ts +1 -0
- package/eslint-rules/rule-context.ts +2 -0
- package/package.json +2 -1
- package/skills/paper-pipeline/references/pipeline-status-template.md +4 -1
- package/skills/submit-paper/SKILL.md +18 -10
- package/skills/submit-paper/SKILL.md.spec.ts +17 -9
- package/skills/submit-paper/references/publishers/acm.md +5 -3
- package/src/adapters/memory/index.test.ts +22 -0
- package/src/adapters/memory/index.ts +21 -0
- package/src/adapters/node/index.ts +1 -0
- package/src/adapters/node/move-file.io.ts +21 -0
- package/src/adapters/node/move-file.test.ts +51 -0
- package/src/adapters/yaml/index.test.ts +16 -0
- package/src/adapters/yaml/index.ts +17 -0
- package/src/cli.test.ts +43 -0
- package/src/cli.ts +58 -6
- package/src/cycle-rules.ts +4 -5
- package/src/domain/cycle.test.ts +27 -0
- package/src/domain/cycle.ts +38 -6
- package/src/domain/paper-layout.ts +5 -2
- package/src/domain/paths.test.ts +28 -1
- package/src/domain/paths.ts +34 -1
- package/src/domain/scaffold-layout.ts +1 -1
- package/src/domain/versions-migration.test.ts +410 -0
- package/src/domain/versions-migration.ts +429 -0
- package/src/domain/versions.test.ts +564 -0
- package/src/domain/versions.ts +706 -0
- package/src/ports/move-file.ts +8 -0
- package/src/ports/yaml.ts +5 -0
- package/src/rule-cases.test.ts +157 -8
- package/src/submission-freeze.test.ts +320 -0
- package/src/submission-new.test.ts +11 -1
- package/src/submission.ts +82 -11
- package/src/version-rules.test.ts +222 -0
- package/src/version-rules.ts +282 -0
- package/src/versions-command.test.ts +594 -0
- package/src/versions-command.ts +360 -0
- package/src/versions.ts +286 -0
- package/dist/eslint-rules/cycle-stage.d.ts +0 -39
- package/dist/eslint-rules/cycle-stage.d.ts.map +0 -1
- package/dist/eslint-rules/cycle-stage.js +0 -119
- package/dist/eslint-rules/cycle-stage.js.map +0 -1
- package/docs/rules/cycle/stage.md +0 -85
- package/eslint-rules/cycle-stage.test.mjs +0 -131
- package/eslint-rules/cycle-stage.ts +0 -141
package/docs/rules.md
CHANGED
|
@@ -5,13 +5,16 @@ reads and when it fails. Errors fail `paperlint lint`; warnings print and do not
|
|
|
5
5
|
|
|
6
6
|
| Rule | Level | Reads | Fails when |
|
|
7
7
|
| ------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
|
-
| `paper/stages` | error | `PIPELINE-STATUS.md` | a declared stage's PDF is missing, or the `bytes:` it names does not match the file's real size, or a frozen PDF exists that no stage declares
|
|
9
|
-
| `paper/source` | error | `PIPELINE-STATUS.md` | a declared stage has no frozen `.tex` beside its PDF (a commit hash does not count — squash and gc destroy it)
|
|
8
|
+
| `paper/stages` | error | `PIPELINE-STATUS.md` | a declared stage's PDF is missing, or the `bytes:` it names does not match the file's real size, or a frozen PDF exists that no stage declares. In a paper that keeps `cycles`, the `stages:` list is retired: one finding names `paperlint versions migrate`, and [`versions/layout`](rules/versions/layout.md) judges `versions/` |
|
|
9
|
+
| `paper/source` | error | `PIPELINE-STATUS.md` | a declared stage has no frozen `.tex` beside its PDF (a commit hash does not count — squash and gc destroy it). Silent in a paper that keeps `cycles`: there [`versions/layout`](rules/versions/layout.md) holds each PDF to its source |
|
|
10
10
|
| [`study/declare-sample`](rules/study/declare-sample.md) | experimental; off unless a paper turns it on (`"error"`) | each study's `repro/<study>/prereg.json` | a study's pre-registration does not declare its `population` (what the claim is about, source, size), the `strata` and what each of its `screens` removed per stratum value, its `coverage` (share 0–1 or null with a reason, and the strata it cannot reach), or the `author_check` of the drawn units (a doubtful verdict needs a recorded `decision`); reported on `PIPELINE-STATUS.md`, once per problem — [page](rules/study/declare-sample.md) |
|
|
11
11
|
| [`cycle/record`](rules/cycle/record.md) | error | `paperlint.json` (`cycles`) | the paper's attempts at venues (`cycles`, one entry per attempt) hold one source's deadlines out of order (registration ≤ submission ≤ resubmission < notification < camera-ready), or copy a portal reading the venue's preset carries; reported on `PIPELINE-STATUS.md`. A record that does not parse — a bare date, two open cycles, an unknown key — is refused by `paperlint lint` itself before any rule runs, exit 2 — [page](rules/cycle/record.md) |
|
|
12
12
|
| [`cycle/evidence`](rules/cycle/evidence.md) | error | `paperlint.json` (`cycles`) → the paper folder | a closed cycle (`accepted`, `rejected`, `withdrawn`) or a deadline's `override` names an `evidence` file — the decision mail, the reviews, the withdrawal note, the chairs' mail — that is not in the paper folder; reported on `PIPELINE-STATUS.md` — [page](rules/cycle/evidence.md) |
|
|
13
|
-
| [`cycle/stage`](rules/cycle/stage.md) | error | `PIPELINE-STATUS.md` → `paperlint.json` (`cycles`) | with `cycles` declared, a stage names no `cycle: <id>`, names one no cycle has, or a `submitted`/`camera-ready` stage belongs to a cycle whose `phase` is still `porting` — [page](rules/cycle/stage.md) |
|
|
14
13
|
| [`cycle/port-done`](rules/cycle/port-done.md) | error | `paper.tex` → `paperlint.json` (`cycles`) → the preset's `template` | the current cycle still declares `"phase": "porting"` (which silences the venue's format rules) while the `\documentclass` is already the venue's template: the port is done, drop the phase — [page](rules/cycle/port-done.md) |
|
|
14
|
+
| [`cycle/submitted-frozen`](rules/cycle/submitted-frozen.md) | error | `paperlint.json` (`cycles`) → `versions/` | a cycle has `"submission": { "id" }` — a PDF went to the portal — and no `versions/<cycle>/submitted.pdf`; `submission new | update --save`freeze what they send, so this fires for an upload made another way; reported on`PIPELINE-STATUS.md` — [page](rules/cycle/submitted-frozen.md) |
|
|
15
|
+
| [`cycle/draft-drift`](rules/cycle/draft-drift.md) | warn | `paper.tex` → `paperlint.json` (`cycles`) → `versions/<cycle>/submitted.tex` | the last cycle is open and `paper.tex` (as the last build assembled it) differs from the source frozen when its PDF was sent; says how many lines were added and removed — [page](rules/cycle/draft-drift.md) |
|
|
16
|
+
| [`cycle/camera-ready-source`](rules/cycle/camera-ready-source.md) | error | `paper.tex` → `paperlint.json` (`cycles`) → `versions/<cycle>/` | the last cycle is accepted, its camera-ready is frozen, and the frozen `camera-ready.tex` keeps less than half of the lines of the frozen `submitted.tex`, so it was not built from what was accepted; silent until the camera-ready is frozen — [page](rules/cycle/camera-ready-source.md) |
|
|
17
|
+
| [`versions/layout`](rules/versions/layout.md) | error | `versions/`, `versions/lock.json` → `paperlint.json` (`cycles`) | with `cycles` declared: an entry of `versions/` that is not `lock.json` or a folder named by a cycle id, a file in a cycle's folder that is not `<stage>.pdf`/`.tex` (`submitted`, `camera-ready`, `arxiv`), a PDF without its source (unless the lock records it lost), a PDF sent from a cycle still `porting`, or a frozen file that does not match `versions/lock.json`; reported on `PIPELINE-STATUS.md` — [page](rules/versions/layout.md) |
|
|
15
18
|
| [`paper/folder-venue-leftover`](rules/paper/folder-venue-leftover.md) | warn | the folder name → `paperlint.json` | the paper's folder is named after another shipped venue than the one it extends (`papers/aisec-2026/` for a paper now set for AIDC); reported on `PIPELINE-STATUS.md` — [page](rules/paper/folder-venue-leftover.md) |
|
|
16
19
|
| [`paper/allowed-files-only`](rules/paper/allowed-files-only.md) | error | the paper folder's root listing → `paperlint.json` | a file or folder at the paper's root is not part of the layout (`PLAN.md`, `archive/`, a stray PDF); reported on `PIPELINE-STATUS.md`, once per entry, with where it goes; `extra/` takes anything — [page](rules/paper/allowed-files-only.md) |
|
|
17
20
|
| [`repro/allowed-files-only`](rules/repro/allowed-files-only.md) | error | the listing of the paper's `repro/` | an entry at the top of `repro/` is not `README.md`, `studies.json`, `.gitignore`, `tools/`, `data/` or a study folder named `<id>-<slug>` (or, in a paper without the registry, `<slug>-YYYY-MM-DD`), or a study folder has no `README.md`; reported on `PIPELINE-STATUS.md`, once per entry, with where it goes by kind (a script → `repro/tools/`, data → `repro/data/`, a note → its study or `research/`) — [page](rules/repro/allowed-files-only.md) |
|
|
@@ -279,13 +282,24 @@ The paper body is `paper.tex`, and it gets three rules of its own: `paper/typogr
|
|
|
279
282
|
run on it but judge the files beside it. The scorecard and the review notes are Markdown files, and
|
|
280
283
|
the other five rules read those.
|
|
281
284
|
|
|
282
|
-
##
|
|
285
|
+
## Frozen versions: `versions/<cycle>/` and its lock
|
|
286
|
+
|
|
287
|
+
**A paper that keeps `cycles`** freezes what it sends without a manual step:
|
|
288
|
+
`paperlint submission new --save` and `submission update --save` write the exact PDF they sent and the
|
|
289
|
+
source it was built from into `versions/<cycle>/submitted.{pdf,tex}` (`camera-ready.*` once the cycle
|
|
290
|
+
is accepted), and record their bytes, sha256 and the time in `versions/lock.json`, a file paperlint
|
|
291
|
+
writes and nobody edits. [`versions/layout`](rules/versions/layout.md) then fails if a frozen file no
|
|
292
|
+
longer matches the lock, and `npx paperlint versions` prints each version's state. A file you freeze
|
|
293
|
+
yourself is recorded with `npx paperlint versions lock`. The layout, the states and the move from the
|
|
294
|
+
`stages:` list (`npx paperlint versions migrate`) are in
|
|
295
|
+
[configuration](configuration.md#the-frozen-versions-one-folder-per-attempt).
|
|
296
|
+
|
|
297
|
+
### Without `cycles`: the scorecard's `bytes:` and `sourceBytes:`
|
|
283
298
|
|
|
284
299
|
**When you submit,** copy the PDF and `paper.tex` you sent into the paper's `versions/` folder and
|
|
285
300
|
record the stage in the front matter of `PIPELINE-STATUS.md` (the paper's scorecard). From then on
|
|
286
301
|
`paper/stages` fails if that PDF goes missing or changes size, and `paper/source` if its source was
|
|
287
|
-
not kept.
|
|
288
|
-
([#100](https://github.com/zernie/paperlint/issues/100)). A stage looks like this:
|
|
302
|
+
not kept. A stage looks like this:
|
|
289
303
|
|
|
290
304
|
```yaml
|
|
291
305
|
---
|
|
@@ -301,8 +315,8 @@ stages:
|
|
|
301
315
|
```
|
|
302
316
|
|
|
303
317
|
A stage name may use `a-z`, `0-9`, `.`, `_` and `-`. You write `bytes:` and `sourceBytes:` yourself
|
|
304
|
-
(`wc -c < versions/2026-07-22-submitted.pdf`), or the agent does, once, when the stage is recorded
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
318
|
+
(`wc -c < versions/2026-07-22-submitted.pdf`), or the agent does, once, when the stage is recorded.
|
|
319
|
+
They are not a checksum to maintain: a frozen PDF is never supposed to change, so `bytes:`
|
|
320
|
+
disagreeing with the file means the file was replaced after it was declared, and that is exactly the
|
|
321
|
+
finding. If you really did re-freeze a stage, update the number in the same commit. Recording the
|
|
322
|
+
paper's attempts as `cycles` moves all of this to the folders and the generated lock.
|
package/docs/submission.md
CHANGED
|
@@ -88,6 +88,22 @@ against the venue's call for papers.
|
|
|
88
88
|
The safe order: `new`, `new --save` once; then `show`, `update`, `update --save`, and `show` again
|
|
89
89
|
until it says `match yes`. Every `--save` comes after the author has read the dry run and said yes.
|
|
90
90
|
|
|
91
|
+
## What a save freezes
|
|
92
|
+
|
|
93
|
+
In a paper that keeps `cycles`, a `new --save` or `update --save` the portal took freezes what it
|
|
94
|
+
sent: the exact PDF, and the source the build read (a paper split into several files, assembled), as
|
|
95
|
+
`versions/<cycle>/submitted.{pdf,tex}` — `camera-ready.{pdf,tex}` once the cycle is accepted — and
|
|
96
|
+
records both in `versions/lock.json`. It prints one line:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
frozen versions/beta-2027/submitted.pdf (186504 bytes) and submitted.tex (57210 bytes), recorded in versions/lock.json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A later save in the same cycle replaces them: the last upload is the one under review. `show` ends
|
|
103
|
+
with the table of the paper's versions and their states, as `paperlint versions` prints it. A paper
|
|
104
|
+
without `cycles` is sent as before, and the save says `not frozen`. The layout and the states:
|
|
105
|
+
[configuration](configuration.md#the-frozen-versions-one-folder-per-attempt).
|
|
106
|
+
|
|
91
107
|
## The check before sending
|
|
92
108
|
|
|
93
109
|
A person confirms every send. To help them decide, `new` and `update` first run the venue rules of
|
|
@@ -107,9 +123,11 @@ With no findings it prints one line, `venue rules at their default options: no e
|
|
|
107
123
|
run at paperlint's default levels and options: a `rules` override in your `paperlint.json` or
|
|
108
124
|
preset is applied by `paperlint lint`, not here.
|
|
109
125
|
|
|
110
|
-
The command refuses
|
|
111
|
-
of other bytes)
|
|
112
|
-
|
|
126
|
+
The command refuses two things by itself, each with exit 2 and nothing sent: `--save` when no build
|
|
127
|
+
facts describe this PDF (none, or of other bytes), and — in a paper that keeps `cycles` — `--save`
|
|
128
|
+
when the sources changed since the build, because the PDF and the source on disk would no longer be a
|
|
129
|
+
pair to freeze. Rebuild (`paperlint build`) and run the dry run again. Everything else, including over
|
|
130
|
+
the limit, is the person's call.
|
|
113
131
|
|
|
114
132
|
**The `submission-confirm` hook** (installed by `paperlint init`) stops any `submission new|update`
|
|
115
133
|
with `--save` or `--submit` until the person has said yes to that exact PDF in that exact folder. The flow:
|
|
@@ -139,3 +139,65 @@ test("a frontmatter node without a value (another markdown language) reads as em
|
|
|
139
139
|
stages.rules[rule].create(context).yaml({ type: "yaml" });
|
|
140
140
|
assert.deepEqual(reports, []);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
const CYCLES = { "paperlint.json": JSON.stringify({ cycles: [] }) };
|
|
144
|
+
const RETIRED = (paper) =>
|
|
145
|
+
`\`stages\` is retired for a paper that keeps \`cycles\` in paperlint.json: its frozen versions are versions/<cycle>/<stage>.{pdf,tex}, recorded in versions/lock.json — \`npx paperlint versions migrate ${paper}\` moves the files there and takes the list out`;
|
|
146
|
+
|
|
147
|
+
test("under `cycles`: a `stages` list is one finding naming the migration, and dated files owe it nothing", () => {
|
|
148
|
+
const files = {
|
|
149
|
+
...CYCLES,
|
|
150
|
+
"versions/2026-08-02-submitted.pdf": "x",
|
|
151
|
+
};
|
|
152
|
+
assert.deepEqual(
|
|
153
|
+
lint(
|
|
154
|
+
"retired",
|
|
155
|
+
"stages:\n - stage: submitted\n date: 2026-08-02\n pdf: versions/2026-08-02-submitted.pdf\n bytes: 9",
|
|
156
|
+
{ files },
|
|
157
|
+
),
|
|
158
|
+
[RETIRED("retired")],
|
|
159
|
+
);
|
|
160
|
+
assert.deepEqual(
|
|
161
|
+
lint("retired-src", "stages:\n - stage: submitted\n date: 2026-08-02", {
|
|
162
|
+
rule: "source",
|
|
163
|
+
files,
|
|
164
|
+
}),
|
|
165
|
+
[],
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("under `cycles`: no list, an empty one, or no frontmatter is silent; YAML that does not parse is still named", () => {
|
|
170
|
+
assert.deepEqual(lint("retired-none", "title: x", { files: CYCLES }), []);
|
|
171
|
+
assert.deepEqual(lint("retired-empty", "stages: []", { files: CYCLES }), []);
|
|
172
|
+
const [msg] = lint("retired-yaml", "stages: [", { files: CYCLES });
|
|
173
|
+
assert.match(msg, /^the frontmatter does not parse as YAML: /);
|
|
174
|
+
// A paperlint.json that does not parse keeps the flat form's checks: `paperlint lint` refuses it before any rule runs.
|
|
175
|
+
assert.deepEqual(
|
|
176
|
+
lint("retired-broken", "stages:", {
|
|
177
|
+
files: { "paperlint.json": "{" },
|
|
178
|
+
}),
|
|
179
|
+
[
|
|
180
|
+
"`stages` must be a LIST of entries, not empty — a paper can reach the same stage twice",
|
|
181
|
+
],
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("under `cycles`: a frontmatter node without a value reads as empty YAML", () => {
|
|
186
|
+
const dir = writeTree(join(root, "retired-valueless"), CYCLES);
|
|
187
|
+
const reports = [];
|
|
188
|
+
const context = {
|
|
189
|
+
filename: join(dir, "PIPELINE-STATUS.md"),
|
|
190
|
+
cwd: dir,
|
|
191
|
+
report: (r) => reports.push(r),
|
|
192
|
+
};
|
|
193
|
+
stages.rules.stages.create(context).yaml({ type: "yaml" });
|
|
194
|
+
assert.deepEqual(reports, []);
|
|
195
|
+
// Linted from the paper itself, the command names it as `.`.
|
|
196
|
+
stages.rules.stages
|
|
197
|
+
.create(context)
|
|
198
|
+
.yaml({ type: "yaml", value: "stages:\n - stage: submitted" });
|
|
199
|
+
assert.deepEqual(
|
|
200
|
+
reports.map((r) => r.data),
|
|
201
|
+
[{ paper: "." }],
|
|
202
|
+
);
|
|
203
|
+
});
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
* keyed by stage name holds one. The `versions/<date>-<stage>.pdf` convention already holds
|
|
47
47
|
* many, and the field must not be weaker than the filenames it describes.
|
|
48
48
|
*/
|
|
49
|
-
import { existsSync, statSync, readdirSync } from "node:fs";
|
|
50
|
-
import { join, dirname } from "node:path";
|
|
49
|
+
import { existsSync, readFileSync, statSync, readdirSync } from "node:fs";
|
|
50
|
+
import { join, dirname, relative } from "node:path";
|
|
51
51
|
import assert from "node:assert/strict";
|
|
52
52
|
import { load } from "js-yaml";
|
|
53
53
|
import { z } from "zod";
|
|
@@ -68,6 +68,58 @@ const StageRecord = z
|
|
|
68
68
|
sourceLost: z.unknown().optional(),
|
|
69
69
|
})
|
|
70
70
|
.catch({});
|
|
71
|
+
/** What these rules read of the paper's settings: whether it keeps `cycles` at all. */
|
|
72
|
+
const Settings = z
|
|
73
|
+
.looseObject({ cycles: z.array(z.unknown()).optional() })
|
|
74
|
+
.catch({});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the paper keeps `cycles` in its paperlint.json. Its frozen versions are then
|
|
78
|
+
* `versions/<cycle>/<stage>.{pdf,tex}` recorded in `versions/lock.json`, judged by `versions/layout`,
|
|
79
|
+
* and the `stages:` list is retired: `paperlint versions migrate` turns it into folders.
|
|
80
|
+
*/
|
|
81
|
+
function keepsCycles(dir: string): boolean {
|
|
82
|
+
try {
|
|
83
|
+
const raw: unknown = JSON.parse(
|
|
84
|
+
readFileSync(join(dir, "paperlint.json"), "utf8"),
|
|
85
|
+
);
|
|
86
|
+
return Settings.parse(raw).cycles !== undefined;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Under `cycles`: a non-empty `stages:` list is one finding, naming the command that retires it. */
|
|
93
|
+
function retired(context: RuleContext, dir: string) {
|
|
94
|
+
return {
|
|
95
|
+
yaml(node: { readonly value?: string }) {
|
|
96
|
+
const parsed = (() => {
|
|
97
|
+
try {
|
|
98
|
+
return { ok: true as const, data: load(node.value ?? "") };
|
|
99
|
+
} catch (e) {
|
|
100
|
+
assert.ok(e instanceof Error);
|
|
101
|
+
return { ok: false as const, reason: e.message };
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
if (!parsed.ok) {
|
|
105
|
+
context.report({
|
|
106
|
+
node,
|
|
107
|
+
messageId: "badYaml",
|
|
108
|
+
data: { reason: parsed.reason },
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const raw = Frontmatter.parse(parsed.data).stages;
|
|
113
|
+
if (raw === undefined || (Array.isArray(raw) && raw.length === 0)) return;
|
|
114
|
+
context.report({
|
|
115
|
+
node,
|
|
116
|
+
messageId: "retired",
|
|
117
|
+
data: { paper: relative(context.cwd, dir) || "." },
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
71
123
|
const REQUIRED = ["date", "pdf", "bytes"] satisfies readonly (
|
|
72
124
|
"date" | "pdf" | "bytes"
|
|
73
125
|
)[];
|
|
@@ -126,6 +178,12 @@ function frozenPdfs(
|
|
|
126
178
|
return out;
|
|
127
179
|
}
|
|
128
180
|
|
|
181
|
+
/** The frozen PDFs in `versions/` that no declared stage of the same day names. */
|
|
182
|
+
const undeclared = (dir: string, records: readonly Declared[]) =>
|
|
183
|
+
frozenPdfs(join(dir, "versions")).filter(
|
|
184
|
+
(f) => !records.some((r) => r.stage === f.stage && r.date === f.date),
|
|
185
|
+
);
|
|
186
|
+
|
|
129
187
|
export default {
|
|
130
188
|
rules: {
|
|
131
189
|
stages: {
|
|
@@ -150,12 +208,14 @@ export default {
|
|
|
150
208
|
"«{{stage}}» ({{date}}): {{want}} bytes declared, {{got}} on disk — this is NOT that file",
|
|
151
209
|
fileNotDeclared:
|
|
152
210
|
"`versions/{{file}}` is frozen, but no «{{stage}}» stage on {{date}} is declared in `stages` — the artefact ran ahead of the declaration",
|
|
211
|
+
retired:
|
|
212
|
+
"`stages` is retired for a paper that keeps `cycles` in paperlint.json: its frozen versions are versions/<cycle>/<stage>.{pdf,tex}, recorded in versions/lock.json — `npx paperlint versions migrate {{paper}}` moves the files there and takes the list out",
|
|
153
213
|
},
|
|
154
214
|
},
|
|
155
215
|
create(context: RuleContext) {
|
|
156
216
|
const dir = dirname(context.filename);
|
|
217
|
+
if (keepsCycles(dir)) return retired(context, dir);
|
|
157
218
|
let declared: Declared[] | null = null; // null = there was no frontmatter at all
|
|
158
|
-
|
|
159
219
|
return {
|
|
160
220
|
yaml(node: { readonly value?: string }) {
|
|
161
221
|
let data: unknown;
|
|
@@ -252,16 +312,13 @@ export default {
|
|
|
252
312
|
// 🔴 On `root:exit` rather than inside `yaml`, because the case worth catching is
|
|
253
313
|
// precisely the one with NO frontmatter at all — where the `yaml` visitor never runs.
|
|
254
314
|
"root:exit"(node: object) {
|
|
255
|
-
|
|
256
|
-
for (const f of frozenPdfs(join(dir, "versions"))) {
|
|
257
|
-
if (records.some((r) => r.stage === f.stage && r.date === f.date))
|
|
258
|
-
continue;
|
|
315
|
+
undeclared(dir, declared ?? []).forEach((f) => {
|
|
259
316
|
context.report({
|
|
260
317
|
node,
|
|
261
318
|
messageId: "fileNotDeclared",
|
|
262
319
|
data: { file: f.name, stage: f.stage, date: f.date },
|
|
263
320
|
});
|
|
264
|
-
}
|
|
321
|
+
});
|
|
265
322
|
},
|
|
266
323
|
};
|
|
267
324
|
},
|
|
@@ -319,6 +376,8 @@ export default {
|
|
|
319
376
|
},
|
|
320
377
|
create(context: RuleContext) {
|
|
321
378
|
const dir = dirname(context.filename);
|
|
379
|
+
// Under `cycles` the lock records each frozen source, and `versions/layout` checks it.
|
|
380
|
+
if (keepsCycles(dir)) return {};
|
|
322
381
|
return {
|
|
323
382
|
yaml(node: { readonly value?: string }) {
|
|
324
383
|
let data: unknown;
|
|
@@ -55,6 +55,8 @@ export interface RuleReport {
|
|
|
55
55
|
|
|
56
56
|
export interface RuleContext {
|
|
57
57
|
readonly filename: string;
|
|
58
|
+
/** The directory ESLint runs from: what a command a message names is relative to. */
|
|
59
|
+
readonly cwd: string;
|
|
58
60
|
readonly options: readonly unknown[];
|
|
59
61
|
readonly settings: Readonly<Record<string, unknown>>;
|
|
60
62
|
readonly sourceCode: RuleSourceCode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperlint",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "13.0.0",
|
|
4
4
|
"description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@unified-latex/unified-latex-util-replace": "^1.8.4",
|
|
31
31
|
"@unified-latex/unified-latex-util-visit": "^1.8.4",
|
|
32
32
|
"ajv": "^6.12.6",
|
|
33
|
+
"diff": "^8.0.4",
|
|
33
34
|
"eslint": "^10.9.1",
|
|
34
35
|
"js-yaml": "^4.1.0",
|
|
35
36
|
"markdown-it": "^14.3.0",
|
|
@@ -11,7 +11,10 @@ the verdict line's wording and the `☐`/`☑` marks as they are, or the hook go
|
|
|
11
11
|
|
|
12
12
|
## The front matter
|
|
13
13
|
|
|
14
|
-
Above the sections, the file carries YAML.
|
|
14
|
+
Above the sections, the file carries YAML. In a paper WITHOUT `cycles` in its `paperlint.json`, the
|
|
15
|
+
rules read its `stages` field (a paper with `cycles` keeps no such list: its frozen versions are the
|
|
16
|
+
folders `versions/<cycle>/` and the generated `versions/lock.json`, and `paperlint versions migrate`
|
|
17
|
+
moves an old list there):
|
|
15
18
|
|
|
16
19
|
```yaml
|
|
17
20
|
---
|
|
@@ -4,7 +4,7 @@ description: End-to-end playbook for submitting a peer-reviewed paper to a doubl
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Skill]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:567bfff9583abdf1 compiled from skills/submit-paper/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# submit-paper — get a reviewed paper from "done" to "ready for review"
|
|
10
10
|
|
|
@@ -137,20 +137,28 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
|
|
|
137
137
|
|
|
138
138
|
## 7. Freeze the version you actually uploaded
|
|
139
139
|
|
|
140
|
-
🔴 **
|
|
141
|
-
|
|
140
|
+
🔴 **What the reviewers read is the one version git cannot rebuild for you later** — it was built
|
|
141
|
+
outside the repo and lives on the portal. Freeze it at the send.
|
|
142
|
+
|
|
143
|
+
In a paper that keeps `cycles` in `paperlint.json`, the send freezes it:
|
|
144
|
+
`paperlint submission update <paper> --save` (and `new --save`) writes the exact PDF it sent and the
|
|
145
|
+
source the build read into
|
|
142
146
|
|
|
143
147
|
```
|
|
144
|
-
<paper-dir>/versions/<
|
|
148
|
+
<paper-dir>/versions/<cycle id>/submitted.pdf
|
|
149
|
+
<paper-dir>/versions/<cycle id>/submitted.tex
|
|
145
150
|
```
|
|
146
151
|
|
|
147
|
-
and
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
and records their bytes and sha256 in `versions/lock.json` (generated; never edit it). After an
|
|
153
|
+
acceptance the same command freezes `camera-ready.{pdf,tex}`. An upload made on the portal by hand:
|
|
154
|
+
put that PDF and its source there under those names and run `npx paperlint versions lock <paper>`.
|
|
155
|
+
`npx paperlint versions` prints which version is under review and how far `paper.tex` has moved.
|
|
150
156
|
|
|
151
|
-
**This is enforced, not advised** — `
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
**This is enforced, not advised** — `cycle/submitted-frozen` fails a cycle that has a submission and
|
|
158
|
+
no frozen `submitted.pdf`, and `versions/layout` fails a frozen file that no longer matches the lock.
|
|
159
|
+
A paper without `cycles` records each stage in `PIPELINE-STATUS.md`'s `stages:` list instead,
|
|
160
|
+
checked by `paper/stages` and `paper/source`; `npx paperlint versions migrate` moves it to the
|
|
161
|
+
folders once it keeps `cycles`.
|
|
154
162
|
|
|
155
163
|
**Why it is a step and not hygiene:** `agenticdev-2026` kept a folder for exactly this for a month and
|
|
156
164
|
the file inside was the wrong build (340 952 B vs the real 352 357 B). It surfaced only when a reviewer
|
|
@@ -153,20 +153,28 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
|
|
|
153
153
|
|
|
154
154
|
## 7. Freeze the version you actually uploaded
|
|
155
155
|
|
|
156
|
-
🔴 **
|
|
157
|
-
|
|
156
|
+
🔴 **What the reviewers read is the one version git cannot rebuild for you later** — it was built
|
|
157
|
+
outside the repo and lives on the portal. Freeze it at the send.
|
|
158
|
+
|
|
159
|
+
In a paper that keeps \`cycles\` in \`paperlint.json\`, the send freezes it:
|
|
160
|
+
\`paperlint submission update <paper> --save\` (and \`new --save\`) writes the exact PDF it sent and the
|
|
161
|
+
source the build read into
|
|
158
162
|
|
|
159
163
|
\`\`\`
|
|
160
|
-
<paper-dir>/versions/<
|
|
164
|
+
<paper-dir>/versions/<cycle id>/submitted.pdf
|
|
165
|
+
<paper-dir>/versions/<cycle id>/submitted.tex
|
|
161
166
|
\`\`\`
|
|
162
167
|
|
|
163
|
-
and
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
and records their bytes and sha256 in \`versions/lock.json\` (generated; never edit it). After an
|
|
169
|
+
acceptance the same command freezes \`camera-ready.{pdf,tex}\`. An upload made on the portal by hand:
|
|
170
|
+
put that PDF and its source there under those names and run \`npx paperlint versions lock <paper>\`.
|
|
171
|
+
\`npx paperlint versions\` prints which version is under review and how far \`paper.tex\` has moved.
|
|
166
172
|
|
|
167
|
-
**This is enforced, not advised** — \`
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
**This is enforced, not advised** — \`cycle/submitted-frozen\` fails a cycle that has a submission and
|
|
174
|
+
no frozen \`submitted.pdf\`, and \`versions/layout\` fails a frozen file that no longer matches the lock.
|
|
175
|
+
A paper without \`cycles\` records each stage in \`PIPELINE-STATUS.md\`'s \`stages:\` list instead,
|
|
176
|
+
checked by \`paper/stages\` and \`paper/source\`; \`npx paperlint versions migrate\` moves it to the
|
|
177
|
+
folders once it keeps \`cycles\`.
|
|
170
178
|
|
|
171
179
|
**Why it is a step and not hygiene:** \`agenticdev-2026\` kept a folder for exactly this for a month and
|
|
172
180
|
the file inside was the wrong build (340 952 B vs the real 352 357 B). It surfaced only when a reviewer
|
|
@@ -81,9 +81,11 @@ need to wait for them or make them up.
|
|
|
81
81
|
|
|
82
82
|
## The frozen version
|
|
83
83
|
|
|
84
|
-
Camera-ready is a separate **stage** in the
|
|
85
|
-
`paper
|
|
86
|
-
|
|
84
|
+
Camera-ready is a separate **stage** in `versions/`: once the cycle's `outcome` is `accepted`,
|
|
85
|
+
`paperlint submission update <paper> --save` freezes the PDF it sends and its source as
|
|
86
|
+
`versions/<cycle>/camera-ready.{pdf,tex}` and records them in `versions/lock.json`. A camera-ready
|
|
87
|
+
uploaded through the publisher's own system (not the review portal) goes there by hand, followed by
|
|
88
|
+
`npx paperlint versions lock <paper>`.
|
|
87
89
|
|
|
88
90
|
**Why this isn't a formality:** the arXiv posting and the extension paper get diffed against
|
|
89
91
|
camera-ready later, and reviewer references to line numbers only resolve against the exact render
|
|
@@ -6,9 +6,31 @@ import {
|
|
|
6
6
|
fixedDownload,
|
|
7
7
|
memoryFiles,
|
|
8
8
|
memoryListEntries,
|
|
9
|
+
memoryCopyFile,
|
|
10
|
+
memoryMoveFile,
|
|
9
11
|
memoryWorkspace,
|
|
10
12
|
} from "./index.ts";
|
|
11
13
|
|
|
14
|
+
test("memoryMoveFile: the bytes move to the new path; a path holding nothing throws", () => {
|
|
15
|
+
const files = memoryFiles({ "/p/a.pdf": "x" });
|
|
16
|
+
const move = memoryMoveFile(files);
|
|
17
|
+
move(absolutePath("/p/a.pdf"), absolutePath("/p/v/b.pdf"));
|
|
18
|
+
assert.deepEqual([...files.map.keys()], ["/p/v/b.pdf"]);
|
|
19
|
+
assert.throws(() => {
|
|
20
|
+
move(absolutePath("/p/a.pdf"), absolutePath("/p/c.pdf"));
|
|
21
|
+
}, /ENOENT/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("memoryCopyFile: the bytes under the new path too; a path holding nothing throws", () => {
|
|
25
|
+
const files = memoryFiles({ "/p/a.tex": "x" });
|
|
26
|
+
const copy = memoryCopyFile(files);
|
|
27
|
+
copy(absolutePath("/p/a.tex"), absolutePath("/p/v/b.tex"));
|
|
28
|
+
assert.deepEqual([...files.map.keys()], ["/p/a.tex", "/p/v/b.tex"]);
|
|
29
|
+
assert.throws(() => {
|
|
30
|
+
copy(absolutePath("/p/none"), absolutePath("/p/c.tex"));
|
|
31
|
+
}, /ENOENT/);
|
|
32
|
+
});
|
|
33
|
+
|
|
12
34
|
test("memoryWorkspace: a `within` that throws is recorded as `threw`, and the error goes on", () => {
|
|
13
35
|
const ws = memoryWorkspace();
|
|
14
36
|
assert.throws(
|
|
@@ -7,6 +7,7 @@ import type { Download } from "../../ports/download.ts";
|
|
|
7
7
|
import type { Files } from "../../ports/files.ts";
|
|
8
8
|
import type { ListDir } from "../../ports/talk-media.ts";
|
|
9
9
|
import type { ListEntries } from "../../ports/list-entries.ts";
|
|
10
|
+
import type { CopyFile, MoveFile } from "../../ports/move-file.ts";
|
|
10
11
|
import type { Command, ProcessExit, RunProcess } from "../../ports/process.ts";
|
|
11
12
|
import type { NotAPromise, Scratch, Workspace } from "../../ports/workspace.ts";
|
|
12
13
|
import { err, ok } from "../../domain/result.ts";
|
|
@@ -73,6 +74,26 @@ export const memoryListDir =
|
|
|
73
74
|
),
|
|
74
75
|
];
|
|
75
76
|
|
|
77
|
+
/** `MoveFile` over `memoryFiles`: the bytes under the new path, none under the old; nothing there throws. */
|
|
78
|
+
/** `CopyFile` over `memoryFiles`: the bytes under the new path too; nothing there throws. */
|
|
79
|
+
export const memoryCopyFile =
|
|
80
|
+
(files: MemoryFiles): CopyFile =>
|
|
81
|
+
(from, to) => {
|
|
82
|
+
const b = files.map.get(from);
|
|
83
|
+
if (b === undefined) throw new Error(`ENOENT: no file to copy at ${from}`);
|
|
84
|
+
files.writeAtomic(to, b);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const memoryMoveFile =
|
|
88
|
+
(files: MemoryFiles): MoveFile =>
|
|
89
|
+
(from, to) => {
|
|
90
|
+
const b = files.map.get(from);
|
|
91
|
+
if (b === undefined) throw new Error(`ENOENT: no file to move at ${from}`);
|
|
92
|
+
files.writeAtomic(to, b);
|
|
93
|
+
// eslint-disable-next-line functional/immutable-data -- a test double's disk is the mutable map memoryFiles keeps
|
|
94
|
+
files.map.delete(from);
|
|
95
|
+
};
|
|
96
|
+
|
|
76
97
|
/**
|
|
77
98
|
* `ListEntries` over `memoryFiles`: each name directly under a directory, a file when it is one, else a
|
|
78
99
|
* directory. Memory holds no links: the walk's refusal to follow one is the node adapter's to show.
|
|
@@ -36,3 +36,4 @@ export { spawnProcess, type SpawnSync } from "./process.io.ts";
|
|
|
36
36
|
export { hostDirs } from "./host.io.ts";
|
|
37
37
|
export { nodeListDir } from "./list-dir.io.ts";
|
|
38
38
|
export { nodeListEntries } from "./list-entries.io.ts";
|
|
39
|
+
export { nodeCopyFile, nodeMoveFile } from "./move-file.io.ts";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** `MoveFile` and `CopyFile` over `node:fs`: a rename or a copy, the directory made first. */
|
|
2
|
+
import { copyFileSync, mkdirSync, renameSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
5
|
+
import type { CopyFile, MoveFile } from "../../ports/move-file.ts";
|
|
6
|
+
|
|
7
|
+
export const nodeMoveFile: MoveFile = (
|
|
8
|
+
from: AbsolutePath,
|
|
9
|
+
to: AbsolutePath,
|
|
10
|
+
): void => {
|
|
11
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
12
|
+
renameSync(from, to);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const nodeCopyFile: CopyFile = (
|
|
16
|
+
from: AbsolutePath,
|
|
17
|
+
to: AbsolutePath,
|
|
18
|
+
): void => {
|
|
19
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
20
|
+
copyFileSync(from, to);
|
|
21
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** `nodeMoveFile`: the file arrives with its bytes, into a folder made for it, and is gone from where it was. */
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { absolutePath } from "../../domain/paths.ts";
|
|
6
|
+
import { useTempDir } from "../../../test/support.ts";
|
|
7
|
+
import { nodeCopyFile, nodeMoveFile } from "./move-file.io.ts";
|
|
8
|
+
|
|
9
|
+
const root = useTempDir("paperlint-move-file-");
|
|
10
|
+
|
|
11
|
+
describe("nodeMoveFile", () => {
|
|
12
|
+
it("moves a file into a folder that does not exist yet", () => {
|
|
13
|
+
writeFileSync(join(root, "a.pdf"), "bytes");
|
|
14
|
+
nodeMoveFile(
|
|
15
|
+
absolutePath(join(root, "a.pdf")),
|
|
16
|
+
absolutePath(join(root, "x", "y", "b.pdf")),
|
|
17
|
+
);
|
|
18
|
+
expect(existsSync(join(root, "a.pdf"))).toBe(false);
|
|
19
|
+
expect(readFileSync(join(root, "x", "y", "b.pdf"), "utf8")).toBe("bytes");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("throws when there is nothing to move", () => {
|
|
23
|
+
expect(() => {
|
|
24
|
+
nodeMoveFile(
|
|
25
|
+
absolutePath(join(root, "none")),
|
|
26
|
+
absolutePath(join(root, "z")),
|
|
27
|
+
);
|
|
28
|
+
}).toThrow();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("nodeCopyFile", () => {
|
|
33
|
+
it("copies a file into a folder that does not exist yet, and leaves it where it was", () => {
|
|
34
|
+
writeFileSync(join(root, "s.tex"), "source");
|
|
35
|
+
nodeCopyFile(
|
|
36
|
+
absolutePath(join(root, "s.tex")),
|
|
37
|
+
absolutePath(join(root, "c", "d", "s.tex")),
|
|
38
|
+
);
|
|
39
|
+
expect(readFileSync(join(root, "s.tex"), "utf8")).toBe("source");
|
|
40
|
+
expect(readFileSync(join(root, "c", "d", "s.tex"), "utf8")).toBe("source");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("throws when there is nothing to copy", () => {
|
|
44
|
+
expect(() => {
|
|
45
|
+
nodeCopyFile(
|
|
46
|
+
absolutePath(join(root, "none")),
|
|
47
|
+
absolutePath(join(root, "w")),
|
|
48
|
+
);
|
|
49
|
+
}).toThrow();
|
|
50
|
+
});
|
|
51
|
+
});
|