@scientific-method/standard-checker 1.1.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.
- package/dist/checks/deviations.d.ts +6 -0
- package/dist/checks/deviations.js +29 -2
- package/dist/checks/evidence-entries.js +24 -7
- package/dist/checks/parity.js +19 -23
- package/dist/files.d.ts +10 -0
- package/dist/files.js +38 -2
- package/dist/generate/parity-md.js +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -6,6 +6,7 @@ import { isSuperseded } from "../evidence.js";
|
|
|
6
6
|
import { asList } from "../ids.js";
|
|
7
7
|
import { checkAddress, checkOffset } from "../locations.js";
|
|
8
8
|
import { locationRule } from "../standard.js";
|
|
9
|
+
import { parseScalar } from "../yaml.js";
|
|
9
10
|
/** Checks a finding's method, environment and locations. */
|
|
10
11
|
export function checkFinding(ctx, e) {
|
|
11
12
|
const { problem } = ctx;
|
|
@@ -121,6 +122,24 @@ export function checkExperiment(ctx, id, e, isSup) {
|
|
|
121
122
|
const builds = asList(meta.builds);
|
|
122
123
|
if (builds.length !== 1)
|
|
123
124
|
problem(file, "an experiment lists exactly one build", "ENTRY-TYPES-7");
|
|
125
|
+
// The standard's experiment entry lists the starting states: a save patch, a save, new-game,
|
|
126
|
+
// emulated-call or null, where a save or a patch is in saves/. A missing field is reported with
|
|
127
|
+
// the other required fields. Which save hashes the fixture needs depends on the form, so an
|
|
128
|
+
// unknown one gets no save hash problem. A path in saves/ names a file under it, so it has a
|
|
129
|
+
// name after saves/ and no empty, . or .. segment.
|
|
130
|
+
const state = meta.starting_state;
|
|
131
|
+
const savePath = typeof state === "string" &&
|
|
132
|
+
state.startsWith("saves/") &&
|
|
133
|
+
state
|
|
134
|
+
.split("/")
|
|
135
|
+
.slice(1)
|
|
136
|
+
.every((s) => s !== "" && s !== "." && s !== "..");
|
|
137
|
+
const fromSave = state === null || savePath;
|
|
138
|
+
if (state !== undefined && !fromSave && state !== "new-game" && state !== "emulated-call") {
|
|
139
|
+
// A string the front matter would read as another value, such as a quoted "null", is shown quoted.
|
|
140
|
+
const shown = typeof state === "string" && state !== "" && parseScalar(state) === state ? state : JSON.stringify(state);
|
|
141
|
+
problem(file, `starting_state ${shown} is none of the forms the standard defines: a save or save patch in saves/, new-game, emulated-call or null`);
|
|
142
|
+
}
|
|
124
143
|
const fixture = meta.fixture && join(specDir, "experiments", meta.fixture);
|
|
125
144
|
if (!fixture || !existsSync(fixture))
|
|
126
145
|
problem(file, `fixture ${meta.fixture} does not exist`);
|
|
@@ -132,13 +151,13 @@ export function checkExperiment(ctx, id, e, isSup) {
|
|
|
132
151
|
// Only a new game and an emulated call start without a save. starting_state null still has
|
|
133
152
|
// one: a save that cannot be committed, kept with the captures and found by its hash.
|
|
134
153
|
const save = fx.starting_state;
|
|
135
|
-
if (
|
|
154
|
+
if (fromSave && !save?.xxh3) {
|
|
136
155
|
const where = "gives the hash of the save its runs started from in starting_state.xxh3";
|
|
137
|
-
problem(fixture,
|
|
156
|
+
problem(fixture, state === null
|
|
138
157
|
? `${where}; starting_state null names a save kept with the captures, and an experiment that starts without a save has starting_state new-game or emulated-call`
|
|
139
158
|
: where);
|
|
140
159
|
}
|
|
141
|
-
if (
|
|
160
|
+
if (savePath && state.endsWith(".patch.json") && !save?.base_xxh3)
|
|
142
161
|
problem(fixture, "a patch fixture gives the base save's hash as well");
|
|
143
162
|
for (const key of ["xxh3", "base_xxh3"])
|
|
144
163
|
if (save?.[key] && !/^[0-9a-f]{32}$/.test(String(save[key])))
|
|
@@ -156,10 +175,8 @@ export function checkExperiment(ctx, id, e, isSup) {
|
|
|
156
175
|
problem(fixture, `is not valid JSON: ${err.message}`);
|
|
157
176
|
}
|
|
158
177
|
}
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
!existsSync(join(specDir, "experiments", meta.starting_state)))
|
|
162
|
-
problem(file, `starting_state ${meta.starting_state} does not exist`);
|
|
178
|
+
if (savePath && !existsSync(join(specDir, "experiments", state)))
|
|
179
|
+
problem(file, `starting_state ${state} does not exist`);
|
|
163
180
|
// A recording is committed in recordings/, kept with the captures, or one of the build's files.
|
|
164
181
|
if (typeof meta.recording === "string" && meta.recording !== "") {
|
|
165
182
|
const rec = meta.recording;
|
package/dist/checks/parity.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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": "
|
|
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",
|