paperlint 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/adapters/latex/index.d.ts +1 -1
  2. package/dist/adapters/latex/index.d.ts.map +1 -1
  3. package/dist/adapters/latex/index.js +3 -2
  4. package/dist/adapters/latex/index.js.map +1 -1
  5. package/dist/adapters/latex/prose.d.ts +6 -1
  6. package/dist/adapters/latex/prose.d.ts.map +1 -1
  7. package/dist/adapters/latex/prose.js +142 -31
  8. package/dist/adapters/latex/prose.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +15 -18
  11. package/dist/cli.js.map +1 -1
  12. package/dist/domain/register.d.ts +96 -0
  13. package/dist/domain/register.d.ts.map +1 -0
  14. package/dist/domain/register.js +136 -0
  15. package/dist/domain/register.js.map +1 -0
  16. package/dist/domain/tex-document.d.ts +19 -0
  17. package/dist/domain/tex-document.d.ts.map +1 -1
  18. package/dist/domain/tex-document.js +8 -3
  19. package/dist/domain/tex-document.js.map +1 -1
  20. package/dist/ports/latex.d.ts +6 -1
  21. package/dist/ports/latex.d.ts.map +1 -1
  22. package/dist/presets.d.ts +3 -0
  23. package/dist/presets.d.ts.map +1 -1
  24. package/dist/presets.js +4 -1
  25. package/dist/presets.js.map +1 -1
  26. package/dist/register-bands.d.ts +37 -0
  27. package/dist/register-bands.d.ts.map +1 -0
  28. package/dist/register-bands.js +196 -0
  29. package/dist/register-bands.js.map +1 -0
  30. package/dist/register.d.ts +4 -2
  31. package/dist/register.d.ts.map +1 -1
  32. package/dist/register.js +6 -45
  33. package/dist/register.js.map +1 -1
  34. package/dist/tex-requirements.d.ts +6 -0
  35. package/dist/tex-requirements.d.ts.map +1 -1
  36. package/dist/tex-requirements.js +26 -6
  37. package/dist/tex-requirements.js.map +1 -1
  38. package/dist/tex-venue-rules.d.ts +1 -1
  39. package/dist/tex-venue-rules.d.ts.map +1 -1
  40. package/dist/tex-venue-rules.js.map +1 -1
  41. package/docs/rules/tex/claim-emphasis.md +71 -0
  42. package/docs/rules/tex/contrast-frames.md +100 -0
  43. package/docs/rules/tex/relation-markers.md +72 -0
  44. package/docs/rules.md +46 -42
  45. package/package.json +1 -1
  46. package/presets/aidc.jsonc +50 -0
  47. package/presets/venue-profile.schema.json +48 -1
  48. package/skills/grade-paper-writing/SKILL.md +59 -27
  49. package/skills/grade-paper-writing/SKILL.md.spec.ts +58 -26
  50. package/src/adapters/latex/index.test.ts +91 -0
  51. package/src/adapters/latex/index.ts +3 -2
  52. package/src/adapters/latex/prose.ts +188 -58
  53. package/src/cli.ts +19 -18
  54. package/src/domain/register.test.ts +153 -0
  55. package/src/domain/register.ts +261 -0
  56. package/src/domain/tex-document.ts +37 -3
  57. package/src/ports/latex.ts +6 -0
  58. package/src/presets.ts +7 -1
  59. package/src/register-bands.test.ts +327 -0
  60. package/src/register-bands.ts +291 -0
  61. package/src/register.ts +7 -37
  62. package/src/rule-cases.test.ts +67 -17
  63. package/src/tex-requirements.ts +47 -8
  64. package/src/tex-venue-rules.ts +7 -1
@@ -168,6 +168,14 @@ const FORMAL = Array.from(
168
168
  () =>
169
169
  "The rule reads the body of the paper and counts every sentence it holds.",
170
170
  ).join(" ");
171
+ /** FORMAL with `n` more sentences `s`: a body whose rate of one register measure is set. */
172
+ const formalWith = (n: number, s: string): string =>
173
+ `${FORMAL} ${Array.from({ length: n }, () => s).join(" ")}`;
174
+ /** FORMAL with enough relation markers for AIDC's band (above 25.4 per 10,000 words). */
175
+ const LINKED = formalWith(
176
+ 10,
177
+ "Therefore, the rule counts the sentence because the body holds it.",
178
+ );
171
179
  const STATUS_FILE = `${P}/PIPELINE-STATUS.md`;
172
180
  const SHIPPED_STATUS =
173
181
  "---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/a.pdf\n bytes: 1\n---\n# PIPELINE-STATUS\n";
@@ -357,6 +365,32 @@ const CASES: Readonly<Record<string, RuleCases>> = {
357
365
  },
358
366
  silent: paper(tex(FORMAL)),
359
367
  },
368
+ // The three register bands judge a body against the anchors of AIDC's preset; the body opens on
369
+ // line 3, right after the \\section, where the finding about the whole body stands.
370
+ "tex/contrast-frames": {
371
+ reports: {
372
+ tree: aidc(formalWith(5, "It reads the word, not the operation.")),
373
+ file: TEX_FILE,
374
+ severity: 1,
375
+ line: 3,
376
+ },
377
+ silent: aidc(LINKED),
378
+ },
379
+ "tex/claim-emphasis": {
380
+ reports: {
381
+ tree: aidc(
382
+ formalWith(6, "We find that \\textbf{the rule counts 46 sentences}."),
383
+ ),
384
+ file: TEX_FILE,
385
+ severity: 1,
386
+ line: 3,
387
+ },
388
+ silent: aidc(LINKED),
389
+ },
390
+ "tex/relation-markers": {
391
+ reports: { tree: aidc(FORMAL), file: TEX_FILE, severity: 1, line: 3 },
392
+ silent: aidc(LINKED),
393
+ },
360
394
  "bib/reachable-entry": {
361
395
  reports: {
362
396
  tree: paper(inlineBib("@book{a,\n title = {A},\n year = {2026}\n}")),
@@ -498,23 +532,39 @@ describe("every registered rule has cases, and every case names a registered rul
498
532
  });
499
533
  });
500
534
 
535
+ /**
536
+ * A case lints its tree through the whole CLI: a body long enough for a rate to be judged (2 000
537
+ * words and more) takes seconds under coverage, on a machine running every suite at once.
538
+ */
539
+ const CASE_TIMEOUT_MS = 30_000;
540
+
501
541
  describe.each(Object.entries(CASES))("%s, through real ESLint", (id, c) => {
502
- it(`reports on ${c.reports.file}:${String(c.reports.line)} at severity ${String(c.reports.severity)}`, async () => {
503
- const got = (await lintTree(c.reports.tree)).filter((m) => m.ruleId === id);
504
- expect(
505
- got.some(
506
- (m) =>
507
- m.file === c.reports.file &&
508
- m.line === c.reports.line &&
509
- m.severity === c.reports.severity,
510
- ),
511
- JSON.stringify(got),
512
- ).toBe(true);
513
- });
542
+ it(
543
+ `reports on ${c.reports.file}:${String(c.reports.line)} at severity ${String(c.reports.severity)}`,
544
+ { timeout: CASE_TIMEOUT_MS },
545
+ async () => {
546
+ const got = (await lintTree(c.reports.tree)).filter(
547
+ (m) => m.ruleId === id,
548
+ );
549
+ expect(
550
+ got.some(
551
+ (m) =>
552
+ m.file === c.reports.file &&
553
+ m.line === c.reports.line &&
554
+ m.severity === c.reports.severity,
555
+ ),
556
+ JSON.stringify(got),
557
+ ).toBe(true);
558
+ },
559
+ );
514
560
 
515
- it("stays silent on a conforming paper", async () => {
516
- expect((await lintTree(c.silent)).filter((m) => m.ruleId === id)).toEqual(
517
- [],
518
- );
519
- });
561
+ it(
562
+ "stays silent on a conforming paper",
563
+ { timeout: CASE_TIMEOUT_MS },
564
+ async () => {
565
+ expect((await lintTree(c.silent)).filter((m) => m.ruleId === id)).toEqual(
566
+ [],
567
+ );
568
+ },
569
+ );
520
570
  });
@@ -28,6 +28,7 @@ import Ajv from "ajv";
28
28
  import { presetsDir } from "./package-dirs.ts";
29
29
  import { CONFIG_FILE } from "#lib/paper-config";
30
30
  import { fieldOf } from "./domain/record.ts";
31
+ import type { RegisterAnchor, RegisterMeasureName } from "./domain/register.ts";
31
32
 
32
33
  /** CTAN package name → the names that prove it is installed. */
33
34
  export type PackageProofs = Readonly<Record<string, readonly string[]>>;
@@ -180,6 +181,11 @@ export interface PresetFile {
180
181
  readonly format: VenueFormat;
181
182
  /** rule id → ESLint entry, checked against the shipped rules where a config is built. */
182
183
  readonly rules: Readonly<Record<string, unknown>>;
184
+ /**
185
+ * The accepted papers the register rules measure a body against; null when the file names none
186
+ * (a child's list replaces its parent's).
187
+ */
188
+ readonly registerAnchors: readonly RegisterAnchor[] | null;
183
189
  }
184
190
 
185
191
  type KindsJson = Readonly<
@@ -217,6 +223,12 @@ interface PresetJson {
217
223
  Partial<Pick<TexRequirements, "tools">>;
218
224
  readonly format?: FormatJson;
219
225
  readonly rules?: Readonly<Record<string, unknown>>;
226
+ readonly register?: {
227
+ readonly anchors: readonly ({
228
+ readonly paper: string;
229
+ readonly words: number;
230
+ } & Readonly<Record<RegisterMeasureName, number>>)[];
231
+ };
220
232
  }
221
233
 
222
234
  /** An optional field as the typed preset holds it: absent is null. */
@@ -258,6 +270,38 @@ const sectionsOf = (
258
270
  ? null
259
271
  : r.map((x) => ({ title: x.title, position: orNull(x.position) }));
260
272
 
273
+ /** A preset's JSONC text → its value, or an Error naming the syntax problem. */
274
+ function jsoncOf(text: string, file: string): unknown {
275
+ const parsed = typescript().parseConfigFileTextToJson(file, text);
276
+ const config: unknown = parsed.config;
277
+ const error = parsed.error;
278
+ if (error)
279
+ throw new Error(
280
+ `${file}: not valid JSONC — ${typescript().flattenDiagnosticMessageText(error.messageText, " ")}`,
281
+ );
282
+ return config;
283
+ }
284
+
285
+ /** A preset's `tex` block, `tools` defaulted; null when the file declares none. */
286
+ const texOf = (t: PresetJson["tex"]): TexRequirements | null =>
287
+ t ? { packages: t.packages, tools: t.tools ?? {} } : null;
288
+
289
+ /** A preset's `register.anchors`, each with its counts; null when the file names none. */
290
+ const anchorsOf = (
291
+ r: PresetJson["register"],
292
+ ): readonly RegisterAnchor[] | null =>
293
+ r === undefined
294
+ ? null
295
+ : r.anchors.map((a) => ({
296
+ paper: a.paper,
297
+ words: a.words,
298
+ counts: {
299
+ contrast_frames: a.contrast_frames,
300
+ claim_emphasis: a.claim_emphasis,
301
+ relation_markers: a.relation_markers,
302
+ },
303
+ }));
304
+
261
305
  /**
262
306
  * The text of one preset file → the typed file, or an Error naming every problem. The ONE parser
263
307
  * of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
@@ -271,13 +315,7 @@ export function parsePreset(
271
315
  file: string,
272
316
  dir: string = presetsDir(),
273
317
  ): PresetFile {
274
- const parsed = typescript().parseConfigFileTextToJson(file, text);
275
- const config: unknown = parsed.config;
276
- const error = parsed.error;
277
- if (error)
278
- throw new Error(
279
- `${file}: not valid JSONC — ${typescript().flattenDiagnosticMessageText(error.messageText, " ")}`,
280
- );
318
+ const config = jsoncOf(text, file);
281
319
  const validate = validatorFor(dir);
282
320
  if (!matchesSchema(config, validate))
283
321
  throw new Error(
@@ -291,9 +329,10 @@ export function parsePreset(
291
329
  aliases: j.aliases ?? [],
292
330
  blind: orNull(j.blind),
293
331
  requiredSections: sectionsOf(j.required_sections),
294
- tex: j.tex ? { packages: j.tex.packages, tools: j.tex.tools ?? {} } : null,
332
+ tex: texOf(j.tex),
295
333
  format: formatOf(j.format),
296
334
  rules: j.rules ?? {},
335
+ registerAnchors: anchorsOf(j.register),
297
336
  };
298
337
  }
299
338
 
@@ -298,7 +298,13 @@ export function judgeLeftover(
298
298
 
299
299
  /** Where a rule's page lives: `docs/rules/tex/<name>.md` on the default branch. */
300
300
  export const ruleDocsUrl = (
301
- name: TexVenueRuleName | "claim-provenance" | "register",
301
+ name:
302
+ | TexVenueRuleName
303
+ | "claim-provenance"
304
+ | "register"
305
+ | "contrast-frames"
306
+ | "claim-emphasis"
307
+ | "relation-markers",
302
308
  ): string =>
303
309
  `https://github.com/zernie/paperlint/blob/main/docs/rules/tex/${name}.md`;
304
310