@molecule/api-ai-tools 1.6.2 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-16T15:37:48.365Z
6
+ Generated: 2026-10-16T19:48:43.608Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -132,6 +132,29 @@ interface ToolSchema {
132
132
  }
133
133
  ```
134
134
 
135
+ #### `SuiteRunInFlight`
136
+
137
+ One suite run, as the rule that decides whether a second may start beside it reads it.
138
+
139
+ Both callers describe the runs in flight the same way and ask the same question of them; what
140
+ differs is only which runs each one knows about — the hold reads its ledger of background runs,
141
+ the note reads the one run it is waiting on.
142
+
143
+ ```typescript
144
+ interface SuiteRunInFlight {
145
+ /** Whether the run invokes a suite at all: a `vitest`/`jest` run is not one, and cannot be. */
146
+ runsSuite: boolean
147
+ /** The app and database the run measures, as {@link suiteCommandTarget} keys it. */
148
+ target: string
149
+ /**
150
+ * Whether the command being judged STOPS this run before starting its own — a kill comes first,
151
+ * so a run the command itself stops is not in the way (xr-molecule-flash-m87's
152
+ * `kill 19408 … ; npx playwright test …`).
153
+ */
154
+ stoppedByCommand?: boolean
155
+ }
156
+ ```
157
+
135
158
  #### `ExecutionBackend`
136
159
 
137
160
  Abstraction over the execution environment.
@@ -606,6 +629,108 @@ function commandRunsNoTests(command: string): boolean
606
629
 
607
630
  **Returns:** True when the command invokes a runner only to print.
608
631
 
632
+ #### `withoutHeredocs(command)`
633
+
634
+ A command with its heredoc bodies removed: the runs, kills and hosts it names in a file it
635
+ WRITES are not what the command itself does.
636
+
637
+ The header is kept (`cat > run.sh <<'EOF' …EOF` leaves `cat > run.sh`), which is what the
638
+ readers here want: everything before the body is still readable as the command's own words.
639
+
640
+ ```typescript
641
+ function withoutHeredocs(command: string): string
642
+ ```
643
+
644
+ - `command` — The shell command.
645
+
646
+ **Returns:** The command without its heredoc bodies.
647
+
648
+ #### `suiteCommandTarget(command)`
649
+
650
+ The server a suite command points at, as a comparable key: the host it names (`HOST=`,
651
+ `BASE_URL=`, `PLAYWRIGHT_BASE_URL=`, `--variable host=`, or a bare URL), or `local` for a run
652
+ that names none — a suite run in the sandbox goes to the sandbox's own app unless the command
653
+ says otherwise. Every localhost spelling collapses to `local` (one sandbox, one app, one
654
+ database).
655
+
656
+ ```typescript
657
+ function suiteCommandTarget(command: string): string
658
+ ```
659
+
660
+ - `command` — The suite command.
661
+
662
+ **Returns:** The target key.
663
+
664
+ #### `scopedRerunCommand(command, locations)`
665
+
666
+ The whole-suite command made scoped to the tests it should re-run instead: the same
667
+ invocation — same directory, same settings — with the named `file:line`s in place of the whole
668
+ suite, `--last-failed` dropped (it re-runs the tests that never ran too, X1 flash-m60) and
669
+ `--retries=0` set (a re-run of a known failure does not need to pay for retries).
670
+
671
+ ```typescript
672
+ function scopedRerunCommand(command: string, locations: readonly string[]): string
673
+ ```
674
+
675
+ - `command` — The whole-suite run's command.
676
+ - `locations` — The failing `file:line`s to re-run, as the run's own log printed them — or, for a run that started outside the suite, the absolute form Playwright matches each file argument against.
677
+
678
+ **Returns:** The scoped command, or the command unchanged when it runs no Playwright suite.
679
+
680
+ #### `suiteRunInTheWay(candidate, inFlight)`
681
+
682
+ The run in flight that makes a second run a hazard, or null when nothing is in the way.
683
+
684
+ This is the ONE rule the mid-run note and the suite-overlap hold both answer with: a suite run
685
+ may start only when this returns null, because two suites against one app and database share its
686
+ rows — the failures one causes the other cannot be told from real ones. A run that invokes no
687
+ suite is never in the way, and neither is one the candidate command stops first.
688
+
689
+ ```typescript
690
+ function suiteRunInTheWay(candidate: { target: string }, inFlight: readonly T[]): T | null
691
+ ```
692
+
693
+ - `candidate` — The run about to start, by the app and database it would measure ({@link suiteCommandTarget}).
694
+ - `inFlight` — The suite runs already going, in the order the caller found them.
695
+
696
+ **Returns:** The first run in the way, or null.
697
+
698
+ #### `isDiagnosisRun(command)`
699
+
700
+ Whether a command walks a PART of a suite instead of all of it: a cap that stops it at a failure
701
+ count, the failures the last run printed (`--last-failed`, `--only-changed`), a title filter, or
702
+ named files/lines.
703
+
704
+ THE one reading of that question, in one place. Two sites act on the answer and must never
705
+ disagree:
706
+
707
+ - the command rewriter that adds `--retries=0` to a background Playwright run
708
+ ({@link ./utilities.js} `streamingBackgroundCommand`) — a diagnosis walks each failure once,
709
+ while the run the suite is judged by keeps the suite's own retries; and
710
+ - the platform's own notes (molecule-dev's `suite-cli-override.ts`), which must not tell the
711
+ executor to drop the `--retries=0` the platform itself has just added — the contradiction X1
712
+ flash-m74 and m75 filed, and m76 filed a third time.
713
+
714
+ Until xr-molecule-flash-m89 they were a regex and a predicate that only LOOKED alike, and the
715
+ gap was exactly the run the platform had asked for: its `testRunNote` says "run the whole suite
716
+ once with `--max-failures=0`", and the rewriter read that cap flag as a capped diagnosis and
717
+ stripped the suite's retries from the uncapped proof run that follows a deploy — the one run the
718
+ graded run is compared against (+25:03, +31:39, +36:40 and +49:57, the last two against the
719
+ deployed URL, where a network blip is likeliest and the retries it dropped are the ones that
720
+ absorb it).
721
+
722
+ `--max-failures=0` is Playwright's own "no cap" ({@link boundsFailures}), so it is not a
723
+
724
+ _(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/suite-overlap.d.ts`.)_
725
+
726
+ ```typescript
727
+ function isDiagnosisRun(command: string): boolean
728
+ ```
729
+
730
+ - `command` — The command as the executor sent it.
731
+
732
+ **Returns:** True when the run is a diagnosis or a re-run, not the whole suite.
733
+
609
734
  #### `discoverSkills(backend)`
610
735
 
611
736
  Discover skills from a project directory.
@@ -644,6 +769,27 @@ function buildAgentPrompt(ctx: PromptContext): string
644
769
 
645
770
  **Returns:** Fully assembled system prompt text for the coding agent.
646
771
 
772
+ #### `taskStopCommand(backend, taskId)`
773
+
774
+ The command that stops a running background task, read from the sandbox's own pid files.
775
+
776
+ THE one builder for it, so every note that hands the executor a way to stop a run hands over the
777
+ same pid-only form: a `pkill -f <something>` matches the shell running it (its argv IS the
778
+ command), and `molecule-dev/api/src/ai/pattern-kill-guard.ts` holds exactly that — X1
779
+ xr-molecule-flash-m87's stuck note suggested `kill $(cat /tmp/<id>.pid) 2>/dev/null; pkill -f
780
+ <id>.sh`, which the platform's own guard then refused, and the same shape cost a refused call at
781
+ +21:57. The pid tree is the whole of what the pattern was for: the detached shell and every
782
+ process under it.
783
+
784
+ ```typescript
785
+ function taskStopCommand(backend: ExecutionBackend, taskId: string): Promise<string>
786
+ ```
787
+
788
+ - `backend` — The execution backend.
789
+ - `taskId` — The task's `mol-bg-…` handle.
790
+
791
+ **Returns:** The command to hand the executor — `kill <pids>` — falling back to the task's own pid file when the walk comes back empty (a sandbox without `pgrep`, or a pid file already gone, in which case the pid-file form is still the one the platform's guard names).
792
+
647
793
  #### `moleculeDistReadNote(path)`
648
794
 
649
795
  The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
@@ -662,6 +808,36 @@ function moleculeDistReadNote(path: string): string | null
662
808
 
663
809
  **Returns:** The note, or null when the path is not inside a package's `dist`.
664
810
 
811
+ #### `installedPackageWriteRefusal(path)`
812
+
813
+ The refusal for a write whose target is inside an installed package, or null
814
+ when the path is not under a `node_modules` directory.
815
+
816
+ `node_modules` is not the project's code: nothing written there survives the
817
+ next install, no deploy carries it, and a sandbox whose dependency tree is
818
+ shared (the pre-installed superset) breaks every other run through it. X0
819
+ xr-molecule-flash-m91 (+04:43) called `write_file` on
820
+ `node_modules/@molecule/api-resource-comment/dist/numbers.d.ts` while reading
821
+ the package's declarations and clobbered its runtime module a call later
822
+ (`module.exports = {}` over the ESM handler its own code imports) — the write
823
+ answered `ok: true`, and 18 s went to noticing, `npm pack`-ing the package and
824
+ putting the file back. Reading these files stays allowed (that is how the
825
+ declarations above are read at all).
826
+
827
+ The restore route is named because the model that has ALREADY clobbered a file
828
+ (this refusal is too late for it) is the one who needs it, and because the
829
+ obvious repair — `npm install <pkg>` — is itself refused in the pre-installed
830
+ sandbox. `npm pack` + a `tar` extraction over the package directory is what
831
+ m91 used, and what both guards leave alone.
832
+
833
+ ```typescript
834
+ function installedPackageWriteRefusal(path: string): string | null
835
+ ```
836
+
837
+ - `path` — An absolute path a tool is about to write.
838
+
839
+ **Returns:** The one-line refusal naming the package, or null when nothing is installed there.
840
+
665
841
  #### `buildTools(backend, config)`
666
842
 
667
843
  Build a complete set of AI agent tools bound to an execution backend.
@@ -1343,6 +1519,31 @@ function failureCausesNote(log: string): string | null
1343
1519
 
1344
1520
  **Returns:** The note, or null.
1345
1521
 
1522
+ #### `rerunOfRunningSuite(run, locations)`
1523
+
1524
+ The command that re-runs the tests at `locations` the way the run IN FLIGHT ran them: the same
1525
+ invocation, in the same directory, narrowed to those tests — or null when the running command
1526
+ is not a Playwright run this can narrow.
1527
+
1528
+ The `locations` are the paths the run's own log printed, which are relative to the directory
1529
+ that run started in, so a re-run has to resolve them there: the command names that directory
1530
+ unless it already changes into one of its own, because the executor's next command starts
1531
+ wherever it starts. The returned command is for AFTER the run ends — it is a second run against
1532
+ the same app and database, and that is exactly what is refused while the run walks
1533
+ ({@link suiteRunInTheWay}).
1534
+
1535
+ ```typescript
1536
+ function rerunOfRunningSuite(
1537
+ run: { command: string; cwd?: string },
1538
+ locations: readonly string[],
1539
+ ): string | null
1540
+ ```
1541
+
1542
+ - `run` — The run in flight: the command it was started with, as it was run, and the directory that command ran in.
1543
+ - `locations` — The failing `file:line`s to re-run, as the run's log printed them.
1544
+
1545
+ **Returns:** The re-run command, or null when there is no suite to narrow.
1546
+
1346
1547
  #### `repeatingFailureNote(found, o)`
1347
1548
 
1348
1549
  The sentence that names a cause repeating among a running suite's failures, mid-run.
@@ -1358,15 +1559,30 @@ cause was certain at the third, 47 s in, and the note answers exactly that; X1 f
1358
1559
  stopped eight runs at their first repeating cause in 14.5 minutes and never finished one, so
1359
1560
  the note must not say to stop when the run still has other causes to name.
1360
1561
 
1562
+ The run is STILL WALKING while this note is written — both callers produce it mid-run — so a
1563
+ re-run of one of its tests is a second run against the same app and database, and the platform
1564
+ refuses that ({@link suiteRunInTheWay}, the same rule its overlap hold answers with):
1565
+ xr-molecule-flash-m89 (2026-10-16) read _"Start on them now, while the run keeps walking.
1566
+ `null-fields.spec.ts:53` alone reproduces the first one."_ and sent exactly that re-run 23 s
1567
+ later, where the hold answered *"A playwright suite is already running against this app and
1568
+
1569
+ _(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/utilities.d.ts`.)_
1570
+
1361
1571
  ```typescript
1362
1572
  function repeatingFailureNote(
1363
1573
  found: RepeatingFailure,
1364
- o: { taskId: string; log: string; stop: string; universal?: UniversalCause | null },
1574
+ o: {
1575
+ taskId: string
1576
+ log: string
1577
+ stop: string
1578
+ universal?: UniversalCause | null
1579
+ run?: { command: string; cwd?: string }
1580
+ },
1365
1581
  ): string
1366
1582
  ```
1367
1583
 
1368
1584
  - `found` — What {@link repeatingFailure} found.
1369
- - `o` — The run's facts: its `taskId`, its `log` path, `stop` — the shell command that stops it, printed only for a {@link UniversalCause} — and `universal`, what {@link universalCause} found (absent means the run is not failing whole).
1585
+ - `o` — The run's facts: its `taskId`, its `log` path, `stop` — the shell command that stops it, printed only for a {@link UniversalCause} — `universal`, what {@link universalCause} found (absent means the run is not failing whole), and `run`, how the run was started ({@link rerunOfRunningSuite}) when the caller knows it.
1370
1586
 
1371
1587
  **Returns:** The note.
1372
1588
 
@@ -1779,6 +1995,18 @@ Peer dependencies:
1779
1995
  `newlineNote` hint beside an unrelated-looking parse error, which an executor
1780
1996
  repairs by rewriting the fused line's text and fuses the next one. The
1781
1997
  `newlineNote` alone is still returned when the file parses.
1998
+ - **A replacement that only changes a line break at its boundary is refused,
1999
+ not applied.** When `new_string` is `old_string` with its trailing newline
2000
+ added or removed and nothing else, the edit can only move text across lines —
2001
+ joining the next line onto the replaced text, or pushing the rest of the line
2002
+ down — which is what a weak model does by accident when it repeats a line it
2003
+ means to keep (flash-m28, m55, m62, m89, five times in the m89 run alone). The
2004
+ replacement is skipped, the rest of the batch is applied, and the result
2005
+ carries `skippedNote`: one line naming the replacement, what applying it would
2006
+ have done, and how to say it on purpose (include the next line in `old_string`).
2007
+ The file's own next character decides: text on the same line is a join or a
2008
+ split and is refused; a blank line after it, or the end of the file, means no
2009
+ text crosses lines and the edit is applied as written.
1782
2010
  - **A parameter under another name is not a broken tool.** Every tool's input
1783
2011
  is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
1784
2012
  snake_case), and a handler that throws returns an error naming the missing
@@ -22,7 +22,7 @@
22
22
  * (`head -30 run-api-tests-hurl.sh`, `grep -n playwright e2e/README.md`,
23
23
  * `git show HEAD:run-api-tests.sh`), and a runner named in a heredoc body — a file a command
24
24
  * WRITES is the caller's business, so a command with a heredoc goes through the caller's own
25
- * stripper first (`withoutHeredocs` in molecule-dev's suite checks).
25
+ * stripper first (`withoutHeredocs`, in `./suite-overlap.js`).
26
26
  *
27
27
  * @module
28
28
  */
@@ -1 +1 @@
1
- {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;CACb;AAqRD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAEjE;AA+BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
1
+ {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;CACb;AA8TD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAEjE;AA+BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
@@ -22,7 +22,7 @@
22
22
  * (`head -30 run-api-tests-hurl.sh`, `grep -n playwright e2e/README.md`,
23
23
  * `git show HEAD:run-api-tests.sh`), and a runner named in a heredoc body — a file a command
24
24
  * WRITES is the caller's business, so a command with a heredoc goes through the caller's own
25
- * stripper first (`withoutHeredocs` in molecule-dev's suite checks).
25
+ * stripper first (`withoutHeredocs`, in `./suite-overlap.js`).
26
26
  *
27
27
  * @module
28
28
  */
@@ -206,50 +206,84 @@ function commandStart(words) {
206
206
  * A single `&` separates only when it backgrounds a command: the one in a redirect (`2>&1`,
207
207
  * `&>log`) is part of the word it sits in.
208
208
  *
209
+ * The nestings are a STACK, not a (quote, depth) pair, because they interleave: a command
210
+ * substitution opened inside a double quote can itself hold double quotes, and each of those can
211
+ * hold another substitution. `DIR="$(cd "$(dirname "$0")" && pwd)"` is exactly that shape — it
212
+ * opens `"`, `$(`, `"`, `$(`, `"` in order — and every runner script of the RealWorld kind has it.
213
+ * A flat walk read that first inner `"` as the CLOSE of the outer one, so the `)` that ends the
214
+ * outer substitution landed inside a quote that was never opened, the depth was left standing at
215
+ * 1, and every newline in the rest of the script was swallowed: the script's own `hurl --test`
216
+ * never reached command position, and X1 xr-molecule-flash-m89 saw seven passing Hurl runs the
217
+ * platform could not see (the runner is read from the tree's own script TEXT —
218
+ * `molecule-dev/api/src/ai/supplied-tree-guard.ts` — so a script that does not classify records
219
+ * no runner, and every run of it reads as nothing).
220
+ *
209
221
  * @param command - The shell command.
210
222
  * @returns Each segment's text, in order, with empty ones dropped.
211
223
  */
212
224
  function splitSegments(command) {
213
225
  const parts = [];
214
226
  let buffer = '';
215
- let quote = null;
216
- let substitutions = 0;
227
+ /** The nestings this position sits inside, outermost first. */
228
+ const stack = [];
217
229
  for (let i = 0; i < command.length; i++) {
218
230
  const char = command[i] ?? '';
219
- if (quote !== null) {
231
+ const next = command[i + 1];
232
+ const top = stack[stack.length - 1];
233
+ // A single-quoted string expands nothing at all: no escape, no substitution, and no quote
234
+ // but its own ends it.
235
+ if (top === 'single') {
220
236
  buffer += char;
221
- if (char === '\\' && quote === '"' && i + 1 < command.length) {
222
- buffer += command[i + 1] ?? '';
223
- i += 1;
224
- }
225
- else if (char === quote)
226
- quote = null;
237
+ if (char === "'")
238
+ stack.pop();
227
239
  continue;
228
240
  }
229
- if (char === '"' || char === "'") {
230
- quote = char;
241
+ // A command substitution, wherever a shell opens one — inside double quotes too, and never
242
+ // inside single quotes (handled above). The `(` is consumed here so the depth is counted once.
243
+ if (char === '$' && next === '(') {
244
+ stack.push('sub');
245
+ buffer += '$(';
246
+ i += 1;
247
+ continue;
248
+ }
249
+ // Inside double quotes: a `"` closes them, a backslash escapes the next character (so
250
+ // `"\$("` opens nothing), and everything else is literal — a `'`, a `|`, a newline included.
251
+ if (top === 'double') {
252
+ if (char === '"')
253
+ stack.pop();
254
+ else if (char === '\\' && next !== undefined) {
255
+ buffer += char + next;
256
+ i += 1;
257
+ continue;
258
+ }
231
259
  buffer += char;
232
260
  continue;
233
261
  }
234
- if (char === '\\' && i + 1 < command.length) {
235
- buffer += char + (command[i + 1] ?? '');
236
- i += 1;
262
+ if (char === '"') {
263
+ stack.push('double');
264
+ buffer += char;
237
265
  continue;
238
266
  }
239
- if (char === '$' && command[i + 1] === '(') {
240
- substitutions += 1;
267
+ if (char === "'") {
268
+ stack.push('single');
241
269
  buffer += char;
242
270
  continue;
243
271
  }
244
- if (substitutions > 0) {
272
+ // Inside a substitution or a subshell of one: its own parentheses nest, its `|`/`;`/`&&`
273
+ // belong to the nested command, and its newlines are that command's too.
274
+ if (top === 'sub') {
245
275
  if (char === '(')
246
- substitutions += 1;
276
+ stack.push('sub');
247
277
  else if (char === ')')
248
- substitutions -= 1;
278
+ stack.pop();
249
279
  buffer += char;
250
280
  continue;
251
281
  }
252
- const next = command[i + 1];
282
+ if (char === '\\' && next !== undefined) {
283
+ buffer += char + next;
284
+ i += 1;
285
+ continue;
286
+ }
253
287
  if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
254
288
  parts.push(buffer);
255
289
  buffer = '';
package/dist/index.d.ts CHANGED
@@ -51,6 +51,18 @@
51
51
  * `newlineNote` hint beside an unrelated-looking parse error, which an executor
52
52
  * repairs by rewriting the fused line's text and fuses the next one. The
53
53
  * `newlineNote` alone is still returned when the file parses.
54
+ * - **A replacement that only changes a line break at its boundary is refused,
55
+ * not applied.** When `new_string` is `old_string` with its trailing newline
56
+ * added or removed and nothing else, the edit can only move text across lines —
57
+ * joining the next line onto the replaced text, or pushing the rest of the line
58
+ * down — which is what a weak model does by accident when it repeats a line it
59
+ * means to keep (flash-m28, m55, m62, m89, five times in the m89 run alone). The
60
+ * replacement is skipped, the rest of the batch is applied, and the result
61
+ * carries `skippedNote`: one line naming the replacement, what applying it would
62
+ * have done, and how to say it on purpose (include the next line in `old_string`).
63
+ * The file's own next character decides: text on the same line is a join or a
64
+ * split and is refused; a blank line after it, or the end of the file, means no
65
+ * text crosses lines and the edit is applied as written.
54
66
  * - **A parameter under another name is not a broken tool.** Every tool's input
55
67
  * is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
56
68
  * snake_case), and a handler that throws returns an error naming the missing
@@ -124,6 +136,7 @@ export * from './command-position.js';
124
136
  export * from './input-normalizer.js';
125
137
  export * from './no-test-run.js';
126
138
  export * from './schemas.js';
139
+ export * from './suite-overlap.js';
127
140
  export * from './system-prompt.js';
128
141
  export * from './tools.js';
129
142
  export * from './types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsHG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -51,6 +51,18 @@
51
51
  * `newlineNote` hint beside an unrelated-looking parse error, which an executor
52
52
  * repairs by rewriting the fused line's text and fuses the next one. The
53
53
  * `newlineNote` alone is still returned when the file parses.
54
+ * - **A replacement that only changes a line break at its boundary is refused,
55
+ * not applied.** When `new_string` is `old_string` with its trailing newline
56
+ * added or removed and nothing else, the edit can only move text across lines —
57
+ * joining the next line onto the replaced text, or pushing the rest of the line
58
+ * down — which is what a weak model does by accident when it repeats a line it
59
+ * means to keep (flash-m28, m55, m62, m89, five times in the m89 run alone). The
60
+ * replacement is skipped, the rest of the batch is applied, and the result
61
+ * carries `skippedNote`: one line naming the replacement, what applying it would
62
+ * have done, and how to say it on purpose (include the next line in `old_string`).
63
+ * The file's own next character decides: text on the same line is a join or a
64
+ * split and is refused; a blank line after it, or the end of the file, means no
65
+ * text crosses lines and the edit is applied as written.
54
66
  * - **A parameter under another name is not a broken tool.** Every tool's input
55
67
  * is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
56
68
  * snake_case), and a handler that throws returns an error naming the missing
@@ -124,6 +136,7 @@ export * from './command-position.js';
124
136
  export * from './input-normalizer.js';
125
137
  export * from './no-test-run.js';
126
138
  export * from './schemas.js';
139
+ export * from './suite-overlap.js';
127
140
  export * from './system-prompt.js';
128
141
  export * from './tools.js';
129
142
  export * from './types.js';
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The ONE rule that decides whether a suite run may start while another is already walking, and
3
+ * the command readings both sides of it need.
4
+ *
5
+ * Two sites ask "may a second run start now?" about the same pair of runs — the mid-run note that
6
+ * tells the executor what to do next while the suite it is waiting on is still going
7
+ * ({@link ./utilities.js} `repeatingFailureNote`), and the hold that refuses the run when it
8
+ * arrives (molecule-dev's `suite-overlap-hold.ts`) — and until xr-molecule-flash-m89 they answered
9
+ * it separately. That run's +19:59 `wait_for_task` handed over a repeating cause with "the test
10
+ * alone reproduces the first one … start on them now, while the run keeps walking"; 23 s later the
11
+ * executor sent exactly that re-run, and the overlap hold refused it — "A playwright suite is
12
+ * already running against this app and database … Wait for it." Two orders, one run, and the round
13
+ * trip between them was the whole cost.
14
+ *
15
+ * So the rule lives here ({@link suiteRunInTheWay}) and both of them call it: a run may start only
16
+ * when nothing in flight is already measuring the same app and database. What "the same app and
17
+ * database" MEANS lives here too ({@link suiteCommandTarget}) — the note and the hold have to key
18
+ * a command the same way before they can agree on whether two of them collide, and the re-run a
19
+ * note hands over is built from the same reading of the running command
20
+ * ({@link scopedRerunCommand}).
21
+ *
22
+ * The same treatment for the other pair that answered one question twice: whether a run walks the
23
+ * WHOLE suite or a part of it. The rewriter that decides whether a background Playwright run gets
24
+ * `--retries=0` ({@link ./utilities.js} `streamingBackgroundCommand`) and the platform's own notes
25
+ * (molecule-dev's `suite-cli-override.ts`) asked it separately — a regex there, a predicate here —
26
+ * until xr-molecule-flash-m89 showed the two answers were never the same question
27
+ * ({@link isDiagnosisRun}).
28
+ *
29
+ * @module
30
+ */
31
+ /**
32
+ * A command with its heredoc bodies removed: the runs, kills and hosts it names in a file it
33
+ * WRITES are not what the command itself does.
34
+ *
35
+ * The header is kept (`cat > run.sh <<'EOF' …EOF` leaves `cat > run.sh`), which is what the
36
+ * readers here want: everything before the body is still readable as the command's own words.
37
+ *
38
+ * @param command - The shell command.
39
+ * @returns The command without its heredoc bodies.
40
+ */
41
+ export declare function withoutHeredocs(command: string): string;
42
+ /**
43
+ * The server a suite command points at, as a comparable key: the host it names (`HOST=`,
44
+ * `BASE_URL=`, `PLAYWRIGHT_BASE_URL=`, `--variable host=`, or a bare URL), or `local` for a run
45
+ * that names none — a suite run in the sandbox goes to the sandbox's own app unless the command
46
+ * says otherwise. Every localhost spelling collapses to `local` (one sandbox, one app, one
47
+ * database).
48
+ *
49
+ * @param command - The suite command.
50
+ * @returns The target key.
51
+ */
52
+ export declare function suiteCommandTarget(command: string): string;
53
+ /**
54
+ * The whole-suite command made scoped to the tests it should re-run instead: the same
55
+ * invocation — same directory, same settings — with the named `file:line`s in place of the whole
56
+ * suite, `--last-failed` dropped (it re-runs the tests that never ran too, X1 flash-m60) and
57
+ * `--retries=0` set (a re-run of a known failure does not need to pay for retries).
58
+ *
59
+ * @param command - The whole-suite run's command.
60
+ * @param locations - The failing `file:line`s to re-run, as the run's own log printed them — or,
61
+ * for a run that started outside the suite, the absolute form Playwright matches each file
62
+ * argument against.
63
+ * @returns The scoped command, or the command unchanged when it runs no Playwright suite.
64
+ */
65
+ export declare function scopedRerunCommand(command: string, locations: readonly string[]): string;
66
+ /**
67
+ * One suite run, as the rule that decides whether a second may start beside it reads it.
68
+ *
69
+ * Both callers describe the runs in flight the same way and ask the same question of them; what
70
+ * differs is only which runs each one knows about — the hold reads its ledger of background runs,
71
+ * the note reads the one run it is waiting on.
72
+ */
73
+ export interface SuiteRunInFlight {
74
+ /** Whether the run invokes a suite at all: a `vitest`/`jest` run is not one, and cannot be. */
75
+ runsSuite: boolean;
76
+ /** The app and database the run measures, as {@link suiteCommandTarget} keys it. */
77
+ target: string;
78
+ /**
79
+ * Whether the command being judged STOPS this run before starting its own — a kill comes first,
80
+ * so a run the command itself stops is not in the way (xr-molecule-flash-m87's
81
+ * `kill 19408 … ; npx playwright test …`).
82
+ */
83
+ stoppedByCommand?: boolean;
84
+ }
85
+ /**
86
+ * The run in flight that makes a second run a hazard, or null when nothing is in the way.
87
+ *
88
+ * This is the ONE rule the mid-run note and the suite-overlap hold both answer with: a suite run
89
+ * may start only when this returns null, because two suites against one app and database share its
90
+ * rows — the failures one causes the other cannot be told from real ones. A run that invokes no
91
+ * suite is never in the way, and neither is one the candidate command stops first.
92
+ *
93
+ * @param candidate - The run about to start, by the app and database it would measure
94
+ * ({@link suiteCommandTarget}).
95
+ * @param inFlight - The suite runs already going, in the order the caller found them.
96
+ * @returns The first run in the way, or null.
97
+ */
98
+ export declare function suiteRunInTheWay<T extends SuiteRunInFlight>(candidate: {
99
+ target: string;
100
+ }, inFlight: readonly T[]): T | null;
101
+ /**
102
+ * Whether a command walks a PART of a suite instead of all of it: a cap that stops it at a failure
103
+ * count, the failures the last run printed (`--last-failed`, `--only-changed`), a title filter, or
104
+ * named files/lines.
105
+ *
106
+ * THE one reading of that question, in one place. Two sites act on the answer and must never
107
+ * disagree:
108
+ *
109
+ * - the command rewriter that adds `--retries=0` to a background Playwright run
110
+ * ({@link ./utilities.js} `streamingBackgroundCommand`) — a diagnosis walks each failure once,
111
+ * while the run the suite is judged by keeps the suite's own retries; and
112
+ * - the platform's own notes (molecule-dev's `suite-cli-override.ts`), which must not tell the
113
+ * executor to drop the `--retries=0` the platform itself has just added — the contradiction X1
114
+ * flash-m74 and m75 filed, and m76 filed a third time.
115
+ *
116
+ * Until xr-molecule-flash-m89 they were a regex and a predicate that only LOOKED alike, and the
117
+ * gap was exactly the run the platform had asked for: its `testRunNote` says "run the whole suite
118
+ * once with `--max-failures=0`", and the rewriter read that cap flag as a capped diagnosis and
119
+ * stripped the suite's retries from the uncapped proof run that follows a deploy — the one run the
120
+ * graded run is compared against (+25:03, +31:39, +36:40 and +49:57, the last two against the
121
+ * deployed URL, where a network blip is likeliest and the retries it dropped are the ones that
122
+ * absorb it).
123
+ *
124
+ * `--max-failures=0` is Playwright's own "no cap" ({@link boundsFailures}), so it is not a
125
+ * diagnosis. A command that starts no Playwright run is not one either (false): the question is
126
+ * about a run already read as a Playwright suite.
127
+ *
128
+ * @param command - The command as the executor sent it.
129
+ * @returns True when the run is a diagnosis or a re-run, not the whole suite.
130
+ */
131
+ export declare function isDiagnosisRun(command: string): boolean;
132
+ //# sourceMappingURL=suite-overlap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suite-overlap.d.ts","sourceRoot":"","sources":["../src/suite-overlap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoB1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAexF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,SAAS,EAAE,OAAO,CAAA;IAClB,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,EACzD,SAAS,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EAC7B,QAAQ,EAAE,SAAS,CAAC,EAAE,GACrB,CAAC,GAAG,IAAI,CAMV;AAqDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQvD"}