@molecule/api-ai-tools 1.16.8 → 1.17.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-11-06T03:40:01.617Z
6
+ Generated: 2026-11-06T13:53:38.704Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -51,7 +51,7 @@ for await (const event of requireProvider().chat({ system, tools, messages, stre
51
51
  ## Installation
52
52
 
53
53
  ```bash
54
- npm install @molecule/api-ai-tools @molecule/api-ai
54
+ npm install @molecule/api-ai-tools @molecule/api-ai typescript
55
55
  ```
56
56
 
57
57
  ## API
@@ -404,6 +404,111 @@ interface RunnerOutputFilter {
404
404
  }
405
405
  ```
406
406
 
407
+ #### `FeedbackSource`
408
+
409
+ A source file already read by the caller. Paths are absolute.
410
+
411
+ ```typescript
412
+ interface FeedbackSource {
413
+ path: string
414
+ content: string
415
+ }
416
+ ```
417
+
418
+ #### `FeedbackSpec`
419
+
420
+ One supplied or scaffolded spec, with its normal, locally targeted runner command.
421
+
422
+ ```typescript
423
+ interface FeedbackSpec extends FeedbackSource {
424
+ command: string
425
+ cwd: string
426
+ }
427
+ ```
428
+
429
+ #### `FeedbackProject`
430
+
431
+ The inputs the host already knows; no suite discovery or vendor selection in the scheduler.
432
+
433
+ ```typescript
434
+ interface FeedbackProject {
435
+ /** Base for relative source-change paths, already resolved by the host. */
436
+ root?: string
437
+ sources: FeedbackSource[]
438
+ specs: FeedbackSpec[]
439
+ }
440
+ ```
441
+
442
+ #### `PlaywrightFeedbackFailure`
443
+
444
+ A native failure, never a static prediction or a completed-suite proof.
445
+
446
+ ```typescript
447
+ interface PlaywrightFeedbackFailure {
448
+ spec: string
449
+ line: number
450
+ error: string
451
+ /** The native runner's locator or assertion, verbatim apart from redaction. */
452
+ locator: string
453
+ /** Playwright's own failure-time accessibility snapshot, when recorded. */
454
+ rendered?: string
455
+ /** Route render dependencies affected by this write, or the triggering source paths. */
456
+ sourceFiles: string[]
457
+ }
458
+ ```
459
+
460
+ #### `PlaywrightFeedbackResult`
461
+
462
+ A diagnosis of one spec file at one source revision. A pass is not whole-suite proof.
463
+
464
+ ```typescript
465
+ interface PlaywrightFeedbackResult {
466
+ spec: string
467
+ revision: number
468
+ durationMs: number
469
+ status: 'passed' | 'failed' | 'unavailable'
470
+ failures: PlaywrightFeedbackFailure[]
471
+ /** Native counts for this one diagnostic selection, never whole-suite proof. */
472
+ nativeStats?: PlaywrightReportStats
473
+ reason?: string
474
+ }
475
+ ```
476
+
477
+ #### `PlaywrightFeedbackOptions`
478
+
479
+ Host operations for the same scheduler in a CLI process or a remote sandbox.
480
+
481
+ ```typescript
482
+ interface PlaywrightFeedbackOptions {
483
+ load: () => Promise<FeedbackProject>
484
+ run: (
485
+ spec: FeedbackSpec,
486
+ sources: string[],
487
+ revision: number,
488
+ emit: (result: PlaywrightFeedbackResult) => void,
489
+ ) => Promise<PlaywrightFeedbackResult>
490
+ /** Only current native failures are delivered. Passing apps receive no note. */
491
+ onFailures?: (result: PlaywrightFeedbackResult) => void
492
+ /** Operational state is separate from defect feedback. */
493
+ onResult?: (result: PlaywrightFeedbackResult) => void
494
+ debounceMs?: number
495
+ }
496
+ ```
497
+
498
+ #### `PlaywrightFeedback`
499
+
500
+ A nonblocking source-change subscription.
501
+
502
+ ```typescript
503
+ interface PlaywrightFeedback {
504
+ changed: (paths: readonly string[]) => void
505
+ takeFailures: () => PlaywrightFeedbackResult[]
506
+ /** Wait only in tests or shutdown; file writes and deploys must never await this. */
507
+ settled: () => Promise<void>
508
+ close: () => Promise<void>
509
+ }
510
+ ```
511
+
407
512
  #### `PlaywrightReportAttachment`
408
513
 
409
514
  One artifact the runner saved for one test, as the report records it.
@@ -2550,6 +2655,96 @@ function pipelineExitCodeNote(command: string, exitCode: number, output: string)
2550
2655
 
2551
2656
  **Returns:** One sentence naming the stage the code came from, or null.
2552
2657
 
2658
+ #### `selectFeedbackSpecs(project, changed)`
2659
+
2660
+ Prioritize spec files potentially affected by writes. Every file still runs: a click or
2661
+ a helper can navigate somewhere a literal-goto index cannot see. This index never excludes work.
2662
+
2663
+ ```typescript
2664
+ function selectFeedbackSpecs(
2665
+ project: FeedbackProject,
2666
+ changed: readonly string[],
2667
+ ): Array<{ spec: FeedbackSpec; sourceFiles: string[] }>
2668
+ ```
2669
+
2670
+ - `project` — Caller-supplied source and runner inventory.
2671
+ - `changed` — Absolute source paths changed since the previous diagnosis.
2672
+
2673
+ **Returns:** Spec files to run, with their rendering-source context.
2674
+
2675
+ #### `isPlaywrightFeedbackSource(path)`
2676
+
2677
+ Whether a source change can alter markup, copy, styling or local runner configuration.
2678
+
2679
+ ```typescript
2680
+ function isPlaywrightFeedbackSource(path: string): boolean
2681
+ ```
2682
+
2683
+ - `path` — The path reported by the host's file-change subscription.
2684
+
2685
+ **Returns:** True for app source and data formats; never a correctness judgment.
2686
+
2687
+ #### `createPlaywrightFeedback(options)`
2688
+
2689
+ Subscribe to writes without making them wait for browser work.
2690
+
2691
+ ```typescript
2692
+ function createPlaywrightFeedback(options: PlaywrightFeedbackOptions): PlaywrightFeedback
2693
+ ```
2694
+
2695
+ - `options` — Source loader, native runner and delivery callbacks.
2696
+
2697
+ **Returns:** A source-change subscription; settle/close are for tests and shutdown only.
2698
+
2699
+ #### `readFeedbackSources(backend, sourceRoots, suiteFiles)`
2700
+
2701
+ Read explicit source roots and supplied helper/spec paths in one backend round trip.
2702
+ The standard filesystem and TypeScript parser own traversal/syntax; this is transport batching.
2703
+
2704
+ ```typescript
2705
+ function readFeedbackSources(
2706
+ backend: ExecutionBackend,
2707
+ sourceRoots: readonly string[],
2708
+ suiteFiles: readonly string[],
2709
+ ): Promise<FeedbackSource[]>
2710
+ ```
2711
+
2712
+ - `backend` — Local or sandbox execution backend.
2713
+ - `sourceRoots` — App source directories, absolute.
2714
+ - `suiteFiles` — Supplied spec/helper paths, absolute.
2715
+
2716
+ **Returns:** Existing, bounded text sources. Missing/unsupported files are ignored.
2717
+
2718
+ #### `runPlaywrightFeedback(backend, spec, sourceFiles, revision, options)`
2719
+
2720
+ Run ONE spec's native command. Callers supply the correctly configured local command and
2721
+ append these runner flags at composition time (rather than modifying shell text here).
2722
+ Native failures are diagnosis, never proof that a complete supplied suite passed.
2723
+
2724
+ ```typescript
2725
+ function runPlaywrightFeedback(
2726
+ backend: ExecutionBackend,
2727
+ spec: FeedbackSpec,
2728
+ sourceFiles: string[],
2729
+ revision: number,
2730
+ options: {
2731
+ command: (flags: string[]) => string
2732
+ scratchRoot?: string
2733
+ timeoutMs?: number
2734
+ emit?: (result: PlaywrightFeedbackResult) => void
2735
+ pollMs?: number
2736
+ },
2737
+ ): Promise<PlaywrightFeedbackResult>
2738
+ ```
2739
+
2740
+ - `backend` — Existing execution backend.
2741
+ - `spec` — The supplied spec and its normal per-file command.
2742
+ - `sourceFiles` — Triggering component files, known to the write/index.
2743
+ - `revision` — Source revision the scheduler will validate before delivery.
2744
+ - `options` — Output location, command composition and a bounded backend budget.
2745
+
2746
+ **Returns:** The actual native outcome and actionable failures only.
2747
+
2553
2748
  #### `reportCarriesIdentities(status)`
2554
2749
 
2555
2750
  The report statuses for which the report's own per-test identities and attachments may be used.
@@ -5804,7 +5999,15 @@ Peer dependencies:
5804
5999
  ### Runtime Dependencies
5805
6000
 
5806
6001
  - `@molecule/api-ai`
5807
-
6002
+ - `typescript`
6003
+
6004
+ - **Background Playwright feedback:** `createPlaywrightFeedback({ load, run, onFailures })`
6005
+ coalesces `changed(paths)` events without delaying writes. Use `readFeedbackSources`
6006
+ to batch source reads over either backend and `runPlaywrightFeedback` to run each
6007
+ supplied spec through its native command. Its incremental reporter supplies exact
6008
+ locator failures and the failure-time page snapshot. Healthy runs emit no note;
6009
+ no suite schedules no runner. `settled()`/`close()` are for tests and shutdown,
6010
+ never a deployment barrier. Diagnoses are not whole-suite completion proof.
5808
6011
  - **Opaque command plans:** prepare each logical step with `streamingBackgroundCommand`,
5809
6012
  call `createCommandPlan`, and attach it to the executed wrapper with `commandWithPlan`.
5810
6013
  The launcher prints each step's `startMarker` and `endMarker` on their own lines around
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/backends/local.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAsDxE"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/backends/local.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CA6FxE"}
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @module
6
6
  */
7
- import { execFile } from 'child_process';
7
+ import { spawn } from 'child_process';
8
8
  import { mkdir, readdir, readFile, unlink, writeFile } from 'fs/promises';
9
9
  import { dirname } from 'path';
10
10
  /**
@@ -38,23 +38,69 @@ export function createLocalBackend(projectRoot) {
38
38
  },
39
39
  run(command, opts) {
40
40
  return new Promise((resolve) => {
41
- // Use shell: true for command strings (grep pipelines, find, etc.)
42
- // Input is sanitized via shellQuote at the tool level
43
- execFile('sh', ['-c', command], {
41
+ // A native runner owns children. execFile's timeout killed only sh,
42
+ // leaving the runner/browser alive. Use the OS process group, with a
43
+ // graceful signal followed by a bounded forceful cleanup of that group.
44
+ const grouped = process.platform !== 'win32';
45
+ const child = spawn('sh', ['-c', command], {
44
46
  cwd: opts?.cwd || projectRoot,
45
- timeout: opts?.timeout || 30000,
46
- maxBuffer: 10 * 1024 * 1024,
47
- encoding: 'utf8',
48
- }, (error, stdout, stderr) => {
49
- if (error && 'code' in error && typeof error.code === 'number') {
50
- resolve({ stdout: stdout || '', stderr: stderr || '', exitCode: error.code });
47
+ detached: grouped,
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ });
50
+ let stdout = '', stderr = '', closed = false, code = 1;
51
+ let stopped;
52
+ let force;
53
+ const finish = () => {
54
+ if (closed && !force)
55
+ resolve({ stdout, stderr, exitCode: stopped === 'timeout' ? 124 : stopped ? 1 : code });
56
+ };
57
+ const signal = (value) => {
58
+ try {
59
+ if (grouped && child.pid)
60
+ process.kill(-child.pid, value);
61
+ else
62
+ child.kill(value);
51
63
  }
52
- else if (error) {
53
- resolve({ stdout: stdout || '', stderr: stderr || error.message, exitCode: 1 });
64
+ catch (_error) {
65
+ /* the owned group already exited */
54
66
  }
55
- else {
56
- resolve({ stdout: stdout || '', stderr: stderr || '', exitCode: 0 });
67
+ };
68
+ const stop = (why) => {
69
+ if (stopped)
70
+ return;
71
+ stopped = why;
72
+ signal('SIGTERM');
73
+ force = setTimeout(() => {
74
+ signal('SIGKILL');
75
+ force = undefined;
76
+ finish();
77
+ }, 1000);
78
+ };
79
+ const timeout = Math.min(opts?.timeout || 30_000, opts?.budgetMs || Infinity);
80
+ const deadline = setTimeout(() => stop('timeout'), timeout);
81
+ const collect = (text, channel) => {
82
+ if (stopped)
83
+ return;
84
+ if (stdout.length + stderr.length + text.length > 10 * 1024 * 1024) {
85
+ stderr += '\nCommand exceeded the 10 MB output limit.';
86
+ stop('output');
87
+ return;
57
88
  }
89
+ if (channel === 'stdout')
90
+ stdout += text;
91
+ else
92
+ stderr += text;
93
+ };
94
+ child.stdout.setEncoding('utf8').on('data', (text) => collect(text, 'stdout'));
95
+ child.stderr.setEncoding('utf8').on('data', (text) => collect(text, 'stderr'));
96
+ child.on('error', (error) => {
97
+ stderr += error.message;
98
+ });
99
+ child.on('close', (exitCode) => {
100
+ closed = true;
101
+ code = exitCode ?? 1;
102
+ clearTimeout(deadline);
103
+ finish();
58
104
  });
59
105
  });
60
106
  },
package/dist/index.d.ts CHANGED
@@ -12,6 +12,13 @@
12
12
  * (identity, tool listing, project docs, discovered skills).
13
13
  *
14
14
  * @remarks
15
+ * - **Background Playwright feedback:** `createPlaywrightFeedback({ load, run, onFailures })`
16
+ * coalesces `changed(paths)` events without delaying writes. Use `readFeedbackSources`
17
+ * to batch source reads over either backend and `runPlaywrightFeedback` to run each
18
+ * supplied spec through its native command. Its incremental reporter supplies exact
19
+ * locator failures and the failure-time page snapshot. Healthy runs emit no note;
20
+ * no suite schedules no runner. `settled()`/`close()` are for tests and shutdown,
21
+ * never a deployment barrier. Diagnoses are not whole-suite completion proof.
15
22
  * - **Opaque command plans:** prepare each logical step with `streamingBackgroundCommand`,
16
23
  * call `createCommandPlan`, and attach it to the executed wrapper with `commandWithPlan`.
17
24
  * The launcher prints each step's `startMarker` and `endMarker` on their own lines around
@@ -165,6 +172,9 @@ export * from './line-diff.js';
165
172
  export * from './no-test-run.js';
166
173
  export * from './output-filter.js';
167
174
  export * from './pipeline-exit-note.js';
175
+ export * from './playwright-feedback.js';
176
+ export * from './playwright-feedback-selection.js';
177
+ export * from './playwright-feedback-types.js';
168
178
  export * from './playwright-report.js';
169
179
  export * from './schemas.js';
170
180
  export * from './server-command.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwJG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+JG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,oCAAoC,CAAA;AAClD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,wBAAwB,CAAA;AACtC,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -12,6 +12,13 @@
12
12
  * (identity, tool listing, project docs, discovered skills).
13
13
  *
14
14
  * @remarks
15
+ * - **Background Playwright feedback:** `createPlaywrightFeedback({ load, run, onFailures })`
16
+ * coalesces `changed(paths)` events without delaying writes. Use `readFeedbackSources`
17
+ * to batch source reads over either backend and `runPlaywrightFeedback` to run each
18
+ * supplied spec through its native command. Its incremental reporter supplies exact
19
+ * locator failures and the failure-time page snapshot. Healthy runs emit no note;
20
+ * no suite schedules no runner. `settled()`/`close()` are for tests and shutdown,
21
+ * never a deployment barrier. Diagnoses are not whole-suite completion proof.
15
22
  * - **Opaque command plans:** prepare each logical step with `streamingBackgroundCommand`,
16
23
  * call `createCommandPlan`, and attach it to the executed wrapper with `commandWithPlan`.
17
24
  * The launcher prints each step's `startMarker` and `endMarker` on their own lines around
@@ -165,6 +172,9 @@ export * from './line-diff.js';
165
172
  export * from './no-test-run.js';
166
173
  export * from './output-filter.js';
167
174
  export * from './pipeline-exit-note.js';
175
+ export * from './playwright-feedback.js';
176
+ export * from './playwright-feedback-selection.js';
177
+ export * from './playwright-feedback-types.js';
168
178
  export * from './playwright-report.js';
169
179
  export * from './schemas.js';
170
180
  export * from './server-command.js';
@@ -0,0 +1,16 @@
1
+ import type { PlaywrightFeedbackFailure } from './playwright-feedback-types.js';
2
+ /**
3
+ * A standard Playwright reporter, placed beside each run's private outputs. It
4
+ * records onTestEnd, so the first exact failure need not wait for the rest of
5
+ * the spec file. No page injection, test rewrite, screenshots or trace overhead.
6
+ * @returns A CommonJS reporter loadable by the project's installed Playwright.
7
+ */
8
+ export declare function feedbackReporterSource(): string;
9
+ /**
10
+ * Read native failure events; incomplete final lines wait for the next read.
11
+ * @param text - Reporter JSONL, not runner-log inference.
12
+ * @param sourceFiles - Triggering source files from the scheduler.
13
+ * @returns Locator failures only, redacted and bounded.
14
+ */
15
+ export declare function nativeFeedbackFailures(text: string, sourceFiles: string[]): PlaywrightFeedbackFailure[];
16
+ //# sourceMappingURL=playwright-feedback-reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-feedback-reporter.d.ts","sourceRoot":"","sources":["../src/playwright-feedback-reporter.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAA;AAG/E;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAyB/C;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EAAE,GACpB,yBAAyB,EAAE,CAsC7B"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Native Playwright events for incremental background feedback.
3
+ *
4
+ * @module
5
+ */
6
+ import { stripVTControlCharacters } from 'node:util';
7
+ import { redactSecrets } from './utilities.js';
8
+ /**
9
+ * A standard Playwright reporter, placed beside each run's private outputs. It
10
+ * records onTestEnd, so the first exact failure need not wait for the rest of
11
+ * the spec file. No page injection, test rewrite, screenshots or trace overhead.
12
+ * @returns A CommonJS reporter loadable by the project's installed Playwright.
13
+ */
14
+ export function feedbackReporterSource() {
15
+ return String.raw `
16
+ const fs = require('node:fs');
17
+ module.exports = class FeedbackReporter {
18
+ onTestEnd(test, result) {
19
+ if (result.status === test.expectedStatus || !['failed','timedOut'].includes(result.status)) return;
20
+ const errors = result.errors || [];
21
+ const error = errors.map(e => e.message || '').join('\n');
22
+ const location = [...errors].reverse().find(e => e.location)?.location || test.location;
23
+ const attachment = result.attachments.find(a => a.name === 'error-context' && a.path);
24
+ let rendered;
25
+ if (attachment) try {
26
+ const text = fs.readFileSync(attachment.path, 'utf8');
27
+ const fence = String.fromCharCode(96).repeat(3);
28
+ const yaml = text.split(fence + 'yaml\n')[1];
29
+ if (yaml) rendered = yaml.split(fence)[0].trim().slice(0, 2000);
30
+ else {
31
+ const section = text.split('# Page snapshot')[1];
32
+ if (section) rendered = section.split(fence)[1]?.split('\n').slice(1).join('\n').trim().slice(0, 2000);
33
+ }
34
+ } catch {}
35
+ if (process.env.MOL_TEST_FEEDBACK_EVENTS) fs.appendFileSync(process.env.MOL_TEST_FEEDBACK_EVENTS,
36
+ JSON.stringify({spec: location.file, line: location.line, error: error.slice(0, 6000), rendered}) + '\n');
37
+ }
38
+ };`;
39
+ }
40
+ /**
41
+ * Read native failure events; incomplete final lines wait for the next read.
42
+ * @param text - Reporter JSONL, not runner-log inference.
43
+ * @param sourceFiles - Triggering source files from the scheduler.
44
+ * @returns Locator failures only, redacted and bounded.
45
+ */
46
+ export function nativeFeedbackFailures(text, sourceFiles) {
47
+ const out = [];
48
+ if (text.length > 1_000_000)
49
+ return out;
50
+ for (const line of text.split('\n').slice(0, -1)) {
51
+ try {
52
+ const event = JSON.parse(line);
53
+ if (typeof event.spec !== 'string' ||
54
+ typeof event.line !== 'number' ||
55
+ typeof event.error !== 'string')
56
+ continue;
57
+ const error = redactSecrets(stripVTControlCharacters(event.error).slice(0, 6_000));
58
+ const locator = /(?:Locator:\s*|waiting for )(locator\([^\n]+|getBy[^\n]+)/.exec(error)?.[1] ??
59
+ /strict mode violation: ([^\n]+)/.exec(error)?.[1] ??
60
+ /(?:^|\n)((?:Error: )?expect\([^\n]+|AssertionError:[^\n]+)/.exec(error)?.[1];
61
+ if (!locator)
62
+ continue;
63
+ out.push({
64
+ spec: event.spec,
65
+ line: event.line,
66
+ error,
67
+ locator,
68
+ ...(typeof event.rendered === 'string' && event.rendered
69
+ ? { rendered: redactSecrets(event.rendered.slice(0, 2000)) }
70
+ : {}),
71
+ sourceFiles,
72
+ });
73
+ }
74
+ catch (_error) {
75
+ /* not a completed native event */
76
+ }
77
+ }
78
+ return out;
79
+ }
@@ -0,0 +1,13 @@
1
+ import type { FeedbackProject, FeedbackSpec } from './playwright-feedback-types.js';
2
+ /**
3
+ * Prioritize spec files potentially affected by writes. Every file still runs: a click or
4
+ * a helper can navigate somewhere a literal-goto index cannot see. This index never excludes work.
5
+ * @param project - Caller-supplied source and runner inventory.
6
+ * @param changed - Absolute source paths changed since the previous diagnosis.
7
+ * @returns Spec files to run, with their rendering-source context.
8
+ */
9
+ export declare function selectFeedbackSpecs(project: FeedbackProject, changed: readonly string[]): Array<{
10
+ spec: FeedbackSpec;
11
+ sourceFiles: string[];
12
+ }>;
13
+ //# sourceMappingURL=playwright-feedback-selection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-feedback-selection.d.ts","sourceRoot":"","sources":["../src/playwright-feedback-selection.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,eAAe,EAAkB,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAsLnG;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,KAAK,CAAC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAwBtD"}
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Prioritize native spec files from explicit route declarations and source imports.
3
+ * TypeScript parses syntax; Playwright judges behavior. Unreadable/dynamic routing
4
+ * falls back to all specs, rather than treating an absent index entry as coverage.
5
+ * @module
6
+ */
7
+ import { createRequire } from 'node:module';
8
+ import { posix } from 'node:path';
9
+ const require = createRequire(import.meta.url);
10
+ let compiler;
11
+ const typescript = () => (compiler ??= require('typescript'));
12
+ /** Get the text of a literal or the static prefix of a route template. */
13
+ function routeText(node) {
14
+ const ts = typescript();
15
+ if (!node)
16
+ return undefined;
17
+ if (ts.isJsxExpression(node))
18
+ return routeText(node.expression);
19
+ if (ts.isStringLiteralLike(node))
20
+ return node.text;
21
+ if (ts.isTemplateExpression(node))
22
+ return `${node.head.text}*`;
23
+ return undefined;
24
+ }
25
+ /** Parse each file once, resolving only relative source imports we actually have. */
26
+ function sourceIndex(files) {
27
+ const ts = typescript();
28
+ const trees = new Map(files.map((f) => [
29
+ f.path,
30
+ ts.createSourceFile(f.path, f.content, ts.ScriptTarget.Latest, true),
31
+ ]));
32
+ const imports = new Map();
33
+ const dependencies = new Map();
34
+ for (const [file, tree] of trees) {
35
+ const names = new Map(), deps = new Set();
36
+ const resolve = (text) => {
37
+ if (!text.startsWith('.'))
38
+ return undefined;
39
+ const base = posix.resolve(posix.dirname(file), text);
40
+ return [
41
+ base,
42
+ base.replace(/\.[cm]?jsx?$/, '.ts'),
43
+ base.replace(/\.[cm]?jsx?$/, '.tsx'),
44
+ ...['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx'].map((ext) => base + ext),
45
+ ].find((p) => trees.has(p));
46
+ };
47
+ const visit = (node) => {
48
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
49
+ const target = resolve(node.moduleSpecifier.text);
50
+ if (target) {
51
+ deps.add(target);
52
+ const clause = node.importClause;
53
+ if (clause?.name)
54
+ names.set(clause.name.text, target);
55
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings))
56
+ for (const n of clause.namedBindings.elements)
57
+ names.set(n.name.text, target);
58
+ }
59
+ }
60
+ if (ts.isExportDeclaration(node) &&
61
+ node.moduleSpecifier &&
62
+ ts.isStringLiteral(node.moduleSpecifier)) {
63
+ const target = resolve(node.moduleSpecifier.text);
64
+ if (target)
65
+ deps.add(target);
66
+ }
67
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
68
+ const text = routeText(node.arguments[0]), target = text && resolve(text);
69
+ if (target)
70
+ deps.add(target);
71
+ }
72
+ ts.forEachChild(node, visit);
73
+ };
74
+ visit(tree);
75
+ imports.set(file, names);
76
+ dependencies.set(file, deps);
77
+ }
78
+ return { trees, imports, dependencies };
79
+ }
80
+ /** The explicit routes a module declares; unknown framework shapes remain unindexed. */
81
+ function routeFiles(files) {
82
+ const ts = typescript();
83
+ const { trees, imports, dependencies } = sourceIndex(files);
84
+ const out = [];
85
+ const closure = (file, seen) => {
86
+ if (seen.has(file))
87
+ return;
88
+ seen.add(file);
89
+ for (const dep of dependencies.get(file) ?? [])
90
+ closure(dep, seen);
91
+ };
92
+ for (const [file, tree] of trees) {
93
+ const add = (path, element) => {
94
+ const route = routeText(path);
95
+ // Relative and nested declarations need the router's own resolution. Do not guess it.
96
+ if (!route?.startsWith('/') || !element)
97
+ return;
98
+ const selected = new Set([file]);
99
+ const refs = (node) => {
100
+ if (ts.isIdentifier(node)) {
101
+ const target = imports.get(file)?.get(node.text);
102
+ if (target)
103
+ closure(target, selected);
104
+ }
105
+ ts.forEachChild(node, refs);
106
+ };
107
+ refs(element);
108
+ // A local component, namespace or lazy binding cannot be resolved narrowly here.
109
+ if (selected.size === 1)
110
+ closure(file, selected);
111
+ out.push({ route, files: selected });
112
+ };
113
+ const visit = (node) => {
114
+ if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
115
+ const attrs = node.attributes.properties.filter(ts.isJsxAttribute);
116
+ const path = attrs.find((a) => a.name.getText(tree) === 'path')?.initializer;
117
+ const element = attrs.find((a) => /^(element|component|Component)$/.test(a.name.getText(tree)))?.initializer;
118
+ add(path, element);
119
+ }
120
+ if (ts.isObjectLiteralExpression(node)) {
121
+ const props = node.properties.filter(ts.isPropertyAssignment);
122
+ add(props.find((p) => p.name.getText(tree).replace(/['"]/g, '') === 'path')?.initializer, props.find((p) => /^(element|component|Component)$/.test(p.name.getText(tree).replace(/['"]/g, '')))?.initializer);
123
+ }
124
+ ts.forEachChild(node, visit);
125
+ };
126
+ visit(tree);
127
+ }
128
+ return out;
129
+ }
130
+ /** Goto targets in a spec and its supplied helpers. Unknown targets force a conservative run. */
131
+ function specRoutes(spec, index) {
132
+ const ts = typescript();
133
+ const { trees, dependencies } = index;
134
+ const seen = new Set(), routes = [];
135
+ let unknown = false;
136
+ const read = (file) => {
137
+ if (seen.has(file))
138
+ return;
139
+ seen.add(file);
140
+ const tree = trees.get(file);
141
+ if (!tree) {
142
+ unknown = true;
143
+ return;
144
+ }
145
+ const visit = (node) => {
146
+ if (ts.isCallExpression(node) &&
147
+ ts.isPropertyAccessExpression(node.expression) &&
148
+ node.expression.name.text === 'goto') {
149
+ const route = routeText(node.arguments[0]);
150
+ if (route?.startsWith('/'))
151
+ routes.push(route.split('?')[0].split('#')[0]);
152
+ else
153
+ unknown = true;
154
+ }
155
+ ts.forEachChild(node, visit);
156
+ };
157
+ visit(tree);
158
+ for (const dep of dependencies.get(file) ?? [])
159
+ read(dep);
160
+ };
161
+ read(spec.path);
162
+ return { routes, unknown: unknown || routes.length === 0 };
163
+ }
164
+ /** Compare route patterns for selection only, never for a correctness claim. */
165
+ function routeOverlaps(a, b) {
166
+ const left = a.split('/').filter(Boolean), right = b.split('/').filter(Boolean);
167
+ const n = Math.max(left.length, right.length);
168
+ for (let i = 0; i < n; i++) {
169
+ if (left[i] === '*' || right[i] === '*')
170
+ return true;
171
+ if (!left[i] || !right[i])
172
+ return false;
173
+ if (!left[i].startsWith(':') && !right[i].startsWith(':') && left[i] !== right[i])
174
+ return false;
175
+ }
176
+ return true;
177
+ }
178
+ /**
179
+ * Prioritize spec files potentially affected by writes. Every file still runs: a click or
180
+ * a helper can navigate somewhere a literal-goto index cannot see. This index never excludes work.
181
+ * @param project - Caller-supplied source and runner inventory.
182
+ * @param changed - Absolute source paths changed since the previous diagnosis.
183
+ * @returns Spec files to run, with their rendering-source context.
184
+ */
185
+ export function selectFeedbackSpecs(project, changed) {
186
+ if (project.specs.length === 0)
187
+ return [];
188
+ const routes = routeFiles(project.sources);
189
+ const affected = routes.filter((r) => changed.some((p) => r.files.has(p)));
190
+ const index = sourceIndex([...project.sources, ...project.specs]);
191
+ return project.specs
192
+ .map((spec) => {
193
+ const targets = specRoutes(spec, index);
194
+ const hits = affected.filter((r) => targets.routes.some((path) => routeOverlaps(r.route, path)));
195
+ return {
196
+ spec,
197
+ priority: hits.length,
198
+ sourceFiles: [
199
+ ...new Set(hits.length
200
+ ? hits.flatMap((r) => [...r.files]).filter((p) => changed.includes(p))
201
+ : changed),
202
+ ],
203
+ };
204
+ })
205
+ .sort((a, b) => b.priority - a.priority);
206
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Native Playwright diagnosis while an app is being written.
3
+ *
4
+ * @module
5
+ */
6
+ import type { PlaywrightReportStats } from './playwright-report.js';
7
+ /** A source file already read by the caller. Paths are absolute. */
8
+ export interface FeedbackSource {
9
+ path: string;
10
+ content: string;
11
+ }
12
+ /** One supplied or scaffolded spec, with its normal, locally targeted runner command. */
13
+ export interface FeedbackSpec extends FeedbackSource {
14
+ command: string;
15
+ cwd: string;
16
+ }
17
+ /** The inputs the host already knows; no suite discovery or vendor selection in the scheduler. */
18
+ export interface FeedbackProject {
19
+ /** Base for relative source-change paths, already resolved by the host. */
20
+ root?: string;
21
+ sources: FeedbackSource[];
22
+ specs: FeedbackSpec[];
23
+ }
24
+ /** A native failure, never a static prediction or a completed-suite proof. */
25
+ export interface PlaywrightFeedbackFailure {
26
+ spec: string;
27
+ line: number;
28
+ error: string;
29
+ /** The native runner's locator or assertion, verbatim apart from redaction. */
30
+ locator: string;
31
+ /** Playwright's own failure-time accessibility snapshot, when recorded. */
32
+ rendered?: string;
33
+ /** Route render dependencies affected by this write, or the triggering source paths. */
34
+ sourceFiles: string[];
35
+ }
36
+ /** A diagnosis of one spec file at one source revision. A pass is not whole-suite proof. */
37
+ export interface PlaywrightFeedbackResult {
38
+ spec: string;
39
+ revision: number;
40
+ durationMs: number;
41
+ status: 'passed' | 'failed' | 'unavailable';
42
+ failures: PlaywrightFeedbackFailure[];
43
+ /** Native counts for this one diagnostic selection, never whole-suite proof. */
44
+ nativeStats?: PlaywrightReportStats;
45
+ reason?: string;
46
+ }
47
+ /** Host operations for the same scheduler in a CLI process or a remote sandbox. */
48
+ export interface PlaywrightFeedbackOptions {
49
+ load: () => Promise<FeedbackProject>;
50
+ run: (spec: FeedbackSpec, sources: string[], revision: number, emit: (result: PlaywrightFeedbackResult) => void) => Promise<PlaywrightFeedbackResult>;
51
+ /** Only current native failures are delivered. Passing apps receive no note. */
52
+ onFailures?: (result: PlaywrightFeedbackResult) => void;
53
+ /** Operational state is separate from defect feedback. */
54
+ onResult?: (result: PlaywrightFeedbackResult) => void;
55
+ debounceMs?: number;
56
+ }
57
+ /** A nonblocking source-change subscription. */
58
+ export interface PlaywrightFeedback {
59
+ changed: (paths: readonly string[]) => void;
60
+ takeFailures: () => PlaywrightFeedbackResult[];
61
+ /** Wait only in tests or shutdown; file writes and deploys must never await this. */
62
+ settled: () => Promise<void>;
63
+ close: () => Promise<void>;
64
+ }
65
+ //# sourceMappingURL=playwright-feedback-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-feedback-types.d.ts","sourceRoot":"","sources":["../src/playwright-feedback-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAEnE,oEAAoE;AACpE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,yFAAyF;AACzF,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,kGAAkG;AAClG,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,KAAK,EAAE,YAAY,EAAE,CAAA;CACtB;AAED,8EAA8E;AAC9E,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAA;IACf,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wFAAwF;IACxF,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;AAED,4FAA4F;AAC5F,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAA;IAC3C,QAAQ,EAAE,yBAAyB,EAAE,CAAA;IACrC,gFAAgF;IAChF,WAAW,CAAC,EAAE,qBAAqB,CAAA;IACnC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,mFAAmF;AACnF,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAA;IACpC,GAAG,EAAE,CACH,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,MAAM,EAAE,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,KAC7C,OAAO,CAAC,wBAAwB,CAAC,CAAA;IACtC,gFAAgF;IAChF,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,CAAA;IACvD,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,CAAA;IACrD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,gDAAgD;AAChD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAA;IAC3C,YAAY,EAAE,MAAM,wBAAwB,EAAE,CAAA;IAC9C,qFAAqF;IACrF,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import type { FeedbackSource, FeedbackSpec, PlaywrightFeedback, PlaywrightFeedbackOptions, PlaywrightFeedbackResult } from './playwright-feedback-types.js';
2
+ import type { ExecutionBackend } from './types.js';
3
+ /**
4
+ * Whether a source change can alter markup, copy, styling or local runner configuration.
5
+ * @param path - The path reported by the host's file-change subscription.
6
+ * @returns True for app source and data formats; never a correctness judgment.
7
+ */
8
+ export declare function isPlaywrightFeedbackSource(path: string): boolean;
9
+ /**
10
+ * Subscribe to writes without making them wait for browser work.
11
+ * @param options - Source loader, native runner and delivery callbacks.
12
+ * @returns A source-change subscription; settle/close are for tests and shutdown only.
13
+ */
14
+ export declare function createPlaywrightFeedback(options: PlaywrightFeedbackOptions): PlaywrightFeedback;
15
+ /**
16
+ * Read explicit source roots and supplied helper/spec paths in one backend round trip.
17
+ * The standard filesystem and TypeScript parser own traversal/syntax; this is transport batching.
18
+ * @param backend - Local or sandbox execution backend.
19
+ * @param sourceRoots - App source directories, absolute.
20
+ * @param suiteFiles - Supplied spec/helper paths, absolute.
21
+ * @returns Existing, bounded text sources. Missing/unsupported files are ignored.
22
+ */
23
+ export declare function readFeedbackSources(backend: ExecutionBackend, sourceRoots: readonly string[], suiteFiles: readonly string[]): Promise<FeedbackSource[]>;
24
+ /**
25
+ * Run ONE spec's native command. Callers supply the correctly configured local command and
26
+ * append these runner flags at composition time (rather than modifying shell text here).
27
+ * Native failures are diagnosis, never proof that a complete supplied suite passed.
28
+ * @param backend - Existing execution backend.
29
+ * @param spec - The supplied spec and its normal per-file command.
30
+ * @param sourceFiles - Triggering component files, known to the write/index.
31
+ * @param revision - Source revision the scheduler will validate before delivery.
32
+ * @param options - Output location, command composition and a bounded backend budget.
33
+ * @returns The actual native outcome and actionable failures only.
34
+ */
35
+ export declare function runPlaywrightFeedback(backend: ExecutionBackend, spec: FeedbackSpec, sourceFiles: string[], revision: number, options: {
36
+ command: (flags: string[]) => string;
37
+ scratchRoot?: string;
38
+ timeoutMs?: number;
39
+ emit?: (result: PlaywrightFeedbackResult) => void;
40
+ pollMs?: number;
41
+ }): Promise<PlaywrightFeedbackResult>;
42
+ //# sourceMappingURL=playwright-feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-feedback.d.ts","sourceRoot":"","sources":["../src/playwright-feedback.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAEV,cAAc,EACd,YAAY,EACZ,kBAAkB,EAElB,yBAAyB,EACzB,wBAAwB,EACzB,MAAM,gCAAgC,CAAA;AAEvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAGlD;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,CAyG/F;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,gBAAgB,EACzB,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,OAAO,CAAC,cAAc,EAAE,CAAC,CAkB3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,EAAE,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IACP,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,CAAA;IACpC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,CAAA;IACjD,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GACA,OAAO,CAAC,wBAAwB,CAAC,CAwInC"}
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Run the project's real Playwright files while an agent keeps writing.
3
+ *
4
+ * The native runner, JSON reporter and error-context snapshot already answer what
5
+ * failed and what rendered. The small scheduler only coalesces writes, prioritizes
6
+ * route-related spec files, runs one at a time, and discards stale revisions.
7
+ * No correctness inference, foreground barrier, extra browser or passing-run note.
8
+ * @module
9
+ */
10
+ import { randomUUID } from 'node:crypto';
11
+ import { posix } from 'node:path';
12
+ import { feedbackReporterSource, nativeFeedbackFailures } from './playwright-feedback-reporter.js';
13
+ import { selectFeedbackSpecs } from './playwright-feedback-selection.js';
14
+ import { summarizePlaywrightReport } from './playwright-report.js';
15
+ import { redactSecrets, shellQuote } from './utilities.js';
16
+ /**
17
+ * Whether a source change can alter markup, copy, styling or local runner configuration.
18
+ * @param path - The path reported by the host's file-change subscription.
19
+ * @returns True for app source and data formats; never a correctness judgment.
20
+ */
21
+ export function isPlaywrightFeedbackSource(path) {
22
+ return /\.(?:[cm]?[jt]sx?|vue|svelte|html?|css|json|mdx?)$/i.test(path);
23
+ }
24
+ /**
25
+ * Subscribe to writes without making them wait for browser work.
26
+ * @param options - Source loader, native runner and delivery callbacks.
27
+ * @returns A source-change subscription; settle/close are for tests and shutdown only.
28
+ */
29
+ export function createPlaywrightFeedback(options) {
30
+ let revision = 0, timer, active;
31
+ let stopped = false;
32
+ const changed = new Set(), waiting = [];
33
+ const delivered = new Map();
34
+ const failing = new Set();
35
+ const start = () => {
36
+ if (active || stopped || changed.size === 0)
37
+ return;
38
+ const version = revision, paths = [...changed];
39
+ changed.clear();
40
+ active = (async () => {
41
+ let project;
42
+ try {
43
+ project = await options.load();
44
+ }
45
+ catch (_error) {
46
+ return;
47
+ }
48
+ if (stopped || version !== revision)
49
+ return;
50
+ const sourcePaths = project.root ? paths.map((p) => posix.resolve(project.root, p)) : paths;
51
+ // The native failures already name the files to rerun first after an edit.
52
+ const selected = selectFeedbackSpecs(project, sourcePaths).sort((a, b) => Number(failing.has(b.spec.path)) - Number(failing.has(a.spec.path)));
53
+ for (const { spec, sourceFiles } of selected) {
54
+ if (stopped || version !== revision)
55
+ break;
56
+ const emit = (result) => {
57
+ if (stopped || version !== revision || result.status !== 'failed')
58
+ return;
59
+ failing.add(spec.path);
60
+ const identities = delivered.get(spec.path) ?? new Set();
61
+ const failures = result.failures.filter((f) => {
62
+ const identity = JSON.stringify(f);
63
+ if (identities.has(identity))
64
+ return false;
65
+ identities.add(identity);
66
+ return true;
67
+ });
68
+ delivered.set(spec.path, identities);
69
+ if (!failures.length)
70
+ return;
71
+ const fresh = { ...result, failures };
72
+ waiting.push(fresh);
73
+ options.onFailures?.(fresh);
74
+ };
75
+ let result;
76
+ try {
77
+ result = await options.run(spec, sourceFiles, version, emit);
78
+ }
79
+ catch (_error) {
80
+ continue;
81
+ } // runner availability is not a markup defect
82
+ if (stopped || version !== revision)
83
+ break;
84
+ options.onResult?.(result);
85
+ if (result.status === 'passed')
86
+ failing.delete(spec.path);
87
+ if (result.status !== 'failed')
88
+ delivered.delete(spec.path);
89
+ emit(result);
90
+ }
91
+ })().finally(() => {
92
+ active = undefined;
93
+ if (!stopped && changed.size && !timer)
94
+ schedule();
95
+ });
96
+ };
97
+ const schedule = () => {
98
+ if (timer)
99
+ clearTimeout(timer);
100
+ timer = setTimeout(() => {
101
+ timer = undefined;
102
+ start();
103
+ }, options.debounceMs ?? 250);
104
+ timer.unref?.();
105
+ };
106
+ const settled = async () => {
107
+ while (timer || active || (!stopped && changed.size)) {
108
+ if (timer) {
109
+ clearTimeout(timer);
110
+ timer = undefined;
111
+ start();
112
+ }
113
+ if (active)
114
+ await active;
115
+ else if (!stopped && changed.size)
116
+ start();
117
+ }
118
+ };
119
+ return {
120
+ changed(paths) {
121
+ if (stopped || paths.length === 0)
122
+ return;
123
+ revision++;
124
+ for (const path of paths)
125
+ changed.add(path);
126
+ waiting.length = 0;
127
+ // A queued diagnosis can be invalidated before its host delivers it. A
128
+ // new revision must be allowed to report the same still-failing locator.
129
+ delivered.clear();
130
+ schedule();
131
+ },
132
+ takeFailures() {
133
+ return waiting.splice(0).filter((r) => r.revision === revision);
134
+ },
135
+ settled,
136
+ async close() {
137
+ stopped = true;
138
+ if (timer)
139
+ clearTimeout(timer);
140
+ timer = undefined;
141
+ changed.clear();
142
+ waiting.length = 0;
143
+ if (active)
144
+ await active;
145
+ },
146
+ };
147
+ }
148
+ /**
149
+ * Read explicit source roots and supplied helper/spec paths in one backend round trip.
150
+ * The standard filesystem and TypeScript parser own traversal/syntax; this is transport batching.
151
+ * @param backend - Local or sandbox execution backend.
152
+ * @param sourceRoots - App source directories, absolute.
153
+ * @param suiteFiles - Supplied spec/helper paths, absolute.
154
+ * @returns Existing, bounded text sources. Missing/unsupported files are ignored.
155
+ */
156
+ export async function readFeedbackSources(backend, sourceRoots, suiteFiles) {
157
+ if (sourceRoots.length === 0 && suiteFiles.length === 0)
158
+ return [];
159
+ const program = `const fs=require('node:fs'),p=require('node:path');const seen=new Set(),out=[];let bytes=0;
160
+ const read=f=>{if(seen.has(f)||out.length>=1000||bytes>=4000000)return;seen.add(f);try{const s=fs.lstatSync(f);if(s.isSymbolicLink())return;if(s.isDirectory()){for(const n of fs.readdirSync(f))if(!['node_modules','.git','dist','build'].includes(n))read(p.join(f,n));}else if(/\\.[cm]?[jt]sx?$/.test(f)&&s.size<=250000){const content=fs.readFileSync(f,'utf8');bytes+=content.length;out.push({path:f,content});}}catch{}};
161
+ for(const f of ${JSON.stringify([...sourceRoots, ...suiteFiles])})read(f);process.stdout.write(JSON.stringify(out));`;
162
+ const result = await backend.run(`node -e ${shellQuote(program)}`, { timeout: 10_000 });
163
+ if (result.exitCode !== 0)
164
+ return [];
165
+ try {
166
+ const value = JSON.parse(result.stdout);
167
+ return Array.isArray(value)
168
+ ? value.filter((f) => f && typeof f.path === 'string' && typeof f.content === 'string')
169
+ : [];
170
+ }
171
+ catch (_error) {
172
+ return [];
173
+ }
174
+ }
175
+ /**
176
+ * Run ONE spec's native command. Callers supply the correctly configured local command and
177
+ * append these runner flags at composition time (rather than modifying shell text here).
178
+ * Native failures are diagnosis, never proof that a complete supplied suite passed.
179
+ * @param backend - Existing execution backend.
180
+ * @param spec - The supplied spec and its normal per-file command.
181
+ * @param sourceFiles - Triggering component files, known to the write/index.
182
+ * @param revision - Source revision the scheduler will validate before delivery.
183
+ * @param options - Output location, command composition and a bounded backend budget.
184
+ * @returns The actual native outcome and actionable failures only.
185
+ */
186
+ export async function runPlaywrightFeedback(backend, spec, sourceFiles, revision, options) {
187
+ const started = Date.now();
188
+ const dir = posix.join(options.scratchRoot ?? posix.join(backend.projectRoot, '.mol', 'test-feedback'), randomUUID());
189
+ const reportPath = posix.join(dir, 'report.json');
190
+ const reporterPath = posix.join(dir, 'feedback-reporter.cjs');
191
+ const eventsPath = posix.join(dir, 'failures.jsonl');
192
+ const timeout = options.timeoutMs ?? 120_000;
193
+ const base = { spec: spec.path, revision, failures: [] };
194
+ const unavailable = (reason) => ({
195
+ ...base,
196
+ status: 'unavailable',
197
+ durationMs: Date.now() - started,
198
+ reason,
199
+ });
200
+ const native = options.command([
201
+ `--reporter=${shellQuote(reporterPath + ',json')}`,
202
+ '--workers=1',
203
+ '--retries=0',
204
+ `--global-timeout=${timeout}`,
205
+ `--output=${shellQuote(posix.join(dir, 'results'))}`,
206
+ ]);
207
+ // The standard OS priority keeps background diagnosis behind the dev server
208
+ // and builds when they share one CPU. Playwright owns its global deadline.
209
+ const command = `export PLAYWRIGHT_JSON_OUTPUT_FILE=${shellQuote(reportPath)} MOL_TEST_FEEDBACK_EVENTS=${shellQuote(eventsPath)}; nice -n 10 sh -c ${shellQuote(native)}`;
210
+ let poll, reading, done = false;
211
+ const emitted = new Set();
212
+ const nativeFailures = new Map();
213
+ const readEvents = async () => {
214
+ try {
215
+ const text = await backend.readFile(eventsPath);
216
+ for (const failure of nativeFeedbackFailures(text, sourceFiles)) {
217
+ const identity = JSON.stringify(failure);
218
+ nativeFailures.set(identity, failure);
219
+ if (emitted.has(identity))
220
+ continue;
221
+ emitted.add(identity);
222
+ options.emit?.({
223
+ ...base,
224
+ failures: [failure],
225
+ status: 'failed',
226
+ durationMs: Date.now() - started,
227
+ });
228
+ }
229
+ }
230
+ catch (_error) {
231
+ /* the first failure has not arrived yet */
232
+ }
233
+ };
234
+ const tick = () => {
235
+ if (done)
236
+ return;
237
+ reading = readEvents().finally(() => {
238
+ if (!done) {
239
+ poll = setTimeout(tick, options.pollMs ?? 1000);
240
+ poll.unref?.();
241
+ }
242
+ });
243
+ };
244
+ try {
245
+ await backend.writeFile(reporterPath, feedbackReporterSource());
246
+ if (options.emit)
247
+ tick();
248
+ const result = await backend.run(command, {
249
+ cwd: spec.cwd,
250
+ timeout: timeout + 10_000,
251
+ budgetMs: timeout + 5_000,
252
+ });
253
+ done = true;
254
+ if (poll)
255
+ clearTimeout(poll);
256
+ if (reading)
257
+ await reading;
258
+ await readEvents();
259
+ let reportText;
260
+ try {
261
+ reportText = await backend.readFile(reportPath);
262
+ }
263
+ catch (_error) {
264
+ return unavailable(`the native runner produced no JSON report: ${redactSecrets((result.stderr || result.stdout).trim().slice(0, 2000))}`);
265
+ }
266
+ const report = summarizePlaywrightReport(reportText, {
267
+ path: reportPath,
268
+ sanitize: redactSecrets,
269
+ });
270
+ if (report.status !== 'complete' ||
271
+ !report.stats ||
272
+ report.stats.expected + report.stats.unexpected + report.stats.flaky === 0 ||
273
+ report.stats.skipped > 0) {
274
+ let errors = [];
275
+ try {
276
+ errors = JSON.parse(reportText).errors ?? [];
277
+ }
278
+ catch (_error) {
279
+ /* the native summary already records malformed JSON */
280
+ }
281
+ const detail = errors.map((e) => e.message ?? '').join('\n') ||
282
+ result.stderr ||
283
+ report.reason ||
284
+ 'zero executed tests or skipped tests';
285
+ return {
286
+ ...unavailable(`the native runner did not complete every selected test: ${redactSecrets(detail.slice(0, 2000))}`),
287
+ ...(report.stats ? { nativeStats: report.stats } : {}),
288
+ };
289
+ }
290
+ base.failures = [...nativeFailures.values()];
291
+ if (base.failures.length)
292
+ return {
293
+ ...base,
294
+ nativeStats: report.stats,
295
+ status: 'failed',
296
+ durationMs: Date.now() - started,
297
+ };
298
+ if (result.exitCode !== 0 || report.stats.unexpected || report.stats.flaky)
299
+ return unavailable('the native run failed outside a locator assertion');
300
+ return {
301
+ ...base,
302
+ nativeStats: report.stats,
303
+ status: 'passed',
304
+ durationMs: Date.now() - started,
305
+ };
306
+ }
307
+ finally {
308
+ done = true;
309
+ if (poll)
310
+ clearTimeout(poll);
311
+ if (reading)
312
+ await reading;
313
+ // Only this run's private generated files; the supplied suite and its outputs stay untouched.
314
+ await backend
315
+ .run(`node -e ${shellQuote(`require('node:fs').rmSync(${JSON.stringify(dir)},{recursive:true,force:true})`)}`, { timeout: 5_000 })
316
+ .catch(() => undefined);
317
+ }
318
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.16.8",
3
+ "version": "1.17.0",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@molecule/api-ai": "1.6.0",
35
+ "@playwright/test": "1.64.1",
35
36
  "@types/node": "26.6.5",
36
37
  "esbuild": "0.28.2",
37
38
  "typescript": "6.0.3",
@@ -46,5 +47,8 @@
46
47
  "bugs": "https://github.com/molecule-dev/molecule/issues",
47
48
  "publishConfig": {
48
49
  "access": "public"
50
+ },
51
+ "dependencies": {
52
+ "typescript": "6.0.3"
49
53
  }
50
54
  }