paperlint 4.0.0 → 4.1.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 +16 -23
- package/CLAUDE.md +31 -11
- package/CONTRIBUTING.md +23 -7
- package/README.md +11 -8
- package/dist/adapters/banal/invocation.d.ts +1 -1
- package/dist/adapters/latex/document-class.d.ts +22 -0
- package/dist/adapters/latex/document-class.d.ts.map +1 -0
- package/dist/adapters/latex/document-class.js +102 -0
- package/dist/adapters/latex/document-class.js.map +1 -0
- package/dist/adapters/latex/hidden.d.ts +16 -0
- package/dist/adapters/latex/hidden.d.ts.map +1 -0
- package/dist/adapters/latex/hidden.js +49 -0
- package/dist/adapters/latex/hidden.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +29 -0
- package/dist/adapters/latex/index.d.ts.map +1 -0
- package/dist/adapters/latex/index.js +18 -0
- package/dist/adapters/latex/index.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts +54 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -0
- package/dist/adapters/latex/nodes.js +93 -0
- package/dist/adapters/latex/nodes.js.map +1 -0
- package/dist/adapters/latex/outline.d.ts +10 -0
- package/dist/adapters/latex/outline.d.ts.map +1 -0
- package/dist/adapters/latex/outline.js +39 -0
- package/dist/adapters/latex/outline.js.map +1 -0
- package/dist/adapters/latex/parse.d.ts +42 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -0
- package/dist/adapters/latex/parse.js +94 -0
- package/dist/adapters/latex/parse.js.map +1 -0
- package/dist/adapters/latex/rendered.d.ts +10 -0
- package/dist/adapters/latex/rendered.d.ts.map +1 -0
- package/dist/adapters/latex/rendered.js +39 -0
- package/dist/adapters/latex/rendered.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +12 -12
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/build.js +4 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +29 -5
- package/dist/cli.js.map +1 -1
- package/dist/domain/tex-document.d.ts +84 -0
- package/dist/domain/tex-document.d.ts.map +1 -0
- package/dist/domain/tex-document.js +43 -0
- package/dist/domain/tex-document.js.map +1 -0
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +2 -2
- package/dist/facts-file.js.map +1 -1
- package/dist/new-paper.d.ts +5 -0
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +3 -0
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts +5 -0
- package/dist/package-dirs.d.ts.map +1 -0
- package/dist/package-dirs.js +14 -0
- package/dist/package-dirs.js.map +1 -0
- package/dist/ports/latex.d.ts +24 -0
- package/dist/ports/latex.d.ts.map +1 -0
- package/dist/ports/latex.js +2 -0
- package/dist/ports/latex.js.map +1 -0
- package/dist/presets.d.ts +15 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -6
- package/dist/presets.js.map +1 -1
- package/dist/tex-requirements.d.ts +11 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +14 -8
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +83 -0
- package/dist/tex-venue-rules.d.ts.map +1 -0
- package/dist/tex-venue-rules.js +254 -0
- package/dist/tex-venue-rules.js.map +1 -0
- package/dist/venue-rules.d.ts +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/docs/configuration.md +3 -2
- package/docs/e2e.md +69 -40
- package/docs/install.md +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/package-location.md +5 -5
- package/docs/prior-art/repro/README.md +1 -1
- package/docs/prior-art/test-tooling.md +21 -10
- package/docs/rules/tex/required-section.md +73 -0
- package/docs/rules/tex/template.md +62 -0
- package/docs/rules/tex/venue-leftover.md +71 -0
- package/docs/rules.md +70 -48
- package/docs/testing.md +17 -4
- package/docs/texlive-install-decision.md +1 -1
- package/docs/toolchain.md +2 -2
- package/eslint.config.mjs +155 -15
- package/fixtures/build-e2e/acmart/expect.json +26 -0
- package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/aidc/expect.json +14 -0
- package/fixtures/build-e2e/aidc/paper.tex +46 -0
- package/fixtures/build-e2e/aidc/paperlint.json +1 -0
- package/fixtures/build-e2e/broken/expect.json +11 -0
- package/fixtures/build-e2e/cite/expect.json +27 -0
- package/fixtures/build-e2e/empty/expect.json +12 -0
- package/fixtures/build-e2e/fallback/expect.json +53 -0
- package/fixtures/build-e2e/guards/expect.json +6 -0
- package/fixtures/build-e2e/no-source/expect.json +10 -0
- package/fixtures/build-e2e/unbalanced/expect.json +60 -0
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.ts +1 -1
- package/fixtures/render-paper/build-clean.sh +3 -5
- package/package.json +13 -2
- package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
- package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
- package/presets/aidc.jsonc +73 -0
- package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
- package/presets/ieee-conference.jsonc +66 -0
- package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
- package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
- package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
- package/scripts/check.harness.ts +64 -10
- package/scripts/check.test.ts +60 -13
- package/scripts/check.ts +83 -30
- package/scripts/test.test.ts +1 -1
- package/scripts/test.ts +1 -1
- package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
- package/skills/paper-pipeline/SKILL.md +2 -2
- package/skills/paper-pipeline/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
- package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/check-render.sh +16 -63
- package/skills/render-paper/render-paper.harness.mjs +17 -59
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.ts +1 -1
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +1 -1
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/latex/document-class.ts +147 -0
- package/src/adapters/latex/hidden.ts +70 -0
- package/src/adapters/latex/index.test.ts +361 -0
- package/src/adapters/latex/index.ts +55 -0
- package/src/adapters/latex/nodes.ts +141 -0
- package/src/adapters/latex/outline.ts +64 -0
- package/src/adapters/latex/parse.ts +136 -0
- package/src/adapters/latex/rendered.ts +59 -0
- package/src/adapters/pdfjs/fill.test.ts +1 -1
- package/src/adapters/pdfjs/fill.ts +18 -10
- package/src/build.harness.mjs +5 -7
- package/src/build.ts +4 -4
- package/src/cli.harness.mjs +1 -1
- package/src/cli.test.ts +2 -8
- package/src/cli.ts +33 -8
- package/src/domain/tex-document.test.ts +80 -0
- package/src/domain/tex-document.ts +110 -0
- package/src/engine.harness.mjs +1 -1
- package/src/facts-file.test.ts +4 -7
- package/src/facts-file.ts +2 -2
- package/src/latex-loop.harness.ts +1 -1
- package/src/new-paper.test.ts +28 -3
- package/src/new-paper.ts +8 -0
- package/src/package-dirs.ts +17 -0
- package/src/paper-settings-commands.test.ts +6 -1
- package/src/ports/latex.ts +29 -0
- package/src/presets.test.ts +65 -4
- package/src/presets.ts +38 -15
- package/src/rule-cases.test.ts +474 -0
- package/src/rule-docs.test.ts +159 -0
- package/src/tex-requirements.harness.ts +13 -23
- package/src/tex-requirements.ts +35 -8
- package/src/tex-venue-rules.test.ts +659 -0
- package/src/tex-venue-rules.ts +429 -0
- package/src/toolchain.harness.mjs +1 -1
- package/src/venue-rules.test.ts +96 -4
- package/src/venue-rules.ts +1 -1
- package/templates/paper/paper.tex +2 -1
- /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
- /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// AIDC @ IEEE ACSAC 2026 (Workshop on Agentic AI in Offensive and Defensive Cyber Operations) — a
|
|
2
|
+
// VENUE preset over the IEEE conference family. Everything the IEEEtran template decides (page
|
|
3
|
+
// size, columns, fonts, body size, TeX packages) comes from ieee-conference.jsonc; this file adds
|
|
4
|
+
// what AIDC's call for papers sets.
|
|
5
|
+
//
|
|
6
|
+
// Verbatim from https://aidcworkshop.github.io/ (fetched 2026-09-27):
|
|
7
|
+
// «All submitted papers must be prepared in US Letter size, not A4, using the double-column IEEE
|
|
8
|
+
// conference format. This requirement will be strictly enforced.»
|
|
9
|
+
// «LaTeX submissions must use the IEEEtran.cls version 1.8b template with the
|
|
10
|
+
// \documentclass[conference,compsoc]{IEEEtran} document class option.»
|
|
11
|
+
// «Regular technical papers: Up to 12 pages excluding references and appendices.»
|
|
12
|
+
// «Short position papers or work-in-progress (WIP) papers: Up to 6 pages, excluding references
|
|
13
|
+
// and appendices.»
|
|
14
|
+
// «Submissions must be properly anonymized for double-blind review.»
|
|
15
|
+
//
|
|
16
|
+
// THE DEADLINE PEOPLE GET WRONG (not a lint fact): the HotCRP portal closes 2026-10-02 11:59:59 UTC
|
|
17
|
+
// (`"sub":1790942399` on https://aidc.submit.acsac.org/deadlines, recorded in paperlint#90), while
|
|
18
|
+
// the workshop page says «October 2, 2026 (Anywhere-on-Earth, AoE)» — a day later. The portal is
|
|
19
|
+
// what refuses the upload.
|
|
20
|
+
//
|
|
21
|
+
// THE REVIEW IS DOUBLE-BLIND: `"blind":true` in the same portal data, and the call above.
|
|
22
|
+
{
|
|
23
|
+
"extends": "paperlint:ieee-conference",
|
|
24
|
+
|
|
25
|
+
// What the venue is called in a paper's text (tex/venue-leftover): the workshop, and the
|
|
26
|
+
// conference it is held at.
|
|
27
|
+
"aliases": ["AIDC", "ACSAC"],
|
|
28
|
+
|
|
29
|
+
"template": "\\documentclass[conference,compsoc]{IEEEtran}",
|
|
30
|
+
|
|
31
|
+
// «Authors must follow the ACSAC AI Usage Policy for all workshop submissions.» (the call above).
|
|
32
|
+
// Verbatim from https://www.acsac.org/2026/submissions/ai/ (fetched 2026-09-27):
|
|
33
|
+
// «If LLMs are used, authors must include a separate, clearly marked section titled "LLM Usage
|
|
34
|
+
// Statement" at the end of the paper. This section does not count towards the page limit.»
|
|
35
|
+
// «Failure to comply with these requirements is grounds for desk rejection without further
|
|
36
|
+
// review.»
|
|
37
|
+
// tex/required-section cannot know whether LLMs were used, so it requires the section always: an
|
|
38
|
+
// author who used none says so in it. A bold paragraph («Use of Generative AI.», what AISec asked
|
|
39
|
+
// for) is not a section with this title and does not count.
|
|
40
|
+
"required_sections": [{ "title": "LLM Usage Statement", "position": "last" }],
|
|
41
|
+
|
|
42
|
+
"format": {
|
|
43
|
+
// \textwidth and \textheight of [conference,compsoc], measured (ieee-conference.jsonc).
|
|
44
|
+
"textblock_w_in": 7.0,
|
|
45
|
+
"textblock_h_in": 9.02,
|
|
46
|
+
|
|
47
|
+
// 🔴 PAGE LIMITS ARE NOT GATED HERE, AND THAT IS A MEASUREMENT. banal splits pages into body
|
|
48
|
+
// and references by what is on them, and a page that holds ANY non-bibliography text counts as
|
|
49
|
+
// body. Measured 2026-09-27 on a [conference,compsoc] paper whose body ends at the foot of
|
|
50
|
+
// page 12:
|
|
51
|
+
// LLM Usage Statement before the references → body 13, references 0
|
|
52
|
+
// an appendix before the references → body 13, references 2
|
|
53
|
+
// both placed after the references → body right, the tail counted as references
|
|
54
|
+
// AIDC counts neither in the limit («excluding references and appendices»; the ACSAC AI policy:
|
|
55
|
+
// the LLM Usage Statement «does not count towards the page limit»), and tex/required-section
|
|
56
|
+
// accepts the statement before or after the bibliography. A gate on banal's body count would
|
|
57
|
+
// therefore fail a correct 12-page paper — the realm.jsonc case again. The numbers below are
|
|
58
|
+
// data for humans, not an input to pdf/limits.
|
|
59
|
+
"page_limits_not_gated": {
|
|
60
|
+
"_": "Body pages, excluding references, appendices and the LLM Usage Statement. banal counts a page holding an appendix or the statement before the bibliography as body, so its body count is not this number.",
|
|
61
|
+
"regular": 12,
|
|
62
|
+
"short": 6,
|
|
63
|
+
"references_and_appendices": "unlimited",
|
|
64
|
+
"llm_usage_statement": "not counted (ACSAC AI policy)",
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// «Short position papers or work-in-progress (WIP) papers» share one limit, so one kind.
|
|
68
|
+
"kinds": {
|
|
69
|
+
"regular": {},
|
|
70
|
+
"short": {},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
{
|
|
26
26
|
"extends": "paperlint:acm-sigconf",
|
|
27
27
|
|
|
28
|
+
// What the venue is called in a paper's text (tex/venue-leftover): the workshop, and the
|
|
29
|
+
// conference it is held at.
|
|
30
|
+
"aliases": ["AISec", "ACM CCS"],
|
|
31
|
+
|
|
28
32
|
"format": {
|
|
29
33
|
// Four tracks (original research · position/open-problem · SoK · benchmark) — the limits are
|
|
30
34
|
// the SAME for all of them, the venue doesn't split them. The keys are kept separate so a
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// IEEE `IEEEtran` conference — the TEMPLATE FAMILY: everything IEEE's conference template decides,
|
|
2
|
+
// whatever the venue. A venue preset extends it ("extends": "paperlint:ieee-conference") and adds
|
|
3
|
+
// what its call for papers sets: the class options it names, the page limit of each kind of paper,
|
|
4
|
+
// the sections it requires.
|
|
5
|
+
//
|
|
6
|
+
// 🔴 THE NUMBERS ARE MEASURED, NOT COPIED. Built on 2026-09-27 from IEEE's own template files
|
|
7
|
+
// (`bare_conf.tex` and `bare_conf_compsoc.tex`, V1.4b 2015/08/26, from CTAN), and from a 13-page
|
|
8
|
+
// paper of generated prose on each, with IEEEtran.cls V1.8b 2015/08/26, pdflatex, TeX Live 2026
|
|
9
|
+
// (`paperlint toolchain`) and TeX Live 2023 (Debian) — the same fonts on both. What banal and
|
|
10
|
+
// pdf.js reported, and what the class sets, the two option sets side by side:
|
|
11
|
+
//
|
|
12
|
+
// [conference] [conference,compsoc]
|
|
13
|
+
// page 8.5 x 11 in 8.5 x 11 in
|
|
14
|
+
// columns 2 2
|
|
15
|
+
// body size (banal) 10.3 10.3 (declared 10pt)
|
|
16
|
+
// fonts (pdf.js) NimbusRomNo9L-Regu / -Medi / -ReguItal / -MediItal, Type 1, embedded
|
|
17
|
+
// \textwidth 516pt = 7.14 in 505.89pt = 7.00 in
|
|
18
|
+
// \textheight 672pt = 9.30 in 652.04pt = 9.02 in
|
|
19
|
+
// section headings I. SMALL CAPS 1. Title Case
|
|
20
|
+
//
|
|
21
|
+
// So `compsoc` changes the text block and the headings, and nothing the `pdf/*` rules judge: page
|
|
22
|
+
// size, columns, body size and fonts are identical. One family is enough; a venue that requires
|
|
23
|
+
// `compsoc` names it in its own `template` (tex/template checks it) and its own text block (see
|
|
24
|
+
// aidc.jsonc). The planted defects fail as they should, measured on the same compsoc paper:
|
|
25
|
+
// `a4paper` added to the options → page 8.264 x 11.694 in: pdf/geometry
|
|
26
|
+
// Computer Modern (`\rmdefault{cmr}`) → fonts CMR10, CMBX12, …: pdf/fonts, no NimbusRomNo9L
|
|
27
|
+
{
|
|
28
|
+
"template": "\\documentclass[conference]{IEEEtran}",
|
|
29
|
+
|
|
30
|
+
// TeX Live packages the IEEEtran template needs ON TOP OF tex-base.jsonc, each with the files that
|
|
31
|
+
// prove it is present (kpsewhich, after every install). Found 2026-09-27 by the loop build →
|
|
32
|
+
// missing file → tlmgr search --file → install, on the two template files above.
|
|
33
|
+
// ieeetran — the class and its bibliography style.
|
|
34
|
+
// cite — `bare_conf_compsoc.tex` loads it (`\usepackage[nocompress]{cite}` under compsoc) and
|
|
35
|
+
// stops with "File `cite.sty' not found" without it.
|
|
36
|
+
// psnfss · times — IEEEtran sets the body in Times (`ot1ptm.fd`, `ptmr7t.tfm`), drawn from the
|
|
37
|
+
// URW clone (`utmr8a.pfb`), which is what the PDF names NimbusRomNo9L.
|
|
38
|
+
"tex": {
|
|
39
|
+
"packages": {
|
|
40
|
+
"ieeetran": ["IEEEtran.cls", "IEEEtran.bst"],
|
|
41
|
+
"cite": ["cite.sty"],
|
|
42
|
+
"psnfss": ["ot1ptm.fd"],
|
|
43
|
+
"times": ["ptmr7t.tfm", "utmr8a.pfb"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
"format": {
|
|
48
|
+
"page_size": "letter",
|
|
49
|
+
"page_w_in": 8.5,
|
|
50
|
+
"page_h_in": 11.0,
|
|
51
|
+
|
|
52
|
+
// \textwidth and \textheight of [conference], as the class sets them.
|
|
53
|
+
"textblock_w_in": 7.14,
|
|
54
|
+
"textblock_h_in": 9.3,
|
|
55
|
+
|
|
56
|
+
"columns": 2,
|
|
57
|
+
|
|
58
|
+
// IEEEtran's conference default is 10pt; banal measures the rendered mode, 10.3.
|
|
59
|
+
"body_pt": 10,
|
|
60
|
+
"body_pt_tol": 0.5,
|
|
61
|
+
|
|
62
|
+
// The Times clone. Headings and the title are set in the same family, so a `fonts_title` would
|
|
63
|
+
// repeat this check and report a Computer Modern build twice.
|
|
64
|
+
"fonts_text": "NimbusRomNo9L",
|
|
65
|
+
},
|
|
66
|
+
}
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
// acmart (letter 8.5x11, size 9, Libertine) shares NOTHING with this — copying the numbers from
|
|
9
9
|
// `agenticdev.jsonc` would give a gate that reddens on a correct paper.
|
|
10
10
|
{
|
|
11
|
-
|
|
11
|
+
// ACL's template is not a class: `acl_latex.tex` opens with `\documentclass[11pt]{article}` and
|
|
12
|
+
// loads `acl.sty` as a package (https://github.com/acl-org/acl-style-files, checked 2026-09-27).
|
|
13
|
+
// So the class `tex/template` compares against is article, at 11pt.
|
|
14
|
+
"template": "\\documentclass[11pt]{article}",
|
|
15
|
+
|
|
16
|
+
// What the venue is called in a paper's text (tex/venue-leftover): the workshop, and the
|
|
17
|
+
// conference it is held at.
|
|
18
|
+
"aliases": ["REALM", "EMNLP"],
|
|
12
19
|
|
|
13
20
|
// TeX Live packages the ACL template needs ON TOP OF tex-base.jsonc (the shape is
|
|
14
21
|
// venue-profile.schema.json). acl.sty itself travels with the paper; these are what it loads.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
% paper-guards.tex — guards that turn a paper defect into a BUILD FAILURE.
|
|
2
2
|
%
|
|
3
3
|
% Included from the paper's preamble: \input{paper-guards}
|
|
4
|
-
% Found via TEXINPUTS, which check-render.sh sets (the `
|
|
4
|
+
% Found via TEXINPUTS, which check-render.sh sets (the package's `presets/tex/` directory).
|
|
5
5
|
%
|
|
6
6
|
% WHY A SEPARATE FILE, NOT A COPY IN EVERY PAPER. The first edition of the guard (08-26) lived
|
|
7
7
|
% inline in `agenticdev-2026/paper.tex`, and a measurement that same evening showed the cost: of
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://github.com/zernie/paperlint/venue-profile.schema.json",
|
|
4
4
|
"title": "Venue preset",
|
|
5
|
-
"description": "One venue preset or template family (
|
|
5
|
+
"description": "One venue preset or template family (presets/<name>.jsonc), and the TeX base set (presets/tex-base.jsonc). A paper's paperlint.json names one with \"extends\"; a preset may extend another. `format` and `rules` are read by paperlint's pdf/* venue rules and its lint config (src/venue-rules.ts, src/presets.ts); `tex` by `paperlint toolchain` and `paperlint build`.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
@@ -20,10 +20,38 @@
|
|
|
20
20
|
"$ref": "#/definitions/tex"
|
|
21
21
|
},
|
|
22
22
|
"template": {
|
|
23
|
-
"description": "The \\documentclass the venue's template uses.",
|
|
23
|
+
"description": "The \\documentclass the venue's template uses: a whole line, `\\documentclass[conference,compsoc]{IEEEtran}`, or a bare class name. `tex/template` requires a paper's class to be this one and to carry every option named here.",
|
|
24
24
|
"type": "string",
|
|
25
25
|
"minLength": 1
|
|
26
26
|
},
|
|
27
|
+
"aliases": {
|
|
28
|
+
"description": "Other names the venue goes by in a paper's text — its acronym, its parent conference (`[\"AISec\", \"ACM CCS\"]`). Read by `tex/venue-leftover`: a paper that extends another venue and says one of these is flagged. Matched as whole words, case-sensitive. Collected along the `extends` chain, with `name`.",
|
|
29
|
+
"type": "array",
|
|
30
|
+
"uniqueItems": true,
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"minLength": 1
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required_sections": {
|
|
37
|
+
"description": "Sections the venue requires, read by `tex/required-section`. Each is a `\\section` or `\\section*` whose title equals `title` once whitespace is collapsed. `position: \"last\"`: after every section of the body (the ones before the appendix and the bibliography); it may stand after the bibliography. A child preset's list replaces its parent's.",
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"required": ["title"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"title": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"minLength": 1
|
|
47
|
+
},
|
|
48
|
+
"position": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"enum": ["last"]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
27
55
|
"format": {
|
|
28
56
|
"description": "What the venue's call for papers sets. Every key is optional; a child preset's key replaces its parent's, and a child's kind replaces that kind.",
|
|
29
57
|
"type": "object",
|
package/scripts/check.harness.ts
CHANGED
|
@@ -60,29 +60,51 @@ const PackageJson = z.looseObject({
|
|
|
60
60
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
61
61
|
const ROOT = dirname(HERE);
|
|
62
62
|
|
|
63
|
-
const { GATES, NOT_COVERED, outcome,
|
|
63
|
+
const { GATES, NOT_COVERED, outcome, skippedIn, commandOf } =
|
|
64
64
|
await import("./check.ts");
|
|
65
65
|
|
|
66
66
|
const check = createChecker();
|
|
67
67
|
|
|
68
68
|
// ── the workflow is the ORACLE, not a copy of it ───────────────────────────────────────────
|
|
69
|
-
// A
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
check("exit 0 is a pass", outcome(0) === "pass");
|
|
69
|
+
// A skip is a THIRD outcome. Folding it into "pass" printed "all gates passed" on a machine where
|
|
70
|
+
// the e2e never ran (Codex review on #45); folding it into "fail" would make the command red for
|
|
71
|
+
// anyone without pnpm or TeX, and it would be ignored. The count is vitest's own, from its report.
|
|
72
|
+
check("exit 0 of a plain gate is a pass", outcome(0) === "pass");
|
|
73
73
|
check(
|
|
74
|
-
|
|
75
|
-
outcome(
|
|
74
|
+
"exit 0 of a vitest gate whose report counts no skip is a pass",
|
|
75
|
+
outcome(0, { skipped: 0 }) === "pass",
|
|
76
76
|
);
|
|
77
|
-
check("the skip code is the one vigiles' runner uses (77)", SKIP_EXIT === 77);
|
|
78
77
|
check(
|
|
79
|
-
"
|
|
80
|
-
outcome(
|
|
78
|
+
"exit 0 of a vitest gate whose report counts skipped tests is a SKIP — neither a pass nor a failure",
|
|
79
|
+
outcome(0, { skipped: 2 }) === "skip",
|
|
80
|
+
);
|
|
81
|
+
check(
|
|
82
|
+
"🔴 exit 0 of a vitest gate with no readable report is a FAILURE — an unread count cannot be a pass",
|
|
83
|
+
outcome(0, { skipped: null }) === "fail",
|
|
84
|
+
);
|
|
85
|
+
check(
|
|
86
|
+
"any nonzero exit is a failure, skips or not",
|
|
87
|
+
outcome(1) === "fail" &&
|
|
88
|
+
outcome(2, { skipped: 0 }) === "fail" &&
|
|
89
|
+
outcome(1, { skipped: 3 }) === "fail",
|
|
81
90
|
);
|
|
82
91
|
check(
|
|
83
92
|
"a process killed by a signal (status null) is a failure, not a skip",
|
|
84
93
|
outcome(null) === "fail",
|
|
85
94
|
);
|
|
95
|
+
check(
|
|
96
|
+
"skippedIn counts skipped and todo tests from vitest's JSON report",
|
|
97
|
+
skippedIn(
|
|
98
|
+
JSON.stringify({ numTotalTests: 9, numPendingTests: 3, numTodoTests: 1 }),
|
|
99
|
+
) === 4 &&
|
|
100
|
+
skippedIn(JSON.stringify({ numPendingTests: 0, numTodoTests: 0 })) === 0,
|
|
101
|
+
);
|
|
102
|
+
check(
|
|
103
|
+
"skippedIn: no report, not JSON, or not vitest's shape is null — never zero",
|
|
104
|
+
skippedIn(null) === null &&
|
|
105
|
+
skippedIn("not json") === null &&
|
|
106
|
+
skippedIn("{}") === null,
|
|
107
|
+
);
|
|
86
108
|
|
|
87
109
|
// Every workflow, not just ci.yml: a harness reading one file would stop seeing a job added to
|
|
88
110
|
// another without a sound. (macOS lived in platform.yml from 2026-09-23 to 2026-09-24; it is now
|
|
@@ -188,6 +210,13 @@ for (const g of GATES) {
|
|
|
188
210
|
),
|
|
189
211
|
);
|
|
190
212
|
}
|
|
213
|
+
for (const g of GATES.filter((x) => x.vitest)) {
|
|
214
|
+
// Guards: the skip count is read from vitest's JSON report, so the script must BE a vitest run.
|
|
215
|
+
check(
|
|
216
|
+
`gate «${g.name}» is marked vitest, and its script is a vitest run (npm run ${String(g.script)})`,
|
|
217
|
+
(scripts[g.script ?? ""] ?? "").startsWith("vitest run"),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
191
220
|
check(
|
|
192
221
|
"`check` itself is wired as a script, or nobody can run any of this",
|
|
193
222
|
typeof scripts.check === "string",
|
|
@@ -260,6 +289,31 @@ for (const g of GATES) {
|
|
|
260
289
|
);
|
|
261
290
|
}
|
|
262
291
|
|
|
292
|
+
// ── the e2e runs are found by vitest's projects, never listed ──────────────────────────────
|
|
293
|
+
// A list of e2e files is a second copy of the directory, and the three copies that existed
|
|
294
|
+
// (package.json, this script, CI) disagreed: two of four, four of four, a step per file (#132).
|
|
295
|
+
const namesE2eFile = (run: string | undefined): string[] =>
|
|
296
|
+
commandsIn(run)
|
|
297
|
+
.flat()
|
|
298
|
+
.filter((t) => /(^|\/)test\/e2e\//.test(t));
|
|
299
|
+
check(
|
|
300
|
+
"no npm script names a file under test/e2e/ — the vitest projects find them",
|
|
301
|
+
Object.values(scripts).flatMap(namesE2eFile).length === 0,
|
|
302
|
+
JSON.stringify(scripts),
|
|
303
|
+
);
|
|
304
|
+
check(
|
|
305
|
+
"no workflow step names a file under test/e2e/",
|
|
306
|
+
Object.values(workflows)
|
|
307
|
+
.flatMap((wf) => Object.values(wf.jobs ?? {}))
|
|
308
|
+
.flatMap((j) => j.steps ?? [])
|
|
309
|
+
.flatMap((st) => namesE2eFile(st.run)).length === 0,
|
|
310
|
+
);
|
|
311
|
+
check(
|
|
312
|
+
"no gate names a file under test/e2e/",
|
|
313
|
+
GATES.flatMap((g) => commandOf(g).filter((t) => t.includes("test/e2e/")))
|
|
314
|
+
.length === 0,
|
|
315
|
+
);
|
|
316
|
+
|
|
263
317
|
// ── and the tail is not optional ───────────────────────────────────────────────────────────
|
|
264
318
|
check(
|
|
265
319
|
"NOT_COVERED is non-empty — if it ever is, either CI shrank or someone silenced the tail",
|
package/scripts/check.test.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* check.ts's runGates — the one command run before a push — on every outcome, with an injected
|
|
3
|
-
* spawn: a pass, a
|
|
3
|
+
* spawn: a pass, a skip (counted in a vitest gate's JSON report) and a failure, and THE TAIL that
|
|
4
|
+
* names what it does not cover.
|
|
4
5
|
*/
|
|
5
6
|
import assert from "node:assert/strict";
|
|
6
7
|
import { spawnSync } from "node:child_process";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { test } from "vitest";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
GATES,
|
|
12
|
+
NOT_COVERED,
|
|
13
|
+
reportArgs,
|
|
14
|
+
runGates,
|
|
15
|
+
type Gate,
|
|
16
|
+
} from "./check.ts";
|
|
10
17
|
|
|
11
18
|
const capture = () => {
|
|
12
19
|
const out: string[] = [];
|
|
@@ -17,10 +24,12 @@ const capture = () => {
|
|
|
17
24
|
return { out, io };
|
|
18
25
|
};
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
const SKIPPED_2 = JSON.stringify({ numPendingTests: 2, numTodoTests: 0 });
|
|
28
|
+
|
|
29
|
+
test("runGates: a pass, a vitest gate that skipped tests, and a failure, then the tail naming what is not covered", () => {
|
|
21
30
|
const gates: Gate[] = [
|
|
22
31
|
{ name: "passes", job: "gates", run: ["true"] },
|
|
23
|
-
{ name: "skips", job: "gates",
|
|
32
|
+
{ name: "skips", job: "gates", script: "test:e2e:x", vitest: true },
|
|
24
33
|
{
|
|
25
34
|
name: "fails",
|
|
26
35
|
job: null,
|
|
@@ -28,15 +37,30 @@ test("runGates: a pass, a declared skip (77) and a failure, then the tail naming
|
|
|
28
37
|
run: ["false"],
|
|
29
38
|
},
|
|
30
39
|
];
|
|
31
|
-
const status: Record<string, number> = { true: 0,
|
|
40
|
+
const status: Record<string, number> = { true: 0, npm: 0, false: 3 };
|
|
41
|
+
const calls: string[][] = [];
|
|
32
42
|
const written: string[] = [];
|
|
33
43
|
const { out, io } = capture();
|
|
34
44
|
const code = runGates({
|
|
35
45
|
gates,
|
|
36
|
-
spawn: (cmd) =>
|
|
46
|
+
spawn: (cmd, args) => {
|
|
47
|
+
calls.push([cmd, ...args]);
|
|
48
|
+
return { status: status[cmd] ?? null };
|
|
49
|
+
},
|
|
37
50
|
write: (s) => written.push(s),
|
|
51
|
+
reportOf: () => SKIPPED_2,
|
|
38
52
|
...io,
|
|
39
53
|
});
|
|
54
|
+
// The vitest gate is asked for its JSON report, beside its usual output.
|
|
55
|
+
const vitestCall = calls[1] ?? [];
|
|
56
|
+
const file = (vitestCall.at(-1) ?? "").replace("--outputFile.json=", "");
|
|
57
|
+
assert.deepEqual(vitestCall, [
|
|
58
|
+
"npm",
|
|
59
|
+
"run",
|
|
60
|
+
"-s",
|
|
61
|
+
"test:e2e:x",
|
|
62
|
+
...reportArgs(file),
|
|
63
|
+
]);
|
|
40
64
|
assert.deepEqual(
|
|
41
65
|
{ code, written, out },
|
|
42
66
|
{
|
|
@@ -46,7 +70,7 @@ test("runGates: a pass, a declared skip (77) and a failure, then the tail naming
|
|
|
46
70
|
"",
|
|
47
71
|
"E 🔴 gates failed:",
|
|
48
72
|
"E fails (false → 3)",
|
|
49
|
-
"⏳ skipped: skips (
|
|
73
|
+
"⏳ skipped: skips (npm run -s test:e2e:x → 2 test(s) skipped)",
|
|
50
74
|
"\nWhat this command does NOT cover:",
|
|
51
75
|
...Object.entries(NOT_COVERED).map(
|
|
52
76
|
([job, why]) => ` CI job «${job}» — ${why}`,
|
|
@@ -62,12 +86,11 @@ test("runGates: all passing, with a skip, says how many passed and that the skip
|
|
|
62
86
|
const code = runGates({
|
|
63
87
|
gates: [
|
|
64
88
|
{ name: "a", job: "gates", script: "build" },
|
|
65
|
-
{ name: "b", job: "gates",
|
|
89
|
+
{ name: "b", job: "gates", script: "test:e2e:x", vitest: true },
|
|
66
90
|
],
|
|
67
|
-
spawn: (
|
|
68
|
-
status: cmd === "npm" && args[2] === "build" ? 0 : 77,
|
|
69
|
-
}),
|
|
91
|
+
spawn: () => ({ status: 0 }),
|
|
70
92
|
write: () => {},
|
|
93
|
+
reportOf: () => SKIPPED_2,
|
|
71
94
|
...io,
|
|
72
95
|
});
|
|
73
96
|
assert.deepEqual(
|
|
@@ -75,19 +98,43 @@ test("runGates: all passing, with a skip, says how many passed and that the skip
|
|
|
75
98
|
[
|
|
76
99
|
0,
|
|
77
100
|
"✓ 1 gate(s) passed, 1 SKIPPED — not run, not passed",
|
|
78
|
-
"⏳ skipped: b (
|
|
101
|
+
"⏳ skipped: b (npm run -s test:e2e:x → 2 test(s) skipped)",
|
|
79
102
|
],
|
|
80
103
|
);
|
|
81
104
|
assert.ok(GATES.length > 0);
|
|
82
105
|
});
|
|
83
106
|
|
|
107
|
+
test("🔴 runGates: a vitest gate that exits 0 but leaves no report is a FAILURE, not a pass", () => {
|
|
108
|
+
const { out, io } = capture();
|
|
109
|
+
const code = runGates({
|
|
110
|
+
gates: [{ name: "v", job: "gates", script: "test:e2e:x", vitest: true }],
|
|
111
|
+
spawn: () => ({ status: 0 }),
|
|
112
|
+
write: () => {},
|
|
113
|
+
...io,
|
|
114
|
+
});
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
[code, out[1], out[2]],
|
|
117
|
+
[
|
|
118
|
+
1,
|
|
119
|
+
"E 🔴 gates failed:",
|
|
120
|
+
"E v (npm run -s test:e2e:x → exit 0, but no vitest JSON report to read skips from)",
|
|
121
|
+
],
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
84
125
|
test("run as a program with every gate passing: the plain verdict, exit 0", () => {
|
|
85
126
|
// spawnSync is replaced before check.ts loads, so the real file runs end to end — every gate
|
|
86
127
|
// "passes" in microseconds instead of the eleven real ones running.
|
|
87
128
|
const allPass = [
|
|
88
129
|
'import cp from "node:child_process";',
|
|
89
130
|
'import { syncBuiltinESMExports } from "node:module";',
|
|
90
|
-
|
|
131
|
+
'import fs from "node:fs";',
|
|
132
|
+
// A vitest gate writes its report where check.ts asked; a real run of none of them happens.
|
|
133
|
+
"cp.spawnSync = (cmd, args = []) => {",
|
|
134
|
+
' const o = args.find((a) => a.startsWith("--outputFile.json="));',
|
|
135
|
+
" if (o) fs.writeFileSync(o.slice(18), JSON.stringify({ numPendingTests: 0, numTodoTests: 0 }));",
|
|
136
|
+
" return { status: 0 };",
|
|
137
|
+
"};",
|
|
91
138
|
"syncBuiltinESMExports();",
|
|
92
139
|
].join("\n");
|
|
93
140
|
const r = spawnSync(
|