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
package/src/paper-settings.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* One file name at two levels, one schema (lib/paper-config.mjs → SETTINGS_KEYS):
|
|
5
5
|
*
|
|
6
6
|
* paperlint.json the project (optional): papersDir, rules, defaults
|
|
7
|
-
* <paper>/paperlint.json this paper: { extends, kind, pdf, rules, identity, talk, submission }
|
|
7
|
+
* <paper>/paperlint.json this paper: { extends, kind, pdf, rules, identity, talk, submission } —
|
|
8
|
+
* or `cycles`, from whose current attempt extends, kind and submission
|
|
9
|
+
* are DERIVED (src/domain/cycle.ts); the two forms do not mix
|
|
8
10
|
* a venue preset `paperlint:<name>` (shipped) or `./x.jsonc` (src/presets.ts)
|
|
9
11
|
*
|
|
10
12
|
* The paper's `extends`, `kind` and `pdf` win over the root's; with no own value, the root's is
|
|
@@ -36,6 +38,13 @@ import {
|
|
|
36
38
|
parsePaperSubmission,
|
|
37
39
|
type PaperSubmission,
|
|
38
40
|
} from "./domain/submission.ts";
|
|
41
|
+
import {
|
|
42
|
+
cycleProblemText,
|
|
43
|
+
cyclesOf,
|
|
44
|
+
parseCycles,
|
|
45
|
+
venueCycleOf,
|
|
46
|
+
type Cycles,
|
|
47
|
+
} from "./domain/cycle.ts";
|
|
39
48
|
|
|
40
49
|
/** What one paper declares. Every absent field is null. */
|
|
41
50
|
export interface PaperSettings {
|
|
@@ -60,6 +69,12 @@ export interface PaperSettings {
|
|
|
60
69
|
readonly talk: PaperTalk | null;
|
|
61
70
|
/** Which submission on the venue's portal is this paper's; null when it says none. Never the root's. */
|
|
62
71
|
readonly submission: PaperSubmission | null;
|
|
72
|
+
/**
|
|
73
|
+
* The paper's attempts at venues and the current one, when the file declares `cycles`; null for a
|
|
74
|
+
* file in the flat form. With cycles, `extends`, `kind` and `submission` above are the current
|
|
75
|
+
* attempt's — derived, never written beside it. Never the root's.
|
|
76
|
+
*/
|
|
77
|
+
readonly cycles: Cycles | null;
|
|
63
78
|
}
|
|
64
79
|
|
|
65
80
|
/** Why a paper's settings cannot be read. */
|
|
@@ -139,28 +154,85 @@ function rulesAndTalk(
|
|
|
139
154
|
: submission;
|
|
140
155
|
}
|
|
141
156
|
|
|
142
|
-
/** The
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
/** The keys the current cycle supplies: writing one beside `cycles` is a second source for it. */
|
|
158
|
+
const DERIVED_FROM_CYCLE = ["extends", "kind", "submission"] as const;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* `cycles`, parsed with its current attempt, or null when the file is in the flat form. A file that
|
|
162
|
+
* has both `cycles` and a derived key is refused: two sources for one fact drift apart.
|
|
163
|
+
*/
|
|
164
|
+
function cyclesField(
|
|
165
|
+
json: Readonly<Record<string, unknown>>,
|
|
166
|
+
): Result<Cycles | null, string> {
|
|
167
|
+
const list = parseCycles(json["cycles"]);
|
|
168
|
+
if (!list.ok) return list;
|
|
169
|
+
if (list.value === null) return ok(null);
|
|
170
|
+
const beside = DERIVED_FROM_CYCLE.filter((k) => k in json);
|
|
171
|
+
if (beside.length > 0)
|
|
172
|
+
return err(
|
|
173
|
+
`"${beside.join('", "')}" beside "cycles": with cycles, ${beside.length === 1 ? "it is" : "they are"} the current attempt's and cannot be set here — move the value into the open cycle`,
|
|
174
|
+
);
|
|
175
|
+
const cycles = cyclesOf(list.value);
|
|
176
|
+
return cycles.ok ? cycles : err(cycleProblemText(cycles.error));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The current attempt's venue, kind and submission, as the flat keys every reader takes. An accepted
|
|
181
|
+
* attempt keeps them: the camera-ready and the talk are judged against the venue that accepted.
|
|
182
|
+
*/
|
|
183
|
+
function derived(
|
|
184
|
+
cycles: Cycles,
|
|
185
|
+
): Pick<PaperSettings, (typeof DERIVED_FROM_CYCLE)[number]> {
|
|
186
|
+
const c = venueCycleOf(cycles.current);
|
|
187
|
+
if (c === null) return { extends: null, kind: null, submission: null };
|
|
188
|
+
return {
|
|
189
|
+
extends: c.venue.kind === "preset" ? c.venue.extends : null,
|
|
190
|
+
kind: c.kind,
|
|
191
|
+
submission: c.submission,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The keys that may not be in a paper's file: unknown ones, and the project's. */
|
|
196
|
+
function refusedKeys(
|
|
197
|
+
json: Readonly<Record<string, unknown>>,
|
|
198
|
+
): Result<Readonly<Record<string, unknown>>, string> {
|
|
147
199
|
const unknown = Object.keys(json).filter((k) => !KNOWN.includes(k));
|
|
148
200
|
if (unknown.length > 0)
|
|
149
201
|
return err(
|
|
150
202
|
`unknown key "${String(unknown[0])}" — known keys: ${KNOWN.join(", ")}`,
|
|
151
203
|
);
|
|
152
204
|
const project = Object.keys(json).filter((k) => ROOT_ONLY_KEYS.includes(k));
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
205
|
+
return project.length > 0
|
|
206
|
+
? err(
|
|
207
|
+
`"${String(project[0])}" is a project setting — set it in the root ${CONFIG_FILE}, not in a paper's`,
|
|
208
|
+
)
|
|
209
|
+
: ok(json);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The parsed JSON of `paperlint.json` → the settings, or one line saying what is wrong. Pure. */
|
|
213
|
+
export function parsePaperSettings(
|
|
214
|
+
json: unknown,
|
|
215
|
+
): Result<PaperSettings, string> {
|
|
216
|
+
if (!isObject(json)) return err("must be a JSON object");
|
|
217
|
+
const keys = refusedKeys(json);
|
|
218
|
+
if (!keys.ok) return keys;
|
|
157
219
|
const fields = stringFields(json);
|
|
158
220
|
if (!fields.ok) return fields;
|
|
159
221
|
const identity = identityField(json);
|
|
160
222
|
if (!identity.ok) return identity;
|
|
161
223
|
const rest = rulesAndTalk(json);
|
|
162
224
|
if (!rest.ok) return rest;
|
|
163
|
-
|
|
225
|
+
const cycles = cyclesField(json);
|
|
226
|
+
if (!cycles.ok) return cycles;
|
|
227
|
+
const flat = {
|
|
228
|
+
...fields.value,
|
|
229
|
+
...rest.value,
|
|
230
|
+
identity: identity.value,
|
|
231
|
+
cycles: cycles.value,
|
|
232
|
+
};
|
|
233
|
+
return ok(
|
|
234
|
+
cycles.value === null ? flat : { ...flat, ...derived(cycles.value) },
|
|
235
|
+
);
|
|
164
236
|
}
|
|
165
237
|
|
|
166
238
|
/** A JSON file through `files`: undefined when absent, the problem when it does not parse. */
|
|
@@ -251,6 +323,7 @@ function merge(
|
|
|
251
323
|
rules: null,
|
|
252
324
|
talk: null,
|
|
253
325
|
submission: null,
|
|
326
|
+
cycles: null,
|
|
254
327
|
};
|
|
255
328
|
const merged: PaperSettings = {
|
|
256
329
|
extends: p.extends ?? r.extends,
|
|
@@ -260,6 +333,7 @@ function merge(
|
|
|
260
333
|
identity: joined(r.identity, p.identity),
|
|
261
334
|
talk: p.talk,
|
|
262
335
|
submission: p.submission,
|
|
336
|
+
cycles: p.cycles,
|
|
263
337
|
};
|
|
264
338
|
return paper === null && Object.values(merged).every((v) => v === null)
|
|
265
339
|
? null
|
package/src/parse-args.test.ts
CHANGED
|
@@ -22,7 +22,6 @@ describe("parseArgs — `--flag=value` is the same as `--flag value`", () => {
|
|
|
22
22
|
["--config=o.json", "config", "o.json"],
|
|
23
23
|
["--venue=agenticdev", "venue", "agenticdev"],
|
|
24
24
|
["--kind=short", "kind", "short"],
|
|
25
|
-
["--format=md", "format", "md"],
|
|
26
25
|
["--paper=p", "paper", "p"],
|
|
27
26
|
] as const)("%s", (flag, key, value) => {
|
|
28
27
|
const a = parseArgs(["new", "x", flag]);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A venue's deadlines as its portal publishes them — the port the scheduled refresh of the shipped
|
|
3
|
+
* presets reads through (`scripts/refresh-deadlines.ts`). Shaped by what the refresh needs (the
|
|
4
|
+
* portal's deadlines, once), not by any portal's page; one adapter per portal kind
|
|
5
|
+
* (`src/adapters/hotcrp/`).
|
|
6
|
+
*/
|
|
7
|
+
import type { Result } from "../domain/result.ts";
|
|
8
|
+
import type { PortalDeadlines } from "../domain/deadline.ts";
|
|
9
|
+
import type { PortalFailure } from "../domain/submission.ts";
|
|
10
|
+
|
|
11
|
+
export interface VenueDeadlines {
|
|
12
|
+
/** The deadlines the portal publishes now. Needs no account: the page is public. */
|
|
13
|
+
read(): Promise<Result<PortalDeadlines, PortalFailure>>;
|
|
14
|
+
}
|
package/src/presets.test.ts
CHANGED
|
@@ -103,6 +103,23 @@ describe("shipped presets: paperlint:msr", () => {
|
|
|
103
103
|
kind: "hotcrp",
|
|
104
104
|
url: "https://msr2027.hotcrp.com",
|
|
105
105
|
});
|
|
106
|
+
// The portal's deadlines, read 2026-10-05 off cassette/deadlines-msr2027.html's page.
|
|
107
|
+
expect(r.value.deadlines).toEqual([
|
|
108
|
+
{
|
|
109
|
+
what: "submission",
|
|
110
|
+
at: "2026-10-20T04:00:00Z",
|
|
111
|
+
source: "portal",
|
|
112
|
+
url: "https://msr2027.hotcrp.com/deadlines",
|
|
113
|
+
read: "2026-10-05",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
what: "resubmission",
|
|
117
|
+
at: "2026-10-23T04:00:00Z",
|
|
118
|
+
source: "portal",
|
|
119
|
+
url: "https://msr2027.hotcrp.com/deadlines",
|
|
120
|
+
read: "2026-10-05",
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
106
123
|
// No rule turned on: the IEEE capitalization rule has not been read and quoted.
|
|
107
124
|
expect(r.value.rules).toEqual({});
|
|
108
125
|
expect(r.value.registerAnchors).toEqual([]);
|
|
@@ -326,6 +343,94 @@ describe("a project's own preset: what its children replace", () => {
|
|
|
326
343
|
});
|
|
327
344
|
});
|
|
328
345
|
|
|
346
|
+
describe("a project's own preset: its deadlines", () => {
|
|
347
|
+
const reading = (what: string, at: string, source = "portal") => ({
|
|
348
|
+
what,
|
|
349
|
+
at,
|
|
350
|
+
source,
|
|
351
|
+
url: "https://example.org/deadlines",
|
|
352
|
+
read: "2026-10-05",
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("a child's deadlines replace its parent's whole; none named keeps the parent's", () => {
|
|
356
|
+
const own = resolve("./mine.jsonc", {
|
|
357
|
+
"/work/papers/p/mine.jsonc": venue({
|
|
358
|
+
extends: "paperlint:msr",
|
|
359
|
+
deadlines: [reading("submission", "2026-10-21T04:00:00Z")],
|
|
360
|
+
}),
|
|
361
|
+
});
|
|
362
|
+
expect(own.ok && own.value.deadlines).toEqual([
|
|
363
|
+
reading("submission", "2026-10-21T04:00:00Z"),
|
|
364
|
+
]);
|
|
365
|
+
const inherited = resolve("./mine.jsonc", {
|
|
366
|
+
"/work/papers/p/mine.jsonc": venue({ extends: "paperlint:msr" }),
|
|
367
|
+
});
|
|
368
|
+
expect(inherited.ok && inherited.value.deadlines.length).toBe(2);
|
|
369
|
+
const none = resolve("./mine.jsonc", {
|
|
370
|
+
"/work/papers/p/mine.jsonc": venue({ extends: "paperlint:aisec" }),
|
|
371
|
+
});
|
|
372
|
+
expect(none.ok && none.value.deadlines).toEqual([]);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("the portal's and the call's reading of one kind may stand side by side", () => {
|
|
376
|
+
const r = resolve("./mine.jsonc", {
|
|
377
|
+
"/work/papers/p/mine.jsonc": venue({
|
|
378
|
+
extends: "paperlint:aisec",
|
|
379
|
+
deadlines: [
|
|
380
|
+
reading("submission", "2026-10-20T04:00:00Z"),
|
|
381
|
+
reading("submission", "2026-10-21T11:59:59Z", "call"),
|
|
382
|
+
],
|
|
383
|
+
}),
|
|
384
|
+
});
|
|
385
|
+
expect(r.ok && r.value.deadlines.map((d) => d.source)).toEqual([
|
|
386
|
+
"portal",
|
|
387
|
+
"call",
|
|
388
|
+
]);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe("a project's own preset: deadlines it refuses", () => {
|
|
393
|
+
const reading = (what: string, at: string, source = "portal") => ({
|
|
394
|
+
what,
|
|
395
|
+
at,
|
|
396
|
+
source,
|
|
397
|
+
url: "https://example.org/deadlines",
|
|
398
|
+
read: "2026-10-05",
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it.each([
|
|
402
|
+
[
|
|
403
|
+
"two readings of one kind from one source",
|
|
404
|
+
[
|
|
405
|
+
reading("submission", "2026-10-20T04:00:00Z"),
|
|
406
|
+
reading("submission", "2026-10-21T04:00:00Z"),
|
|
407
|
+
],
|
|
408
|
+
/"deadlines" holds two "portal" readings of "submission" — keep one, the latest/,
|
|
409
|
+
],
|
|
410
|
+
[
|
|
411
|
+
"an instant that is not UTC as a portal publishes it",
|
|
412
|
+
[reading("submission", "2026-10-20 AoE")],
|
|
413
|
+
/deadlines/,
|
|
414
|
+
],
|
|
415
|
+
[
|
|
416
|
+
"a reading without the day it was read",
|
|
417
|
+
[{ ...reading("submission", "2026-10-20T04:00:00Z"), read: undefined }],
|
|
418
|
+
/deadlines/,
|
|
419
|
+
],
|
|
420
|
+
])("refuses %s, naming the file", (_what, deadlines, re) => {
|
|
421
|
+
const r = resolve("./mine.jsonc", {
|
|
422
|
+
"/work/papers/p/mine.jsonc": venue({
|
|
423
|
+
extends: "paperlint:aisec",
|
|
424
|
+
deadlines,
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
expect(r.ok).toBe(false);
|
|
428
|
+
if (r.ok) return;
|
|
429
|
+
expect(presetProblemText(r.error)).toMatch(/mine\.jsonc/);
|
|
430
|
+
expect(presetProblemText(r.error)).toMatch(re);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
329
434
|
describe("what does not resolve, and says why", () => {
|
|
330
435
|
it.each([
|
|
331
436
|
[
|
package/src/presets.ts
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
* tex union — a child never removes a package its parent needs
|
|
26
26
|
* format per key, the child wins; `kinds` by kind name, a child's kind replaces that kind
|
|
27
27
|
* rules per rule id, the child wins; `ruleOrigins` keeps which file set each
|
|
28
|
-
* template the child wins, with its `template_forbids`; so does `name`, and `required_sections`, `register`, `talk
|
|
29
|
-
* `portal` (each whole)
|
|
28
|
+
* template the child wins, with its `template_forbids`; so does `name`, and `required_sections`, `register`, `talk`,
|
|
29
|
+
* `portal` and `deadlines` (each whole)
|
|
30
30
|
* aliases union, with every `name` — what the venue is called along the chain
|
|
31
31
|
*
|
|
32
32
|
* ── THE ID AND THE LABEL ──────────────────────────────────────────────────────────
|
|
@@ -63,6 +63,7 @@ import { messageOf } from "./domain/text.ts";
|
|
|
63
63
|
import type { RegisterAnchor } from "./domain/register.ts";
|
|
64
64
|
import type { VenueTalk } from "./domain/talk.ts";
|
|
65
65
|
import type { VenuePortal } from "./domain/submission.ts";
|
|
66
|
+
import type { Reading } from "./domain/deadline.ts";
|
|
66
67
|
|
|
67
68
|
/** The prefix of a shipped preset's spec. */
|
|
68
69
|
export const SHIPPED_PREFIX = "paperlint:";
|
|
@@ -123,6 +124,11 @@ export interface Preset {
|
|
|
123
124
|
readonly talk: VenueTalk | null;
|
|
124
125
|
/** Where the venue takes submissions (`paperlint submission`); null when no file of the chain says. */
|
|
125
126
|
readonly portal: VenuePortal | null;
|
|
127
|
+
/**
|
|
128
|
+
* The edition's deadlines as they were read (the portal's, refreshed by paperlint's scheduled
|
|
129
|
+
* workflow); empty when no file of the chain names any. A paper's current cycle derives them.
|
|
130
|
+
*/
|
|
131
|
+
readonly deadlines: readonly Reading[];
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
/** Why a spec does not resolve. */
|
|
@@ -293,8 +299,24 @@ const NOTHING_MERGED: Merging = {
|
|
|
293
299
|
registerAnchors: [],
|
|
294
300
|
talk: null,
|
|
295
301
|
portal: null,
|
|
302
|
+
deadlines: [],
|
|
296
303
|
};
|
|
297
304
|
|
|
305
|
+
/** The blocks a child replaces whole: the child's when it names one, else its parents'. Pure. */
|
|
306
|
+
const wholeBlocks = (
|
|
307
|
+
acc: Merging,
|
|
308
|
+
p: PresetFile,
|
|
309
|
+
): Pick<
|
|
310
|
+
Merging,
|
|
311
|
+
"requiredSections" | "registerAnchors" | "talk" | "portal" | "deadlines"
|
|
312
|
+
> => ({
|
|
313
|
+
requiredSections: p.requiredSections ?? acc.requiredSections,
|
|
314
|
+
registerAnchors: p.registerAnchors ?? acc.registerAnchors,
|
|
315
|
+
talk: p.talk ?? acc.talk,
|
|
316
|
+
portal: p.portal ?? acc.portal,
|
|
317
|
+
deadlines: p.deadlines ?? acc.deadlines,
|
|
318
|
+
});
|
|
319
|
+
|
|
298
320
|
/** One file of the chain, `file`, merged over what its parents gave. Pure. */
|
|
299
321
|
const mergeOne = (
|
|
300
322
|
acc: Merging,
|
|
@@ -308,7 +330,6 @@ const mergeOne = (
|
|
|
308
330
|
aliases: [...new Set([...acc.aliases, ...namesOf(p.identity), ...p.aliases])],
|
|
309
331
|
blind: p.blind ?? acc.blind,
|
|
310
332
|
mentions: p.mentions ?? acc.mentions,
|
|
311
|
-
requiredSections: p.requiredSections ?? acc.requiredSections,
|
|
312
333
|
tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
|
|
313
334
|
format: mergeFormat(acc.format, p.format),
|
|
314
335
|
rules: { ...acc.rules, ...p.rules },
|
|
@@ -316,9 +337,7 @@ const mergeOne = (
|
|
|
316
337
|
...acc.ruleOrigins,
|
|
317
338
|
...Object.fromEntries(Object.keys(p.rules).map((id) => [id, file])),
|
|
318
339
|
},
|
|
319
|
-
|
|
320
|
-
talk: p.talk ?? acc.talk,
|
|
321
|
-
portal: p.portal ?? acc.portal,
|
|
340
|
+
...wholeBlocks(acc, p),
|
|
322
341
|
});
|
|
323
342
|
|
|
324
343
|
/** The chain, root first, merged into one preset. Pure. */
|
|
@@ -457,15 +476,29 @@ export function paperPreset(paperDir: string, deps: PresetDeps): PaperPreset {
|
|
|
457
476
|
: { kind: "preset-problem", settings, problem: r.error };
|
|
458
477
|
}
|
|
459
478
|
|
|
479
|
+
/** A paper's settings or preset that cannot be used. */
|
|
480
|
+
export type UnusablePreset = Extract<
|
|
481
|
+
PaperPreset,
|
|
482
|
+
{ readonly kind: "settings-problem" | "preset-problem" }
|
|
483
|
+
>;
|
|
484
|
+
|
|
485
|
+
/** The one-line reason a paper's settings or preset cannot be used. */
|
|
486
|
+
export const unusablePresetLine = (
|
|
487
|
+
paperDir: string,
|
|
488
|
+
p: UnusablePreset,
|
|
489
|
+
): string =>
|
|
490
|
+
p.kind === "preset-problem"
|
|
491
|
+
? `${join(paperDir, CONFIG_FILE)}: ${presetProblemText(p.problem)}`
|
|
492
|
+
: settingsProblemLine(paperDir, p.problem);
|
|
493
|
+
|
|
460
494
|
/** The one-line reason a paper's settings or preset cannot be used, or null when they can. */
|
|
461
495
|
export function paperPresetProblem(
|
|
462
496
|
paperDir: string,
|
|
463
497
|
p: PaperPreset,
|
|
464
498
|
): string | null {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return settingsProblemLine(paperDir, p.problem);
|
|
499
|
+
return p.kind === "preset-problem" || p.kind === "settings-problem"
|
|
500
|
+
? unusablePresetLine(paperDir, p)
|
|
501
|
+
: null;
|
|
469
502
|
}
|
|
470
503
|
|
|
471
504
|
/** The one line naming a paper's `paperlint.json` that does not parse, and why. */
|
package/src/rule-cases.test.ts
CHANGED
|
@@ -316,6 +316,88 @@ const onPdf = (tree: Record<string, string>, line = 1) => ({
|
|
|
316
316
|
line,
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
+
/** One open cycle on the ACM family: what a flat `{ "extends": "paperlint:acm-sigconf" }` says, as a record. */
|
|
320
|
+
const OPEN_CYCLE = JSON.stringify({
|
|
321
|
+
cycles: [
|
|
322
|
+
{
|
|
323
|
+
id: "open-2027",
|
|
324
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
325
|
+
opened: "2026-09-09",
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
/**
|
|
330
|
+
* One open cycle whose registration deadline is after its submission deadline: a date typed wrong.
|
|
331
|
+
* The record's STRUCTURAL problems — two open cycles, a bare date, an unknown key — never reach a
|
|
332
|
+
* rule: `paperRuleBlocks` refuses the settings file before lint runs (measured 2026-10-05: one line
|
|
333
|
+
* naming the file and both ids, exit 2), as it refuses any unreadable `paperlint.json`. The order of
|
|
334
|
+
* the deadlines is what the record parses with and the rule still judges.
|
|
335
|
+
*/
|
|
336
|
+
const DISORDERED = JSON.stringify({
|
|
337
|
+
cycles: [
|
|
338
|
+
{
|
|
339
|
+
id: "open-2027",
|
|
340
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
341
|
+
opened: "2026-09-09",
|
|
342
|
+
// The call's own abstract day after its paper day: a date typed wrong. (The portal's and the
|
|
343
|
+
// call's readings disagreeing is not a finding: the earlier binds.)
|
|
344
|
+
deadlines: [
|
|
345
|
+
{
|
|
346
|
+
what: "registration",
|
|
347
|
+
observed: [
|
|
348
|
+
{
|
|
349
|
+
at: "2026-10-25 AoE",
|
|
350
|
+
source: "call",
|
|
351
|
+
url: "https://conf.example/cfp",
|
|
352
|
+
read: "2026-09-09",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
what: "submission",
|
|
358
|
+
observed: [
|
|
359
|
+
{
|
|
360
|
+
at: "2026-10-20T04:00:00Z",
|
|
361
|
+
source: "portal",
|
|
362
|
+
url: "https://conf.example/deadlines",
|
|
363
|
+
read: "2026-09-09",
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
at: "2026-10-23 AoE",
|
|
367
|
+
source: "call",
|
|
368
|
+
url: "https://conf.example/cfp",
|
|
369
|
+
read: "2026-09-09",
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
});
|
|
377
|
+
/** One closed cycle whose evidence is `reviews/decision.md`. */
|
|
378
|
+
const CLOSED_CYCLE = JSON.stringify({
|
|
379
|
+
cycles: [
|
|
380
|
+
{
|
|
381
|
+
id: "first-2026",
|
|
382
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
383
|
+
opened: "2026-07-01",
|
|
384
|
+
outcome: {
|
|
385
|
+
kind: "rejected",
|
|
386
|
+
date: "2026-09-08",
|
|
387
|
+
evidence: "reviews/decision.md",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
});
|
|
392
|
+
/** A BlindConf cycle still declared porting — on a paper whose class line is already BlindConf's. */
|
|
393
|
+
const BLIND_PORTING = {
|
|
394
|
+
id: "blindconf-2027",
|
|
395
|
+
venue: { kind: "preset", extends: "./blindconf.jsonc" },
|
|
396
|
+
opened: "2026-09-09",
|
|
397
|
+
phase: "porting",
|
|
398
|
+
// What `blind()` declares flat; with cycles it moves into the record's siblings, not the cycle.
|
|
399
|
+
};
|
|
400
|
+
|
|
319
401
|
const CASES: Readonly<Record<string, RuleCases>> = {
|
|
320
402
|
"paper/stages": {
|
|
321
403
|
reports: {
|
|
@@ -326,6 +408,68 @@ const CASES: Readonly<Record<string, RuleCases>> = {
|
|
|
326
408
|
},
|
|
327
409
|
silent: paper(tex("x")),
|
|
328
410
|
},
|
|
411
|
+
// ── the cycle rules: the paper's attempts in paperlint.json, judged on PIPELINE-STATUS.md ──────
|
|
412
|
+
"cycle/record": {
|
|
413
|
+
reports: {
|
|
414
|
+
tree: { ...paper(tex("x")), [`${P}/paperlint.json`]: DISORDERED },
|
|
415
|
+
file: STATUS_FILE,
|
|
416
|
+
severity: 2,
|
|
417
|
+
line: 1,
|
|
418
|
+
},
|
|
419
|
+
silent: paper(tex("x")),
|
|
420
|
+
},
|
|
421
|
+
"cycle/evidence": {
|
|
422
|
+
reports: {
|
|
423
|
+
tree: { ...paper(tex("x")), [`${P}/paperlint.json`]: CLOSED_CYCLE },
|
|
424
|
+
file: STATUS_FILE,
|
|
425
|
+
severity: 2,
|
|
426
|
+
line: 1,
|
|
427
|
+
},
|
|
428
|
+
silent: {
|
|
429
|
+
...paper(tex("x")),
|
|
430
|
+
[`${P}/paperlint.json`]: CLOSED_CYCLE,
|
|
431
|
+
[`${P}/reviews/decision.md`]: "# decision\n",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
"cycle/stage": {
|
|
435
|
+
reports: {
|
|
436
|
+
tree: {
|
|
437
|
+
...paper(tex("x")),
|
|
438
|
+
[STATUS_FILE]: SHIPPED_STATUS,
|
|
439
|
+
[`${P}/paperlint.json`]: OPEN_CYCLE,
|
|
440
|
+
[`${P}/versions/a.pdf`]: "x",
|
|
441
|
+
},
|
|
442
|
+
file: STATUS_FILE,
|
|
443
|
+
severity: 2,
|
|
444
|
+
line: 1,
|
|
445
|
+
},
|
|
446
|
+
silent: {
|
|
447
|
+
...paper(tex("x")),
|
|
448
|
+
[STATUS_FILE]: SHIPPED_STATUS.replace(
|
|
449
|
+
"date: 2026-07-22",
|
|
450
|
+
"cycle: open-2027\n date: 2026-07-22",
|
|
451
|
+
),
|
|
452
|
+
[`${P}/paperlint.json`]: OPEN_CYCLE,
|
|
453
|
+
[`${P}/versions/a.pdf`]: "x",
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
"cycle/port-done": {
|
|
457
|
+
reports: {
|
|
458
|
+
tree: {
|
|
459
|
+
...blind("Text."),
|
|
460
|
+
[`${P}/paperlint.json`]: JSON.stringify({ cycles: [BLIND_PORTING] }),
|
|
461
|
+
},
|
|
462
|
+
file: TEX_FILE,
|
|
463
|
+
severity: 2,
|
|
464
|
+
line: 1,
|
|
465
|
+
},
|
|
466
|
+
silent: {
|
|
467
|
+
...blind("Text."),
|
|
468
|
+
[`${P}/paperlint.json`]: JSON.stringify({
|
|
469
|
+
cycles: [{ ...BLIND_PORTING, phase: "prepared" }],
|
|
470
|
+
}),
|
|
471
|
+
},
|
|
472
|
+
},
|
|
329
473
|
"paper/source": {
|
|
330
474
|
reports: {
|
|
331
475
|
tree: { ...paper(tex("x")), [STATUS_FILE]: SHIPPED_STATUS },
|
package/src/structure.harness.ts
CHANGED
|
@@ -47,7 +47,6 @@ const paper = (name: string, files: readonly string[]) => {
|
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
49
|
paper("complete", ["PIPELINE-STATUS.md", "paper.tex", "refs.bib"]);
|
|
50
|
-
paper("complete-md", ["PIPELINE-STATUS.md", "paper.md"]);
|
|
51
50
|
// the marker is present (paper.tex), the scorecard is not — the directory is linted by ZERO
|
|
52
51
|
// rules and reports clean
|
|
53
52
|
paper("no-scorecard", ["paper.tex", "refs.bib"]);
|
|
@@ -65,10 +64,6 @@ try {
|
|
|
65
64
|
"a complete paper directory — NOT A SINGLE finding",
|
|
66
65
|
at("complete").length === 0,
|
|
67
66
|
);
|
|
68
|
-
check(
|
|
69
|
-
"and `paper.md` counts on equal footing with `paper.tex` — the corpus holds both forms",
|
|
70
|
-
at("complete-md").length === 0,
|
|
71
|
-
);
|
|
72
67
|
check(
|
|
73
68
|
"a missing scorecard — a finding",
|
|
74
69
|
at("no-scorecard").length === 1 &&
|
|
@@ -86,10 +81,11 @@ try {
|
|
|
86
81
|
/no-scorecard/.test(at("no-scorecard")[0]?.message ?? ""),
|
|
87
82
|
);
|
|
88
83
|
check(
|
|
89
|
-
"a directory with no source — a finding
|
|
84
|
+
"a directory with no source — a finding naming `paper.tex`",
|
|
90
85
|
at("no-source").length === 1 &&
|
|
91
|
-
/`paper\.tex
|
|
92
|
-
|
|
86
|
+
/`paper\.tex` — a paper directory with no source/.test(
|
|
87
|
+
at("no-source")[0]?.message ?? "",
|
|
88
|
+
),
|
|
93
89
|
);
|
|
94
90
|
|
|
95
91
|
// 🔴 THE PAIRED HALF: detection is GENEROUS. Without this the check would scream about every
|
|
@@ -124,9 +120,7 @@ try {
|
|
|
124
120
|
{
|
|
125
121
|
cwd: root,
|
|
126
122
|
},
|
|
127
|
-
).some(
|
|
128
|
-
(x) => x.file.endsWith("complete-md") && /refs\.bib/.test(x.message),
|
|
129
|
-
),
|
|
123
|
+
).some((x) => x.file.endsWith("no-source") && /refs\.bib/.test(x.message)),
|
|
130
124
|
);
|
|
131
125
|
check(
|
|
132
126
|
"a nonexistent root does not crash it — that's the empty-set guard talking",
|
package/src/structure.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { readdirSync, existsSync } from "node:fs";
|
|
|
26
26
|
import { join, relative, basename, isAbsolute, sep } from "node:path";
|
|
27
27
|
import type { StructureConfig, StructureFinding } from "./types.ts";
|
|
28
28
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
29
|
+
import { SOURCE_FILE } from "./new-paper.ts";
|
|
29
30
|
|
|
30
31
|
/** The requirements after the consumer's config is laid over the defaults. */
|
|
31
32
|
type Rules = Required<StructureConfig>;
|
|
@@ -38,9 +39,9 @@ type Rules = Required<StructureConfig>;
|
|
|
38
39
|
* byte-compared by the `paper/stages` rule anyway.
|
|
39
40
|
*/
|
|
40
41
|
export const STRUCTURE_DEFAULTS = {
|
|
41
|
-
markers: ["PIPELINE-STATUS.md",
|
|
42
|
+
markers: ["PIPELINE-STATUS.md", SOURCE_FILE, CONFIG_FILE],
|
|
42
43
|
require: ["PIPELINE-STATUS.md"],
|
|
43
|
-
requireOneOf: [[
|
|
44
|
+
requireOneOf: [[SOURCE_FILE]],
|
|
44
45
|
ignore: [],
|
|
45
46
|
};
|
|
46
47
|
|
|
@@ -118,7 +119,7 @@ export function checkStructure(
|
|
|
118
119
|
* The message NAMES THE CONSEQUENCE rather than restating the condition. "missing
|
|
119
120
|
* PIPELINE-STATUS.md" without the second half reads as nitpicking about formatting; with it you
|
|
120
121
|
* can see which checks the directory silently loses — the rules that read the scorecard; the rules
|
|
121
|
-
* over `paper.
|
|
122
|
+
* over `paper.tex` still run.
|
|
122
123
|
*/
|
|
123
124
|
function whyMissingMatters(file: string, dirName: string): string {
|
|
124
125
|
if (file === "PIPELINE-STATUS.md")
|