@trofimyukdev/repo-truth 0.0.0-stage → 0.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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -2
  3. package/dist/action.d.ts +52 -0
  4. package/dist/action.js +182 -0
  5. package/dist/action.js.map +1 -0
  6. package/dist/checks/action-pins.d.ts +52 -0
  7. package/dist/checks/action-pins.js +196 -0
  8. package/dist/checks/action-pins.js.map +1 -0
  9. package/dist/checks/autosquash.d.ts +23 -0
  10. package/dist/checks/autosquash.js +85 -0
  11. package/dist/checks/autosquash.js.map +1 -0
  12. package/dist/checks/bidi-controls.d.ts +58 -0
  13. package/dist/checks/bidi-controls.js +172 -0
  14. package/dist/checks/bidi-controls.js.map +1 -0
  15. package/dist/checks/binary-files.d.ts +53 -0
  16. package/dist/checks/binary-files.js +157 -0
  17. package/dist/checks/binary-files.js.map +1 -0
  18. package/dist/checks/config-changes.d.ts +57 -0
  19. package/dist/checks/config-changes.js +175 -0
  20. package/dist/checks/config-changes.js.map +1 -0
  21. package/dist/checks/credentials.d.ts +35 -0
  22. package/dist/checks/credentials.js +138 -0
  23. package/dist/checks/credentials.js.map +1 -0
  24. package/dist/checks/install-scripts.d.ts +39 -0
  25. package/dist/checks/install-scripts.js +173 -0
  26. package/dist/checks/install-scripts.js.map +1 -0
  27. package/dist/checks/lint-rules.d.ts +49 -0
  28. package/dist/checks/lint-rules.js +184 -0
  29. package/dist/checks/lint-rules.js.map +1 -0
  30. package/dist/checks/path-policy.d.ts +40 -0
  31. package/dist/checks/path-policy.js +195 -0
  32. package/dist/checks/path-policy.js.map +1 -0
  33. package/dist/checks/raised-timeouts.d.ts +66 -0
  34. package/dist/checks/raised-timeouts.js +248 -0
  35. package/dist/checks/raised-timeouts.js.map +1 -0
  36. package/dist/checks/script-changes.d.ts +51 -0
  37. package/dist/checks/script-changes.js +199 -0
  38. package/dist/checks/script-changes.js.map +1 -0
  39. package/dist/checks/secret-files.d.ts +58 -0
  40. package/dist/checks/secret-files.js +165 -0
  41. package/dist/checks/secret-files.js.map +1 -0
  42. package/dist/checks/strictness.d.ts +59 -0
  43. package/dist/checks/strictness.js +260 -0
  44. package/dist/checks/strictness.js.map +1 -0
  45. package/dist/checks/submodules.d.ts +51 -0
  46. package/dist/checks/submodules.js +172 -0
  47. package/dist/checks/submodules.js.map +1 -0
  48. package/dist/checks/suppressions.d.ts +53 -0
  49. package/dist/checks/suppressions.js +203 -0
  50. package/dist/checks/suppressions.js.map +1 -0
  51. package/dist/checks/workflow-permissions.d.ts +56 -0
  52. package/dist/checks/workflow-permissions.js +237 -0
  53. package/dist/checks/workflow-permissions.js.map +1 -0
  54. package/dist/checks/workflow-weakening.d.ts +38 -0
  55. package/dist/checks/workflow-weakening.js +172 -0
  56. package/dist/checks/workflow-weakening.js.map +1 -0
  57. package/dist/cli.d.ts +31 -0
  58. package/dist/cli.js +300 -0
  59. package/dist/cli.js.map +1 -0
  60. package/dist/commit-range.d.ts +28 -0
  61. package/dist/commit-range.js +152 -0
  62. package/dist/commit-range.js.map +1 -0
  63. package/dist/config.d.ts +35 -0
  64. package/dist/config.js +235 -0
  65. package/dist/config.js.map +1 -0
  66. package/dist/conflict-markers.d.ts +40 -0
  67. package/dist/conflict-markers.js +234 -0
  68. package/dist/conflict-markers.js.map +1 -0
  69. package/dist/dependency-sources.d.ts +32 -0
  70. package/dist/dependency-sources.js +260 -0
  71. package/dist/dependency-sources.js.map +1 -0
  72. package/dist/figure-claims.d.ts +35 -0
  73. package/dist/figure-claims.js +139 -0
  74. package/dist/figure-claims.js.map +1 -0
  75. package/dist/figure-text.d.ts +22 -0
  76. package/dist/figure-text.js +119 -0
  77. package/dist/figure-text.js.map +1 -0
  78. package/dist/index.d.ts +46 -0
  79. package/dist/index.js +38 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/large-blobs.d.ts +35 -0
  82. package/dist/large-blobs.js +163 -0
  83. package/dist/large-blobs.js.map +1 -0
  84. package/dist/lockfile-drift.d.ts +36 -0
  85. package/dist/lockfile-drift.js +206 -0
  86. package/dist/lockfile-drift.js.map +1 -0
  87. package/dist/lowered-thresholds.d.ts +70 -0
  88. package/dist/lowered-thresholds.js +281 -0
  89. package/dist/lowered-thresholds.js.map +1 -0
  90. package/dist/measurement-rule.d.ts +39 -0
  91. package/dist/measurement-rule.js +213 -0
  92. package/dist/measurement-rule.js.map +1 -0
  93. package/dist/portable-paths.d.ts +37 -0
  94. package/dist/portable-paths.js +215 -0
  95. package/dist/portable-paths.js.map +1 -0
  96. package/dist/registry.d.ts +43 -0
  97. package/dist/registry.js +144 -0
  98. package/dist/registry.js.map +1 -0
  99. package/dist/replay-plan.d.ts +36 -0
  100. package/dist/replay-plan.js +196 -0
  101. package/dist/replay-plan.js.map +1 -0
  102. package/dist/replayed-figures.d.ts +36 -0
  103. package/dist/replayed-figures.js +159 -0
  104. package/dist/replayed-figures.js.map +1 -0
  105. package/dist/snapshot-refreshes.d.ts +36 -0
  106. package/dist/snapshot-refreshes.js +202 -0
  107. package/dist/snapshot-refreshes.js.map +1 -0
  108. package/dist/stray-files.d.ts +52 -0
  109. package/dist/stray-files.js +234 -0
  110. package/dist/stray-files.js.map +1 -0
  111. package/dist/symlinks.d.ts +28 -0
  112. package/dist/symlinks.js +193 -0
  113. package/dist/symlinks.js.map +1 -0
  114. package/dist/trailers.d.ts +36 -0
  115. package/dist/trailers.js +110 -0
  116. package/dist/trailers.js.map +1 -0
  117. package/dist/weakened-tests.d.ts +94 -0
  118. package/dist/weakened-tests.js +458 -0
  119. package/dist/weakened-tests.js.map +1 -0
  120. package/package.json +31 -4
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The registry of checks `repo-truth check` runs.
3
+ *
4
+ * This array is data the command reads when it runs; the command holds no list
5
+ * of its own. Adding a check means adding an entry here and nothing else.
6
+ */
7
+ import { readdir } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { OPTION_KINDS } from "./config.js";
11
+ import { checkCommitRange } from "./commit-range.js";
12
+ import { checkMeasurementRule } from "./measurement-rule.js";
13
+ import { checkTrailers } from "./trailers.js";
14
+ import { checkWeakenedTests } from "./weakened-tests.js";
15
+ import { checkStrayFiles } from "./stray-files.js";
16
+ import { checkLockfileDrift } from "./lockfile-drift.js";
17
+ import { checkConflictMarkers } from "./conflict-markers.js";
18
+ import { checkPortablePaths } from "./portable-paths.js";
19
+ import { checkSymlinks } from "./symlinks.js";
20
+ import { checkLargeBlobs } from "./large-blobs.js";
21
+ import { checkLoweredThresholds } from "./lowered-thresholds.js";
22
+ import { checkSnapshotRefreshes } from "./snapshot-refreshes.js";
23
+ import { checkDependencySources } from "./dependency-sources.js";
24
+ import { checkReplayedFigures } from "./replayed-figures.js";
25
+ function entry(name, check, declared = {}) {
26
+ const options = Object.fromEntries(Object.entries(declared));
27
+ return {
28
+ name,
29
+ options,
30
+ run: (repo, range, given) => {
31
+ const handed = {};
32
+ if (typeof given === "object" && given !== null) {
33
+ for (const option of Object.keys(options)) {
34
+ if (Object.hasOwn(given, option))
35
+ handed[option] = given[option];
36
+ }
37
+ }
38
+ delete handed.config;
39
+ if (repo.config !== undefined)
40
+ handed.config = repo.config;
41
+ return check(Object.assign(handed, { cwd: repo.cwd, base: range.base, candidate: range.candidate }));
42
+ },
43
+ };
44
+ }
45
+ const RESERVED_OPTIONS = ["cwd", "base", "candidate", "sanctions", "config"];
46
+ /** What is wrong with a module's optionKinds, or undefined when it is valid. */
47
+ function optionKindsProblem(value) {
48
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
49
+ return "its optionKinds is not a plain object";
50
+ }
51
+ const prototype = Object.getPrototypeOf(value);
52
+ if (prototype !== Object.prototype && prototype !== null)
53
+ return "its optionKinds is not a plain object";
54
+ for (const [option, kind] of Object.entries(value)) {
55
+ if (RESERVED_OPTIONS.includes(option))
56
+ return "its optionKinds declares the option " + option + ", which is not its to declare";
57
+ if (typeof kind !== "string" || !OPTION_KINDS.includes(kind)) {
58
+ return "its optionKinds gives the option " + option + " a kind that is none of " + OPTION_KINDS.join(", ");
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+ /** The options of every entry given, by entry name, in the form parseConfig reads. */
64
+ export function optionKindsOf(entries) {
65
+ return Object.fromEntries(entries.map((e) => [e.name, Object.fromEntries(Object.entries(e.options ?? {}))]));
66
+ }
67
+ const OWN_ENTRIES = [
68
+ entry("RT-01", checkCommitRange),
69
+ entry("RT-02", checkMeasurementRule),
70
+ entry("RT-03", checkTrailers),
71
+ entry("RT-04", checkWeakenedTests),
72
+ entry("RT-05", checkStrayFiles),
73
+ entry("RT-06", checkLockfileDrift),
74
+ entry("RT-09", checkConflictMarkers),
75
+ entry("RT-10", checkPortablePaths),
76
+ entry("RT-11", checkSymlinks),
77
+ entry("RT-12", checkLargeBlobs, { maxBytes: "integer" }),
78
+ entry("RT-14", checkLoweredThresholds),
79
+ entry("RT-16", checkSnapshotRefreshes),
80
+ entry("RT-18", checkDependencySources, { registries: "strings" }),
81
+ entry("RT-19", checkReplayedFigures, { timeoutMs: "integer" }),
82
+ ];
83
+ function failingEntry(file, extension, cause) {
84
+ const name = "checks/" + file.slice(0, -extension.length);
85
+ const record = { name, status: "fail", evidence: [file + ": " + cause] };
86
+ return { name, options: {}, run: () => record };
87
+ }
88
+ function isModuleFile(file, extension) {
89
+ return (file.endsWith(extension) &&
90
+ !file.endsWith(".d.ts") &&
91
+ !file.includes(".test.") &&
92
+ !file.includes(".spec."));
93
+ }
94
+ /**
95
+ * The entries for the check modules directly in one directory, in the order of
96
+ * their file names. A module that cannot be registered becomes an entry whose
97
+ * record fails and names the file and the cause. A missing directory yields none.
98
+ */
99
+ export async function loadCheckDirectory(directory, extension) {
100
+ const path = fileURLToPath(directory);
101
+ let dirents;
102
+ try {
103
+ dirents = await readdir(path, { withFileTypes: true });
104
+ }
105
+ catch {
106
+ return [];
107
+ }
108
+ const files = dirents
109
+ .filter((d) => d.isFile() && isModuleFile(d.name, extension))
110
+ .map((d) => d.name)
111
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
112
+ const entries = [];
113
+ const taken = new Set(OWN_ENTRIES.map((e) => e.name));
114
+ for (const file of files) {
115
+ let problem;
116
+ let mod = {};
117
+ try {
118
+ mod = ((await import(pathToFileURL(join(path, file)).href)) ?? {});
119
+ if (typeof mod.name !== "string" || mod.name === "")
120
+ problem = "exports no name that is a non-empty string";
121
+ else if (typeof mod.check !== "function")
122
+ problem = "exports no check that is a function";
123
+ else if (taken.has(mod.name))
124
+ problem = "its name " + mod.name + " is already registered";
125
+ else if ("optionKinds" in mod)
126
+ problem = optionKindsProblem(mod.optionKinds);
127
+ }
128
+ catch (error) {
129
+ problem = "could not be loaded: " + (error instanceof Error ? error.message : String(error));
130
+ }
131
+ if (problem !== undefined) {
132
+ const failed = failingEntry(file, extension, problem);
133
+ taken.add(failed.name);
134
+ entries.push(failed);
135
+ continue;
136
+ }
137
+ taken.add(mod.name);
138
+ entries.push(entry(mod.name, mod.check, mod.optionKinds));
139
+ }
140
+ return entries;
141
+ }
142
+ export const REGISTRY = [...OWN_ENTRIES];
143
+ REGISTRY.push(...(await loadCheckDirectory(new URL("checks/", import.meta.url), import.meta.url.endsWith(".ts") ? ".ts" : ".js")));
144
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Replay plans.
3
+ *
4
+ * A page or a commit quotes a git command beside a figure, and RT-19 runs
5
+ * that command again to see whether the figure still holds. A line of
6
+ * documentation is text anybody can write, so this module decides which
7
+ * command text may run at all, and it decides by lists of what is safe,
8
+ * never by a list of what is dangerous:
9
+ *
10
+ * - the text is split into words the way a POSIX shell splits it, with
11
+ * nothing expanded; anything a shell would read as a second command, a
12
+ * redirection, a substitution, an expansion or a glob ends the decision;
13
+ * - the words are `git`, one of a fixed set of read-only subcommands, and
14
+ * then only options on that subcommand's list, a lone `--`, or arguments
15
+ * that do not begin with a minus sign, optionally piped to `wc -l`;
16
+ * - every revision names a commit by its id, so the answer is the same on
17
+ * any clone, whatever its branches, tags and `HEAD`.
18
+ *
19
+ * The decision is pure: it starts no process, reads and writes no file and
20
+ * imports nothing. A command it does not plan is not replayable, which is an
21
+ * answer and never an error.
22
+ */
23
+ /** What RT-19 runs for a replayable command. */
24
+ export interface ReplayPlan {
25
+ /** The command's words after `git` and before its pipe, quotes removed. */
26
+ readonly args: readonly string[];
27
+ /** Whether the command ends with `| wc -l`. */
28
+ readonly countLines: boolean;
29
+ /** Every commit id the revisions name, each once, in order of first naming. */
30
+ readonly commits: readonly string[];
31
+ }
32
+ /**
33
+ * Decide whether a quoted git command may be run again, and with what.
34
+ * Returns null when it is not replayable; never throws.
35
+ */
36
+ export declare function planReplay(command: string): ReplayPlan | null;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Replay plans.
3
+ *
4
+ * A page or a commit quotes a git command beside a figure, and RT-19 runs
5
+ * that command again to see whether the figure still holds. A line of
6
+ * documentation is text anybody can write, so this module decides which
7
+ * command text may run at all, and it decides by lists of what is safe,
8
+ * never by a list of what is dangerous:
9
+ *
10
+ * - the text is split into words the way a POSIX shell splits it, with
11
+ * nothing expanded; anything a shell would read as a second command, a
12
+ * redirection, a substitution, an expansion or a glob ends the decision;
13
+ * - the words are `git`, one of a fixed set of read-only subcommands, and
14
+ * then only options on that subcommand's list, a lone `--`, or arguments
15
+ * that do not begin with a minus sign, optionally piped to `wc -l`;
16
+ * - every revision names a commit by its id, so the answer is the same on
17
+ * any clone, whatever its branches, tags and `HEAD`.
18
+ *
19
+ * The decision is pure: it starts no process, reads and writes no file and
20
+ * imports nothing. A command it does not plan is not replayable, which is an
21
+ * answer and never an error.
22
+ */
23
+ /** The characters a word may hold outside quotes. */
24
+ const BARE = /^[A-Za-z0-9.\-_/:=^@,%]$/;
25
+ /** What a double-quoted part may not hold: a shell expands or escapes these. */
26
+ const DOUBLE_QUOTE_UNSAFE = /[$`\\]/;
27
+ /** A commit id, optionally with a caret and digits after it. */
28
+ const ID = "([0-9a-fA-F]{7,40})(?:\\^[0-9]+)?";
29
+ /** A revision: an id, or two ids joined by `..` or `...`. */
30
+ const REVISION = new RegExp(`^${ID}(?:\\.\\.\\.?${ID})?$`);
31
+ const REV_LIST_OPTIONS = new Set(["--count", "--merges", "--no-merges", "--first-parent", "--reverse"]);
32
+ const LOG_OPTIONS = new Set(["--oneline", "--merges", "--no-merges", "--first-parent", "--reverse"]);
33
+ const GREP_OPTIONS = new Set(["-c", "-l", "-n", "-E", "-F", "-i", "-w", "-e", "--count", "--files-with-matches"]);
34
+ const LS_TREE_OPTIONS = new Set(["-r", "-d", "-t", "--name-only", "--full-tree"]);
35
+ function isLogOption(option) {
36
+ if (LOG_OPTIONS.has(option) || /^-[0-9]+$/.test(option) || option.startsWith("--date=")) {
37
+ return true;
38
+ }
39
+ // `%G` verifies a signature, and verifying one starts a program.
40
+ return (option.startsWith("--format=") || option.startsWith("--pretty=")) && !option.includes("%G");
41
+ }
42
+ /** Each subcommand that may be replayed, and the options it is planned with. */
43
+ const SUBCOMMANDS = new Map([
44
+ ["rev-list", (option) => REV_LIST_OPTIONS.has(option)],
45
+ ["log", isLogOption],
46
+ ["grep", (option) => GREP_OPTIONS.has(option)],
47
+ ["ls-tree", (option) => LS_TREE_OPTIONS.has(option)],
48
+ ]);
49
+ /**
50
+ * Split a command into words and pipes as a POSIX shell would, expanding
51
+ * nothing. Returns null for anything a shell would read as more than words
52
+ * and pipes, and for a quote that is not closed.
53
+ */
54
+ function tokenize(command) {
55
+ const tokens = [];
56
+ let word = null;
57
+ const endWord = () => {
58
+ if (word !== null) {
59
+ tokens.push({ kind: "word", text: word });
60
+ word = null;
61
+ }
62
+ };
63
+ let i = 0;
64
+ while (i < command.length) {
65
+ const c = command[i];
66
+ if (c === " " || c === "\t") {
67
+ endWord();
68
+ i += 1;
69
+ }
70
+ else if (c === "|") {
71
+ endWord();
72
+ tokens.push({ kind: "pipe" });
73
+ i += 1;
74
+ }
75
+ else if (c === "'" || c === '"') {
76
+ const close = command.indexOf(c, i + 1);
77
+ if (close < 0) {
78
+ return null;
79
+ }
80
+ const part = command.slice(i + 1, close);
81
+ if (c === '"' && DOUBLE_QUOTE_UNSAFE.test(part)) {
82
+ return null;
83
+ }
84
+ word = (word ?? "") + part;
85
+ i = close + 1;
86
+ }
87
+ else if (BARE.test(c)) {
88
+ word = (word ?? "") + c;
89
+ i += 1;
90
+ }
91
+ else {
92
+ return null;
93
+ }
94
+ }
95
+ endWord();
96
+ return tokens;
97
+ }
98
+ function isWord(token, text) {
99
+ return token !== undefined && token.kind === "word" && token.text === text;
100
+ }
101
+ /**
102
+ * Decide whether a quoted git command may be run again, and with what.
103
+ * Returns null when it is not replayable; never throws.
104
+ */
105
+ export function planReplay(command) {
106
+ if (typeof command !== "string" || command.includes("\0")) {
107
+ return null;
108
+ }
109
+ const tokens = tokenize(command);
110
+ if (tokens === null) {
111
+ return null;
112
+ }
113
+ let body = tokens;
114
+ let countLines = false;
115
+ const pipes = tokens.filter((token) => token.kind === "pipe").length;
116
+ if (pipes > 1) {
117
+ return null;
118
+ }
119
+ if (pipes === 1) {
120
+ const n = tokens.length;
121
+ if (tokens[n - 3]?.kind !== "pipe" || !isWord(tokens[n - 2], "wc") || !isWord(tokens[n - 1], "-l")) {
122
+ return null;
123
+ }
124
+ body = tokens.slice(0, n - 3);
125
+ countLines = true;
126
+ }
127
+ const words = body.map((token) => (token.kind === "word" ? token.text : "|"));
128
+ if (words[0] !== "git" || words.length < 2) {
129
+ return null;
130
+ }
131
+ const subcommand = words[1];
132
+ const isOption = SUBCOMMANDS.get(subcommand);
133
+ if (isOption === undefined) {
134
+ return null;
135
+ }
136
+ // Arguments before the first lone `--`, and every word after it, in order.
137
+ const before = [];
138
+ const after = [];
139
+ let dashDash = false;
140
+ let patternGiven = false;
141
+ const rest = words.slice(2);
142
+ for (let k = 0; k < rest.length; k += 1) {
143
+ const word = rest[k];
144
+ if (word === "--" && !dashDash) {
145
+ dashDash = true;
146
+ continue;
147
+ }
148
+ if (word.startsWith("-") && word !== "--" && !isOption(word)) {
149
+ return null;
150
+ }
151
+ if (dashDash) {
152
+ after.push(word);
153
+ continue;
154
+ }
155
+ if (word.startsWith("-")) {
156
+ if (subcommand === "grep" && word === "-e") {
157
+ const pattern = rest[k + 1];
158
+ if (pattern === undefined || (pattern.startsWith("-") && pattern !== "--" && !isOption(pattern))) {
159
+ return null;
160
+ }
161
+ patternGiven = true;
162
+ k += 1;
163
+ }
164
+ continue;
165
+ }
166
+ before.push(word);
167
+ }
168
+ let revisions;
169
+ if (subcommand === "ls-tree") {
170
+ const first = before[0] ?? after[0];
171
+ revisions = first === undefined ? [] : [first];
172
+ }
173
+ else if (subcommand === "grep" && !patternGiven) {
174
+ revisions = before.slice(1);
175
+ }
176
+ else {
177
+ revisions = before;
178
+ }
179
+ if (revisions.length === 0) {
180
+ return null;
181
+ }
182
+ const commits = [];
183
+ for (const revision of revisions) {
184
+ const match = REVISION.exec(revision);
185
+ if (match === null) {
186
+ return null;
187
+ }
188
+ for (const id of [match[1], match[2]]) {
189
+ if (id !== undefined && !commits.includes(id)) {
190
+ commits.push(id);
191
+ }
192
+ }
193
+ }
194
+ return { args: words.slice(1), countLines, commits };
195
+ }
196
+ //# sourceMappingURL=replay-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-plan.js","sourceRoot":"","sources":["../src/replay-plan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Replayed figures.
3
+ *
4
+ * RT-02 asks that a figure carry the command that printed it; this runs that
5
+ * command again and compares. The claims a range adds are read by
6
+ * `readFigureClaims` (RT-21), and whether a claim's command may run at all is
7
+ * decided by `planReplay` (RT-20); this module holds neither a reader nor a
8
+ * word splitter of its own. A planned command runs as `git --no-pager` and the
9
+ * plan's words, as an argument vector with no shell, bounded in time, and only
10
+ * once every commit the plan names is held by the repository.
11
+ *
12
+ * A command `planReplay` does not plan is counted as not replayable and never
13
+ * failed. A different output, a commit the repository does not hold, a
14
+ * non-zero exit and a replay that runs out of time are findings.
15
+ *
16
+ * The check fails closed: a failure the claim reader answers, an invalid
17
+ * `timeoutMs` or a git invocation other than a replay that fails is a `fail`
18
+ * record naming the cause, never a pass and never a thrown error.
19
+ */
20
+ import type { CheckRecord } from "./index.js";
21
+ /** How long one replay may run, in milliseconds, when the caller sets no bound. */
22
+ export declare const DEFAULT_REPLAY_TIMEOUT_MS = 10000;
23
+ /** The most characters of an output a finding shows. */
24
+ export declare const SHOWN_OUTPUT_LENGTH = 120;
25
+ export interface ReplayedFiguresOptions {
26
+ /** The git repository to run in. */
27
+ readonly cwd: string;
28
+ /** The revision the range starts after. */
29
+ readonly base: string;
30
+ /** The revision the range ends at. */
31
+ readonly candidate: string;
32
+ /** How long one replay may run, in milliseconds. */
33
+ readonly timeoutMs?: number;
34
+ }
35
+ /** Replay every figure claim the range adds and compare each with what it says. */
36
+ export declare function checkReplayedFigures(options: ReplayedFiguresOptions): Promise<CheckRecord>;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Replayed figures.
3
+ *
4
+ * RT-02 asks that a figure carry the command that printed it; this runs that
5
+ * command again and compares. The claims a range adds are read by
6
+ * `readFigureClaims` (RT-21), and whether a claim's command may run at all is
7
+ * decided by `planReplay` (RT-20); this module holds neither a reader nor a
8
+ * word splitter of its own. A planned command runs as `git --no-pager` and the
9
+ * plan's words, as an argument vector with no shell, bounded in time, and only
10
+ * once every commit the plan names is held by the repository.
11
+ *
12
+ * A command `planReplay` does not plan is counted as not replayable and never
13
+ * failed. A different output, a commit the repository does not hold, a
14
+ * non-zero exit and a replay that runs out of time are findings.
15
+ *
16
+ * The check fails closed: a failure the claim reader answers, an invalid
17
+ * `timeoutMs` or a git invocation other than a replay that fails is a `fail`
18
+ * record naming the cause, never a pass and never a thrown error.
19
+ */
20
+ import { spawn } from "node:child_process";
21
+ import { readFigureClaims } from "./figure-claims.js";
22
+ import { planReplay } from "./replay-plan.js";
23
+ const NAME = "RT-19";
24
+ /** How long one replay may run, in milliseconds, when the caller sets no bound. */
25
+ export const DEFAULT_REPLAY_TIMEOUT_MS = 10000;
26
+ /** The most characters of an output a finding shows. */
27
+ export const SHOWN_OUTPUT_LENGTH = 120;
28
+ /**
29
+ * Run git with an argument vector and no shell. A run still going at the
30
+ * bound is killed with its whole process group and answered at once, so a
31
+ * process it left behind holding a pipe cannot hold the check.
32
+ */
33
+ function runGit(cwd, args, timeoutMs) {
34
+ return new Promise((resolve, reject) => {
35
+ const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], detached: true });
36
+ const out = [];
37
+ const err = [];
38
+ child.stdout.on("data", (chunk) => out.push(chunk));
39
+ child.stderr.on("data", (chunk) => err.push(chunk));
40
+ const timer = setTimeout(() => {
41
+ try {
42
+ process.kill(-child.pid, "SIGKILL");
43
+ }
44
+ catch {
45
+ child.kill("SIGKILL");
46
+ }
47
+ child.stdout.destroy();
48
+ child.stderr.destroy();
49
+ resolve({ timedOut: true, code: null, signal: null, stdout: "", stderr: "" });
50
+ }, timeoutMs);
51
+ child.on("error", (error) => {
52
+ clearTimeout(timer);
53
+ reject(error);
54
+ });
55
+ child.on("close", (code, signal) => {
56
+ clearTimeout(timer);
57
+ resolve({
58
+ timedOut: false,
59
+ code,
60
+ signal,
61
+ stdout: Buffer.concat(out).toString("utf8"),
62
+ stderr: Buffer.concat(err).toString("utf8").trim(),
63
+ });
64
+ });
65
+ });
66
+ }
67
+ /** Escape every character outside printable ASCII so a line cannot replay a terminal sequence or split. */
68
+ function escapeText(text) {
69
+ let escaped = "";
70
+ for (const char of text) {
71
+ const codePoint = char.codePointAt(0) ?? 0;
72
+ if (codePoint >= 0x20 && codePoint <= 0x7e) {
73
+ escaped += char;
74
+ }
75
+ else {
76
+ escaped += `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
77
+ }
78
+ }
79
+ return escaped;
80
+ }
81
+ function shown(output) {
82
+ const chars = [...output];
83
+ if (chars.length <= SHOWN_OUTPUT_LENGTH)
84
+ return `\`${output}\``;
85
+ return `\`${chars.slice(0, SHOWN_OUTPUT_LENGTH).join("")}\`...`;
86
+ }
87
+ function finding(claim, printed) {
88
+ return escapeText(`${claim.where}: \`${claim.command}\` printed ${printed} claims ${shown(claim.claimed)}`);
89
+ }
90
+ /** Whether the repository holds `id` as a commit. Any answer but yes or no is thrown. */
91
+ async function holdsCommit(cwd, id, timeoutMs) {
92
+ const run = await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${id}^{commit}`], timeoutMs);
93
+ if (run.code === 0)
94
+ return true;
95
+ if (run.code === 1)
96
+ return false;
97
+ const cause = run.timedOut ? "timed out" : run.stderr || `exit code ${run.code ?? run.signal}`;
98
+ throw new Error(`git rev-parse could not look up commit ${id}: ${cause}`);
99
+ }
100
+ /** Replay one planned claim; the finding it is, or `null` when it is reproduced. */
101
+ async function replay(cwd, claim, plan, timeoutMs, held) {
102
+ for (const id of plan.commits) {
103
+ if (!held.has(id))
104
+ held.set(id, await holdsCommit(cwd, id, timeoutMs));
105
+ if (held.get(id) === false)
106
+ return finding(claim, `nothing: the repository does not hold ${id};`);
107
+ }
108
+ const run = await runGit(cwd, ["--no-pager", ...plan.args], timeoutMs);
109
+ if (run.timedOut)
110
+ return finding(claim, `nothing: timed out after ${timeoutMs} ms;`);
111
+ if (run.code !== 0) {
112
+ return finding(claim, run.code === null ? `nothing: killed by ${run.signal};` : `nothing: exit code ${run.code};`);
113
+ }
114
+ const output = plan.countLines
115
+ ? String(run.stdout.split("\n").length - 1)
116
+ : run.stdout.endsWith("\n")
117
+ ? run.stdout.slice(0, -1)
118
+ : run.stdout;
119
+ return output === claim.claimed ? null : finding(claim, shown(output));
120
+ }
121
+ function failure(cause) {
122
+ return { name: NAME, status: "fail", evidence: [escapeText(cause)] };
123
+ }
124
+ /** Replay every figure claim the range adds and compare each with what it says. */
125
+ export async function checkReplayedFigures(options) {
126
+ try {
127
+ const timeoutMs = options.timeoutMs ?? DEFAULT_REPLAY_TIMEOUT_MS;
128
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
129
+ return failure(`timeoutMs ${String(timeoutMs)} is not a positive number of milliseconds`);
130
+ }
131
+ const read = await readFigureClaims({ cwd: options.cwd, base: options.base, candidate: options.candidate });
132
+ // The reader's text is held as it answers it; it quotes the revision it names.
133
+ if ("failure" in read)
134
+ return { name: NAME, status: "fail", evidence: [read.failure] };
135
+ const findings = [];
136
+ const held = new Map();
137
+ let reproduced = 0;
138
+ let notReplayable = 0;
139
+ for (const claim of read.claims) {
140
+ const plan = planReplay(claim.command);
141
+ if (plan === null) {
142
+ notReplayable += 1;
143
+ continue;
144
+ }
145
+ const found = await replay(options.cwd, claim, plan, timeoutMs, held);
146
+ if (found === null)
147
+ reproduced += 1;
148
+ else
149
+ findings.push(found);
150
+ }
151
+ if (findings.length > 0)
152
+ return { name: NAME, status: "fail", evidence: findings };
153
+ return { name: NAME, status: "pass", evidence: [`${reproduced} reproduced, ${notReplayable} not replayable`] };
154
+ }
155
+ catch (error) {
156
+ return failure(`replayed-figures check could not run: ${error instanceof Error ? error.message : String(error)}`);
157
+ }
158
+ }
159
+ //# sourceMappingURL=replayed-figures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replayed-figures.js","sourceRoot":"","sources":["../src/replayed-figures.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Snapshot refreshes.
3
+ *
4
+ * A snapshot test passes when the output matches what was recorded, so the
5
+ * cheapest way to make a failing one pass is to record again. This check
6
+ * reports every snapshot `base..candidate` rewrites or deletes, and says
7
+ * whether a commit that did it also changed a file that is neither a snapshot
8
+ * nor a test. A new snapshot, or one only renamed with its content unchanged,
9
+ * is not a finding. Inline snapshots are not this version's.
10
+ *
11
+ * Only the range's changes are read, through git with an argument vector: the
12
+ * diff from the merge base of the two revisions to the candidate, with renames
13
+ * detected, and the paths each commit of `base..candidate` changed. A snapshot
14
+ * the base changed before the range is not reported.
15
+ */
16
+ import type { CheckRecord } from "./index.js";
17
+ /**
18
+ * The shapes of a snapshot path, each tested against a path relative to the
19
+ * repository root. The check reads this list each time it runs, so a consumer
20
+ * appends its own shape.
21
+ */
22
+ export declare const SNAPSHOT_PATHS: RegExp[];
23
+ export type SnapshotCategory = "snapshot-refreshed" | "snapshot-deleted";
24
+ export interface SnapshotRefreshesOptions {
25
+ readonly cwd: string;
26
+ readonly base: string;
27
+ readonly candidate: string;
28
+ /** Keys are finding ids; values are the reasons. */
29
+ readonly sanctions?: Readonly<Record<string, string>> | undefined;
30
+ }
31
+ /**
32
+ * Report the snapshots `base..candidate` rewrites or deletes. Fails while any
33
+ * finding is unsanctioned, and fails closed: a revision that names no commit,
34
+ * or any git invocation that fails, is a `fail` record naming the cause.
35
+ */
36
+ export declare function checkSnapshotRefreshes(options: SnapshotRefreshesOptions): Promise<CheckRecord>;