@molecule/api-ai-tools 1.9.18 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-18T15:48:52.035Z
6
+ Generated: 2026-10-18T22:07:04.913Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -66,7 +66,9 @@ One statement or pipeline stage of a shell command: what it runs, and with what.
66
66
  interface CommandSegment {
67
67
  /**
68
68
  * The word in command position, its directory removed (`/usr/bin/hurl` → `hurl`): the program
69
- * the shell would run once whatever wrappers stood in front of it are taken away.
69
+ * the shell would run once whatever wrappers stood in front of it are taken away. When a
70
+ * wrapper runs a tool's own CLI by file, this is the tool's name — `node
71
+ * <path>/node_modules/.bin/playwright test` is a `playwright` invocation, not a `node` one.
70
72
  */
71
73
  program: string
72
74
  /** The words after it, as written — quotes kept, so a quoted `"a|b"` is one word. */
@@ -284,9 +286,13 @@ A cutting stage that follows the runner, and the command with the whole pipe rem
284
286
 
285
287
  ```typescript
286
288
  interface RunnerOutputFilter {
287
- /** The stage exactly as the runner's command wrote it — `tail -6`. */
289
+ /**
290
+ * The stage exactly as the runner's command wrote it — `tail -6`. When several cutting filters
291
+ * follow the runner, this is the whole trailing chain as written, pipes included:
292
+ * `grep -E "Failure|actual" | head -40`.
293
+ */
288
294
  stage: string
289
- /** The pipe as a sentence names it — `| tail -6`. */
295
+ /** The pipe as a sentence names it — `| tail -6`, or the whole chain `| grep … | head -40`. */
290
296
  pipe: string
291
297
  /**
292
298
  * The command with the pipe and the stage removed — `hurl --variable uid=… --test /tmp/t.hurl
@@ -333,6 +339,17 @@ interface LeadingSleep {
333
339
  }
334
340
  ```
335
341
 
342
+ #### `RunEnvSetting`
343
+
344
+ One `NAME=value` the request requires of a suite run.
345
+
346
+ ```typescript
347
+ interface RunEnvSetting {
348
+ name: string
349
+ value: string
350
+ }
351
+ ```
352
+
336
353
  #### `SuiteRunInFlight`
337
354
 
338
355
  One suite run, as the rule that decides whether a second may start beside it reads it.
@@ -511,6 +528,17 @@ interface TraceAction {
511
528
  pageId?: string
512
529
  /** The frame the action ran in, from the same snapshot. */
513
530
  frameId?: string
531
+ /**
532
+ * The selector a `fill`/`type`/`selectOption` acted on (`textarea[name="bio"]`), from the
533
+ * trace's own params. Kept so the note can compare what the test typed with what the page
534
+ * submitted (m173: the field it typed into was sent empty).
535
+ */
536
+ selector?: string
537
+ /**
538
+ * The value a `fill`/`type`/`selectOption` typed or chose, from the trace's own params —
539
+ * absent when the action carried none (a `selectOption` by label only).
540
+ */
541
+ value?: string
514
542
  }
515
543
  ```
516
544
 
@@ -561,6 +589,33 @@ interface TraceMissingText {
561
589
  }
562
590
  ```
563
591
 
592
+ #### `TraceReadContradiction`
593
+
594
+ A write the trace recorded answering a boolean field, and a LATER GET of the same resource
595
+ answering the opposite for that field.
596
+
597
+ This is the pair that reads as a frontend race and is not one: xr-molecule-flash-m181's follow
598
+ test sent `POST /api/profiles/:username/follow` and the API answered
599
+ `{"profile":{…,"following":true}}`, then a `GET /api/profiles/:username` answered
600
+ `{"profile":{…,"following":false}}` — the READ route never saw the viewer, so it hydrated
601
+ `following` from nobody. The run rewrote the profile component twice, the API client once, and
602
+ added optimistic state before anyone read the route's middleware; the write itself was fine.
603
+
604
+ ```typescript
605
+ interface TraceReadContradiction {
606
+ /** The boolean field both answered (`following`, `favorited`). */
607
+ field: string
608
+ /** What the write's response said it is. */
609
+ writeValue: boolean
610
+ /** What the later read's response said it is. */
611
+ readValue: boolean
612
+ /** The write that answered it (`POST /api/profiles/x/follow`). */
613
+ write: TraceRequest
614
+ /** The later GET of the same resource (`GET /api/profiles/x`). */
615
+ read: TraceRequest
616
+ }
617
+ ```
618
+
564
619
  #### `TraceArtifact`
565
620
 
566
621
  What one trace says: the test it belongs to, the requests it made, and its console lines.
@@ -641,6 +696,15 @@ interface TraceArtifact {
641
696
  * read before this field existed.
642
697
  */
643
698
  missingText?: TraceMissingText
699
+ /**
700
+ * A write's answer a later GET of the same resource contradicted — the read route does not see
701
+ * the viewer (xr-molecule-flash-m181's `following`). The note leads with it and sends the run at
702
+ * the route's middleware, not the page's state handling.
703
+ *
704
+ * Absent when no write and later read in the trace answered the same boolean field differently,
705
+ * and on traces read before this field existed.
706
+ */
707
+ readContradiction?: TraceReadContradiction
644
708
  }
645
709
  ```
646
710
 
@@ -897,6 +961,26 @@ interface ToolBuildConfig {
897
961
  */
898
962
  excludedTestTitles?: () => readonly string[]
899
963
 
964
+ /**
965
+ * The `NAME=value` settings the person's request REQUIRES of a suite run (molecule-dev's
966
+ * `requiredRunSettings` — its mode, say), asked when the cause note composes a narrowed re-run
967
+ * of a finished run's failures. A re-run composed from the executor's own command inherits what
968
+ * that command set — and nothing it left out, so a run that measured the suite in `spa` handed
969
+ * back a re-run that would measure `spa` again (xr-molecule-flash-m182 +12:06: the request names
970
+ * `TEST_MODE=fullstack`, the run and its re-run both omitted it).
971
+ *
972
+ * A getter, not an array: the request is read per turn while the tool set is built once, and
973
+ * what the suite's own files read is the consumer's reading. Unset answers "nothing required".
974
+ *
975
+ * @param command - The command the finished run was started with.
976
+ * @param cwd - The resolved working directory it ran in.
977
+ * @returns The settings to put on a re-run of that command.
978
+ */
979
+ requiredSuiteEnv?: (
980
+ command: string,
981
+ cwd: string,
982
+ ) => Promise<readonly { name: string; value: string }[] | null | undefined>
983
+
900
984
  /**
901
985
  * The supplied suite's own budget, when the Playwright config a command would load sets
902
986
  * different settings, asked BEFORE the run executes. The consumer knows where the suite and its
@@ -1699,10 +1783,13 @@ function commandRunsNoTests(command: string): boolean
1699
1783
 
1700
1784
  The output filter that follows the runner in `command`, or null when there is none.
1701
1785
 
1702
- Only a stage of the runner's OWN pipeline counts (its separator is `|`), and only when it is the
1703
- LAST stage of that pipeline: a `| grep … | wc -l` chain would leave a dangling pipe if the
1704
- middle stage were cut out, so a chain is not answered here at all. A filter before the runner
1705
- (`cat run.log | tail -20 && hurl …`) reads something else's output and is not this.
1786
+ Only stages of the runner's OWN pipeline count (their separator is `|`), and they are read as
1787
+ one trailing CHAIN: `| grep … | head -40` is two cuts, and removing only the first would leave
1788
+ the run piped into the second. Every stage after the runner must be a cutting filter and the
1789
+ chain must run to the pipeline's end — a pipeline that continues into `wc -l` is not answered
1790
+ here, because removing the cutting stages alone would still leave the failure hidden. A filter
1791
+ before the runner (`cat run.log | tail -20 && hurl …`) reads something else's output and is not
1792
+ this.
1706
1793
 
1707
1794
  ```typescript
1708
1795
  function outputFilterAfterRunner(
@@ -1881,13 +1968,17 @@ function withSuiteConfig(command: string, config: string | null | undefined): st
1881
1968
 
1882
1969
  **Returns:** The command with the flag, or the command unchanged.
1883
1970
 
1884
- #### `scopedRerunCommand(command, locations, excluded, config, cwd)`
1971
+ #### `scopedRerunCommand(command, locations, excluded, config, cwd, env)`
1885
1972
 
1886
1973
  The whole-suite command made scoped to the tests it should re-run instead: the same
1887
1974
  invocation — same directory, same settings — with the named `file:line`s in place of the whole
1888
1975
  suite, `--last-failed` dropped (it re-runs the tests that never ran too, X1 flash-m60) and
1889
1976
  `--retries=0` set (a re-run of a known failure does not need to pay for retries).
1890
1977
 
1978
+ The re-run keeps the run's `> <file>` redirect only in the sense of keeping its OUTPUT there:
1979
+ the target is rewritten to a fresh sibling ({@link redirectToFreshFile}), so a re-run never
1980
+ overwrites the log the run itself is the evidence for (xr-molecule-flash-m173).
1981
+
1891
1982
  `config` is the config the run was measured under ({@link withSuiteConfig}): the narrowed re-run
1892
1983
  carries it whenever the command itself does not, so a run that dropped `--config` cannot be
1893
1984
  handed back a command with the same defect (xr-molecule-flash-m113: every failing test was
@@ -1900,6 +1991,7 @@ function scopedRerunCommand(
1900
1991
  excluded?: readonly string[],
1901
1992
  config?: string | null,
1902
1993
  cwd?: string | null,
1994
+ env?: readonly RunEnvSetting[],
1903
1995
  ): string
1904
1996
  ```
1905
1997
 
@@ -1908,9 +2000,32 @@ function scopedRerunCommand(
1908
2000
  - `excluded` — The test titles the request carves out. They are inverted out (`--grep-invert`) so a re-run of the failing locations cannot report one of them again — xr-molecule-flash-m101 re-ran the whole 139-test suite and spent ~4 minutes to be told about a test the request had already said does not count.
1909
2001
  - `config` — The config the run loaded, when the caller knows it; added when the command does not name one.
1910
2002
  - `cwd` — The directory `exec_command` was given, when it was; the base a location is resolved against when the command does not `cd` anywhere.
2003
+ - `env` — The settings the REQUEST requires of the suite and this run left out ({@link withRunEnv}): the re-run carries them, so a re-run of a run measured in the wrong mode is not measured in that mode again (xr-molecule-flash-m182 +12:06).
1911
2004
 
1912
2005
  **Returns:** The scoped command, or the command unchanged when it runs no Playwright suite.
1913
2006
 
2007
+ #### `withRunEnv(command, settings)`
2008
+
2009
+ The command with the request's required settings put in front of its RUN statement — the ONE
2010
+ place every composed re-run gets them (xr-molecule-flash-m182).
2011
+
2012
+ The settings are written at the statement's own assignment offset, never in front of the whole
2013
+ command: a shell binds an assignment there to the statement's program, while one in front of the
2014
+ whole command is the environment of its FIRST statement alone (`APP_BASE=… cd app && npx
2015
+ playwright test` sets the variable for `cd`; xr-molecule-flash-m103 +28:08). A name the command
2016
+ gives a different value is replaced, and one it already gives the required value is left as
2017
+ written. When the parse cannot place the statement (a run inside a nested `sh -c '…'`), the
2018
+ settings become `export`s, which no statement boundary can swallow.
2019
+
2020
+ ```typescript
2021
+ function withRunEnv(command: string, settings?: readonly RunEnvSetting[]): string
2022
+ ```
2023
+
2024
+ - `command` — The command to carry them.
2025
+ - `settings` — The `NAME`/`value` pairs the request requires.
2026
+
2027
+ **Returns:** The command, or the command unchanged when it already carries every setting.
2028
+
1914
2029
  #### `suiteRunInTheWay(candidate, inFlight)`
1915
2030
 
1916
2031
  The run in flight that makes a second run a hazard, or null when nothing is in the way.
@@ -2226,6 +2341,25 @@ function stalledDocumentSubresource(request: TraceRequest): boolean
2226
2341
 
2227
2342
  **Returns:** True when the request is a document subresource that never finished.
2228
2343
 
2344
+ #### `isCredentialExchangePath(path, authEndpoints)`
2345
+
2346
+ Whether a request PATH asks the app to mint a credential — a login, a registration, a token
2347
+ endpoint — as opposed to a protected resource that presents one.
2348
+
2349
+ This is the path half of {@link isCredentialExchange}, exported so a caller holding only a
2350
+ `METHOD path` reads the same classification: a Hurl failing entry says
2351
+ `POST /api/users/login` and nothing about the body, and the whole-suite note must not explain
2352
+ a wrong-password login that answered 200 as a cookie-jar effect (xr-molecule-flash-m176).
2353
+
2354
+ ```typescript
2355
+ function isCredentialExchangePath(path: string, authEndpoints?: readonly string[]): boolean
2356
+ ```
2357
+
2358
+ - `path` — The request path (`/api/users/login`); a query or fragment is ignored.
2359
+ - `authEndpoints` — The endpoints the app's auth config names (`/users/login`).
2360
+
2361
+ **Returns:** True when the path names a credential exchange.
2362
+
2229
2363
  #### `failureTraceNote(items, target, earlierFailures, apiPrefixes, report)`
2230
2364
 
2231
2365
  The note a failed suite run's result carries: what each failing test's page actually sent, and
@@ -3372,14 +3506,20 @@ reads as ten defects in the app.
3372
3506
  function failureCausesNote(
3373
3507
  log: string,
3374
3508
  excluded?: readonly string[],
3375
- run?: { command: string; cwd?: string; log?: string; config?: string | null },
3509
+ run?: {
3510
+ command: string
3511
+ cwd?: string
3512
+ log?: string
3513
+ config?: string | null
3514
+ env?: readonly RunEnvSetting[]
3515
+ },
3376
3516
  pageErrors?: readonly CausePageErrors[],
3377
3517
  ): string | null
3378
3518
  ```
3379
3519
 
3380
3520
  - `log` — The finished run's output.
3381
3521
  - `excluded` — The test titles the request carves out, if the caller knows them.
3382
- - `run` — How the run was started, when the caller knows it: the narrowed re-run is built from `command`/`cwd`/`config`, and `log` (the path the output was written to) is what the "left out" sentence greps when more causes exist than the result has room for.
3522
+ - `run` — How the run was started, when the caller knows it: the narrowed re-run is built from `command`/`cwd`/`config`/`env`, and `log` (the path the output was written to) is what the "left out" sentence greps when more causes exist than the result has room for.
3383
3523
 
3384
3524
  **Returns:** The note, or null.
3385
3525
 
@@ -3398,13 +3538,13 @@ refused while the run walks ({@link suiteRunInTheWay}).
3398
3538
 
3399
3539
  ```typescript
3400
3540
  function rerunOfRunningSuite(
3401
- run: { command: string; cwd?: string; config?: string | null },
3541
+ run: { command: string; cwd?: string; config?: string | null; env?: readonly RunEnvSetting[] },
3402
3542
  locations: readonly string[],
3403
3543
  excluded?: readonly string[],
3404
3544
  ): string | null
3405
3545
  ```
3406
3546
 
3407
- - `run` — The run in flight: the command it was started with, as it was run, the directory that command ran in, and the config it loaded when the caller knows it.
3547
+ - `run` — The run in flight: the command it was started with, as it was run, the directory that command ran in, the config it loaded when the caller knows it, and the settings the request required and the run left out (`env`) — the re-run carries them, so it never reproduces the configuration the run was measured under by mistake (m182).
3408
3548
  - `locations` — The failing `file:line`s to re-run, as the run's log printed them.
3409
3549
  - `excluded` — The test titles the request carves out: the re-run inverts them out (`--grep-invert`) so it cannot report them again.
3410
3550
 
@@ -3443,7 +3583,7 @@ function repeatingFailureNote(
3443
3583
  log: string
3444
3584
  stop: string
3445
3585
  universal?: UniversalCause | null
3446
- run?: { command: string; cwd?: string }
3586
+ run?: { command: string; cwd?: string; env?: readonly RunEnvSetting[] }
3447
3587
  editsInvalidateRun?: boolean
3448
3588
  },
3449
3589
  ): string
@@ -4026,11 +4166,22 @@ const UNIVERSAL_CAUSE_FILES: 3
4026
4166
  #### `EMPTY_PAGE_CAUSE_TESTS`
4027
4167
 
4028
4168
  How many tests a run must have walked — with nothing passing since its first failure — before
4029
- "the page renders nothing" can be its one cause. A handful of failures is a handful of bugs;
4030
- a page that never painted fails every spec that reaches it, and m141's walk had 36 by +20:30.
4031
-
4032
- ```typescript
4033
- const EMPTY_PAGE_CAUSE_TESTS: 15
4169
+ "the page renders nothing" can be its one cause.
4170
+
4171
+ THREE, and the number is the cost of the alternative. The old floor of 15 was chosen when the
4172
+ reading was only offered at a completed run; once it STOPS the walk, every test waited for is
4173
+ a test whose result is thrown away, and a blank page costs its per-test budget each time
4174
+ (m177: 18 tests, ~4.2 minutes, on a page whose thrown error was visible in the first
4175
+ `pageerror`). Three is the smallest count that is not one page's own bug: the cause additionally
4176
+ needs three DISTINCT spec files (`UNIVERSAL_CAUSE_FILES`, or the files a narrowed run selected),
4177
+ each spec waiting for its OWN first element, and no test passing after the first failure. A page
4178
+ that renders misses elements, but three unrelated specs in three files failing at three
4179
+ different first locators with nothing passing in between is the page, not three bugs. At the
4180
+ suite's own 5 s action timeout that verdict lands ~20 s into the walk, so the early stop costs
4181
+ about one test's worth of time instead of fifteen.
4182
+
4183
+ ```typescript
4184
+ const EMPTY_PAGE_CAUSE_TESTS: 3
4034
4185
  ```
4035
4186
 
4036
4187
  #### `CONNECTION_CAUSE_TESTS`
@@ -36,7 +36,9 @@
36
36
  export interface CommandSegment {
37
37
  /**
38
38
  * The word in command position, its directory removed (`/usr/bin/hurl` → `hurl`): the program
39
- * the shell would run once whatever wrappers stood in front of it are taken away.
39
+ * the shell would run once whatever wrappers stood in front of it are taken away. When a
40
+ * wrapper runs a tool's own CLI by file, this is the tool's name — `node
41
+ * <path>/node_modules/.bin/playwright test` is a `playwright` invocation, not a `node` one.
40
42
  */
41
43
  program: string;
42
44
  /** The words after it, as written — quotes kept, so a quoted `"a|b"` is one word. */
@@ -1 +1 @@
1
- {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;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;IACZ;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAqND;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAmjBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAmCD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;IAC9B,0FAA0F;IAC1F,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAE/E;AAmDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,MAAM,GAAG,IAAI,CAyDf;AAwFD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,GAC1C;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAoD7C;AAED;;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AA8RD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAwkBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAmCD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;IAC9B,0FAA0F;IAC1F,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAE/E;AAmDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,MAAM,GAAG,IAAI,CAyDf;AA2FD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,GAC1C;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAoD7C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
@@ -68,6 +68,75 @@ const LAUNCHERS = new Set([
68
68
  ]);
69
69
  /** The shells whose `-c '<command>'` argument is a command of its own. */
70
70
  const SHELLS = new Set(['ash', 'bash', 'dash', 'ksh', 'sh', 'zsh']);
71
+ /**
72
+ * Node, which is a wrapper only when the script it runs IS a tool's own CLI — never in general:
73
+ * `node script.mjs` RUNS that script, so the parser's answer for it stays `node`.
74
+ *
75
+ * The Playwright runner is spelled this way whenever a project reaches it by file instead of by
76
+ * the `.bin` shim: xr-molecule-flash-m179 (+17:51) ran
77
+ * `node /workspace/node_modules/.bin/playwright test` after `npx playwright test` in every
78
+ * earlier call, and no reader recognised it — the run got no `--trace`, no suite-budget pin and
79
+ * no proof credit, and the platform spent the rest of the turn re-walking a suite it had already
80
+ * walked green.
81
+ */
82
+ const NODE = new Set(['node']);
83
+ /**
84
+ * The Playwright CLI, as the path operand of a `node` invocation: the package's `.bin` shim, or
85
+ * the `cli.js` a package installs (`playwright/cli.js`, `@playwright/test/cli.js`).
86
+ */
87
+ const PLAYWRIGHT_CLI = /(?:^|\/)node_modules\/(?:\.bin\/playwright|(?:playwright|@playwright\/test)\/cli\.(?:js|mjs|cjs))$/;
88
+ /**
89
+ * The runner a `node` script operand IS, or null when the operand is any other script.
90
+ *
91
+ * @param word - The script operand, as written.
92
+ * @returns The runner's name (`playwright`), or null.
93
+ */
94
+ function nodeScriptRunner(word) {
95
+ return PLAYWRIGHT_CLI.test(unquoted(word)) ? 'playwright' : null;
96
+ }
97
+ /**
98
+ * Node's own value-taking options: the ones that consume the word after them, so the script
99
+ * operand is not mistaken for a value (`node -r tsx/cli.js …`). Node also accepts `--flag=value`.
100
+ */
101
+ const NODE_VALUE_FLAGS = new Set([
102
+ '-e',
103
+ '--eval',
104
+ '-p',
105
+ '--print',
106
+ '-r',
107
+ '--require',
108
+ '--import',
109
+ '--loader',
110
+ '--experimental-loader',
111
+ '--input-type',
112
+ '--conditions',
113
+ '--title',
114
+ ]);
115
+ /**
116
+ * The word a `node` invocation runs as its script: the first operand after node's own options,
117
+ * with `--` ending the options. Null when node runs no file (`node --test`, `node -e '<code>'`).
118
+ *
119
+ * A wrong guess here is harmless: the operand is then checked against {@link PLAYWRIGHT_CLI}, so
120
+ * an ordinary script (`node probe.mjs`) is never read as a runner.
121
+ *
122
+ * @param words - The segment's words.
123
+ * @param from - The index just after `node`.
124
+ * @returns The script operand's index, or null.
125
+ */
126
+ function nodeScriptIndex(words, from) {
127
+ let at = from;
128
+ for (let guard = 0; guard < MAX_WRAPPERS && at < words.length; guard++) {
129
+ const word = words[at] ?? '';
130
+ if (!word.startsWith('-'))
131
+ return at;
132
+ const flag = unquoted(word);
133
+ // `--` ends node's options; the script is the next word.
134
+ if (flag === '--')
135
+ return at + 1 < words.length ? at + 1 : null;
136
+ at += flag.includes('=') || !NODE_VALUE_FLAGS.has(flag) ? 1 : 2;
137
+ }
138
+ return null;
139
+ }
71
140
  /** Package runners that run the program named after them (`npx playwright test`). */
72
141
  const PACKAGE_RUNNERS = new Set(['bunx', 'npx', 'pnpx']);
73
142
  /** Package managers whose sub-command runs a package's binary (`bun x`, `pnpm dlx`, `npm exec`). */
@@ -345,6 +414,8 @@ function nestedCommand(words, from) {
345
414
  * @returns The index of the word in command position (one past the end when the segment runs
346
415
  * nothing), the command string of a `sh -c '<command>'` this segment turns out to be, and the
347
416
  * seconds its own `timeout` wrapper allows that command (`timeout 280 hurl …` → 280) or null.
417
+ * `program` names what runs when that is not the word itself's name — `node <path>/playwright`
418
+ * is a `playwright` invocation, not a `node` one ({@link nodeScriptRunner}).
348
419
  */
349
420
  function commandStart(words) {
350
421
  let at = 0;
@@ -364,14 +435,14 @@ function commandStart(words) {
364
435
  if (SHELLS.has(name)) {
365
436
  const nested = nestedCommand(words, at + 1);
366
437
  if (nested !== null)
367
- return { at, nested, timeoutSeconds };
438
+ return { at, nested, timeoutSeconds, program: null };
368
439
  at += 1 + flagWords(words, at + 1, name);
369
440
  continue;
370
441
  }
371
442
  if (PACKAGE_RUNNERS.has(name)) {
372
443
  const runner = packageRunnerOptions(words, at + 1, name);
373
444
  if (runner.nested !== null)
374
- return { at, nested: runner.nested, timeoutSeconds };
445
+ return { at, nested: runner.nested, timeoutSeconds, program: null };
375
446
  at += 1 + runner.count;
376
447
  continue;
377
448
  }
@@ -380,19 +451,35 @@ function commandStart(words) {
380
451
  const subcommand = unquoted(words[at + 1] ?? '');
381
452
  const runner = packageRunnerOptions(words, at + 2, `${name} ${subcommand}`);
382
453
  if (runner.nested !== null)
383
- return { at, nested: runner.nested, timeoutSeconds };
454
+ return { at, nested: runner.nested, timeoutSeconds, program: null };
384
455
  at += 2 + runner.count;
385
456
  continue;
386
457
  }
458
+ if (name === 'yarn' && unquoted(words[at + 1] ?? '') === 'playwright') {
459
+ // Yarn classic runs the binary named after it (`yarn playwright test`); Yarn 2+ spells the
460
+ // same thing `yarn exec`/`yarn dlx` ({@link PACKAGE_RUNNER_SUBCOMMANDS}). Only the runner's
461
+ // own name is read through it — `yarn test` is the project's own script, not a binary.
462
+ at += 1;
463
+ continue;
464
+ }
465
+ if (NODE.has(name)) {
466
+ // `node <path>/playwright test` is a Playwright invocation; `node probe.mjs` runs the
467
+ // project's own script and stays a `node` statement (`program: null`).
468
+ const script = nodeScriptIndex(words, at + 1);
469
+ const runner = script === null ? null : nodeScriptRunner(words[script] ?? '');
470
+ return runner === null
471
+ ? { at, nested: null, timeoutSeconds, program: null }
472
+ : { at: script ?? at, nested: null, timeoutSeconds, program: runner };
473
+ }
387
474
  if (!LAUNCHERS.has(name))
388
- return { at, nested: null, timeoutSeconds };
475
+ return { at, nested: null, timeoutSeconds, program: null };
389
476
  at += 1 + flagWords(words, at + 1, name);
390
477
  if (name === 'timeout' && DURATION.test(words[at] ?? '')) {
391
478
  timeoutSeconds = durationSeconds(words[at] ?? '');
392
479
  at += 1;
393
480
  }
394
481
  }
395
- return { at, nested: null, timeoutSeconds };
482
+ return { at, nested: null, timeoutSeconds, program: null };
396
483
  }
397
484
  /**
398
485
  * A command with every heredoc BODY blanked to spaces, so its length and every offset in it are
@@ -993,7 +1080,9 @@ function locatedSegmentsOf(command, depth) {
993
1080
  assignmentOffset: part.start + leading.end,
994
1081
  programOffset: words[start.at] === undefined ? null : part.start + (wordOffset(part.text, start.at) ?? 0),
995
1082
  };
996
- const program = words[start.at] === undefined ? '' : programName(words[start.at] ?? '');
1083
+ // The runner the word RUNS names, when that is not the word's own name (`node <path>/playwright`
1084
+ // is a `playwright` invocation, xr-molecule-flash-m179).
1085
+ const program = words[start.at] === undefined ? '' : (start.program ?? programName(words[start.at] ?? ''));
997
1086
  const args = words.slice(start.at + 1);
998
1087
  const redirectTarget = redirectTargetOf(part.text, part.depths, program, args);
999
1088
  if (start.nested !== null) {
@@ -19,9 +19,13 @@
19
19
  import { type CommandSegment } from './command-position.js';
20
20
  /** A cutting stage that follows the runner, and the command with the whole pipe removed. */
21
21
  export interface RunnerOutputFilter {
22
- /** The stage exactly as the runner's command wrote it — `tail -6`. */
22
+ /**
23
+ * The stage exactly as the runner's command wrote it — `tail -6`. When several cutting filters
24
+ * follow the runner, this is the whole trailing chain as written, pipes included:
25
+ * `grep -E "Failure|actual" | head -40`.
26
+ */
23
27
  stage: string;
24
- /** The pipe as a sentence names it — `| tail -6`. */
28
+ /** The pipe as a sentence names it — `| tail -6`, or the whole chain `| grep … | head -40`. */
25
29
  pipe: string;
26
30
  /**
27
31
  * The command with the pipe and the stage removed — `hurl --variable uid=… --test /tmp/t.hurl
@@ -33,10 +37,13 @@ export interface RunnerOutputFilter {
33
37
  /**
34
38
  * The output filter that follows the runner in `command`, or null when there is none.
35
39
  *
36
- * Only a stage of the runner's OWN pipeline counts (its separator is `|`), and only when it is the
37
- * LAST stage of that pipeline: a `| grep … | wc -l` chain would leave a dangling pipe if the
38
- * middle stage were cut out, so a chain is not answered here at all. A filter before the runner
39
- * (`cat run.log | tail -20 && hurl …`) reads something else's output and is not this.
40
+ * Only stages of the runner's OWN pipeline count (their separator is `|`), and they are read as
41
+ * one trailing CHAIN: `| grep … | head -40` is two cuts, and removing only the first would leave
42
+ * the run piped into the second. Every stage after the runner must be a cutting filter and the
43
+ * chain must run to the pipeline's end — a pipeline that continues into `wc -l` is not answered
44
+ * here, because removing the cutting stages alone would still leave the failure hidden. A filter
45
+ * before the runner (`cat run.log | tail -20 && hurl …`) reads something else's output and is not
46
+ * this.
40
47
  *
41
48
  * @param command - The command as the executor sent it.
42
49
  * @param runs - Whether a segment is the runner this run is about (the caller knows its own kind:
@@ -1 +1 @@
1
- {"version":3,"file":"output-filter.d.ts","sourceRoot":"","sources":["../src/output-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,uBAAuB,CAAA;AASnF,4FAA4F;AAC5F,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,EAC1C,UAAU,SAAI,GACb,kBAAkB,GAAG,IAAI,CAsB3B"}
1
+ {"version":3,"file":"output-filter.d.ts","sourceRoot":"","sources":["../src/output-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,uBAAuB,CAAA;AAS9B,4FAA4F;AAC5F,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,EAC1C,UAAU,SAAI,GACb,kBAAkB,GAAG,IAAI,CAmC3B"}
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * @module
18
18
  */
19
- import { locatedCommandSegments } from './command-position.js';
19
+ import { locatedCommandSegments, } from './command-position.js';
20
20
  /**
21
21
  * The programs whose job is to re-shape what an earlier stage printed and that can DROP lines —
22
22
  * the reader's own cut, never part of the run. `tee`, `sort` and `wc` are output plumbing too, but
@@ -26,10 +26,13 @@ const CUTTING_FILTERS = new Set(['awk', 'cut', 'grep', 'head', 'sed', 'tail']);
26
26
  /**
27
27
  * The output filter that follows the runner in `command`, or null when there is none.
28
28
  *
29
- * Only a stage of the runner's OWN pipeline counts (its separator is `|`), and only when it is the
30
- * LAST stage of that pipeline: a `| grep … | wc -l` chain would leave a dangling pipe if the
31
- * middle stage were cut out, so a chain is not answered here at all. A filter before the runner
32
- * (`cat run.log | tail -20 && hurl …`) reads something else's output and is not this.
29
+ * Only stages of the runner's OWN pipeline count (their separator is `|`), and they are read as
30
+ * one trailing CHAIN: `| grep … | head -40` is two cuts, and removing only the first would leave
31
+ * the run piped into the second. Every stage after the runner must be a cutting filter and the
32
+ * chain must run to the pipeline's end — a pipeline that continues into `wc -l` is not answered
33
+ * here, because removing the cutting stages alone would still leave the failure hidden. A filter
34
+ * before the runner (`cat run.log | tail -20 && hurl …`) reads something else's output and is not
35
+ * this.
33
36
  *
34
37
  * @param command - The command as the executor sent it.
35
38
  * @param runs - Whether a segment is the runner this run is about (the caller knows its own kind:
@@ -51,23 +54,39 @@ export function outputFilterAfterRunner(command, runs, occurrence = 1) {
51
54
  runners.push(index);
52
55
  });
53
56
  const runnerAt = runners[occurrence - 1];
54
- if (runnerAt === undefined || runnerAt + 1 >= entries.length)
57
+ if (runnerAt === undefined)
55
58
  return null;
56
59
  const runner = entries[runnerAt];
57
- const filter = entries[runnerAt + 1];
58
- // The stage must be fed by the runner's own pipe, and nothing may be piped out of the filter:
59
- // those are the two positions where cutting it out leaves a command that still runs the run.
60
- if (!runner || !filter || filter.separator.trim() !== '|')
60
+ if (!runner || runner.end === null)
61
61
  return null;
62
- if (!filter.segment || !CUTTING_FILTERS.has(filter.segment.program))
62
+ // The chain is every stage fed by the runner's own pipe, until the pipeline ends. Each one has
63
+ // to be a cutting filter: a stage that does not drop lines ends the chain, and the chain then
64
+ // stops before the pipeline's end — which is not answerable (`| grep … | wc -l` would still
65
+ // hide the assertion in a count).
66
+ const chain = [];
67
+ for (let index = runnerAt + 1; index < entries.length; index++) {
68
+ const entry = entries[index];
69
+ if (!entry || entry.separator.trim() !== '|')
70
+ break;
71
+ if (!entry.segment || !CUTTING_FILTERS.has(entry.segment.program))
72
+ break;
73
+ chain.push(entry);
74
+ if (entry.terminator.trim() !== '|')
75
+ break;
76
+ }
77
+ const first = chain[0];
78
+ const last = chain[chain.length - 1];
79
+ if (!first || !last)
63
80
  return null;
64
- if (filter.terminator.trim() === '|')
81
+ // A pipeline that continues past the chain means a non-cutting stage sits after it.
82
+ if (last.terminator.trim() === '|')
65
83
  return null;
66
- if (runner.end === null || filter.start === null || filter.end === null)
84
+ if (first.start === null || last.end === null)
67
85
  return null;
86
+ const stage = command.slice(first.start, last.end);
68
87
  return {
69
- stage: command.slice(filter.start, filter.end),
70
- pipe: `| ${command.slice(filter.start, filter.end)}`,
71
- command: `${command.slice(0, runner.end)}${command.slice(filter.end)}`,
88
+ stage,
89
+ pipe: `| ${stage}`,
90
+ command: `${command.slice(0, runner.end)}${command.slice(last.end)}`,
72
91
  };
73
92
  }