paperlint 10.4.0 → 10.4.1
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/dist/CLAUDE.md.spec.d.ts.map +1 -1
- package/dist/CLAUDE.md.spec.js +6 -3
- package/dist/CLAUDE.md.spec.js.map +1 -1
- package/dist/adapters/latex/captions.d.ts +9 -0
- package/dist/adapters/latex/captions.d.ts.map +1 -0
- package/dist/adapters/latex/captions.js +40 -0
- package/dist/adapters/latex/captions.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +1 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +6 -0
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/node/list-entries.io.d.ts.map +1 -1
- package/dist/adapters/node/list-entries.io.js +3 -1
- package/dist/adapters/node/list-entries.io.js.map +1 -1
- package/dist/domain/decide-first.d.ts +11 -1
- package/dist/domain/decide-first.d.ts.map +1 -1
- package/dist/domain/decide-first.js +6 -0
- package/dist/domain/decide-first.js.map +1 -1
- package/dist/domain/index-columns.d.ts.map +1 -1
- package/dist/domain/index-columns.js +2 -0
- package/dist/domain/index-columns.js.map +1 -1
- package/dist/domain/study-registry.d.ts +65 -8
- package/dist/domain/study-registry.d.ts.map +1 -1
- package/dist/domain/study-registry.js +63 -19
- package/dist/domain/study-registry.js.map +1 -1
- package/dist/outputs-before-decision-rule.d.ts +1 -1
- package/dist/outputs-before-decision-rule.d.ts.map +1 -1
- package/dist/outputs-before-decision-rule.js +21 -17
- package/dist/outputs-before-decision-rule.js.map +1 -1
- package/dist/ports/list-entries.d.ts +7 -4
- package/dist/ports/list-entries.d.ts.map +1 -1
- package/dist/ports/study-deps.d.ts +14 -0
- package/dist/ports/study-deps.d.ts.map +1 -0
- package/dist/ports/study-deps.js +2 -0
- package/dist/ports/study-deps.js.map +1 -0
- package/dist/ports/study-folder.d.ts +43 -0
- package/dist/ports/study-folder.d.ts.map +1 -0
- package/dist/ports/study-folder.js +2 -0
- package/dist/ports/study-folder.js.map +1 -0
- package/dist/presets.d.ts +1 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +1 -2
- package/dist/presets.js.map +1 -1
- package/dist/studies.d.ts.map +1 -1
- package/dist/studies.js +10 -7
- package/dist/studies.js.map +1 -1
- package/dist/study-folder.d.ts +14 -0
- package/dist/study-folder.d.ts.map +1 -0
- package/dist/study-folder.js +123 -0
- package/dist/study-folder.js.map +1 -0
- package/dist/study-gate.d.ts +1 -1
- package/dist/study-gate.d.ts.map +1 -1
- package/dist/study-gate.js +65 -28
- package/dist/study-gate.js.map +1 -1
- package/dist/study-index-input.d.ts +1 -1
- package/dist/study-index-input.d.ts.map +1 -1
- package/dist/study-index-input.js +8 -3
- package/dist/study-index-input.js.map +1 -1
- package/dist/study-modes.d.ts +38 -27
- package/dist/study-modes.d.ts.map +1 -1
- package/dist/study-modes.js +42 -53
- package/dist/study-modes.js.map +1 -1
- package/dist/study-registry-rule.d.ts +2 -6
- package/dist/study-registry-rule.d.ts.map +1 -1
- package/dist/study-registry-rule.js +32 -41
- package/dist/study-registry-rule.js.map +1 -1
- package/docs/design/study-modes.md +46 -28
- package/docs/rules/repro/outputs-before-decision.md +3 -0
- package/docs/rules/repro/study-registered.md +3 -2
- package/docs/rules/tex/heading-case.md +1 -1
- package/eslint.config.mjs +36 -24
- package/package.json +1 -1
- package/skills/grade-paper-writing/SKILL.md +4 -2
- package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -1
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +16 -3
- package/skills/grade-paper-writing/prose-lint.mjs +127 -95
- package/skills/grade-paper-writing/prose-lint.test.ts +119 -10
- package/src/CLAUDE.md +7 -4
- package/src/CLAUDE.md.spec.ts +6 -3
- package/src/adapters/latex/captions.test.ts +57 -0
- package/src/adapters/latex/captions.ts +56 -0
- package/src/adapters/latex/index.ts +1 -0
- package/src/adapters/latex/parse.ts +7 -0
- package/src/adapters/node/list-entries.io.ts +2 -1
- package/src/adapters/node/list-entries.test.ts +18 -0
- package/src/domain/decide-first.test.ts +33 -0
- package/src/domain/decide-first.ts +16 -1
- package/src/domain/index-columns.test.ts +49 -4
- package/src/domain/index-columns.ts +2 -0
- package/src/domain/study-measurements.test.ts +7 -3
- package/src/domain/study-registry.test.ts +34 -6
- package/src/domain/study-registry.ts +123 -31
- package/src/outputs-before-decision-rule.test.ts +47 -2
- package/src/outputs-before-decision-rule.ts +31 -22
- package/src/ports/list-entries.ts +7 -4
- package/src/ports/study-deps.ts +14 -0
- package/src/ports/study-folder.ts +38 -0
- package/src/presets.ts +1 -2
- package/src/studies-index-modes.test.ts +2 -2
- package/src/studies.ts +19 -10
- package/src/study-folder.test.ts +237 -0
- package/src/study-folder.ts +176 -0
- package/src/study-gate.test.ts +158 -7
- package/src/study-gate.ts +127 -54
- package/src/study-index-input.ts +12 -4
- package/src/study-modes.ts +67 -80
- package/src/study-registry-rule.test.ts +87 -0
- package/src/study-registry-rule.ts +58 -66
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
+
exports[`--flags-only: a metric over its threshold (not a caption) is a finding too, and the report flags the same line 1`] = `
|
|
4
|
+
"✍️ prose-lint — paper.tex — 1 finding(s):
|
|
5
|
+
sentences whose main claim is a negation: 75%
|
|
6
|
+
run prose-lint.mjs on the file without --flags-only for every metric
|
|
7
|
+
"
|
|
8
|
+
`;
|
|
9
|
+
|
|
3
10
|
exports[`--flags-only: an over-long caption flags and exits 1 1`] = `
|
|
4
11
|
"✍️ prose-lint — paper.tex — 2 finding(s):
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
all metadiscourse/1000w 363.6 (Hyland RA baseline 64.9, warn >90)
|
|
13
|
+
figure captions over the limit: 2
|
|
14
|
+
> 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…"
|
|
15
|
+
> figures/fig1.tex: caption is 106 words (warn >100) — a caption this long is a section under a picture
|
|
16
|
+
run prose-lint.mjs on the file without --flags-only for every metric
|
|
8
17
|
"
|
|
9
18
|
`;
|
|
10
19
|
|
|
@@ -37,6 +46,7 @@ ok all metadiscourse/1000w 0 (Hyland RA baseline 64.9, warn >90)
|
|
|
37
46
|
ok "rather than"/instead-of/10,000w 0 (warn >25; not epanorthosis, no published baseline)
|
|
38
47
|
ok sentences ending on a cross-ref/citation/hedge (wasted stress position): 0
|
|
39
48
|
ok sentences whose main claim is a negation: 0%
|
|
49
|
+
ok figure captions over the limit: 0
|
|
40
50
|
topic positions (whose story is this?): "a rendered page." | "it was hyphenated across a" | "next page text here."
|
|
41
51
|
|
|
42
52
|
(thresholds and their sources are in THRESHOLDS at the top of this file)
|
|
@@ -64,6 +74,9 @@ FLAG sentences ending on a cross-ref/citation/hedge (wasted stress position): 12
|
|
|
64
74
|
> …], and that results from a handful of runs need honest statistics [1].
|
|
65
75
|
> …g code [1], and models that memorize answers instead of reasoning [1].
|
|
66
76
|
ok sentences whose main claim is a negation: 37%
|
|
77
|
+
FLAG figure captions over the limit: 2
|
|
78
|
+
> paper.tex: caption sentence of 52 words (warn >40) — "Two things make it different from how efficiency tools are usually measured: the tool is installed the way a u…"
|
|
79
|
+
> paper.tex: caption sentence of 44 words (warn >40) — "The advertised 65% is an output-token reduction; because output is only math of the dollar cost, even a perfec…"
|
|
67
80
|
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
81
|
|
|
69
82
|
(thresholds and their sources are in THRESHOLDS at the top of this file)
|
|
@@ -17,17 +17,18 @@
|
|
|
17
17
|
*
|
|
18
18
|
* A `.txt` is the text of the rendered PDF — the page a reviewer actually reads.
|
|
19
19
|
*
|
|
20
|
-
* The figure captions are measured too
|
|
20
|
+
* The figure captions are measured too, in the report: every `\caption{}` in `paper.tex` and the
|
|
21
21
|
* files it includes, or, beside a rendered page, in `figures/*.tex`.
|
|
22
22
|
*
|
|
23
|
-
* Advisory:
|
|
23
|
+
* Advisory: the report prints every check and exits 0. `--flags-only` prints the checks the report
|
|
24
|
+
* marks FLAG, each with the lines under it, and nothing on a clean paper; it exits 1 when there is
|
|
25
|
+
* one — the form for a hook.
|
|
24
26
|
*/
|
|
25
27
|
import { paperProse } from "#src/paper-prose";
|
|
26
28
|
import { paperRecord } from "#src/paper-record";
|
|
27
|
-
import { latexReader } from "#src/adapters/latex/index";
|
|
29
|
+
import { captionsOf, latexReader, parseLatex } from "#src/adapters/latex/index";
|
|
28
30
|
import { nodeFiles } from "#src/adapters/node/index";
|
|
29
31
|
import { sourcesCodec } from "#src/adapters/sources-record/index";
|
|
30
|
-
import { texToMdast } from "#eslint-rules/latex-language";
|
|
31
32
|
|
|
32
33
|
const THRESHOLDS = {
|
|
33
34
|
metadiscourseTotalPer1000: {
|
|
@@ -363,77 +364,116 @@ function figureFiles(pagePath, fs, path) {
|
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
/**
|
|
366
|
-
* Every `\caption
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* for the rules that read notes. So a caption is skipped when its argument is blank in the
|
|
370
|
-
* projection or it starts inside a comment: a quoted or commented-out `\caption` is not one.
|
|
367
|
+
* Every `\caption` of `sources` (`{ file, text }`) a reader sees, measured. The captions come from the
|
|
368
|
+
* parsed source (`captionsOf`, src/adapters/latex/captions.ts): its optional argument, star and spacing
|
|
369
|
+
* are the parser's, and a caption that is commented out, in verbatim or in a macro's definition is not one.
|
|
371
370
|
*/
|
|
372
371
|
function captions(sources) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
depth = 1,
|
|
382
|
-
j = i;
|
|
383
|
-
while (j < s.length && depth) {
|
|
384
|
-
if (s[j] === "{") depth++;
|
|
385
|
-
else if (s[j] === "}") depth--;
|
|
386
|
-
j++;
|
|
387
|
-
}
|
|
388
|
-
if (
|
|
389
|
-
!/\S/.test(seen.slice(i, j - 1)) ||
|
|
390
|
-
comments.some(([a, b]) => m.index >= a && m.index < b)
|
|
391
|
-
)
|
|
392
|
-
continue;
|
|
393
|
-
const text = s
|
|
394
|
-
.slice(i, j - 1)
|
|
395
|
-
.replace(/\\[a-zA-Z]+\s*/g, " ")
|
|
396
|
-
.replace(/[{}]/g, " ")
|
|
397
|
-
.replace(/---/g, " ")
|
|
398
|
-
.replace(/\s+/g, " ")
|
|
399
|
-
.trim();
|
|
400
|
-
if (!text) continue;
|
|
401
|
-
// A non-empty caption splits into at least one sentence.
|
|
402
|
-
const sents = splitSentences(text);
|
|
403
|
-
out.push({
|
|
404
|
-
file: f,
|
|
405
|
-
words: words(text),
|
|
372
|
+
return sources.flatMap(({ file, text }) =>
|
|
373
|
+
captionsOf(parseLatex(text)).map((raw) => {
|
|
374
|
+
// A dash with no spaces round it joins two words in the count, as it never did here.
|
|
375
|
+
const caption = raw.replace(/---/g, " ");
|
|
376
|
+
const sents = splitSentences(caption);
|
|
377
|
+
return {
|
|
378
|
+
file,
|
|
379
|
+
words: words(caption),
|
|
406
380
|
longest: Math.max(...sents.map(words)),
|
|
407
381
|
longestSentence: sents.slice().sort((a, b) => words(b) - words(a))[0],
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
382
|
+
};
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The findings on the captions of the file `f`: one line per caption sentence or caption over its
|
|
389
|
+
* limit. A paper's captions are in its own files — paper.tex and what it includes, wherever a figure
|
|
390
|
+
* is set; a rendered page has the `.tex` of `figures/` beside it. The report and `--flags-only` both
|
|
391
|
+
* read them from here.
|
|
392
|
+
*/
|
|
393
|
+
function captionFindings(f, tex, fs, path) {
|
|
394
|
+
return captions(tex ? tex.files : figureFiles(f, fs, path)).flatMap((c) =>
|
|
395
|
+
CAPTION_LIMITS.filter(over(c)).map(say(c)),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The two limits a caption is held to: the `THRESHOLDS` key, what of a caption it is measured on, and
|
|
401
|
+
* how a finding reads. A caption is over a limit when its measure exceeds the threshold's `warn`.
|
|
402
|
+
*/
|
|
403
|
+
const CAPTION_LIMITS = [
|
|
404
|
+
{
|
|
405
|
+
key: "captionSentenceWords",
|
|
406
|
+
measure: (c) => c.longest,
|
|
407
|
+
say: (c, warn) =>
|
|
408
|
+
`${c.file}: caption sentence of ${c.longest} words (warn >${warn}) — "${c.longestSentence.slice(0, 110)}…"`,
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
key: "captionWords",
|
|
412
|
+
measure: (c) => c.words,
|
|
413
|
+
say: (c, warn) =>
|
|
414
|
+
`${c.file}: caption is ${c.words} words (warn >${warn}) — a caption this long is a section under a picture`,
|
|
415
|
+
},
|
|
416
|
+
];
|
|
417
|
+
const over = (c) => (limit) => limit.measure(c) > THRESHOLDS[limit.key].warn;
|
|
418
|
+
const say = (c) => (limit) => limit.say(c, THRESHOLDS[limit.key].warn);
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Every line of the report as data, in the order it is printed: `ok` is true (passed), false (a
|
|
422
|
+
* finding, marked `FLAG`) or null (a figure, no check), `under` the lines that name the offenders.
|
|
423
|
+
* The report prints all of it; `--flags-only` prints the findings: one list, two renderings.
|
|
424
|
+
*/
|
|
425
|
+
function checks(r, captionLines) {
|
|
426
|
+
const T = THRESHOLDS;
|
|
427
|
+
return [
|
|
428
|
+
{
|
|
429
|
+
ok: r.metadiscoursePer1000 <= T.metadiscourseTotalPer1000.warn,
|
|
430
|
+
text: `all metadiscourse/1000w ${r.metadiscoursePer1000} (Hyland RA baseline 64.9, warn >${T.metadiscourseTotalPer1000.warn})`,
|
|
431
|
+
under: [],
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
ok: null,
|
|
435
|
+
text: `nominalisations/1000w ${r.nominalisationsPer1000}`,
|
|
436
|
+
under: [],
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
ok: r.contrastivePer10k <= T.contrastivePer10k.warn,
|
|
440
|
+
text: `"rather than"/instead-of/10,000w ${r.contrastivePer10k} (warn >${T.contrastivePer10k.warn}; not epanorthosis, no published baseline)`,
|
|
441
|
+
under: [],
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
ok: r.wastedStress.length === 0,
|
|
445
|
+
text: `sentences ending on a cross-ref/citation/hedge (wasted stress position): ${r.wastedStress.length}`,
|
|
446
|
+
under: r.wastedStress.map((s) => `…${s.slice(-70)}`),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
ok: r.negatedSentencePct <= T.negatedMainClausePct.warn,
|
|
450
|
+
text: `sentences whose main claim is a negation: ${r.negatedSentencePct}%`,
|
|
451
|
+
under: [],
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
ok: captionLines.length === 0,
|
|
455
|
+
text: `figure captions over the limit: ${captionLines.length}`,
|
|
456
|
+
under: captionLines,
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
ok: null,
|
|
460
|
+
text: `topic positions (whose story is this?): ${r.topics.map((t) => `"${t}"`).join(" | ")}`,
|
|
461
|
+
under: [],
|
|
462
|
+
},
|
|
463
|
+
];
|
|
412
464
|
}
|
|
413
465
|
|
|
414
|
-
|
|
415
|
-
|
|
466
|
+
const MARK = { true: "ok ", false: "FLAG", null: " " };
|
|
467
|
+
|
|
468
|
+
function report(name, r, rows) {
|
|
416
469
|
console.log(`\n=== ${name} — ${r.words} words, ${r.sentences} sentences ===`);
|
|
417
470
|
console.log(
|
|
418
471
|
`sentence lengths ${JSON.stringify(r.sentenceLens)} mean ${r.meanLen} sd ${r.sdLen} CV ${r.cv}`,
|
|
419
472
|
);
|
|
420
|
-
|
|
421
|
-
`${
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
console.log(
|
|
425
|
-
`${flag(r.contrastivePer10k <= THRESHOLDS.contrastivePer10k.warn)} "rather than"/instead-of/10,000w ${r.contrastivePer10k} (warn >${THRESHOLDS.contrastivePer10k.warn}; not epanorthosis, no published baseline)`,
|
|
426
|
-
);
|
|
427
|
-
console.log(
|
|
428
|
-
`${flag(r.wastedStress.length === 0)} sentences ending on a cross-ref/citation/hedge (wasted stress position): ${r.wastedStress.length}`,
|
|
429
|
-
);
|
|
430
|
-
r.wastedStress.forEach((s) => console.log(` > …${s.slice(-70)}`));
|
|
431
|
-
console.log(
|
|
432
|
-
`${flag(r.negatedSentencePct <= THRESHOLDS.negatedMainClausePct.warn)} sentences whose main claim is a negation: ${r.negatedSentencePct}%`,
|
|
433
|
-
);
|
|
434
|
-
console.log(
|
|
435
|
-
` topic positions (whose story is this?): ${r.topics.map((t) => `"${t}"`).join(" | ")}`,
|
|
436
|
-
);
|
|
473
|
+
rows.forEach(({ ok, text, under }) => {
|
|
474
|
+
console.log(`${MARK[ok]} ${text}`);
|
|
475
|
+
under.forEach((l) => console.log(` > ${l}`));
|
|
476
|
+
});
|
|
437
477
|
}
|
|
438
478
|
|
|
439
479
|
const args = process.argv.slice(2);
|
|
@@ -474,14 +514,14 @@ function pdfTextOnly(t) {
|
|
|
474
514
|
.replace(/\f/g, "\n\n");
|
|
475
515
|
}
|
|
476
516
|
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
517
|
+
// `--flags-only` prints the findings of the report (every line it marks FLAG, with the lines under
|
|
518
|
+
// it) and nothing on a clean paper, and exits 1 when there is one: the form for a hook. The report
|
|
519
|
+
// prints all of `checks` and exits 0.
|
|
520
|
+
const mode = args.includes("--flags-only")
|
|
521
|
+
? "flags"
|
|
522
|
+
: args.includes("--headings")
|
|
523
|
+
? "headings"
|
|
524
|
+
: "report";
|
|
485
525
|
let flagged = 0;
|
|
486
526
|
for (const f of args.filter((a) => !a.startsWith("--"))) {
|
|
487
527
|
const raw = fs.readFileSync(f, "utf8");
|
|
@@ -509,33 +549,25 @@ for (const f of args.filter((a) => !a.startsWith("--"))) {
|
|
|
509
549
|
process.exit(2);
|
|
510
550
|
}
|
|
511
551
|
const r = analyse(text, sentences);
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (c.longest > THRESHOLDS.captionSentenceWords.warn)
|
|
518
|
-
lines.push(
|
|
519
|
-
` ${c.file}: caption sentence of ${c.longest} words (warn >${THRESHOLDS.captionSentenceWords.warn}) — "${c.longestSentence.slice(0, 110)}…"`,
|
|
520
|
-
);
|
|
521
|
-
if (c.words > THRESHOLDS.captionWords.warn)
|
|
522
|
-
lines.push(
|
|
523
|
-
` ${c.file}: caption is ${c.words} words (warn >${THRESHOLDS.captionWords.warn}) — a caption this long is a section under a picture`,
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
if (lines.length) {
|
|
527
|
-
flagged += lines.length;
|
|
552
|
+
const rows = () => checks(r, captionFindings(f, tex, fs, path));
|
|
553
|
+
if (mode === "flags") {
|
|
554
|
+
const found = rows().filter((row) => row.ok === false);
|
|
555
|
+
if (found.length) {
|
|
556
|
+
flagged += found.length;
|
|
528
557
|
// The header STATES the count, so a caller need not count output lines — header and footer
|
|
529
558
|
// are not findings.
|
|
530
559
|
console.error(
|
|
531
|
-
`✍️ prose-lint — ${f.split("/").pop()} — ${
|
|
560
|
+
`✍️ prose-lint — ${f.split("/").pop()} — ${found.length} finding(s):`,
|
|
532
561
|
);
|
|
533
|
-
|
|
562
|
+
found.forEach(({ text, under }) => {
|
|
563
|
+
console.error(` ${text}`);
|
|
564
|
+
under.forEach((l) => console.error(` > ${l}`));
|
|
565
|
+
});
|
|
534
566
|
console.error(
|
|
535
|
-
" run prose-lint.mjs on the file without --flags-only for
|
|
567
|
+
" run prose-lint.mjs on the file without --flags-only for every metric",
|
|
536
568
|
);
|
|
537
569
|
}
|
|
538
|
-
} else if (
|
|
570
|
+
} else if (mode === "headings") {
|
|
539
571
|
// The whole-document question, printed as one list. Not a gate: "does this heading name a
|
|
540
572
|
// concrete noun" is judgement and pretending otherwise would be the exact failure this file
|
|
541
573
|
// exists to stop. What was missing is cheaper and was the real cause — nobody had ever seen the
|
|
@@ -558,10 +590,10 @@ for (const f of args.filter((a) => !a.startsWith("--"))) {
|
|
|
558
590
|
'a linter, a configuration, a repository, a rule — not "answers", "kinds", "moves".',
|
|
559
591
|
);
|
|
560
592
|
} else {
|
|
561
|
-
report(f.split("/").pop(), r);
|
|
593
|
+
report(f.split("/").pop(), r, rows());
|
|
562
594
|
}
|
|
563
595
|
}
|
|
564
|
-
if (
|
|
596
|
+
if (mode !== "flags")
|
|
565
597
|
console.log(
|
|
566
598
|
"\n(thresholds and their sources are in THRESHOLDS at the top of this file)",
|
|
567
599
|
);
|
|
@@ -8,7 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import { dirname } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { expect, test } from "vitest";
|
|
11
|
-
import { cpSync } from "node:fs";
|
|
11
|
+
import { cpSync, readFileSync } from "node:fs";
|
|
12
12
|
import { layRecord, recordedTree } from "../../test/recorded-fixture.ts";
|
|
13
13
|
import { runNode, useTempDir, writeTree } from "../../test/support.ts";
|
|
14
14
|
|
|
@@ -62,6 +62,16 @@ const TREE = {
|
|
|
62
62
|
"commented/paper.tex": tex(
|
|
63
63
|
`Prose here.\n% \\caption{Commented ${LONG_CAPTION_SENTENCE}.}\n\\begin{verbatim}\n\\caption{Quoted ${LONG_CAPTION_SENTENCE}.}\n\\end{verbatim}\n\\begin{figure}\\caption{Real ${LONG_CAPTION_SENTENCE}.}\\end{figure}`,
|
|
64
64
|
),
|
|
65
|
+
// The caption command with an optional argument, and with a space before its brace.
|
|
66
|
+
"optarg/paper.tex": tex(
|
|
67
|
+
`Prose here. \\begin{figure}\\caption[Short]{${LONG_CAPTION_SENTENCE}.}\\end{figure}`,
|
|
68
|
+
),
|
|
69
|
+
"spaced/paper.tex": tex(
|
|
70
|
+
`Prose here. \\begin{figure}\\caption {${LONG_CAPTION_SENTENCE}.}\\end{figure}`,
|
|
71
|
+
),
|
|
72
|
+
"negated/paper.tex": tex(
|
|
73
|
+
"It is not fast. It is not small. It is not red. Nothing here is plain.",
|
|
74
|
+
),
|
|
65
75
|
"ends-on-marks/paper.tex": tex(
|
|
66
76
|
"The method holds on every input~\\cite{knuth}. The proof is given in full (Section~\\ref{proof}). The result is new.",
|
|
67
77
|
),
|
|
@@ -83,6 +93,9 @@ const READ: Readonly<Record<string, readonly string[]>> = {
|
|
|
83
93
|
inline: ["paper.tex", "sec/results.tex"],
|
|
84
94
|
commented: ["paper.tex"],
|
|
85
95
|
"ends-on-marks": ["paper.tex"],
|
|
96
|
+
negated: ["paper.tex"],
|
|
97
|
+
optarg: ["paper.tex"],
|
|
98
|
+
spaced: ["paper.tex"],
|
|
86
99
|
empty: ["paper.tex"],
|
|
87
100
|
};
|
|
88
101
|
writeTree(
|
|
@@ -182,6 +195,23 @@ test("--flags-only: an over-long caption flags and exits 1", () => {
|
|
|
182
195
|
expect(r.stderr).toMatchSnapshot();
|
|
183
196
|
});
|
|
184
197
|
|
|
198
|
+
test("--flags-only: a metric over its threshold (not a caption) is a finding too, and the report flags the same line", () => {
|
|
199
|
+
const file = join(root, "negated", "paper.tex");
|
|
200
|
+
const only = runNode(SCRIPT, ["--flags-only", file]);
|
|
201
|
+
const report = runNode(SCRIPT, [file]);
|
|
202
|
+
// Guards: the mode promised "print nothing on a clean paper, any output means a finding" and
|
|
203
|
+
// looked only at captions; the metrics the report FLAGs are findings of the same kind.
|
|
204
|
+
assert.deepEqual(
|
|
205
|
+
{ status: only.status, stdout: only.stdout },
|
|
206
|
+
{ status: 1, stdout: "" },
|
|
207
|
+
);
|
|
208
|
+
expect(only.stderr).toMatchSnapshot();
|
|
209
|
+
assert.match(
|
|
210
|
+
report.stdout,
|
|
211
|
+
/^FLAG sentences whose main claim is a negation: 75%$/m,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
185
215
|
test("--flags-only: a paper with no figures directory is clean, exit 0 and silent", () => {
|
|
186
216
|
assert.deepEqual(
|
|
187
217
|
runNode(SCRIPT, ["--flags-only", join(root, "clean", "paper.tex")]),
|
|
@@ -248,17 +278,13 @@ test("🔴 --flags-only reads the captions of the paper.tex and of the files it
|
|
|
248
278
|
stdout: r.stdout,
|
|
249
279
|
flagged: r.stderr
|
|
250
280
|
.split("\n")
|
|
251
|
-
.filter((l) => l.startsWith("
|
|
281
|
+
.filter((l) => l.startsWith(" > "))
|
|
252
282
|
.map((l) => l.split(":")[0]),
|
|
253
283
|
},
|
|
254
284
|
{
|
|
255
285
|
status: 1,
|
|
256
286
|
stdout: "",
|
|
257
|
-
flagged: [
|
|
258
|
-
" paper.tex",
|
|
259
|
-
" sec/results.tex",
|
|
260
|
-
" run prose-lint.mjs on the file without --flags-only for the sentences",
|
|
261
|
-
],
|
|
287
|
+
flagged: [" > paper.tex", " > sec/results.tex"],
|
|
262
288
|
},
|
|
263
289
|
);
|
|
264
290
|
});
|
|
@@ -274,8 +300,14 @@ test("--flags-only on a rendered page reads the captions in figures/*.tex beside
|
|
|
274
300
|
join(root, "rendered", "page.txt"),
|
|
275
301
|
]);
|
|
276
302
|
assert.deepEqual(
|
|
277
|
-
{
|
|
278
|
-
|
|
303
|
+
{
|
|
304
|
+
status: r.status,
|
|
305
|
+
first: r.stderr
|
|
306
|
+
.split("\n")
|
|
307
|
+
.find((l) => l.startsWith(" > "))
|
|
308
|
+
?.split(":")[0],
|
|
309
|
+
},
|
|
310
|
+
{ status: 1, first: " > figures/f.tex" },
|
|
279
311
|
);
|
|
280
312
|
// …and a page with no figures/ beside it has no caption to flag.
|
|
281
313
|
assert.deepEqual(runNode(SCRIPT, ["--flags-only", join(root, "page.txt")]), {
|
|
@@ -296,9 +328,86 @@ test("🔴 a caption commented out or quoted in verbatim is not measured; a real
|
|
|
296
328
|
status: r.status,
|
|
297
329
|
findings: r.stderr
|
|
298
330
|
.split("\n")
|
|
299
|
-
.filter((l) => l.startsWith("
|
|
331
|
+
.filter((l) => l.startsWith(" > paper.tex:"))
|
|
300
332
|
.map((l) => /— "(\w+)/.exec(l)?.[1]),
|
|
301
333
|
},
|
|
302
334
|
{ status: 1, findings: ["Real"] },
|
|
303
335
|
);
|
|
304
336
|
});
|
|
337
|
+
|
|
338
|
+
/** The `prose-lint.mjs` invocations SKILL.md documents (Step 6), their arguments with `<paper-dir>` filled in. */
|
|
339
|
+
const documentedInvocations = (paperDir: string): readonly string[][] =>
|
|
340
|
+
readFileSync(join(dirname(SCRIPT), "SKILL.md"), "utf8")
|
|
341
|
+
.split("\n")
|
|
342
|
+
.flatMap((line) => {
|
|
343
|
+
const m =
|
|
344
|
+
/node \$\{CLAUDE_SKILL_DIR\}\/prose-lint\.mjs ([^#]+?)\s*(#|$)/.exec(
|
|
345
|
+
line,
|
|
346
|
+
);
|
|
347
|
+
return m?.[1] === undefined
|
|
348
|
+
? []
|
|
349
|
+
: [m[1].replaceAll("<paper-dir>", paperDir).split(/\s+/)];
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("🔴 the invocation SKILL.md documents reports an over-long caption, with no flag the skill does not name", () => {
|
|
353
|
+
const documented = documentedInvocations(join(root, "p"));
|
|
354
|
+
// Guards: the report is the documented way to run the checks (Step 6), and captions were
|
|
355
|
+
// measured only under an undocumented flag — an over-long caption produced no finding.
|
|
356
|
+
const report = documented.find((a) => !a.includes("--headings"));
|
|
357
|
+
assert.ok(report !== undefined, "SKILL.md documents a report invocation");
|
|
358
|
+
const r = runNode(SCRIPT, report);
|
|
359
|
+
assert.deepEqual(
|
|
360
|
+
{
|
|
361
|
+
status: r.status,
|
|
362
|
+
flagged: r.stdout
|
|
363
|
+
.split("\n")
|
|
364
|
+
.filter(
|
|
365
|
+
(l) => l.startsWith("FLAG figure captions") || l.includes("caption"),
|
|
366
|
+
),
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
status: 0,
|
|
370
|
+
flagged: [
|
|
371
|
+
"FLAG figure captions over the limit: 2",
|
|
372
|
+
' > 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…"',
|
|
373
|
+
" > figures/fig1.tex: caption is 106 words (warn >100) — a caption this long is a section under a picture",
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("the report says a paper whose captions are within the limits is clean on that line", () => {
|
|
380
|
+
const r = runNode(SCRIPT, [join(root, "clean", "paper.tex")]);
|
|
381
|
+
// Guards: the line is printed on a clean paper too, so its absence is never read as a pass.
|
|
382
|
+
assert.match(r.stdout, /^ok {3}figure captions over the limit: 0$/m);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("🔴 a caption with an optional argument, or a space before its brace, is measured", () => {
|
|
386
|
+
const found = (dir: string) => {
|
|
387
|
+
const r = runNode(SCRIPT, [join(root, dir, "paper.tex")]);
|
|
388
|
+
return [
|
|
389
|
+
r.status,
|
|
390
|
+
/^FLAG figure captions over the limit: (\d+)$/m.exec(r.stdout)?.[1],
|
|
391
|
+
];
|
|
392
|
+
};
|
|
393
|
+
// Guards: the scan matched `\caption{` only, so `\caption[Short]{…}` and `\caption {…}` were never
|
|
394
|
+
// read and the report printed "ok ... 0" for a long caption.
|
|
395
|
+
expect([found("optarg"), found("spaced")]).toEqual([
|
|
396
|
+
[0, "1"],
|
|
397
|
+
[0, "1"],
|
|
398
|
+
]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("🔴 the caption of the package's own p1 fixture, which has an optional argument, is reported", () => {
|
|
402
|
+
const p1 = join(
|
|
403
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
404
|
+
"..",
|
|
405
|
+
"..",
|
|
406
|
+
"fixtures",
|
|
407
|
+
"paper-sources",
|
|
408
|
+
"p1",
|
|
409
|
+
"paper.tex",
|
|
410
|
+
);
|
|
411
|
+
const r = runNode(SCRIPT, ["--flags-only", p1]);
|
|
412
|
+
assert.match(r.stderr, /— "Optional-arg caption this sentence/);
|
|
413
|
+
});
|
package/src/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- vigiles:sha256:
|
|
1
|
+
<!-- vigiles:sha256:599c83e36edde981 compiled from src/CLAUDE.md.spec.ts -->
|
|
2
2
|
|
|
3
3
|
# CLAUDE.md
|
|
4
4
|
|
|
@@ -21,12 +21,15 @@ test — a parser for banal's JSON is pure and is still banal's.
|
|
|
21
21
|
|
|
22
22
|
| element | where | holds | may import |
|
|
23
23
|
| --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
-
| `domain` | `src/domain/` | values and decisions in the paper's vocabulary | `domain`;
|
|
25
|
-
| `port` | `src/ports/` | interfaces **shaped by what the inside needs**, never by what a tool does (Graça: "created to fit the Application Core needs and not simply mimic the tools APIs") | `domain`, `port`; the same
|
|
26
|
-
| `app` | `src/*.ts` | use cases over ports; **no I/O defaults** — a use case without its deps does not compile | `domain`, `port`, `app`, the package's `.mjs`;
|
|
24
|
+
| `domain` | `src/domain/` | values and decisions in the paper's vocabulary | `domain`; any package or Node built-in that is no tool package (`TOOL_PACKAGES`) and no effect |
|
|
25
|
+
| `port` | `src/ports/` | interfaces **shaped by what the inside needs**, never by what a tool does (Graça: "created to fit the Application Core needs and not simply mimic the tools APIs") | `domain`, `port`; the same as the domain |
|
|
26
|
+
| `app` | `src/*.ts` | use cases over ports; **no I/O defaults** — a use case without its deps does not compile | `domain`, `port`, `app`, the package's `.mjs`; the same externals as the domain |
|
|
27
27
|
| `adapter` | `src/adapters/<tool>/` | everything that knows `<tool>` exists, pure parsers included; used through its `index.ts` | `domain`, `port`, its own folder, its tool's packages, the package's `.mjs`. **Never** `app`, **never** another adapter |
|
|
28
28
|
| `root` | `src/cli.ts` | reads `process.*`, builds adapters, wires ports | anything |
|
|
29
29
|
|
|
30
|
+
A package that exists because of one external tool goes in `TOOL_PACKAGES` (`eslint.config.mjs`), in the same PR that
|
|
31
|
+
adds its adapter; a pure utility library (`remeda`, `zod`, `ts-essentials`) needs nothing, and effects are axis B's.
|
|
32
|
+
|
|
30
33
|
Folders are ELEMENTS of `eslint-plugin-boundaries`, single files are CATEGORIES (`root`, `app`,
|
|
31
34
|
`test`, `io`): v7 matches element patterns as folders. Everyone — tests included — reaches an
|
|
32
35
|
adapter through its `index.ts`; the files inside one adapter import each other freely.
|
package/src/CLAUDE.md.spec.ts
CHANGED
|
@@ -18,12 +18,15 @@ test — a parser for banal's JSON is pure and is still banal's.
|
|
|
18
18
|
|
|
19
19
|
| element | where | holds | may import |
|
|
20
20
|
| --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
-
| \`domain\` | \`src/domain/\` | values and decisions in the paper's vocabulary | \`domain\`;
|
|
22
|
-
| \`port\` | \`src/ports/\` | interfaces **shaped by what the inside needs**, never by what a tool does (Graça: "created to fit the Application Core needs and not simply mimic the tools APIs") | \`domain\`, \`port\`; the same
|
|
23
|
-
| \`app\` | \`src/*.ts\` | use cases over ports; **no I/O defaults** — a use case without its deps does not compile | \`domain\`, \`port\`, \`app\`, the package's \`.mjs\`;
|
|
21
|
+
| \`domain\` | \`src/domain/\` | values and decisions in the paper's vocabulary | \`domain\`; any package or Node built-in that is no tool package (\`TOOL_PACKAGES\`) and no effect |
|
|
22
|
+
| \`port\` | \`src/ports/\` | interfaces **shaped by what the inside needs**, never by what a tool does (Graça: "created to fit the Application Core needs and not simply mimic the tools APIs") | \`domain\`, \`port\`; the same as the domain |
|
|
23
|
+
| \`app\` | \`src/*.ts\` | use cases over ports; **no I/O defaults** — a use case without its deps does not compile | \`domain\`, \`port\`, \`app\`, the package's \`.mjs\`; the same externals as the domain |
|
|
24
24
|
| \`adapter\` | \`src/adapters/<tool>/\` | everything that knows \`<tool>\` exists, pure parsers included; used through its \`index.ts\` | \`domain\`, \`port\`, its own folder, its tool's packages, the package's \`.mjs\`. **Never** \`app\`, **never** another adapter |
|
|
25
25
|
| \`root\` | \`src/cli.ts\` | reads \`process.*\`, builds adapters, wires ports | anything |
|
|
26
26
|
|
|
27
|
+
A package that exists because of one external tool goes in \`TOOL_PACKAGES\` (\`eslint.config.mjs\`), in the same PR that
|
|
28
|
+
adds its adapter; a pure utility library (\`remeda\`, \`zod\`, \`ts-essentials\`) needs nothing, and effects are axis B's.
|
|
29
|
+
|
|
27
30
|
Folders are ELEMENTS of \`eslint-plugin-boundaries\`, single files are CATEGORIES (\`root\`, \`app\`,
|
|
28
31
|
\`test\`, \`io\`): v7 matches element patterns as folders. Everyone — tests included — reaches an
|
|
29
32
|
adapter through its \`index.ts\`; the files inside one adapter import each other freely.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The captions of a LaTeX source (`captionsOf`) on real parses: the mandatory argument of `\caption`,
|
|
3
|
+
* whatever optional argument, star or space precedes it, and none that a reader of the PDF never sees.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { captionsOf, parseLatex } from "./index.ts";
|
|
7
|
+
|
|
8
|
+
const captions = (src: string): readonly string[] =>
|
|
9
|
+
captionsOf(parseLatex(src));
|
|
10
|
+
|
|
11
|
+
describe("captionsOf", () => {
|
|
12
|
+
it("the text of a caption: formatting kept as words, whitespace collapsed", () => {
|
|
13
|
+
expect(
|
|
14
|
+
captions(
|
|
15
|
+
"\\begin{figure}\\caption{\\textbf{Big.} Some words.}\\end{figure}",
|
|
16
|
+
),
|
|
17
|
+
).toEqual(["Big. Some words."]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("with an optional argument, a star, or a space before the brace", () => {
|
|
21
|
+
// Guards: a scan for `\caption{` read none of these.
|
|
22
|
+
expect(
|
|
23
|
+
captions(
|
|
24
|
+
"\\caption[Short]{With option.} \\caption*{Starred.} \\caption {Spaced.}\n\\caption\n {Next line.}",
|
|
25
|
+
),
|
|
26
|
+
).toEqual(["With option.", "Starred.", "Spaced.", "Next line."]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("in order, in the files' text as given; a caption with no words is left out", () => {
|
|
30
|
+
expect(
|
|
31
|
+
captions(
|
|
32
|
+
"\\caption{One.}\\caption{}\\caption{\\centering}\\caption{Two.}",
|
|
33
|
+
),
|
|
34
|
+
).toEqual(["One.", "Two."]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("not one that is commented out, in verbatim, or in a macro's definition", () => {
|
|
38
|
+
const src = [
|
|
39
|
+
"% \\caption{Commented.}",
|
|
40
|
+
"\\begin{verbatim}\\caption{Quoted.}\\end{verbatim}",
|
|
41
|
+
"\\newcommand{\\fig}{\\caption{Defined.}}",
|
|
42
|
+
"\\caption{Real.}",
|
|
43
|
+
].join("\n");
|
|
44
|
+
expect(captions(src)).toEqual(["Real."]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("captionsOf — what a reader sees", () => {
|
|
49
|
+
it("a formula counts as one word and an escaped character is itself", () => {
|
|
50
|
+
// Guards: dropping them made a caption of 54 words one of 40 and let it pass.
|
|
51
|
+
expect(
|
|
52
|
+
captions(
|
|
53
|
+
"\\caption{Cut by 65\\% of $20\\%$ and Welch $p$; \\textbf{bold} \\& more.}",
|
|
54
|
+
),
|
|
55
|
+
).toEqual(["Cut by 65% of math and Welch math; bold & more."]);
|
|
56
|
+
});
|
|
57
|
+
});
|