@scientific-method/standard-checker 2.0.0 → 2.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.
@@ -3,8 +3,14 @@ import type { Context } from "../context.ts";
3
3
  export interface Deviation {
4
4
  /** The spec IDs its Departs from item names. */
5
5
  departs: string[];
6
+ /** The spec IDs its Replaces item names: the entries it replaces entirely. */
7
+ replaces: string[];
6
8
  /** Whether its Dropped item says it was dropped. */
7
9
  dropped: boolean;
10
+ /** Whether its Default is mandatory. */
11
+ mandatory: boolean;
12
+ /** The test files its Tests item lists, which check the rebuild does what the deviation says. */
13
+ tests: string[];
8
14
  /** Its file. */
9
15
  file: string;
10
16
  }
@@ -3,7 +3,7 @@
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  import { basename, join } from "node:path";
5
5
  import { checkResolves, isSuperseded } from "../evidence.js";
6
- import { termFiles } from "../files.js";
6
+ import { checkTestFiles, termFiles } from "../files.js";
7
7
  import { areaOf, idsIn, kindOf } from "../ids.js";
8
8
  import { readText } from "../markdown.js";
9
9
  /** Reads and checks deviations/. Returns deviation ID -> the deviation. */
@@ -44,10 +44,12 @@ export function checkDeviations(ctx) {
44
44
  const item = Object.fromEntries(items);
45
45
  const order = [
46
46
  "Departs from",
47
+ ...("Replaces" in item ? ["Replaces"] : []),
47
48
  "Reason",
48
49
  "Setting",
49
50
  "Default",
50
51
  ...("Justification" in item ? ["Justification"] : []),
52
+ ...("Tests" in item ? ["Tests"] : []),
51
53
  "Dropped",
52
54
  ];
53
55
  if (items
@@ -59,7 +61,12 @@ export function checkDeviations(ctx) {
59
61
  const dropped = Boolean(item.Dropped) && item.Dropped !== "no";
60
62
  if (dropped && !/^\d{4}-\d{2}-\d{2}\b/.test(item.Dropped))
61
63
  problem(file, `${title}: Dropped gives the date, YYYY-MM-DD, and the reason`);
64
+ const replaces = idsIn(item.Replaces);
62
65
  checkResolves(ctx, file, departs, `${title} Departs from`);
66
+ checkResolves(ctx, file, replaces, `${title} Replaces`);
67
+ // The Tests item lists the test files that check the rebuild does what the deviation says. A
68
+ // dropped deviation's tests may have gone with it.
69
+ let tests = [];
63
70
  if (!dropped) {
64
71
  if (!departs.some((x) => ["RULE", "FMT", "SCR"].includes(kindOf(x))))
65
72
  problem(file, `${title}: Departs from names at least one rule, format or screen`);
@@ -67,8 +74,28 @@ export function checkDeviations(ctx) {
67
74
  if (isSuperseded(entries, x))
68
75
  problem(file, `${title} departs from ${x}, which is superseded`);
69
76
  checkDeviationDefault(ctx, file, title, item, departs);
77
+ // Replaces names the entries of Departs from that a mandatory deviation replaces entirely, which
78
+ // is what lets their rows become deviated.
79
+ if ("Replaces" in item) {
80
+ if (item.Default !== "mandatory")
81
+ problem(file, `${title}: only a mandatory deviation has a Replaces item`);
82
+ if (replaces.length === 0)
83
+ problem(file, `${title}: Replaces names at least one entry`);
84
+ for (const x of replaces) {
85
+ if (!departs.includes(x))
86
+ problem(file, `${title}: Replaces names ${x}, which Departs from does not`);
87
+ if (!["RULE", "FMT", "SCR"].includes(kindOf(x)))
88
+ problem(file, `${title}: Replaces names ${x}, which is not a rule, format or screen`);
89
+ }
90
+ }
91
+ // Nothing records a local run of a deviation's tests, so they run in CI and never need GAME_DIR.
92
+ if ("Tests" in item) {
93
+ tests = checkTestFiles(ctx, file, title, item.Tests, false);
94
+ if (tests.length === 0)
95
+ problem(file, `${title}: Tests lists at least one test file; leave the item out when there is none`);
96
+ }
70
97
  }
71
- deviations.set(title, { departs, dropped, file });
98
+ deviations.set(title, { departs, replaces, dropped, mandatory: item.Default === "mandatory", tests, file });
72
99
  }
73
100
  }
74
101
  return deviations;
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { basename, join } from "node:path";
5
5
  import { collectPlaceholders } from "../code-files.js";
6
6
  import { mayBeInterrupted, onlyEmulatedRuns } from "../evidence.js";
7
- import { markdownTree, walk } from "../files.js";
7
+ import { checkTestFiles, markdownTree, NEEDS_GAME, walk } from "../files.js";
8
8
  import { areaOf, compareIds, kindOf } from "../ids.js";
9
9
  import { readText, tables } from "../markdown.js";
10
10
  import { LINE_LIMIT } from "../standard.js";
@@ -24,10 +24,9 @@ export function checkParity(ctx, deviations) {
24
24
  const parityRows = new Map(); // spec ID -> { cells, file }
25
25
  const parityCounts = { status: {}, code: {} };
26
26
  const validatedTests = new Map(); // marked test file of a validated row -> [{ specId, file }]
27
- // A test file that reads the original's files through GAME_DIR says so with this comment. It runs
27
+ // A test file that reads the original's files through GAME_DIR says so with NEEDS_GAME. It runs
28
28
  // only on a maintainer's machine, so its validated rows need it in VALIDATION.md; every other test
29
29
  // runs in CI.
30
- const NEEDS_GAME = /needs:\s*GAME_DIR/;
31
30
  const needsGame = (p) => existsSync(p) && NEEDS_GAME.test(readFileSync(p, "utf8"));
32
31
  // A PARITY.md that still holds the rows is left alone until they have moved, so the check does not
33
32
  // overwrite them with the totals.
@@ -101,24 +100,7 @@ export function checkParity(ctx, deviations) {
101
100
  problem(file, `${specId}: an unknown entry cannot be complete`);
102
101
  if (code === "complete" && placeholders.has(specId))
103
102
  problem(file, `${specId}: a PLACEHOLDER comment cites it, so it cannot be complete`);
104
- const testFiles = tests === "None"
105
- ? []
106
- : tests
107
- .split(",")
108
- .map((x) => x.trim())
109
- .filter(Boolean);
110
- for (const tf of testFiles) {
111
- const p = join(repoDir, tf);
112
- if (!existsSync(p))
113
- problem(file, `${specId}: test file ${tf} does not exist`);
114
- else {
115
- const text = readFileSync(p, "utf8");
116
- if (!text.includes(specId))
117
- problem(file, `${specId}: test file ${tf} does not mention ${specId}`);
118
- if (text.includes("GAME_DIR") && !NEEDS_GAME.test(text))
119
- problem(file, `${specId}: test file ${tf} mentions GAME_DIR without a "needs: GAME_DIR" comment, so CI would skip it unseen`);
120
- }
121
- }
103
+ const testFiles = checkTestFiles(ctx, file, specId, tests);
122
104
  const listedDevs = devs === "None"
123
105
  ? []
124
106
  : devs
@@ -131,11 +113,25 @@ export function checkParity(ctx, deviations) {
131
113
  .sort(compareIds);
132
114
  if (listedDevs.slice().sort(compareIds).join(",") !== expectedDevs.join(","))
133
115
  problem(file, `${specId}: Deviations must be ${expectedDevs.join(", ") || "None"}`);
116
+ // A row whose entry a mandatory deviation replaces entirely cannot be compared with the original,
117
+ // so it has no tests of its own: they belong in the deviation's Tests item. It is deviated once
118
+ // every mandatory deviation it lists has tests.
119
+ const mandatory = listedDevs
120
+ .map((x) => deviations.get(x))
121
+ .filter((d) => Boolean(d?.mandatory && !d.dropped));
122
+ const replacing = listedDevs.find((x) => {
123
+ const d = deviations.get(x);
124
+ return d?.mandatory && !d.dropped && d.replaces.includes(specId);
125
+ });
126
+ const replaced = replacing !== undefined;
127
+ if (replaced && testFiles.length > 0)
128
+ problem(file, `${specId}: ${replacing} replaces it, so Tests must be None; list the tests in the deviation's Tests item`);
129
+ const deviated = replaced && mandatory.every((d) => d.tests.length > 0);
134
130
  let expectedStatus;
135
131
  if (code !== "complete" || e.meta.status === "disputed")
136
132
  expectedStatus = e.meta.status;
137
- else if (testFiles.length === 0)
138
- expectedStatus = "implemented";
133
+ else if (testFiles.length === 0 || replaced)
134
+ expectedStatus = deviated ? "deviated" : "implemented";
139
135
  else if (["supported", "established"].includes(e.meta.status))
140
136
  expectedStatus = "validated";
141
137
  else {
package/dist/files.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Context } from "./context.ts";
1
2
  /** A path with backslashes turned into forward slashes. */
2
3
  export declare const toSlash: (p: string) => string;
3
4
  /**
@@ -9,3 +10,12 @@ export declare function walk(dir: string, fn: (path: string) => void): void;
9
10
  export declare function markdownTree(dir: string): Map<string, string>;
10
11
  /** The paths of the files and directories in dir, apart from .gitkeep. */
11
12
  export declare const termFiles: (dir: string) => string[];
13
+ /** The comment of a test file that reads the original's files through GAME_DIR, which CI skips. */
14
+ export declare const NEEDS_GAME: RegExp;
15
+ /**
16
+ * Checks the test files that cell lists for id, comma-separated by their path from the repository
17
+ * root, and returns them. `None` lists none. Each listed path is a file that mentions id, and one that
18
+ * mentions GAME_DIR carries a "needs: GAME_DIR" comment, since CI skips it. Without needsGame, a file
19
+ * that mentions GAME_DIR at all is a problem. Each problem goes to file.
20
+ */
21
+ export declare function checkTestFiles(ctx: Context, file: string, id: string, cell: string, needsGame?: boolean): string[];
package/dist/files.js CHANGED
@@ -1,5 +1,5 @@
1
- // Walking the repository's directories.
2
- import { existsSync, readdirSync, statSync } from "node:fs";
1
+ // Walking the repository's directories, and reading the test files a row or deviation lists.
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { join, relative } from "node:path";
4
4
  /** A path with backslashes turned into forward slashes. */
5
5
  export const toSlash = (p) => p.replaceAll("\\", "/");
@@ -33,3 +33,39 @@ export function markdownTree(dir) {
33
33
  export const termFiles = (dir) => readdirSync(dir)
34
34
  .filter((name) => name !== ".gitkeep")
35
35
  .map((name) => join(dir, name));
36
+ /** The comment of a test file that reads the original's files through GAME_DIR, which CI skips. */
37
+ export const NEEDS_GAME = /needs:\s*GAME_DIR/;
38
+ /**
39
+ * Checks the test files that cell lists for id, comma-separated by their path from the repository
40
+ * root, and returns them. `None` lists none. Each listed path is a file that mentions id, and one that
41
+ * mentions GAME_DIR carries a "needs: GAME_DIR" comment, since CI skips it. Without needsGame, a file
42
+ * that mentions GAME_DIR at all is a problem. Each problem goes to file.
43
+ */
44
+ export function checkTestFiles(ctx, file, id, cell, needsGame = true) {
45
+ const { problem } = ctx;
46
+ const listed = cell === "None"
47
+ ? []
48
+ : cell
49
+ .split(",")
50
+ .map((x) => x.replaceAll("`", "").trim())
51
+ .filter(Boolean);
52
+ // Whole IDs only, as ID_RE reads them, so RULE-SCORE-0010 does not mention RULE-SCORE-001.
53
+ const mention = new RegExp(`\\b${id}\\b`);
54
+ for (const tf of listed) {
55
+ const p = join(ctx.config.repoDir, tf);
56
+ if (!existsSync(p))
57
+ problem(file, `${id}: test file ${tf} does not exist`);
58
+ else if (!statSync(p).isFile())
59
+ problem(file, `${id}: test file ${tf} is not a file`);
60
+ else {
61
+ const text = readFileSync(p, "utf8");
62
+ if (!mention.test(text))
63
+ problem(file, `${id}: test file ${tf} does not mention ${id}`);
64
+ if (text.includes("GAME_DIR") && !needsGame)
65
+ problem(file, `${id}: test file ${tf} mentions GAME_DIR, but it has to run in CI`);
66
+ else if (text.includes("GAME_DIR") && !NEEDS_GAME.test(text))
67
+ problem(file, `${id}: test file ${tf} mentions GAME_DIR without a "needs: GAME_DIR" comment, so CI would skip it unseen`);
68
+ }
69
+ }
70
+ return listed;
71
+ }
@@ -21,7 +21,7 @@ export function generateParity(ctx, parity, generated) {
21
21
  "",
22
22
  "| Status | Rows |",
23
23
  "|---|---|",
24
- ...["unknown", "sourced", "supported", "established", "disputed", "implemented", "validated"].map((k) => `| ${k} | ${parityCounts.status[k] ?? 0} |`),
24
+ ...["unknown", "sourced", "supported", "established", "disputed", "implemented", "deviated", "validated"].map((k) => `| ${k} | ${parityCounts.status[k] ?? 0} |`),
25
25
  "",
26
26
  "| Code | Rows |",
27
27
  "|---|---|",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scientific-method/standard-checker",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Checks a restoration's spec/, parity/ and deviations/ against version 1 of the dinorefurb documentation standard.",
5
5
  "type": "module",
6
6
  "license": "MIT",