faberun 0.19.0 → 0.19.2

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.
@@ -11,6 +11,7 @@ import { validateNodeSnapshot } from "../contract/snapshot.mjs";
11
11
 
12
12
  /** @typedef {ReturnType<typeof import("./lock.mjs").acquire>} LockHandle */
13
13
  /** @typedef {import("../contract/index.mjs").NodeSnapshot} NodeSnapshot */
14
+ /** @typedef {import("../contract/index.mjs").ValidatedContract} ValidatedContract */
14
15
 
15
16
  const NODES_DIR_NAME = "nodes";
16
17
 
@@ -60,3 +61,35 @@ export function listNodeSnapshots(runDir) {
60
61
  throw error;
61
62
  }
62
63
  }
64
+
65
+ /**
66
+ * The persisted node snapshots of one run.
67
+ *
68
+ * `tolerateMissing` returns only the snapshots that exist instead of refusing
69
+ * the run. For resume and supervise a node the contract declares and the run
70
+ * never persisted is corruption and stays fatal; for `cancel` it is the
71
+ * ordinary shape of what is being cancelled. A launch writes `contract.json`
72
+ * first and can die before it writes any node -- probing providers, shelling
73
+ * out to git, claiming the run ref -- and the directory then holds a
74
+ * contract, an occupied ref and no node state at all. Such a node started
75
+ * nothing, holds no invocation and no worktree, so there is nothing to
76
+ * terminate and only the git names to release, which is what cancel is for.
77
+ *
78
+ * @param {string} runDir
79
+ * @param {ValidatedContract} contract
80
+ * @param {{tolerateMissing?: boolean}} [options]
81
+ * @returns {NodeSnapshot[]}
82
+ */
83
+ export function readRunNodes(runDir, contract, options = {}) {
84
+ const names = listNodeSnapshots(runDir);
85
+ const expected = new Map(contract.nodes.map((node) => [`${node.id}.json`, node]));
86
+ for (const name of names) if (!expected.has(name)) throw new TypeError(`unexpected persisted node snapshot ${name}`);
87
+ return contract.nodes.flatMap((node) => {
88
+ const name = `${node.id}.json`;
89
+ if (!names.includes(name)) {
90
+ if (options.tolerateMissing === true) return [];
91
+ throw new TypeError(`missing persisted node snapshot ${name}`);
92
+ }
93
+ return [validateNodeSnapshot(readNodeSnapshot(runDir, node.id), node)];
94
+ });
95
+ }
package/src/util.mjs CHANGED
@@ -229,3 +229,49 @@ export function readJsonTolerant(path) {
229
229
  export function finite(value) {
230
230
  return typeof value === "number" && Number.isFinite(value) ? value : null;
231
231
  }
232
+
233
+ /**
234
+ * Split a command string into the words a POSIX shell would pass as argv, for
235
+ * the two readers handed a command as a display string rather than an argv: a
236
+ * Definition of Done `command` proof, whose words `engine/scope.mjs` matches
237
+ * against the files the node wrote, and the node:test filters one declares
238
+ * (`engine/judge-gate.mjs`). Quotes group and are stripped; whitespace
239
+ * outside them separates.
240
+ *
241
+ * Deliberately not a shell: no expansion, no substitution, no operators, and
242
+ * no backslash escape -- on Windows a backslash is a path separator and
243
+ * `C:\Users\x` must survive this intact. The question both callers ask is
244
+ * "which words does this command name", and a quoted path holding a space is
245
+ * one word: splitting it on whitespace produced two fragments that matched no
246
+ * file, so a proof naming a real path read as naming none.
247
+ *
248
+ * @param {string} text
249
+ * @returns {string[]}
250
+ */
251
+ export function shellWords(text) {
252
+ /** @type {string[]} */
253
+ const words = [];
254
+ /** @type {string|null} */
255
+ let current = null;
256
+ /** @type {string|null} */
257
+ let quote = null;
258
+ for (const character of text) {
259
+ if (quote === null && /\s/u.test(character)) {
260
+ if (current !== null) words.push(current);
261
+ current = null;
262
+ continue;
263
+ }
264
+ if (quote === null && (character === '"' || character === "'")) {
265
+ quote = character;
266
+ current ??= "";
267
+ continue;
268
+ }
269
+ if (quote === character) {
270
+ quote = null;
271
+ continue;
272
+ }
273
+ current = (current ?? "") + character;
274
+ }
275
+ if (current !== null) words.push(current);
276
+ return words;
277
+ }