@descent-vtt/spec-harness 0.18.0 → 0.20.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 (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +372 -0
  4. package/README.md +78 -11
  5. package/dist/briefs.d.ts +45 -0
  6. package/dist/briefs.js +73 -0
  7. package/dist/briefs.js.map +1 -1
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +384 -84
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands.js +7 -2
  12. package/dist/commands.js.map +1 -1
  13. package/dist/config.d.ts +15 -1
  14. package/dist/config.js +16 -3
  15. package/dist/config.js.map +1 -1
  16. package/dist/configure.d.ts +142 -32
  17. package/dist/configure.js +316 -89
  18. package/dist/configure.js.map +1 -1
  19. package/dist/fs.d.ts +150 -3
  20. package/dist/fs.js +459 -17
  21. package/dist/fs.js.map +1 -1
  22. package/dist/git.d.ts +85 -9
  23. package/dist/git.js +140 -16
  24. package/dist/git.js.map +1 -1
  25. package/dist/hooks.d.ts +111 -0
  26. package/dist/hooks.js +172 -16
  27. package/dist/hooks.js.map +1 -1
  28. package/dist/round.d.ts +8 -0
  29. package/dist/round.js +32 -9
  30. package/dist/round.js.map +1 -1
  31. package/dist/sandbox.js +4 -3
  32. package/dist/sandbox.js.map +1 -1
  33. package/dist/server.d.ts +25 -1
  34. package/dist/server.js +194 -27
  35. package/dist/server.js.map +1 -1
  36. package/dist/setup.d.ts +6 -3
  37. package/dist/setup.js +74 -23
  38. package/dist/setup.js.map +1 -1
  39. package/dist/siblings.d.ts +42 -7
  40. package/dist/siblings.js +135 -30
  41. package/dist/siblings.js.map +1 -1
  42. package/dist/types.d.ts +5 -0
  43. package/dist/types.js.map +1 -1
  44. package/dist/usage.d.ts +66 -12
  45. package/dist/usage.js +134 -21
  46. package/dist/usage.js.map +1 -1
  47. package/dist/workspace.d.ts +80 -1
  48. package/dist/workspace.js +188 -8
  49. package/dist/workspace.js.map +1 -1
  50. package/hooks/hooks.json +16 -0
  51. package/package.json +2 -2
package/dist/fs.js CHANGED
@@ -2,28 +2,115 @@
2
2
  * The edge where the harness meets the disk: the configuration, the state
3
3
  * directory outside the work tree, and paths as the filesystem spells them.
4
4
  */
5
- import { existsSync, realpathSync, statSync } from 'node:fs';
6
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
7
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
6
+ import { lstat, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
7
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
8
  import { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig } from './config.js';
9
- /** Reads `.spec-harness.json` at the root, or the defaults when there is none. */
9
+ /** What stands at a path, links followed, or `null` where nothing does: a link to nothing is nothing. */
10
+ function standing(path) {
11
+ try {
12
+ return statSync(path);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /**
19
+ * Whether a regular file stands at a path, links followed. Only one is read
20
+ * where a file is expected: opened, a named pipe nothing writes to is read
21
+ * without end, and one read as the process waits on it, as the harness reads
22
+ * a few small files, stops every timer with it (ADR-0002).
23
+ */
24
+ export function isFile(path) {
25
+ return standing(path)?.isFile() === true;
26
+ }
27
+ /**
28
+ * Reads `.spec-harness.json` at the root, or the defaults when there is none.
29
+ *
30
+ * Only a file is read. A named pipe under the file's name was read without
31
+ * end, by every command and by the hook before every write; and Node's words
32
+ * for a directory, EISDIR, are not the harness's. A file that cannot be read
33
+ * is said as that, with the reason: one the account may not read, or one
34
+ * another process holds open on Windows, was said to be JSON that is not
35
+ * valid, which sent a person to its text. What does not load is thrown with
36
+ * the path it stands at, which the hook lets a write reach (ADR-0005).
37
+ */
10
38
  export async function loadConfig(root) {
11
39
  const file = join(root, CONFIG_FILE);
12
- if (!existsSync(file))
40
+ const there = standing(file);
41
+ if (there === null)
13
42
  return { config: DEFAULT_CONFIG, file: null };
14
- let raw;
43
+ if (!there.isFile())
44
+ throw new ConfigError(`${CONFIG_FILE} is ${there.isDirectory() ? 'a directory' : 'neither a file nor a directory'}, not a configuration file`, file);
45
+ let text;
15
46
  try {
16
47
  // A Buffer parses as its text, so the encoding is equivalent to its mutant.
17
- raw = JSON.parse(await readFile(file, 'utf8'));
48
+ text = await readFile(file, 'utf8');
49
+ }
50
+ catch (error) {
51
+ throw new ConfigError(`${CONFIG_FILE} cannot be read: ${error.message}`, file);
52
+ }
53
+ let raw;
54
+ try {
55
+ raw = JSON.parse(text);
18
56
  }
19
57
  catch (error) {
20
- // A directory under the file's name is not JSON that went wrong, and
21
- // Node's words for it, EISDIR, are not the harness's.
22
- if (error.code === 'EISDIR')
23
- throw new ConfigError(`${CONFIG_FILE} is a directory, not a configuration file`);
24
- throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${error.message}`);
58
+ throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${error.message}`, file);
59
+ }
60
+ try {
61
+ return { config: parseConfig(raw), file };
62
+ }
63
+ catch (error) {
64
+ // parseConfig throws nothing but its own refusal, whose words are kept.
65
+ throw new ConfigError(error.message, file);
66
+ }
67
+ }
68
+ /**
69
+ * The real path of a file that stands at `path` under that name alone: a
70
+ * file, not a link to one, and with no other name, since a write to a file
71
+ * with two names writes both. `null` for anything else, and for nothing.
72
+ */
73
+ function soleFile(path) {
74
+ try {
75
+ const entry = lstatSync(path);
76
+ return entry.isFile() && entry.nlink === 1 ? realpathSync.native(path) : null;
77
+ }
78
+ catch {
79
+ // Nothing at the path, or nothing the account may look at. Its callers
80
+ // compare the answer with a real path, which neither `null` nor what an
81
+ // empty block answers is, so that mutant is equivalent.
82
+ return null;
83
+ }
84
+ }
85
+ /**
86
+ * Whether every path of a write names `file` and nothing else: a file that
87
+ * stands under that name alone, which the path names as the hook resolves a
88
+ * path to decide it ({@link realSpelling}), and as the filesystem resolves
89
+ * it to write, which on Linux and macOS follows a link before a `..` where
90
+ * the hook reads the `..` first. A path that only reads like it, a link to
91
+ * it, a second name of it, and a file of another work tree are none of
92
+ * these. `cwd` is what a relative path is read from, its words kept as
93
+ * written.
94
+ */
95
+ export function writesOnly(paths, file, cwd) {
96
+ // Where the file is none, no path's real spelling is `null`, so no path
97
+ // names it.
98
+ const own = soleFile(file);
99
+ return paths.every((given) => {
100
+ const path = isAbsolute(given) ? given : `${cwd}${sep}${given}`;
101
+ return soleFile(path) === own && realSpelling(path) === own;
102
+ });
103
+ }
104
+ /**
105
+ * A path where the harness keeps its state that holds something else: said
106
+ * in a line, exit 2, where Node's ENOTDIR and a stack sent a person looking
107
+ * for a defect (spec-core ADR-0005).
108
+ */
109
+ export class StateError extends Error {
110
+ constructor(message) {
111
+ super(message);
112
+ this.name = 'StateError';
25
113
  }
26
- return { config: parseConfig(raw), file };
27
114
  }
28
115
  /**
29
116
  * The real spelling of a path: links resolved and, on a filesystem that
@@ -63,6 +150,17 @@ export function realSpelling(path) {
63
150
  export function isTop(directory, top) {
64
151
  return realSpelling(directory).split(sep).join('/') === top;
65
152
  }
153
+ /**
154
+ * The repository's main work tree, where git keeps the repository as that
155
+ * work tree's `.git`, as `git init` and `git clone` do and every linked
156
+ * worktree's common directory then is: `null` for a common directory of any
157
+ * other name, a bare repository's, a submodule's under its superproject's
158
+ * `modules`, or one kept apart with `--separate-git-dir`. Read from the
159
+ * name, with no process: it says only where an install is looked for.
160
+ */
161
+ export function mainWorkTree(commonDir) {
162
+ return basename(commonDir) === '.git' ? dirname(commonDir) : null;
163
+ }
66
164
  /** Whether a `.git` stands in `directory`, a repository's or the file that names one, as at the top of a work tree. */
67
165
  export function holdsGit(directory) {
68
166
  return existsSync(join(directory, '.git'));
@@ -81,12 +179,344 @@ export function repositoryPath(given, root, cwd) {
81
179
  return null;
82
180
  return rel.split(sep).join('/');
83
181
  }
84
- /** The harness's own directory under the git common directory, created on demand (ADR-0003). */
182
+ /**
183
+ * The top of every work tree of the repository whose common directory is
184
+ * `common`, `top` among them, read with no process: the main work tree,
185
+ * whose `.git` the common directory is, and each linked worktree, by the
186
+ * `gitdir` file git keeps for it under the common directory's `worktrees`,
187
+ * which names the worktree's `.git` (gitrepository-layout(5)), absolute or
188
+ * from its own directory. `null` where they cannot all be named: a common
189
+ * directory of any other name, whose main work tree only its configuration
190
+ * names, and an entry under `worktrees` that cannot be read.
191
+ */
192
+ export function workTrees(top, common) {
193
+ const main = mainWorkTree(common);
194
+ if (main === null)
195
+ return null;
196
+ const kept = join(common, 'worktrees');
197
+ let names = [];
198
+ try {
199
+ names = readdirSync(kept);
200
+ }
201
+ catch (error) {
202
+ // No such directory is no linked worktree, and anything else there is
203
+ // one that cannot be read.
204
+ if (error.code !== 'ENOENT')
205
+ return null;
206
+ }
207
+ const linked = names.map((name) => textOf(join(kept, name, 'gitdir')));
208
+ if (linked.includes(null))
209
+ return null;
210
+ // The line's end stands in the last name, the worktree's `.git`, which
211
+ // dirname takes away with it.
212
+ return [top, main, ...names.map((name, at) => dirname(resolve(kept, name, linked[at])))];
213
+ }
214
+ /**
215
+ * Whether every path lies outside every work tree of the repository whose
216
+ * top and common directory are given: such a path is no file of the
217
+ * repository's, so no brief of its decides it, whatever is wrong with the
218
+ * brief or the configuration (ADR-0005). Where the work trees cannot all
219
+ * be named, a path is not known to be outside them, and this is `false`.
220
+ */
221
+ export function outsideEvery(paths, top, common, cwd) {
222
+ // A write that names no path is no write outside: no caller asks of
223
+ // one, and a test holds the answer.
224
+ if (paths.length === 0)
225
+ return false;
226
+ // A path in the work tree that was opened, as nearly every write's is,
227
+ // is told with nothing of git's directory read; read anyway, `top` is
228
+ // among the work trees, so the mutant without this test is equivalent.
229
+ if (paths.some((path) => repositoryPath(path, top, cwd) !== null))
230
+ return false;
231
+ // A path is read here as the hook reads one, its `..` before its links,
232
+ // where Linux and macOS write it after: `/elsewhere/link/../a.ts`, the
233
+ // link to a directory of the work tree, reads as outside and is written
234
+ // inside. Such a path is not known to be outside.
235
+ if (paths.some((path) => path.split(/[\\/]/).includes('..')))
236
+ return false;
237
+ const trees = workTrees(top, common);
238
+ return trees !== null && paths.every((path) => trees.every((tree) => repositoryPath(path, tree, cwd) === null));
239
+ }
240
+ /**
241
+ * The harness's own directory under the git common directory, created on
242
+ * demand (ADR-0003). Where something that is no directory stands at its path,
243
+ * or at one between it and git's, Node refuses to make it, with ENOTDIR or
244
+ * EEXIST, and the harness says which path holds what.
245
+ */
85
246
  export async function stateDirectory(commonDir, ...parts) {
86
247
  const directory = join(commonDir, 'spec-harness', ...parts);
87
- await mkdir(directory, { recursive: true });
248
+ try {
249
+ await mkdir(directory, { recursive: true });
250
+ }
251
+ catch (error) {
252
+ const { code } = error;
253
+ if (code !== 'ENOTDIR' && code !== 'EEXIST')
254
+ throw error;
255
+ // mkdir refuses for this alone, so one of the steps is there and is no
256
+ // directory, and every step before it is one.
257
+ const steps = ['spec-harness', ...parts].map((_, at, all) => join(commonDir, ...all.slice(0, at + 1)));
258
+ const there = steps.find((step) => !standing(step).isDirectory());
259
+ throw new StateError(`${there} is not a directory, where spec-harness keeps its state in one: move it away, and run the command again`);
260
+ }
88
261
  return directory;
89
262
  }
263
+ /**
264
+ * A work tree and its git directories as the files git keeps name them, read
265
+ * with no process: the nearest directory at or above `root` that holds a
266
+ * `.git`; the git directory, that `.git` or the one a `.git` file names
267
+ * (`gitdir: <path>`, relative to the file's directory); and the common
268
+ * directory, the one that git directory's `commondir` file names, relative
269
+ * to it, or the git directory itself (gitrepository-layout(5)). `exact` is
270
+ * whether git, asked, would name the same work tree: not where
271
+ * `GIT_DIR` or `GIT_WORK_TREE` sends it elsewhere, and not where the
272
+ * repository's configuration names a `worktree`, which `core.worktree`
273
+ * moves it to (git-config(1)), or keeps one of its own for a worktree.
274
+ * `null` where no directory up to the top of the filesystem holds one, or
275
+ * a `.git` file names none.
276
+ */
277
+ export function gitDirectories(root, env) {
278
+ let top = realSpelling(root);
279
+ let found = null;
280
+ for (;;) {
281
+ found = standing(join(top, '.git'));
282
+ if (found !== null)
283
+ break;
284
+ const parent = dirname(top);
285
+ if (parent === top)
286
+ return null;
287
+ top = parent;
288
+ }
289
+ const dotGit = join(top, '.git');
290
+ let gitDir = dotGit;
291
+ if (found.isFile()) {
292
+ // The path is the rest of the line: `.` stops at a line's end, a
293
+ // carriage return among them, so the mutant that drops `$` is
294
+ // equivalent; and a Buffer is read as its text, so is the encoding's.
295
+ const named = /^gitdir: (.+)$/m.exec(readFileSync(dotGit, 'utf8'))?.[1];
296
+ if (named === undefined)
297
+ return null;
298
+ gitDir = resolve(top, named);
299
+ }
300
+ const commondir = textOf(join(gitDir, 'commondir'));
301
+ const common = commondir === null ? gitDir : resolve(gitDir, commondir.trim());
302
+ const moved = env['GIT_DIR'] !== undefined || env['GIT_WORK_TREE'] !== undefined;
303
+ // No configuration holds no key, as a text without one does: the mutant
304
+ // of the empty text is equivalent.
305
+ const elsewhere = /^\s*worktree\s*=/im.test(textOf(join(common, 'config')) ?? '') || existsSync(join(gitDir, 'config.worktree'));
306
+ return { top, git: gitDir, common, exact: !moved && !elsewhere };
307
+ }
308
+ /** A file's text, or `null` where it cannot be read or is no file. */
309
+ function textOf(file) {
310
+ if (!isFile(file))
311
+ return null;
312
+ try {
313
+ return readFileSync(file, 'utf8');
314
+ }
315
+ catch {
316
+ return null;
317
+ }
318
+ }
319
+ /**
320
+ * How long a call's decisions are kept for a run after the write that does
321
+ * not come: a call refused by the person, one that failed, one stopped
322
+ * (ADR-0002). A day, since a call waits on a person's prompt for as long as
323
+ * the person is away, and an entry removed too soon costs only that call's
324
+ * answer being decided again.
325
+ */
326
+ export const CALL_LIFE_MS = 24 * 60 * 60 * 1000;
327
+ /**
328
+ * Keeps what a run before a write decided, under the common directory, for
329
+ * the run after it, and removes what no run after a write read in a day.
330
+ * Nothing here may change the answer before the write: whatever fails is
331
+ * passed over, and the run after the write then decides as it did.
332
+ */
333
+ export async function keepCall(commonDir, name, text, now = Date.now()) {
334
+ try {
335
+ const directory = await stateDirectory(commonDir, 'calls');
336
+ await writeAtomic(join(directory, name), text);
337
+ // A file another run takes away before it is removed here ends the
338
+ // pruning in the catch below, where `force` would pass over it, and what
339
+ // is left is pruned by a later run: the mutants of `force` are
340
+ // equivalent.
341
+ for (const entry of await readdir(directory)) {
342
+ const file = join(directory, entry);
343
+ if (now - (await stat(file)).mtimeMs > CALL_LIFE_MS)
344
+ await rm(file, { force: true });
345
+ }
346
+ }
347
+ catch {
348
+ // A full disk, a directory another run removed: the call is decided again after the write.
349
+ }
350
+ }
351
+ /** What was kept for a call, taken away as it is read; `null` where nothing was. */
352
+ export async function takeCall(commonDir, name) {
353
+ const file = join(commonDir, 'spec-harness', 'calls', name);
354
+ // What is no file holds nothing kept, and is written over at the next write.
355
+ if (!isFile(file))
356
+ return null;
357
+ try {
358
+ const text = await readFile(file, 'utf8');
359
+ // Taken away by another run since it was read, the mutants of `force`
360
+ // decide the call again, which answers as the text read does: they are
361
+ // equivalent.
362
+ await rm(file, { force: true });
363
+ return text;
364
+ }
365
+ catch {
366
+ return null;
367
+ }
368
+ }
369
+ /** SHA-256, loaded where something is hashed: a run that keeps no answer loads no hash (ADR-0002). */
370
+ export async function hasher() {
371
+ const { createHash } = await import('node:crypto');
372
+ return (data) => createHash('sha256').update(data).digest('hex');
373
+ }
374
+ /**
375
+ * How long a session's answer of spec-brief's is kept once it was last
376
+ * kept (ADR-0003). A day, as a call's: an answer removed costs one start of
377
+ * spec-brief, and one kept is a few kilobytes.
378
+ */
379
+ export const ANSWER_LIFE_MS = 24 * 60 * 60 * 1000;
380
+ /**
381
+ * Keeps a session's answer of spec-brief's under the common directory, with
382
+ * the key it answers, and removes what was kept over a day ago. Whatever
383
+ * fails is passed over: the answer is the one given whether or not it is
384
+ * kept.
385
+ */
386
+ export async function keepAnswer(commonDir, name, key, answer, now = Date.now()) {
387
+ try {
388
+ const directory = await stateDirectory(commonDir, 'answers');
389
+ await writeAtomic(join(directory, name), `${JSON.stringify({ key, answer })}\n`);
390
+ // A file another run takes away before it is removed here ends the
391
+ // pruning in the catch below, where `force` would pass over it, and what
392
+ // is left is pruned by a later run: the mutants of `force` are
393
+ // equivalent, and so is the one that also removes a file kept a day ago
394
+ // to the millisecond.
395
+ for (const entry of await readdir(directory)) {
396
+ const file = join(directory, entry);
397
+ if (now - (await stat(file)).mtimeMs > ANSWER_LIFE_MS)
398
+ await rm(file, { force: true });
399
+ }
400
+ }
401
+ catch {
402
+ // A full disk, a directory another run removed: the next run starts spec-brief.
403
+ }
404
+ }
405
+ /**
406
+ * The answer kept under `name` for `key`, or `null` where there is none:
407
+ * nothing there, something that is not a file, a text that is not what
408
+ * `keepAnswer` writes, or another key's.
409
+ */
410
+ export async function keptAnswer(commonDir, name, key) {
411
+ const file = join(commonDir, 'spec-harness', 'answers', name);
412
+ try {
413
+ // Only a file is read: a named pipe would be read without end.
414
+ if (!(await stat(file)).isFile())
415
+ return null;
416
+ // A text that holds null throws on the read, and any other that is no
417
+ // object has no key: the catch answers for both. A Buffer parses as its
418
+ // text, so the encoding is equivalent to its mutant.
419
+ const kept = JSON.parse(await readFile(file, 'utf8'));
420
+ return kept.key === key && typeof kept.answer === 'string' ? kept.answer : null;
421
+ }
422
+ catch {
423
+ // Falling out of the catch answers undefined, which no answer is read
424
+ // from either: the mutant without the return is equivalent.
425
+ return null;
426
+ }
427
+ }
428
+ /** Whether a path that could not be read names nothing: it, or a directory on the way to it, is not there. */
429
+ function nothingThere(error) {
430
+ const { code } = error;
431
+ return code === 'ENOENT' || code === 'ENOTDIR';
432
+ }
433
+ /**
434
+ * Whether a file reads as listed: a file, never a pipe or a directory, whose
435
+ * bytes have the digest; or, listed as not there, nothing at its path.
436
+ */
437
+ async function fileReads(file, sha256, hash) {
438
+ let there;
439
+ try {
440
+ there = await stat(file);
441
+ }
442
+ catch (error) {
443
+ return sha256 === null && nothingThere(error);
444
+ }
445
+ // Only a file is read: a named pipe would be read without end. A file
446
+ // listed as not there has no digest a file's bytes have.
447
+ if (!there.isFile())
448
+ return false;
449
+ return hash(await readFile(file)) === sha256;
450
+ }
451
+ /**
452
+ * Whether a directory is there and holds the files listed, and no other: a
453
+ * file as spec-brief lists one, an entry that is a file itself, where a
454
+ * directory or a link is not one. What is no directory, a named pipe among
455
+ * it, is refused by the listing, which throws. Listed as not there, nothing
456
+ * may stand at its path in any form, a link that leads nowhere among them.
457
+ */
458
+ async function entriesRead(directory, entries) {
459
+ if (entries === null) {
460
+ try {
461
+ await lstat(directory);
462
+ return false;
463
+ }
464
+ catch (error) {
465
+ return nothingThere(error);
466
+ }
467
+ }
468
+ // The entries are names no two of which are one (inputsOf), and so are a
469
+ // directory's: as many of each, every one of the one among the other, is
470
+ // the same names, in whatever order the disk lists them.
471
+ const names = (await readdir(directory, { withFileTypes: true })).flatMap((entry) => (entry.isFile() ? [entry.name] : []));
472
+ const listed = new Set(entries);
473
+ return names.length === listed.size && names.every((name) => listed.has(name));
474
+ }
475
+ /**
476
+ * Whether every input of an answer reads as listed, with no process
477
+ * started: each file by the digest of its bytes, or as not there; each
478
+ * directory by its entries; and, where git was asked, the index of the work
479
+ * tree's git directory and each ignore file. A path is read from the work
480
+ * tree's top, or as it stands where it is absolute. Anything that cannot be
481
+ * read, and an index that is not the one git reads for the work tree, is a
482
+ * doubt, and answers `false` (ADR-0002).
483
+ */
484
+ export async function readsAsListed(root, inputs, hash, env = process.env) {
485
+ const at = (path) => (isAbsolute(path) ? path : join(root, ...path.split('/')));
486
+ const git = inputs.git;
487
+ let index = null;
488
+ if (git !== null) {
489
+ // The index of the work tree's own git directory, as the files git keeps
490
+ // name it: not where git is sent elsewhere, GIT_DIR, GIT_WORK_TREE or a
491
+ // worktree key (`exact`), GIT_INDEX_FILE or GIT_COMMON_DIR. A top git
492
+ // named always holds a `.git`, so the test of none is the compiler's, and
493
+ // a null read past it throws, which the caller reads as a doubt: that
494
+ // mutant is equivalent. The top found is the work tree's own but where
495
+ // git names a top that holds no `.git`, a work tree `core.worktree`
496
+ // names away from its repository and inside another, whose index the
497
+ // one found would be.
498
+ const found = gitDirectories(root, env);
499
+ if (found === null || !found.exact || env['GIT_INDEX_FILE'] !== undefined || env['GIT_COMMON_DIR'] !== undefined || found.top !== realSpelling(root))
500
+ return false;
501
+ index = join(found.git, 'index');
502
+ }
503
+ // Made once every check is known to be made, so that none is left to fail
504
+ // with no one reading it. A text put among them where git was asked
505
+ // nothing is one `every` reads as true, so that mutant is equivalent.
506
+ const checks = [
507
+ ...inputs.files.map((file) => fileReads(at(file.path), file.sha256, hash)),
508
+ ...inputs.directories.map((directory) => entriesRead(at(directory.path), directory.entries)),
509
+ ...(git === null ? [] : [fileReads(index, git.index, hash), ...git.ignores.map((file) => fileReads(at(file.path), file.sha256, hash))]),
510
+ ];
511
+ try {
512
+ return (await Promise.all(checks)).every((reads) => reads);
513
+ }
514
+ catch {
515
+ // Falling out of the catch answers undefined, which the caller reads as
516
+ // false: the mutant without the return is equivalent.
517
+ return false;
518
+ }
519
+ }
90
520
  /** Writes a file whole or not at all: a temporary file renamed over the target. */
91
521
  export async function writeAtomic(file, content) {
92
522
  await mkdir(dirname(file), { recursive: true });
@@ -95,10 +525,19 @@ export async function writeAtomic(file, content) {
95
525
  await writeFile(temporary, content, 'utf8');
96
526
  await rename(temporary, file);
97
527
  }
98
- /** A JSON object from a file: `null` when there is no file, `'unreadable'` when it holds anything else. */
528
+ /**
529
+ * A JSON object from a file: `null` when there is no file, `'unreadable'`
530
+ * when it holds anything else. Only a file is read: a named pipe where a
531
+ * settings file is expected was read without end, and neither doctor nor
532
+ * init came back. What is neither a file nor a directory holds no JSON
533
+ * object, as a directory holds none.
534
+ */
99
535
  export async function readJsonObject(file) {
100
- if (!existsSync(file))
536
+ const there = standing(file);
537
+ if (there === null)
101
538
  return null;
539
+ if (!there.isFile())
540
+ return 'unreadable';
102
541
  try {
103
542
  // A Buffer parses as its text, so the encoding is equivalent to its mutant.
104
543
  const value = JSON.parse(await readFile(file, 'utf8'));
@@ -119,7 +558,10 @@ export function isDirectory(path) {
119
558
  return false;
120
559
  }
121
560
  }
561
+ /** A file's text, or `null` where it cannot be read or is no file: a brief, a document one cites, a shim. */
122
562
  export async function readText(file) {
563
+ if (!isFile(file))
564
+ return null;
123
565
  try {
124
566
  return await readFile(file, 'utf8');
125
567
  }
package/dist/fs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets the disk: the configuration, the state\n * directory outside the work tree, and paths as the filesystem spells them.\n */\n\nimport { existsSync, realpathSync, statSync } from 'node:fs';\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\n\nimport { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, type HarnessConfig } from './config.js';\n\n/** Reads `.spec-harness.json` at the root, or the defaults when there is none. */\nexport async function loadConfig(root: string): Promise<{ config: HarnessConfig; file: string | null }> {\n const file = join(root, CONFIG_FILE);\n if (!existsSync(file)) return { config: DEFAULT_CONFIG, file: null };\n let raw: unknown;\n try {\n // A Buffer parses as its text, so the encoding is equivalent to its mutant.\n raw = JSON.parse(await readFile(file, 'utf8'));\n } catch (error) {\n // A directory under the file's name is not JSON that went wrong, and\n // Node's words for it, EISDIR, are not the harness's.\n if ((error as NodeJS.ErrnoException).code === 'EISDIR') throw new ConfigError(`${CONFIG_FILE} is a directory, not a configuration file`);\n throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${(error as Error).message}`);\n }\n return { config: parseConfig(raw), file };\n}\n\n/**\n * The real spelling of a path: links resolved and, on a filesystem that\n * ignores case, the case the directory entries have. A path that does not\n * exist yet is resolved through its nearest existing ancestor.\n *\n * This is what stops `SRC/db/schema.ts` from walking past a protection on\n * `src/db/schema.ts` on Windows or macOS, where both name one file.\n */\nexport function realSpelling(path: string): string {\n const missing: string[] = [];\n let current = resolve(path);\n for (;;) {\n try {\n // join leaves a real path as it is, and reads a segment's leading\n // separator as the one between it and the last.\n return join(realpathSync.native(current), ...missing.reverse());\n } catch {\n const parent = dirname(current);\n // Only a root that does not exist fails at the top, a drive Windows\n // does not have; everywhere else the loop ends in the try.\n if (parent === current) return resolve(path);\n missing.push(current.slice(parent.length));\n current = parent;\n }\n }\n}\n\n/**\n * Whether `directory` is the one git names as the top of a work tree: the\n * two spelled alike once the filesystem has spelled the directory, links\n * and short names resolved, as git spells a top. A spelling that differs is\n * read as another directory: what git said from the one is then not taken\n * for what it would say from the other.\n */\nexport function isTop(directory: string, top: string): boolean {\n return realSpelling(directory).split(sep).join('/') === top;\n}\n\n/** Whether a `.git` stands in `directory`, a repository's or the file that names one, as at the top of a work tree. */\nexport function holdsGit(directory: string): boolean {\n return existsSync(join(directory, '.git'));\n}\n\n/**\n * A path as the repository names it - relative to the root, POSIX-separated,\n * real spelling - or `null` when it lies outside the root.\n */\nexport function repositoryPath(given: string, root: string, cwd: string): string | null {\n const absolute = isAbsolute(given) ? given : resolve(cwd, given);\n const real = realSpelling(absolute);\n const realRoot = realSpelling(root);\n const rel = relative(realRoot, real);\n // `..env` at the root is inside it; only a `..` segment climbs out.\n if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;\n return rel.split(sep).join('/');\n}\n\n/** The harness's own directory under the git common directory, created on demand (ADR-0003). */\nexport async function stateDirectory(commonDir: string, ...parts: string[]): Promise<string> {\n const directory = join(commonDir, 'spec-harness', ...parts);\n await mkdir(directory, { recursive: true });\n return directory;\n}\n\n/** Writes a file whole or not at all: a temporary file renamed over the target. */\nexport async function writeAtomic(file: string, content: string): Promise<void> {\n await mkdir(dirname(file), { recursive: true });\n const temporary = `${file}.${process.pid}.tmp`;\n // utf8 is writeFile's own default, so naming it is equivalent to its mutant.\n await writeFile(temporary, content, 'utf8');\n await rename(temporary, file);\n}\n\n/** A JSON object from a file: `null` when there is no file, `'unreadable'` when it holds anything else. */\nexport async function readJsonObject(file: string): Promise<Record<string, unknown> | null | 'unreadable'> {\n if (!existsSync(file)) return null;\n try {\n // A Buffer parses as its text, so the encoding is equivalent to its mutant.\n const value: unknown = JSON.parse(await readFile(file, 'utf8'));\n return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : 'unreadable';\n } catch {\n return 'unreadable';\n }\n}\n\n/** Whether a path names a directory that is there; one that cannot be read names none. */\nexport function isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n // Each caller reads the answer with `!`, so the mutant that answers\n // nothing here is equivalent.\n return false;\n }\n}\n\nexport async function readText(file: string): Promise<string | null> {\n try {\n return await readFile(file, 'utf8');\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets the disk: the configuration, the state\n * directory outside the work tree, and paths as the filesystem spells them.\n */\n\nimport { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync, type Stats } from 'node:fs';\nimport { lstat, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\n\nimport type { BriefInputs } from './briefs.js';\nimport { ConfigError, CONFIG_FILE, DEFAULT_CONFIG, parseConfig, type HarnessConfig } from './config.js';\n\n/** What stands at a path, links followed, or `null` where nothing does: a link to nothing is nothing. */\nfunction standing(path: string): Stats | null {\n try {\n return statSync(path);\n } catch {\n return null;\n }\n}\n\n/**\n * Whether a regular file stands at a path, links followed. Only one is read\n * where a file is expected: opened, a named pipe nothing writes to is read\n * without end, and one read as the process waits on it, as the harness reads\n * a few small files, stops every timer with it (ADR-0002).\n */\nexport function isFile(path: string): boolean {\n return standing(path)?.isFile() === true;\n}\n\n/**\n * Reads `.spec-harness.json` at the root, or the defaults when there is none.\n *\n * Only a file is read. A named pipe under the file's name was read without\n * end, by every command and by the hook before every write; and Node's words\n * for a directory, EISDIR, are not the harness's. A file that cannot be read\n * is said as that, with the reason: one the account may not read, or one\n * another process holds open on Windows, was said to be JSON that is not\n * valid, which sent a person to its text. What does not load is thrown with\n * the path it stands at, which the hook lets a write reach (ADR-0005).\n */\nexport async function loadConfig(root: string): Promise<{ config: HarnessConfig; file: string | null }> {\n const file = join(root, CONFIG_FILE);\n const there = standing(file);\n if (there === null) return { config: DEFAULT_CONFIG, file: null };\n if (!there.isFile()) throw new ConfigError(`${CONFIG_FILE} is ${there.isDirectory() ? 'a directory' : 'neither a file nor a directory'}, not a configuration file`, file);\n let text: string;\n try {\n // A Buffer parses as its text, so the encoding is equivalent to its mutant.\n text = await readFile(file, 'utf8');\n } catch (error) {\n throw new ConfigError(`${CONFIG_FILE} cannot be read: ${(error as Error).message}`, file);\n }\n let raw: unknown;\n try {\n raw = JSON.parse(text);\n } catch (error) {\n throw new ConfigError(`${CONFIG_FILE} is not valid JSON: ${(error as Error).message}`, file);\n }\n try {\n return { config: parseConfig(raw), file };\n } catch (error) {\n // parseConfig throws nothing but its own refusal, whose words are kept.\n throw new ConfigError((error as Error).message, file);\n }\n}\n\n/**\n * The real path of a file that stands at `path` under that name alone: a\n * file, not a link to one, and with no other name, since a write to a file\n * with two names writes both. `null` for anything else, and for nothing.\n */\nfunction soleFile(path: string): string | null {\n try {\n const entry = lstatSync(path);\n return entry.isFile() && entry.nlink === 1 ? realpathSync.native(path) : null;\n } catch {\n // Nothing at the path, or nothing the account may look at. Its callers\n // compare the answer with a real path, which neither `null` nor what an\n // empty block answers is, so that mutant is equivalent.\n return null;\n }\n}\n\n/**\n * Whether every path of a write names `file` and nothing else: a file that\n * stands under that name alone, which the path names as the hook resolves a\n * path to decide it ({@link realSpelling}), and as the filesystem resolves\n * it to write, which on Linux and macOS follows a link before a `..` where\n * the hook reads the `..` first. A path that only reads like it, a link to\n * it, a second name of it, and a file of another work tree are none of\n * these. `cwd` is what a relative path is read from, its words kept as\n * written.\n */\nexport function writesOnly(paths: readonly string[], file: string, cwd: string): boolean {\n // Where the file is none, no path's real spelling is `null`, so no path\n // names it.\n const own = soleFile(file);\n return paths.every((given) => {\n const path = isAbsolute(given) ? given : `${cwd}${sep}${given}`;\n return soleFile(path) === own && realSpelling(path) === own;\n });\n}\n\n/**\n * A path where the harness keeps its state that holds something else: said\n * in a line, exit 2, where Node's ENOTDIR and a stack sent a person looking\n * for a defect (spec-core ADR-0005).\n */\nexport class StateError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'StateError';\n }\n}\n\n/**\n * The real spelling of a path: links resolved and, on a filesystem that\n * ignores case, the case the directory entries have. A path that does not\n * exist yet is resolved through its nearest existing ancestor.\n *\n * This is what stops `SRC/db/schema.ts` from walking past a protection on\n * `src/db/schema.ts` on Windows or macOS, where both name one file.\n */\nexport function realSpelling(path: string): string {\n const missing: string[] = [];\n let current = resolve(path);\n for (;;) {\n try {\n // join leaves a real path as it is, and reads a segment's leading\n // separator as the one between it and the last.\n return join(realpathSync.native(current), ...missing.reverse());\n } catch {\n const parent = dirname(current);\n // Only a root that does not exist fails at the top, a drive Windows\n // does not have; everywhere else the loop ends in the try.\n if (parent === current) return resolve(path);\n missing.push(current.slice(parent.length));\n current = parent;\n }\n }\n}\n\n/**\n * Whether `directory` is the one git names as the top of a work tree: the\n * two spelled alike once the filesystem has spelled the directory, links\n * and short names resolved, as git spells a top. A spelling that differs is\n * read as another directory: what git said from the one is then not taken\n * for what it would say from the other.\n */\nexport function isTop(directory: string, top: string): boolean {\n return realSpelling(directory).split(sep).join('/') === top;\n}\n\n/**\n * The repository's main work tree, where git keeps the repository as that\n * work tree's `.git`, as `git init` and `git clone` do and every linked\n * worktree's common directory then is: `null` for a common directory of any\n * other name, a bare repository's, a submodule's under its superproject's\n * `modules`, or one kept apart with `--separate-git-dir`. Read from the\n * name, with no process: it says only where an install is looked for.\n */\nexport function mainWorkTree(commonDir: string): string | null {\n return basename(commonDir) === '.git' ? dirname(commonDir) : null;\n}\n\n/** Whether a `.git` stands in `directory`, a repository's or the file that names one, as at the top of a work tree. */\nexport function holdsGit(directory: string): boolean {\n return existsSync(join(directory, '.git'));\n}\n\n/**\n * A path as the repository names it - relative to the root, POSIX-separated,\n * real spelling - or `null` when it lies outside the root.\n */\nexport function repositoryPath(given: string, root: string, cwd: string): string | null {\n const absolute = isAbsolute(given) ? given : resolve(cwd, given);\n const real = realSpelling(absolute);\n const realRoot = realSpelling(root);\n const rel = relative(realRoot, real);\n // `..env` at the root is inside it; only a `..` segment climbs out.\n if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;\n return rel.split(sep).join('/');\n}\n\n/**\n * The top of every work tree of the repository whose common directory is\n * `common`, `top` among them, read with no process: the main work tree,\n * whose `.git` the common directory is, and each linked worktree, by the\n * `gitdir` file git keeps for it under the common directory's `worktrees`,\n * which names the worktree's `.git` (gitrepository-layout(5)), absolute or\n * from its own directory. `null` where they cannot all be named: a common\n * directory of any other name, whose main work tree only its configuration\n * names, and an entry under `worktrees` that cannot be read.\n */\nexport function workTrees(top: string, common: string): string[] | null {\n const main = mainWorkTree(common);\n if (main === null) return null;\n const kept = join(common, 'worktrees');\n let names: string[] = [];\n try {\n names = readdirSync(kept);\n } catch (error) {\n // No such directory is no linked worktree, and anything else there is\n // one that cannot be read.\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return null;\n }\n const linked = names.map((name) => textOf(join(kept, name, 'gitdir')));\n if (linked.includes(null)) return null;\n // The line's end stands in the last name, the worktree's `.git`, which\n // dirname takes away with it.\n return [top, main, ...names.map((name, at) => dirname(resolve(kept, name, linked[at]!)))];\n}\n\n/**\n * Whether every path lies outside every work tree of the repository whose\n * top and common directory are given: such a path is no file of the\n * repository's, so no brief of its decides it, whatever is wrong with the\n * brief or the configuration (ADR-0005). Where the work trees cannot all\n * be named, a path is not known to be outside them, and this is `false`.\n */\nexport function outsideEvery(paths: readonly string[], top: string, common: string, cwd: string): boolean {\n // A write that names no path is no write outside: no caller asks of\n // one, and a test holds the answer.\n if (paths.length === 0) return false;\n // A path in the work tree that was opened, as nearly every write's is,\n // is told with nothing of git's directory read; read anyway, `top` is\n // among the work trees, so the mutant without this test is equivalent.\n if (paths.some((path) => repositoryPath(path, top, cwd) !== null)) return false;\n // A path is read here as the hook reads one, its `..` before its links,\n // where Linux and macOS write it after: `/elsewhere/link/../a.ts`, the\n // link to a directory of the work tree, reads as outside and is written\n // inside. Such a path is not known to be outside.\n if (paths.some((path) => path.split(/[\\\\/]/).includes('..'))) return false;\n const trees = workTrees(top, common);\n return trees !== null && paths.every((path) => trees.every((tree) => repositoryPath(path, tree, cwd) === null));\n}\n\n/**\n * The harness's own directory under the git common directory, created on\n * demand (ADR-0003). Where something that is no directory stands at its path,\n * or at one between it and git's, Node refuses to make it, with ENOTDIR or\n * EEXIST, and the harness says which path holds what.\n */\nexport async function stateDirectory(commonDir: string, ...parts: string[]): Promise<string> {\n const directory = join(commonDir, 'spec-harness', ...parts);\n try {\n await mkdir(directory, { recursive: true });\n } catch (error) {\n const { code } = error as NodeJS.ErrnoException;\n if (code !== 'ENOTDIR' && code !== 'EEXIST') throw error;\n // mkdir refuses for this alone, so one of the steps is there and is no\n // directory, and every step before it is one.\n const steps = ['spec-harness', ...parts].map((_, at, all) => join(commonDir, ...all.slice(0, at + 1)));\n const there = steps.find((step) => !standing(step)!.isDirectory())!;\n throw new StateError(`${there} is not a directory, where spec-harness keeps its state in one: move it away, and run the command again`);\n }\n return directory;\n}\n\n/**\n * A work tree and its git directories as the files git keeps name them, read\n * with no process: the nearest directory at or above `root` that holds a\n * `.git`; the git directory, that `.git` or the one a `.git` file names\n * (`gitdir: <path>`, relative to the file's directory); and the common\n * directory, the one that git directory's `commondir` file names, relative\n * to it, or the git directory itself (gitrepository-layout(5)). `exact` is\n * whether git, asked, would name the same work tree: not where\n * `GIT_DIR` or `GIT_WORK_TREE` sends it elsewhere, and not where the\n * repository's configuration names a `worktree`, which `core.worktree`\n * moves it to (git-config(1)), or keeps one of its own for a worktree.\n * `null` where no directory up to the top of the filesystem holds one, or\n * a `.git` file names none.\n */\nexport function gitDirectories(root: string, env: Readonly<Record<string, string | undefined>>): { readonly top: string; readonly git: string; readonly common: string; readonly exact: boolean } | null {\n let top = realSpelling(root);\n let found: Stats | null = null;\n for (;;) {\n found = standing(join(top, '.git'));\n if (found !== null) break;\n const parent = dirname(top);\n if (parent === top) return null;\n top = parent;\n }\n const dotGit = join(top, '.git');\n let gitDir = dotGit;\n if (found.isFile()) {\n // The path is the rest of the line: `.` stops at a line's end, a\n // carriage return among them, so the mutant that drops `$` is\n // equivalent; and a Buffer is read as its text, so is the encoding's.\n const named = /^gitdir: (.+)$/m.exec(readFileSync(dotGit, 'utf8'))?.[1];\n if (named === undefined) return null;\n gitDir = resolve(top, named);\n }\n const commondir = textOf(join(gitDir, 'commondir'));\n const common = commondir === null ? gitDir : resolve(gitDir, commondir.trim());\n const moved = env['GIT_DIR'] !== undefined || env['GIT_WORK_TREE'] !== undefined;\n // No configuration holds no key, as a text without one does: the mutant\n // of the empty text is equivalent.\n const elsewhere = /^\\s*worktree\\s*=/im.test(textOf(join(common, 'config')) ?? '') || existsSync(join(gitDir, 'config.worktree'));\n return { top, git: gitDir, common, exact: !moved && !elsewhere };\n}\n\n/** A file's text, or `null` where it cannot be read or is no file. */\nfunction textOf(file: string): string | null {\n if (!isFile(file)) return null;\n try {\n return readFileSync(file, 'utf8');\n } catch {\n return null;\n }\n}\n\n/**\n * How long a call's decisions are kept for a run after the write that does\n * not come: a call refused by the person, one that failed, one stopped\n * (ADR-0002). A day, since a call waits on a person's prompt for as long as\n * the person is away, and an entry removed too soon costs only that call's\n * answer being decided again.\n */\nexport const CALL_LIFE_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Keeps what a run before a write decided, under the common directory, for\n * the run after it, and removes what no run after a write read in a day.\n * Nothing here may change the answer before the write: whatever fails is\n * passed over, and the run after the write then decides as it did.\n */\nexport async function keepCall(commonDir: string, name: string, text: string, now: number = Date.now()): Promise<void> {\n try {\n const directory = await stateDirectory(commonDir, 'calls');\n await writeAtomic(join(directory, name), text);\n // A file another run takes away before it is removed here ends the\n // pruning in the catch below, where `force` would pass over it, and what\n // is left is pruned by a later run: the mutants of `force` are\n // equivalent.\n for (const entry of await readdir(directory)) {\n const file = join(directory, entry);\n if (now - (await stat(file)).mtimeMs > CALL_LIFE_MS) await rm(file, { force: true });\n }\n } catch {\n // A full disk, a directory another run removed: the call is decided again after the write.\n }\n}\n\n/** What was kept for a call, taken away as it is read; `null` where nothing was. */\nexport async function takeCall(commonDir: string, name: string): Promise<string | null> {\n const file = join(commonDir, 'spec-harness', 'calls', name);\n // What is no file holds nothing kept, and is written over at the next write.\n if (!isFile(file)) return null;\n try {\n const text = await readFile(file, 'utf8');\n // Taken away by another run since it was read, the mutants of `force`\n // decide the call again, which answers as the text read does: they are\n // equivalent.\n await rm(file, { force: true });\n return text;\n } catch {\n return null;\n }\n}\n\n/** SHA-256 of a text or of bytes, in hex. */\nexport type Hash = (data: string | Uint8Array) => string;\n\n/** SHA-256, loaded where something is hashed: a run that keeps no answer loads no hash (ADR-0002). */\nexport async function hasher(): Promise<Hash> {\n const { createHash } = await import('node:crypto');\n return (data) => createHash('sha256').update(data).digest('hex');\n}\n\n/**\n * How long a session's answer of spec-brief's is kept once it was last\n * kept (ADR-0003). A day, as a call's: an answer removed costs one start of\n * spec-brief, and one kept is a few kilobytes.\n */\nexport const ANSWER_LIFE_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Keeps a session's answer of spec-brief's under the common directory, with\n * the key it answers, and removes what was kept over a day ago. Whatever\n * fails is passed over: the answer is the one given whether or not it is\n * kept.\n */\nexport async function keepAnswer(commonDir: string, name: string, key: string, answer: string, now: number = Date.now()): Promise<void> {\n try {\n const directory = await stateDirectory(commonDir, 'answers');\n await writeAtomic(join(directory, name), `${JSON.stringify({ key, answer })}\\n`);\n // A file another run takes away before it is removed here ends the\n // pruning in the catch below, where `force` would pass over it, and what\n // is left is pruned by a later run: the mutants of `force` are\n // equivalent, and so is the one that also removes a file kept a day ago\n // to the millisecond.\n for (const entry of await readdir(directory)) {\n const file = join(directory, entry);\n if (now - (await stat(file)).mtimeMs > ANSWER_LIFE_MS) await rm(file, { force: true });\n }\n } catch {\n // A full disk, a directory another run removed: the next run starts spec-brief.\n }\n}\n\n/**\n * The answer kept under `name` for `key`, or `null` where there is none:\n * nothing there, something that is not a file, a text that is not what\n * `keepAnswer` writes, or another key's.\n */\nexport async function keptAnswer(commonDir: string, name: string, key: string): Promise<string | null> {\n const file = join(commonDir, 'spec-harness', 'answers', name);\n try {\n // Only a file is read: a named pipe would be read without end.\n if (!(await stat(file)).isFile()) return null;\n // A text that holds null throws on the read, and any other that is no\n // object has no key: the catch answers for both. A Buffer parses as its\n // text, so the encoding is equivalent to its mutant.\n const kept: { readonly key?: unknown; readonly answer?: unknown } = JSON.parse(await readFile(file, 'utf8'));\n return kept.key === key && typeof kept.answer === 'string' ? kept.answer : null;\n } catch {\n // Falling out of the catch answers undefined, which no answer is read\n // from either: the mutant without the return is equivalent.\n return null;\n }\n}\n\n/** Whether a path that could not be read names nothing: it, or a directory on the way to it, is not there. */\nfunction nothingThere(error: unknown): boolean {\n const { code } = error as NodeJS.ErrnoException;\n return code === 'ENOENT' || code === 'ENOTDIR';\n}\n\n/**\n * Whether a file reads as listed: a file, never a pipe or a directory, whose\n * bytes have the digest; or, listed as not there, nothing at its path.\n */\nasync function fileReads(file: string, sha256: string | null, hash: Hash): Promise<boolean> {\n let there: Stats;\n try {\n there = await stat(file);\n } catch (error) {\n return sha256 === null && nothingThere(error);\n }\n // Only a file is read: a named pipe would be read without end. A file\n // listed as not there has no digest a file's bytes have.\n if (!there.isFile()) return false;\n return hash(await readFile(file)) === sha256;\n}\n\n/**\n * Whether a directory is there and holds the files listed, and no other: a\n * file as spec-brief lists one, an entry that is a file itself, where a\n * directory or a link is not one. What is no directory, a named pipe among\n * it, is refused by the listing, which throws. Listed as not there, nothing\n * may stand at its path in any form, a link that leads nowhere among them.\n */\nasync function entriesRead(directory: string, entries: readonly string[] | null): Promise<boolean> {\n if (entries === null) {\n try {\n await lstat(directory);\n return false;\n } catch (error) {\n return nothingThere(error);\n }\n }\n // The entries are names no two of which are one (inputsOf), and so are a\n // directory's: as many of each, every one of the one among the other, is\n // the same names, in whatever order the disk lists them.\n const names = (await readdir(directory, { withFileTypes: true })).flatMap((entry) => (entry.isFile() ? [entry.name] : []));\n const listed = new Set(entries);\n return names.length === listed.size && names.every((name) => listed.has(name));\n}\n\n/**\n * Whether every input of an answer reads as listed, with no process\n * started: each file by the digest of its bytes, or as not there; each\n * directory by its entries; and, where git was asked, the index of the work\n * tree's git directory and each ignore file. A path is read from the work\n * tree's top, or as it stands where it is absolute. Anything that cannot be\n * read, and an index that is not the one git reads for the work tree, is a\n * doubt, and answers `false` (ADR-0002).\n */\nexport async function readsAsListed(root: string, inputs: BriefInputs, hash: Hash, env: Readonly<Record<string, string | undefined>> = process.env): Promise<boolean> {\n const at = (path: string): string => (isAbsolute(path) ? path : join(root, ...path.split('/')));\n const git = inputs.git;\n let index: string | null = null;\n if (git !== null) {\n // The index of the work tree's own git directory, as the files git keeps\n // name it: not where git is sent elsewhere, GIT_DIR, GIT_WORK_TREE or a\n // worktree key (`exact`), GIT_INDEX_FILE or GIT_COMMON_DIR. A top git\n // named always holds a `.git`, so the test of none is the compiler's, and\n // a null read past it throws, which the caller reads as a doubt: that\n // mutant is equivalent. The top found is the work tree's own but where\n // git names a top that holds no `.git`, a work tree `core.worktree`\n // names away from its repository and inside another, whose index the\n // one found would be.\n const found = gitDirectories(root, env);\n if (found === null || !found.exact || env['GIT_INDEX_FILE'] !== undefined || env['GIT_COMMON_DIR'] !== undefined || found.top !== realSpelling(root)) return false;\n index = join(found.git, 'index');\n }\n // Made once every check is known to be made, so that none is left to fail\n // with no one reading it. A text put among them where git was asked\n // nothing is one `every` reads as true, so that mutant is equivalent.\n const checks = [\n ...inputs.files.map((file) => fileReads(at(file.path), file.sha256, hash)),\n ...inputs.directories.map((directory) => entriesRead(at(directory.path), directory.entries)),\n ...(git === null ? [] : [fileReads(index!, git.index, hash), ...git.ignores.map((file) => fileReads(at(file.path), file.sha256, hash))]),\n ];\n try {\n return (await Promise.all(checks)).every((reads) => reads);\n } catch {\n // Falling out of the catch answers undefined, which the caller reads as\n // false: the mutant without the return is equivalent.\n return false;\n }\n}\n\n/** Writes a file whole or not at all: a temporary file renamed over the target. */\nexport async function writeAtomic(file: string, content: string): Promise<void> {\n await mkdir(dirname(file), { recursive: true });\n const temporary = `${file}.${process.pid}.tmp`;\n // utf8 is writeFile's own default, so naming it is equivalent to its mutant.\n await writeFile(temporary, content, 'utf8');\n await rename(temporary, file);\n}\n\n/**\n * A JSON object from a file: `null` when there is no file, `'unreadable'`\n * when it holds anything else. Only a file is read: a named pipe where a\n * settings file is expected was read without end, and neither doctor nor\n * init came back. What is neither a file nor a directory holds no JSON\n * object, as a directory holds none.\n */\nexport async function readJsonObject(file: string): Promise<Record<string, unknown> | null | 'unreadable'> {\n const there = standing(file);\n if (there === null) return null;\n if (!there.isFile()) return 'unreadable';\n try {\n // A Buffer parses as its text, so the encoding is equivalent to its mutant.\n const value: unknown = JSON.parse(await readFile(file, 'utf8'));\n return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : 'unreadable';\n } catch {\n return 'unreadable';\n }\n}\n\n/** Whether a path names a directory that is there; one that cannot be read names none. */\nexport function isDirectory(path: string): boolean {\n try {\n return statSync(path).isDirectory();\n } catch {\n // Each caller reads the answer with `!`, so the mutant that answers\n // nothing here is equivalent.\n return false;\n }\n}\n\n/** A file's text, or `null` where it cannot be read or is no file: a brief, a document one cites, a shim. */\nexport async function readText(file: string): Promise<string | null> {\n if (!isFile(file)) return null;\n try {\n return await readFile(file, 'utf8');\n } catch {\n return null;\n }\n}\n"]}