@molecule/api-ai-tools 1.8.3 → 1.9.1

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-17T05:02:46.912Z
6
+ Generated: 2026-10-17T07:58:25.504Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -197,6 +197,23 @@ interface CutRun {
197
197
  }
198
198
  ```
199
199
 
200
+ #### `RunFailureClass`
201
+
202
+ One failure a runner printed, classified by the layer that failed.
203
+
204
+ ```typescript
205
+ interface RunFailureClass {
206
+ /** The layer that failed. */
207
+ kind: RunFailureKind
208
+ /** For a `connection` failure, what the transport did. */
209
+ connection?: ConnectionFailureKind
210
+ /** How long the request waited before the transport gave up, in seconds, when printed. */
211
+ seconds?: number
212
+ /** The `file:line` the runner printed for the failure, when it printed one. */
213
+ at?: string
214
+ }
215
+ ```
216
+
200
217
  #### `NormalizableSchema`
201
218
 
202
219
  The JSON-Schema-ish shape every tool in this package declares.
@@ -313,8 +330,43 @@ interface TraceRequest {
313
330
  method: string
314
331
  /** The request's path (`/api/articles/x/favorite`), or the whole URL when it is cross-origin. */
315
332
  path: string
316
- /** The response status, or 0 when the request never got a response. */
333
+ /**
334
+ * The response status as the trace recorded it. Playwright writes `-1` when a request was never
335
+ * answered (its HAR entry starts at `-1` and only a response event overwrites it); `0` is the
336
+ * same absence when the snapshot carried no response at all. Neither is a server failure.
337
+ */
317
338
  status: number
339
+ /**
340
+ * Playwright's own error text for a request that failed WITHOUT a response
341
+ * (`net::ERR_CONNECTION_REFUSED`) — a real failure. A request that got no response and recorded
342
+ * no text was aborted or still in flight when the test ended (the page closed under it), so it
343
+ * says nothing about the server (xr-molecule-flash-m112 +30:45).
344
+ */
345
+ failure?: string
346
+ /**
347
+ * What the page SENT, for a 4xx answered by the app's own API — bounded (see
348
+ * {@link MAX_TRACED_BODY_CHARS}). A body wrapped as `{"data":{…}}` is visible here in one line:
349
+ * app-http's verbs take the body as their second argument, so a helper that passes
350
+ * `{ data: body }` there ships the wrapper (xr-molecule-flash-m111 +11:36).
351
+ */
352
+ sent?: string
353
+ /** What came back for that 4xx, bounded the same way — the API's own error body. */
354
+ got?: string
355
+ }
356
+ ```
357
+
358
+ #### `TraceAction`
359
+
360
+ One action the traced page performed, with the trace's own timing for it.
361
+
362
+ ```typescript
363
+ interface TraceAction {
364
+ /** The Playwright API the entry names: `Frame.goto`, `Locator.fill`, `Locator.click`. */
365
+ api: string
366
+ /** When the action started, on the trace's own monotonic clock (ms). */
367
+ start: number
368
+ /** When it finished, or null when the run ended before it did. */
369
+ end: number | null
318
370
  }
319
371
  ```
320
372
 
@@ -332,6 +384,12 @@ interface TraceArtifact {
332
384
  requests: TraceRequest[]
333
385
  /** The console errors and page errors the page raised. */
334
386
  console: string[]
387
+ /**
388
+ * The page's own actions in the order the trace recorded them, bounded — what a test that ran
389
+ * out of a per-test budget spends its time on. A test that repeats one action prints it once per
390
+ * repeat, so the gap between two starts is one repeat's measured cost.
391
+ */
392
+ actions: TraceAction[]
335
393
  }
336
394
  ```
337
395
 
@@ -685,6 +743,20 @@ interface FailureCause {
685
743
  * for a duration group, whose blocks carried no bodies.
686
744
  */
687
745
  evidence: string[]
746
+ /**
747
+ * The suite's own per-test budget in ms, when the cause is the test-budget kill
748
+ * (`Test timeout of 15000ms exceeded`). The signature normalizes the number away
749
+ * ({@link normalizedSignature}) so equal causes group, and that number is the one fact that
750
+ * predicts a test spending its budget on a repeated action (xr-molecule-flash-m110 +20:44).
751
+ */
752
+ budgetMs?: number
753
+ /**
754
+ * The layer the failure came from ({@link classifyRunFailure}). A `connection` cause never
755
+ * reached the app, so it is NOT a cause the app's code can fix: the note says to re-run it
756
+ * once and read the app's log on a repeat, instead of sending a fix at a handler
757
+ * (xr-molecule-flash-m110 +18:42 rewrote a CORS middleware for a 20 s connection timeout).
758
+ */
759
+ kind: RunFailureKind
688
760
  }
689
761
  ```
690
762
 
@@ -716,6 +788,23 @@ type CutBy =
716
788
  | 'process-stop'
717
789
  ```
718
790
 
791
+ #### `RunFailureKind`
792
+
793
+ The layer a failure came from: the transport (no answer at all), the response's status, or an
794
+ assertion over a response that arrived.
795
+
796
+ ```typescript
797
+ type RunFailureKind = 'connection' | 'status' | 'assertion'
798
+ ```
799
+
800
+ #### `ConnectionFailureKind`
801
+
802
+ What the transport did when a request never got an answer.
803
+
804
+ ```typescript
805
+ type ConnectionFailureKind = 'timeout' | 'refused' | 'reset' | 'dns' | 'tls' | 'empty'
806
+ ```
807
+
719
808
  #### `DiscoveredSkill` _(deprecated)_
720
809
 
721
810
  Re-export SkillEntry as DiscoveredSkill for backwards compatibility.
@@ -1001,6 +1090,36 @@ function cutRunSentence(run: CutRun): string
1001
1090
 
1002
1091
  **Returns:** The sentence.
1003
1092
 
1093
+ #### `classifyRunFailure(text)`
1094
+
1095
+ Classify ONE failure's text — a Hurl `error:` block, or a Playwright failure message/body.
1096
+
1097
+ The transport is read first: a block that carries a connection error AND an assertion is a
1098
+ request whose assertion never ran, so it is a connection failure. Only a block that carries no
1099
+ failure signature at all answers null — unknown is never a connection failure, because a
1100
+ misread transport hides a real app failure behind "just re-run it".
1101
+
1102
+ ```typescript
1103
+ function classifyRunFailure(text: string): RunFailureClass | null
1104
+ ```
1105
+
1106
+ - `text` — One failure's text, as the runner printed it.
1107
+
1108
+ **Returns:** The classification, or null when the text carries no failure this reads.
1109
+
1110
+ #### `allConnectionFailures(failures)`
1111
+
1112
+ Whether every one of the classified failures is a connection failure — the only shape in which
1113
+ no response the app produced was measured.
1114
+
1115
+ ```typescript
1116
+ function allConnectionFailures(failures: readonly (RunFailureClass | null)[]): boolean
1117
+ ```
1118
+
1119
+ - `failures` — The run's classifications, in printed order.
1120
+
1121
+ **Returns:** True when there is at least one and none of them is the app's.
1122
+
1004
1123
  #### `normalizeToolInput(schema, raw)`
1005
1124
 
1006
1125
  Move known aliases onto the declared parameter names and report what is still
@@ -1160,24 +1279,68 @@ function suiteCommandTarget(command: string): string
1160
1279
 
1161
1280
  **Returns:** The target key.
1162
1281
 
1163
- #### `scopedRerunCommand(command, locations, excluded)`
1282
+ #### `namesSuiteConfig(command)`
1283
+
1284
+ Whether a Playwright statement names its own config — the reading {@link withSuiteConfig} and
1285
+ {@link scopedRerunCommand} share, so neither adds a second `--config` to a command that has one.
1286
+
1287
+ ```typescript
1288
+ function namesSuiteConfig(command: string): boolean
1289
+ ```
1290
+
1291
+ - `command` — A command, or the Playwright statement inside it.
1292
+
1293
+ **Returns:** True when the statement loads a config of its own.
1294
+
1295
+ #### `withSuiteConfig(command, config)`
1296
+
1297
+ The command with a `--config` added to the Playwright invocation it starts — or unchanged when
1298
+ it already names one, or starts no Playwright run.
1299
+
1300
+ The flag rides the same statement the runner does: a `cd`, an env prefix and a `timeout` before
1301
+ it are left exactly as they were, and so is everything chained after the run (`| tail -50`).
1302
+
1303
+ xr-molecule-flash-m113 (+19:09): the model's fix-round re-run dropped the
1304
+ `--config playwright.conduit.config.ts` both earlier runs had carried, so Playwright started with
1305
+ NO base URL and all 10 failures were `page.goto: Cannot navigate to invalid URL` — a failure list
1306
+ that says nothing about the app. The platform composes commands back to the executor too (the
1307
+ narrowed re-run of a finished run's failures), and a command it hands back has to carry the
1308
+ config the run was measured under, never the shape that caused that finding.
1309
+
1310
+ ```typescript
1311
+ function withSuiteConfig(command: string, config: string | null | undefined): string
1312
+ ```
1313
+
1314
+ - `command` — The command as the executor sent it.
1315
+ - `config` — The config the run loads, when the caller knows it.
1316
+
1317
+ **Returns:** The command with the flag, or the command unchanged.
1318
+
1319
+ #### `scopedRerunCommand(command, locations, excluded, config)`
1164
1320
 
1165
1321
  The whole-suite command made scoped to the tests it should re-run instead: the same
1166
1322
  invocation — same directory, same settings — with the named `file:line`s in place of the whole
1167
1323
  suite, `--last-failed` dropped (it re-runs the tests that never ran too, X1 flash-m60) and
1168
1324
  `--retries=0` set (a re-run of a known failure does not need to pay for retries).
1169
1325
 
1326
+ `config` is the config the run was measured under ({@link withSuiteConfig}): the narrowed re-run
1327
+ carries it whenever the command itself does not, so a run that dropped `--config` cannot be
1328
+ handed back a command with the same defect (xr-molecule-flash-m113: every failing test was
1329
+ `Cannot navigate to invalid URL` because the re-run loaded no base URL).
1330
+
1170
1331
  ```typescript
1171
1332
  function scopedRerunCommand(
1172
1333
  command: string,
1173
1334
  locations: readonly string[],
1174
1335
  excluded?: readonly string[],
1336
+ config?: string | null,
1175
1337
  ): string
1176
1338
  ```
1177
1339
 
1178
1340
  - `command` — The whole-suite run's command.
1179
1341
  - `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.
1180
1342
  - `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.
1343
+ - `config` — The config the run loaded, when the caller knows it; added when the command does not name one.
1181
1344
 
1182
1345
  **Returns:** The scoped command, or the command unchanged when it runs no Playwright suite.
1183
1346
 
@@ -1395,10 +1558,17 @@ on `unzip` would make the read fail on every sandbox that has none. The zip is r
1395
1558
  central directory and `zlib.inflateRawSync` — about forty lines, and the only thing that has to
1396
1559
  be true of a `trace.zip` is that it IS one.
1397
1560
 
1398
- A request is kept over a static asset when it failed (`status >= 400`) or never answered
1399
- (`status === 0`), and over anything else when it is the page talking to the app
1400
- (`fetch`/`xhr`/`document`): a failing test's trace holds the whole page load, and the answer is
1401
- always at the end of it.
1561
+ A request is kept over a static asset when it failed — a `status >= 400`, or no response at all
1562
+ together with Playwright's own error text (`-1` plus `_failureText`, a refused connection) — and
1563
+ over anything else when it is the page talking to the app (`fetch`/`xhr`/`document`): a failing
1564
+ test's trace holds the whole page load. A request with no response and NO error text was aborted
1565
+ or still in flight when the test ended; it is kept too, but as what it is, not as a failure.
1566
+
1567
+ A request answered 4xx by the app's OWN API also keeps what the page SENT and the first of what
1568
+ came back: Playwright stores either inline (`text`) or in a resource entry (`_file`), and both
1569
+ are read. That is where a wrapped body shows: xr-molecule-flash-m111's helper passed
1570
+
1571
+ _(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/trace-artifacts.d.ts`.)_
1402
1572
 
1403
1573
  ```typescript
1404
1574
  function traceArtifactsScript(): string
@@ -1421,7 +1591,7 @@ function parseTraceArtifacts(stdout: string): TraceArtifact[]
1421
1591
 
1422
1592
  **Returns:** One entry per trace it printed.
1423
1593
 
1424
- #### `failureTraceNote(items, target)`
1594
+ #### `failureTraceNote(items, target, earlierFailures)`
1425
1595
 
1426
1596
  The note a failed suite run's result carries: what each failing test's page actually sent, and
1427
1597
  what came back.
@@ -1432,14 +1602,34 @@ with them: read the handler the route reaches, not the selector. It is BOUNDED t
1432
1602
  {@link MAX_TRACE_NOTE_TESTS} tests with {@link MAX_TRACE_NOTE_REQUESTS} requests each, because it
1433
1603
  rides in a result the model pays for.
1434
1604
 
1605
+ The three kinds are kept apart, because they mean different things. A FAILED request (4xx/5xx, or
1606
+ a network error with Playwright's own text) is a cause. A request with NO RESPONSE RECORDED —
1607
+ Playwright's `-1`, no error text — was still in flight when the test ended (the page closed under
1608
+ it); listing it as a failure sent a run hunting an API that never answered anything wrong
1609
+ (xr-molecule-flash-m112 +30:45). The rest are the page's last answered requests.
1610
+
1611
+ A 4xx request carries what the page SENT beside its status, so a wrapped body is visible in one
1612
+ line rather than inferred from the status; when one of those bodies is an object whose only key
1613
+ is `data`, the note names the call shape that produces it (xr-molecule-flash-m111 +11:36).
1614
+
1615
+ WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
1616
+ needs a recorded response, a real failure or a console error ({@link traceRowEvidence}), rows are
1617
+
1618
+ _(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/trace-artifacts.d.ts`.)_
1619
+
1435
1620
  ```typescript
1436
- function failureTraceNote(items: readonly TraceArtifact[], target: string): string | null
1621
+ function failureTraceNote(
1622
+ items: readonly TraceArtifact[],
1623
+ target: string,
1624
+ earlierFailures?: readonly string[],
1625
+ ): string | null
1437
1626
  ```
1438
1627
 
1439
1628
  - `items` — The traces {@link parseTraceArtifacts} read.
1440
1629
  - `target` — Where the run went — the deployed host, as the note names it.
1630
+ - `earlierFailures` — The failing `file:line`s earlier walks of this suite left, so a repeat is ranked above a one-off. Empty when the caller knows of no earlier walk.
1441
1631
 
1442
- **Returns:** The note, or null when no trace held a request or a console error.
1632
+ **Returns:** The note, or null when no trace held a recorded response, a real failure or a console error.
1443
1633
 
1444
1634
  #### `suiteRecordsTraces(configText)`
1445
1635
 
@@ -2382,17 +2572,21 @@ the caller knows how the run was started, the note also hands over the narrowed
2382
2572
  exactly those tests ({@link rerunOfRunningSuite}) instead of leaving the executor to re-run the
2383
2573
  whole suite.
2384
2574
 
2575
+ A run whose failures are ALL {@link NO_BASE_URL_ERROR} is not a set of causes at all: it is one
2576
+ configuration failure, said in one line ({@link noBaseUrlConfigNote}) rather than a list that
2577
+ reads as ten defects in the app.
2578
+
2385
2579
  ```typescript
2386
2580
  function failureCausesNote(
2387
2581
  log: string,
2388
2582
  excluded?: readonly string[],
2389
- run?: { command: string; cwd?: string; log?: string },
2583
+ run?: { command: string; cwd?: string; log?: string; config?: string | null },
2390
2584
  ): string | null
2391
2585
  ```
2392
2586
 
2393
2587
  - `log` — The finished run's output.
2394
2588
  - `excluded` — The test titles the request carves out, if the caller knows them.
2395
- - `run` — How the run was started, when the caller knows it: the narrowed re-run is built from `command`/`cwd`, 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.
2589
+ - `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.
2396
2590
 
2397
2591
  **Returns:** The note, or null.
2398
2592
 
@@ -2411,13 +2605,13 @@ the same app and database, and that is exactly what is refused while the run wal
2411
2605
 
2412
2606
  ```typescript
2413
2607
  function rerunOfRunningSuite(
2414
- run: { command: string; cwd?: string },
2608
+ run: { command: string; cwd?: string; config?: string | null },
2415
2609
  locations: readonly string[],
2416
2610
  excluded?: readonly string[],
2417
2611
  ): string | null
2418
2612
  ```
2419
2613
 
2420
- - `run` — The run in flight: the command it was started with, as it was run, and the directory that command ran in.
2614
+ - `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.
2421
2615
  - `locations` — The failing `file:line`s to re-run, as the run's log printed them.
2422
2616
  - `excluded` — The test titles the request carves out: the re-run inverts them out (`--grep-invert`) so it cannot report them again.
2423
2617
 
@@ -2622,6 +2816,22 @@ How many console errors one test's trace contributes.
2622
2816
  const MAX_TRACED_CONSOLE: 4
2623
2817
  ```
2624
2818
 
2819
+ #### `MAX_TRACED_ACTIONS`
2820
+
2821
+ How many of the page's own actions one test's trace contributes, the LAST ones.
2822
+
2823
+ ```typescript
2824
+ const MAX_TRACED_ACTIONS: 40
2825
+ ```
2826
+
2827
+ #### `MAX_TRACED_BODY_CHARS`
2828
+
2829
+ How much of a 4xx request's sent body (or its response body) the trace read keeps.
2830
+
2831
+ ```typescript
2832
+ const MAX_TRACED_BODY_CHARS: 300
2833
+ ```
2834
+
2625
2835
  #### `MAX_TRACE_NOTE_TESTS`
2626
2836
 
2627
2837
  How many tests the note in a run's result names.
@@ -2646,6 +2856,22 @@ How long a URL the note prints, before the middle is cut.
2646
2856
  const TRACE_NOTE_URL_CHARS: 96
2647
2857
  ```
2648
2858
 
2859
+ #### `TRACE_NOTE_SENT_CHARS`
2860
+
2861
+ How much of the sent body the note prints — enough to see a `{"data":…}` wrapper.
2862
+
2863
+ ```typescript
2864
+ const TRACE_NOTE_SENT_CHARS: 160
2865
+ ```
2866
+
2867
+ #### `TRACE_NOTE_GOT_CHARS`
2868
+
2869
+ How much of the response body the note prints — the API's first error line.
2870
+
2871
+ ```typescript
2872
+ const TRACE_NOTE_GOT_CHARS: 120
2873
+ ```
2874
+
2649
2875
  #### `DIAGNOSIS_TRACE_FLAG`
2650
2876
 
2651
2877
  The `--trace` flag this platform adds, and the only value it adds it with.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * What a failing request actually hit, read off the runner's own output.
3
+ *
4
+ * Three layers fail in three different ways, and only one of them is the app's code:
5
+ *
6
+ * - **connection** — the request never got an answer at all: the transport timed out, was
7
+ * refused, reset, or could not resolve the name or negotiate TLS. Nothing about a handler,
8
+ * a header or CORS was measured, so a re-run is the only thing that tells a cold start (a
9
+ * machine deployed minutes ago whose boot finishes after the URL is live) from a real outage.
10
+ * - **status** — a response arrived and the HTTP status disagreed with what the suite asserts.
11
+ * - **assertion** — a response arrived and an assertion over its body failed.
12
+ *
13
+ * X0 xr-molecule-flash-m110 (+18:42 → +20:09) is the case this exists for: one Hurl file on the
14
+ * freshly deployed URL failed with `(28) Operation timed out … 0 bytes received` while the other
15
+ * twelve passed and 138 requests executed, and every note on the result read it as an assertion.
16
+ * The executor rewrote a CORS middleware, bisected a second server and latency-compared the two,
17
+ * then re-ran the same file — which passed in 3 s. A single re-run was the whole fix, and the one
18
+ * fact that would have said so is the failure's LAYER.
19
+ *
20
+ * ONE classifier, used by the causes note ({@link ./utilities.ts} `failureCausesNote`) and by the
21
+ * platform's own proof result (`molecule-dev` `suite-proof.ts`), so the two can never disagree
22
+ * about whether a failure was the app's to fix.
23
+ *
24
+ * @module
25
+ */
26
+ /**
27
+ * The layer a failure came from: the transport (no answer at all), the response's status, or an
28
+ * assertion over a response that arrived.
29
+ */
30
+ export type RunFailureKind = 'connection' | 'status' | 'assertion';
31
+ /** What the transport did when a request never got an answer. */
32
+ export type ConnectionFailureKind = 'timeout' | 'refused' | 'reset' | 'dns' | 'tls' | 'empty';
33
+ /** One failure a runner printed, classified by the layer that failed. */
34
+ export interface RunFailureClass {
35
+ /** The layer that failed. */
36
+ kind: RunFailureKind;
37
+ /** For a `connection` failure, what the transport did. */
38
+ connection?: ConnectionFailureKind;
39
+ /** How long the request waited before the transport gave up, in seconds, when printed. */
40
+ seconds?: number;
41
+ /** The `file:line` the runner printed for the failure, when it printed one. */
42
+ at?: string;
43
+ }
44
+ /**
45
+ * Classify ONE failure's text — a Hurl `error:` block, or a Playwright failure message/body.
46
+ *
47
+ * The transport is read first: a block that carries a connection error AND an assertion is a
48
+ * request whose assertion never ran, so it is a connection failure. Only a block that carries no
49
+ * failure signature at all answers null — unknown is never a connection failure, because a
50
+ * misread transport hides a real app failure behind "just re-run it".
51
+ *
52
+ * @param text - One failure's text, as the runner printed it.
53
+ * @returns The classification, or null when the text carries no failure this reads.
54
+ */
55
+ export declare function classifyRunFailure(text: string): RunFailureClass | null;
56
+ /**
57
+ * Whether every one of the classified failures is a connection failure — the only shape in which
58
+ * no response the app produced was measured.
59
+ *
60
+ * @param failures - The run's classifications, in printed order.
61
+ * @returns True when there is at least one and none of them is the app's.
62
+ */
63
+ export declare function allConnectionFailures(failures: ReadonlyArray<RunFailureClass | null>): failures is ReadonlyArray<RunFailureClass & {
64
+ kind: 'connection';
65
+ }>;
66
+ //# sourceMappingURL=failure-kind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-kind.d.ts","sourceRoot":"","sources":["../src/failure-kind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAA;AAElE,iEAAiE;AACjE,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,CAAA;AAE7F,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,6BAA6B;IAC7B,IAAI,EAAE,cAAc,CAAA;IACpB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,qBAAqB,CAAA;IAClC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+EAA+E;IAC/E,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AA0ED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAoBvE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,aAAa,CAAC,eAAe,GAAG,IAAI,CAAC,GAC9C,QAAQ,IAAI,aAAa,CAAC,eAAe,GAAG;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,CAAC,CAErE"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * What a failing request actually hit, read off the runner's own output.
3
+ *
4
+ * Three layers fail in three different ways, and only one of them is the app's code:
5
+ *
6
+ * - **connection** — the request never got an answer at all: the transport timed out, was
7
+ * refused, reset, or could not resolve the name or negotiate TLS. Nothing about a handler,
8
+ * a header or CORS was measured, so a re-run is the only thing that tells a cold start (a
9
+ * machine deployed minutes ago whose boot finishes after the URL is live) from a real outage.
10
+ * - **status** — a response arrived and the HTTP status disagreed with what the suite asserts.
11
+ * - **assertion** — a response arrived and an assertion over its body failed.
12
+ *
13
+ * X0 xr-molecule-flash-m110 (+18:42 → +20:09) is the case this exists for: one Hurl file on the
14
+ * freshly deployed URL failed with `(28) Operation timed out … 0 bytes received` while the other
15
+ * twelve passed and 138 requests executed, and every note on the result read it as an assertion.
16
+ * The executor rewrote a CORS middleware, bisected a second server and latency-compared the two,
17
+ * then re-ran the same file — which passed in 3 s. A single re-run was the whole fix, and the one
18
+ * fact that would have said so is the failure's LAYER.
19
+ *
20
+ * ONE classifier, used by the causes note ({@link ./utilities.ts} `failureCausesNote`) and by the
21
+ * platform's own proof result (`molecule-dev` `suite-proof.ts`), so the two can never disagree
22
+ * about whether a failure was the app's to fix.
23
+ *
24
+ * @module
25
+ */
26
+ /**
27
+ * Transport-level failures, most specific first: the patterns curl/Hurl and Chromium/Node print
28
+ * for a request that never got an answer. A message can carry more than one (`page.goto: Timeout
29
+ * 30000ms exceeded … net::ERR_CONNECTION_REFUSED`), and the more specific reading wins.
30
+ */
31
+ const CONNECTION_FAILURES = [
32
+ // Hurl/curl's numbered errors (curl's exit codes, printed as `(28) Operation timed out …`).
33
+ ['empty', /\(\s*52\)\s*Empty reply from server/],
34
+ ['timeout', /\(\s*28\)\s*Operation timed out|Operation timed out after \d+/],
35
+ ['dns', /\(\s*6\)\s*Could not resolve host|Could not resolve host/],
36
+ [
37
+ 'tls',
38
+ /\(\s*(?:35|51|58|59|60|77|83|90|91)\)\s|SSL connect error|SSL certificate problem|self[- ]signed certificate|UNABLE_TO_VERIFY_LEAF_SIGNATURE|CERT_HAS_EXPIRED/i,
39
+ ],
40
+ [
41
+ 'refused',
42
+ /\(\s*7\)\s*(?:Failed to connect|Couldn't connect)|Failed to connect to\s|Couldn't connect to server/,
43
+ ],
44
+ ['reset', /\(\s*5[56]\)\s*(?:Recv|Send) failure|Connection reset by peer|socket hang up/],
45
+ // The browser's own net errors, and the Node errors underneath them.
46
+ ['timeout', /net::ERR_(?:CONNECTION_)?TIMED_OUT|\bETIMEDOUT\b/],
47
+ ['dns', /net::ERR_NAME_(?:NOT_RESOLVED|RESOLUTION_FAILED)|\bENOTFOUND\b|\bEAI_AGAIN\b/],
48
+ ['refused', /net::ERR_CONNECTION_REFUSED|\bECONNREFUSED\b/],
49
+ ['empty', /net::ERR_EMPTY_RESPONSE/],
50
+ ['reset', /net::ERR_CONNECTION_(?:RESET|CLOSED|ABORTED)|\bECONNRESET\b/],
51
+ ['tls', /net::ERR_(?:CERT|SSL|BAD_SSL)\w*/i],
52
+ ];
53
+ /** A status assertion: the response arrived and its code is what failed. */
54
+ const STATUS_FAILURE = /^\s*error:\s*Assert status code\b|actual value is <\d{3}>|\bHTTP(?:\/[\d.]+)?\s+[1-5]\d{2}\b|Expected[: ]+[1-5]\d{2}\b|\.status\(\)/m;
55
+ /** Any other assertion over a response that arrived. */
56
+ const ASSERTION_FAILURE = /^\s*error:\s*Assert\b|\bAssertionError\b|\bexpect\(|\bExpected[: ]|\bReceived[: ]|\bjsonpath\b|^\s*Error:/m;
57
+ /** Hurl's `--> file:line:col`, the location of the failing request or assertion. */
58
+ const HURL_AT = /-->\s+(\S+?):(\d+)(?::\d+)?\s*$/m;
59
+ /** A spec file and line, the location a Playwright block names. */
60
+ const SPEC_AT = /([\w./~-]+\.(?:spec|test)\.[cm]?[jt]sx?):(\d+)(?::\d+)?/;
61
+ /** How long a request waited, as curl prints it (`Operation timed out after 20002 milliseconds`). */
62
+ const WAITED_MS = /Operation timed out after (\d+) milliseconds/;
63
+ /** How long a request waited, as Playwright prints it (`Timeout 30000ms exceeded`). */
64
+ const WAITED_PLAYWRIGHT = /(?<!Test )\bTimeout (\d+)ms exceeded/;
65
+ /**
66
+ * The `file:line` a failure block names, when it names one.
67
+ *
68
+ * @param text - The failure block.
69
+ * @returns The location, or undefined when the block carries none.
70
+ */
71
+ function failureLocation(text) {
72
+ const hurl = HURL_AT.exec(text);
73
+ if (hurl)
74
+ return `${hurl[1]}:${hurl[2]}`;
75
+ const spec = SPEC_AT.exec(text);
76
+ return spec ? `${spec[1]}:${spec[2]}` : undefined;
77
+ }
78
+ /**
79
+ * How long the request waited before it gave up, in seconds, when the runner printed it.
80
+ *
81
+ * @param text - The failure block.
82
+ * @returns The seconds, rounded, or undefined.
83
+ */
84
+ function waitedSeconds(text) {
85
+ const ms = WAITED_MS.exec(text) ?? WAITED_PLAYWRIGHT.exec(text);
86
+ return ms ? Math.round(Number(ms[1]) / 1000) : undefined;
87
+ }
88
+ /**
89
+ * Classify ONE failure's text — a Hurl `error:` block, or a Playwright failure message/body.
90
+ *
91
+ * The transport is read first: a block that carries a connection error AND an assertion is a
92
+ * request whose assertion never ran, so it is a connection failure. Only a block that carries no
93
+ * failure signature at all answers null — unknown is never a connection failure, because a
94
+ * misread transport hides a real app failure behind "just re-run it".
95
+ *
96
+ * @param text - One failure's text, as the runner printed it.
97
+ * @returns The classification, or null when the text carries no failure this reads.
98
+ */
99
+ export function classifyRunFailure(text) {
100
+ const body = String(text ?? '');
101
+ if (body.trim().length === 0)
102
+ return null;
103
+ for (const [connection, pattern] of CONNECTION_FAILURES) {
104
+ if (!pattern.test(body))
105
+ continue;
106
+ const result = { kind: 'connection', connection };
107
+ const at = failureLocation(body);
108
+ if (at !== undefined)
109
+ result.at = at;
110
+ const seconds = waitedSeconds(body);
111
+ if (seconds !== undefined)
112
+ result.seconds = seconds;
113
+ return result;
114
+ }
115
+ const at = failureLocation(body);
116
+ const kind = STATUS_FAILURE.test(body)
117
+ ? 'status'
118
+ : ASSERTION_FAILURE.test(body)
119
+ ? 'assertion'
120
+ : null;
121
+ if (kind === null)
122
+ return null;
123
+ return at === undefined ? { kind } : { kind, at };
124
+ }
125
+ /**
126
+ * Whether every one of the classified failures is a connection failure — the only shape in which
127
+ * no response the app produced was measured.
128
+ *
129
+ * @param failures - The run's classifications, in printed order.
130
+ * @returns True when there is at least one and none of them is the app's.
131
+ */
132
+ export function allConnectionFailures(failures) {
133
+ return failures.length > 0 && failures.every((failure) => failure?.kind === 'connection');
134
+ }
package/dist/index.d.ts CHANGED
@@ -134,6 +134,7 @@ export * from './backends/sandbox.js';
134
134
  export * from './browser-guard.js';
135
135
  export * from './command-position.js';
136
136
  export * from './cut-run.js';
137
+ export * from './failure-kind.js';
137
138
  export * from './input-normalizer.js';
138
139
  export * from './no-test-run.js';
139
140
  export * from './schemas.js';
@@ -1 +1 @@
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,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,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,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -134,6 +134,7 @@ export * from './backends/sandbox.js';
134
134
  export * from './browser-guard.js';
135
135
  export * from './command-position.js';
136
136
  export * from './cut-run.js';
137
+ export * from './failure-kind.js';
137
138
  export * from './input-normalizer.js';
138
139
  export * from './no-test-run.js';
139
140
  export * from './schemas.js';