@continuum8032/playwright-ai-tools 0.2.0-oidc-bootstrap.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/dist/index.cjs ADDED
@@ -0,0 +1,2030 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ DeterministicAnalyzer: () => DeterministicAnalyzer,
34
+ FailureGrouper: () => FailureGrouper,
35
+ MemoryStore: () => MemoryStore,
36
+ OpenAIResponsesProvider: () => OpenAIResponsesProvider,
37
+ PostgresStore: () => PostgresStore,
38
+ SQLiteStore: () => SQLiteStore,
39
+ TemplateTestGenerationProvider: () => TemplateTestGenerationProvider,
40
+ analyzeFailures: () => analyzeFailures,
41
+ assertBrowserGenerationAllowed: () => assertBrowserGenerationAllowed,
42
+ benchmarkFailures: () => benchmarkFailures,
43
+ classifyFailure: () => classifyFailure,
44
+ enrichFailureWithArtifacts: () => enrichFailureWithArtifacts,
45
+ evaluatePatchPolicy: () => evaluatePatchPolicy,
46
+ generatePatchSuggestions: () => generatePatchSuggestions,
47
+ generatePlaywrightTestDraft: () => generatePlaywrightTestDraft,
48
+ htmlReport: () => htmlReport,
49
+ importLegacyRows: () => importLegacyRows,
50
+ isDestructiveManualCase: () => isDestructiveManualCase,
51
+ markdownReport: () => markdownReport,
52
+ mcpSummary: () => mcpSummary,
53
+ normalizePlaywrightJsonReport: () => normalizePlaywrightJsonReport,
54
+ parseTraceArtifact: () => parseTraceArtifact,
55
+ writePortableReport: () => writePortableReport
56
+ });
57
+ module.exports = __toCommonJS(index_exports);
58
+
59
+ // src/signature.ts
60
+ var import_node_crypto = require("crypto");
61
+ function normalizeErrorForSignature(error) {
62
+ return error.replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, "UUID").replace(/\d+/g, "N").replace(/(['"])(.*?)\1/g, (_match, quote, value) => {
63
+ const normalizedValue = value.replace(/\d+/g, "N");
64
+ return `${quote}${normalizedValue}${quote}`;
65
+ }).replace(/\/[\w./-]+/g, "PATH").slice(0, 500);
66
+ }
67
+ function failureSignature(failure) {
68
+ return (0, import_node_crypto.createHash)("sha256").update(normalizeErrorForSignature(failure.error)).digest("hex");
69
+ }
70
+ function readablePattern(error) {
71
+ const firstLine2 = error.split("\n")[0] || "Unknown failure";
72
+ return firstLine2.length > 120 ? `${firstLine2.slice(0, 117)}...` : firstLine2;
73
+ }
74
+ function extractSelectors(error) {
75
+ const selectors = /* @__PURE__ */ new Set();
76
+ const patterns = [
77
+ /locator\((['"`])(.+?)\1\)/g,
78
+ /selector\s+(['"`])(.+?)\1/g,
79
+ /(?:getByTestId|data-testid)[^\n'"`]*(['"`])(.+?)\1/g
80
+ ];
81
+ for (const pattern of patterns) {
82
+ for (const match of error.matchAll(pattern)) {
83
+ const value = match[2]?.trim();
84
+ if (value) {
85
+ selectors.add(value);
86
+ }
87
+ }
88
+ }
89
+ return [...selectors];
90
+ }
91
+ function extractUrls(error) {
92
+ const urls = /* @__PURE__ */ new Set();
93
+ for (const match of error.matchAll(/https?:\/\/[^\s)'"]+|\/api\/[^\s)'"]+/g)) {
94
+ urls.add(match[0]);
95
+ }
96
+ return [...urls];
97
+ }
98
+
99
+ // src/taxonomy.ts
100
+ function firstLine(text) {
101
+ return text.split("\n")[0] || text;
102
+ }
103
+ function evidence(failure) {
104
+ const items = [firstLine(failure.error)];
105
+ for (const item of failure.consoleErrors || []) {
106
+ items.push(`console:${item.level}:${item.text}`);
107
+ }
108
+ for (const item of failure.networkFailures || []) {
109
+ items.push(`network:${item.method || "GET"}:${item.status || item.errorText}:${item.url}`);
110
+ }
111
+ for (const item of failure.lastActions || []) {
112
+ items.push(`action:${item.name}${item.selector ? `:${item.selector}` : ""}`);
113
+ }
114
+ return items.filter(Boolean);
115
+ }
116
+ function evidenceRefs(failure) {
117
+ const refs = ["error"];
118
+ if (failure.consoleErrors?.length) refs.push("consoleErrors[0]");
119
+ if (failure.networkFailures?.length) refs.push("networkFailures[0]");
120
+ if (failure.lastActions?.length) refs.push("lastActions[-1]");
121
+ if (failure.pageSnapshot) refs.push("pageSnapshot");
122
+ if (failure.artifactRefs?.length || failure.artifacts?.length) refs.push("artifactRefs");
123
+ return refs;
124
+ }
125
+ function matchFailure(failure) {
126
+ const text = [
127
+ failure.error,
128
+ ...(failure.consoleErrors || []).map((item) => item.text),
129
+ ...(failure.networkFailures || []).map((item) => `${item.status || ""} ${item.errorText || ""} ${item.url}`)
130
+ ].join("\n").toLowerCase();
131
+ if (/strict mode violation|resolved to \d+ elements|locator\(.+\) resolved/.test(text)) {
132
+ return {
133
+ category: "selector",
134
+ risk: "medium",
135
+ side: "test_bug",
136
+ rootCause: "A locator is ambiguous or no longer points to the intended element.",
137
+ suggestions: ["Make the locator specific to the intended role, label, text, or test id."],
138
+ confidence: 94,
139
+ humanReview: false,
140
+ reason: "Playwright reported strict locator matching evidence."
141
+ };
142
+ }
143
+ if (/not receiving pointer events|intercepts pointer events|element is covered|element is detached|not enabled|not editable/.test(text)) {
144
+ return {
145
+ category: "interaction",
146
+ risk: "medium",
147
+ side: "test_bug",
148
+ rootCause: "The page changed the interaction state before Playwright could complete the action.",
149
+ suggestions: ["Wait for the actionable UI state and inspect overlays, disabled states, or animations."],
150
+ confidence: 88,
151
+ humanReview: true,
152
+ reason: "The failure describes an element actionability or interaction problem."
153
+ };
154
+ }
155
+ if (/screenshot|snapshot|pixel|visual|image comparison|tohavescreenshot/.test(text)) {
156
+ return {
157
+ category: "visual",
158
+ risk: "medium",
159
+ side: "unknown",
160
+ rootCause: "A visual assertion differs from the stored baseline or expected snapshot.",
161
+ suggestions: ["Inspect the screenshot diff and update the baseline only if the product change is intended."],
162
+ confidence: 86,
163
+ humanReview: true,
164
+ reason: "The error contains visual comparison evidence."
165
+ };
166
+ }
167
+ if (/no test data|fixture|seed|factory|test data|not found for .*user|missing data/.test(text)) {
168
+ return {
169
+ category: "test_data",
170
+ risk: "medium",
171
+ side: "test_bug",
172
+ rootCause: "The test appears to depend on missing or inconsistent data setup.",
173
+ suggestions: ["Make the data fixture explicit and validate setup before the user journey starts."],
174
+ confidence: 84,
175
+ humanReview: false,
176
+ reason: "The failure references missing seeded data or fixture setup."
177
+ };
178
+ }
179
+ if (/net::err_|econnrefused|enotfound|timed out connecting|500|502|503|504|internal server error|service unavailable|fetch failed/.test(text)) {
180
+ return {
181
+ category: "environment",
182
+ risk: "high",
183
+ side: "env_issue",
184
+ rootCause: "A backend, network, or environment dependency failed during the test.",
185
+ suggestions: ["Check dependency health, request logs, and CI environment configuration for the failing endpoint."],
186
+ confidence: 86,
187
+ humanReview: true,
188
+ reason: "Network or server failure evidence was found."
189
+ };
190
+ }
191
+ if (/typeerror|referenceerror|cannot read properties|is not defined|uncaught|unhandled/.test(text)) {
192
+ return {
193
+ category: "runtime",
194
+ risk: "high",
195
+ side: "app_bug",
196
+ rootCause: "The application or test runtime threw an exception during the scenario.",
197
+ suggestions: ["Inspect the console stack, failing page state, and recent runtime changes."],
198
+ confidence: 88,
199
+ humanReview: true,
200
+ reason: "Runtime exception evidence was found in the error or console output."
201
+ };
202
+ }
203
+ if (/timeout|waiting for locator|waiting for selector|tobevisible|to be visible|waiting for .*state/.test(text)) {
204
+ return {
205
+ category: "timing",
206
+ risk: "medium",
207
+ side: "test_bug",
208
+ rootCause: "A Playwright wait timed out before the expected element or state appeared.",
209
+ suggestions: [
210
+ "Verify the expected UI state is still correct.",
211
+ "Wait for the user-visible state with an assertion before acting.",
212
+ "Check whether navigation or data setup leaves the element hidden."
213
+ ],
214
+ confidence: 90,
215
+ humanReview: false,
216
+ reason: "Playwright reported a timeout while waiting for an expected state."
217
+ };
218
+ }
219
+ return {
220
+ category: "unknown",
221
+ risk: "high",
222
+ side: "unknown",
223
+ rootCause: "No deterministic taxonomy rule matched this failure.",
224
+ suggestions: ["Review trace, screenshot, console output, network output, and recent changes."],
225
+ confidence: 30,
226
+ humanReview: true,
227
+ reason: "No deterministic classifier matched the available evidence."
228
+ };
229
+ }
230
+ function analysisWithDefaults(analysis, failure, source = analysis.source || "unknown") {
231
+ const classified = matchFailure(failure);
232
+ const urls = extractUrls(failure.error);
233
+ return {
234
+ side: analysis.side || classified.side,
235
+ rootCause: analysis.rootCause || classified.rootCause,
236
+ evidence: analysis.evidence?.length ? analysis.evidence : evidence(failure),
237
+ reproductionSteps: analysis.reproductionSteps?.length ? analysis.reproductionSteps : failure.steps?.length ? failure.steps : ["Run the failing Playwright test."],
238
+ suspects: analysis.suspects || {
239
+ selectors: extractSelectors(failure.error),
240
+ pages: urls.filter((url) => !url.startsWith("/api/")),
241
+ endpoints: [
242
+ ...urls.filter((url) => url.startsWith("/api/")),
243
+ ...(failure.networkFailures || []).map((item) => item.url)
244
+ ]
245
+ },
246
+ fixSuggestions: analysis.fixSuggestions?.length ? analysis.fixSuggestions : classified.suggestions,
247
+ confidence: Number(analysis.confidence ?? classified.confidence),
248
+ category: analysis.category || classified.category,
249
+ risk: analysis.risk || classified.risk,
250
+ evidenceRefs: analysis.evidenceRefs?.length ? analysis.evidenceRefs : evidenceRefs(failure),
251
+ confidenceReasons: analysis.confidenceReasons?.length ? analysis.confidenceReasons : [classified.reason],
252
+ needsHumanReview: analysis.needsHumanReview ?? classified.humanReview,
253
+ suggestedFixes: analysis.suggestedFixes,
254
+ source,
255
+ tokenUsage: analysis.tokenUsage
256
+ };
257
+ }
258
+ function classifyFailure(failure) {
259
+ const matched = matchFailure(failure);
260
+ return analysisWithDefaults(
261
+ {
262
+ side: matched.side,
263
+ rootCause: matched.rootCause,
264
+ fixSuggestions: matched.suggestions,
265
+ confidence: matched.confidence,
266
+ category: matched.category,
267
+ risk: matched.risk,
268
+ needsHumanReview: matched.humanReview,
269
+ source: matched.category === "unknown" ? "unknown" : "deterministic"
270
+ },
271
+ failure,
272
+ matched.category === "unknown" ? "unknown" : "deterministic"
273
+ );
274
+ }
275
+
276
+ // src/analyzer.ts
277
+ function deterministicAnalysis(failure) {
278
+ const classified = classifyFailure(failure);
279
+ return classified.category === "unknown" ? null : classified;
280
+ }
281
+ function unknownAnalysis(failure) {
282
+ return analysisWithDefaults(
283
+ {
284
+ rootCause: "No deterministic pattern matched and no AI provider was configured.",
285
+ source: "unknown"
286
+ },
287
+ failure,
288
+ "unknown"
289
+ );
290
+ }
291
+ function contextQuery(failure) {
292
+ return [
293
+ failure.title,
294
+ failure.file,
295
+ ...extractSelectors(failure.error),
296
+ ...failure.steps || [],
297
+ failure.error.split("\n")[0]
298
+ ].filter(Boolean).join(" ").slice(0, 500);
299
+ }
300
+ function queryTokens(query) {
301
+ return [
302
+ ...new Set(
303
+ query.toLowerCase().split(/[^a-z0-9_-]+/i).map((item) => item.trim()).filter((item) => item.length > 2)
304
+ )
305
+ ];
306
+ }
307
+ function matchesAnyToken(value, tokens) {
308
+ const text = JSON.stringify(value).toLowerCase();
309
+ return tokens.some((token) => text.includes(token));
310
+ }
311
+ function memorySuggestion(record) {
312
+ const value = record.value || {};
313
+ const summary = typeof value.summary === "string" ? value.summary : typeof value.text === "string" ? value.text : JSON.stringify(value);
314
+ return summary.length > 240 ? `${summary.slice(0, 237)}...` : summary;
315
+ }
316
+ async function contextForFailure(store, failure, options) {
317
+ if (!store) {
318
+ return { memories: [], feedbackReasons: [], similarFailures: [], similarAnalyses: [] };
319
+ }
320
+ const query = contextQuery(failure);
321
+ const tokens = queryTokens(query);
322
+ const namespace = options.projectContext?.namespace || "default";
323
+ const limit = options.similarityLimit ?? 5;
324
+ const memories = [
325
+ ...await store.searchMemory({ namespace, limit: 100 }),
326
+ ...namespace === "default" ? [] : await store.searchMemory({ namespace: "default", limit: 100 })
327
+ ].filter((record) => matchesAnyToken(record, tokens)).slice(0, limit);
328
+ const feedback = (await store.searchFeedback({ limit: 100 })).filter((record) => matchesAnyToken(record, tokens)).slice(0, limit);
329
+ const similarFailures = (await store.findFailures({ limit: 100 })).filter((record) => matchesAnyToken(record, tokens)).slice(0, limit);
330
+ const similarAnalyses = (await Promise.all(similarFailures.map((item) => store.getCachedAnalysis(failureSignature(item))))).filter((item) => Boolean(item));
331
+ return {
332
+ memories,
333
+ feedbackReasons: feedback.map((record) => `feedback:${record.decision}${record.reason ? `:${record.reason}` : ""}`),
334
+ similarFailures,
335
+ similarAnalyses
336
+ };
337
+ }
338
+ function applyContext(analysis, context) {
339
+ const knownFixes = context.memories.filter((record) => ["known_fix", "knowledge", "known_issue", "owner"].includes(record.kind));
340
+ const suggestions = knownFixes.map(memorySuggestion).filter(Boolean);
341
+ const confidenceReasons = [
342
+ ...analysis.confidenceReasons,
343
+ ...knownFixes.map((record) => `memory:${record.kind}:${record.key}`),
344
+ ...context.feedbackReasons
345
+ ];
346
+ if (context.similarFailures.length > 0) {
347
+ confidenceReasons.push(
348
+ `history:${context.similarFailures.length} similar failure${context.similarFailures.length === 1 ? "" : "s"}`
349
+ );
350
+ }
351
+ const flakyFeedback = context.feedbackReasons.some((reason) => reason.startsWith("feedback:mark_flaky"));
352
+ return {
353
+ ...analysis,
354
+ side: flakyFeedback && analysis.side === "unknown" ? "flaky_test" : analysis.side,
355
+ fixSuggestions: [...analysis.fixSuggestions, ...suggestions],
356
+ evidence: analysis.evidence,
357
+ evidenceRefs: analysis.evidenceRefs,
358
+ confidenceReasons: [...new Set(confidenceReasons)],
359
+ needsHumanReview: analysis.needsHumanReview || flakyFeedback || knownFixes.some((record) => record.kind === "known_issue")
360
+ };
361
+ }
362
+ var DeterministicAnalyzer = class {
363
+ constructor(options = {}) {
364
+ this.provider = options.provider;
365
+ this.outputLanguage = options.outputLanguage || process.env.PW_AI_OUTPUT_LANGUAGE || "ru";
366
+ }
367
+ async analyzeFailure(failure, similarAnalyses = []) {
368
+ const deterministic = deterministicAnalysis(failure);
369
+ if (deterministic) {
370
+ return deterministic;
371
+ }
372
+ if (this.provider) {
373
+ const ai = await this.provider.analyzeFailure({
374
+ failure,
375
+ outputLanguage: this.outputLanguage,
376
+ similarAnalyses
377
+ });
378
+ return analysisWithDefaults(ai, failure, "ai");
379
+ }
380
+ return unknownAnalysis(failure);
381
+ }
382
+ async analyzeFailures(failures) {
383
+ return Promise.all(failures.map((failure) => this.analyzeFailure(failure)));
384
+ }
385
+ };
386
+ async function analyzeFailures(failures, options = {}) {
387
+ const outputLanguage = options.outputLanguage || process.env.PW_AI_OUTPUT_LANGUAGE || "ru";
388
+ const analyzer = new DeterministicAnalyzer({ provider: options.provider, outputLanguage });
389
+ const results = [];
390
+ for (const failure of failures) {
391
+ const signature = failureSignature(failure);
392
+ const cached = options.store ? await options.store.getCachedAnalysis(signature) : null;
393
+ if (cached) {
394
+ results.push(cached);
395
+ continue;
396
+ }
397
+ const context = await contextForFailure(options.store, failure, options);
398
+ const rawAnalysis = await analyzer.analyzeFailure(failure, context.similarAnalyses);
399
+ let analysis = applyContext(rawAnalysis, context);
400
+ if (options.store) {
401
+ await options.store.saveAnalysis(signature, analysis);
402
+ }
403
+ results.push(analysis);
404
+ }
405
+ return results;
406
+ }
407
+
408
+ // src/suggestions.ts
409
+ var import_node_fs = require("fs");
410
+ var import_node_path = __toESM(require("path"), 1);
411
+ function assertionCount(text) {
412
+ return (text.match(/\bexpect\s*\(/g) || []).length;
413
+ }
414
+ function onlyTimeoutChanged(oldSnippet, newSnippet) {
415
+ const oldNormalized = oldSnippet.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
416
+ const newNormalized = newSnippet.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
417
+ return oldNormalized === newNormalized && /timeout/i.test(`${oldSnippet}
418
+ ${newSnippet}`);
419
+ }
420
+ function testIdLocator(selector) {
421
+ const match = selector.match(/\[data-testid=['"]?([^'"\]]+)['"]?\]/i);
422
+ if (match) return `page.getByTestId("${match[1]}")`;
423
+ return `page.locator(${JSON.stringify(selector)})`;
424
+ }
425
+ function firstSelector(failure) {
426
+ return extractSelectors(failure.error)[0] || failure.lastActions?.find((action) => action.selector)?.selector;
427
+ }
428
+ function sourceLine(failure, options) {
429
+ if (!failure.line) return void 0;
430
+ const filePath = import_node_path.default.isAbsolute(failure.file) ? failure.file : import_node_path.default.resolve(options.workspaceRoot || process.cwd(), failure.file);
431
+ if (!(0, import_node_fs.existsSync)(filePath)) return void 0;
432
+ const lines = (0, import_node_fs.readFileSync)(filePath, "utf8").split(/\r?\n/);
433
+ return lines[failure.line - 1];
434
+ }
435
+ function evaluatePatchPolicy(suggestion) {
436
+ const oldAssertions = assertionCount(suggestion.oldSnippet);
437
+ const newAssertions = assertionCount(suggestion.newSnippet);
438
+ if (oldAssertions > 0 && newAssertions < oldAssertions) {
439
+ return {
440
+ ...suggestion,
441
+ policyDecision: "blocked",
442
+ reviewReason: "Blocked: patch weakens or removes an assertion."
443
+ };
444
+ }
445
+ if (/assertion removed|delete assertion|remove assertion/i.test(suggestion.newSnippet)) {
446
+ return {
447
+ ...suggestion,
448
+ policyDecision: "blocked",
449
+ reviewReason: "Blocked: patch appears to remove an assertion."
450
+ };
451
+ }
452
+ if (onlyTimeoutChanged(suggestion.oldSnippet, suggestion.newSnippet) && suggestion.evidence.length === 0) {
453
+ return {
454
+ ...suggestion,
455
+ policyDecision: "blocked",
456
+ reviewReason: "Blocked: timeout-only changes require supporting evidence."
457
+ };
458
+ }
459
+ if (/\btest\.skip\b|\breturn;\s*$|TODO:\s*remove check/i.test(suggestion.newSnippet)) {
460
+ return {
461
+ ...suggestion,
462
+ policyDecision: "blocked",
463
+ reviewReason: "Blocked: patch changes test intent instead of explaining the failure."
464
+ };
465
+ }
466
+ return {
467
+ ...suggestion,
468
+ policyDecision: "review",
469
+ reviewReason: suggestion.reviewReason || "Requires human approval; core never applies patches automatically."
470
+ };
471
+ }
472
+ function generatePatchSuggestions(failure, analysis, options = {}) {
473
+ const evidence2 = [...analysis.evidence || [], ...analysis.evidenceRefs || []].filter(Boolean);
474
+ const selector = firstSelector(failure);
475
+ const locator = selector ? testIdLocator(selector) : 'page.locator("<target>")';
476
+ const lastAction = failure.lastActions?.slice(-1)[0];
477
+ const oldSourceLine = sourceLine(failure, options);
478
+ let suggestion;
479
+ if (analysis.category === "selector") {
480
+ suggestion = {
481
+ filePath: failure.file,
482
+ oldSnippet: `await ${locator}.click();`,
483
+ newSnippet: `await ${locator}.filter({ hasText: "<expected text>" }).click();`,
484
+ explanation: "Narrow the locator to the intended element instead of relying on an ambiguous match.",
485
+ evidence: evidence2,
486
+ risk: analysis.risk,
487
+ policyDecision: "review",
488
+ reviewReason: ""
489
+ };
490
+ } else if (analysis.category === "environment") {
491
+ suggestion = {
492
+ filePath: failure.file,
493
+ oldSnippet: 'await page.goto("<page>");',
494
+ newSnippet: 'const response = await page.goto("<page>");\nawait expect(response, "page request should succeed").toBeOK();',
495
+ explanation: "Expose the failing backend or environment dependency before continuing with UI assertions.",
496
+ evidence: evidence2,
497
+ risk: analysis.risk,
498
+ policyDecision: "review",
499
+ reviewReason: ""
500
+ };
501
+ } else if (analysis.category === "runtime") {
502
+ suggestion = {
503
+ filePath: failure.file,
504
+ oldSnippet: 'await page.goto("<page>");',
505
+ newSnippet: 'const consoleErrors: string[] = [];\npage.on("console", (message) => message.type() === "error" && consoleErrors.push(message.text()));\nawait page.goto("<page>");',
506
+ explanation: "Capture runtime console evidence so the owner can fix the thrown exception with context.",
507
+ evidence: evidence2,
508
+ risk: analysis.risk,
509
+ policyDecision: "review",
510
+ reviewReason: ""
511
+ };
512
+ } else {
513
+ const actionLocator = lastAction?.selector ? testIdLocator(lastAction.selector) : locator;
514
+ suggestion = {
515
+ filePath: failure.file,
516
+ line: failure.line,
517
+ oldSnippet: oldSourceLine || `await ${actionLocator}.click();`,
518
+ newSnippet: oldSourceLine ? `${oldSourceLine.match(/^\s*/)?.[0] || ""}await expect(${actionLocator}).toBeVisible();
519
+ ${oldSourceLine}` : `await expect(${actionLocator}).toBeVisible();
520
+ await ${actionLocator}.click();`,
521
+ explanation: "Wait for the user-visible state before the action; do not increase timeouts without evidence.",
522
+ evidence: evidence2,
523
+ risk: analysis.risk,
524
+ policyDecision: "review",
525
+ reviewReason: ""
526
+ };
527
+ }
528
+ return [evaluatePatchPolicy(suggestion)];
529
+ }
530
+
531
+ // src/benchmark.ts
532
+ function roundMetric(value) {
533
+ return Number(value.toFixed(4));
534
+ }
535
+ function benchmarkFailures(fixtures) {
536
+ const seenSignatures = /* @__PURE__ */ new Set();
537
+ const failures = fixtures.map((fixture) => {
538
+ const analysis = classifyFailure(fixture);
539
+ const suggestions = generatePatchSuggestions(fixture, analysis);
540
+ const signature = failureSignature(fixture);
541
+ const cacheHit = seenSignatures.has(signature);
542
+ seenSignatures.add(signature);
543
+ const expectedCategory = fixture.expectedCategory || "unknown";
544
+ return {
545
+ title: fixture.title,
546
+ file: fixture.file,
547
+ expectedCategory,
548
+ actualCategory: analysis.category,
549
+ correct: expectedCategory === analysis.category,
550
+ signature,
551
+ cacheHit,
552
+ analysis,
553
+ suggestions
554
+ };
555
+ });
556
+ const total = failures.length;
557
+ const correct = failures.filter((failure) => failure.correct).length;
558
+ const unknown = failures.filter((failure) => failure.actualCategory === "unknown").length;
559
+ const cacheHits = failures.filter((failure) => failure.cacheHit).length;
560
+ const blocked = failures.reduce(
561
+ (count, failure) => count + failure.suggestions.filter((suggestion) => suggestion.policyDecision === "blocked").length,
562
+ 0
563
+ );
564
+ const review = failures.reduce(
565
+ (count, failure) => count + failure.suggestions.filter((suggestion) => suggestion.policyDecision === "review").length,
566
+ 0
567
+ );
568
+ return {
569
+ metrics: {
570
+ total,
571
+ correct,
572
+ accuracy: total ? roundMetric(correct / total) : 0,
573
+ unknown,
574
+ unknownRate: total ? roundMetric(unknown / total) : 0,
575
+ cacheHits,
576
+ cacheHitRate: total ? roundMetric(cacheHits / total) : 0,
577
+ suggestions: { blocked, review }
578
+ },
579
+ failures
580
+ };
581
+ }
582
+
583
+ // src/evidence.ts
584
+ var import_node_path3 = __toESM(require("path"), 1);
585
+
586
+ // src/trace.ts
587
+ var import_promises = require("fs/promises");
588
+ var import_node_zlib = require("zlib");
589
+ var import_node_path2 = __toESM(require("path"), 1);
590
+ function parseEvents(raw) {
591
+ const trimmed = raw.trim();
592
+ if (!trimmed) return [];
593
+ try {
594
+ const parsed = JSON.parse(trimmed);
595
+ if (Array.isArray(parsed)) return parsed;
596
+ if (Array.isArray(parsed.events)) return parsed.events;
597
+ if (Array.isArray(parsed.traceEvents)) return parsed.traceEvents;
598
+ return [parsed];
599
+ } catch {
600
+ return trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
601
+ }
602
+ }
603
+ function isZip(buffer, filePath) {
604
+ return import_node_path2.default.extname(filePath).toLowerCase() === ".zip" || buffer.readUInt32LE(0) === 67324752;
605
+ }
606
+ function isTextTraceEntry(name) {
607
+ const lower = name.toLowerCase();
608
+ if (lower.startsWith("resources/")) return false;
609
+ return lower.endsWith(".trace") || lower.endsWith(".network") || lower.endsWith(".json") || lower.endsWith(".jsonl") || lower.includes("trace") || lower.includes("network");
610
+ }
611
+ function findEndOfCentralDirectory(buffer) {
612
+ for (let index = buffer.length - 22; index >= 0; index -= 1) {
613
+ if (buffer.readUInt32LE(index) === 101010256) return index;
614
+ }
615
+ throw new Error("ZIP central directory not found in trace artifact");
616
+ }
617
+ function extractZipTexts(buffer) {
618
+ const eocd = findEndOfCentralDirectory(buffer);
619
+ const totalEntries = buffer.readUInt16LE(eocd + 10);
620
+ let offset = buffer.readUInt32LE(eocd + 16);
621
+ const texts = [];
622
+ for (let entry = 0; entry < totalEntries; entry += 1) {
623
+ if (buffer.readUInt32LE(offset) !== 33639248) break;
624
+ const method = buffer.readUInt16LE(offset + 10);
625
+ const compressedSize = buffer.readUInt32LE(offset + 20);
626
+ const nameLength = buffer.readUInt16LE(offset + 28);
627
+ const extraLength = buffer.readUInt16LE(offset + 30);
628
+ const commentLength = buffer.readUInt16LE(offset + 32);
629
+ const localOffset = buffer.readUInt32LE(offset + 42);
630
+ const name = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8");
631
+ offset += 46 + nameLength + extraLength + commentLength;
632
+ if (!isTextTraceEntry(name) || buffer.readUInt32LE(localOffset) !== 67324752) continue;
633
+ const localNameLength = buffer.readUInt16LE(localOffset + 26);
634
+ const localExtraLength = buffer.readUInt16LE(localOffset + 28);
635
+ const dataStart = localOffset + 30 + localNameLength + localExtraLength;
636
+ const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
637
+ if (method === 0) {
638
+ texts.push(compressed.toString("utf8"));
639
+ } else if (method === 8) {
640
+ texts.push((0, import_node_zlib.inflateRawSync)(compressed).toString("utf8"));
641
+ }
642
+ }
643
+ return texts;
644
+ }
645
+ async function readTraceTexts(filePath) {
646
+ const buffer = await (0, import_promises.readFile)(filePath);
647
+ if (isZip(buffer, filePath)) {
648
+ return extractZipTexts(buffer);
649
+ }
650
+ return [buffer.toString("utf8")];
651
+ }
652
+ function timestamp(event) {
653
+ return event.wallTime || event.timestamp || event.time;
654
+ }
655
+ function actionFromEvent(event) {
656
+ const name = event.apiName || event.method || event.action;
657
+ if (!name) return null;
658
+ return {
659
+ name: String(name),
660
+ selector: event.selector || event.params?.selector,
661
+ url: event.url || event.params?.url,
662
+ timestamp: timestamp(event),
663
+ pageId: event.pageId
664
+ };
665
+ }
666
+ function consoleFromEvent(event) {
667
+ const level = String(event.level || event.messageType || "").toLowerCase();
668
+ const text = event.text || event.message || event.args?.join?.(" ");
669
+ if (!text || !["error", "warning", "warn"].includes(level)) return null;
670
+ return {
671
+ level: level === "warn" ? "warning" : level,
672
+ text: String(text),
673
+ url: event.location?.url || event.url,
674
+ lineNumber: event.location?.lineNumber,
675
+ columnNumber: event.location?.columnNumber,
676
+ timestamp: timestamp(event)
677
+ };
678
+ }
679
+ function networkFromEvent(event) {
680
+ const snapshot = event.snapshot || event;
681
+ const request = snapshot.request || event.request || {};
682
+ const response = snapshot.response || event.response || {};
683
+ const url = request.url || event.url;
684
+ const status = Number(response.status ?? event.status ?? 0);
685
+ const errorText = event.errorText || snapshot.errorText || response.errorText;
686
+ if (!url || status < 400 && !errorText) return null;
687
+ return {
688
+ url: String(url),
689
+ method: request.method || event.method,
690
+ status: status || void 0,
691
+ statusText: response.statusText,
692
+ errorText,
693
+ timestamp: timestamp(event)
694
+ };
695
+ }
696
+ function trimLastActions(actions) {
697
+ return actions.slice(-10);
698
+ }
699
+ async function parseTraceArtifact(filePath) {
700
+ const texts = await readTraceTexts(filePath);
701
+ const events = texts.flatMap(parseEvents);
702
+ const summary = {
703
+ consoleErrors: [],
704
+ networkFailures: [],
705
+ lastActions: [],
706
+ artifactRefs: [{ type: "trace", path: filePath }]
707
+ };
708
+ for (const event of events) {
709
+ if (!event || typeof event !== "object") continue;
710
+ if (event.type === "context-options" || event.browserName || event.viewport) {
711
+ const { type: _type, ...context } = event;
712
+ summary.browserContext = { ...summary.browserContext || {}, ...context };
713
+ }
714
+ if (event.type === "before" || event.type === "action") {
715
+ const action = actionFromEvent(event);
716
+ if (action) summary.lastActions = trimLastActions([...summary.lastActions, action]);
717
+ }
718
+ if (event.type === "after" && event.result) {
719
+ summary.page = {
720
+ ...summary.page || {},
721
+ url: event.result.url || summary.page?.url,
722
+ title: event.result.title || summary.page?.title
723
+ };
724
+ }
725
+ const consoleEvent = consoleFromEvent(event);
726
+ if (consoleEvent) summary.consoleErrors.push(consoleEvent);
727
+ const networkEvent = networkFromEvent(event);
728
+ if (networkEvent) summary.networkFailures.push(networkEvent);
729
+ const snapshot = event.snapshot?.html || event.snapshot?.body || event.html;
730
+ if (event.type === "page-snapshot" && snapshot) {
731
+ summary.pageSnapshot = String(snapshot);
732
+ summary.page = { ...summary.page || {}, snapshot: summary.pageSnapshot };
733
+ }
734
+ }
735
+ return summary;
736
+ }
737
+
738
+ // src/evidence.ts
739
+ function isTraceArtifact(artifact) {
740
+ const haystack = `${artifact.type} ${artifact.path} ${artifact.contentType || ""}`.toLowerCase();
741
+ return haystack.includes("trace") || haystack.includes("jsonl") || haystack.includes("application/zip");
742
+ }
743
+ function resolveArtifactPath(artifactPath, root) {
744
+ if (import_node_path3.default.isAbsolute(artifactPath)) return artifactPath;
745
+ return import_node_path3.default.resolve(root || process.cwd(), artifactPath);
746
+ }
747
+ async function enrichFailureWithArtifacts(failure, options = {}) {
748
+ const artifacts = failure.artifacts || [];
749
+ const artifactRefs = [...failure.artifactRefs || [], ...artifacts];
750
+ let enriched = { ...failure, artifactRefs };
751
+ for (const artifact of artifacts.filter(isTraceArtifact)) {
752
+ let trace;
753
+ try {
754
+ trace = await parseTraceArtifact(resolveArtifactPath(artifact.path, options.artifactRoot));
755
+ } catch (error) {
756
+ enriched = {
757
+ ...enriched,
758
+ artifactRefs: [
759
+ ...artifactRefs,
760
+ {
761
+ ...artifact,
762
+ metadata: {
763
+ ...artifact.metadata || {},
764
+ parseError: error?.message || String(error)
765
+ }
766
+ }
767
+ ]
768
+ };
769
+ continue;
770
+ }
771
+ enriched = {
772
+ ...enriched,
773
+ trace,
774
+ browserContext: trace.browserContext || enriched.browserContext,
775
+ page: trace.page || enriched.page,
776
+ pageSnapshot: trace.pageSnapshot || enriched.pageSnapshot,
777
+ consoleErrors: [...enriched.consoleErrors || [], ...trace.consoleErrors],
778
+ networkFailures: [...enriched.networkFailures || [], ...trace.networkFailures],
779
+ lastActions: [...enriched.lastActions || [], ...trace.lastActions].slice(-10),
780
+ artifactRefs: [...artifactRefs, ...trace.artifactRefs]
781
+ };
782
+ }
783
+ return enriched;
784
+ }
785
+
786
+ // src/grouper.ts
787
+ var FailureGrouper = class {
788
+ group(failures) {
789
+ const groups = /* @__PURE__ */ new Map();
790
+ for (const failure of failures) {
791
+ const signature = failureSignature(failure);
792
+ groups.set(signature, [...groups.get(signature) || [], failure]);
793
+ }
794
+ return [...groups.entries()].map(([signature, groupedFailures]) => ({
795
+ signature,
796
+ pattern: readablePattern(groupedFailures[0]?.error || ""),
797
+ failures: groupedFailures
798
+ })).sort((a, b) => b.failures.length - a.failures.length);
799
+ }
800
+ };
801
+
802
+ // src/generation.ts
803
+ var import_promises2 = require("fs/promises");
804
+ var import_node_path4 = __toESM(require("path"), 1);
805
+ var import_test = require("@playwright/test");
806
+ var DESTRUCTIVE_PATTERNS = [
807
+ /\bdelete\b/i,
808
+ /\bremove\b/i,
809
+ /\bdestroy\b/i,
810
+ /\bpay\b/i,
811
+ /\bpayment\b/i,
812
+ /\bpurchase\b/i,
813
+ /\bsubmit\s+order\b/i,
814
+ /\bplace\s+order\b/i,
815
+ /\brefund\b/i,
816
+ /\bcancel\s+subscription\b/i
817
+ ];
818
+ function safeSegment(value, fallback) {
819
+ const normalized = (value || fallback).trim().replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 100);
820
+ return normalized || fallback;
821
+ }
822
+ function isLocalUrl(rawUrl) {
823
+ try {
824
+ const url = new URL(rawUrl);
825
+ return ["localhost", "127.0.0.1", "::1"].includes(url.hostname);
826
+ } catch {
827
+ return false;
828
+ }
829
+ }
830
+ function destructiveText(manualCase) {
831
+ return [
832
+ manualCase.title,
833
+ manualCase.description,
834
+ manualCase.preconditions,
835
+ ...manualCase.steps.flatMap((step) => [step.action, step.expected])
836
+ ].filter(Boolean).join("\n");
837
+ }
838
+ function isDestructiveManualCase(manualCase) {
839
+ const text = destructiveText(manualCase);
840
+ return DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(text));
841
+ }
842
+ function assertBrowserGenerationAllowed(request) {
843
+ if (!isLocalUrl(request.url) && !request.confirmBrowserRun) {
844
+ throw new Error(`Refusing to open external URL ${request.url}. Pass --confirm-browser-run to allow browser exploration.`);
845
+ }
846
+ if (isDestructiveManualCase(request.manualCase) && !request.allowDestructiveActions) {
847
+ throw new Error("Manual test case appears to contain destructive actions. Pass --allow-destructive-actions to override.");
848
+ }
849
+ }
850
+ function truncate(value, maxLength) {
851
+ return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
852
+ }
853
+ function defaultSource(manualCase) {
854
+ return manualCase.source?.provider || manualCase.source?.project || "manual";
855
+ }
856
+ var TemplateTestGenerationProvider = class {
857
+ async generateTest(request) {
858
+ const title = request.manualCase.title.replace(/'/g, "\\'");
859
+ const steps = request.manualCase.steps.map((step) => ` // Step ${step.index ?? ""}: ${step.action}${step.expected ? ` -> ${step.expected}` : ""}`.trimEnd()).join("\n");
860
+ return {
861
+ code: [
862
+ "import { test, expect } from '@playwright/test';",
863
+ "",
864
+ `test('${title}', async ({ page }) => {`,
865
+ ` await page.goto('${request.url}');`,
866
+ steps || " // No manual steps were supplied.",
867
+ " await expect(page).toHaveURL(/./);",
868
+ "});"
869
+ ].join("\n"),
870
+ locators: [],
871
+ coverage: request.manualCase.steps.map((step) => ({
872
+ stepIndex: step.index,
873
+ status: "partial",
874
+ notes: "Template provider preserved the manual step as a draft comment."
875
+ })),
876
+ warnings: ["Template provider generated a draft without AI locator reasoning. Review before committing."]
877
+ };
878
+ }
879
+ };
880
+ async function generatePlaywrightTestDraft(request) {
881
+ assertBrowserGenerationAllowed(request);
882
+ const outputDir = request.outputDir || "generated-tests";
883
+ const source = safeSegment(request.sourceName || defaultSource(request.manualCase), "manual");
884
+ const caseKey = safeSegment(request.manualCase.key || request.manualCase.id, "case");
885
+ const caseDir = import_node_path4.default.join(outputDir, source);
886
+ const filePath = import_node_path4.default.join(caseDir, `${caseKey}.spec.ts`);
887
+ const reportPath = import_node_path4.default.join(caseDir, `${caseKey}.generation-report.json`);
888
+ const screenshotPath = import_node_path4.default.join(caseDir, `${caseKey}.png`);
889
+ await (0, import_promises2.mkdir)(caseDir, { recursive: true });
890
+ const browser = await import_test.chromium.launch({ headless: true });
891
+ try {
892
+ const context = await browser.newContext(request.storageState ? { storageState: request.storageState } : {});
893
+ try {
894
+ const page = await context.newPage();
895
+ await page.goto(request.url, { waitUntil: "domcontentloaded" });
896
+ const screenshot = await page.screenshot({ fullPage: true });
897
+ const [title, html, bodyText] = await Promise.all([
898
+ page.title(),
899
+ page.content(),
900
+ page.locator("body").innerText({ timeout: 2e3 }).catch(() => "")
901
+ ]);
902
+ await (0, import_promises2.writeFile)(screenshotPath, screenshot);
903
+ const browserEvidence = {
904
+ requestedUrl: request.url,
905
+ currentUrl: page.url(),
906
+ title,
907
+ domSummary: truncate(bodyText, 8e3),
908
+ htmlSnippet: truncate(html, 12e3),
909
+ screenshotBase64: screenshot.toString("base64")
910
+ };
911
+ const generated = await request.provider.generateTest({
912
+ manualCase: request.manualCase,
913
+ url: request.url,
914
+ browser: browserEvidence,
915
+ outputLanguage: request.outputLanguage
916
+ });
917
+ const draft = {
918
+ manualCase: request.manualCase,
919
+ filePath,
920
+ reportPath,
921
+ screenshotPath,
922
+ code: generated.code,
923
+ locators: generated.locators || [],
924
+ coverage: generated.coverage || [],
925
+ warnings: generated.warnings || [],
926
+ browser: {
927
+ requestedUrl: browserEvidence.requestedUrl,
928
+ currentUrl: browserEvidence.currentUrl,
929
+ title: browserEvidence.title,
930
+ domSummary: browserEvidence.domSummary,
931
+ htmlSnippet: browserEvidence.htmlSnippet
932
+ },
933
+ needsHumanReview: true
934
+ };
935
+ const report = {
936
+ manualCase: request.manualCase,
937
+ browser: draft.browser,
938
+ locators: draft.locators,
939
+ coverage: draft.coverage,
940
+ warnings: draft.warnings,
941
+ needsHumanReview: draft.needsHumanReview,
942
+ artifacts: { filePath, reportPath, screenshotPath }
943
+ };
944
+ await (0, import_promises2.writeFile)(filePath, `${generated.code.trimEnd()}
945
+ `, "utf8");
946
+ await (0, import_promises2.writeFile)(reportPath, `${JSON.stringify(report, null, 2)}
947
+ `, "utf8");
948
+ return draft;
949
+ } finally {
950
+ await context.close();
951
+ }
952
+ } finally {
953
+ await browser.close();
954
+ }
955
+ }
956
+
957
+ // src/normalize.ts
958
+ function walkSuites(suites, visitor) {
959
+ for (const suite of suites || []) {
960
+ for (const spec of suite.specs || []) {
961
+ visitor(spec, suite);
962
+ }
963
+ walkSuites(suite.suites || [], visitor);
964
+ }
965
+ }
966
+ function resultError(result) {
967
+ return result?.error?.message || result?.error?.stack || result?.errors?.[0]?.message || result?.errors?.[0]?.stack || "Unknown error";
968
+ }
969
+ function normalizeSteps(result) {
970
+ const collected = [];
971
+ const visit = (step, insideUserStep) => {
972
+ if (!step) return;
973
+ const category = step.category;
974
+ const isUserStep = category === "test.step";
975
+ const isExpect = category === "expect";
976
+ if ((isUserStep || isExpect) && !insideUserStep && step.title) {
977
+ collected.push(String(step.title));
978
+ }
979
+ for (const child of step.steps || []) {
980
+ visit(child, insideUserStep || isUserStep);
981
+ }
982
+ };
983
+ for (const step of result?.steps || []) {
984
+ visit(step, false);
985
+ }
986
+ return collected;
987
+ }
988
+ function attachmentType(contentType, name) {
989
+ if (contentType?.startsWith("image/")) return "screenshot";
990
+ if (contentType?.includes("zip")) return "trace";
991
+ if (contentType?.includes("video")) return "video";
992
+ if (name?.toLowerCase().includes("trace")) return "trace";
993
+ return name || "artifact";
994
+ }
995
+ function normalizeArtifacts(result) {
996
+ return (result?.attachments || []).filter((attachment) => attachment?.path).map((attachment) => ({
997
+ type: attachmentType(attachment.contentType, attachment.name),
998
+ path: String(attachment.path),
999
+ contentType: attachment.contentType
1000
+ }));
1001
+ }
1002
+ function normalizePlaywrightJsonReport(report, options = {}) {
1003
+ const failures = [];
1004
+ const run = options.runId ? { id: options.runId } : void 0;
1005
+ walkSuites(report?.suites || [], (spec) => {
1006
+ for (const testCase of spec.tests || []) {
1007
+ for (const result of testCase.results || []) {
1008
+ if (!["failed", "timedOut", "interrupted"].includes(result.status)) {
1009
+ continue;
1010
+ }
1011
+ failures.push({
1012
+ title: String(spec.title || testCase.title || "unnamed test"),
1013
+ file: String(spec.file || testCase.location?.file || "unknown"),
1014
+ line: spec.line || testCase.location?.line,
1015
+ project: testCase.projectName,
1016
+ status: result.status,
1017
+ retry: result.retry || 0,
1018
+ durationMs: result.duration || 0,
1019
+ error: resultError(result),
1020
+ steps: normalizeSteps(result),
1021
+ artifacts: normalizeArtifacts(result),
1022
+ annotations: (testCase.annotations || []).map((annotation) => ({
1023
+ type: String(annotation.type),
1024
+ description: annotation.description
1025
+ })),
1026
+ run
1027
+ });
1028
+ }
1029
+ }
1030
+ });
1031
+ return failures;
1032
+ }
1033
+
1034
+ // src/providers/openai.ts
1035
+ var analysisSchema = {
1036
+ type: "object",
1037
+ properties: {
1038
+ side: { type: "string", enum: ["app_bug", "test_bug", "flaky_test", "env_issue", "unknown"] },
1039
+ rootCause: { type: "string" },
1040
+ evidence: { type: "array", items: { type: "string" } },
1041
+ reproductionSteps: { type: "array", items: { type: "string" } },
1042
+ suspects: {
1043
+ type: "object",
1044
+ properties: {
1045
+ selectors: { type: "array", items: { type: "string" } },
1046
+ pages: { type: "array", items: { type: "string" } },
1047
+ endpoints: { type: "array", items: { type: "string" } }
1048
+ },
1049
+ required: ["selectors", "pages", "endpoints"],
1050
+ additionalProperties: false
1051
+ },
1052
+ fixSuggestions: { type: "array", items: { type: "string" } },
1053
+ confidence: { type: "number", minimum: 0, maximum: 100 },
1054
+ category: {
1055
+ type: "string",
1056
+ enum: ["selector", "timing", "runtime", "test_data", "visual", "interaction", "environment", "unknown"]
1057
+ },
1058
+ risk: { type: "string", enum: ["low", "medium", "high"] },
1059
+ evidenceRefs: { type: "array", items: { type: "string" } },
1060
+ confidenceReasons: { type: "array", items: { type: "string" } },
1061
+ needsHumanReview: { type: "boolean" }
1062
+ },
1063
+ required: [
1064
+ "side",
1065
+ "rootCause",
1066
+ "evidence",
1067
+ "reproductionSteps",
1068
+ "suspects",
1069
+ "fixSuggestions",
1070
+ "confidence",
1071
+ "category",
1072
+ "risk",
1073
+ "evidenceRefs",
1074
+ "confidenceReasons",
1075
+ "needsHumanReview"
1076
+ ],
1077
+ additionalProperties: false
1078
+ };
1079
+ var generationSchema = {
1080
+ type: "object",
1081
+ properties: {
1082
+ code: { type: "string" },
1083
+ locators: {
1084
+ type: "array",
1085
+ items: {
1086
+ type: "object",
1087
+ properties: {
1088
+ stepIndex: { type: "number" },
1089
+ selector: { type: "string" },
1090
+ reason: { type: "string" }
1091
+ },
1092
+ required: ["selector", "reason"],
1093
+ additionalProperties: false
1094
+ }
1095
+ },
1096
+ coverage: {
1097
+ type: "array",
1098
+ items: {
1099
+ type: "object",
1100
+ properties: {
1101
+ stepIndex: { type: "number" },
1102
+ status: { type: "string", enum: ["covered", "partial", "missing"] },
1103
+ notes: { type: "string" }
1104
+ },
1105
+ required: ["status", "notes"],
1106
+ additionalProperties: false
1107
+ }
1108
+ },
1109
+ warnings: { type: "array", items: { type: "string" } }
1110
+ },
1111
+ required: ["code", "locators", "coverage", "warnings"],
1112
+ additionalProperties: false
1113
+ };
1114
+ function normalizeAnalysis(data, usage) {
1115
+ return {
1116
+ side: data.side || "unknown",
1117
+ rootCause: data.rootCause || data.root_cause || "No root cause returned",
1118
+ evidence: data.evidence || [],
1119
+ reproductionSteps: data.reproductionSteps || data.reproduction_steps || [],
1120
+ suspects: data.suspects || { selectors: [], pages: [], endpoints: [] },
1121
+ fixSuggestions: data.fixSuggestions || data.fix || [],
1122
+ confidence: Number(data.confidence ?? 0),
1123
+ category: data.category || "unknown",
1124
+ risk: data.risk || "high",
1125
+ evidenceRefs: data.evidenceRefs || [],
1126
+ confidenceReasons: data.confidenceReasons || [],
1127
+ needsHumanReview: data.needsHumanReview ?? true,
1128
+ source: "ai",
1129
+ tokenUsage: usage ? {
1130
+ inputTokens: usage.input_tokens || usage.inputTokens || 0,
1131
+ outputTokens: usage.output_tokens || usage.outputTokens || 0,
1132
+ totalTokens: usage.total_tokens || usage.totalTokens || 0
1133
+ } : void 0
1134
+ };
1135
+ }
1136
+ function normalizeGeneratedTest(data) {
1137
+ return {
1138
+ code: String(data.code || ""),
1139
+ locators: Array.isArray(data.locators) ? data.locators : [],
1140
+ coverage: Array.isArray(data.coverage) ? data.coverage : [],
1141
+ warnings: Array.isArray(data.warnings) ? data.warnings : []
1142
+ };
1143
+ }
1144
+ function outputText(response) {
1145
+ if (response.output_text) return response.output_text;
1146
+ for (const item of response.output || []) {
1147
+ for (const content of item.content || []) {
1148
+ if (content.text) return content.text;
1149
+ }
1150
+ }
1151
+ return "";
1152
+ }
1153
+ function compactFailure(failure) {
1154
+ return {
1155
+ title: failure.title,
1156
+ file: failure.file,
1157
+ line: failure.line,
1158
+ project: failure.project,
1159
+ status: failure.status,
1160
+ retry: failure.retry,
1161
+ durationMs: failure.durationMs,
1162
+ error: failure.error,
1163
+ steps: failure.steps || [],
1164
+ artifacts: failure.artifacts || [],
1165
+ annotations: failure.annotations || [],
1166
+ browserContext: failure.browserContext,
1167
+ page: failure.page,
1168
+ consoleErrors: failure.consoleErrors || [],
1169
+ networkFailures: failure.networkFailures || [],
1170
+ lastActions: failure.lastActions || [],
1171
+ artifactRefs: failure.artifactRefs || failure.artifacts || []
1172
+ };
1173
+ }
1174
+ var OpenAIResponsesProvider = class {
1175
+ constructor(options = {}) {
1176
+ this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
1177
+ this.model = options.model ?? process.env.PW_AI_MODEL ?? process.env.AI_MODEL ?? "gpt-4.1-mini";
1178
+ this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1/responses";
1179
+ this.transport = options.transport ?? ((url, init) => fetch(url, init));
1180
+ }
1181
+ async analyzeFailure(request) {
1182
+ if (!this.apiKey) {
1183
+ throw new Error("OPENAI_API_KEY is required for OpenAIResponsesProvider");
1184
+ }
1185
+ const payload = {
1186
+ model: this.model,
1187
+ instructions: `You are a generic Playwright failure analyzer. Return concise ${request.outputLanguage} JSON only. Use only the supplied test failure, trace evidence, steps, artifacts, and similar analyses. Every conclusion must point to supplied evidenceRefs.`,
1188
+ input: JSON.stringify({
1189
+ failure: compactFailure(request.failure),
1190
+ similarAnalyses: request.similarAnalyses || []
1191
+ }),
1192
+ text: {
1193
+ format: {
1194
+ type: "json_schema",
1195
+ name: "playwright_failure_analysis",
1196
+ strict: true,
1197
+ schema: analysisSchema
1198
+ }
1199
+ }
1200
+ };
1201
+ const response = await this.transport(this.baseUrl, {
1202
+ method: "POST",
1203
+ headers: {
1204
+ authorization: `Bearer ${this.apiKey}`,
1205
+ "content-type": "application/json"
1206
+ },
1207
+ body: JSON.stringify(payload)
1208
+ });
1209
+ if (!response.ok) {
1210
+ throw new Error(`OpenAI Responses request failed with status ${response.status}`);
1211
+ }
1212
+ const body = await response.json();
1213
+ const rawText = outputText(body);
1214
+ if (!rawText) {
1215
+ throw new Error("OpenAI Responses payload did not contain output text");
1216
+ }
1217
+ return normalizeAnalysis(JSON.parse(rawText), body.usage);
1218
+ }
1219
+ async generateTest(request) {
1220
+ if (!this.apiKey) {
1221
+ throw new Error("OPENAI_API_KEY is required for OpenAIResponsesProvider");
1222
+ }
1223
+ const browser = {
1224
+ requestedUrl: request.browser.requestedUrl,
1225
+ currentUrl: request.browser.currentUrl,
1226
+ title: request.browser.title,
1227
+ domSummary: request.browser.domSummary,
1228
+ htmlSnippet: request.browser.htmlSnippet
1229
+ };
1230
+ const content = [
1231
+ {
1232
+ type: "input_text",
1233
+ text: JSON.stringify({
1234
+ manualCase: request.manualCase,
1235
+ url: request.url,
1236
+ browser
1237
+ })
1238
+ }
1239
+ ];
1240
+ if (request.browser.screenshotBase64) {
1241
+ content.push({
1242
+ type: "input_image",
1243
+ image_url: `data:image/png;base64,${request.browser.screenshotBase64}`
1244
+ });
1245
+ }
1246
+ const payload = {
1247
+ model: this.model,
1248
+ instructions: `You generate review-only Playwright tests from manual test cases. Return concise ${request.outputLanguage || "ru"} JSON only. Use only the supplied manual steps, browser DOM evidence, URL, and screenshot. Prefer user-facing locators and data-testid selectors when the evidence supports them. Do not weaken assertions, do not invent credentials, and include warnings for any uncertain step.`,
1249
+ input: [{ role: "user", content }],
1250
+ text: {
1251
+ format: {
1252
+ type: "json_schema",
1253
+ name: "playwright_test_generation",
1254
+ strict: true,
1255
+ schema: generationSchema
1256
+ }
1257
+ }
1258
+ };
1259
+ const response = await this.transport(this.baseUrl, {
1260
+ method: "POST",
1261
+ headers: {
1262
+ authorization: `Bearer ${this.apiKey}`,
1263
+ "content-type": "application/json"
1264
+ },
1265
+ body: JSON.stringify(payload)
1266
+ });
1267
+ if (!response.ok) {
1268
+ throw new Error(`OpenAI Responses request failed with status ${response.status}`);
1269
+ }
1270
+ const body = await response.json();
1271
+ const rawText = outputText(body);
1272
+ if (!rawText) {
1273
+ throw new Error("OpenAI Responses payload did not contain output text");
1274
+ }
1275
+ return normalizeGeneratedTest(JSON.parse(rawText));
1276
+ }
1277
+ };
1278
+
1279
+ // src/report.ts
1280
+ var import_promises3 = require("fs/promises");
1281
+ var import_node_path5 = __toESM(require("path"), 1);
1282
+ function escapeHtml(value) {
1283
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1284
+ }
1285
+ function normalizedReport(report) {
1286
+ return {
1287
+ ...report,
1288
+ suggestions: report.suggestions || report.failures.map((failure, index) => {
1289
+ const analysis = report.analyses[index];
1290
+ return analysis ? generatePatchSuggestions(failure, analysis) : [];
1291
+ })
1292
+ };
1293
+ }
1294
+ function markdownReport(report) {
1295
+ const portable = normalizedReport(report);
1296
+ const lines = [
1297
+ "# Playwright AI Analysis Report",
1298
+ "",
1299
+ `Run: ${portable.run.id}`,
1300
+ `Failures: ${portable.failures.length}`,
1301
+ ""
1302
+ ];
1303
+ portable.failures.forEach((failure, index) => {
1304
+ const analysis = portable.analyses[index];
1305
+ const suggestions = portable.suggestions[index] || [];
1306
+ lines.push(`## ${failure.title}`);
1307
+ lines.push("");
1308
+ lines.push(`- File: ${failure.file}${failure.line ? `:${failure.line}` : ""}`);
1309
+ lines.push(`- Category: ${analysis?.category || "unknown"}`);
1310
+ lines.push(`- Side: ${analysis?.side || "unknown"}`);
1311
+ lines.push(`- Risk: ${analysis?.risk || "high"}`);
1312
+ lines.push(`- Confidence: ${analysis?.confidence ?? 0}`);
1313
+ lines.push(`- Root cause: ${analysis?.rootCause || "n/a"}`);
1314
+ if (analysis?.evidenceRefs?.length) {
1315
+ lines.push(`- Evidence refs: ${analysis.evidenceRefs.join(", ")}`);
1316
+ }
1317
+ if (failure.consoleErrors?.length || failure.networkFailures?.length) {
1318
+ lines.push("- Evidence:");
1319
+ for (const item of failure.consoleErrors || []) lines.push(` - Console ${item.level}: ${item.text}`);
1320
+ for (const item of failure.networkFailures || []) {
1321
+ lines.push(` - Network ${item.status || item.errorText}: ${item.method || "GET"} ${item.url}`);
1322
+ }
1323
+ }
1324
+ if (suggestions.length) {
1325
+ lines.push("");
1326
+ lines.push("### Suggested Fixes");
1327
+ for (const suggestion of suggestions) {
1328
+ lines.push(`- ${suggestion.policyDecision}: ${suggestion.explanation} (${suggestion.reviewReason})`);
1329
+ }
1330
+ }
1331
+ lines.push("");
1332
+ });
1333
+ return `${lines.join("\n")}
1334
+ `;
1335
+ }
1336
+ function htmlReport(report) {
1337
+ const portable = normalizedReport(report);
1338
+ const groups = new FailureGrouper().group(portable.failures);
1339
+ const groupSummary = groups.map((group) => `<li><code>${escapeHtml(group.signature)}</code> ${group.failures.length} failure(s)</li>`).join("");
1340
+ const failures = portable.failures.map((failure, index) => {
1341
+ const analysis = portable.analyses[index];
1342
+ const suggestions = portable.suggestions[index] || [];
1343
+ const evidence2 = [
1344
+ ...(failure.consoleErrors || []).map((item) => `Console ${item.level}: ${item.text}`),
1345
+ ...(failure.networkFailures || []).map(
1346
+ (item) => `Network ${item.status || item.errorText}: ${item.method || "GET"} ${item.url}`
1347
+ ),
1348
+ ...(failure.lastActions || []).map((item) => `Action ${item.name}${item.selector ? ` ${item.selector}` : ""}`),
1349
+ ...(failure.artifactRefs || failure.artifacts || []).map((item) => `Artifact ${item.type}: ${item.path}`)
1350
+ ];
1351
+ return `<section class="failure" data-category="${escapeHtml(analysis?.category || "unknown")}">
1352
+ <h2>${escapeHtml(failure.title)}</h2>
1353
+ <p><strong>${escapeHtml(analysis?.category || "unknown")}</strong> ${escapeHtml(analysis?.side || "unknown")} risk=${escapeHtml(analysis?.risk || "high")} confidence=${escapeHtml(analysis?.confidence ?? 0)} humanReview=${escapeHtml(analysis?.needsHumanReview ?? true)}</p>
1354
+ <p>${escapeHtml(analysis?.rootCause || failure.error)}</p>
1355
+ <h3>Evidence</h3>
1356
+ <ul>${evidence2.map((item) => `<li>${escapeHtml(item)}</li>`).join("") || "<li>No extra evidence collected.</li>"}</ul>
1357
+ <h3>Suggested Fixes</h3>
1358
+ <ul>${suggestions.map(
1359
+ (suggestion) => `<li><strong>${escapeHtml(suggestion.policyDecision)}</strong> ${escapeHtml(suggestion.explanation)}<pre>${escapeHtml(suggestion.newSnippet)}</pre><small>${escapeHtml(suggestion.reviewReason)}</small></li>`
1360
+ ).join("") || "<li>No suggestion available.</li>"}</ul>
1361
+ </section>`;
1362
+ }).join("\n");
1363
+ return `<!doctype html>
1364
+ <html lang="en">
1365
+ <head>
1366
+ <meta charset="utf-8">
1367
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1368
+ <title>Playwright AI Analysis Report</title>
1369
+ <style>
1370
+ body{font-family:system-ui,-apple-system,Segoe UI,sans-serif;margin:0;background:#f7f7f4;color:#171717}
1371
+ header,main{max-width:1120px;margin:0 auto;padding:24px}
1372
+ header{border-bottom:1px solid #d8d8d2}
1373
+ .summary{display:flex;gap:16px;flex-wrap:wrap}
1374
+ .pill{border:1px solid #c9c9c1;border-radius:6px;padding:6px 10px;background:white}
1375
+ .failure{background:white;border:1px solid #d8d8d2;border-radius:8px;padding:18px;margin:18px 0}
1376
+ pre{white-space:pre-wrap;background:#111;color:#f5f5f5;border-radius:6px;padding:12px;overflow:auto}
1377
+ code{font-size:.9em}
1378
+ </style>
1379
+ </head>
1380
+ <body>
1381
+ <header>
1382
+ <h1>Playwright AI Analysis Report</h1>
1383
+ <div class="summary"><span class="pill">Run ${escapeHtml(portable.run.id)}</span><span class="pill">Failures ${portable.failures.length}</span><span class="pill">Cache hits ${portable.analyses.filter((analysis) => analysis.source === "cache").length}</span></div>
1384
+ </header>
1385
+ <main>
1386
+ <h2>Groups</h2>
1387
+ <ul>${groupSummary}</ul>
1388
+ ${failures}
1389
+ </main>
1390
+ </body>
1391
+ </html>
1392
+ `;
1393
+ }
1394
+ function mcpSummary(report) {
1395
+ const portable = normalizedReport(report);
1396
+ return portable.failures.map((failure, index) => {
1397
+ const analysis = portable.analyses[index];
1398
+ return [
1399
+ `failure="${failure.title}"`,
1400
+ `file=${failure.file}${failure.line ? `:${failure.line}` : ""}`,
1401
+ `category=${analysis?.category || "unknown"}`,
1402
+ `risk=${analysis?.risk || "high"}`,
1403
+ `source=${analysis?.source || "unknown"}`,
1404
+ `needsHumanReview=${analysis?.needsHumanReview ?? true}`
1405
+ ].join(" ");
1406
+ }).join("\n");
1407
+ }
1408
+ async function writePortableReport(outputDir, report) {
1409
+ await (0, import_promises3.mkdir)(outputDir, { recursive: true });
1410
+ const portable = normalizedReport(report);
1411
+ await (0, import_promises3.writeFile)(import_node_path5.default.join(outputDir, "ai-analysis-report.json"), `${JSON.stringify(portable, null, 2)}
1412
+ `, "utf8");
1413
+ await (0, import_promises3.writeFile)(import_node_path5.default.join(outputDir, "ai-analysis-report.md"), markdownReport(portable), "utf8");
1414
+ await (0, import_promises3.writeFile)(import_node_path5.default.join(outputDir, "ai-analysis-report.html"), htmlReport(portable), "utf8");
1415
+ }
1416
+
1417
+ // src/stores/memory.ts
1418
+ function now() {
1419
+ return (/* @__PURE__ */ new Date()).toISOString();
1420
+ }
1421
+ function normalizeTags(tags) {
1422
+ return [...new Set(tags || [])].filter(Boolean).sort();
1423
+ }
1424
+ function recordText(record) {
1425
+ return JSON.stringify([record.key, record.kind, record.source, record.tags, record.value]).toLowerCase();
1426
+ }
1427
+ var MemoryStore = class {
1428
+ constructor() {
1429
+ this.runs = /* @__PURE__ */ new Map();
1430
+ this.failures = [];
1431
+ this.analyses = /* @__PURE__ */ new Map();
1432
+ this.memory = /* @__PURE__ */ new Map();
1433
+ this.feedback = [];
1434
+ }
1435
+ async migrate() {
1436
+ }
1437
+ async saveRun(run) {
1438
+ this.runs.set(run.id, run);
1439
+ }
1440
+ async saveFailure(failure) {
1441
+ this.failures.push(failure);
1442
+ }
1443
+ async saveAnalysis(signature, analysis) {
1444
+ this.analyses.set(signature, analysis);
1445
+ }
1446
+ async getCachedAnalysis(signature) {
1447
+ const cached = this.analyses.get(signature);
1448
+ return cached ? { ...cached, source: "cache" } : null;
1449
+ }
1450
+ async upsertMemory(input) {
1451
+ const key = `${input.namespace}:${input.kind}:${input.key}`;
1452
+ const existing = this.memory.get(key);
1453
+ this.memory.set(key, {
1454
+ namespace: input.namespace,
1455
+ kind: input.kind,
1456
+ key: input.key,
1457
+ value: input.value,
1458
+ tags: normalizeTags(input.tags),
1459
+ confidence: input.confidence ?? 1,
1460
+ source: input.source,
1461
+ supersededBy: existing?.supersededBy ?? null,
1462
+ createdAt: existing?.createdAt ?? now(),
1463
+ updatedAt: now()
1464
+ });
1465
+ }
1466
+ async searchMemory(input) {
1467
+ const query = input.query?.toLowerCase().trim();
1468
+ const tags = new Set(input.tags || []);
1469
+ const matches = [...this.memory.values()].filter((record) => {
1470
+ if (input.namespace && record.namespace !== input.namespace) return false;
1471
+ if (input.kind && record.kind !== input.kind) return false;
1472
+ if (tags.size > 0 && !record.tags.some((tag) => tags.has(tag))) return false;
1473
+ if (query && !recordText(record).includes(query)) return false;
1474
+ return !record.supersededBy;
1475
+ });
1476
+ return matches.slice(0, input.limit ?? 20);
1477
+ }
1478
+ async recordFeedback(input) {
1479
+ this.feedback.push({
1480
+ signature: input.signature,
1481
+ decision: input.decision,
1482
+ reason: input.reason || "",
1483
+ metadata: input.metadata || {},
1484
+ createdAt: now()
1485
+ });
1486
+ }
1487
+ async searchFeedback(input) {
1488
+ const query = input.query?.toLowerCase().trim();
1489
+ return this.feedback.filter((record) => {
1490
+ if (input.signature && record.signature !== input.signature) return false;
1491
+ if (input.decision && record.decision !== input.decision) return false;
1492
+ if (query && !JSON.stringify(record).toLowerCase().includes(query)) return false;
1493
+ return true;
1494
+ }).slice(0, input.limit ?? 20);
1495
+ }
1496
+ async findFailures(input) {
1497
+ const query = input.query?.toLowerCase().trim();
1498
+ return this.failures.filter((failure) => {
1499
+ if (input.signature && failureSignature(failure) !== input.signature) return false;
1500
+ if (input.file && failure.file !== input.file) return false;
1501
+ if (query && !JSON.stringify(failure).toLowerCase().includes(query)) return false;
1502
+ return true;
1503
+ }).slice(0, input.limit ?? 20);
1504
+ }
1505
+ async getRun(id) {
1506
+ return this.runs.get(id) || null;
1507
+ }
1508
+ async stats() {
1509
+ return {
1510
+ runs: this.runs.size,
1511
+ failures: this.failures.length,
1512
+ analyses: this.analyses.size,
1513
+ memoryRecords: this.memory.size,
1514
+ feedbackRecords: this.feedback.length
1515
+ };
1516
+ }
1517
+ async close() {
1518
+ }
1519
+ };
1520
+
1521
+ // src/stores/sqlite.ts
1522
+ var import_promises4 = require("fs/promises");
1523
+ var import_node_path6 = __toESM(require("path"), 1);
1524
+ var import_sql = __toESM(require("sql.js"), 1);
1525
+ var sqlModule;
1526
+ function sql() {
1527
+ sqlModule || (sqlModule = (0, import_sql.default)());
1528
+ return sqlModule;
1529
+ }
1530
+ function now2() {
1531
+ return (/* @__PURE__ */ new Date()).toISOString();
1532
+ }
1533
+ function normalizeTags2(tags) {
1534
+ return [...new Set(tags || [])].filter(Boolean).sort();
1535
+ }
1536
+ function parsePayload(value) {
1537
+ return typeof value === "string" ? JSON.parse(value) : value;
1538
+ }
1539
+ function hasSQLiteHeader(buffer) {
1540
+ return buffer.subarray(0, 16).toString("utf8") === "SQLite format 3\0";
1541
+ }
1542
+ var SQLiteStore = class {
1543
+ constructor(options = {}) {
1544
+ this.loaded = false;
1545
+ this.filePath = options.path || import_node_path6.default.join(process.cwd(), ".playwright-ai-tools", "ai-tools.sqlite");
1546
+ this.inMemory = this.filePath === ":memory:";
1547
+ }
1548
+ async migrate() {
1549
+ const db = await this.load();
1550
+ db.run(`
1551
+ CREATE TABLE IF NOT EXISTS runs (
1552
+ id TEXT PRIMARY KEY,
1553
+ payload TEXT NOT NULL,
1554
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
1555
+ );
1556
+
1557
+ CREATE TABLE IF NOT EXISTS failures (
1558
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1559
+ signature TEXT,
1560
+ file TEXT,
1561
+ title TEXT,
1562
+ run_id TEXT,
1563
+ payload TEXT NOT NULL,
1564
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
1565
+ );
1566
+
1567
+ CREATE TABLE IF NOT EXISTS analyses (
1568
+ signature TEXT PRIMARY KEY,
1569
+ payload TEXT NOT NULL,
1570
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
1571
+ );
1572
+
1573
+ CREATE TABLE IF NOT EXISTS memory_records (
1574
+ id TEXT PRIMARY KEY,
1575
+ namespace TEXT NOT NULL,
1576
+ kind TEXT NOT NULL,
1577
+ key TEXT NOT NULL,
1578
+ tags TEXT NOT NULL,
1579
+ payload TEXT NOT NULL,
1580
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
1581
+ );
1582
+
1583
+ CREATE TABLE IF NOT EXISTS feedback_records (
1584
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1585
+ signature TEXT NOT NULL,
1586
+ decision TEXT NOT NULL,
1587
+ reason TEXT,
1588
+ payload TEXT NOT NULL,
1589
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
1590
+ );
1591
+
1592
+ CREATE INDEX IF NOT EXISTS idx_failures_signature ON failures(signature);
1593
+ CREATE INDEX IF NOT EXISTS idx_failures_file ON failures(file);
1594
+ CREATE INDEX IF NOT EXISTS idx_memory_namespace_kind ON memory_records(namespace, kind);
1595
+ CREATE INDEX IF NOT EXISTS idx_feedback_signature ON feedback_records(signature);
1596
+ CREATE INDEX IF NOT EXISTS idx_feedback_decision ON feedback_records(decision);
1597
+ CREATE INDEX IF NOT EXISTS idx_analyses_category ON analyses(json_extract(payload, '$.category'));
1598
+ CREATE INDEX IF NOT EXISTS idx_analyses_risk ON analyses(json_extract(payload, '$.risk'));
1599
+ CREATE INDEX IF NOT EXISTS idx_failures_error ON failures(json_extract(payload, '$.error'));
1600
+ `);
1601
+ await this.persist();
1602
+ }
1603
+ async saveRun(run) {
1604
+ const db = await this.load();
1605
+ db.run(
1606
+ "INSERT OR REPLACE INTO runs (id, payload) VALUES (?, ?)",
1607
+ [run.id, JSON.stringify(run)]
1608
+ );
1609
+ await this.persist();
1610
+ }
1611
+ async saveFailure(failure) {
1612
+ const db = await this.load();
1613
+ const signature = failureSignature(failure);
1614
+ const runId = typeof failure.run?.id === "string" ? failure.run.id : null;
1615
+ db.run(
1616
+ "INSERT INTO failures (signature, file, title, run_id, payload) VALUES (?, ?, ?, ?, ?)",
1617
+ [signature, failure.file, failure.title, runId, JSON.stringify(failure)]
1618
+ );
1619
+ await this.persist();
1620
+ }
1621
+ async saveAnalysis(signature, analysis) {
1622
+ const db = await this.load();
1623
+ db.run(
1624
+ `
1625
+ INSERT INTO analyses (signature, payload, updated_at)
1626
+ VALUES (?, ?, CURRENT_TIMESTAMP)
1627
+ ON CONFLICT(signature) DO UPDATE SET payload = excluded.payload, updated_at = CURRENT_TIMESTAMP
1628
+ `,
1629
+ [signature, JSON.stringify(analysis)]
1630
+ );
1631
+ await this.persist();
1632
+ }
1633
+ async getCachedAnalysis(signature) {
1634
+ const rows = await this.all("SELECT payload FROM analyses WHERE signature = ? LIMIT 1", [signature]);
1635
+ return rows[0]?.payload ? { ...parsePayload(rows[0].payload), source: "cache" } : null;
1636
+ }
1637
+ async upsertMemory(record) {
1638
+ const db = await this.load();
1639
+ const id = `${record.namespace}:${record.kind}:${record.key}`;
1640
+ const existing = await this.searchMemory({
1641
+ namespace: record.namespace,
1642
+ kind: record.kind,
1643
+ query: record.key,
1644
+ limit: 100
1645
+ });
1646
+ const previous = existing.find((item) => item.key === record.key);
1647
+ const memory = {
1648
+ namespace: record.namespace,
1649
+ kind: record.kind,
1650
+ key: record.key,
1651
+ value: record.value,
1652
+ tags: normalizeTags2(record.tags),
1653
+ confidence: record.confidence ?? 1,
1654
+ source: record.source,
1655
+ supersededBy: previous?.supersededBy ?? null,
1656
+ createdAt: previous?.createdAt ?? now2(),
1657
+ updatedAt: now2()
1658
+ };
1659
+ db.run(
1660
+ `
1661
+ INSERT INTO memory_records (id, namespace, kind, key, tags, payload, updated_at)
1662
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
1663
+ ON CONFLICT(id) DO UPDATE SET
1664
+ namespace = excluded.namespace,
1665
+ kind = excluded.kind,
1666
+ key = excluded.key,
1667
+ tags = excluded.tags,
1668
+ payload = excluded.payload,
1669
+ updated_at = CURRENT_TIMESTAMP
1670
+ `,
1671
+ [id, memory.namespace, memory.kind, memory.key, JSON.stringify(memory.tags), JSON.stringify(memory)]
1672
+ );
1673
+ await this.persist();
1674
+ }
1675
+ async searchMemory(input) {
1676
+ const rows = await this.all("SELECT payload FROM memory_records ORDER BY updated_at DESC");
1677
+ const query = input.query?.toLowerCase().trim();
1678
+ const tags = new Set(input.tags || []);
1679
+ return rows.map((row) => parsePayload(row.payload)).filter((record) => {
1680
+ if (input.namespace && record.namespace !== input.namespace) return false;
1681
+ if (input.kind && record.kind !== input.kind) return false;
1682
+ if (tags.size > 0 && !record.tags.some((tag) => tags.has(tag))) return false;
1683
+ if (query && !JSON.stringify([record.key, record.kind, record.source, record.tags, record.value]).toLowerCase().includes(query)) {
1684
+ return false;
1685
+ }
1686
+ return !record.supersededBy;
1687
+ }).slice(0, input.limit ?? 20);
1688
+ }
1689
+ async recordFeedback(input) {
1690
+ const db = await this.load();
1691
+ const feedback = {
1692
+ signature: input.signature,
1693
+ decision: input.decision,
1694
+ reason: input.reason || "",
1695
+ metadata: input.metadata || {},
1696
+ createdAt: now2()
1697
+ };
1698
+ db.run(
1699
+ "INSERT INTO feedback_records (signature, decision, reason, payload) VALUES (?, ?, ?, ?)",
1700
+ [feedback.signature, feedback.decision, feedback.reason, JSON.stringify(feedback)]
1701
+ );
1702
+ await this.persist();
1703
+ }
1704
+ async searchFeedback(input) {
1705
+ const rows = await this.all("SELECT payload FROM feedback_records ORDER BY created_at DESC");
1706
+ const query = input.query?.toLowerCase().trim();
1707
+ return rows.map((row) => parsePayload(row.payload)).filter((record) => {
1708
+ if (input.signature && record.signature !== input.signature) return false;
1709
+ if (input.decision && record.decision !== input.decision) return false;
1710
+ if (query && !JSON.stringify(record).toLowerCase().includes(query)) return false;
1711
+ return true;
1712
+ }).slice(0, input.limit ?? 20);
1713
+ }
1714
+ async findFailures(input) {
1715
+ const rows = await this.all("SELECT payload FROM failures ORDER BY id DESC");
1716
+ const query = input.query?.toLowerCase().trim();
1717
+ return rows.map((row) => parsePayload(row.payload)).filter((failure) => {
1718
+ if (input.signature && failureSignature(failure) !== input.signature) return false;
1719
+ if (input.file && failure.file !== input.file) return false;
1720
+ if (query && !JSON.stringify(failure).toLowerCase().includes(query)) return false;
1721
+ return true;
1722
+ }).slice(0, input.limit ?? 20);
1723
+ }
1724
+ async getRun(id) {
1725
+ const rows = await this.all("SELECT payload FROM runs WHERE id = ? LIMIT 1", [id]);
1726
+ return rows[0]?.payload ? parsePayload(rows[0].payload) : null;
1727
+ }
1728
+ async stats() {
1729
+ const tables = ["runs", "failures", "analyses", "memory_records", "feedback_records"];
1730
+ const stats = {};
1731
+ for (const table of tables) {
1732
+ const rows = await this.all(`SELECT COUNT(*) AS count FROM ${table}`);
1733
+ stats[table === "memory_records" ? "memoryRecords" : table === "feedback_records" ? "feedbackRecords" : table] = Number(rows[0]?.count || 0);
1734
+ }
1735
+ return stats;
1736
+ }
1737
+ async close() {
1738
+ if (!this.db) return;
1739
+ await this.persist();
1740
+ this.db.close();
1741
+ this.db = void 0;
1742
+ this.loaded = false;
1743
+ }
1744
+ async load() {
1745
+ if (this.loaded && this.db) return this.db;
1746
+ this.loaded = true;
1747
+ const SQL = await sql();
1748
+ if (this.inMemory) {
1749
+ this.db = new SQL.Database();
1750
+ await this.migrate();
1751
+ return this.db;
1752
+ }
1753
+ try {
1754
+ const buffer = await (0, import_promises4.readFile)(this.filePath);
1755
+ if (buffer.length === 0) {
1756
+ this.db = new SQL.Database();
1757
+ await this.migrate();
1758
+ } else if (hasSQLiteHeader(buffer)) {
1759
+ this.db = new SQL.Database(buffer);
1760
+ } else {
1761
+ this.db = new SQL.Database();
1762
+ await this.migrate();
1763
+ await this.importSnapshot(JSON.parse(buffer.toString("utf8")));
1764
+ }
1765
+ } catch (error) {
1766
+ if (error?.code !== "ENOENT") {
1767
+ throw error;
1768
+ }
1769
+ this.db = new SQL.Database();
1770
+ await this.migrate();
1771
+ }
1772
+ return this.db;
1773
+ }
1774
+ async importSnapshot(snapshot) {
1775
+ for (const run of snapshot.runs || []) {
1776
+ await this.saveRun(run);
1777
+ }
1778
+ for (const failure of snapshot.failures || []) {
1779
+ await this.saveFailure(failure);
1780
+ }
1781
+ for (const [signature, analysis] of snapshot.analyses || []) {
1782
+ await this.saveAnalysis(signature, analysis);
1783
+ }
1784
+ for (const [, memory] of snapshot.memory || []) {
1785
+ await this.upsertMemory(memory);
1786
+ }
1787
+ }
1788
+ async all(sqlText, params = []) {
1789
+ const db = await this.load();
1790
+ const statement = db.prepare(sqlText, params);
1791
+ const rows = [];
1792
+ try {
1793
+ while (statement.step()) {
1794
+ rows.push(statement.getAsObject());
1795
+ }
1796
+ } finally {
1797
+ statement.free();
1798
+ }
1799
+ return rows;
1800
+ }
1801
+ async persist() {
1802
+ if (!this.db || this.inMemory) return;
1803
+ await (0, import_promises4.mkdir)(import_node_path6.default.dirname(this.filePath), { recursive: true });
1804
+ await (0, import_promises4.writeFile)(this.filePath, Buffer.from(this.db.export()));
1805
+ }
1806
+ };
1807
+
1808
+ // src/stores/postgres.ts
1809
+ var PostgresStore = class extends MemoryStore {
1810
+ constructor(executor) {
1811
+ super();
1812
+ this.executor = executor;
1813
+ }
1814
+ async migrate() {
1815
+ if (!this.executor) return;
1816
+ await this.executor.query(`
1817
+ CREATE TABLE IF NOT EXISTS pw_ai_runs (id text PRIMARY KEY, payload jsonb NOT NULL, created_at timestamptz DEFAULT now());
1818
+ CREATE TABLE IF NOT EXISTS pw_ai_failures (id bigserial PRIMARY KEY, signature text, file text, title text, run_id text, payload jsonb NOT NULL, created_at timestamptz DEFAULT now());
1819
+ CREATE TABLE IF NOT EXISTS pw_ai_analyses (signature text PRIMARY KEY, payload jsonb NOT NULL, updated_at timestamptz DEFAULT now());
1820
+ CREATE TABLE IF NOT EXISTS pw_ai_memory (id text PRIMARY KEY, namespace text NOT NULL, kind text NOT NULL, key text NOT NULL, tags text[] DEFAULT '{}', payload jsonb NOT NULL, updated_at timestamptz DEFAULT now());
1821
+ CREATE TABLE IF NOT EXISTS pw_ai_feedback (id bigserial PRIMARY KEY, signature text NOT NULL, decision text NOT NULL, reason text, payload jsonb NOT NULL, created_at timestamptz DEFAULT now());
1822
+ CREATE INDEX IF NOT EXISTS idx_pw_ai_failures_signature ON pw_ai_failures(signature);
1823
+ CREATE INDEX IF NOT EXISTS idx_pw_ai_memory_namespace_kind ON pw_ai_memory(namespace, kind);
1824
+ CREATE INDEX IF NOT EXISTS idx_pw_ai_feedback_signature ON pw_ai_feedback(signature);
1825
+ `);
1826
+ }
1827
+ async saveRun(run) {
1828
+ if (!this.executor) return super.saveRun(run);
1829
+ await this.executor.query(
1830
+ "INSERT INTO pw_ai_runs (id, payload) VALUES ($1, $2::jsonb) ON CONFLICT (id) DO UPDATE SET payload = EXCLUDED.payload",
1831
+ [run.id, JSON.stringify(run)]
1832
+ );
1833
+ }
1834
+ async saveFailure(failure) {
1835
+ if (!this.executor) return super.saveFailure(failure);
1836
+ await this.executor.query(
1837
+ "INSERT INTO pw_ai_failures (signature, file, title, run_id, payload) VALUES ($1, $2, $3, $4, $5::jsonb)",
1838
+ [failureSignature(failure), failure.file, failure.title, typeof failure.run?.id === "string" ? failure.run.id : null, JSON.stringify(failure)]
1839
+ );
1840
+ }
1841
+ async saveAnalysis(signature, analysis) {
1842
+ if (!this.executor) return super.saveAnalysis(signature, analysis);
1843
+ await this.executor.query(
1844
+ "INSERT INTO pw_ai_analyses (signature, payload) VALUES ($1, $2::jsonb) ON CONFLICT (signature) DO UPDATE SET payload = EXCLUDED.payload",
1845
+ [signature, JSON.stringify(analysis)]
1846
+ );
1847
+ }
1848
+ async getCachedAnalysis(signature) {
1849
+ if (!this.executor) return super.getCachedAnalysis(signature);
1850
+ const result = await this.executor.query("SELECT payload FROM pw_ai_analyses WHERE signature = $1 LIMIT 1", [
1851
+ signature
1852
+ ]);
1853
+ return result.rows[0]?.payload ? { ...result.rows[0].payload, source: "cache" } : null;
1854
+ }
1855
+ async upsertMemory(record) {
1856
+ if (!this.executor) return super.upsertMemory(record);
1857
+ const id = `${record.namespace}:${record.kind}:${record.key}`;
1858
+ const tags = [...new Set(record.tags || [])].filter(Boolean).sort();
1859
+ const payload = {
1860
+ namespace: record.namespace,
1861
+ kind: record.kind,
1862
+ key: record.key,
1863
+ value: record.value,
1864
+ tags,
1865
+ confidence: record.confidence ?? 1,
1866
+ source: record.source,
1867
+ supersededBy: null,
1868
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1869
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1870
+ };
1871
+ await this.executor.query(
1872
+ `
1873
+ INSERT INTO pw_ai_memory (id, namespace, kind, key, tags, payload)
1874
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb)
1875
+ ON CONFLICT (id) DO UPDATE SET
1876
+ namespace = EXCLUDED.namespace,
1877
+ kind = EXCLUDED.kind,
1878
+ key = EXCLUDED.key,
1879
+ tags = EXCLUDED.tags,
1880
+ payload = EXCLUDED.payload,
1881
+ updated_at = now()
1882
+ `,
1883
+ [id, payload.namespace, payload.kind, payload.key, payload.tags, JSON.stringify(payload)]
1884
+ );
1885
+ }
1886
+ async searchMemory(input) {
1887
+ if (!this.executor) return super.searchMemory(input);
1888
+ const result = await this.executor.query("SELECT payload FROM pw_ai_memory ORDER BY updated_at DESC LIMIT $1", [
1889
+ input.limit ?? 100
1890
+ ]);
1891
+ const query = input.query?.toLowerCase().trim();
1892
+ const tags = new Set(input.tags || []);
1893
+ return result.rows.map((row) => row.payload).filter((record) => {
1894
+ if (input.namespace && record.namespace !== input.namespace) return false;
1895
+ if (input.kind && record.kind !== input.kind) return false;
1896
+ if (tags.size > 0 && !record.tags.some((tag) => tags.has(tag))) return false;
1897
+ if (query && !JSON.stringify(record).toLowerCase().includes(query)) return false;
1898
+ return !record.supersededBy;
1899
+ }).slice(0, input.limit ?? 20);
1900
+ }
1901
+ async recordFeedback(record) {
1902
+ if (!this.executor) return super.recordFeedback(record);
1903
+ const payload = {
1904
+ signature: record.signature,
1905
+ decision: record.decision,
1906
+ reason: record.reason || "",
1907
+ metadata: record.metadata || {},
1908
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1909
+ };
1910
+ await this.executor.query(
1911
+ "INSERT INTO pw_ai_feedback (signature, decision, reason, payload) VALUES ($1, $2, $3, $4::jsonb)",
1912
+ [payload.signature, payload.decision, payload.reason, JSON.stringify(payload)]
1913
+ );
1914
+ }
1915
+ async searchFeedback(input) {
1916
+ if (!this.executor) return super.searchFeedback(input);
1917
+ const result = await this.executor.query("SELECT payload FROM pw_ai_feedback ORDER BY created_at DESC LIMIT $1", [
1918
+ input.limit ?? 100
1919
+ ]);
1920
+ const query = input.query?.toLowerCase().trim();
1921
+ return result.rows.map((row) => row.payload).filter((record) => {
1922
+ if (input.signature && record.signature !== input.signature) return false;
1923
+ if (input.decision && record.decision !== input.decision) return false;
1924
+ if (query && !JSON.stringify(record).toLowerCase().includes(query)) return false;
1925
+ return true;
1926
+ }).slice(0, input.limit ?? 20);
1927
+ }
1928
+ async findFailures(input) {
1929
+ if (!this.executor) return super.findFailures(input);
1930
+ const result = await this.executor.query("SELECT payload FROM pw_ai_failures ORDER BY id DESC LIMIT $1", [
1931
+ input.limit ?? 100
1932
+ ]);
1933
+ const query = input.query?.toLowerCase().trim();
1934
+ return result.rows.map((row) => row.payload).filter((failure) => {
1935
+ if (input.signature && failureSignature(failure) !== input.signature) return false;
1936
+ if (input.file && failure.file !== input.file) return false;
1937
+ if (query && !JSON.stringify(failure).toLowerCase().includes(query)) return false;
1938
+ return true;
1939
+ }).slice(0, input.limit ?? 20);
1940
+ }
1941
+ async close() {
1942
+ await this.executor?.close?.();
1943
+ }
1944
+ };
1945
+
1946
+ // src/legacy.ts
1947
+ function parseJsonLike(value) {
1948
+ if (!value) return {};
1949
+ if (typeof value === "string") {
1950
+ try {
1951
+ return JSON.parse(value);
1952
+ } catch {
1953
+ return {};
1954
+ }
1955
+ }
1956
+ return value;
1957
+ }
1958
+ function normalizeLegacyAnalysis(row) {
1959
+ const analysis = parseJsonLike(row.analysis_result);
1960
+ return {
1961
+ signature: row.error_cache_key,
1962
+ analysis: {
1963
+ side: row.side || analysis.side || "unknown",
1964
+ rootCause: row.root_cause || analysis.rootCause || analysis.root_cause || "No root cause recorded",
1965
+ evidence: analysis.evidence || [],
1966
+ reproductionSteps: analysis.reproductionSteps || analysis.reproduction_steps || [],
1967
+ suspects: analysis.suspects || { selectors: [], pages: [], endpoints: [] },
1968
+ fixSuggestions: row.fix_suggestions || analysis.fixSuggestions || analysis.fix || [],
1969
+ confidence: Number(row.confidence ?? analysis.confidence ?? 0),
1970
+ category: analysis.category || "unknown",
1971
+ risk: analysis.risk || "high",
1972
+ evidenceRefs: analysis.evidenceRefs || [],
1973
+ confidenceReasons: analysis.confidenceReasons || ["Imported cached analysis."],
1974
+ needsHumanReview: analysis.needsHumanReview ?? true,
1975
+ source: "cache"
1976
+ }
1977
+ };
1978
+ }
1979
+ function importLegacyRows(input) {
1980
+ const artifactsByResultId = /* @__PURE__ */ new Map();
1981
+ for (const artifact of input.artifacts || []) {
1982
+ const id = Number(artifact.test_result_id);
1983
+ artifactsByResultId.set(id, [...artifactsByResultId.get(id) || [], artifact]);
1984
+ }
1985
+ return {
1986
+ failures: (input.testResults || []).map((row) => ({
1987
+ title: row.test_title,
1988
+ file: row.test_file,
1989
+ line: row.test_line,
1990
+ project: row.project_name,
1991
+ status: row.status,
1992
+ retry: row.retry_index || 0,
1993
+ durationMs: row.duration_ms || 0,
1994
+ error: row.error_message || row.error_stack || "Unknown error",
1995
+ artifacts: (artifactsByResultId.get(Number(row.id)) || []).map((artifact) => ({
1996
+ type: artifact.artifact_type || "artifact",
1997
+ path: artifact.file_path,
1998
+ contentType: artifact.mime_type
1999
+ })),
2000
+ run: row.run_id ? { id: row.run_id } : void 0
2001
+ })),
2002
+ analyses: (input.analysisCache || []).filter((row) => row.error_cache_key).map(normalizeLegacyAnalysis)
2003
+ };
2004
+ }
2005
+ // Annotate the CommonJS export names for ESM import in node:
2006
+ 0 && (module.exports = {
2007
+ DeterministicAnalyzer,
2008
+ FailureGrouper,
2009
+ MemoryStore,
2010
+ OpenAIResponsesProvider,
2011
+ PostgresStore,
2012
+ SQLiteStore,
2013
+ TemplateTestGenerationProvider,
2014
+ analyzeFailures,
2015
+ assertBrowserGenerationAllowed,
2016
+ benchmarkFailures,
2017
+ classifyFailure,
2018
+ enrichFailureWithArtifacts,
2019
+ evaluatePatchPolicy,
2020
+ generatePatchSuggestions,
2021
+ generatePlaywrightTestDraft,
2022
+ htmlReport,
2023
+ importLegacyRows,
2024
+ isDestructiveManualCase,
2025
+ markdownReport,
2026
+ mcpSummary,
2027
+ normalizePlaywrightJsonReport,
2028
+ parseTraceArtifact,
2029
+ writePortableReport
2030
+ });