@molecule/api-ai-tools 1.7.0 → 1.8.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.
@@ -64,6 +64,122 @@ const PACKAGE_RUNNER_HEADS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
64
64
  * a script of the project's, and nothing is stripped from them.
65
65
  */
66
66
  const PACKAGE_RUNNER_SUBCOMMANDS = new Set(['dlx', 'exec', 'x']);
67
+ /**
68
+ * npm's value-taking options: `npm exec`'s own (`-p/--package`, `-c/--call`, `-w/--workspace`,
69
+ * `--prefix`) and the string-valued configs `npm config ls -l` lists in the sandbox image, which
70
+ * npm accepts on the command line of any invocation.
71
+ */
72
+ const NPM_VALUE_FLAGS = [
73
+ '-c',
74
+ '--call',
75
+ '-p',
76
+ '--package',
77
+ '-w',
78
+ '--workspace',
79
+ '--prefix',
80
+ '--registry',
81
+ '--cache',
82
+ '--userconfig',
83
+ '--globalconfig',
84
+ '--loglevel',
85
+ '--logs-dir',
86
+ '--script-shell',
87
+ '--shell',
88
+ '--tag',
89
+ '--otp',
90
+ '--scope',
91
+ '--location',
92
+ '--user-agent',
93
+ '--which',
94
+ '--viewer',
95
+ '--proxy',
96
+ '--https-proxy',
97
+ '--cert',
98
+ '--key',
99
+ '--ca',
100
+ '--cafile',
101
+ '--editor',
102
+ '--message',
103
+ '--node-options',
104
+ '--save-prefix',
105
+ '--preid',
106
+ '--access',
107
+ '--also',
108
+ '--audit-level',
109
+ '--auth-type',
110
+ '--before',
111
+ '--browser',
112
+ '--cidr',
113
+ '--cpu',
114
+ '--depth',
115
+ '--expect-result-count',
116
+ '--expect-results',
117
+ '--install-strategy',
118
+ '--libc',
119
+ '--local-address',
120
+ '--lockfile-version',
121
+ '--only',
122
+ '--os',
123
+ '--pack-destination',
124
+ '--provenance-file',
125
+ '--replace-registry-host',
126
+ '--sbom-format',
127
+ '--sbom-type',
128
+ '--searchexclude',
129
+ '--searchopts',
130
+ '--tag-version-prefix',
131
+ ];
132
+ /** pnpm dlx's value-taking options (`pnpm dlx --help`); its `-c` is the boolean shell-mode. */
133
+ const PNPM_DLX_VALUE_FLAGS = ['--allow-build', '--package', '--reporter'];
134
+ /**
135
+ * The package manager's OWN options that take their value from the next word — read from the
136
+ * versions the sandbox image (`node:22-trixie-slim`) ships: npm 10.9.9 (`npm exec --help`, and
137
+ * every string-valued key of `npm config ls -l`), pnpm 10.34.7 (`pnpm help exec`,
138
+ * `pnpm dlx --help`) and Yarn 4.18.1 (`yarn dlx --help`). A flag absent here is skipped as a
139
+ * boolean, which is what every documented switch is.
140
+ *
141
+ * The table is keyed per manager because the same short flag means different things in two of
142
+ * them: `-c` is npm's `--call <command>` (a value, and a command of its own —
143
+ * {@link PACKAGE_RUNNER_CALL_FLAGS}) but pnpm's `--shell-mode` (a switch), and `-w` is npm's
144
+ * `--workspace <name>` (a value) but pnpm's `--workspace-root` (a switch).
145
+ */
146
+ const PACKAGE_RUNNER_VALUE_FLAGS = {
147
+ // npm exec's documented options, plus the config flags npm accepts on any invocation.
148
+ npx: NPM_VALUE_FLAGS,
149
+ 'npm exec': NPM_VALUE_FLAGS,
150
+ 'npm x': NPM_VALUE_FLAGS,
151
+ // `npx --prefix /app playwright test` is how flash-m101's run reached the app's own Playwright.
152
+ bunx: ['-p', '--package'],
153
+ 'bun x': ['-p', '--package'],
154
+ pnpx: PNPM_DLX_VALUE_FLAGS,
155
+ 'pnpm dlx': PNPM_DLX_VALUE_FLAGS,
156
+ 'pnpm x': PNPM_DLX_VALUE_FLAGS,
157
+ 'pnpm exec': [
158
+ '-C',
159
+ '--dir',
160
+ '--loglevel',
161
+ '--resume-from',
162
+ '--reporter',
163
+ '--filter',
164
+ '--filter-prod',
165
+ '--changed-files-ignore-pattern',
166
+ '--workspace-concurrency',
167
+ '--prefix',
168
+ ],
169
+ 'yarn dlx': ['-p', '--package'],
170
+ 'yarn exec': [],
171
+ 'yarn x': [],
172
+ };
173
+ /**
174
+ * The package-runner options whose VALUE is itself a command (`npm exec -c '<command>'`), so it is
175
+ * parsed as one: `npx -c 'playwright test'` runs Playwright, and its program is not the word after
176
+ * the flag.
177
+ */
178
+ const PACKAGE_RUNNER_CALL_FLAGS = {
179
+ npx: ['-c', '--call'],
180
+ 'npm exec': ['-c', '--call'],
181
+ 'npm x': ['-c', '--call'],
182
+ };
67
183
  /** Wrapper flags that are followed by their own value (`sudo -u root`, `timeout -k 5`). */
68
184
  const WRAPPER_VALUE_FLAGS = {
69
185
  env: ['-S', '--split-string', '-u', '--unset', '-C', '--chdir'],
@@ -149,6 +265,44 @@ function flagWords(words, from, wrapper) {
149
265
  }
150
266
  return count;
151
267
  }
268
+ /**
269
+ * How many words a package runner (`npx`, `npm exec`, `pnpm dlx`, `yarn dlx`) owns before the
270
+ * program it runs — its own options, and each value flag's value — plus the command string of a
271
+ * `-c '<command>'`, which is a command of its own rather than the program word.
272
+ *
273
+ * A `--` ends the options: `npm exec --prefix x -- playwright test` runs `playwright` with `test`,
274
+ * so the terminator is consumed and the walk stops there.
275
+ *
276
+ * @param words - The segment's words.
277
+ * @param from - The index just after the runner (or after `npm exec`).
278
+ * @param key - The runner's table key (`npm`, `npx`, `pnpm exec`, `yarn dlx`, …).
279
+ * @returns The count of words the runner owns, and the `-c` command string when it carries one.
280
+ */
281
+ function packageRunnerOptions(words, from, key) {
282
+ const withValue = PACKAGE_RUNNER_VALUE_FLAGS[key] ?? [];
283
+ const callFlags = PACKAGE_RUNNER_CALL_FLAGS[key] ?? [];
284
+ let count = 0;
285
+ while (from + count < words.length) {
286
+ const word = words[from + count] ?? '';
287
+ if (!word.startsWith('-'))
288
+ break;
289
+ // `--` ends the runner's options; the program is the next word.
290
+ if (word === '--')
291
+ return { count: count + 1, nested: null };
292
+ const flag = unquoted(word);
293
+ // `--flag=value` carries its value inside the same word.
294
+ if (word.includes('=')) {
295
+ count += 1;
296
+ continue;
297
+ }
298
+ if (callFlags.includes(flag)) {
299
+ const value = words[from + count + 1];
300
+ return { count: count + 2, nested: value === undefined ? null : unquoted(value) };
301
+ }
302
+ count += withValue.includes(flag) ? 2 : 1;
303
+ }
304
+ return { count, nested: null };
305
+ }
152
306
  /**
153
307
  * The `-c '<command>'` string a shell was handed, or null when the shell was given a FILE
154
308
  * (`bash run.sh`) — the wrapper walk then steps past its flags instead.
@@ -201,12 +355,19 @@ function commandStart(words) {
201
355
  continue;
202
356
  }
203
357
  if (PACKAGE_RUNNERS.has(name)) {
204
- at += 1 + flagWords(words, at + 1, name);
358
+ const runner = packageRunnerOptions(words, at + 1, name);
359
+ if (runner.nested !== null)
360
+ return { at, nested: runner.nested, timeoutSeconds };
361
+ at += 1 + runner.count;
205
362
  continue;
206
363
  }
207
364
  if (PACKAGE_RUNNER_HEADS.has(name) &&
208
365
  PACKAGE_RUNNER_SUBCOMMANDS.has(unquoted(words[at + 1] ?? ''))) {
209
- at += 2 + flagWords(words, at + 2, name);
366
+ const subcommand = unquoted(words[at + 1] ?? '');
367
+ const runner = packageRunnerOptions(words, at + 2, `${name} ${subcommand}`);
368
+ if (runner.nested !== null)
369
+ return { at, nested: runner.nested, timeoutSeconds };
370
+ at += 2 + runner.count;
210
371
  continue;
211
372
  }
212
373
  if (!LAUNCHERS.has(name))
@@ -239,11 +400,14 @@ function commandStart(words) {
239
400
  * no runner, and every run of it reads as nothing).
240
401
  *
241
402
  * @param command - The shell command.
242
- * @returns Each segment's text, in order, with empty ones dropped.
403
+ * @returns Each segment's text and the offset in the command its first character sits at, in
404
+ * order, with empty ones dropped.
243
405
  */
244
406
  function splitSegments(command) {
245
407
  const parts = [];
246
408
  let buffer = '';
409
+ /** The offset in the command of the current buffer's first character. */
410
+ let bufferStart = 0;
247
411
  /** The nestings this position sits inside, outermost first. */
248
412
  const stack = [];
249
413
  for (let i = 0; i < command.length; i++) {
@@ -305,25 +469,33 @@ function splitSegments(command) {
305
469
  continue;
306
470
  }
307
471
  if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
308
- parts.push(buffer);
472
+ parts.push({ text: buffer, start: bufferStart });
309
473
  buffer = '';
310
474
  i += 1;
475
+ bufferStart = i + 1;
311
476
  continue;
312
477
  }
313
478
  if (char === ';' || char === '|' || char === '\n') {
314
- parts.push(buffer);
479
+ parts.push({ text: buffer, start: bufferStart });
315
480
  buffer = '';
481
+ bufferStart = i + 1;
316
482
  continue;
317
483
  }
318
484
  if (char === '&' && command[i - 1] !== '>' && next !== '>') {
319
- parts.push(buffer);
485
+ parts.push({ text: buffer, start: bufferStart });
320
486
  buffer = '';
487
+ bufferStart = i + 1;
321
488
  continue;
322
489
  }
323
490
  buffer += char;
324
491
  }
325
- parts.push(buffer);
326
- return parts.map((part) => part.trim()).filter(Boolean);
492
+ parts.push({ text: buffer, start: bufferStart });
493
+ return parts
494
+ .map((part) => ({
495
+ text: part.text.trim(),
496
+ start: part.start + (part.text.length - part.text.trimStart().length),
497
+ }))
498
+ .filter((part) => part.text !== '');
327
499
  }
328
500
  /**
329
501
  * One segment's words: quotes kept whole, grouping parentheses dropped, and a `(` written
@@ -345,6 +517,38 @@ function segmentWords(text) {
345
517
  }
346
518
  return cleaned;
347
519
  }
520
+ /** {@link WORD} anchored at an offset, for walking a statement's raw words. */
521
+ const WORD_AT = new RegExp(WORD.source, 'y');
522
+ /**
523
+ * A statement's own leading `NAME=value` words, and where they end.
524
+ *
525
+ * The shell binds these to the command the statement runs, so they are the environment THAT
526
+ * program sees — which is why a caller writing a variable for a run has to write it here and not
527
+ * at the start of the whole command.
528
+ *
529
+ * @param text - One statement's text.
530
+ * @returns The assignments as written, and the offset in `text` just before the word that runs.
531
+ */
532
+ function leadingAssignments(text) {
533
+ const words = [];
534
+ let at = 0;
535
+ for (let guard = 0; guard < MAX_WRAPPERS; guard++) {
536
+ while (at < text.length && /\s/.test(text[at] ?? ''))
537
+ at += 1;
538
+ // Grouping in front of the statement (`(npx …)`, `{ …`): not a word the shell assigns with.
539
+ if (text[at] === '(' || text[at] === '{') {
540
+ at += 1;
541
+ continue;
542
+ }
543
+ WORD_AT.lastIndex = at;
544
+ const match = WORD_AT.exec(text);
545
+ if (!match || !ENV_ASSIGNMENT.test(match[0]))
546
+ return { words, end: at };
547
+ words.push(match[0]);
548
+ at += match[0].length;
549
+ }
550
+ return { words, end: at };
551
+ }
348
552
  /**
349
553
  * The segments of a command, each with its program and arguments — the answer every classifier
350
554
  * that reads a command as "this runs the suite" is asking for.
@@ -353,66 +557,92 @@ function segmentWords(text) {
353
557
  * @returns One entry per statement or pipeline stage that runs something, in order.
354
558
  */
355
559
  export function commandSegments(command) {
356
- return segmentsOf(command, 0);
560
+ return locatedSegmentsOf(command, 0).flatMap((located) => located.segment === null ? [] : [located.segment]);
357
561
  }
358
562
  /**
359
- * The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
360
- * under (`timeout 280 hurl --test …` → 280), or null when it carries none.
361
- *
362
- * This is the bound `man timeout` enforces by KILLING the command — so a run that used it did not
363
- * finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
364
- * other command reader uses, rather than matched as text: `timeout` in an argument
365
- * (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
366
- * the bound ({@link WRAPPER_VALUE_FLAGS}).
563
+ * Every statement of a command, with its own assignments and where it sits — assignment-only
564
+ * statements included, because the variables those persist are exactly what a later statement of
565
+ * the same command reads.
367
566
  *
368
567
  * @param command - The shell command.
369
- * @returns The seconds the command allows itself, or null.
568
+ * @returns One entry per statement or pipeline stage, in order.
370
569
  */
371
- export function commandTimeoutSeconds(command) {
372
- let bound = null;
373
- for (const segment of segmentsOf(command, 0)) {
374
- if (segment.timeoutSeconds === null)
375
- continue;
376
- bound = bound === null ? segment.timeoutSeconds : Math.max(bound, segment.timeoutSeconds);
377
- }
378
- return bound;
570
+ export function locatedCommandSegments(command) {
571
+ return locatedSegmentsOf(command, 0);
379
572
  }
380
573
  /**
381
- * {@link commandSegments}'s recursion, so a `sh -c '<command>'` is opened as far as
382
- * {@link MAX_NESTING} allows.
574
+ * {@link locatedCommandSegments}'s recursion, so a `sh -c '<command>'` is opened as far as
575
+ * {@link MAX_NESTING} allows. A nested statement's offsets are cleared: they are positions in
576
+ * the nested string, not in the command this walk started from.
383
577
  *
384
578
  * @param command - The shell command.
385
579
  * @param depth - How many `sh -c` strings this command already sits inside.
386
- * @returns The segments.
580
+ * @returns The located statements.
387
581
  */
388
- function segmentsOf(command, depth) {
582
+ function locatedSegmentsOf(command, depth) {
389
583
  if (typeof command !== 'string' || depth > MAX_NESTING)
390
584
  return [];
391
585
  const out = [];
392
- for (const text of splitSegments(command)) {
393
- const words = segmentWords(text);
394
- if (words.length === 0)
395
- continue;
586
+ for (const part of splitSegments(command)) {
587
+ const words = segmentWords(part.text);
396
588
  const start = commandStart(words);
589
+ const leading = leadingAssignments(part.text);
397
590
  if (start.nested !== null) {
398
- out.push(...segmentsOf(start.nested, depth + 1));
591
+ for (const inner of locatedSegmentsOf(start.nested, depth + 1))
592
+ out.push({ ...inner, start: null, assignmentOffset: null });
399
593
  continue;
400
594
  }
401
595
  const word = words[start.at];
402
- // Only wrappers and assignments: nothing this segment runs.
403
- if (word === undefined)
596
+ if (word === undefined) {
597
+ // Only wrappers and assignments: nothing this statement runs, but the variables it sets
598
+ // persist in the shell the rest of the command runs in.
599
+ if (leading.words.length > 0)
600
+ out.push({
601
+ segment: null,
602
+ assignments: leading.words,
603
+ start: part.start,
604
+ assignmentOffset: part.start + leading.end,
605
+ });
404
606
  continue;
607
+ }
405
608
  const program = programName(word);
406
609
  const args = words.slice(start.at + 1);
407
610
  out.push({
408
- program,
409
- args,
410
- text: [program, ...args].join(' '),
411
- timeoutSeconds: start.timeoutSeconds,
611
+ segment: {
612
+ program,
613
+ args,
614
+ text: [program, ...args].join(' '),
615
+ timeoutSeconds: start.timeoutSeconds,
616
+ },
617
+ assignments: leading.words,
618
+ start: part.start,
619
+ assignmentOffset: part.start + leading.end,
412
620
  });
413
621
  }
414
622
  return out;
415
623
  }
624
+ /**
625
+ * The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
626
+ * under (`timeout 280 hurl --test …` → 280), or null when it carries none.
627
+ *
628
+ * This is the bound `man timeout` enforces by KILLING the command — so a run that used it did not
629
+ * finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
630
+ * other command reader uses, rather than matched as text: `timeout` in an argument
631
+ * (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
632
+ * the bound ({@link WRAPPER_VALUE_FLAGS}).
633
+ *
634
+ * @param command - The shell command.
635
+ * @returns The seconds the command allows itself, or null.
636
+ */
637
+ export function commandTimeoutSeconds(command) {
638
+ let bound = null;
639
+ for (const segment of commandSegments(command)) {
640
+ if (segment.timeoutSeconds === null)
641
+ continue;
642
+ bound = bound === null ? segment.timeoutSeconds : Math.max(bound, segment.timeoutSeconds);
643
+ }
644
+ return bound;
645
+ }
416
646
  /**
417
647
  * Whether any segment of the command runs a program whose command-position text matches
418
648
  * `pattern` — the one question a runner-kind table asks.
package/dist/cut-run.d.ts CHANGED
@@ -24,6 +24,12 @@
24
24
  * `backends/sandbox.ts`), which reports 124 whatever the pipeline did, and returns everything
25
25
  * printed until then. The budget is the tool's own, so its number belongs to the tool.
26
26
  *
27
+ * A runner's closing summary is what separates "used the bound and finished" from "stopped at the
28
+ * bound", but it is only half the evidence: Playwright prints one when its process is stopped too,
29
+ * listing just the tests it reached. So a summary is a finish only when it accounts for the total
30
+ * the runner announced before it started ({@link suiteSummary}) — a summary that does not
31
+ * reconcile is a stopped run like any other, and the result says so.
32
+ *
27
33
  * @module
28
34
  */
29
35
  /**
@@ -46,14 +52,82 @@ export type CommandRunState = 'ran' | typeof CUT_AT_BUDGET;
46
52
  */
47
53
  export declare function commandRunState(result: unknown): CommandRunState;
48
54
  /**
49
- * Whether text carries a runner's closing summary ({@link SUITE_TALLY}).
55
+ * What a runner's own closing summary reports, and whether it accounts for the whole run.
56
+ *
57
+ * A summary is not by itself proof that the run finished. Playwright prints one when its process
58
+ * is stopped too (SIGTERM/SIGINT), listing only the tests it reached — so a run killed at 36 of
59
+ * 139 tests ends with the same `36 passed (2.5m)` line as a run that walked all 139. The only
60
+ * thing that separates them is the total the runner announced before it started
61
+ * (`Running 139 tests using 1 worker`), which {@link suiteSummary} reconciles the summary against.
62
+ */
63
+ export interface SuiteSummary {
64
+ /**
65
+ * The total the runner announced before its first test, or null when it announced none (Hurl
66
+ * announces nothing; Vitest's own summary line carries its total in parentheses).
67
+ */
68
+ announced: number | null;
69
+ /**
70
+ * Every test the summary accounts for — the ones it passed, failed, marked flaky, timed out,
71
+ * interrupted, skipped or left "did not run". This is how far the walk got, which is what a
72
+ * stopped run's sentence says; a summary that accounts for fewer than {@link announced} was
73
+ * printed by a process that stopped early.
74
+ */
75
+ accounted: number;
76
+ /** Whether the summary accounts for every test the runner announced. */
77
+ finished: boolean;
78
+ }
79
+ /**
80
+ * What the runner's closing summary accounts for, or null when it printed none.
81
+ *
82
+ * Three runners' summaries are read, each by its own wording: Playwright's per-status lines
83
+ * (reconciled against its `Running N tests` announcement), Vitest's single `Tests …` line (its own
84
+ * parenthesised total), and Hurl's `Executed files:` block (which it prints only at the end, and
85
+ * which announces no total — so it is a finish by construction).
86
+ *
87
+ * @param text - The run's stdout and stderr, RAW (a truncated copy can lose the announcement).
88
+ * @returns The summary, or null when the text carries none.
89
+ */
90
+ export declare function suiteSummary(text: string): SuiteSummary | null;
91
+ /**
92
+ * Whether text carries a runner's closing summary.
93
+ *
94
+ * A summary is NOT a finish on its own — a stopped Playwright run prints one too. Use
95
+ * {@link suiteSummary} and its `finished` for that.
50
96
  *
51
97
  * @param text - The run's stdout and stderr.
52
98
  * @returns True when the runner's own end-of-run summary is there.
53
99
  */
54
100
  export declare function suiteTally(text: string): boolean;
101
+ /**
102
+ * The exit code a finished suite run may report.
103
+ *
104
+ * The code in a background result is the code of the command's LAST pipeline stage. When a stage
105
+ * after the suite runner still consumes its output (`| tee run.log`, or a filter the platform could
106
+ * not prove output-only), that stage produced the code, not the run — and `… | grep … | tail -20`
107
+ * exited 0 for a run with a failure in it (xr-molecule-flash-m101 +25:54). No code is reported
108
+ * then: the log's own tally decides.
109
+ *
110
+ * The same reading covers a code of 0 beside a tally that names a failure: whatever ended the
111
+ * pipeline, a run whose log counts a failure did not pass.
112
+ *
113
+ * @param exitCode - The code the pipeline reported (null when it has none).
114
+ * @param output - The run's full log.
115
+ * @param piped - Whether a pipeline stage after the suite runner still consumes its output.
116
+ * @returns The code the run may report, or null when the code is the filter's or contradicts the tally.
117
+ */
118
+ export declare function suiteRunExitCode(exitCode: number | null, output: string, piped: boolean): number | null;
55
119
  /** What stopped a run. */
56
- export type CutBy = 'tool-budget' | 'command-timeout';
120
+ export type CutBy =
121
+ /** This tool's own budget (the backend's `timeout`, just past it). */
122
+ 'tool-budget'
123
+ /** The `timeout <N>` the command itself carried. */
124
+ | 'command-timeout'
125
+ /**
126
+ * Something outside the run: the runner's summary does not account for the total it announced,
127
+ * and neither of the two bounds above had fired — the process was killed by something this
128
+ * platform does not own (the sandbox's own process supervisor, the box going away, a signal).
129
+ */
130
+ | 'process-stop';
57
131
  /** A run a budget stopped, as its result reports it. */
58
132
  export interface CutRun {
59
133
  /** The bound the run was allowed, in seconds. */
@@ -65,9 +139,15 @@ export interface CutRun {
65
139
  * the pipeline's filter (`… | tail`) rather than to the run — the case that read as success.
66
140
  */
67
141
  exitCode: number | null;
68
- /** How many files or tests the runner's own output shows finishing, or null when it shows none. */
142
+ /**
143
+ * How far the run got, as its own output counts it — the files that finished, or the tests its
144
+ * summary accounts for — or null when the output says nothing.
145
+ */
69
146
  ran: number | null;
70
- /** How many the run was walking, when the runner printed it (Playwright's `[n/K]`), or null. */
147
+ /**
148
+ * How many the run was walking: the total its runner announced (`Running 139 tests`), or
149
+ * Playwright's `[n/K]`, whichever the output carries; null when neither does.
150
+ */
71
151
  total: number | null;
72
152
  /** The noun the runner counts in. */
73
153
  noun: 'files' | 'tests';
@@ -80,6 +160,9 @@ export interface CutRun {
80
160
  *
81
161
  * Evidence, in order — never a guess:
82
162
  *
163
+ * - the runner's own summary, when it printed one: it is a finish only if it accounts for the total
164
+ * the runner announced. A summary that accounts for fewer is a walk a process stopped early, and
165
+ * falls through to the branches below with its own counts ({@link suiteSummary}).
83
166
  * - `timeout`'s own 124 (the code the backend's wrapper returns whenever the TOOL's budget fired,
84
167
  * whatever the pipeline ended with) — and, when the clock says the command's own smaller bound
85
168
  * ran out first and less than the budget, that bound instead.
@@ -88,14 +171,19 @@ export interface CutRun {
88
171
  * killed at its own bound. Its exit code, when the pipeline handed one back, is the filter's.
89
172
  * - the run used this tool's whole budget behind a filter that withheld its output, and printed no
90
173
  * closing summary: the budget stopped it, and no code in the result is the run's.
174
+ * - a summary that does not account for its announced total, with neither bound fired: something
175
+ * outside this platform stopped the process ({@link CutBy} `process-stop`).
91
176
  *
92
177
  * @param command - The command the executor sent.
93
178
  * @param result - Its result.
94
179
  * @param elapsedMs - How long the command ran, in ms.
95
180
  * @param budgetSeconds - This tool's foreground budget, in seconds (0 when it has none).
181
+ * @param rawOutput - The run's output BEFORE truncation, when the caller has it: the announcement
182
+ * a summary is reconciled against is the first line a long run prints, which is exactly what a
183
+ * size cap drops. Detection reads this; the sentence's last line still comes from `result`.
96
184
  * @returns The stopped run, or null.
97
185
  */
98
- export declare function commandCutRun(command: string, result: unknown, elapsedMs: number, budgetSeconds?: number): CutRun | null;
186
+ export declare function commandCutRun(command: string, result: unknown, elapsedMs: number, budgetSeconds?: number, rawOutput?: string): CutRun | null;
99
187
  /**
100
188
  * The facts a result records about the run a budget stopped it in — what a reader needs to say
101
189
  * how far the run got WITHOUT re-reading its log or re-deriving the counts.
@@ -110,12 +198,14 @@ export declare function commandCutRun(command: string, result: unknown, elapsedM
110
198
  */
111
199
  export declare function cutRunFacts(result: unknown): CutRun | null;
112
200
  /**
113
- * The first line of a stopped run's result: how long it was allowed, what stopped it, how far it
114
- * got, and the last thing it printed.
201
+ * The first line of a stopped run's result: how far it got, what stopped it, the last thing it
202
+ * printed, and what the reader must do next.
115
203
  *
116
204
  * Written to be quotable by the model in one sentence and to leave no room for "nearly passing":
117
205
  * the counts come from the runner's own output ({@link runProgress}), and a `total` the caller
118
- * read elsewhere (the suite's own file count) fills in the "M of K" it can then state.
206
+ * read elsewhere (the suite's own file count) fills in the "of N" it can then state. The sentence
207
+ * says how many tests the stop left unrun — a number a reader cannot round up to "the suite
208
+ * passed" — and hands over the one move that survives a long run: run it in the background.
119
209
  *
120
210
  * @param run - The stopped run.
121
211
  * @returns The sentence.
@@ -1 +1 @@
1
- {"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AA0BD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK,GAAG,aAAa,GAAG,iBAAiB,CAAA;AAErD,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mGAAmG;IACnG,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,gGAAgG;IAChG,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AA2CD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,GAChB,MAAM,GAAG,IAAI,CA2Df;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAgB1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAkBlD"}
1
+ {"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AAiBD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAkDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA4B9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,IAAI,CAIf;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK;AACf,sEAAsE;AACpE,aAAa;AACf,oDAAoD;GAClD,iBAAiB;AACnB;;;;GAIG;GACD,cAAc,CAAA;AAElB,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAgFf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAgB1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAuBlD"}