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
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A VENUE'S DEADLINE — what each page said about it, and which instant binds.
|
|
3
|
+
*
|
|
4
|
+
* One entry per deadline of an attempt, in a cycle's `deadlines`:
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "what": "submission",
|
|
8
|
+
* "observed": [
|
|
9
|
+
* { "at": "2026-10-20T04:00:00Z", "source": "portal", "url": "…/deadlines", "read": "2026-10-05" },
|
|
10
|
+
* { "at": "2026-10-23 AoE", "source": "call", "url": "…/cfp", "read": "2026-10-05" }
|
|
11
|
+
* ],
|
|
12
|
+
* "override": { "at": "2026-10-27T04:00:00Z", "reason": "…", "evidence": "mail/extension.eml" }
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* ── OBSERVED, THEN OVERRIDDEN ──────────────────────────────────────────────────────────
|
|
16
|
+
* A READING is what one page said on one day: its instant, whether the page was the portal (which
|
|
17
|
+
* refuses the upload) or the call (which only announces), where, and when it was read. The portal
|
|
18
|
+
* and the call often disagree — an AoE day in a call is a day after the portal's UTC instant, and a
|
|
19
|
+
* HotCRP "submissions must be completed by" can fall on the call's abstract day — so both are kept,
|
|
20
|
+
* and the EARLIER binds: the safe reading of two. An OVERRIDE is a human decision that another
|
|
21
|
+
* instant binds (the chairs extended by mail); it wins, and it carries its reason and a file as
|
|
22
|
+
* evidence, so it is never a silent edit of a reading.
|
|
23
|
+
*
|
|
24
|
+
* ── THE WORDS ───────────────────────────────────────────────────────────────────────────
|
|
25
|
+
* A call says "abstract" and "paper"; HotCRP says registration, submission and resubmission. Here:
|
|
26
|
+
* `registration` is the abstract registration (a call's "abstract" date), `submission` the full paper
|
|
27
|
+
* (a call's "paper" date, HotCRP's "submissions must be completed by"), `resubmission` the updates a
|
|
28
|
+
* portal allows to a completed submission. Recorded honestly, a call's "paper" day and a portal's
|
|
29
|
+
* earlier "submission" cutoff are two readings of ONE deadline, and the earlier binds.
|
|
30
|
+
*
|
|
31
|
+
* Parsed by hand: the domain imports no schema library (see `src/CLAUDE.md`). Every object in the
|
|
32
|
+
* record refuses a key it does not know: an `override` typed under another name would otherwise look
|
|
33
|
+
* recorded and not be in force.
|
|
34
|
+
*/
|
|
35
|
+
import { err, ok, type Result } from "./result.ts";
|
|
36
|
+
import { isRecord } from "./record.ts";
|
|
37
|
+
|
|
38
|
+
/** A calendar day, `YYYY-MM-DD`. */
|
|
39
|
+
export type IsoDate = string;
|
|
40
|
+
/** A moment, as an ISO string in UTC: `2026-10-20T04:00:00Z`. Never a bare date. */
|
|
41
|
+
export type Instant = string;
|
|
42
|
+
|
|
43
|
+
export type DeadlineWhat =
|
|
44
|
+
| "registration"
|
|
45
|
+
| "submission"
|
|
46
|
+
| "resubmission"
|
|
47
|
+
| "notification"
|
|
48
|
+
| "camera-ready";
|
|
49
|
+
/** The kinds, in the order a venue's deadlines keep. */
|
|
50
|
+
export const DEADLINE_WHATS: readonly DeadlineWhat[] = [
|
|
51
|
+
"registration",
|
|
52
|
+
"submission",
|
|
53
|
+
"resubmission",
|
|
54
|
+
"notification",
|
|
55
|
+
"camera-ready",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** What each kind is, in a call's words where they differ — said in every message that names the kinds. */
|
|
59
|
+
export const WHAT_MEANS: Readonly<Record<DeadlineWhat, string | null>> = {
|
|
60
|
+
registration: 'abstract registration: a call\'s "abstract" date',
|
|
61
|
+
submission: 'the full paper: a call\'s "paper" date',
|
|
62
|
+
resubmission: "updates to a completed submission",
|
|
63
|
+
notification: null,
|
|
64
|
+
"camera-ready": null,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type DeadlineSource = "portal" | "call";
|
|
68
|
+
|
|
69
|
+
/** What one page said about one deadline, and on which day it was read. */
|
|
70
|
+
export interface Observation {
|
|
71
|
+
readonly at: Instant;
|
|
72
|
+
readonly source: DeadlineSource;
|
|
73
|
+
/** The page it was read on. */
|
|
74
|
+
readonly url: string;
|
|
75
|
+
readonly read: IsoDate;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A human decision that another instant binds, with why and the file it rests on. */
|
|
79
|
+
export interface Override {
|
|
80
|
+
readonly at: Instant;
|
|
81
|
+
readonly reason: string;
|
|
82
|
+
/** A file in the paper folder: the mail, the saved page. `cycle/evidence` requires it on disk. */
|
|
83
|
+
readonly evidence: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One deadline of an attempt, as the paper records it. */
|
|
87
|
+
export interface Deadline {
|
|
88
|
+
readonly what: DeadlineWhat;
|
|
89
|
+
readonly observed: readonly Observation[];
|
|
90
|
+
readonly override: Override | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** One reading of one deadline, as a venue preset carries it. */
|
|
94
|
+
export interface Reading extends Observation {
|
|
95
|
+
readonly what: DeadlineWhat;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** What binds a deadline: the override, or the reading that was earliest. */
|
|
99
|
+
export type Binding =
|
|
100
|
+
| { readonly kind: "override"; readonly override: Override }
|
|
101
|
+
| { readonly kind: "reading"; readonly reading: Observation };
|
|
102
|
+
|
|
103
|
+
/** A deadline in force: its instant, what set it, and every reading of it. */
|
|
104
|
+
export interface InForce {
|
|
105
|
+
readonly what: DeadlineWhat;
|
|
106
|
+
readonly at: Instant;
|
|
107
|
+
readonly by: Binding;
|
|
108
|
+
readonly readings: readonly Observation[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* What a venue's portal says about its deadlines, in paperlint's words — what the scheduled refresh
|
|
113
|
+
* of the shipped presets reads (`scripts/refresh-deadlines.ts`). One adapter per portal kind.
|
|
114
|
+
*/
|
|
115
|
+
export interface PortalDeadlines {
|
|
116
|
+
/** The portal's clock when it answered, Unix seconds. */
|
|
117
|
+
readonly now: number;
|
|
118
|
+
/** Whether the portal takes submissions now. */
|
|
119
|
+
readonly open: boolean;
|
|
120
|
+
/** How the venue reviews, as the portal says it; null when it does not say. */
|
|
121
|
+
readonly blind: boolean | "optional" | "until-review" | null;
|
|
122
|
+
readonly deadlines: readonly PortalDeadline[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One deadline the portal lists: its kind when it is one paperlint names, the portal's own label, and when. */
|
|
126
|
+
export interface PortalDeadline {
|
|
127
|
+
readonly what: DeadlineWhat | null;
|
|
128
|
+
readonly label: string;
|
|
129
|
+
readonly at: Instant;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── instants ───────────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
135
|
+
export const isIsoDate = (v: unknown): v is IsoDate =>
|
|
136
|
+
typeof v === "string" && ISO_DATE.test(v) && !Number.isNaN(Date.parse(v));
|
|
137
|
+
|
|
138
|
+
const asInstant = (ms: number): Instant =>
|
|
139
|
+
new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
140
|
+
|
|
141
|
+
/** Unix seconds → the instant, in UTC. */
|
|
142
|
+
export const instantOfUnix = (seconds: number): Instant =>
|
|
143
|
+
asInstant(seconds * 1000);
|
|
144
|
+
|
|
145
|
+
/** The forms `at` takes, for every message that refuses one. */
|
|
146
|
+
const INSTANT_FORMS = `an instant with its zone (2026-10-20T04:00:00Z) or a call's "YYYY-MM-DD AoE"`;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* An ISO instant with its zone (`Z` or `±hh:mm`), or a call's `YYYY-MM-DD AoE`; normalised to UTC.
|
|
150
|
+
* A bare date or a zoneless time is refused: it is the deadline people get wrong.
|
|
151
|
+
*/
|
|
152
|
+
export function instantOf(text: string): Result<Instant, string> {
|
|
153
|
+
const bad = err(
|
|
154
|
+
`"${text}" must be an instant with its zone (2026-10-20T04:00:00Z, 2026-10-20T09:00:00+05:00) or a call's "YYYY-MM-DD AoE"`,
|
|
155
|
+
);
|
|
156
|
+
const aoe = /^(\d{4}-\d{2}-\d{2}) AoE$/i.exec(text.trim());
|
|
157
|
+
if (aoe) {
|
|
158
|
+
const day = String(aoe[1]); // the group always participates in a match
|
|
159
|
+
// The end of that day at UTC−12: 23:59:59 there is 11:59:59 UTC of the next day.
|
|
160
|
+
const ms = Date.parse(`${day}T23:59:59-12:00`);
|
|
161
|
+
return Number.isNaN(ms) ? bad : ok(asInstant(ms));
|
|
162
|
+
}
|
|
163
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(Z|[+-]\d{2}:\d{2})$/.test(text))
|
|
164
|
+
return bad;
|
|
165
|
+
const ms = Date.parse(text);
|
|
166
|
+
return Number.isNaN(ms) ? bad : ok(asInstant(ms));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── parsing ────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/** The first key of `v` that is not in `known`, as the refusal naming it; null when there is none. */
|
|
172
|
+
export const unknownKey = (
|
|
173
|
+
v: Readonly<Record<string, unknown>>,
|
|
174
|
+
known: readonly string[],
|
|
175
|
+
at: string,
|
|
176
|
+
whose: string,
|
|
177
|
+
): string | null => {
|
|
178
|
+
const k = Object.keys(v).find((x) => !known.includes(x));
|
|
179
|
+
return k === undefined
|
|
180
|
+
? null
|
|
181
|
+
: `${at}: unknown key "${k}" — ${whose} keys: ${known.join(", ")}`;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** `v[key]` as an instant, or the refusal naming the key and both forms. */
|
|
185
|
+
const instantField = (
|
|
186
|
+
v: Readonly<Record<string, unknown>>,
|
|
187
|
+
key: string,
|
|
188
|
+
at: string,
|
|
189
|
+
): Result<Instant, string> => {
|
|
190
|
+
const text = v[key];
|
|
191
|
+
const instant =
|
|
192
|
+
typeof text === "string" ? instantOf(text) : err("not a string");
|
|
193
|
+
return instant.ok
|
|
194
|
+
? instant
|
|
195
|
+
: err(
|
|
196
|
+
`${at}: "${key}" must be ${INSTANT_FORMS}, got ${JSON.stringify(text)}`,
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const nonEmpty = (v: unknown): v is string => typeof v === "string" && v !== "";
|
|
201
|
+
|
|
202
|
+
const READING_KEYS: readonly string[] = ["at", "source", "url", "read"];
|
|
203
|
+
|
|
204
|
+
/** `source`, `url` and `read` of a reading: where and when it was read. */
|
|
205
|
+
function provenance(
|
|
206
|
+
v: Readonly<Record<string, unknown>>,
|
|
207
|
+
at: string,
|
|
208
|
+
): Result<Omit<Observation, "at">, string> {
|
|
209
|
+
const { source, url, read } = v;
|
|
210
|
+
if (source !== "portal" && source !== "call")
|
|
211
|
+
return err(
|
|
212
|
+
`${at}: "source" must be "portal" or "call", got ${JSON.stringify(source)} — a deadline moved by mail is an "override", with the mail as its "evidence"`,
|
|
213
|
+
);
|
|
214
|
+
if (!nonEmpty(url))
|
|
215
|
+
return err(`${at}: "url" must be where the deadline was read`);
|
|
216
|
+
if (!isIsoDate(read))
|
|
217
|
+
return err(`${at}: "read" must be the day the page was read, YYYY-MM-DD`);
|
|
218
|
+
return ok({ source, url, read });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseObservation(v: unknown, at: string): Result<Observation, string> {
|
|
222
|
+
if (!isRecord(v))
|
|
223
|
+
return err(`${at}: a reading must be { "at", "source", "url", "read" }`);
|
|
224
|
+
const unknown = unknownKey(v, READING_KEYS, at, "a reading's");
|
|
225
|
+
if (unknown !== null) return err(unknown);
|
|
226
|
+
const instant = instantField(v, "at", at);
|
|
227
|
+
if (!instant.ok) return instant;
|
|
228
|
+
const rest = provenance(v, at);
|
|
229
|
+
return rest.ok ? ok({ at: instant.value, ...rest.value }) : rest;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Each item of a list through `one`; the first refusal, naming its index. */
|
|
233
|
+
function eachOf<T>(
|
|
234
|
+
list: readonly unknown[],
|
|
235
|
+
at: string,
|
|
236
|
+
one: (v: unknown, at: string) => Result<T, string>,
|
|
237
|
+
): Result<readonly T[], string> {
|
|
238
|
+
return list.reduce<Result<readonly T[], string>>((acc, v, i) => {
|
|
239
|
+
if (!acc.ok) return acc;
|
|
240
|
+
const p = one(v, `${at}[${String(i)}]`);
|
|
241
|
+
return p.ok ? ok([...acc.value, p.value]) : p;
|
|
242
|
+
}, ok([]));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The readings of one deadline: at most one per source — a re-read replaces the earlier one. */
|
|
246
|
+
function parseObserved(
|
|
247
|
+
v: unknown,
|
|
248
|
+
at: string,
|
|
249
|
+
): Result<readonly Observation[], string> {
|
|
250
|
+
if (v === undefined) return ok([]);
|
|
251
|
+
if (!Array.isArray(v))
|
|
252
|
+
return err(
|
|
253
|
+
`${at}: "observed" must be a list of readings { "at", "source", "url", "read" }`,
|
|
254
|
+
);
|
|
255
|
+
const list = eachOf(v, `${at}.observed`, parseObservation);
|
|
256
|
+
if (!list.ok) return list;
|
|
257
|
+
const twice = (["portal", "call"] as const).find(
|
|
258
|
+
(s) => list.value.filter((o) => o.source === s).length > 1,
|
|
259
|
+
);
|
|
260
|
+
return twice === undefined
|
|
261
|
+
? list
|
|
262
|
+
: err(
|
|
263
|
+
`${at}: two "${twice}" readings — keep one per source, the latest (its "read" says when)`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const OVERRIDE_KEYS: readonly string[] = ["at", "reason", "evidence"];
|
|
268
|
+
|
|
269
|
+
function parseOverride(
|
|
270
|
+
v: unknown,
|
|
271
|
+
at: string,
|
|
272
|
+
): Result<Override | null, string> {
|
|
273
|
+
if (v === undefined) return ok(null);
|
|
274
|
+
if (!isRecord(v))
|
|
275
|
+
return err(`${at}: "override" must be { "at", "reason", "evidence" }`);
|
|
276
|
+
const where = `${at}.override`;
|
|
277
|
+
const unknown = unknownKey(v, OVERRIDE_KEYS, where, "an override's");
|
|
278
|
+
if (unknown !== null) return err(unknown);
|
|
279
|
+
const instant = instantField(v, "at", where);
|
|
280
|
+
if (!instant.ok) return instant;
|
|
281
|
+
const { reason, evidence } = v;
|
|
282
|
+
if (!nonEmpty(reason))
|
|
283
|
+
return err(
|
|
284
|
+
`${where}: "reason" must say why this instant binds rather than the readings (the chairs extended it by mail, …)`,
|
|
285
|
+
);
|
|
286
|
+
return nonEmpty(evidence)
|
|
287
|
+
? ok({ at: instant.value, reason, evidence })
|
|
288
|
+
: err(
|
|
289
|
+
`${where}: "evidence" must be a file in the paper folder — the mail or the saved page the override rests on`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const isWhat = (v: unknown): v is DeadlineWhat =>
|
|
294
|
+
DEADLINE_WHATS.some((w) => w === v);
|
|
295
|
+
|
|
296
|
+
/** Every kind with what it is, for the message that refuses an unknown one. */
|
|
297
|
+
const KINDS_TEXT = DEADLINE_WHATS.map((w) => {
|
|
298
|
+
const means = WHAT_MEANS[w];
|
|
299
|
+
return means === null ? w : `${w} (${means})`;
|
|
300
|
+
}).join(", ");
|
|
301
|
+
|
|
302
|
+
const DEADLINE_KEYS: readonly string[] = ["what", "observed", "override"];
|
|
303
|
+
|
|
304
|
+
function parseDeadline(v: unknown, at: string): Result<Deadline, string> {
|
|
305
|
+
if (!isRecord(v))
|
|
306
|
+
return err(
|
|
307
|
+
`${at}: a deadline must be { "what", "observed": [ … ], "override"? }`,
|
|
308
|
+
);
|
|
309
|
+
const unknown = unknownKey(v, DEADLINE_KEYS, at, "a deadline's");
|
|
310
|
+
if (unknown !== null) return err(unknown);
|
|
311
|
+
const what = v["what"];
|
|
312
|
+
if (!isWhat(what))
|
|
313
|
+
return err(
|
|
314
|
+
`${at}: "what" must be one of ${KINDS_TEXT}; got ${JSON.stringify(what)}`,
|
|
315
|
+
);
|
|
316
|
+
const observed = parseObserved(v["observed"], at);
|
|
317
|
+
if (!observed.ok) return observed;
|
|
318
|
+
const override = parseOverride(v["override"], at);
|
|
319
|
+
if (!override.ok) return override;
|
|
320
|
+
return observed.value.length === 0 && override.value === null
|
|
321
|
+
? err(
|
|
322
|
+
`${at}: the "${what}" deadline records nothing — add what a page said ("observed") or an "override"`,
|
|
323
|
+
)
|
|
324
|
+
: ok({ what, observed: observed.value, override: override.value });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The first kind with two entries, as the refusal naming both; null when each kind has one. */
|
|
328
|
+
function twoEntries(list: readonly Deadline[], at: string): string | null {
|
|
329
|
+
const second = list.findIndex(
|
|
330
|
+
(d, i) => list.findIndex((e) => e.what === d.what) !== i,
|
|
331
|
+
);
|
|
332
|
+
const d = list[second];
|
|
333
|
+
if (d === undefined) return null;
|
|
334
|
+
const first = list.findIndex((e) => e.what === d.what);
|
|
335
|
+
return `${at}: the "${d.what}" deadline has two entries (deadlines[${String(first)}] and deadlines[${String(second)}]) — one entry per deadline: put every reading of it in that entry's "observed" list`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** A cycle's `deadlines` value → the list, or one line naming the place and the cure. Pure. */
|
|
339
|
+
export function parseDeadlines(
|
|
340
|
+
v: unknown,
|
|
341
|
+
at: string,
|
|
342
|
+
): Result<readonly Deadline[], string> {
|
|
343
|
+
if (v === undefined) return ok([]);
|
|
344
|
+
if (!Array.isArray(v)) return err(`${at}: "deadlines" must be a list`);
|
|
345
|
+
const list = eachOf(v, `${at}.deadlines`, parseDeadline);
|
|
346
|
+
if (!list.ok) return list;
|
|
347
|
+
const twice = twoEntries(list.value, at);
|
|
348
|
+
return twice === null ? list : err(twice);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── deriving ───────────────────────────────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
/** The earliest of the readings; the first of equals. */
|
|
354
|
+
const earliest = (readings: readonly Observation[]): Observation | undefined =>
|
|
355
|
+
readings.reduce<Observation | undefined>(
|
|
356
|
+
(best, o) => (best === undefined || o.at < best.at ? o : best),
|
|
357
|
+
undefined,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const withoutWhat = ({ at, source, url, read }: Reading): Observation => ({
|
|
361
|
+
at,
|
|
362
|
+
source,
|
|
363
|
+
url,
|
|
364
|
+
read,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
/** One kind in force from the preset's readings and the paper's entry; null when neither says anything. */
|
|
368
|
+
function oneInForce(
|
|
369
|
+
what: DeadlineWhat,
|
|
370
|
+
own: Deadline | undefined,
|
|
371
|
+
preset: readonly Reading[],
|
|
372
|
+
): InForce | null {
|
|
373
|
+
const readings = [
|
|
374
|
+
...preset.filter((r) => r.what === what).map(withoutWhat),
|
|
375
|
+
...(own?.observed ?? []),
|
|
376
|
+
];
|
|
377
|
+
const override = own?.override ?? null;
|
|
378
|
+
if (override !== null)
|
|
379
|
+
return {
|
|
380
|
+
what,
|
|
381
|
+
at: override.at,
|
|
382
|
+
by: { kind: "override", override },
|
|
383
|
+
readings,
|
|
384
|
+
};
|
|
385
|
+
const first = earliest(readings);
|
|
386
|
+
return first === undefined
|
|
387
|
+
? null
|
|
388
|
+
: { what, at: first.at, by: { kind: "reading", reading: first }, readings };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Every deadline in force for an attempt: the paper's own entries over the venue preset's readings
|
|
393
|
+
* (empty for a venue without a preset). The override, else the earliest reading. Pure.
|
|
394
|
+
*/
|
|
395
|
+
export function deadlinesInForce(
|
|
396
|
+
own: readonly Deadline[],
|
|
397
|
+
preset: readonly Reading[],
|
|
398
|
+
): readonly InForce[] {
|
|
399
|
+
return DEADLINE_WHATS.flatMap((what) => {
|
|
400
|
+
const d = oneInForce(
|
|
401
|
+
what,
|
|
402
|
+
own.find((x) => x.what === what),
|
|
403
|
+
preset,
|
|
404
|
+
);
|
|
405
|
+
return d === null ? [] : [d];
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** One instant per kind, the earliest a layer holds for it, in the order of the kinds. */
|
|
410
|
+
type Layer = readonly { readonly what: DeadlineWhat; readonly at: Instant }[];
|
|
411
|
+
|
|
412
|
+
const layerOf = (entries: Layer): Layer =>
|
|
413
|
+
DEADLINE_WHATS.flatMap((what) => {
|
|
414
|
+
const ats = entries.filter((e) => e.what === what).map((e) => e.at);
|
|
415
|
+
const first = ats.toSorted()[0];
|
|
416
|
+
return first === undefined ? [] : [{ what, at: first }];
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
/** The pairs of a layer out of the order registration ≤ submission ≤ resubmission < notification < camera-ready. */
|
|
420
|
+
const inversions = (
|
|
421
|
+
layer: Layer,
|
|
422
|
+
says: (what: DeadlineWhat, at: Instant, later: boolean) => string,
|
|
423
|
+
): readonly string[] =>
|
|
424
|
+
layer.flatMap((d, i) => {
|
|
425
|
+
const before = layer[i - 1];
|
|
426
|
+
return before !== undefined && before.at > d.at
|
|
427
|
+
? [
|
|
428
|
+
`${says(before.what, before.at, false)} is after ${says(d.what, d.at, true)}`,
|
|
429
|
+
]
|
|
430
|
+
: [];
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const readingsOf = (
|
|
434
|
+
own: readonly Deadline[],
|
|
435
|
+
preset: readonly Reading[],
|
|
436
|
+
source: DeadlineSource,
|
|
437
|
+
): Layer => [
|
|
438
|
+
...preset.filter((r) => r.source === source),
|
|
439
|
+
...own.flatMap((d) =>
|
|
440
|
+
d.observed
|
|
441
|
+
.filter((o) => o.source === source)
|
|
442
|
+
.map((o) => ({ what: d.what, at: o.at })),
|
|
443
|
+
),
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The deadlines out of order WITHIN one source — the portal's readings, the call's, the overrides —
|
|
448
|
+
* the shape a date typed wrong or an AoE day miscounted takes. Across sources a disagreement is what
|
|
449
|
+
* the record holds, and the earlier binds; it is not a problem. Pure.
|
|
450
|
+
*/
|
|
451
|
+
export function deadlineOrderProblems(
|
|
452
|
+
own: readonly Deadline[],
|
|
453
|
+
preset: readonly Reading[],
|
|
454
|
+
): readonly string[] {
|
|
455
|
+
const overrides = own.flatMap((d) =>
|
|
456
|
+
d.override === null ? [] : [{ what: d.what, at: d.override.at }],
|
|
457
|
+
);
|
|
458
|
+
return [
|
|
459
|
+
...inversions(
|
|
460
|
+
layerOf(readingsOf(own, preset, "portal")),
|
|
461
|
+
(w, at, later) => `${later ? "its" : "the portal's"} "${w}" (${at})`,
|
|
462
|
+
),
|
|
463
|
+
...inversions(
|
|
464
|
+
layerOf(readingsOf(own, preset, "call")),
|
|
465
|
+
(w, at, later) => `${later ? "its" : "the call's"} "${w}" (${at})`,
|
|
466
|
+
),
|
|
467
|
+
...inversions(
|
|
468
|
+
layerOf(overrides),
|
|
469
|
+
(w, at) => `the override of "${w}" (${at})`,
|
|
470
|
+
),
|
|
471
|
+
];
|
|
472
|
+
}
|
|
@@ -160,7 +160,7 @@ describe("paper/folder-venue-leftover — silent", () => {
|
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
it("judged once per paper, on PIPELINE-STATUS.md only", () => {
|
|
163
|
-
expect(lint("aisec-2026", AIDC, { file: "paper.
|
|
163
|
+
expect(lint("aisec-2026", AIDC, { file: "paper.tex" })).toEqual([]);
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
it("a paper on a bare template family declares no venue: a venue in its folder may be its own", () => {
|
package/src/init.test.ts
CHANGED
|
@@ -55,8 +55,8 @@ function project(): string {
|
|
|
55
55
|
"paperlint.json": JSON.stringify({ papersDir: "papers" }),
|
|
56
56
|
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
57
57
|
"eslint-rules/fixtures/x/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
58
|
-
"eslint-rules/fixtures/y/paper.
|
|
59
|
-
"drafts/z/paper.
|
|
58
|
+
"eslint-rules/fixtures/y/paper.tex": "% y\n",
|
|
59
|
+
"drafts/z/paper.tex": "% z\n",
|
|
60
60
|
};
|
|
61
61
|
for (const [p, text] of Object.entries(files)) {
|
|
62
62
|
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
@@ -291,7 +291,7 @@ describe("paperlint init — a paperlint.json it cannot use stops it before anyt
|
|
|
291
291
|
for (const json of ["null\n", "[1]\n"]) {
|
|
292
292
|
const root = tree({
|
|
293
293
|
"paperlint.json": json,
|
|
294
|
-
"drafts/a/paper.
|
|
294
|
+
"drafts/a/paper.tex": "% a\n",
|
|
295
295
|
});
|
|
296
296
|
const decl = await declarePapers(root, () =>
|
|
297
297
|
choosePapers(root, { interactive: false }),
|
|
@@ -308,7 +308,7 @@ describe("paperlint init — a paperlint.json it cannot use stops it before anyt
|
|
|
308
308
|
it("a file without a trailing newline keeps having none", async () => {
|
|
309
309
|
const root = tree({
|
|
310
310
|
"paperlint.json": '{"x":1}',
|
|
311
|
-
"drafts/a/paper.
|
|
311
|
+
"drafts/a/paper.tex": "% a\n",
|
|
312
312
|
});
|
|
313
313
|
const decl = await declarePapers(root, () =>
|
|
314
314
|
choosePapers(root, { interactive: false }),
|
|
@@ -322,7 +322,7 @@ describe("paperlint init — a paperlint.json it cannot use stops it before anyt
|
|
|
322
322
|
|
|
323
323
|
describe("paperlint init — how several candidate directories were decided, said", () => {
|
|
324
324
|
const several = () =>
|
|
325
|
-
tree({ "drafts/a/paper.
|
|
325
|
+
tree({ "drafts/a/paper.tex": "% a\n", "writing/b/paper.tex": "% b\n" });
|
|
326
326
|
const section = (out: string[]) =>
|
|
327
327
|
out.slice(out.indexOf("papers directory") + 1, out.indexOf("settings") - 1);
|
|
328
328
|
|
|
@@ -530,7 +530,6 @@ describe("paperlint init — a first paper named after a venue", () => {
|
|
|
530
530
|
createPaper: (_root, name) => Promise.resolve((made.push(name), 0)),
|
|
531
531
|
});
|
|
532
532
|
expect(made).toEqual([]);
|
|
533
|
-
expect(asked.filter((q) => /format/.test(q))).toEqual([]);
|
|
534
533
|
expect(r.out[r.out.indexOf("first paper") + 1]).toBe(
|
|
535
534
|
' ✗ no paper created — `aisec-2026` — the paper folder names a venue ("AISec"); venues change on resubmission, so name it after the work\n if the venue really belongs in the name, rerun `paperlint new` with --allow-venue-name',
|
|
536
535
|
);
|
|
@@ -539,7 +538,7 @@ describe("paperlint init — a first paper named after a venue", () => {
|
|
|
539
538
|
|
|
540
539
|
describe("paperlint init — paths are shown relative to where it was run", () => {
|
|
541
540
|
it("a path that IS the working directory is shown whole, not as an empty string", async () => {
|
|
542
|
-
const root = tree({ "papers/a/paper.
|
|
541
|
+
const root = tree({ "papers/a/paper.tex": "% a\n" });
|
|
543
542
|
const home = join(root, ".claude", "skills");
|
|
544
543
|
mkdirSync(home, { recursive: true });
|
|
545
544
|
const r = await initRun(root, {
|
package/src/init.ts
CHANGED
|
@@ -63,12 +63,7 @@ import {
|
|
|
63
63
|
type Merge,
|
|
64
64
|
type WireResult,
|
|
65
65
|
} from "./hooks-settings.ts";
|
|
66
|
-
import {
|
|
67
|
-
DEFAULT_FORMAT,
|
|
68
|
-
isFormat,
|
|
69
|
-
nameProblem,
|
|
70
|
-
type PaperFormat,
|
|
71
|
-
} from "./new-paper.ts";
|
|
66
|
+
import { nameProblem } from "./new-paper.ts";
|
|
72
67
|
// The one source for the consumer's config key lives in lib/ (the ESLint rules and the skill scripts
|
|
73
68
|
// import it too). It is imported compiled, from dist/, the one path that resolves the same from src/
|
|
74
69
|
// and from dist/ — see CONTRIBUTING.md.
|
|
@@ -572,17 +567,11 @@ export interface InitOptions {
|
|
|
572
567
|
merge?: Merge | undefined;
|
|
573
568
|
/** `--paper <name>`: create this paper, even without a terminal. */
|
|
574
569
|
paper?: string | null;
|
|
575
|
-
/** `--format tex|md` for that paper. */
|
|
576
|
-
format?: PaperFormat | null;
|
|
577
570
|
/**
|
|
578
571
|
* Creates one paper and lints it — `paperlint new`'s own routine, passed in by the CLI so `init` and
|
|
579
572
|
* `new` cannot drift into two implementations.
|
|
580
573
|
*/
|
|
581
|
-
createPaper?: (
|
|
582
|
-
papersRoot: string,
|
|
583
|
-
name: string,
|
|
584
|
-
format: PaperFormat,
|
|
585
|
-
) => Promise<number>;
|
|
574
|
+
createPaper?: (papersRoot: string, name: string) => Promise<number>;
|
|
586
575
|
/**
|
|
587
576
|
* The venues a paper name may not name (`shippedVenueNames`), so `--paper aisec-2026` is refused
|
|
588
577
|
* before anything is asked. `createPaper` refuses it again whatever this holds.
|
|
@@ -717,7 +706,6 @@ export async function init(
|
|
|
717
706
|
hooks = true,
|
|
718
707
|
merge,
|
|
719
708
|
paper = null,
|
|
720
|
-
format = null,
|
|
721
709
|
createPaper,
|
|
722
710
|
venues = [],
|
|
723
711
|
run = spawnSync,
|
|
@@ -824,18 +812,11 @@ export async function init(
|
|
|
824
812
|
}
|
|
825
813
|
if (wanted !== null && createPaper) {
|
|
826
814
|
const problem = nameProblem(wanted, venues);
|
|
827
|
-
let fmt: PaperFormat = format ?? DEFAULT_FORMAT;
|
|
828
|
-
if (!problem && format === null && interactive) {
|
|
829
|
-
const f = (
|
|
830
|
-
await askOrDefault(ask, ` format: tex / md [${DEFAULT_FORMAT}] `)
|
|
831
|
-
)?.trim();
|
|
832
|
-
if (isFormat(f)) fmt = f;
|
|
833
|
-
}
|
|
834
815
|
if (problem) {
|
|
835
816
|
log(` ✗ no paper created — ${problem}`);
|
|
836
817
|
paperCode = 2;
|
|
837
818
|
} else {
|
|
838
|
-
const code = await createPaper(papersAbs, wanted
|
|
819
|
+
const code = await createPaper(papersAbs, wanted);
|
|
839
820
|
if (code !== 0) {
|
|
840
821
|
log(` ⚠ the new paper's lint exited ${String(code)} — see above`);
|
|
841
822
|
paperCode = code;
|
package/src/lint-fix.test.ts
CHANGED
|
@@ -20,6 +20,10 @@ afterEach(() => {
|
|
|
20
20
|
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
/** A paper.tex whose whole body is `body`. */
|
|
24
|
+
const PAPER = (body: string): string =>
|
|
25
|
+
`\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
|
|
26
|
+
|
|
23
27
|
function project(
|
|
24
28
|
settings: Record<string, unknown>,
|
|
25
29
|
files: Record<string, string> = {},
|
|
@@ -30,7 +34,7 @@ function project(
|
|
|
30
34
|
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
31
35
|
"paperlint.json": JSON.stringify({ papersDir: "papers", ...settings }),
|
|
32
36
|
"papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
|
|
33
|
-
"papers/a/paper.
|
|
37
|
+
"papers/a/paper.tex": PAPER("See §5, §6 and §7; $p < .05$."),
|
|
34
38
|
...files,
|
|
35
39
|
};
|
|
36
40
|
for (const [p, text] of Object.entries(all)) {
|
|
@@ -64,15 +68,17 @@ describe("paperlint lint --fix", () => {
|
|
|
64
68
|
const fixed = await lint(root, ["--fix"]);
|
|
65
69
|
expect(ruleIds(fixed.out)).not.toContain("paper/section-word");
|
|
66
70
|
expect(ruleIds(fixed.out)).not.toContain("paper/leading-zero");
|
|
67
|
-
expect(readFileSync(join(root, "papers/a/paper.
|
|
68
|
-
"
|
|
71
|
+
expect(readFileSync(join(root, "papers/a/paper.tex"), "utf8")).toBe(
|
|
72
|
+
PAPER("See Section 5, Section 6 and Section 7; $p < 0.05$."),
|
|
69
73
|
);
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
it("without --fix nothing is written", async () => {
|
|
73
77
|
const root = project({});
|
|
74
78
|
await lint(root);
|
|
75
|
-
expect(readFileSync(join(root, "papers/a/paper.
|
|
79
|
+
expect(readFileSync(join(root, "papers/a/paper.tex"), "utf8")).toMatch(
|
|
80
|
+
/§5/,
|
|
81
|
+
);
|
|
76
82
|
});
|
|
77
83
|
});
|
|
78
84
|
|