@molecule/api-ai-tools 1.10.10 → 1.11.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-20T14:43:37.900Z
6
+ Generated: 2026-10-21T08:55:11.822Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -105,6 +105,28 @@ interface CommandSegment {
105
105
  }
106
106
  ```
107
107
 
108
+ #### `ExecutedCommand`
109
+
110
+ One command a shell command actually executes, wherever the shell runs it.
111
+
112
+ ```typescript
113
+ interface ExecutedCommand {
114
+ /**
115
+ * The word in command position, its directory removed (`/usr/bin/printenv` → `printenv`) — or
116
+ * the launcher itself when it runs alone and nothing follows it (`x | env` → `env`).
117
+ */
118
+ program: string
119
+ /**
120
+ * Its arguments, as written with quotes kept — the statement's own words AFTER the program,
121
+ * including an input redirect's path (`< /proc/self/environ`), so a reader of secret files sees
122
+ * the file it reads.
123
+ */
124
+ args: readonly string[]
125
+ /** The program and its arguments as one line, what a pattern anchored at the program matches. */
126
+ text: string
127
+ }
128
+ ```
129
+
108
130
  #### `LocatedCommandSegment`
109
131
 
110
132
  One statement of a shell command, with where it sits and the variables it sets for itself.
@@ -705,6 +727,81 @@ interface TraceHiddenBox {
705
727
  }
706
728
  ```
707
729
 
730
+ #### `TraceCountMatch`
731
+
732
+ One element a failing locator count matched, read from the trace's own failure snapshot.
733
+
734
+ A count assertion reports only `Expected: 3 / Received: 4`; the element that made it 4 is not
735
+ in the error. Playwright's trace does hold it — either marked `__playwright_target__` in the
736
+ failure snapshot (`expect(locator).toHaveCount(n)`), or recoverable by replaying the recorded
737
+ selector against the snapshot's DOM in a browser (`locator.count()` then `expect(n).toBe(k)`) —
738
+ so each match is named by what it is and where it sits, never by a guess.
739
+
740
+ ```typescript
741
+ interface TraceCountMatch {
742
+ /** The element's tag, lowercased (`div`). */
743
+ tag: string
744
+ /** Its `class` attribute, bounded (`card-block`). */
745
+ classes?: string
746
+ /** Its own text, whitespace-collapsed and bounded. */
747
+ text?: string
748
+ /** Its ancestors, outermost first (`body > div.comment-form > div.card`), bounded. */
749
+ path?: string
750
+ /** True when a `<form>` is one of its ancestors — the m321 extra match's shape. */
751
+ inForm?: true
752
+ }
753
+ ```
754
+
755
+ #### `TraceCountMismatch`
756
+
757
+ A failing assertion that compared a LOCATOR COUNT (`expect(await locator.count()).toBe(n)` or
758
+ `expect(locator).toHaveCount(n)`), with the locator the trace recorded and, when the failure
759
+ snapshot can supply them, the elements it matched.
760
+
761
+ xr-molecule-flash-m321's `comments.spec.ts:86` sat on `Expected: 3 / Received: 4` for ~30
762
+ minutes and three whole suite walks: the helper counted `.card:not(.comment-form) .card-block`,
763
+ the app's comment form was a `.comment-form` wrapper around a plain `.card`, and the form's own
764
+ `.card-block` made the fourth. The trace had the selector (its `Frame.queryCount` entry) and the
765
+ four elements (its failure snapshot); the note printed neither. This reading prints both, and
766
+ fails closed — a selector outside the browser's own CSS support, a missing browser, or a replay
767
+ that disagrees with the recorded count names no elements rather than inventing them.
768
+
769
+ ```typescript
770
+ interface TraceCountMismatch {
771
+ /** The locator as the trace or the assertion printed it (`locator('.card:not(…')`). */
772
+ locator: string
773
+ /** The selector inside that locator, when the trace recorded one. */
774
+ selector?: string
775
+ /** The number the assertion expected. */
776
+ expected: number
777
+ /** The number the page matched. */
778
+ received: number
779
+ /**
780
+ * Where the locator came from: the assertion's own message, or the `locator.count()` entry the
781
+ * trace recorded just before the failing assertion.
782
+ */
783
+ source: 'assertion' | 'locator-count'
784
+ /** The matched elements, bounded to {@link MAX_COUNT_MATCHES} in document order. */
785
+ matched?: TraceCountMatch[]
786
+ /** How many elements matched in total, before the display bound. */
787
+ matchedTotal?: number
788
+ /**
789
+ * How the elements were read: Playwright's own `__playwright_target__` marks in the failure
790
+ * snapshot, or the recorded selector replayed against that snapshot in a browser.
791
+ */
792
+ matchedFrom?: 'snapshot-targets' | 'selector-replay'
793
+ /**
794
+ * Why no elements are named, when they are not. `unsupported-selector` is a selector the
795
+ * browser's `querySelectorAll` rejects (a `getByRole`/`getByText` engine, `>>` chaining);
796
+ * `no-snapshot` is a trace with no readable DOM snapshot at the failure; `count-mismatch` is a
797
+ * replay that disagreed with the recorded count, so the snapshot is not evidence for this
798
+ * assertion; `browser-unavailable` is a project whose Playwright module or recorded engine
799
+ * executable could not be resolved, so the selector could not be replayed at all.
800
+ */
801
+ unreadable?: 'unsupported-selector' | 'no-snapshot' | 'count-mismatch' | 'browser-unavailable'
802
+ }
803
+ ```
804
+
708
805
  #### `TraceArtifact`
709
806
 
710
807
  What one trace says: the test it belongs to, the requests it made, and its console lines.
@@ -801,6 +898,13 @@ interface TraceArtifact {
801
898
  * leaving the run to probe computed styles (xr-molecule-flash-m197).
802
899
  */
803
900
  hiddenBox?: TraceHiddenBox
901
+ /**
902
+ * A failing assertion that compared a locator count, with the locator the trace recorded and the
903
+ * elements its failure snapshot matched ({@link TraceCountMismatch}). The row prints them, so a
904
+ * count that is off by one names the extra element instead of leaving the run to probe for it
905
+ * (xr-molecule-flash-m321's 30-minute `Expected: 3 / Received: 4`).
906
+ */
907
+ countMismatch?: TraceCountMismatch
804
908
  /**
805
909
  * The method+path groups this test's page sent {@link TRACE_NOTE_LOOP_REPEATS} times or more,
806
910
  * most-repeated first — a render loop the note leads with (xr-molecule-flash-m215: 2,106
@@ -1588,6 +1692,32 @@ function commandSegments(command: string): CommandSegment[]
1588
1692
 
1589
1693
  **Returns:** One entry per statement or pipeline stage that runs something, in order.
1590
1694
 
1695
+ #### `executedCommands(command)`
1696
+
1697
+ Every command a shell command actually executes, at any depth — the statements themselves, the
1698
+ body of a `sh -c '<command>'`, and the body of every command substitution (`$(…)`, backticks).
1699
+
1700
+ This is the reading a policy that asks "does this command run X" needs, and it is deliberately
1701
+ not a text match. The command-position parser already knows what runs and what is only quoted
1702
+ data ({@link commandSegments}), so a pattern never reads a `|` inside a quoted grep alternation
1703
+ as a pipeline: `grep -E "USER CALLS|ENV|BUTTON" app.log` prints log lines, and the `ENV` in
1704
+ its pattern started nothing (X0 xr-molecule-flash-m331 blocked exactly that call as an
1705
+ environment dump). What the shell DOES run stays visible through the same walk: a bare
1706
+ launcher (`ls | env`), a shell string (`bash -c 'env'`), a substitution
1707
+ (`echo "$(printenv)"`) and a nested one (`sh -c 'x; env'`) are all commands here.
1708
+
1709
+ `commandSegments` answers what a pipeline stage runs for a caller matching a runner; this
1710
+ answers with the launcher left standing when it runs alone, since that is what the shell
1711
+ executes then.
1712
+
1713
+ ```typescript
1714
+ function executedCommands(command: string): ExecutedCommand[]
1715
+ ```
1716
+
1717
+ - `command` — The shell command.
1718
+
1719
+ **Returns:** Each command the shell executes, in the order the shell reaches them.
1720
+
1591
1721
  #### `locatedCommandSegments(command)`
1592
1722
 
1593
1723
  Every statement of a command, with its own assignments and where it sits — assignment-only
@@ -2671,6 +2801,37 @@ function hiddenBoxNote(items: readonly TraceArtifact[]): string | null
2671
2801
 
2672
2802
  **Returns:** The sentence, or null when no failure carries a hidden-box reading.
2673
2803
 
2804
+ #### `countMismatchSentence(mismatch)`
2805
+
2806
+ The row's sentence for a failing count assertion: the locator, both numbers, and the elements
2807
+ the failure snapshot held.
2808
+
2809
+ xr-molecule-flash-m321's `comments.spec.ts:86` printed only `Expected: 3 / Received: 4`. The
2810
+ helper counted `.card:not(.comment-form) .card-block`, and the app's comment form was a
2811
+ `.comment-form` wrapper around a plain `.card`, so the form's own `.card-block` was the fourth
2812
+ match. The trace held the selector and the four elements; the note held neither.
2813
+
2814
+ ```typescript
2815
+ function countMismatchSentence(mismatch: TraceCountMismatch): string
2816
+ ```
2817
+
2818
+ - `mismatch` — The trace's count reading ({@link TraceCountMismatch}).
2819
+
2820
+ **Returns:** The sentence.
2821
+
2822
+ #### `countMismatchNote(items)`
2823
+
2824
+ The sentence a count assertion earns when every traced failure is one — the next read before
2825
+ the API or the request is changed.
2826
+
2827
+ ```typescript
2828
+ function countMismatchNote(items: readonly TraceArtifact[]): string | null
2829
+ ```
2830
+
2831
+ - `items` — The traces {@link parseTraceArtifacts} read.
2832
+
2833
+ **Returns:** The sentence, or null when not every traced failure is a count mismatch.
2834
+
2674
2835
  #### `failureTraceNote(items, target, earlierFailures, apiPrefixes, report)`
2675
2836
 
2676
2837
  The note a failed suite run's result carries: what each failing test's page actually sent, and
@@ -3210,6 +3371,15 @@ function suiteRunPriorityLaunch(command: string, launch: string): { launch: stri
3210
3371
 
3211
3372
  Check if a command is blocked for security reasons. Returns error message or null if allowed.
3212
3373
 
3374
+ The first check reads the commands the shell actually RUNS — the statements, a `sh -c`
3375
+ string's body, and every command substitution ({@link executedCommands}) — so a `|` inside a
3376
+ quoted grep alternation is data, not a pipeline: `grep -E "USER CALLS|ENV|BUTTON|passed|failed"
3377
+ app.log` prints log lines, and the `ENV` in its pattern started nothing (X0
3378
+ xr-molecule-flash-m331 blocked exactly that call as an environment dump, twice). What the
3379
+ shell DOES run stays visible through the same walk: `ls | env`, `bash -c 'env'`,
3380
+ `echo "$(printenv)"` and a nested `sh -c 'x; env'` are all commands, and so is `env` itself
3381
+ when nothing follows it.
3382
+
3213
3383
  ```typescript
3214
3384
  function checkBlockedCommand(command: string): string | null
3215
3385
  ```
@@ -74,6 +74,43 @@ export declare function endsInBackground(command: string): boolean;
74
74
  * @returns One entry per statement or pipeline stage that runs something, in order.
75
75
  */
76
76
  export declare function commandSegments(command: string): CommandSegment[];
77
+ /** One command a shell command actually executes, wherever the shell runs it. */
78
+ export interface ExecutedCommand {
79
+ /**
80
+ * The word in command position, its directory removed (`/usr/bin/printenv` → `printenv`) — or
81
+ * the launcher itself when it runs alone and nothing follows it (`x | env` → `env`).
82
+ */
83
+ program: string;
84
+ /**
85
+ * Its arguments, as written with quotes kept — the statement's own words AFTER the program,
86
+ * including an input redirect's path (`< /proc/self/environ`), so a reader of secret files sees
87
+ * the file it reads.
88
+ */
89
+ args: readonly string[];
90
+ /** The program and its arguments as one line, what a pattern anchored at the program matches. */
91
+ text: string;
92
+ }
93
+ /**
94
+ * Every command a shell command actually executes, at any depth — the statements themselves, the
95
+ * body of a `sh -c '<command>'`, and the body of every command substitution (`$(…)`, backticks).
96
+ *
97
+ * This is the reading a policy that asks "does this command run X" needs, and it is deliberately
98
+ * not a text match. The command-position parser already knows what runs and what is only quoted
99
+ * data ({@link commandSegments}), so a pattern never reads a `|` inside a quoted grep alternation
100
+ * as a pipeline: `grep -E "USER CALLS|ENV|BUTTON" app.log` prints log lines, and the `ENV` in
101
+ * its pattern started nothing (X0 xr-molecule-flash-m331 blocked exactly that call as an
102
+ * environment dump). What the shell DOES run stays visible through the same walk: a bare
103
+ * launcher (`ls | env`), a shell string (`bash -c 'env'`), a substitution
104
+ * (`echo "$(printenv)"`) and a nested one (`sh -c 'x; env'`) are all commands here.
105
+ *
106
+ * `commandSegments` answers what a pipeline stage runs for a caller matching a runner; this
107
+ * answers with the launcher left standing when it runs alone, since that is what the shell
108
+ * executes then.
109
+ *
110
+ * @param command - The shell command.
111
+ * @returns Each command the shell executes, in the order the shell reaches them.
112
+ */
113
+ export declare function executedCommands(command: string): ExecutedCommand[];
77
114
  /**
78
115
  * One statement of a shell command, with where it sits and the variables it sets for itself.
79
116
  *
@@ -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;;;;;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;AAgSD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAikBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AA8GD;;;;;;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;AA4GD;;;;;;;;;;;;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;AAgSD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAynBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AA8GD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAED,iFAAiF;AACjF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;CACb;AAmFD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,EAAE,CAGnE;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;AA4GD;;;;;;;;;;;;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"}
@@ -694,14 +694,23 @@ function blankHeredocBodies(command) {
694
694
  * `molecule-dev/api/src/ai/supplied-tree-guard.ts` — so a script that does not classify records
695
695
  * no runner, and every run of it reads as nothing).
696
696
  *
697
+ * The walk also RECORDS the command substitutions it enters (`$(…)` and backticks), because the
698
+ * shell runs those too: a reader that only saw the outer statements could not tell that
699
+ * `echo "$(printenv)"` prints the environment ({@link executedCommands}).
700
+ *
697
701
  * @param command - The shell command.
698
702
  * @returns Each statement's text, the offset its first character sits at, and the separator that
699
703
  * ended it (`|`, `&&`, `;`, a newline, … — '' after the last one), in order, with empty ones
700
- * dropped.
704
+ * dropped — and the bodies of the command substitutions this level enters, as offsets.
701
705
  */
702
- function splitSegments(command) {
706
+ function splitCommand(command) {
703
707
  const source = blankHeredocBodies(command);
704
708
  const parts = [];
709
+ const substitutions = [];
710
+ /** Where each open `$(` body starts; -1 for a bare `(` opened inside one. */
711
+ const subOpens = [];
712
+ /** Where each open backtick body starts. */
713
+ const tickOpens = [];
705
714
  let buffer = '';
706
715
  /** The offset in the command of the current buffer's first character. */
707
716
  let bufferStart = 0;
@@ -739,6 +748,32 @@ function splitSegments(command) {
739
748
  stack.pop();
740
749
  continue;
741
750
  }
751
+ // Backticks open a command substitution wherever one runs — outside quotes, inside double
752
+ // quotes, and inside another substitution or a parameter expansion — but never inside single
753
+ // quotes (handled above). Its body is a command of its own: nothing in it splits THIS command,
754
+ // and a `'` or `"` in it is the nested command's, not this one's.
755
+ if (top !== 'tick' && char === '`') {
756
+ stack.push('tick');
757
+ tickOpens.push(i + 1);
758
+ put(char, -1);
759
+ continue;
760
+ }
761
+ if (top === 'tick') {
762
+ if (char === '\\' && next !== undefined) {
763
+ // An escaped character is literal: `\`` does not close the substitution.
764
+ put(char + next, -1);
765
+ i += 1;
766
+ continue;
767
+ }
768
+ if (char === '`') {
769
+ stack.pop();
770
+ const opened = tickOpens.pop();
771
+ if (opened !== undefined)
772
+ substitutions.push({ start: opened, end: i });
773
+ }
774
+ put(char, -1);
775
+ continue;
776
+ }
742
777
  // Comments are shell syntax only in a command context, including an unquoted `$(`.
743
778
  // Blank their bytes so quotes/operators cannot affect this walk, while every later
744
779
  // position and depth index stays aligned with the original command. Keep the newline.
@@ -753,6 +788,7 @@ function splitSegments(command) {
753
788
  // inside single quotes (handled above). The `(` is consumed here so the depth is counted once.
754
789
  if (char === '$' && next === '(') {
755
790
  stack.push('sub');
791
+ subOpens.push(i + 2);
756
792
  put('$(', -1);
757
793
  i += 1;
758
794
  continue;
@@ -802,10 +838,16 @@ function splitSegments(command) {
802
838
  // Inside a substitution or a subshell of one: its own parentheses nest, its `|`/`;`/`&&`
803
839
  // belong to the nested command, and its newlines are that command's too.
804
840
  if (top === 'sub') {
805
- if (char === '(')
841
+ if (char === '(') {
806
842
  stack.push('sub');
807
- else if (char === ')')
843
+ subOpens.push(-1);
844
+ }
845
+ else if (char === ')') {
808
846
  stack.pop();
847
+ const opened = subOpens.pop();
848
+ if (opened !== undefined && opened >= 0)
849
+ substitutions.push({ start: opened, end: i });
850
+ }
809
851
  put(char, -1);
810
852
  continue;
811
853
  }
@@ -861,7 +903,16 @@ function splitSegments(command) {
861
903
  });
862
904
  lastAfter = part.after;
863
905
  }
864
- return out;
906
+ return { parts: out, substitutions };
907
+ }
908
+ /**
909
+ * A command's statements and pipeline stages — {@link splitCommand}'s parts on their own.
910
+ *
911
+ * @param command - The shell command.
912
+ * @returns Each statement, in order, with empty ones dropped.
913
+ */
914
+ function splitSegments(command) {
915
+ return splitCommand(command).parts;
865
916
  }
866
917
  /**
867
918
  * Whether the outer shell's final statement starts a background job.
@@ -983,6 +1034,114 @@ function leadingAssignments(text) {
983
1034
  export function commandSegments(command) {
984
1035
  return locatedSegmentsOf(command, 0).flatMap((located) => located.segment === null ? [] : [located.segment]);
985
1036
  }
1037
+ /**
1038
+ * A statement's words with its redirections taken out, so the walk that finds the program does
1039
+ * not read a redirect's target as the program: `env > out.txt` RUNS `env`, and `cat > f` runs
1040
+ * `cat` with no file argument. Each word kept carries its index in `words`, because a caller
1041
+ * reading the statement's ARGUMENTS wants it as written — an input redirect's path is one.
1042
+ *
1043
+ * @param words - One statement's words, quotes kept.
1044
+ * @returns The words that are not redirections, and each one's index in `words`.
1045
+ */
1046
+ function withoutRedirects(words) {
1047
+ const kept = [];
1048
+ const index = [];
1049
+ for (let i = 0; i < words.length; i += 1) {
1050
+ const word = words[i] ?? '';
1051
+ // An operator written on its own owns the word after it (`> out.txt`, `< /proc/self/environ`).
1052
+ if (REDIRECT_OPERATOR.test(word)) {
1053
+ i += 1;
1054
+ continue;
1055
+ }
1056
+ // An operator with its target attached (`2>&1`, `>out.txt`, `&>log`) is one word.
1057
+ if (REDIRECT_WORD.test(word))
1058
+ continue;
1059
+ kept.push(word);
1060
+ index.push(i);
1061
+ }
1062
+ return { words: kept, index };
1063
+ }
1064
+ /**
1065
+ * The index of the last launcher a statement names (`x | env`, `sudo nice`), or null when it
1066
+ * names none.
1067
+ *
1068
+ * A statement that only NAMES a launcher runs that launcher: `x | env` pipes x's output into
1069
+ * `env`, which prints the environment, and the walk that reports what a command runs has to say
1070
+ * so ({@link executedCommands}).
1071
+ *
1072
+ * @param words - The statement's words, redirections already taken out.
1073
+ * @returns The launcher's index, or null.
1074
+ */
1075
+ function lastLauncherIndex(words) {
1076
+ for (let i = words.length - 1; i >= 0; i -= 1) {
1077
+ if (LAUNCHERS.has(programName(words[i] ?? '')))
1078
+ return i;
1079
+ }
1080
+ return null;
1081
+ }
1082
+ /**
1083
+ * {@link executedCommands}'s recursion: a statement's own `sh -c '<command>'`, and every command
1084
+ * substitution it carries, is a command of its own.
1085
+ *
1086
+ * @param command - The shell command, or one nested command string.
1087
+ * @param depth - How many nested command strings this one already sits inside.
1088
+ * @returns Each command the shell executes, in order.
1089
+ */
1090
+ function executedCommandsOf(command, depth) {
1091
+ if (typeof command !== 'string' || depth > MAX_NESTING)
1092
+ return [];
1093
+ const out = [];
1094
+ const { parts, substitutions } = splitCommand(command);
1095
+ for (const part of parts) {
1096
+ const words = segmentWords(part.text);
1097
+ const placed = withoutRedirects(words);
1098
+ const start = commandStart(placed.words);
1099
+ // A shell's `-c '<command>'` is the command: what the shell itself does is run it.
1100
+ if (start.nested !== null) {
1101
+ out.push(...executedCommandsOf(start.nested, depth + 1));
1102
+ continue;
1103
+ }
1104
+ const at = placed.words[start.at] === undefined ? lastLauncherIndex(placed.words) : start.at;
1105
+ if (at === null)
1106
+ continue;
1107
+ const program = start.program ?? programName(placed.words[at] ?? '');
1108
+ if (!program)
1109
+ continue;
1110
+ // Arguments from the statement as written, from the word after the program: `cat < /proc/…`
1111
+ // reads that file, and a reader of secret files has to see it.
1112
+ const args = words.slice((placed.index[at] ?? 0) + 1);
1113
+ out.push({ program, args, text: [program, ...args].join(' ') });
1114
+ }
1115
+ for (const substitution of substitutions) {
1116
+ out.push(...executedCommandsOf(command.slice(substitution.start, substitution.end), depth + 1));
1117
+ }
1118
+ return out;
1119
+ }
1120
+ /**
1121
+ * Every command a shell command actually executes, at any depth — the statements themselves, the
1122
+ * body of a `sh -c '<command>'`, and the body of every command substitution (`$(…)`, backticks).
1123
+ *
1124
+ * This is the reading a policy that asks "does this command run X" needs, and it is deliberately
1125
+ * not a text match. The command-position parser already knows what runs and what is only quoted
1126
+ * data ({@link commandSegments}), so a pattern never reads a `|` inside a quoted grep alternation
1127
+ * as a pipeline: `grep -E "USER CALLS|ENV|BUTTON" app.log` prints log lines, and the `ENV` in
1128
+ * its pattern started nothing (X0 xr-molecule-flash-m331 blocked exactly that call as an
1129
+ * environment dump). What the shell DOES run stays visible through the same walk: a bare
1130
+ * launcher (`ls | env`), a shell string (`bash -c 'env'`), a substitution
1131
+ * (`echo "$(printenv)"`) and a nested one (`sh -c 'x; env'`) are all commands here.
1132
+ *
1133
+ * `commandSegments` answers what a pipeline stage runs for a caller matching a runner; this
1134
+ * answers with the launcher left standing when it runs alone, since that is what the shell
1135
+ * executes then.
1136
+ *
1137
+ * @param command - The shell command.
1138
+ * @returns Each command the shell executes, in the order the shell reaches them.
1139
+ */
1140
+ export function executedCommands(command) {
1141
+ if (typeof command !== 'string' || !command.trim())
1142
+ return [];
1143
+ return executedCommandsOf(command, 0);
1144
+ }
986
1145
  /**
987
1146
  * Where a statement's word sits in its own raw text, as {@link segmentWords} counts the words.
988
1147
  *
@@ -1 +1 @@
1
- {"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAMH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AAiBD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAkDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA4B9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,IAAI,CAIf;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK;AACf,sEAAsE;AACpE,aAAa;AACf,oDAAoD;GAClD,iBAAiB;AACnB;;;;GAIG;GACD,cAAc,CAAA;AAElB,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CA4Ff;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmB1D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAyClD"}
1
+ {"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAMH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AAiBD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAkDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA4B9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,IAAI,CAIf;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK;AACf,sEAAsE;AACpE,aAAa;AACf,oDAAoD;GAClD,iBAAiB;AACnB;;;;GAIG;GACD,cAAc,CAAA;AAElB,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAgGf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmB1D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAyClD"}
package/dist/cut-run.js CHANGED
@@ -345,12 +345,16 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
345
345
  inlineScript,
346
346
  };
347
347
  }
348
- // The runner printed a summary that does not account for the total it announced, and neither
349
- // bound above fired: the process was stopped from outside the run. A run that used the whole
350
- // budget is the backend's deadline (which sits just past it); anything else is a stop this tool
351
- // does not own. Either way the summary counts are all that ran, and a pipeline's 0 is not the
352
- // run's code — the run never reached its own verdict.
353
- if (summary !== null && summary.announced !== null) {
348
+ // A runner's summary that does not account for the total it announced, in a command that
349
+ // actually RUNS that runner, and neither bound above fired: the process was stopped from
350
+ // outside the run. The command must run the runner — a reader of a LIVE run's log prints the
351
+ // same announcement and partial tally as text it copies, and reporting that as a stopped run
352
+ // told the executor a suite it had only grepped had been killed (X0 xr-molecule-flash-m345,
353
+ // +50:00: `grep -B2 -A12 … /tmp/mol-bg-….log | head -40` came back `stoppedBy: process-stop`
354
+ // after 1 s). A run that used the whole budget is the backend's deadline (which sits just past
355
+ // it); anything else is a stop this tool does not own. Either way the summary counts are all
356
+ // that ran, and a pipeline's 0 is not the run's code — the run never reached its own verdict.
357
+ if (suite && summary !== null && summary.announced !== null) {
354
358
  const usedBudget = budgetSeconds > 0 && elapsedMs >= atBoundMs(budgetSeconds);
355
359
  return {
356
360
  seconds: usedBudget ? budgetSeconds : Math.max(1, Math.round(elapsedMs / 1000)),
@@ -18,11 +18,14 @@
18
18
  * 401`. So the platform reads it (bounded, per test) and puts it in the run's result
19
19
  * ({@link failureTraceNote}), instead of leaving the executor to build its own browser to find out.
20
20
  *
21
- * Everything here is text in, text out: the sandbox runs {@link traceArtifactsScript} (Node, no
22
- * dependencies — Playwright's zip reader is not importable from a project) and prints one
23
- * `TRACE <json>` line per trace; the platform parses those lines ({@link parseTraceArtifacts}) and
24
- * writes the note ({@link failureTraceNote}). Both halves are covered by a test that runs the
25
- * script against a `trace.zip` produced by a real Playwright run, in the sandbox's own shape.
21
+ * Everything here is text in, text out: the sandbox runs {@link traceArtifactsScript} (plain Node —
22
+ * the zip is read without a dependency) and prints one `TRACE <json>` line per trace; the platform
23
+ * parses those lines ({@link parseTraceArtifacts}) and writes the note ({@link failureTraceNote}).
24
+ * The one exception is the selector replay behind a `locator.count()` reading: it resolves the
25
+ * project's own installed Playwright, replays the recorded selector against the recorded snapshot
26
+ * in that browser's DOM, and reports `browser-unavailable` (naming no elements) when it cannot.
27
+ * Both halves are covered by a test that runs the script against a `trace.zip` produced by a real
28
+ * Playwright run, in the sandbox's own shape.
26
29
  *
27
30
  * @module
28
31
  */
@@ -263,6 +266,73 @@ export interface TraceHiddenBox {
263
266
  /** True when the element has no text and no element children at all. */
264
267
  empty?: true;
265
268
  }
269
+ /**
270
+ * One element a failing locator count matched, read from the trace's own failure snapshot.
271
+ *
272
+ * A count assertion reports only `Expected: 3 / Received: 4`; the element that made it 4 is not
273
+ * in the error. Playwright's trace does hold it — either marked `__playwright_target__` in the
274
+ * failure snapshot (`expect(locator).toHaveCount(n)`), or recoverable by replaying the recorded
275
+ * selector against the snapshot's DOM in a browser (`locator.count()` then `expect(n).toBe(k)`) —
276
+ * so each match is named by what it is and where it sits, never by a guess.
277
+ */
278
+ export interface TraceCountMatch {
279
+ /** The element's tag, lowercased (`div`). */
280
+ tag: string;
281
+ /** Its `class` attribute, bounded (`card-block`). */
282
+ classes?: string;
283
+ /** Its own text, whitespace-collapsed and bounded. */
284
+ text?: string;
285
+ /** Its ancestors, outermost first (`body > div.comment-form > div.card`), bounded. */
286
+ path?: string;
287
+ /** True when a `<form>` is one of its ancestors — the m321 extra match's shape. */
288
+ inForm?: true;
289
+ }
290
+ /**
291
+ * A failing assertion that compared a LOCATOR COUNT (`expect(await locator.count()).toBe(n)` or
292
+ * `expect(locator).toHaveCount(n)`), with the locator the trace recorded and, when the failure
293
+ * snapshot can supply them, the elements it matched.
294
+ *
295
+ * xr-molecule-flash-m321's `comments.spec.ts:86` sat on `Expected: 3 / Received: 4` for ~30
296
+ * minutes and three whole suite walks: the helper counted `.card:not(.comment-form) .card-block`,
297
+ * the app's comment form was a `.comment-form` wrapper around a plain `.card`, and the form's own
298
+ * `.card-block` made the fourth. The trace had the selector (its `Frame.queryCount` entry) and the
299
+ * four elements (its failure snapshot); the note printed neither. This reading prints both, and
300
+ * fails closed — a selector outside the browser's own CSS support, a missing browser, or a replay
301
+ * that disagrees with the recorded count names no elements rather than inventing them.
302
+ */
303
+ export interface TraceCountMismatch {
304
+ /** The locator as the trace or the assertion printed it (`locator('.card:not(…')`). */
305
+ locator: string;
306
+ /** The selector inside that locator, when the trace recorded one. */
307
+ selector?: string;
308
+ /** The number the assertion expected. */
309
+ expected: number;
310
+ /** The number the page matched. */
311
+ received: number;
312
+ /**
313
+ * Where the locator came from: the assertion's own message, or the `locator.count()` entry the
314
+ * trace recorded just before the failing assertion.
315
+ */
316
+ source: 'assertion' | 'locator-count';
317
+ /** The matched elements, bounded to {@link MAX_COUNT_MATCHES} in document order. */
318
+ matched?: TraceCountMatch[];
319
+ /** How many elements matched in total, before the display bound. */
320
+ matchedTotal?: number;
321
+ /**
322
+ * How the elements were read: Playwright's own `__playwright_target__` marks in the failure
323
+ * snapshot, or the recorded selector replayed against that snapshot in a browser.
324
+ */
325
+ matchedFrom?: 'snapshot-targets' | 'selector-replay';
326
+ /**
327
+ * Why no elements are named, when they are not. `unsupported-selector` is a selector the
328
+ * browser's `querySelectorAll` rejects (a `getByRole`/`getByText` engine, `>>` chaining);
329
+ * `no-snapshot` is a trace with no readable DOM snapshot at the failure; `count-mismatch` is a
330
+ * replay that disagreed with the recorded count, so the snapshot is not evidence for this
331
+ * assertion; `browser-unavailable` is a project whose Playwright module or recorded engine
332
+ * executable could not be resolved, so the selector could not be replayed at all.
333
+ */
334
+ unreadable?: 'unsupported-selector' | 'no-snapshot' | 'count-mismatch' | 'browser-unavailable';
335
+ }
266
336
  /**
267
337
  * One method+path a test's page sent many times — the trace of a render loop.
268
338
  *
@@ -376,6 +446,13 @@ export interface TraceArtifact {
376
446
  * leaving the run to probe computed styles (xr-molecule-flash-m197).
377
447
  */
378
448
  hiddenBox?: TraceHiddenBox;
449
+ /**
450
+ * A failing assertion that compared a locator count, with the locator the trace recorded and the
451
+ * elements its failure snapshot matched ({@link TraceCountMismatch}). The row prints them, so a
452
+ * count that is off by one names the extra element instead of leaving the run to probe for it
453
+ * (xr-molecule-flash-m321's 30-minute `Expected: 3 / Received: 4`).
454
+ */
455
+ countMismatch?: TraceCountMismatch;
379
456
  /**
380
457
  * The method+path groups this test's page sent {@link TRACE_NOTE_LOOP_REPEATS} times or more,
381
458
  * most-repeated first — a render loop the note leads with (xr-molecule-flash-m215: 2,106
@@ -601,6 +678,27 @@ export declare function isCredentialExchangePath(path: string, authEndpoints?: r
601
678
  * @returns The sentence, or null when no failure carries a hidden-box reading.
602
679
  */
603
680
  export declare function hiddenBoxNote(items: readonly TraceArtifact[]): string | null;
681
+ /**
682
+ * The row's sentence for a failing count assertion: the locator, both numbers, and the elements
683
+ * the failure snapshot held.
684
+ *
685
+ * xr-molecule-flash-m321's `comments.spec.ts:86` printed only `Expected: 3 / Received: 4`. The
686
+ * helper counted `.card:not(.comment-form) .card-block`, and the app's comment form was a
687
+ * `.comment-form` wrapper around a plain `.card`, so the form's own `.card-block` was the fourth
688
+ * match. The trace held the selector and the four elements; the note held neither.
689
+ *
690
+ * @param mismatch - The trace's count reading ({@link TraceCountMismatch}).
691
+ * @returns The sentence.
692
+ */
693
+ export declare function countMismatchSentence(mismatch: TraceCountMismatch): string;
694
+ /**
695
+ * The sentence a count assertion earns when every traced failure is one — the next read before
696
+ * the API or the request is changed.
697
+ *
698
+ * @param items - The traces {@link parseTraceArtifacts} read.
699
+ * @returns The sentence, or null when not every traced failure is a count mismatch.
700
+ */
701
+ export declare function countMismatchNote(items: readonly TraceArtifact[]): string | null;
604
702
  /**
605
703
  * The note a failed suite run's result carries: what each failing test's page actually sent, and
606
704
  * what came back.