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.
Files changed (261) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/.github/workflows/deadlines.yml +100 -0
  3. package/README.md +6 -0
  4. package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
  5. package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
  6. package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
  7. package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
  8. package/dist/adapters/hotcrp/deadlines.io.js +26 -0
  9. package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
  10. package/dist/adapters/hotcrp/deadlines.js +134 -0
  11. package/dist/adapters/hotcrp/deadlines.js.map +1 -0
  12. package/dist/adapters/hotcrp/index.d.ts +2 -0
  13. package/dist/adapters/hotcrp/index.d.ts.map +1 -1
  14. package/dist/adapters/hotcrp/index.js +2 -0
  15. package/dist/adapters/hotcrp/index.js.map +1 -1
  16. package/dist/build.d.ts.map +1 -1
  17. package/dist/build.js +1 -6
  18. package/dist/build.js.map +1 -1
  19. package/dist/cli.d.ts +3 -4
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +53 -47
  22. package/dist/cli.js.map +1 -1
  23. package/dist/cycle-rules.d.ts +24 -0
  24. package/dist/cycle-rules.d.ts.map +1 -0
  25. package/dist/cycle-rules.js +212 -0
  26. package/dist/cycle-rules.js.map +1 -0
  27. package/dist/domain/cycle.d.ts +148 -0
  28. package/dist/domain/cycle.d.ts.map +1 -0
  29. package/dist/domain/cycle.js +281 -0
  30. package/dist/domain/cycle.js.map +1 -0
  31. package/dist/domain/deadline.d.ts +128 -0
  32. package/dist/domain/deadline.d.ts.map +1 -0
  33. package/dist/domain/deadline.js +281 -0
  34. package/dist/domain/deadline.js.map +1 -0
  35. package/dist/eslint-rules/cycle-stage.d.ts +39 -0
  36. package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
  37. package/dist/eslint-rules/cycle-stage.js +119 -0
  38. package/dist/eslint-rules/cycle-stage.js.map +1 -0
  39. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  40. package/dist/eslint-rules/latex-language.js +10 -13
  41. package/dist/eslint-rules/latex-language.js.map +1 -1
  42. package/dist/eslint-rules/paper-context.js +2 -2
  43. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  44. package/dist/eslint-rules/paper-typography.js +22 -91
  45. package/dist/eslint-rules/paper-typography.js.map +1 -1
  46. package/dist/eslint-rules/papers.d.ts +1 -3
  47. package/dist/eslint-rules/papers.d.ts.map +1 -1
  48. package/dist/eslint-rules/papers.js +2 -4
  49. package/dist/eslint-rules/papers.js.map +1 -1
  50. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  51. package/dist/eslint-rules/tex-build.js +8 -25
  52. package/dist/eslint-rules/tex-build.js.map +1 -1
  53. package/dist/init.d.ts +1 -4
  54. package/dist/init.d.ts.map +1 -1
  55. package/dist/init.js +3 -9
  56. package/dist/init.js.map +1 -1
  57. package/dist/lib/markdown.d.ts +5 -81
  58. package/dist/lib/markdown.d.ts.map +1 -1
  59. package/dist/lib/markdown.js +5 -116
  60. package/dist/lib/markdown.js.map +1 -1
  61. package/dist/lib/paper-config.d.ts.map +1 -1
  62. package/dist/lib/paper-config.js +2 -2
  63. package/dist/lib/paper-config.js.map +1 -1
  64. package/dist/lib/skill-eval-fixture.d.ts +6 -4
  65. package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
  66. package/dist/lib/skill-eval-fixture.js +25 -18
  67. package/dist/lib/skill-eval-fixture.js.map +1 -1
  68. package/dist/new-paper.d.ts +5 -11
  69. package/dist/new-paper.d.ts.map +1 -1
  70. package/dist/new-paper.js +12 -23
  71. package/dist/new-paper.js.map +1 -1
  72. package/dist/paper-files.d.ts +4 -4
  73. package/dist/paper-prose.d.ts +17 -0
  74. package/dist/paper-prose.d.ts.map +1 -0
  75. package/dist/paper-prose.js +46 -0
  76. package/dist/paper-prose.js.map +1 -0
  77. package/dist/paper-settings.d.ts +7 -0
  78. package/dist/paper-settings.d.ts.map +1 -1
  79. package/dist/paper-settings.js +61 -8
  80. package/dist/paper-settings.js.map +1 -1
  81. package/dist/ports/venue-deadlines.d.ts +14 -0
  82. package/dist/ports/venue-deadlines.d.ts.map +1 -0
  83. package/dist/ports/venue-deadlines.js +2 -0
  84. package/dist/ports/venue-deadlines.js.map +1 -0
  85. package/dist/presets.d.ts +12 -0
  86. package/dist/presets.d.ts.map +1 -1
  87. package/dist/presets.js +19 -11
  88. package/dist/presets.js.map +1 -1
  89. package/dist/structure.d.ts.map +1 -1
  90. package/dist/structure.js +4 -3
  91. package/dist/structure.js.map +1 -1
  92. package/dist/submission.d.ts +8 -0
  93. package/dist/submission.d.ts.map +1 -1
  94. package/dist/submission.js +41 -10
  95. package/dist/submission.js.map +1 -1
  96. package/dist/tex-requirements.d.ts +3 -0
  97. package/dist/tex-requirements.d.ts.map +1 -1
  98. package/dist/tex-requirements.js +19 -0
  99. package/dist/tex-requirements.js.map +1 -1
  100. package/dist/tex-venue-rules.d.ts +19 -0
  101. package/dist/tex-venue-rules.d.ts.map +1 -1
  102. package/dist/tex-venue-rules.js +69 -0
  103. package/dist/tex-venue-rules.js.map +1 -1
  104. package/dist/types.d.ts +0 -2
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/venue-rules.d.ts +10 -0
  107. package/dist/venue-rules.d.ts.map +1 -1
  108. package/dist/venue-rules.js +26 -10
  109. package/dist/venue-rules.js.map +1 -1
  110. package/docs/configuration.md +146 -20
  111. package/docs/e2e.md +4 -6
  112. package/docs/prior-art/paper-folder-scaffolding.md +24 -29
  113. package/docs/rules/cycle/evidence.md +87 -0
  114. package/docs/rules/cycle/port-done.md +75 -0
  115. package/docs/rules/cycle/record.md +152 -0
  116. package/docs/rules/cycle/stage.md +85 -0
  117. package/docs/rules/tex/claim-provenance.md +0 -1
  118. package/docs/rules.md +48 -47
  119. package/docs/submission.md +5 -0
  120. package/eslint-rules/cycle-stage.test.mjs +131 -0
  121. package/eslint-rules/cycle-stage.ts +141 -0
  122. package/eslint-rules/latex-language.harness.mjs +3 -3
  123. package/eslint-rules/latex-language.ts +10 -13
  124. package/eslint-rules/paper-context.ts +2 -2
  125. package/eslint-rules/paper-typography.test.mjs +17 -85
  126. package/eslint-rules/paper-typography.ts +22 -98
  127. package/eslint-rules/papers.harness.mjs +1 -1
  128. package/eslint-rules/papers.ts +2 -4
  129. package/eslint-rules/tex-build.harness.mjs +2 -2
  130. package/eslint-rules/tex-build.ts +8 -25
  131. package/eslint.config.mjs +41 -2
  132. package/hooks/hooks.harness.ts +15 -15
  133. package/hooks/paper-edit-guard.hook.mjs +5 -8
  134. package/hooks/paper-edit-guard.test.ts +12 -3
  135. package/hooks/paper-skills-nudge.hook.mjs +2 -3
  136. package/hooks/paper-skills-nudge.test.ts +13 -3
  137. package/hooks/paper-status-gates.hook.mjs +2 -2
  138. package/hooks/paper-status-gates.test.ts +14 -2
  139. package/lib/markdown.test.mjs +5 -60
  140. package/lib/markdown.ts +9 -136
  141. package/lib/paper-config.ts +3 -2
  142. package/lib/skill-checks.test.mjs +4 -1
  143. package/lib/skill-eval-fixture.ts +25 -18
  144. package/package.json +1 -3
  145. package/presets/aidc.jsonc +24 -3
  146. package/presets/aidc.md +6 -3
  147. package/presets/msr.jsonc +25 -1
  148. package/presets/msr.md +5 -1
  149. package/presets/realm.jsonc +1 -1
  150. package/presets/venue-profile.schema.json +39 -0
  151. package/scripts/check.ts +4 -0
  152. package/scripts/harness-api.frozen.json +0 -5
  153. package/scripts/refresh-deadlines.test.ts +431 -0
  154. package/scripts/refresh-deadlines.ts +387 -0
  155. package/skills/cold-read-diff/SKILL.md +3 -3
  156. package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
  157. package/skills/grade-paper-writing/SKILL.md +14 -22
  158. package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
  159. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
  160. package/skills/grade-paper-writing/prose-lint.mjs +108 -278
  161. package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
  162. package/skills/paper-adversarial-review/SKILL.md +2 -2
  163. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  164. package/skills/paper-pipeline/SKILL.md +9 -12
  165. package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
  166. package/skills/paper-pipeline/description-language.eval.mjs +25 -15
  167. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
  168. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
  169. package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
  170. package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
  171. package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
  172. package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
  173. package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
  174. package/skills/paper-pipeline/scripts/README.md +0 -4
  175. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
  176. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
  177. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
  178. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
  179. package/skills/paper-status/SKILL.md +2 -2
  180. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  181. package/skills/render-paper/SKILL.md +2 -2
  182. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  183. package/skills/render-paper/extract-pdf-facts.mjs +2 -2
  184. package/skills/tighten-paper/SKILL.md +29 -42
  185. package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
  186. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
  187. package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
  188. package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
  189. package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
  190. package/src/adapters/hotcrp/cassette/README.md +22 -0
  191. package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
  192. package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
  193. package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
  194. package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
  195. package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
  196. package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
  197. package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
  198. package/src/adapters/hotcrp/deadlines.io.ts +27 -0
  199. package/src/adapters/hotcrp/deadlines.test.ts +170 -0
  200. package/src/adapters/hotcrp/deadlines.ts +161 -0
  201. package/src/adapters/hotcrp/index.ts +2 -0
  202. package/src/build.harness.mjs +2 -2
  203. package/src/build.test.ts +1 -1
  204. package/src/build.ts +1 -6
  205. package/src/cli.harness.mjs +26 -46
  206. package/src/cli.test.ts +41 -40
  207. package/src/cli.ts +57 -58
  208. package/src/cycle-rules.test.ts +278 -0
  209. package/src/cycle-rules.ts +273 -0
  210. package/src/domain/cycle.test.ts +456 -0
  211. package/src/domain/cycle.ts +432 -0
  212. package/src/domain/deadline.test.ts +386 -0
  213. package/src/domain/deadline.ts +472 -0
  214. package/src/folder-venue-rule.test.ts +1 -1
  215. package/src/init.test.ts +6 -7
  216. package/src/init.ts +3 -22
  217. package/src/lint-fix.test.ts +10 -4
  218. package/src/new-paper.harness.mjs +9 -28
  219. package/src/new-paper.test.ts +7 -6
  220. package/src/new-paper.ts +11 -30
  221. package/src/paper-files.test.ts +5 -4
  222. package/src/paper-files.ts +4 -4
  223. package/src/paper-prose.test.ts +49 -0
  224. package/src/paper-prose.ts +67 -0
  225. package/src/paper-settings.test.ts +167 -0
  226. package/src/paper-settings.ts +85 -11
  227. package/src/parse-args.test.ts +0 -1
  228. package/src/ports/venue-deadlines.ts +14 -0
  229. package/src/presets.test.ts +105 -0
  230. package/src/presets.ts +43 -10
  231. package/src/rule-cases.test.ts +144 -0
  232. package/src/structure.harness.ts +5 -11
  233. package/src/structure.ts +4 -3
  234. package/src/submission.test.ts +105 -1
  235. package/src/submission.ts +73 -12
  236. package/src/tex-requirements.ts +31 -0
  237. package/src/tex-venue-rules.test.ts +76 -0
  238. package/src/tex-venue-rules.ts +85 -0
  239. package/src/types.ts +0 -2
  240. package/src/venue-rules.test.ts +82 -0
  241. package/src/venue-rules.ts +44 -12
  242. package/fixtures/real-markdown-paper/baseline.json +0 -22
  243. package/fixtures/real-markdown-paper/baseline.ts +0 -70
  244. package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
  245. package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
  246. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
  247. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
  248. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
  249. package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
  250. package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
  251. package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
  252. package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
  253. package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
  254. package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
  255. package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
  256. package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
  257. package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
  258. package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
  259. package/skills/tighten-paper/structure.mjs +0 -364
  260. package/skills/tighten-paper/structure.test.ts +0 -117
  261. package/templates/paper/paper.md +0 -4
@@ -0,0 +1,212 @@
1
+ /**
2
+ * THE CYCLE RULES over a paper's settings file — the `cycle` plugin, judged once per paper on its
3
+ * `PIPELINE-STATUS.md` (every paper folder has one, whatever its source format; `paper/folder-venue-leftover`
4
+ * reports there for the same reason):
5
+ *
6
+ * cycle/record error `cycles` in paperlint.json parses and names one current attempt: no two
7
+ * open cycles, the open one last, ids unique, every deadline an instant
8
+ * with its zone; each source's deadlines in order; no portal reading the
9
+ * venue's preset already carries
10
+ * cycle/evidence error every closed cycle's `outcome.evidence`, and every deadline override's
11
+ * `evidence`, is a file in the paper folder
12
+ *
13
+ * ── WHY ERROR, NOT WARN ───────────────────────────────────────────────────────────
14
+ * A warning is read once and never again. Each of these names a state that cannot be worked in:
15
+ * two open cycles is dual submission or a decision nobody recorded; a deadline without its zone is
16
+ * the mistake the record exists to prevent; a decision or an override whose evidence is not in the
17
+ * folder is a claim with nothing behind it. Nothing here depends on a clock or the network — the
18
+ * record is judged against itself, the installed venue preset and the disk, like `paper/stages`.
19
+ *
20
+ * ── THE PRESET'S READINGS ARE DERIVED, NOT COPIED ──────────────────────────────────
21
+ * A cycle whose venue is a preset gets that preset's `deadlines` (the portal's readings, refreshed in
22
+ * the package); the paper adds the call's readings and its overrides. A paper's own portal reading
23
+ * of a deadline the preset carries is a second copy of one fact, and it drifts the day the preset is
24
+ * updated — so it is reported.
25
+ *
26
+ * ── WHO SPEAKS WHEN THERE IS NOTHING TO JUDGE ──────────────────────────────────────
27
+ * No `paperlint.json`, one that is not JSON, or one without `cycles` (the flat form): silent — the
28
+ * flat form is still valid, and `pdf/profile` says when the whole file is broken. A `cycles` that
29
+ * does not parse, or names no current attempt (two open cycles), never reaches this rule through
30
+ * `paperlint lint`: `paperRuleBlocks` (src/cli.ts) refuses the settings file before ESLint runs, one
31
+ * line naming the file and the problem, exit 2 — the same strictness as an unknown key (measured
32
+ * 2026-10-05). The `broken` message below is what the rule says when a host runs it without that
33
+ * refusal; under the CLI only `order` is reachable.
34
+ */
35
+ import { basename, dirname, join } from "node:path";
36
+ import { callerPath } from "./caller-path.js";
37
+ import { cycleProblemText, cyclesOf, parseCycles, } from "./domain/cycle.js";
38
+ import { deadlineOrderProblems } from "./domain/deadline.js";
39
+ import { resolvePreset } from "./presets.js";
40
+ import { fieldOf } from "./domain/record.js";
41
+ import { err, ok } from "./domain/result.js";
42
+ import { rulePageUrl } from "./venue-rules.js";
43
+ import { CONFIG_FILE } from "#lib/paper-config";
44
+ const STATUS_FILE = "PIPELINE-STATUS.md";
45
+ /** The `cycles` value of the paper's settings file; undefined when there is no file, no JSON, or no key. */
46
+ function cyclesValue(dir, deps) {
47
+ const bytes = deps.files.readBytes(callerPath(join(dir, CONFIG_FILE)));
48
+ if (bytes === null)
49
+ return undefined;
50
+ try {
51
+ return fieldOf(JSON.parse(new TextDecoder().decode(bytes)), "cycles");
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ /** The paper's cycles, parsed and consistent, or the one line that says what is wrong. */
58
+ export function readCycles(dir, deps) {
59
+ const list = parseCycles(cyclesValue(dir, deps));
60
+ if (!list.ok)
61
+ return list;
62
+ if (list.value === null)
63
+ return ok(null);
64
+ const cycles = cyclesOf(list.value);
65
+ return cycles.ok ? cycles : err(cycleProblemText(cycles.error));
66
+ }
67
+ /** A paper's portal readings of deadlines its venue's preset already carries. */
68
+ function presetCopies(c, preset) {
69
+ return c.deadlines.flatMap((d) => {
70
+ const theirs = preset.readings.find((r) => r.what === d.what && r.source === "portal");
71
+ const ours = d.observed.some((o) => o.source === "portal");
72
+ return theirs !== undefined && ours
73
+ ? [
74
+ {
75
+ messageId: "presetCopy",
76
+ data: {
77
+ id: c.id,
78
+ what: d.what,
79
+ preset: preset.spec,
80
+ at: theirs.at,
81
+ read: theirs.read,
82
+ },
83
+ },
84
+ ]
85
+ : [];
86
+ });
87
+ }
88
+ /** `cycle/record`: the record's own problems. Pure over the read and the presets' readings. */
89
+ export function judgeRecord(read, presetReadings) {
90
+ if (!read.ok)
91
+ return [{ messageId: "broken", data: { why: read.error } }];
92
+ if (read.value === null)
93
+ return [];
94
+ return read.value.list.flatMap((c) => {
95
+ const preset = presetReadings(c);
96
+ return [
97
+ ...deadlineOrderProblems(c.deadlines, preset?.readings ?? []).map((why) => ({ messageId: "order", data: { id: c.id, why } })),
98
+ ...(preset === null ? [] : presetCopies(c, preset)),
99
+ ];
100
+ });
101
+ }
102
+ /** The overrides whose evidence is not on disk. */
103
+ const overrideEvidence = (c, isFile) => c.deadlines.flatMap((d) => d.override !== null && !isFile(d.override.evidence)
104
+ ? [
105
+ {
106
+ messageId: "overrideMissing",
107
+ data: {
108
+ id: c.id,
109
+ what: d.what,
110
+ at: d.override.at,
111
+ evidence: d.override.evidence,
112
+ },
113
+ },
114
+ ]
115
+ : []);
116
+ /** The closed cycles, and the overrides, whose evidence is not on disk. */
117
+ export function judgeEvidence(cycles, isFile) {
118
+ return cycles.flatMap((c) => {
119
+ const o = c.outcome;
120
+ return [
121
+ ...(o.kind !== "open" && !isFile(o.evidence)
122
+ ? [
123
+ {
124
+ messageId: "missing",
125
+ data: {
126
+ id: c.id,
127
+ outcome: o.kind,
128
+ date: o.date,
129
+ evidence: o.evidence,
130
+ },
131
+ },
132
+ ]
133
+ : []),
134
+ ...overrideEvidence(c, isFile),
135
+ ];
136
+ });
137
+ }
138
+ const META = {
139
+ record: {
140
+ type: "suggestion",
141
+ docs: {
142
+ description: "the paper's `cycles` record parses and names one current attempt, each source's deadlines in order, no copy of the preset's portal readings",
143
+ url: rulePageUrl("cycle/record"),
144
+ },
145
+ schema: [],
146
+ messages: {
147
+ broken: `${CONFIG_FILE}: {{why}}`,
148
+ order: `${CONFIG_FILE}, cycle «{{id}}»: {{why}} — a date typed wrong, or an AoE day miscounted (write a call's date as "YYYY-MM-DD AoE" and paperlint converts it)`,
149
+ presetCopy: `${CONFIG_FILE}, cycle «{{id}}»: the "{{what}}" deadline records a portal reading, and the preset {{preset}} carries the portal's reading of it ({{at}}, read {{read}}) — a second copy drifts when paperlint updates the preset: delete this one (the call's reading and an override stay)`,
150
+ },
151
+ },
152
+ evidence: {
153
+ type: "suggestion",
154
+ docs: {
155
+ description: "every closed cycle's decision, and every deadline override, points at a file in the paper folder — the mail, the reviews, the note",
156
+ url: rulePageUrl("cycle/evidence"),
157
+ },
158
+ schema: [],
159
+ messages: {
160
+ missing: "cycle «{{id}}» is {{outcome}} ({{date}}) with evidence `{{evidence}}`, which is not in the paper folder — save the decision mail, the reviews or the note that records the decision there, or correct the path",
161
+ overrideMissing: 'cycle «{{id}}»: the override of the "{{what}}" deadline ({{at}}) rests on `{{evidence}}`, which is not in the paper folder — save the mail or the page that grants it there, or correct the path',
162
+ },
163
+ },
164
+ };
165
+ /** Each cycle's venue preset's readings, resolved from the paper's settings file like `extends`. */
166
+ const presetReadingsOf = (dir, deps) => (c) => {
167
+ if (c.venue.kind !== "preset")
168
+ return null;
169
+ const spec = c.venue.extends;
170
+ const p = resolvePreset(spec, join(dir, CONFIG_FILE), deps);
171
+ return p.ok ? { spec, readings: p.value.deadlines } : null;
172
+ };
173
+ /** The findings of one rule for the paper folder `dir`. */
174
+ function findingsFor(name, dir, deps) {
175
+ const read = readCycles(dir, deps);
176
+ switch (name) {
177
+ case "record":
178
+ return judgeRecord(read, presetReadingsOf(dir, deps));
179
+ case "evidence":
180
+ return read.ok && read.value !== null
181
+ ? judgeEvidence(read.value.list, (rel) => deps.files.isFile(callerPath(join(dir, rel))))
182
+ : [];
183
+ }
184
+ }
185
+ function rule(name, deps) {
186
+ return {
187
+ meta: META[name],
188
+ create(context) {
189
+ if (basename(context.filename) !== STATUS_FILE)
190
+ return {};
191
+ return {
192
+ root() {
193
+ const at = context.sourceCode.getLocFromIndex(0);
194
+ findingsFor(name, dirname(context.filename), deps).forEach((f) => {
195
+ context.report({ loc: { start: at, end: at }, ...f });
196
+ });
197
+ },
198
+ };
199
+ },
200
+ };
201
+ }
202
+ /** The `cycle` plugin's rules over the settings file, for every paper's `PIPELINE-STATUS.md`. */
203
+ export function cycleRules(deps) {
204
+ return { record: rule("record", deps), evidence: rule("evidence", deps) };
205
+ }
206
+ /** The level each `cycle/*` rule on PIPELINE-STATUS.md is on at in paperlint's own config — `stage` too (eslint-rules/cycle-stage.ts). */
207
+ export const CYCLE_RULE_LEVELS = {
208
+ "cycle/record": "error",
209
+ "cycle/evidence": "error",
210
+ "cycle/stage": "error",
211
+ };
212
+ //# sourceMappingURL=cycle-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cycle-rules.js","sourceRoot":"","sources":["../src/cycle-rules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * THE CYCLE — one attempt of a paper at one venue: where it goes, as what kind, when the attempt was
3
+ * decided, the venue's deadlines as they were read, which submission on the portal is this paper's,
4
+ * and how the attempt ended. A paper's `paperlint.json` holds them as a LIST, `cycles`, oldest first:
5
+ *
6
+ * {
7
+ * "id": "beta-2027",
8
+ * "venue": { "kind": "preset", "extends": "paperlint:beta" }, // or { "kind": "named", "name", "url" }
9
+ * "kind": "technical",
10
+ * "opened": "2026-09-09",
11
+ * "phase": "porting", // default "prepared"
12
+ * "deadlines": [{ "what": "submission", "observed": [{ "at": "2026-10-23 AoE", "source": "call", "url": "…", "read": "2026-09-09" }] }],
13
+ * "submission": { "id": 7 },
14
+ * "outcome": { "kind": "open" }
15
+ * }
16
+ *
17
+ * ── WHY A LIST OF ATTEMPTS, NOT ONE VENUE ───────────────────────────────────────────────
18
+ * A paper goes to a venue, is rejected, and goes to the next one; the settings that every reader
19
+ * takes from the file — `extends`, `kind`, `submission` — are the CURRENT attempt's, and change
20
+ * together when the attempt changes. Held flat, they lag the decision (the preset is switched "when
21
+ * the port lands", weeks later) and the decision itself has no field, so it is written as a comment.
22
+ * Here it is a record: the new attempt is a new entry, the old one gets its `outcome`.
23
+ *
24
+ * ── THE PHASE, and why it is DECLARED ────────────────────────────────────────────────────
25
+ * The day a new venue is chosen, the source is still in the old venue's template. The venue's format
26
+ * checks are then not wrong, but not applicable: a page count under the wrong template says nothing
27
+ * about the new limit. `"phase": "porting"` says the port is open work; the format rules are silent
28
+ * under it, and `cycle/port-done` fires when the class line already matches the venue's template
29
+ * and the phase still says porting. The declaration cannot lie for long in either direction, and it
30
+ * is a record of work, not a softer severity. Absent, the phase is `prepared`.
31
+ *
32
+ * ── THE DEADLINES ARE READINGS ───────────────────────────────────────────────────────────
33
+ * Each deadline records what the portal and the call said, where and when, and an optional human
34
+ * override; the earlier reading binds unless overridden (`deadline.ts`). For a venue with a preset,
35
+ * the portal's readings come from the preset — updated with the package — and the paper adds the
36
+ * call's readings and its overrides on top.
37
+ *
38
+ * Parsed by hand: the domain imports no schema library (see `src/CLAUDE.md`).
39
+ */
40
+ import { type Result } from "./result.ts";
41
+ import { type Deadline, type IsoDate } from "./deadline.ts";
42
+ /** The venue of an attempt: a preset paperlint resolves, or a venue named by hand (no preset yet). */
43
+ export type CycleVenue = {
44
+ readonly kind: "preset";
45
+ readonly extends: string;
46
+ } | {
47
+ readonly kind: "named";
48
+ readonly name: string;
49
+ readonly url: string;
50
+ };
51
+ /** How an attempt ended; `open` while it has not. */
52
+ export type Outcome = {
53
+ readonly kind: "open";
54
+ } | {
55
+ readonly kind: "accepted";
56
+ readonly date: IsoDate;
57
+ readonly evidence: string;
58
+ } | {
59
+ readonly kind: "rejected";
60
+ readonly date: IsoDate;
61
+ readonly evidence: string;
62
+ /** A desk rejection (format, scope) — different work follows than after reviews. */
63
+ readonly desk: boolean;
64
+ } | {
65
+ readonly kind: "withdrawn";
66
+ readonly date: IsoDate;
67
+ readonly evidence: string;
68
+ };
69
+ export declare const OUTCOME_KINDS: readonly Outcome["kind"][];
70
+ export type Phase = "porting" | "prepared";
71
+ export interface Cycle {
72
+ /** What a stage refers to (`stages[].cycle`): unique in the list. */
73
+ readonly id: string;
74
+ readonly venue: CycleVenue;
75
+ /** A key of the preset's `format.kinds`; null when the venue has one kind or none is chosen. */
76
+ readonly kind: string | null;
77
+ /** The day this attempt was decided: the rows of the scorecard are judged against it. */
78
+ readonly opened: IsoDate;
79
+ readonly phase: Phase;
80
+ readonly deadlines: readonly Deadline[];
81
+ /** Which submission on the venue's portal is this paper's; null before one exists. */
82
+ readonly submission: {
83
+ readonly id: number;
84
+ } | null;
85
+ readonly outcome: Outcome;
86
+ }
87
+ /**
88
+ * Which attempt the paper is on. An ACCEPTED last cycle is not "parked": the paper lives on at that
89
+ * venue — camera-ready, talk, proceedings — so its venue and kind stay the paper's.
90
+ */
91
+ export type Current = {
92
+ readonly kind: "cycle";
93
+ readonly cycle: Cycle;
94
+ } | {
95
+ readonly kind: "accepted";
96
+ readonly cycle: Cycle;
97
+ }
98
+ /** The last cycle was rejected or withdrawn and none is open: between venues. */
99
+ | {
100
+ readonly kind: "parked";
101
+ readonly last: Cycle;
102
+ }
103
+ /** `cycles: []`. */
104
+ | {
105
+ readonly kind: "none";
106
+ };
107
+ /** Why a list of cycles names no current attempt. */
108
+ export type CycleProblem = {
109
+ readonly kind: "two-open";
110
+ readonly ids: readonly string[];
111
+ } | {
112
+ readonly kind: "open-not-last";
113
+ readonly id: string;
114
+ } | {
115
+ readonly kind: "duplicate-id";
116
+ readonly id: string;
117
+ };
118
+ /** A `cycles` value → the list, null when absent, or one line naming the entry and the key. Pure. */
119
+ export declare function parseCycles(v: unknown): Result<readonly Cycle[] | null, string>;
120
+ /**
121
+ * The attempt the paper is on. At most one cycle may be open, and it is the last: two open cycles is
122
+ * dual submission or a cycle nobody closed, and both are refused by name. Pure.
123
+ */
124
+ export declare function currentCycle(cycles: readonly Cycle[]): Result<Current, CycleProblem>;
125
+ /** One line for a problem, naming what to change. Pure. */
126
+ export declare function cycleProblemText(p: CycleProblem): string;
127
+ /** A paper's `cycles`, parsed, with the attempt they name: minted only when the list is consistent. */
128
+ export interface Cycles {
129
+ readonly list: readonly Cycle[];
130
+ readonly current: Current;
131
+ }
132
+ /** The list with its current attempt, or why the list names none. Pure. */
133
+ export declare const cyclesOf: (list: readonly Cycle[]) => Result<Cycles, CycleProblem>;
134
+ /**
135
+ * The cycle whose venue and kind are the paper's: the open attempt, or the accepted one; null when
136
+ * the paper is between venues or has none.
137
+ */
138
+ export declare const venueCycleOf: (c: Current) => Cycle | null;
139
+ /** The current attempt's own deadline entries; none between venues or before the first attempt. Pure. */
140
+ export declare const currentDeadlines: (c: Current) => readonly Deadline[];
141
+ /**
142
+ * Whether the paper is judged as a blind submission: the venue reviews blind, and this attempt has
143
+ * not been accepted — an accepted paper's camera-ready carries its authors. Pure.
144
+ */
145
+ export declare const effectiveBlind: (venueBlind: boolean, current: Current) => boolean;
146
+ /** Whether the current attempt declares its port to the venue's template as open work. */
147
+ export declare const isPorting: (current: Current) => boolean;
148
+ //# sourceMappingURL=cycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cycle.d.ts","sourceRoot":"","sources":["../../src/domain/cycle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,281 @@
1
+ /**
2
+ * THE CYCLE — one attempt of a paper at one venue: where it goes, as what kind, when the attempt was
3
+ * decided, the venue's deadlines as they were read, which submission on the portal is this paper's,
4
+ * and how the attempt ended. A paper's `paperlint.json` holds them as a LIST, `cycles`, oldest first:
5
+ *
6
+ * {
7
+ * "id": "beta-2027",
8
+ * "venue": { "kind": "preset", "extends": "paperlint:beta" }, // or { "kind": "named", "name", "url" }
9
+ * "kind": "technical",
10
+ * "opened": "2026-09-09",
11
+ * "phase": "porting", // default "prepared"
12
+ * "deadlines": [{ "what": "submission", "observed": [{ "at": "2026-10-23 AoE", "source": "call", "url": "…", "read": "2026-09-09" }] }],
13
+ * "submission": { "id": 7 },
14
+ * "outcome": { "kind": "open" }
15
+ * }
16
+ *
17
+ * ── WHY A LIST OF ATTEMPTS, NOT ONE VENUE ───────────────────────────────────────────────
18
+ * A paper goes to a venue, is rejected, and goes to the next one; the settings that every reader
19
+ * takes from the file — `extends`, `kind`, `submission` — are the CURRENT attempt's, and change
20
+ * together when the attempt changes. Held flat, they lag the decision (the preset is switched "when
21
+ * the port lands", weeks later) and the decision itself has no field, so it is written as a comment.
22
+ * Here it is a record: the new attempt is a new entry, the old one gets its `outcome`.
23
+ *
24
+ * ── THE PHASE, and why it is DECLARED ────────────────────────────────────────────────────
25
+ * The day a new venue is chosen, the source is still in the old venue's template. The venue's format
26
+ * checks are then not wrong, but not applicable: a page count under the wrong template says nothing
27
+ * about the new limit. `"phase": "porting"` says the port is open work; the format rules are silent
28
+ * under it, and `cycle/port-done` fires when the class line already matches the venue's template
29
+ * and the phase still says porting. The declaration cannot lie for long in either direction, and it
30
+ * is a record of work, not a softer severity. Absent, the phase is `prepared`.
31
+ *
32
+ * ── THE DEADLINES ARE READINGS ───────────────────────────────────────────────────────────
33
+ * Each deadline records what the portal and the call said, where and when, and an optional human
34
+ * override; the earlier reading binds unless overridden (`deadline.ts`). For a venue with a preset,
35
+ * the portal's readings come from the preset — updated with the package — and the paper adds the
36
+ * call's readings and its overrides on top.
37
+ *
38
+ * Parsed by hand: the domain imports no schema library (see `src/CLAUDE.md`).
39
+ */
40
+ import { err, ok } from "./result.js";
41
+ import { fieldOf, isRecord } from "./record.js";
42
+ import { isIsoDate, parseDeadlines, unknownKey, } from "./deadline.js";
43
+ export const OUTCOME_KINDS = [
44
+ "open",
45
+ "accepted",
46
+ "rejected",
47
+ "withdrawn",
48
+ ];
49
+ // ── parsing ────────────────────────────────────────────────────────────────────────────
50
+ const KNOWN_KEYS = [
51
+ "id",
52
+ "venue",
53
+ "kind",
54
+ "opened",
55
+ "phase",
56
+ "deadlines",
57
+ "submission",
58
+ "outcome",
59
+ ];
60
+ const where = (i) => `cycles[${String(i)}]`;
61
+ const nonEmpty = (v) => typeof v === "string" && v !== "";
62
+ const VENUE_KEYS = {
63
+ preset: ["kind", "extends"],
64
+ named: ["kind", "name", "url"],
65
+ };
66
+ /** The fields of a venue of `kind`, when they are there. */
67
+ function venueFields(kind, v) {
68
+ const { extends: ext, name, url } = v;
69
+ switch (kind) {
70
+ case "preset":
71
+ return nonEmpty(ext) ? { kind, extends: ext } : null;
72
+ case "named":
73
+ return nonEmpty(name) && nonEmpty(url) ? { kind, name, url } : null;
74
+ }
75
+ }
76
+ /** The venue's own keys refused when unknown, then its shape; null when the kind is neither. */
77
+ function venueShape(v, at) {
78
+ const kind = v["kind"];
79
+ if (kind !== "preset" && kind !== "named")
80
+ return null;
81
+ const unknown = unknownKey(v, VENUE_KEYS[kind], `${at}.venue`, `a ${kind} venue's`);
82
+ if (unknown !== null)
83
+ return err(unknown);
84
+ const venue = venueFields(kind, v);
85
+ return venue === null ? null : ok(venue);
86
+ }
87
+ function parseVenue(v, at) {
88
+ const shaped = isRecord(v) ? venueShape(v, at) : null;
89
+ return (shaped ??
90
+ err(`${at}: "venue" must be { "kind": "preset", "extends": … } or { "kind": "named", "name": …, "url": … }`));
91
+ }
92
+ const isOutcomeKind = (v) => OUTCOME_KINDS.some((k) => k === v);
93
+ /** A decision's shape by kind: `rejected` also says whether it was a desk rejection. */
94
+ const decisionOf = (kind, decided, desk, at) => {
95
+ switch (kind) {
96
+ case "accepted":
97
+ return ok({ kind, ...decided });
98
+ case "withdrawn":
99
+ return ok({ kind, ...decided });
100
+ case "rejected":
101
+ return typeof desk === "boolean" || desk === undefined
102
+ ? ok({ kind, ...decided, desk: desk === true })
103
+ : err(`${at}: "outcome.desk" must be true or false`);
104
+ }
105
+ };
106
+ const OUTCOME_KEYS = {
107
+ open: ["kind"],
108
+ accepted: ["kind", "date", "evidence"],
109
+ rejected: ["kind", "date", "evidence", "desk"],
110
+ withdrawn: ["kind", "date", "evidence"],
111
+ };
112
+ function parseOutcome(v, at) {
113
+ const kind = fieldOf(v, "kind");
114
+ if (!isRecord(v) || !isOutcomeKind(kind))
115
+ return err(`${at}: "outcome" kind must be one of ${OUTCOME_KINDS.join(", ")}, got ${JSON.stringify(kind)}`);
116
+ const unknown = unknownKey(v, OUTCOME_KEYS[kind], `${at}.outcome`, `${kind === "open" || kind === "accepted" ? "an" : "a"} ${kind} outcome's`);
117
+ if (unknown !== null)
118
+ return err(unknown);
119
+ if (kind === "open")
120
+ return ok({ kind });
121
+ const date = v["date"];
122
+ if (!isIsoDate(date))
123
+ return err(`${at}: "outcome" of kind ${kind} needs "date" (YYYY-MM-DD)`);
124
+ const evidence = v["evidence"];
125
+ if (typeof evidence !== "string" || evidence === "")
126
+ return err(`${at}: "outcome" of kind ${kind} needs "evidence": a file in the paper folder (the decision mail, the reviews, the note that records the withdrawal)`);
127
+ return decisionOf(kind, { date, evidence }, v["desk"], at);
128
+ }
129
+ function parseSubmission(v, at) {
130
+ if (v === undefined || v === null)
131
+ return ok(null);
132
+ const unknown = isRecord(v)
133
+ ? unknownKey(v, ["id"], `${at}.submission`, "a submission's")
134
+ : null;
135
+ if (unknown !== null)
136
+ return err(unknown);
137
+ const id = fieldOf(v, "id");
138
+ return typeof id === "number" && Number.isInteger(id) && id > 0
139
+ ? ok({ id })
140
+ : err(`${at}: "submission" must be { "id": <the submission number on the venue's portal> }`);
141
+ }
142
+ const isPhase = (v) => v === "porting" || v === "prepared";
143
+ /** `kind`: a string, or null when absent — the venue has one kind, or none is chosen yet. */
144
+ const kindOf = (v) => v === undefined || v === null
145
+ ? ok(null)
146
+ : typeof v === "string"
147
+ ? ok(v)
148
+ : err('"kind" must be a string or null');
149
+ function parseHead(v, at) {
150
+ const id = v["id"];
151
+ if (typeof id !== "string" || id === "")
152
+ return err(`${at}: "id" must be a non-empty string (what stages refer to)`);
153
+ const venue = parseVenue(v["venue"], at);
154
+ if (!venue.ok)
155
+ return venue;
156
+ const kind = kindOf(v["kind"]);
157
+ if (!kind.ok)
158
+ return err(`${at}: ${kind.error}`);
159
+ const opened = v["opened"];
160
+ if (!isIsoDate(opened))
161
+ return err(`${at}: "opened" must be a date YYYY-MM-DD — the day this attempt was decided`);
162
+ const phase = v["phase"] ?? "prepared";
163
+ if (!isPhase(phase))
164
+ return err(`${at}: "phase" must be "porting" or "prepared"`);
165
+ return ok({
166
+ id,
167
+ venue: venue.value,
168
+ kind: kind.value,
169
+ opened,
170
+ phase,
171
+ });
172
+ }
173
+ function parseTail(v, at) {
174
+ const deadlines = parseDeadlines(v["deadlines"], at);
175
+ if (!deadlines.ok)
176
+ return deadlines;
177
+ const submission = parseSubmission(v["submission"], at);
178
+ if (!submission.ok)
179
+ return submission;
180
+ const outcome = parseOutcome(v["outcome"] ?? { kind: "open" }, at);
181
+ if (!outcome.ok)
182
+ return outcome;
183
+ return ok({
184
+ deadlines: deadlines.value,
185
+ submission: submission.value,
186
+ outcome: outcome.value,
187
+ });
188
+ }
189
+ function parseCycle(v, i) {
190
+ const at = where(i);
191
+ if (!isRecord(v))
192
+ return err(`${at} must be an object`);
193
+ const unknown = Object.keys(v).find((k) => !KNOWN_KEYS.includes(k));
194
+ if (unknown !== undefined)
195
+ return err(`${at}: unknown key "${unknown}" — known keys: ${KNOWN_KEYS.join(", ")}`);
196
+ const head = parseHead(v, at);
197
+ if (!head.ok)
198
+ return head;
199
+ const tail = parseTail(v, at);
200
+ return tail.ok ? ok({ ...head.value, ...tail.value }) : tail;
201
+ }
202
+ /** A `cycles` value → the list, null when absent, or one line naming the entry and the key. Pure. */
203
+ export function parseCycles(v) {
204
+ if (v === undefined)
205
+ return ok(null);
206
+ if (!Array.isArray(v))
207
+ return err(`"cycles" must be a LIST of attempts, oldest first — a paper can go to several venues`);
208
+ return v.reduce((acc, c, i) => {
209
+ if (!acc.ok)
210
+ return acc;
211
+ const p = parseCycle(c, i);
212
+ return p.ok ? ok([...acc.value, p.value]) : p;
213
+ }, ok([]));
214
+ }
215
+ // ── deriving ───────────────────────────────────────────────────────────────────────────
216
+ const isOpen = (c) => c.outcome.kind === "open";
217
+ /**
218
+ * The attempt the paper is on. At most one cycle may be open, and it is the last: two open cycles is
219
+ * dual submission or a cycle nobody closed, and both are refused by name. Pure.
220
+ */
221
+ export function currentCycle(cycles) {
222
+ const dup = cycles.find((c, i) => cycles.findIndex((d) => d.id === c.id) !== i);
223
+ if (dup !== undefined)
224
+ return err({ kind: "duplicate-id", id: dup.id });
225
+ const open = cycles.filter(isOpen);
226
+ if (open.length > 1)
227
+ return err({ kind: "two-open", ids: open.map((c) => c.id) });
228
+ const last = cycles.at(-1);
229
+ if (last === undefined)
230
+ return ok({ kind: "none" });
231
+ const [only] = open;
232
+ if (only !== undefined && only !== last)
233
+ return err({ kind: "open-not-last", id: only.id });
234
+ if (isOpen(last))
235
+ return ok({ kind: "cycle", cycle: last });
236
+ return ok(last.outcome.kind === "accepted"
237
+ ? { kind: "accepted", cycle: last }
238
+ : { kind: "parked", last });
239
+ }
240
+ /** One line for a problem, naming what to change. Pure. */
241
+ export function cycleProblemText(p) {
242
+ switch (p.kind) {
243
+ case "two-open":
244
+ return `cycles ${p.ids.map((id) => `«${id}»`).join(" and ")} are both open — a paper is on one attempt at a time; two open attempts at two venues is dual submission. Close the one that ended: "outcome": { "kind": "rejected" | "withdrawn" | "accepted", "date", "evidence" }`;
245
+ case "open-not-last":
246
+ return `cycle «${p.id}» is open but is not the last entry — cycles are listed oldest first, and the open one is the current attempt`;
247
+ case "duplicate-id":
248
+ return `two cycles carry the id «${p.id}» — a stage's \`cycle:\` could name either; give each attempt its own id`;
249
+ }
250
+ }
251
+ /** The list with its current attempt, or why the list names none. Pure. */
252
+ export const cyclesOf = (list) => {
253
+ const current = currentCycle(list);
254
+ return current.ok ? ok({ list, current: current.value }) : current;
255
+ };
256
+ /**
257
+ * The cycle whose venue and kind are the paper's: the open attempt, or the accepted one; null when
258
+ * the paper is between venues or has none.
259
+ */
260
+ export const venueCycleOf = (c) => {
261
+ switch (c.kind) {
262
+ case "cycle":
263
+ return c.cycle;
264
+ case "accepted":
265
+ return c.cycle;
266
+ case "parked":
267
+ return null;
268
+ case "none":
269
+ return null;
270
+ }
271
+ };
272
+ /** The current attempt's own deadline entries; none between venues or before the first attempt. Pure. */
273
+ export const currentDeadlines = (c) => venueCycleOf(c)?.deadlines ?? [];
274
+ /**
275
+ * Whether the paper is judged as a blind submission: the venue reviews blind, and this attempt has
276
+ * not been accepted — an accepted paper's camera-ready carries its authors. Pure.
277
+ */
278
+ export const effectiveBlind = (venueBlind, current) => venueBlind && current.kind !== "accepted";
279
+ /** Whether the current attempt declares its port to the venue's template as open work. */
280
+ export const isPorting = (current) => current.kind === "cycle" && current.cycle.phase === "porting";
281
+ //# sourceMappingURL=cycle.js.map