staysfixed 0.12.0 → 0.13.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/CHANGELOG.md +32 -2
- package/docs/guards.md +18 -0
- package/docs/how-v2-works.md +10 -0
- package/package.json +1 -1
- package/src/cli/approve.js +4 -1
- package/src/cli/flake.js +4 -1
- package/src/cli/mark.js +5 -1
- package/src/cli/status.js +53 -1
- package/src/cli/trace.js +27 -2
- package/src/core/config.js +136 -25
- package/src/core/stop-tree.js +109 -0
- package/src/drive/browser.js +20 -31
- package/src/drive/page.js +74 -2
- package/src/guard/api.js +14 -9
- package/src/types.js +1 -1
- package/src/v2/adapters/child.js +15 -17
- package/src/v2/adapters/contract.js +122 -1
- package/src/v2/adapters/http.js +152 -30
- package/src/v2/adapters/isolate.js +169 -14
- package/src/v2/adapters/process.js +72 -8
- package/src/v2/adapters/source.js +254 -7
- package/src/v2/adapters/web.js +69 -19
- package/src/v2/browsers.js +136 -24
- package/src/v2/cause.js +46 -5
- package/src/v2/check.js +332 -34
- package/src/v2/cli.js +19 -1
- package/src/v2/coverage.js +555 -18
- package/src/v2/detect.js +737 -40
- package/src/v2/doctor.js +3 -3
- package/src/v2/escalate.js +57 -11
- package/src/v2/init.js +562 -21
- package/src/v2/journeys/answers-probe.js +376 -0
- package/src/v2/journeys/from-exports.js +456 -0
- package/src/v2/journeys/from-suite.js +9 -1
- package/src/v2/mcp/tools.js +185 -12
- package/src/v2/observation.js +145 -0
- package/src/v2/run.js +133 -9
- package/src/v2/selfcheck.js +297 -11
- package/src/v2/store.js +16 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call what a module exports, with fixed inputs, and print the answers.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS. Until 2026-08-31 a library was checked by importing it and writing
|
|
5
|
+
* down the NAMES and SHAPES of what it exported — "slug: a function taking 1 argument". That
|
|
6
|
+
* is a real thing to compare and it is not the thing that breaks. Measured that day on a
|
|
7
|
+
* four-line library: the separator inside `slug` was changed from "-" to "_", so every web
|
|
8
|
+
* address the library produces became a different string, and `isReserved('admin')` went from
|
|
9
|
+
* true to false. Both exports still existed, both still took one argument, so every address
|
|
10
|
+
* agreed and `staysfixed check` answered "Nothing that worked has changed" and exited 0. That
|
|
11
|
+
* is the one answer this tool may never give. Nothing here calls anything, so nothing here
|
|
12
|
+
* could ever have seen it.
|
|
13
|
+
*
|
|
14
|
+
* This program is what closes that hole. It imports the module the same way the shape probe
|
|
15
|
+
* does, calls every exported function it is willing to call with a fixed ladder of inputs,
|
|
16
|
+
* and prints one line per call. The lines are compared like any other output, so a function
|
|
17
|
+
* that starts answering differently changes the text and the check fails with the old answer
|
|
18
|
+
* and the new one side by side.
|
|
19
|
+
*
|
|
20
|
+
* WHY IT IS A FILE AND NOT A STRING. The shape probe is built as one long JavaScript string
|
|
21
|
+
* and handed to `node -e`, wrapped in single quotes. That works on this machine and it makes
|
|
22
|
+
* the code unreadable, and single quotes are not how Windows quotes anything. A file is run
|
|
23
|
+
* by path, reads the same as the rest of the codebase, and can be tested on its own.
|
|
24
|
+
*
|
|
25
|
+
* WHAT KEEPS THIS SAFE. Four things, and none of them is optional:
|
|
26
|
+
*
|
|
27
|
+
* 1. It only ever runs inside the scratch copy the process adapter makes, never in
|
|
28
|
+
* anybody's working folder, with a stopped clock, a scratch HOME and a scratch temp
|
|
29
|
+
* folder, and with every outbound connection recorded and then refused at the wire.
|
|
30
|
+
* That boundary is not this file's doing and this file must never be run outside it.
|
|
31
|
+
* 2. It refuses by name. A function whose name says it deletes, sends, publishes, charges
|
|
32
|
+
* or migrates is NOT called — it is named in the output as not called, with the reason,
|
|
33
|
+
* so the hole is visible instead of silent. This is the same generous name-based guess
|
|
34
|
+
* the tool already makes about routes and commands, made in the same words.
|
|
35
|
+
* 3. It never constructs a class and never reads a property that is really a getter,
|
|
36
|
+
* because both of those run somebody else's code without the name of the thing being
|
|
37
|
+
* called ever appearing at the call site.
|
|
38
|
+
* 4. It stops. Every call has a deadline and the whole run has a deadline, and whatever it
|
|
39
|
+
* did not reach is printed as not reached rather than left out.
|
|
40
|
+
*
|
|
41
|
+
* WHAT IT STILL CANNOT SEE, said here because a reader of the output deserves to know: a
|
|
42
|
+
* function that only misbehaves on an input this ladder does not contain, and a function
|
|
43
|
+
* this program refused to call. Both are named in the output.
|
|
44
|
+
*
|
|
45
|
+
* Usage: node answers-probe.js <module id> (with the working directory inside the copy)
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { existsSync } from 'node:fs';
|
|
49
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
ANSWERS_END,
|
|
53
|
+
ANSWERS_START,
|
|
54
|
+
CALLED_PREFIX,
|
|
55
|
+
MAX_ARGS,
|
|
56
|
+
MAX_FUNCTIONS,
|
|
57
|
+
NOT_CALLED_PREFIX,
|
|
58
|
+
PER_CALL_MS,
|
|
59
|
+
PROBE_INPUTS,
|
|
60
|
+
WHOLE_RUN_MS,
|
|
61
|
+
describeInput,
|
|
62
|
+
whyItWouldNotBeCalled,
|
|
63
|
+
} from './from-exports.js';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Where the module really is.
|
|
67
|
+
*
|
|
68
|
+
* The rule is copied deliberately from the shape probe rather than reinvented: "starts with a
|
|
69
|
+
* dot, or has a slash in it" was the old test, and `index.js` has neither, so Node was asked
|
|
70
|
+
* for a PACKAGE by that name and answered that it did not exist. `staysfixed init` writes
|
|
71
|
+
* exactly `{ module: "index.js" }` for an ordinary package entry, so getting this wrong makes
|
|
72
|
+
* the probe fail identically on both builds — which produces no difference at all and reads
|
|
73
|
+
* exactly like a clean check.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} id
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
function resolveModule(id) {
|
|
79
|
+
const asFile = new URL(id, pathToFileURL(`${process.cwd()}/`)).href;
|
|
80
|
+
let onDisk = false;
|
|
81
|
+
try {
|
|
82
|
+
onDisk = existsSync(fileURLToPath(asFile));
|
|
83
|
+
} catch {
|
|
84
|
+
onDisk = false;
|
|
85
|
+
}
|
|
86
|
+
const looksLikeAPath = id.startsWith('.') || id.startsWith('/') || id.includes('/');
|
|
87
|
+
return looksLikeAPath || onDisk ? asFile : id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Is this function really a class?
|
|
92
|
+
*
|
|
93
|
+
* Constructing a stranger's class is not something this program is willing to do — a
|
|
94
|
+
* constructor can open a file, start a server or connect to a database, and unlike a plain
|
|
95
|
+
* call there is no useful answer to compare afterwards. Calling one without `new` throws, and
|
|
96
|
+
* a page of identical TypeErrors would drown the answers that matter.
|
|
97
|
+
*
|
|
98
|
+
* @param {Function} fn
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
function isAClass(fn) {
|
|
102
|
+
try {
|
|
103
|
+
return /^\s*class[\s{]/.test(Function.prototype.toString.call(fn));
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One value, written the same way every time.
|
|
111
|
+
*
|
|
112
|
+
* Two runs of identical code have to produce identical text here or the whole feature becomes
|
|
113
|
+
* a flake generator, so: object keys are sorted, lists and depth are capped, and anything
|
|
114
|
+
* this function does not recognise is described rather than printed. Nothing reads the clock
|
|
115
|
+
* and nothing reads a random number.
|
|
116
|
+
*
|
|
117
|
+
* @param {unknown} value
|
|
118
|
+
* @param {number} [depth]
|
|
119
|
+
* @param {Set<unknown>} [seen]
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
export function write(value, depth = 0, seen = new Set()) {
|
|
123
|
+
if (value === null) return 'null';
|
|
124
|
+
if (value === undefined) return 'undefined';
|
|
125
|
+
const type = typeof value;
|
|
126
|
+
if (type === 'string') return JSON.stringify(value);
|
|
127
|
+
if (type === 'number') return Object.is(value, -0) ? '-0' : String(value);
|
|
128
|
+
if (type === 'boolean') return String(value);
|
|
129
|
+
if (type === 'bigint') return `${value}n`;
|
|
130
|
+
if (type === 'symbol') return String(value);
|
|
131
|
+
if (type === 'function') {
|
|
132
|
+
const fn = /** @type {Function} */ (value);
|
|
133
|
+
const named = fn.name ? ` called ${fn.name}` : '';
|
|
134
|
+
return `a function${named} taking ${fn.length} ${fn.length === 1 ? 'argument' : 'arguments'}`;
|
|
135
|
+
}
|
|
136
|
+
if (value instanceof Error) return `${value.name}: ${cleanMessage(value.message)}`;
|
|
137
|
+
if (value instanceof Date) {
|
|
138
|
+
// The clock is stopped inside the boundary this runs in, so a date is reproducible. It is
|
|
139
|
+
// written in full rather than bucketed because a library that formats dates is exactly the
|
|
140
|
+
// kind of library whose output must be compared to the character.
|
|
141
|
+
return `Date(${Number.isNaN(value.getTime()) ? 'invalid' : value.toISOString()})`;
|
|
142
|
+
}
|
|
143
|
+
if (value instanceof RegExp) return String(value);
|
|
144
|
+
if (seen.has(value)) return '<the same object again>';
|
|
145
|
+
if (depth >= 3) return Array.isArray(value) ? `a list of ${value.length}` : 'an object, not opened this far down';
|
|
146
|
+
seen.add(value);
|
|
147
|
+
try {
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
const shown = value.slice(0, 20).map((item) => write(item, depth + 1, seen));
|
|
150
|
+
return `[${shown.join(', ')}${value.length > 20 ? `, and ${value.length - 20} more` : ''}]`;
|
|
151
|
+
}
|
|
152
|
+
if (value instanceof Set) {
|
|
153
|
+
const items = [...value].slice(0, 20).map((item) => write(item, depth + 1, seen)).sort();
|
|
154
|
+
return `Set{${items.join(', ')}${value.size > 20 ? `, and ${value.size - 20} more` : ''}}`;
|
|
155
|
+
}
|
|
156
|
+
if (value instanceof Map) {
|
|
157
|
+
const items = [...value.entries()]
|
|
158
|
+
.slice(0, 20)
|
|
159
|
+
.map(([k, v]) => `${write(k, depth + 1, seen)}: ${write(v, depth + 1, seen)}`)
|
|
160
|
+
.sort();
|
|
161
|
+
return `Map{${items.join(', ')}${value.size > 20 ? `, and ${value.size - 20} more` : ''}}`;
|
|
162
|
+
}
|
|
163
|
+
// Own keys only, sorted, and read through a descriptor so a getter is never fired. A
|
|
164
|
+
// getter is somebody else's code running without its name appearing at any call site,
|
|
165
|
+
// which is the one thing this program refuses to do by accident.
|
|
166
|
+
const keys = Object.keys(/** @type {object} */ (value)).sort().slice(0, 30);
|
|
167
|
+
const parts = keys.map((key) => {
|
|
168
|
+
const d = Object.getOwnPropertyDescriptor(/** @type {object} */ (value), key);
|
|
169
|
+
if (d && !('value' in d)) return `${key}: a computed property, not read`;
|
|
170
|
+
return `${key}: ${write(d?.value, depth + 1, seen)}`;
|
|
171
|
+
});
|
|
172
|
+
const total = Object.keys(/** @type {object} */ (value)).length;
|
|
173
|
+
return `{${parts.join(', ')}${total > 30 ? `, and ${total - 30} more` : ''}}`;
|
|
174
|
+
} finally {
|
|
175
|
+
seen.delete(value);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* An error message with this machine's own paths taken out of it.
|
|
181
|
+
*
|
|
182
|
+
* A stack trace or a message carrying the scratch folder's absolute path differs between two
|
|
183
|
+
* builds for a reason that has nothing to do with the product — the two builds are walked in
|
|
184
|
+
* two different scratch folders — and that difference would arrive as a finding nobody caused.
|
|
185
|
+
* The tool strips its own footprint out of stdout further downstream as well; this is the
|
|
186
|
+
* cheap half done at the source.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} text
|
|
189
|
+
* @returns {string}
|
|
190
|
+
*/
|
|
191
|
+
function cleanMessage(text) {
|
|
192
|
+
return String(text ?? '')
|
|
193
|
+
.split('\n')[0]
|
|
194
|
+
.split(process.cwd()).join('.')
|
|
195
|
+
.slice(0, 300);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Call one function once and say what came back, in one line, whatever happened.
|
|
200
|
+
*
|
|
201
|
+
* A throw is an answer and is written down as one. "Still throws the same TypeError" and
|
|
202
|
+
* "used to throw and now returns a number" are both facts worth comparing, and a probe that
|
|
203
|
+
* only recorded successful calls would report the second as nothing at all.
|
|
204
|
+
*
|
|
205
|
+
* @param {Function} fn
|
|
206
|
+
* @param {unknown[]} args
|
|
207
|
+
* @returns {Promise<string>}
|
|
208
|
+
*/
|
|
209
|
+
async function callOnce(fn, args) {
|
|
210
|
+
let answer;
|
|
211
|
+
try {
|
|
212
|
+
answer = fn(...args);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
return `threw ${write(e)}`;
|
|
215
|
+
}
|
|
216
|
+
if (answer === null || typeof answer !== 'object' || typeof (/** @type {any} */ (answer).then) !== 'function') {
|
|
217
|
+
return write(answer);
|
|
218
|
+
}
|
|
219
|
+
// A promise is awaited, because the answer of an async function IS the thing worth
|
|
220
|
+
// comparing and "a promise" is the same nine characters whatever the library does. It is
|
|
221
|
+
// awaited under a deadline: an async function that never settles would otherwise hold the
|
|
222
|
+
// whole run until the journey's own timeout killed it, and a killed journey reports nothing
|
|
223
|
+
// about any of the functions that came after it.
|
|
224
|
+
let timer;
|
|
225
|
+
const deadline = new Promise((resolve) => {
|
|
226
|
+
timer = setTimeout(() => resolve('__staysfixed_no_answer__'), PER_CALL_MS);
|
|
227
|
+
// Unreferenced so it can never be the reason this program stays alive.
|
|
228
|
+
if (typeof timer?.unref === 'function') timer.unref();
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
const settled = await Promise.race([
|
|
232
|
+
Promise.resolve(answer).then((v) => ({ ok: true, v }), (e) => ({ ok: false, v: e })),
|
|
233
|
+
deadline,
|
|
234
|
+
]);
|
|
235
|
+
if (settled === '__staysfixed_no_answer__') {
|
|
236
|
+
return `had still not answered after ${PER_CALL_MS}ms, so nothing about this call is compared`;
|
|
237
|
+
}
|
|
238
|
+
const s = /** @type {{ok: boolean, v: unknown}} */ (settled);
|
|
239
|
+
return s.ok ? `a promise for ${write(s.v)}` : `a promise that failed with ${write(s.v)}`;
|
|
240
|
+
} finally {
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The fixed ladder of arguments one function is called with.
|
|
247
|
+
*
|
|
248
|
+
* FIXED is the whole point. Random or generated inputs would make two runs of the same build
|
|
249
|
+
* disagree with each other, and this tool throws away anything that cannot answer the same
|
|
250
|
+
* way twice — so a fuzzed probe would produce a great deal of noise and nothing that could
|
|
251
|
+
* ever be compared. These are ordinary values a library actually receives, plus the empty and
|
|
252
|
+
* the wrong ones, because "used to throw on null and now returns undefined" is a real change.
|
|
253
|
+
*
|
|
254
|
+
* A function of more than one argument gets the same value in each slot, capped at three.
|
|
255
|
+
* Every combination of the ladder across three slots is a thousand calls per function, which
|
|
256
|
+
* is a cost nobody agreed to for an answer nobody reads.
|
|
257
|
+
*
|
|
258
|
+
* @param {Function} fn
|
|
259
|
+
* @returns {{args: unknown[], shown: string}[]}
|
|
260
|
+
*/
|
|
261
|
+
export function ladderFor(fn) {
|
|
262
|
+
const arity = Math.min(Math.max(Number(fn.length) || 0, 0), MAX_ARGS);
|
|
263
|
+
/** @type {{args: unknown[], shown: string}[]} */
|
|
264
|
+
const out = [{ args: [], shown: '' }];
|
|
265
|
+
if (arity === 0) return out;
|
|
266
|
+
for (const input of PROBE_INPUTS) {
|
|
267
|
+
const args = Array.from({ length: arity }, () => input.value);
|
|
268
|
+
out.push({ args, shown: args.map(() => describeInput(input)).join(', ') });
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function main() {
|
|
274
|
+
const id = process.argv[2];
|
|
275
|
+
if (!id) {
|
|
276
|
+
process.stderr.write('This probe needs the module to import as its one argument.\n');
|
|
277
|
+
process.exitCode = 2;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** @type {Record<string, unknown>} */
|
|
282
|
+
let module;
|
|
283
|
+
try {
|
|
284
|
+
module = await import(resolveModule(id));
|
|
285
|
+
} catch (e) {
|
|
286
|
+
// Said on the complaints channel and NOT on stdout, so the answers block is never half
|
|
287
|
+
// written. The run around this reads an empty stdout plus a non-zero exit as "this never
|
|
288
|
+
// reached the product", which is exactly what happened, and records it as a hole.
|
|
289
|
+
process.stderr.write(`Could not import ${id}: ${cleanMessage(/** @type {Error} */ (e)?.message ?? String(e))}\n`);
|
|
290
|
+
process.exitCode = 1;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const write_ = (/** @type {string} */ line) => process.stdout.write(`${line}\n`);
|
|
295
|
+
write_(ANSWERS_START);
|
|
296
|
+
write_(`module: ${id}`);
|
|
297
|
+
|
|
298
|
+
const names = Object.keys(module).sort();
|
|
299
|
+
/** @type {string[]} */
|
|
300
|
+
const called = [];
|
|
301
|
+
/** @type {string[]} */
|
|
302
|
+
const refused = [];
|
|
303
|
+
/** @type {string[]} */
|
|
304
|
+
const notReached = [];
|
|
305
|
+
/** @type {string[]} */
|
|
306
|
+
const values = [];
|
|
307
|
+
|
|
308
|
+
const startedAt = Date.now();
|
|
309
|
+
let functions = 0;
|
|
310
|
+
|
|
311
|
+
for (const name of names) {
|
|
312
|
+
let value;
|
|
313
|
+
try {
|
|
314
|
+
value = module[name];
|
|
315
|
+
} catch (e) {
|
|
316
|
+
refused.push(`${name} (crashed) — reading it threw ${write(e)}, so it was never called.`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (typeof value !== 'function') {
|
|
320
|
+
// A VALUE IS AN ANSWER TOO, and until 2026-08-31 nothing compared one. The shape probe
|
|
321
|
+
// records an exported string as the words "some text" and an exported number as
|
|
322
|
+
// "number", so `export const API_URL = 'https://api.example.com'` could be repointed at
|
|
323
|
+
// a staging server, or a list of reserved words emptied, and every address agreed. It is
|
|
324
|
+
// the same false all-clear as the one this file exists to stop, wearing a constant
|
|
325
|
+
// instead of a function. The value is written out here in full, deterministically.
|
|
326
|
+
values.push(`${name} = ${write(value)}`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const fn = /** @type {Function} */ (value);
|
|
330
|
+
|
|
331
|
+
if (isAClass(fn)) {
|
|
332
|
+
refused.push(`${name} (not supported here) — it is a class, and this tool never builds one: a constructor can do anything and gives no answer to compare.`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const why = whyItWouldNotBeCalled(name);
|
|
336
|
+
if (why) {
|
|
337
|
+
refused.push(`${name} (irreversible) — ${why}`);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (functions >= MAX_FUNCTIONS) {
|
|
341
|
+
notReached.push(name);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (Date.now() - startedAt > WHOLE_RUN_MS) {
|
|
345
|
+
notReached.push(name);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
functions++;
|
|
350
|
+
called.push(name);
|
|
351
|
+
for (const rung of ladderFor(fn)) {
|
|
352
|
+
const answer = await callOnce(fn, rung.args);
|
|
353
|
+
write_(`${name}(${rung.shown}) -> ${answer}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const line of values) write_(line);
|
|
358
|
+
write_(ANSWERS_END);
|
|
359
|
+
write_(`${CALLED_PREFIX}${called.length === 0 ? '(none)' : called.join(', ')}`);
|
|
360
|
+
// ONE LINE PER NAME, never a list on one line, and each carrying the KIND of hole it is in
|
|
361
|
+
// brackets. Whatever reads this back turns each of these into its own recorded hole at that
|
|
362
|
+
// function's own address — and the kind decides which sentence the owner is shown, because
|
|
363
|
+
// "doing this for real would destroy data" and "this tool cannot observe a class" are two
|
|
364
|
+
// different facts and reporting both as "the project asked us not to" is a third thing that
|
|
365
|
+
// is true of neither. A line naming four functions at once would arrive as one hole with a
|
|
366
|
+
// made-up name and three functions that silently read as fine.
|
|
367
|
+
for (const line of refused) write_(`${NOT_CALLED_PREFIX}${line}`);
|
|
368
|
+
for (const name of notReached) {
|
|
369
|
+
write_(
|
|
370
|
+
`${NOT_CALLED_PREFIX}${name} (timed out) — the probe had already called ${MAX_FUNCTIONS} functions or spent ` +
|
|
371
|
+
`${Math.round(WHOLE_RUN_MS / 1000)} seconds calling, so this one was never reached and nothing here says anything about it.`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await main();
|