paperlint 7.1.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +3 -0
- package/.github/workflows/deadlines.yml +100 -0
- package/README.md +6 -0
- package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
- package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.js +26 -0
- package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
- package/dist/adapters/hotcrp/deadlines.js +134 -0
- package/dist/adapters/hotcrp/deadlines.js.map +1 -0
- package/dist/adapters/hotcrp/index.d.ts +2 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -1
- package/dist/adapters/hotcrp/index.js +2 -0
- package/dist/adapters/hotcrp/index.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -6
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +3 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -47
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts +24 -0
- package/dist/cycle-rules.d.ts.map +1 -0
- package/dist/cycle-rules.js +212 -0
- package/dist/cycle-rules.js.map +1 -0
- package/dist/domain/cycle.d.ts +148 -0
- package/dist/domain/cycle.d.ts.map +1 -0
- package/dist/domain/cycle.js +281 -0
- package/dist/domain/cycle.js.map +1 -0
- package/dist/domain/deadline.d.ts +128 -0
- package/dist/domain/deadline.d.ts.map +1 -0
- package/dist/domain/deadline.js +281 -0
- package/dist/domain/deadline.js.map +1 -0
- package/dist/eslint-rules/cycle-stage.d.ts +39 -0
- package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
- package/dist/eslint-rules/cycle-stage.js +119 -0
- package/dist/eslint-rules/cycle-stage.js.map +1 -0
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.js +2 -2
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +22 -91
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.js.map +1 -1
- package/dist/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-prose.d.ts +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- package/dist/paper-settings.d.ts +7 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +61 -8
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/venue-deadlines.d.ts +14 -0
- package/dist/ports/venue-deadlines.d.ts.map +1 -0
- package/dist/ports/venue-deadlines.js +2 -0
- package/dist/ports/venue-deadlines.js.map +1 -0
- package/dist/presets.d.ts +12 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -11
- package/dist/presets.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.js.map +1 -1
- package/dist/submission.d.ts +8 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +41 -10
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +3 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +19 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +19 -0
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +69 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +10 -0
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +26 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +146 -20
- package/docs/e2e.md +4 -6
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- package/docs/rules/cycle/evidence.md +87 -0
- package/docs/rules/cycle/port-done.md +75 -0
- package/docs/rules/cycle/record.md +152 -0
- package/docs/rules/cycle/stage.md +85 -0
- package/docs/rules/tex/claim-provenance.md +0 -1
- package/docs/rules.md +48 -47
- package/docs/submission.md +5 -0
- package/eslint-rules/cycle-stage.test.mjs +131 -0
- package/eslint-rules/cycle-stage.ts +141 -0
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +2 -2
- package/eslint-rules/paper-typography.test.mjs +17 -85
- package/eslint-rules/paper-typography.ts +22 -98
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +41 -2
- package/hooks/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +3 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +1 -3
- package/presets/aidc.jsonc +24 -3
- package/presets/aidc.md +6 -3
- package/presets/msr.jsonc +25 -1
- package/presets/msr.md +5 -1
- package/presets/realm.jsonc +1 -1
- package/presets/venue-profile.schema.json +39 -0
- package/scripts/check.ts +4 -0
- package/scripts/harness-api.frozen.json +0 -5
- package/scripts/refresh-deadlines.test.ts +431 -0
- package/scripts/refresh-deadlines.ts +387 -0
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +14 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +108 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
- package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- package/src/adapters/hotcrp/cassette/README.md +22 -0
- package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
- package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
- package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
- package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
- package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
- package/src/adapters/hotcrp/deadlines.io.ts +27 -0
- package/src/adapters/hotcrp/deadlines.test.ts +170 -0
- package/src/adapters/hotcrp/deadlines.ts +161 -0
- package/src/adapters/hotcrp/index.ts +2 -0
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +1 -1
- package/src/build.ts +1 -6
- package/src/cli.harness.mjs +26 -46
- package/src/cli.test.ts +41 -40
- package/src/cli.ts +57 -58
- package/src/cycle-rules.test.ts +278 -0
- package/src/cycle-rules.ts +273 -0
- package/src/domain/cycle.test.ts +456 -0
- package/src/domain/cycle.ts +432 -0
- package/src/domain/deadline.test.ts +386 -0
- package/src/domain/deadline.ts +472 -0
- package/src/folder-venue-rule.test.ts +1 -1
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/lint-fix.test.ts +10 -4
- package/src/new-paper.harness.mjs +9 -28
- package/src/new-paper.test.ts +7 -6
- package/src/new-paper.ts +11 -30
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-prose.test.ts +49 -0
- package/src/paper-prose.ts +67 -0
- package/src/paper-settings.test.ts +167 -0
- package/src/paper-settings.ts +85 -11
- package/src/parse-args.test.ts +0 -1
- package/src/ports/venue-deadlines.ts +14 -0
- package/src/presets.test.ts +105 -0
- package/src/presets.ts +43 -10
- package/src/rule-cases.test.ts +144 -0
- package/src/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- package/src/submission.test.ts +105 -1
- package/src/submission.ts +73 -12
- package/src/tex-requirements.ts +31 -0
- package/src/tex-venue-rules.test.ts +76 -0
- package/src/tex-venue-rules.ts +85 -0
- package/src/types.ts +0 -2
- package/src/venue-rules.test.ts +82 -0
- package/src/venue-rules.ts +44 -12
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* THE SHIPPED PRESETS' DEADLINES, RE-READ FROM THEIR PORTALS — run once a day by
|
|
4
|
+
* `.github/workflows/deadlines.yml`, which opens or updates one pull request when a value changed.
|
|
5
|
+
*
|
|
6
|
+
* node scripts/refresh-deadlines.ts [--presets <dir>] [--summary <file.md>]
|
|
7
|
+
*
|
|
8
|
+
* For every preset in the directory whose `portal` has an adapter (HotCRP today), the portal's public
|
|
9
|
+
* deadlines page is read and compared with the preset's `deadlines`:
|
|
10
|
+
*
|
|
11
|
+
* a kind the page lists at the instant the preset holds kept as it is, its `read` day too
|
|
12
|
+
* a kind the page lists at another instant rewritten, `read` = today
|
|
13
|
+
* a kind the page lists that the preset does not hold added, `read` = today
|
|
14
|
+
* a kind the preset holds that the page no longer lists KEPT and noted — a portal may drop a
|
|
15
|
+
* passed deadline; the reading stays true
|
|
16
|
+
* a reading from the call never touched: it is not the portal's
|
|
17
|
+
*
|
|
18
|
+
* Only a changed value rewrites the file, so an unchanged portal produces no diff and no pull
|
|
19
|
+
* request: a `read` day says when the value was last READ DIFFERENT, not when the job last ran.
|
|
20
|
+
*
|
|
21
|
+
* ── IT REPORTS, IT DOES NOT BLOCK ─────────────────────────────────────────────────────────────
|
|
22
|
+
* A portal that is down, gone or no longer a HotCRP page is a `::warning::` line and a line in the
|
|
23
|
+
* summary, and the run exits 0 — the venue's outage is not this repository's defect, and the next
|
|
24
|
+
* day's run reads it again. A preset that does not parse IS this repository's defect: `::error::`,
|
|
25
|
+
* exit 1. The main CI never runs this script against the network; its tests replay the pages
|
|
26
|
+
* recorded in `src/adapters/hotcrp/cassette/`.
|
|
27
|
+
*
|
|
28
|
+
* The block is found on the JSONC's syntax tree (TypeScript's JSON parser, the one the presets are
|
|
29
|
+
* read with) and replaced; everything else in the file — its comments above all — is left as it
|
|
30
|
+
* is, and Prettier formats the result as `npm run fmt` would.
|
|
31
|
+
*/
|
|
32
|
+
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
33
|
+
import { dirname, join } from "node:path";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
35
|
+
import { format } from "prettier";
|
|
36
|
+
import ts from "typescript";
|
|
37
|
+
import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
38
|
+
import { hotcrpDeadlines } from "../src/adapters/hotcrp/index.ts";
|
|
39
|
+
import {
|
|
40
|
+
DEADLINE_WHATS,
|
|
41
|
+
type DeadlineWhat,
|
|
42
|
+
type IsoDate,
|
|
43
|
+
type PortalDeadlines,
|
|
44
|
+
type Reading,
|
|
45
|
+
} from "../src/domain/deadline.ts";
|
|
46
|
+
import type { Result } from "../src/domain/result.ts";
|
|
47
|
+
import {
|
|
48
|
+
supportedPortal,
|
|
49
|
+
type PortalFailure,
|
|
50
|
+
} from "../src/domain/submission.ts";
|
|
51
|
+
import { messageOf } from "../src/domain/text.ts";
|
|
52
|
+
import { presetsDir } from "../src/package-dirs.ts";
|
|
53
|
+
import { parsePreset } from "../src/tex-requirements.ts";
|
|
54
|
+
|
|
55
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
56
|
+
|
|
57
|
+
/** Reads the deadlines page of the portal site at `site` (no trailing slash). */
|
|
58
|
+
export type ReadPortal = (
|
|
59
|
+
site: string,
|
|
60
|
+
) => Promise<Result<PortalDeadlines, PortalFailure>>;
|
|
61
|
+
|
|
62
|
+
/** What happened to one preset. */
|
|
63
|
+
export type Outcome =
|
|
64
|
+
| { readonly kind: "skipped"; readonly why: string }
|
|
65
|
+
| { readonly kind: "broken"; readonly why: string }
|
|
66
|
+
| { readonly kind: "unreadable"; readonly url: string; readonly why: string }
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: "unchanged";
|
|
69
|
+
readonly url: string;
|
|
70
|
+
readonly notes: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
readonly kind: "changed";
|
|
74
|
+
readonly url: string;
|
|
75
|
+
readonly text: string;
|
|
76
|
+
readonly changes: readonly string[];
|
|
77
|
+
readonly notes: readonly string[];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** The preset's readings after the page's answer, what changed, and what is worth a note. */
|
|
81
|
+
export interface Refreshed {
|
|
82
|
+
readonly readings: readonly Reading[];
|
|
83
|
+
readonly changes: readonly string[];
|
|
84
|
+
readonly notes: readonly string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The page's instants for one kind, earliest first. */
|
|
88
|
+
const pageAts = (page: PortalDeadlines, what: DeadlineWhat): string[] =>
|
|
89
|
+
page.deadlines
|
|
90
|
+
.filter((d) => d.what === what)
|
|
91
|
+
.map((d) => d.at)
|
|
92
|
+
.toSorted();
|
|
93
|
+
|
|
94
|
+
/** A kind the page does not list: the preset's reading, if it holds one, is kept and noted. */
|
|
95
|
+
const offThePage = (
|
|
96
|
+
what: DeadlineWhat,
|
|
97
|
+
held: Reading | undefined,
|
|
98
|
+
): Refreshed =>
|
|
99
|
+
held === undefined
|
|
100
|
+
? { readings: [], changes: [], notes: [] }
|
|
101
|
+
: {
|
|
102
|
+
readings: [held],
|
|
103
|
+
changes: [],
|
|
104
|
+
notes: [`${what}: no longer on the page — kept as read ${held.read}`],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** One kind: the preset's portal reading against the page's. */
|
|
108
|
+
function oneKind(
|
|
109
|
+
what: DeadlineWhat,
|
|
110
|
+
held: Reading | undefined,
|
|
111
|
+
ats: readonly string[],
|
|
112
|
+
o: { readonly url: string; readonly today: IsoDate },
|
|
113
|
+
): Refreshed {
|
|
114
|
+
const notes =
|
|
115
|
+
ats.length > 1
|
|
116
|
+
? [`${what}: the page lists ${ats.join(", ")} — the earliest is recorded`]
|
|
117
|
+
: [];
|
|
118
|
+
const at = ats[0];
|
|
119
|
+
if (at === undefined) return offThePage(what, held);
|
|
120
|
+
if (held?.at === at) return { readings: [held], changes: [], notes };
|
|
121
|
+
const fresh: Reading = {
|
|
122
|
+
what,
|
|
123
|
+
at,
|
|
124
|
+
source: "portal",
|
|
125
|
+
url: o.url,
|
|
126
|
+
read: o.today,
|
|
127
|
+
};
|
|
128
|
+
const change =
|
|
129
|
+
held === undefined ? `${what}: added ${at}` : `${what}: ${held.at} → ${at}`;
|
|
130
|
+
return { readings: [fresh], changes: [change], notes };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The preset's readings after the portal answered `page` at `url` on `today`. The call's readings
|
|
135
|
+
* are kept as they are; the portal's are kept, rewritten or added kind by kind. Pure.
|
|
136
|
+
*/
|
|
137
|
+
export function refreshReadings(
|
|
138
|
+
current: readonly Reading[],
|
|
139
|
+
page: PortalDeadlines,
|
|
140
|
+
url: string,
|
|
141
|
+
today: IsoDate,
|
|
142
|
+
): Refreshed {
|
|
143
|
+
const perKind = DEADLINE_WHATS.map((what) => {
|
|
144
|
+
const portal = oneKind(
|
|
145
|
+
what,
|
|
146
|
+
current.find((r) => r.what === what && r.source === "portal"),
|
|
147
|
+
pageAts(page, what),
|
|
148
|
+
{ url, today },
|
|
149
|
+
);
|
|
150
|
+
const call = current.filter((r) => r.what === what && r.source === "call");
|
|
151
|
+
return { ...portal, readings: [...portal.readings, ...call] };
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
readings: perKind.flatMap((k) => k.readings),
|
|
155
|
+
changes: perKind.flatMap((k) => k.changes),
|
|
156
|
+
notes: perKind.flatMap((k) => k.notes),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The top-level property `name` of a JSONC object, on its syntax tree; undefined when absent. */
|
|
161
|
+
function property(
|
|
162
|
+
file: ts.JsonSourceFile,
|
|
163
|
+
name: string,
|
|
164
|
+
): ts.PropertyAssignment | undefined {
|
|
165
|
+
const top = file.statements[0]?.expression;
|
|
166
|
+
if (top === undefined || !ts.isObjectLiteralExpression(top)) return undefined;
|
|
167
|
+
return top.properties
|
|
168
|
+
.filter(ts.isPropertyAssignment)
|
|
169
|
+
.find((p) => ts.isStringLiteral(p.name) && p.name.text === name);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The preset's text with `deadlines` set to `readings`: the value replaced where the key is, or the
|
|
174
|
+
* key inserted after `portal`. Formatted as the repository formats a preset.
|
|
175
|
+
*/
|
|
176
|
+
export async function withDeadlines(
|
|
177
|
+
text: string,
|
|
178
|
+
file: string,
|
|
179
|
+
readings: readonly Reading[],
|
|
180
|
+
): Promise<string> {
|
|
181
|
+
const tree = ts.parseJsonText(file, text);
|
|
182
|
+
const json = JSON.stringify(readings, null, 2);
|
|
183
|
+
const held = property(tree, "deadlines");
|
|
184
|
+
const spliced =
|
|
185
|
+
held === undefined
|
|
186
|
+
? insertAfterPortal(tree, text, json)
|
|
187
|
+
: text.slice(0, held.initializer.getStart(tree)) +
|
|
188
|
+
json +
|
|
189
|
+
text.slice(held.initializer.end);
|
|
190
|
+
return format(spliced, { filepath: file });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** `"deadlines": …` after the `portal` property — only a preset with a portal is ever refreshed. */
|
|
194
|
+
function insertAfterPortal(
|
|
195
|
+
tree: ts.JsonSourceFile,
|
|
196
|
+
text: string,
|
|
197
|
+
json: string,
|
|
198
|
+
): string {
|
|
199
|
+
const portal = property(tree, "portal");
|
|
200
|
+
if (portal === undefined)
|
|
201
|
+
throw new Error(`${tree.fileName}: no "portal" to put "deadlines" after`);
|
|
202
|
+
return `${text.slice(0, portal.end)},\n\n "deadlines": ${json}${text.slice(portal.end)}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A failure to read the page, in one line. */
|
|
206
|
+
function failureText(f: PortalFailure): string {
|
|
207
|
+
switch (f.kind) {
|
|
208
|
+
case "unreachable":
|
|
209
|
+
return `unreachable: ${f.detail}`;
|
|
210
|
+
case "refused":
|
|
211
|
+
return `HTTP ${String(f.httpStatus)}: ${f.messages.map((m) => m.message).join("; ")}`;
|
|
212
|
+
case "malformed":
|
|
213
|
+
return `not the page expected (HTTP ${String(f.httpStatus)}): ${f.detail}`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** One preset: its portal read, its readings compared, its new text when one changed. */
|
|
218
|
+
export async function refreshPreset(o: {
|
|
219
|
+
readonly file: string;
|
|
220
|
+
readonly text: string;
|
|
221
|
+
readonly read: ReadPortal;
|
|
222
|
+
readonly today: IsoDate;
|
|
223
|
+
}): Promise<Outcome> {
|
|
224
|
+
const parsed = ((): ReturnType<typeof parsePreset> | string => {
|
|
225
|
+
try {
|
|
226
|
+
return parsePreset(o.text, o.file, presetsDir());
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return messageOf(e);
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
if (typeof parsed === "string") return { kind: "broken", why: parsed };
|
|
232
|
+
if (parsed.portal === null) return { kind: "skipped", why: "no portal" };
|
|
233
|
+
const portal = supportedPortal(parsed.portal);
|
|
234
|
+
if (!portal.ok) return { kind: "skipped", why: portal.error };
|
|
235
|
+
const url = `${portal.value.url}/deadlines`;
|
|
236
|
+
const page = await o.read(portal.value.url);
|
|
237
|
+
if (!page.ok)
|
|
238
|
+
return { kind: "unreadable", url, why: failureText(page.error) };
|
|
239
|
+
const r = refreshReadings(parsed.deadlines ?? [], page.value, url, o.today);
|
|
240
|
+
if (r.changes.length === 0) return { kind: "unchanged", url, notes: r.notes };
|
|
241
|
+
const text = await withDeadlines(o.text, o.file, r.readings);
|
|
242
|
+
return { kind: "changed", url, text, changes: r.changes, notes: r.notes };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The lines one preset's outcome contributes: to the log (annotations included) and to the summary. */
|
|
246
|
+
function report(
|
|
247
|
+
name: string,
|
|
248
|
+
o: Outcome,
|
|
249
|
+
): { readonly log: readonly string[]; readonly md: readonly string[] } {
|
|
250
|
+
switch (o.kind) {
|
|
251
|
+
case "skipped":
|
|
252
|
+
return { log: [], md: [] };
|
|
253
|
+
case "broken":
|
|
254
|
+
return {
|
|
255
|
+
log: [`::error title=deadlines::${name} does not parse: ${o.why}`],
|
|
256
|
+
md: [`- 🔴 \`${name}\` does not parse: ${o.why}`],
|
|
257
|
+
};
|
|
258
|
+
case "unreadable":
|
|
259
|
+
return {
|
|
260
|
+
log: [
|
|
261
|
+
`::warning title=deadlines::${name}: could not read ${o.url} — ${o.why}`,
|
|
262
|
+
],
|
|
263
|
+
md: [`- ⚠️ \`${name}\`: could not read ${o.url} — ${o.why}`],
|
|
264
|
+
};
|
|
265
|
+
case "unchanged":
|
|
266
|
+
return { log: o.notes.map((n) => `${name}: ${n}`), md: [] };
|
|
267
|
+
case "changed":
|
|
268
|
+
return changedReport(name, o);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** A changed preset: each change and note, in the log and as a list in the summary. */
|
|
273
|
+
const changedReport = (
|
|
274
|
+
name: string,
|
|
275
|
+
o: Extract<Outcome, { kind: "changed" }>,
|
|
276
|
+
): { readonly log: readonly string[]; readonly md: readonly string[] } => {
|
|
277
|
+
const lines = [...o.changes, ...o.notes];
|
|
278
|
+
return {
|
|
279
|
+
log: lines.map((c) => `${name}: ${c}`),
|
|
280
|
+
md: [`- \`${name}\` (${o.url})`, ...lines.map((c) => ` - ${c}`)],
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export interface MainOptions {
|
|
285
|
+
readonly presetsDir: string;
|
|
286
|
+
readonly read: ReadPortal;
|
|
287
|
+
readonly today: IsoDate;
|
|
288
|
+
readonly log: (line: string) => void;
|
|
289
|
+
readonly write: (file: string, text: string) => void;
|
|
290
|
+
/** Where the pull request's body goes; null: nowhere. */
|
|
291
|
+
readonly summary: string | null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Every preset of the directory, refreshed — a changed one written back. */
|
|
295
|
+
async function outcomesOf(
|
|
296
|
+
o: MainOptions,
|
|
297
|
+
): Promise<readonly { readonly name: string; readonly out: Outcome }[]> {
|
|
298
|
+
const names = readdirSync(o.presetsDir)
|
|
299
|
+
.filter((f) => f.endsWith(".jsonc"))
|
|
300
|
+
.toSorted();
|
|
301
|
+
return Promise.all(
|
|
302
|
+
names.map(async (name) => {
|
|
303
|
+
const file = join(o.presetsDir, name);
|
|
304
|
+
const text = readFileSync(file, "utf8");
|
|
305
|
+
const out = await refreshPreset({
|
|
306
|
+
file,
|
|
307
|
+
text,
|
|
308
|
+
read: o.read,
|
|
309
|
+
today: o.today,
|
|
310
|
+
});
|
|
311
|
+
if (out.kind === "changed") o.write(file, out.text);
|
|
312
|
+
return { name, out };
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Every preset of the directory, refreshed. 0 unless a preset does not parse. */
|
|
318
|
+
export async function main(o: MainOptions): Promise<number> {
|
|
319
|
+
const outcomes = await outcomesOf(o);
|
|
320
|
+
const reports = outcomes.map(({ name, out }) => report(name, out));
|
|
321
|
+
reports
|
|
322
|
+
.flatMap((r) => r.log)
|
|
323
|
+
.forEach((l) => {
|
|
324
|
+
o.log(l);
|
|
325
|
+
});
|
|
326
|
+
const count = (k: Outcome["kind"]) =>
|
|
327
|
+
outcomes.filter((x) => x.out.kind === k).length;
|
|
328
|
+
const read = count("changed") + count("unchanged");
|
|
329
|
+
o.log(
|
|
330
|
+
count("changed") === 0
|
|
331
|
+
? `✓ no deadline changed: ${String(read)} preset(s) with a portal read, ${String(count("skipped"))} skipped`
|
|
332
|
+
: `${String(count("changed"))} preset(s) changed`,
|
|
333
|
+
);
|
|
334
|
+
if (o.summary !== null)
|
|
335
|
+
o.write(
|
|
336
|
+
o.summary,
|
|
337
|
+
summaryOf(
|
|
338
|
+
o.today,
|
|
339
|
+
reports.flatMap((r) => r.md),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
return count("broken") > 0 ? 1 : 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The pull request's body. */
|
|
346
|
+
const summaryOf = (today: IsoDate, lines: readonly string[]): string =>
|
|
347
|
+
[
|
|
348
|
+
`The venue presets' deadlines, re-read on ${today} from each preset's portal by \`.github/workflows/deadlines.yml\` (\`scripts/refresh-deadlines.ts\`). A reading whose value changed carries today's \`read\` day; the others keep theirs.`,
|
|
349
|
+
"",
|
|
350
|
+
...lines,
|
|
351
|
+
"",
|
|
352
|
+
"Merging releases a patch; papers get the new dates through their normal update of paperlint.",
|
|
353
|
+
"",
|
|
354
|
+
].join("\n");
|
|
355
|
+
|
|
356
|
+
/** `--presets <dir>` and `--summary <file>` from the command line. */
|
|
357
|
+
export function argsOf(argv: readonly string[]): {
|
|
358
|
+
readonly presets: string;
|
|
359
|
+
readonly summary: string | null;
|
|
360
|
+
} {
|
|
361
|
+
const value = (flag: string): string | null => {
|
|
362
|
+
const i = argv.indexOf(flag);
|
|
363
|
+
return i === -1 ? null : (argv[i + 1] ?? null);
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
presets: value("--presets") ?? join(ROOT, "presets"),
|
|
367
|
+
summary: value("--summary"),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// `isMain`, not a comparison with `file://${argv[1]}`: that is false through a symlink (consumer.mjs).
|
|
372
|
+
if (isMain(import.meta.url)) {
|
|
373
|
+
const args = argsOf(process.argv.slice(2));
|
|
374
|
+
process.exit(
|
|
375
|
+
await main({
|
|
376
|
+
presetsDir: args.presets,
|
|
377
|
+
read: (site) => hotcrpDeadlines({ url: site }).read(),
|
|
378
|
+
// The clock is read here, at the edge: the day a reading was taken.
|
|
379
|
+
today: new Date().toISOString().slice(0, 10),
|
|
380
|
+
log: console.log,
|
|
381
|
+
write: (file, text) => {
|
|
382
|
+
writeFileSync(file, text);
|
|
383
|
+
},
|
|
384
|
+
summary: args.summary,
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
@@ -5,7 +5,7 @@ context: fork
|
|
|
5
5
|
allowed-tools: [Bash, Read, Agent]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
<!-- vigiles:sha256:
|
|
8
|
+
<!-- vigiles:sha256:c3a1f2644781eb6f compiled from skills/cold-read-diff/SKILL.md.spec.ts -->
|
|
9
9
|
|
|
10
10
|
# cold-read-diff — the reader who cannot fake understanding
|
|
11
11
|
|
|
@@ -55,7 +55,7 @@ one, and its confusion is the finding.
|
|
|
55
55
|
|
|
56
56
|
## How to run it
|
|
57
57
|
|
|
58
|
-
1. **Take the diff.** `git diff -U0 -- <paper.
|
|
58
|
+
1. **Take the diff.** `git diff -U0 -- <paper.tex>` since the last commit, or the `new_string` of the
|
|
59
59
|
edits just applied. Prose only — skip comments, tables, bibliography.
|
|
60
60
|
2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
|
|
61
61
|
changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
|
|
@@ -105,7 +105,7 @@ Two more things the same run proved:
|
|
|
105
105
|
- **Expand build-time macros first.** The reader saw seven raw `{{placeholders}}` and correctly
|
|
106
106
|
reported that every headline number was missing. That is a true statement about the extract and a
|
|
107
107
|
false one about the paper.
|
|
108
|
-
- **Strip working comments.** He graded
|
|
108
|
+
- **Strip working comments.** He graded `% JUSTIFY …` comment notes as prose because he could not tell
|
|
109
109
|
them apart — and said so, which is itself a finding about the extract.
|
|
110
110
|
|
|
111
111
|
**So:** take the changed lines, then widen each to its enclosing paragraph, expand macros, drop
|
|
@@ -60,7 +60,7 @@ one, and its confusion is the finding.
|
|
|
60
60
|
|
|
61
61
|
## How to run it
|
|
62
62
|
|
|
63
|
-
1. **Take the diff.** \`git diff -U0 -- <paper.
|
|
63
|
+
1. **Take the diff.** \`git diff -U0 -- <paper.tex>\` since the last commit, or the \`new_string\` of the
|
|
64
64
|
edits just applied. Prose only — skip comments, tables, bibliography.
|
|
65
65
|
2. **Spawn TWO agents with no repository context, one per persona, in parallel.** Give each the
|
|
66
66
|
changed paragraphs **as text in the prompt**, not as a file path. A path is an invitation to read
|
|
@@ -110,7 +110,7 @@ Two more things the same run proved:
|
|
|
110
110
|
- **Expand build-time macros first.** The reader saw seven raw \`{{placeholders}}\` and correctly
|
|
111
111
|
reported that every headline number was missing. That is a true statement about the extract and a
|
|
112
112
|
false one about the paper.
|
|
113
|
-
- **Strip working comments.** He graded
|
|
113
|
+
- **Strip working comments.** He graded \`% JUSTIFY …\` comment notes as prose because he could not tell
|
|
114
114
|
them apart — and said so, which is itself a finding about the extract.
|
|
115
115
|
|
|
116
116
|
**So:** take the changed lines, then widen each to its enclosing paragraph, expand macros, drop
|
|
@@ -4,7 +4,7 @@ description: Use when asking "does this paper read well?" / "is the writing any
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Agent, Skill]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:6401b9117bbf899f compiled from skills/grade-paper-writing/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# grade-paper-writing — grade how the paper READS, then fix it sentence by sentence
|
|
10
10
|
|
|
@@ -53,7 +53,7 @@ derived from that gap.
|
|
|
53
53
|
### 🔴 Read the BUILT PDF, not the source
|
|
54
54
|
|
|
55
55
|
**The stall pass reads the typeset pages.** The source is for locating the fix afterwards, never for
|
|
56
|
-
judging the read. This was previously written as "read the `.tex
|
|
56
|
+
judging the read. This was previously written as "read the `.tex`, **or** run `render-paper`",
|
|
57
57
|
and that *or* is why the pass kept missing what the author saw immediately: he reads the PDF.
|
|
58
58
|
|
|
59
59
|
Things that exist only in the render, every one of which produces a real stall:
|
|
@@ -153,7 +153,7 @@ is what happened.
|
|
|
153
153
|
|
|
154
154
|
## How to run it
|
|
155
155
|
|
|
156
|
-
1. **Read the actual draft** (the `.tex
|
|
156
|
+
1. **Read the actual draft** (the `.tex`, or run `render-paper` to read the built pages as a reader
|
|
157
157
|
would). Grade the writing as written — never from a summary.
|
|
158
158
|
2. **Run the cold-read stall pass FIRST — the full specification is below, under "The stall pass".**
|
|
159
159
|
It was previously a pointer to a reference section that did not exist, which is why the pass drifted
|
|
@@ -215,10 +215,17 @@ is what happened.
|
|
|
215
215
|
5. **Flag the two prose traps by name:** (a) Attention-style dense contribution-less prose used where the
|
|
216
216
|
audience does NOT already care; (b) hedge-stacking ("may possibly in some cases potentially") — replace
|
|
217
217
|
each pile with one precise scope sentence.
|
|
218
|
-
6. 🔴 **Run `prose-lint.mjs` and treat every FLAG as a finding, not as context
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
6. 🔴 **Run `prose-lint.mjs` on the paper and treat every FLAG as a finding, not as context:**
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
node ${CLAUDE_SKILL_DIR}/prose-lint.mjs <paper-dir>/paper.tex # the report
|
|
222
|
+
node ${CLAUDE_SKILL_DIR}/prose-lint.mjs --headings <paper-dir>/paper.tex # the headings as a set
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
It measures the prose a reader sees — includes spliced, no preamble, comments, floats or inline
|
|
226
|
+
`.bib` — so its rates are the paper's, not the source file's. The skill is what must ACT on it.
|
|
227
|
+
Its thresholds are sourced; a FLAG means the text sits above a published human or machine
|
|
228
|
+
baseline, which is a defect with a number attached, not an opinion.
|
|
222
229
|
Two entries in `THRESHOLDS` exist because this skill previously ran five times without either
|
|
223
230
|
firing: the `"not X, but Y"` density had **no threshold at all** and printed as a neutral line
|
|
224
231
|
while standing at 50.8 per 10,000 words against an LLM baseline of 7.8, and the lexicon missed
|
|
@@ -227,21 +234,6 @@ is what happened.
|
|
|
227
234
|
A number printed without a threshold is prose; that is this project's own thesis, and the linter
|
|
228
235
|
was violating it.
|
|
229
236
|
|
|
230
|
-
6a. 🔴 **`prose-lint.mjs` IS NO LONGER ALONE — seven of its twelve gating metrics moved
|
|
231
|
-
on 2026-08-26 into ESLint rules, and running only it shows you five twelfths.** A second
|
|
232
|
-
run is mandatory; without it, step 6 is incomplete:
|
|
233
|
-
|
|
234
|
-
```
|
|
235
|
-
npx eslint "papers/*/paper.md" "papers/*/draft.md"
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
From there come: `paper/citation-density` · `paper/unexplained-jargon` ·
|
|
239
|
-
`paper/multi-claim-sentence` · `paper/conceits` · `paper/hedge-density` ·
|
|
240
|
-
`paper/discourse-subject` · `paper/undefined-coinage` (file — `eslint-rules/paper-craft.mjs`).
|
|
241
|
-
Gain from the move — `file:line:col` on each finding: the old report printed the sentence head,
|
|
242
|
-
and you had to find it in the article by eye. Everything step 6b says about
|
|
243
|
-
meaning regression applies to them just the same: this is diagnosis, not objective.
|
|
244
|
-
|
|
245
237
|
6b. 🔴 **A lint fix that costs meaning is a REGRESSION, and no linter can see it.** The metrics above
|
|
246
238
|
are diagnostics, never objectives. Optimising one directly is how a sentence gets worse while the
|
|
247
239
|
number gets better — and the number is exactly what makes it feel like progress.
|
|
@@ -59,7 +59,7 @@ derived from that gap.
|
|
|
59
59
|
### 🔴 Read the BUILT PDF, not the source
|
|
60
60
|
|
|
61
61
|
**The stall pass reads the typeset pages.** The source is for locating the fix afterwards, never for
|
|
62
|
-
judging the read. This was previously written as "read the \`.tex
|
|
62
|
+
judging the read. This was previously written as "read the \`.tex\`, **or** run \`render-paper\`",
|
|
63
63
|
and that *or* is why the pass kept missing what the author saw immediately: he reads the PDF.
|
|
64
64
|
|
|
65
65
|
Things that exist only in the render, every one of which produces a real stall:
|
|
@@ -159,7 +159,7 @@ is what happened.
|
|
|
159
159
|
|
|
160
160
|
## How to run it
|
|
161
161
|
|
|
162
|
-
1. **Read the actual draft** (the \`.tex
|
|
162
|
+
1. **Read the actual draft** (the \`.tex\`, or run \`render-paper\` to read the built pages as a reader
|
|
163
163
|
would). Grade the writing as written — never from a summary.
|
|
164
164
|
2. **Run the cold-read stall pass FIRST — the full specification is below, under "The stall pass".**
|
|
165
165
|
It was previously a pointer to a reference section that did not exist, which is why the pass drifted
|
|
@@ -221,10 +221,17 @@ is what happened.
|
|
|
221
221
|
5. **Flag the two prose traps by name:** (a) Attention-style dense contribution-less prose used where the
|
|
222
222
|
audience does NOT already care; (b) hedge-stacking ("may possibly in some cases potentially") — replace
|
|
223
223
|
each pile with one precise scope sentence.
|
|
224
|
-
6. 🔴 **Run \`prose-lint.mjs\` and treat every FLAG as a finding, not as context
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
6. 🔴 **Run \`prose-lint.mjs\` on the paper and treat every FLAG as a finding, not as context:**
|
|
225
|
+
|
|
226
|
+
\`\`\`
|
|
227
|
+
node \${CLAUDE_SKILL_DIR}/prose-lint.mjs <paper-dir>/paper.tex # the report
|
|
228
|
+
node \${CLAUDE_SKILL_DIR}/prose-lint.mjs --headings <paper-dir>/paper.tex # the headings as a set
|
|
229
|
+
\`\`\`
|
|
230
|
+
|
|
231
|
+
It measures the prose a reader sees — includes spliced, no preamble, comments, floats or inline
|
|
232
|
+
\`.bib\` — so its rates are the paper's, not the source file's. The skill is what must ACT on it.
|
|
233
|
+
Its thresholds are sourced; a FLAG means the text sits above a published human or machine
|
|
234
|
+
baseline, which is a defect with a number attached, not an opinion.
|
|
228
235
|
Two entries in \`THRESHOLDS\` exist because this skill previously ran five times without either
|
|
229
236
|
firing: the \`"not X, but Y"\` density had **no threshold at all** and printed as a neutral line
|
|
230
237
|
while standing at 50.8 per 10,000 words against an LLM baseline of 7.8, and the lexicon missed
|
|
@@ -233,21 +240,6 @@ is what happened.
|
|
|
233
240
|
A number printed without a threshold is prose; that is this project's own thesis, and the linter
|
|
234
241
|
was violating it.
|
|
235
242
|
|
|
236
|
-
6a. 🔴 **\`prose-lint.mjs\` IS NO LONGER ALONE — seven of its twelve gating metrics moved
|
|
237
|
-
on 2026-08-26 into ESLint rules, and running only it shows you five twelfths.** A second
|
|
238
|
-
run is mandatory; without it, step 6 is incomplete:
|
|
239
|
-
|
|
240
|
-
\`\`\`
|
|
241
|
-
npx eslint "papers/*/paper.md" "papers/*/draft.md"
|
|
242
|
-
\`\`\`
|
|
243
|
-
|
|
244
|
-
From there come: \`paper/citation-density\` · \`paper/unexplained-jargon\` ·
|
|
245
|
-
\`paper/multi-claim-sentence\` · \`paper/conceits\` · \`paper/hedge-density\` ·
|
|
246
|
-
\`paper/discourse-subject\` · \`paper/undefined-coinage\` (file — \`eslint-rules/paper-craft.mjs\`).
|
|
247
|
-
Gain from the move — \`file:line:col\` on each finding: the old report printed the sentence head,
|
|
248
|
-
and you had to find it in the article by eye. Everything step 6b says about
|
|
249
|
-
meaning regression applies to them just the same: this is diagnosis, not objective.
|
|
250
|
-
|
|
251
243
|
6b. 🔴 **A lint fix that costs meaning is a REGRESSION, and no linter can see it.** The metrics above
|
|
252
244
|
are diagnostics, never objectives. Optimising one directly is how a sentence gets worse while the
|
|
253
245
|
number gets better — and the number is exactly what makes it feel like progress.
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
3
|
exports[`--flags-only: an over-long caption flags and exits 1 1`] = `
|
|
4
|
-
"✍️ prose-lint — paper.
|
|
4
|
+
"✍️ prose-lint — paper.tex — 2 finding(s):
|
|
5
5
|
figures/fig1.tex: caption sentence of 46 words (warn >40) — "Big. word0 word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 word13 word14 word15 wor…"
|
|
6
6
|
figures/fig1.tex: caption is 106 words (warn >100) — a caption this long is a section under a picture
|
|
7
|
-
run
|
|
7
|
+
run prose-lint.mjs on the file without --flags-only for the sentences
|
|
8
8
|
"
|
|
9
9
|
`;
|
|
10
10
|
|
|
11
|
-
exports[`--headings
|
|
11
|
+
exports[`--headings on a paper.tex lists its section titles, in order 1`] = `
|
|
12
12
|
"
|
|
13
|
-
=== paper.
|
|
13
|
+
=== paper.tex — every heading, in order ===
|
|
14
14
|
1. Introduction
|
|
15
|
-
2.
|
|
15
|
+
2. The measurement pitfall
|
|
16
|
+
3. Method: a cost-aware, correctness-gated harness
|
|
17
|
+
4. Results
|
|
18
|
+
5. The advertised savings don't show up
|
|
19
|
+
6. A ceiling for any output-trimming tool
|
|
20
|
+
7. Discussion and threats to validity
|
|
21
|
+
8. Related work
|
|
22
|
+
9. Conclusion
|
|
16
23
|
|
|
17
24
|
Read these as a stranger who will read nothing else. Each should name a thing:
|
|
18
25
|
a linter, a configuration, a repository, a rule — not "answers", "kinds", "moves".
|
|
@@ -21,28 +28,8 @@ a linter, a configuration, a repository, a rule — not "answers", "kinds", "mov
|
|
|
21
28
|
"
|
|
22
29
|
`;
|
|
23
30
|
|
|
24
|
-
exports[`
|
|
25
|
-
"prose-lint reads markdown, not LaTeX: <root>/paper.tex
|
|
26
|
-
There is no .tex stripping here — the word count would include the preamble and the .bib,
|
|
27
|
-
and every metric (they are all fractions of the word count) would be understated by roughly half.
|
|
28
|
-
What to cover prose in .tex with — papers/research/2026-08-26-proza-v-latex-zanyatost.md
|
|
31
|
+
exports[`the report over a rendered page: page numbers, hyphenation and the references are dropped 1`] = `
|
|
29
32
|
"
|
|
30
|
-
`;
|
|
31
|
-
|
|
32
|
-
exports[`the full report over markdown and rendered text 1`] = `
|
|
33
|
-
"
|
|
34
|
-
=== paper.md — 82 words, 9 sentences ===
|
|
35
|
-
sentence lengths [7,10,8,6,19,6,8,8,6] mean 8.7 sd 3.9 CV 0.45
|
|
36
|
-
FLAG all metadiscourse/1000w 195.1 (Hyland RA baseline 64.9, warn >90)
|
|
37
|
-
nominalisations/1000w 12.2
|
|
38
|
-
FLAG "rather than"/instead-of/10,000w 243.9 (warn >25; not epanorthosis, no published baseline)
|
|
39
|
-
FLAG sentences ending on a cross-ref/citation/hedge (wasted stress position): 3
|
|
40
|
-
> …Clearly the first result matters (Section 3).
|
|
41
|
-
> …Prior work agrees [1, 2].
|
|
42
|
-
> … full suite of papers, which suggests the method might hold generally.
|
|
43
|
-
ok sentences whose main claim is a negation: 22%
|
|
44
|
-
topic positions (whose story is this?): "in this paper we present" | "it might possibly help, but" | "clearly the first result matters" | "prior work agrees [1, 2]." | "the speedup was 40% on" | "- the first item is" | "- the second item is" | "we believe that rather than" | "instead of prose, the numbers"
|
|
45
|
-
|
|
46
33
|
=== page.txt — 13 words, 3 sentences ===
|
|
47
34
|
sentence lengths [3,6,4] mean 4.3 sd 1.2 CV 0.29
|
|
48
35
|
ok all metadiscourse/1000w 0 (Hyland RA baseline 64.9, warn >90)
|
|
@@ -55,3 +42,30 @@ ok sentences whose main claim is a negation: 0%
|
|
|
55
42
|
(thresholds and their sources are in THRESHOLDS at the top of this file)
|
|
56
43
|
"
|
|
57
44
|
`;
|
|
45
|
+
|
|
46
|
+
exports[`🔴 a paper.tex is measured on the prose a reader sees — not its preamble, comments or inline .bib 1`] = `
|
|
47
|
+
"
|
|
48
|
+
=== paper.tex — 4181 words, 166 sentences ===
|
|
49
|
+
sentence lengths [16,24,12,44,37,27,45,26,33,25,23,16,19,23,9,75,42,14,11,28,19,4,34,15,32,24,39,38,38,9,15,29,26,42,45,78,23,27,18,44,57,20,23,43,30,42,31,27,46,30,30,29,16,53,33,41,23,7,28,27,30,26,14,48,9,26,11,15,5,24,14,16,11,1,38,48,12,18,31,20,44,15,30,36,57,22,25,13,25,52,14,7,17,13,16,7,21,19,18,10,20,7,23,7,5,21,7,27,29,8,14,7,23,16,11,9,20,36,32,25,37,18,7,13,35,37,35,27,16,15,23,29,26,26,12,9,15,19,24,10,22,30,24,18,34,16,50,22,32,63,36,26,12,37,36,12,17,48,10,35,27,24,22,19,35,32] mean 25.2 sd 13.6 CV 0.54
|
|
50
|
+
ok all metadiscourse/1000w 50.2 (Hyland RA baseline 64.9, warn >90)
|
|
51
|
+
nominalisations/1000w 27.3
|
|
52
|
+
FLAG "rather than"/instead-of/10,000w 28.7 (warn >25; not epanorthosis, no published baseline)
|
|
53
|
+
FLAG sentences ending on a cross-ref/citation/hedge (wasted stress position): 12
|
|
54
|
+
> … this kind are already woven into how programmers work day to day [1].
|
|
55
|
+
> …the cost claims of the surrounding tooling are largely taken on trust.
|
|
56
|
+
> …, none of which describe how a coding agent actually runs (Section 1).
|
|
57
|
+
> …n class, and rejects any trial that regresses correctness (Section 1).
|
|
58
|
+
> …und explains why no output-trimming tool can move it much (Section 1).
|
|
59
|
+
> …eaningful under the conditions its consumers face, or it misleads [1].
|
|
60
|
+
> …ently no-ops, which invalidated an earlier run until we caught it [1].
|
|
61
|
+
> …irk of these two tools; it follows from the cost structure (Figure 1).
|
|
62
|
+
> … flagship's output change ranges from a growth to a cut (Table 1) [1].
|
|
63
|
+
> … right [1], compressing prompts [1], and caching repeated context [1].
|
|
64
|
+
> …], and that results from a handful of runs need honest statistics [1].
|
|
65
|
+
> …g code [1], and models that memorize answers instead of reasoning [1].
|
|
66
|
+
ok sentences whose main claim is a negation: 37%
|
|
67
|
+
topic positions (whose story is this?): "a fast-growing ecosystem of plugins," | "the most popular single artifact---a" | "we show that such claims," | "efficiency tools are validated on" | "we introduce a reproducible, cost-aware," | "across 140 runs on two" | "output falls far short of" | "the ceiling is also structural:" | "we argue that trustworthy evaluation" | "ai coding agents and their" | "built on the reasoning-and-acting loops" | "assistants of this kind are" | "an unverified performance claim about" | "while the community has invested" | "we focus on the most" | "the flagship example is a" | "naming the commits matters more" | "two separate claims are bundled" | "the first is that the" | "that one has already been" | "the second claim is the" | "nobody had measured it." | "it fails for a reason" | "this paper is about the" | "rq: when a token-efficiency tool" | "the first half is measured," | "our contribution is not that" | "we identify why the headline" | "we build a cost-aware, correctness-gated" | "we measure two widely used" | "output falls far short of" | "the dollar bill does not" | "the broader point is about" | "a modern agent bills tokens" | "a multi-turn agentic session [1]" | "reading the upstream benchmark code" | "the reported number measures something" | "the fix is to measure" | "the compression literature these tools" | "the harness --- the ecosystem" | "we implement the evaluation as" | "figure 1 shows the whole" | "each tool is delivered as" | "a skill that ships as" | "this step is load-bearing: a" | "we use seven self-contained coding" | "the suite deliberately skews short" | "each task is run with" | "the runs date from july" | "the two tools are the" | "for every run we record" | "on a subscription the \`\`bill''" | "we report dollars---the weighting most" | "each task carries a deterministic" | "these are lightweight structural checks" | "a trial where the skill" | "for each task we report" | "we call an effect significant" | "we also note which effects" | "for the bill we report" | "averages taken over a few" | "this protocol is sized to" | "two things are still required" | "the flagship skill (claim 65%)" | "we sign bill changes so" | "for the flagship, the bill" | "either way the answer is" | "the second tool (claim 63%)" | "it is also not significant." | "its one per-task cell that" | "the intervals are wide: at" | "but they exclude anything near" | "whatever room remains is capped" | "correctness." | "both arms passed the correctness" | "a sharper signal hides in" | "this is a short-session effect" | "this is not a quirk" | "both shares below are measured," | "pooled across all tasks and" | "if output is a fraction" | "even a perfect 65% output" | "the advertised 63--75% and the" | "the flagship's other feature trims" | "output is a small fraction" | "we measure cost: latency and" | "we do not claim \`\`these" | "nor do we dispute the" | "our point is that they" | "we label each by its" | "we run five trials per" | "that is itself part of" | "across tasks the flagship's output" | "the honest quantity is therefore" | "our design refutes the advertised" | "what caps those is the" | "a formal equivalence test (tost---a" | "the harness is not toothless," | "at that scale we read" | "we test one model, and" | "anthropic's tiers all share a" | "the ceiling scales linearly with" | "even if output were priced" | "our tasks top out around" | "this cuts in our favor." | "a longer session re-reads a" | "that pushes the ceiling lower," | "one might object that terser" | "but that downstream saving is" | "so it adds a few" | "the compounded ceiling is --," | "the overhead runs the other" | "an always-loaded tool prompt adds" | "that fixed cost amortizes over" | "the durable long-session prediction is" | "an empirical long-session demonstration is" | "every run in the main" | "the harness decides how many" | "a wordier model would raise" | "the ceiling of section 1" | "a pilot on claude haiku" | "nearly double, and still about" | "both runs ship in the" | "a second harness is reachable" | "the flagship now ships wrappers" | "measuring a second harness therefore" | "wrapper list and the rejected" | "we do not study multi-agent" | "we test two tools, and" | "the harness can a/b any" | "the bias runs in our" | "our per-run bill uses the" | "a user can also invalidate" | "(the first two rewrite the" | "that is, they push cost" | "they lower the ceiling and" | "so our clean-caching tasks, if" | "the correctness gate is a" | "so \`\`zero regressions'' bounds gross" | "a stronger gate that runs" | "still, a gate that never" | "without it, a harness would" | "the efficiency-claim failure is a" | "a community norm of cost-aware," | "execution-based, correctness-gated benchmarks such as" | "we keep their correctness-first discipline" | "several lines of work push" | "we take the same stance---price" | "closest to us, [1] argue" | "we turn that lens on" | "a long line of work" | "capability benchmarks show the same" | "we aim the same critique" | "the industry a/b test cited" | "what we add over such" | "the efficiency-tool market runs on" | "these tools trim what the" | "we measured two of the" | "the numbers are not fabricated;" | "the output you are shown" | "trimming the model's replies to" | "we release the measurements with" | "before you install the next" | "the complete harness of section" | "the measurements are released per" | "individual trial rows are not"
|
|
68
|
+
|
|
69
|
+
(thresholds and their sources are in THRESHOLDS at the top of this file)
|
|
70
|
+
"
|
|
71
|
+
`;
|