assuremind 1.1.2 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -186,6 +186,44 @@ var init_config = __esm({
186
186
  activeProfile: import_zod.z.string().optional(),
187
187
  /** Playwright device descriptor name for emulation (e.g. 'iPhone 15 Pro'). */
188
188
  device: import_zod.z.string().optional(),
189
+ /**
190
+ * RAG (Retrieval-Augmented Generation) — semantic memory from past runs.
191
+ *
192
+ * The AI learns from every generation and healing event:
193
+ * - Code Corpus: retrieve similar past steps during generation
194
+ * - Healing Corpus: retrieve similar past fixes during self-healing
195
+ * - Error Catalog: track recurring error patterns for preventive generation
196
+ */
197
+ rag: import_zod.z.object({
198
+ /** Master switch for RAG memory. */
199
+ enabled: import_zod.z.boolean(),
200
+ /** Code Corpus — semantic retrieval of similar instruction→code mappings. */
201
+ codeCorpus: import_zod.z.object({
202
+ enabled: import_zod.z.boolean(),
203
+ maxEntries: import_zod.z.number().int().positive(),
204
+ similarityThreshold: import_zod.z.number().min(0).max(1),
205
+ directUseThreshold: import_zod.z.number().min(0).max(1)
206
+ }),
207
+ /** Healing Corpus — retrieve similar past healing events during self-healing. */
208
+ healingCorpus: import_zod.z.object({
209
+ enabled: import_zod.z.boolean(),
210
+ maxEntries: import_zod.z.number().int().positive(),
211
+ similarityThreshold: import_zod.z.number().min(0).max(1)
212
+ }),
213
+ /** Error Catalog — aggregate recurring error patterns per URL. */
214
+ errorCatalog: import_zod.z.object({
215
+ enabled: import_zod.z.boolean(),
216
+ maxEntries: import_zod.z.number().int().positive()
217
+ }),
218
+ /** Embedding strategy: 'tfidf' (local, free, offline). */
219
+ embedder: import_zod.z.literal("tfidf")
220
+ }).default({
221
+ enabled: true,
222
+ codeCorpus: { enabled: true, maxEntries: 500, similarityThreshold: 0.65, directUseThreshold: 0.9 },
223
+ healingCorpus: { enabled: true, maxEntries: 300, similarityThreshold: 0.6 },
224
+ errorCatalog: { enabled: true, maxEntries: 200 },
225
+ embedder: "tfidf"
226
+ }),
189
227
  /**
190
228
  * Playwright MCP integration for AI-sighted code generation.
191
229
  *
@@ -251,6 +289,13 @@ var init_config = __esm({
251
289
  },
252
290
  studioPort: 4400,
253
291
  profiles: [],
292
+ rag: {
293
+ enabled: true,
294
+ codeCorpus: { enabled: true, maxEntries: 500, similarityThreshold: 0.65, directUseThreshold: 0.9 },
295
+ healingCorpus: { enabled: true, maxEntries: 300, similarityThreshold: 0.6 },
296
+ errorCatalog: { enabled: true, maxEntries: 200 },
297
+ embedder: "tfidf"
298
+ },
254
299
  mcp: {
255
300
  enabled: true,
256
301
  headless: true,
@@ -1107,7 +1152,7 @@ var init_suite = __esm({
1107
1152
  order: import_zod3.z.number().int().positive(),
1108
1153
  instruction: import_zod3.z.string().min(1),
1109
1154
  generatedCode: import_zod3.z.string(),
1110
- strategy: import_zod3.z.enum(["template", "cache", "batch", "fast", "primary"]),
1155
+ strategy: import_zod3.z.enum(["template", "cache", "rag", "batch", "fast", "primary", "recorder"]),
1111
1156
  stepType: import_zod3.z.enum(["ui", "api", "mock"]).default("ui"),
1112
1157
  lastHealed: import_zod3.z.string().nullable(),
1113
1158
  timeout: import_zod3.z.number().int().positive().optional(),
@@ -1115,8 +1160,10 @@ var init_suite = __esm({
1115
1160
  mockUrl: import_zod3.z.string().optional(),
1116
1161
  mockResponse: import_zod3.z.string().optional(),
1117
1162
  mockStatus: import_zod3.z.number().int().optional(),
1118
- runAudit: import_zod3.z.boolean().optional()
1163
+ runAudit: import_zod3.z.boolean().optional(),
1119
1164
  // Mark this step as a Lighthouse audit checkpoint
1165
+ soft: import_zod3.z.boolean().optional()
1166
+ // Use expect.soft() — test continues on assertion failure
1120
1167
  });
1121
1168
  DataSourceSchema = import_zod3.z.object({
1122
1169
  type: import_zod3.z.enum(["inline", "json-file", "csv-file"]),
@@ -2140,6 +2187,27 @@ var init_template_engine = __esm({
2140
2187
  regex: /^(?:verify|check)\s+(?:the\s+)?["'](.+?)["']\s+(?:field|input)\s+(?:has value|contains|equals)\s+["'](.+?)["']$/i,
2141
2188
  gen: (m) => `await expect(page.getByLabel('${m[1]}')).toHaveValue('${m[2]}');`
2142
2189
  },
2190
+ // ── Soft Assertions (expect.soft — test continues on failure) ──
2191
+ {
2192
+ regex: /^soft\s+(?:verify|check|assert|ensure|confirm)\s+(?:that\s+)?["'](.+?)["']\s+(?:is\s+)?(?:visible|shown|displayed|present|appears)$/i,
2193
+ gen: (m) => `await expect.soft(page.getByText('${m[1]}')).toBeVisible();`
2194
+ },
2195
+ {
2196
+ regex: /^soft\s+(?:verify|check|assert)\s+(?:that\s+)?["'](.+?)["']\s+(?:is\s+)?(?:not visible|hidden|not shown|gone|disappeared|not present)$/i,
2197
+ gen: (m) => `await expect.soft(page.getByText('${m[1]}')).not.toBeVisible();`
2198
+ },
2199
+ {
2200
+ regex: /^soft\s+(?:verify|check)\s+(?:the\s+)?(?:page\s+)?title\s+(?:is|contains)\s+["'](.+?)["']$/i,
2201
+ gen: (m) => `await expect.soft(page).toHaveTitle(/${m[1]}/);`
2202
+ },
2203
+ {
2204
+ regex: /^soft\s+(?:verify|check)\s+(?:the\s+)?url\s+(?:is|contains|matches)\s+["'](.+?)["']$/i,
2205
+ gen: (m) => `await expect.soft(page).toHaveURL(/${(m[1] ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/);`
2206
+ },
2207
+ {
2208
+ regex: /^soft\s+(?:verify|check)\s+(?:the\s+)?["'](.+?)["']\s+(?:field|input)\s+(?:has value|contains|equals)\s+["'](.+?)["']$/i,
2209
+ gen: (m) => `await expect.soft(page.getByLabel('${m[1]}')).toHaveValue('${m[2]}');`
2210
+ },
2143
2211
  // ── Keyboard ──
2144
2212
  {
2145
2213
  regex: /^press\s+(.+)$/i,
@@ -2818,9 +2886,11 @@ var init_smart_router = __esm({
2818
2886
  fastProvider;
2819
2887
  mcpSession;
2820
2888
  actThenScript;
2889
+ ragManager;
2821
2890
  stats = {
2822
2891
  template: 0,
2823
2892
  cache: 0,
2893
+ rag: 0,
2824
2894
  batch: 0,
2825
2895
  fast: 0,
2826
2896
  primary: 0,
@@ -2831,6 +2901,7 @@ var init_smart_router = __esm({
2831
2901
  this.fastProvider = config.fastProvider ?? null;
2832
2902
  this.mcpSession = config.mcpSession ?? null;
2833
2903
  this.actThenScript = config.actThenScript ?? false;
2904
+ this.ragManager = config.ragManager ?? null;
2834
2905
  this.cache = config.cache ?? new CodeCache();
2835
2906
  this.templateEngine = new TemplateEngine();
2836
2907
  this.classifier = new ComplexityClassifier();
@@ -2856,7 +2927,33 @@ var init_smart_router = __esm({
2856
2927
  const fixed = fixButtonSelectors(instruction, fixAntiPatterns(cached.code));
2857
2928
  return { code: fixed, strategy: "cache", cost: 0, model: "local" };
2858
2929
  }
2930
+ let ragExamples = [];
2931
+ let errorWarnings = [];
2932
+ if (this.ragManager) {
2933
+ try {
2934
+ const matches = this.ragManager.codeCorpus.retrieve(instruction, pageContext.url);
2935
+ if (matches.length > 0 && matches[0].score >= 0.9) {
2936
+ const best = matches[0];
2937
+ this.stats.rag++;
2938
+ logger13.info({ instruction, score: best.score, matchedInstruction: best.instruction }, "RAG direct hit");
2939
+ const fixed = fixButtonSelectors(instruction, fixAntiPatterns(best.code));
2940
+ this.cache.set(cacheKey, fixed, pageContext.url);
2941
+ return { code: fixed, strategy: "rag", cost: 0, model: "local" };
2942
+ }
2943
+ ragExamples = matches.filter((m) => m.score >= 0.65);
2944
+ if (ragExamples.length > 0) {
2945
+ logger13.debug({ instruction, examples: ragExamples.length, topScore: ragExamples[0].score }, "RAG examples found");
2946
+ }
2947
+ errorWarnings = this.ragManager.errorCatalog.getWarnings(pageContext.url);
2948
+ if (errorWarnings.length > 0) {
2949
+ logger13.debug({ url: pageContext.url, warnings: errorWarnings.length }, "RAG error warnings");
2950
+ }
2951
+ } catch (err) {
2952
+ logger13.debug({ err }, "RAG retrieval failed \u2014 continuing without memory");
2953
+ }
2954
+ }
2859
2955
  const enrichedContext = this.mcpSession ? await this.enrichWithMcp(instruction, pageContext) : pageContext;
2956
+ const contextWithRag = ragExamples.length > 0 || errorWarnings.length > 0 ? { ...enrichedContext, _ragExamples: ragExamples, _errorWarnings: errorWarnings } : enrichedContext;
2860
2957
  if (this.actThenScript && this.mcpSession && enrichedContext.accessibilityTree) {
2861
2958
  const actResult = await this.tryActThenScript(instruction, enrichedContext);
2862
2959
  if (actResult !== null) {
@@ -2870,8 +2967,8 @@ var init_smart_router = __esm({
2870
2967
  const usesFast = this.fastProvider !== null && complexity !== "complex";
2871
2968
  const provider = usesFast ? this.fastProvider : this.primaryProvider;
2872
2969
  const strategy = usesFast ? "fast" : "primary";
2873
- logger13.debug({ instruction, complexity, strategy }, "AI call");
2874
- const code = await provider.generateCode(instruction, enrichedContext);
2970
+ logger13.debug({ instruction, complexity, strategy, ragExamples: ragExamples.length, errorWarnings: errorWarnings.length }, "AI call");
2971
+ const code = await provider.generateCode(instruction, contextWithRag);
2875
2972
  this.cache.set(cacheKey, code, pageContext.url);
2876
2973
  if (usesFast) {
2877
2974
  this.stats.fast++;
@@ -2910,7 +3007,7 @@ var init_smart_router = __esm({
2910
3007
  }
2911
3008
  // ─── Stats (shown in Studio dashboard) ───────────────────────────────────
2912
3009
  getStats() {
2913
- const freeCalls = this.stats.template + this.stats.cache;
3010
+ const freeCalls = this.stats.template + this.stats.cache + this.stats.rag;
2914
3011
  return {
2915
3012
  ...this.stats,
2916
3013
  freeCalls,
@@ -2930,6 +3027,10 @@ var init_smart_router = __esm({
2930
3027
  getMcpSession() {
2931
3028
  return this.mcpSession;
2932
3029
  }
3030
+ /** Exposes the RAG manager for feedback loop (executor records pass/fail). */
3031
+ getRagManager() {
3032
+ return this.ragManager;
3033
+ }
2933
3034
  // ─── MCP enrichment ─────────────────────────────────────────────────────────
2934
3035
  /**
2935
3036
  * Enrich a PageContext with live accessibility data from the MCP browser.
@@ -3408,6 +3509,17 @@ ${context.interactiveElements || " None captured"}`;
3408
3509
  const accessibilitySection = context.accessibilityTree ? `
3409
3510
  Full accessibility tree (for additional context):
3410
3511
  ${context.accessibilityTree.slice(0, 4e3)}` : "";
3512
+ const ragCtx = context;
3513
+ const ragExamples = ragCtx._ragExamples ?? [];
3514
+ const errorWarnings = ragCtx._errorWarnings ?? [];
3515
+ const ragSection = ragExamples.length > 0 ? `
3516
+ Similar past steps that worked (use as reference \u2014 adapt, don't copy blindly):
3517
+ ${ragExamples.slice(0, 3).map((ex, i) => ` ${i + 1}. "${ex.instruction}" (similarity: ${(ex.score * 100).toFixed(0)}%)
3518
+ ${ex.code}`).join("\n")}` : "";
3519
+ const errorSection = errorWarnings.length > 0 ? `
3520
+ Known issues on this page (avoid these patterns):
3521
+ ${errorWarnings.slice(0, 3).map((w, i) => ` ${i + 1}. ${w.errorPattern} (occurred ${w.frequency}x)${w.suggestedFix ? `
3522
+ Suggested fix: ${w.suggestedFix}` : ""}`).join("\n")}` : "";
3411
3523
  const user = `Current page:
3412
3524
  URL: ${context.url}
3413
3525
  Title: ${context.title}
@@ -3420,7 +3532,7 @@ ${prevSteps}
3420
3532
  Runtime variables:
3421
3533
  ${vars}
3422
3534
 
3423
- Step to implement: "${instruction}"`;
3535
+ Step to implement: "${instruction}"${ragSection}${errorSection}`;
3424
3536
  return { system: SYSTEM_PROMPT2, user };
3425
3537
  }
3426
3538
  var SYSTEM_PROMPT2;
@@ -3533,6 +3645,12 @@ Input value: await expect(page.getByLabel('Email')).toHaveValue('user@exampl
3533
3645
  Not visible: await expect(page.getByText('Error')).not.toBeVisible()
3534
3646
  Count: await expect(page.getByRole('listitem')).toHaveCount(5)
3535
3647
 
3648
+ \u2501\u2501\u2501 SOFT ASSERTIONS (test continues on failure, failures collected at end) \u2501\u2501\u2501
3649
+ When the instruction starts with "Soft verify" or "Soft assert" or "Soft check", use expect.soft() instead of expect():
3650
+ await expect.soft(page).toHaveURL(/dashboard/)
3651
+ await expect.soft(page.getByText('Welcome')).toBeVisible()
3652
+ await expect.soft(page.getByLabel('Email')).toHaveValue('user@example.com')
3653
+
3536
3654
  For "verify user is on X page" \u2192 use toHaveURL() or check for a heading/element specific to that page.`;
3537
3655
  }
3538
3656
  });
@@ -3821,7 +3939,12 @@ Input value: await expect(page.getByLabel('Email')).toHaveValue('user@test.c
3821
3939
  Not visible: await expect(page.getByText('Error')).not.toBeVisible()
3822
3940
 
3823
3941
  "Verify user is on X page" \u2192 await expect(page).toHaveURL(/x-page-path/)
3824
- OR await expect(page.getByRole('heading', { name: 'X' })).toBeVisible()`;
3942
+ OR await expect(page.getByRole('heading', { name: 'X' })).toBeVisible()
3943
+
3944
+ \u2501\u2501\u2501 SOFT ASSERTIONS (test continues on failure) \u2501\u2501\u2501
3945
+ When instruction starts with "Soft verify/assert/check", use expect.soft():
3946
+ await expect.soft(page.getByText('Welcome')).toBeVisible()
3947
+ await expect.soft(page).toHaveURL(/dashboard/)`;
3825
3948
  }
3826
3949
  });
3827
3950
 
@@ -5072,7 +5195,7 @@ function createProvider(overrideProvider, options) {
5072
5195
  );
5073
5196
  }
5074
5197
  }
5075
- function createSmartRouter(rootDir, mcpSession, actThenScript) {
5198
+ function createSmartRouter(rootDir, mcpSession, actThenScript, ragManager) {
5076
5199
  const env = validateEnv();
5077
5200
  const primary = createProvider();
5078
5201
  let fast;
@@ -5089,7 +5212,7 @@ function createSmartRouter(rootDir, mcpSession, actThenScript) {
5089
5212
  }
5090
5213
  const cachePath = rootDir ? import_path12.default.join(rootDir, "results", ".code-cache.json") : void 0;
5091
5214
  const cache = new CodeCache(cachePath);
5092
- return new SmartRouter({ primaryProvider: primary, fastProvider: fast, cache, mcpSession, actThenScript });
5215
+ return new SmartRouter({ primaryProvider: primary, fastProvider: fast, cache, mcpSession, actThenScript, ragManager });
5093
5216
  }
5094
5217
  async function generateSuiteFromStory(story, options) {
5095
5218
  const provider = createProvider();
@@ -5289,9 +5412,10 @@ async function stepRoutes(fastify) {
5289
5412
  id: `step-${(0, import_crypto2.randomUUID)().slice(0, 8)}`,
5290
5413
  order: insertOrder,
5291
5414
  instruction: parsed.data.instruction,
5292
- generatedCode: "",
5293
- strategy: "primary",
5294
- lastHealed: null
5415
+ generatedCode: parsed.data.generatedCode ?? "",
5416
+ strategy: parsed.data.generatedCode ? parsed.data.strategy ?? "recorder" : "primary",
5417
+ lastHealed: null,
5418
+ ...parsed.data.soft ? { soft: true } : {}
5295
5419
  };
5296
5420
  const updatedSteps = [
5297
5421
  ...tc.steps.map((s) => s.order >= insertOrder ? { ...s, order: s.order + 1 } : s),
@@ -5344,8 +5468,12 @@ async function stepRoutes(fastify) {
5344
5468
  previousSteps: tc.steps.filter((s) => s.order < step.order && s.generatedCode).map((s) => ({ instruction: s.instruction, code: s.generatedCode })),
5345
5469
  variables
5346
5470
  });
5471
+ let finalCode = result.code;
5472
+ if (step.soft && finalCode) {
5473
+ finalCode = finalCode.replace(/\bexpect\(/g, "expect.soft(");
5474
+ }
5347
5475
  const updatedSteps = tc.steps.map(
5348
- (s) => s.id === step.id ? { ...s, generatedCode: result.code, strategy: result.strategy } : s
5476
+ (s) => s.id === step.id ? { ...s, generatedCode: finalCode, strategy: result.strategy } : s
5349
5477
  );
5350
5478
  await router.getCache().persist();
5351
5479
  const updated = await updateCase(caseDir, { steps: updatedSteps });
@@ -5396,7 +5524,8 @@ async function stepRoutes(fastify) {
5396
5524
  const r = codeMap.get(s.id);
5397
5525
  if (r && r.code.trim()) {
5398
5526
  actuallyGenerated++;
5399
- return { ...s, generatedCode: r.code, strategy: r.strategy };
5527
+ const code = s.soft ? r.code.replace(/\bexpect\(/g, "expect.soft(") : r.code;
5528
+ return { ...s, generatedCode: code, strategy: r.strategy };
5400
5529
  }
5401
5530
  return s;
5402
5531
  });
@@ -5459,6 +5588,16 @@ async function stepRoutes(fastify) {
5459
5588
  if (parsed.data.auditUrl !== void 0) patch.auditUrl = parsed.data.auditUrl;
5460
5589
  if (parsed.data.lighthouseCategories !== void 0) patch.lighthouseCategories = parsed.data.lighthouseCategories;
5461
5590
  if (parsed.data.runAudit !== void 0) patch.runAudit = parsed.data.runAudit;
5591
+ if (parsed.data.soft !== void 0) {
5592
+ patch.soft = parsed.data.soft;
5593
+ if (s.generatedCode) {
5594
+ if (parsed.data.soft) {
5595
+ patch.generatedCode = s.generatedCode.replace(/\bexpect\(/g, "expect.soft(");
5596
+ } else {
5597
+ patch.generatedCode = s.generatedCode.replace(/\bexpect\.soft\(/g, "expect(");
5598
+ }
5599
+ }
5600
+ }
5462
5601
  return { ...s, ...patch };
5463
5602
  });
5464
5603
  const updated = await updateCase(caseDir, { steps: updatedSteps });
@@ -5485,7 +5624,10 @@ var init_steps = __esm({
5485
5624
  init_utils2();
5486
5625
  AddStepBody = import_zod8.z.object({
5487
5626
  instruction: import_zod8.z.string().min(1),
5488
- order: import_zod8.z.number().int().positive().optional()
5627
+ order: import_zod8.z.number().int().positive().optional(),
5628
+ generatedCode: import_zod8.z.string().optional(),
5629
+ strategy: import_zod8.z.enum(["template", "cache", "rag", "batch", "fast", "primary", "recorder"]).optional(),
5630
+ soft: import_zod8.z.boolean().optional()
5489
5631
  });
5490
5632
  ReorderBody = import_zod8.z.object({
5491
5633
  stepIds: import_zod8.z.array(import_zod8.z.string().min(1))
@@ -5500,7 +5642,8 @@ var init_steps = __esm({
5500
5642
  mockStatus: import_zod8.z.number().int().optional(),
5501
5643
  auditUrl: import_zod8.z.string().optional(),
5502
5644
  lighthouseCategories: import_zod8.z.array(import_zod8.z.enum(["performance", "accessibility", "seo"])).optional(),
5503
- runAudit: import_zod8.z.boolean().optional()
5645
+ runAudit: import_zod8.z.boolean().optional(),
5646
+ soft: import_zod8.z.boolean().optional()
5504
5647
  });
5505
5648
  }
5506
5649
  });
@@ -6510,6 +6653,726 @@ var init_result_store = __esm({
6510
6653
  }
6511
6654
  });
6512
6655
 
6656
+ // src/rag/embedder.ts
6657
+ function tokenize(text) {
6658
+ const cleaned = text.toLowerCase().replace(/\{\{[^}]+\}\}/g, " __var__ ").replace(/"([^"]+)"/g, (_m, p1) => ` ${p1} `).replace(/'([^']+)'/g, (_m, p1) => ` ${p1} `).replace(/[^a-z0-9\s-]/g, " ").trim();
6659
+ const words = cleaned.split(/\s+/).filter((w) => w.length > 1 && !STOPWORDS.has(w));
6660
+ const tokens = [...words];
6661
+ for (let i = 0; i < words.length - 1; i++) {
6662
+ tokens.push(`${words[i]}_${words[i + 1]}`);
6663
+ }
6664
+ return tokens;
6665
+ }
6666
+ function hashTerm(term) {
6667
+ const hash = (0, import_crypto4.createHash)("md5").update(term).digest();
6668
+ return hash.readUInt32LE(0) % VECTOR_DIM;
6669
+ }
6670
+ var import_crypto4, import_fs_extra14, VECTOR_DIM, STOPWORDS, TfIdfEmbedder;
6671
+ var init_embedder = __esm({
6672
+ "src/rag/embedder.ts"() {
6673
+ "use strict";
6674
+ init_cjs_shims();
6675
+ import_crypto4 = require("crypto");
6676
+ import_fs_extra14 = require("fs-extra");
6677
+ VECTOR_DIM = 256;
6678
+ STOPWORDS = /* @__PURE__ */ new Set([
6679
+ "the",
6680
+ "a",
6681
+ "an",
6682
+ "is",
6683
+ "are",
6684
+ "was",
6685
+ "were",
6686
+ "be",
6687
+ "been",
6688
+ "being",
6689
+ "have",
6690
+ "has",
6691
+ "had",
6692
+ "do",
6693
+ "does",
6694
+ "did",
6695
+ "will",
6696
+ "would",
6697
+ "could",
6698
+ "should",
6699
+ "may",
6700
+ "might",
6701
+ "shall",
6702
+ "can",
6703
+ "need",
6704
+ "dare",
6705
+ "ought",
6706
+ "to",
6707
+ "of",
6708
+ "in",
6709
+ "for",
6710
+ "on",
6711
+ "with",
6712
+ "at",
6713
+ "by",
6714
+ "from",
6715
+ "as",
6716
+ "into",
6717
+ "through",
6718
+ "during",
6719
+ "before",
6720
+ "after",
6721
+ "above",
6722
+ "below",
6723
+ "between",
6724
+ "out",
6725
+ "off",
6726
+ "over",
6727
+ "under",
6728
+ "again",
6729
+ "further",
6730
+ "then",
6731
+ "once",
6732
+ "that",
6733
+ "this",
6734
+ "these",
6735
+ "those",
6736
+ "and",
6737
+ "but",
6738
+ "or",
6739
+ "nor",
6740
+ "not",
6741
+ "so",
6742
+ "very",
6743
+ "just",
6744
+ "than",
6745
+ "too",
6746
+ "also",
6747
+ "it",
6748
+ "its"
6749
+ ]);
6750
+ TfIdfEmbedder = class {
6751
+ constructor(vocabPath) {
6752
+ this.vocabPath = vocabPath;
6753
+ }
6754
+ vocab = { docCount: 0, termDocFreq: {} };
6755
+ dirty = false;
6756
+ /* ── Lifecycle ─────────────────────────────────────────────────────────── */
6757
+ /** Load persisted IDF vocabulary from disk */
6758
+ async load() {
6759
+ try {
6760
+ this.vocab = await (0, import_fs_extra14.readJson)(this.vocabPath);
6761
+ } catch {
6762
+ this.vocab = { docCount: 0, termDocFreq: {} };
6763
+ }
6764
+ }
6765
+ /** Persist IDF vocabulary to disk */
6766
+ async save() {
6767
+ if (!this.dirty) return;
6768
+ await (0, import_fs_extra14.outputJson)(this.vocabPath, this.vocab, { spaces: 0 });
6769
+ this.dirty = false;
6770
+ }
6771
+ /* ── Training ──────────────────────────────────────────────────────────── */
6772
+ /**
6773
+ * Add a document to the IDF computation.
6774
+ * Call this for every instruction ingested into a corpus.
6775
+ */
6776
+ addDocument(text) {
6777
+ const tokens = new Set(tokenize(text));
6778
+ this.vocab.docCount++;
6779
+ for (const term of tokens) {
6780
+ this.vocab.termDocFreq[term] = (this.vocab.termDocFreq[term] ?? 0) + 1;
6781
+ }
6782
+ this.dirty = true;
6783
+ }
6784
+ /**
6785
+ * Batch-add multiple documents at once (initial corpus load).
6786
+ */
6787
+ addDocuments(texts) {
6788
+ for (const text of texts) {
6789
+ this.addDocument(text);
6790
+ }
6791
+ }
6792
+ /* ── Embedding ─────────────────────────────────────────────────────────── */
6793
+ /**
6794
+ * Convert a text string into a fixed-dimension TF-IDF vector.
6795
+ * Uses hashing trick to map terms to buckets (no explicit vocabulary limit).
6796
+ */
6797
+ embed(text) {
6798
+ const tokens = tokenize(text);
6799
+ if (tokens.length === 0) return new Array(VECTOR_DIM).fill(0);
6800
+ const tf = {};
6801
+ for (const token of tokens) {
6802
+ tf[token] = (tf[token] ?? 0) + 1;
6803
+ }
6804
+ const vector = new Array(VECTOR_DIM).fill(0);
6805
+ const N = Math.max(this.vocab.docCount, 1);
6806
+ for (const [term, count] of Object.entries(tf)) {
6807
+ const termFreq = count / tokens.length;
6808
+ const docFreq = this.vocab.termDocFreq[term] ?? 0;
6809
+ const idf = Math.log(1 + N / (1 + docFreq));
6810
+ const tfidf = termFreq * idf;
6811
+ const bucket = hashTerm(term);
6812
+ vector[bucket] += tfidf;
6813
+ }
6814
+ const mag = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
6815
+ if (mag > 0) {
6816
+ for (let i = 0; i < vector.length; i++) {
6817
+ vector[i] /= mag;
6818
+ }
6819
+ }
6820
+ return vector;
6821
+ }
6822
+ /** Current corpus size */
6823
+ get documentCount() {
6824
+ return this.vocab.docCount;
6825
+ }
6826
+ };
6827
+ }
6828
+ });
6829
+
6830
+ // src/rag/similarity.ts
6831
+ function cosineSimilarity(a, b) {
6832
+ if (a.length !== b.length || a.length === 0) return 0;
6833
+ let dot = 0;
6834
+ let magA = 0;
6835
+ let magB = 0;
6836
+ for (let i = 0; i < a.length; i++) {
6837
+ dot += a[i] * b[i];
6838
+ magA += a[i] * a[i];
6839
+ magB += b[i] * b[i];
6840
+ }
6841
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
6842
+ return denom === 0 ? 0 : dot / denom;
6843
+ }
6844
+ var init_similarity = __esm({
6845
+ "src/rag/similarity.ts"() {
6846
+ "use strict";
6847
+ init_cjs_shims();
6848
+ }
6849
+ });
6850
+
6851
+ // src/rag/vector-store.ts
6852
+ var import_fs_extra15, VectorStore;
6853
+ var init_vector_store = __esm({
6854
+ "src/rag/vector-store.ts"() {
6855
+ "use strict";
6856
+ init_cjs_shims();
6857
+ import_fs_extra15 = require("fs-extra");
6858
+ init_similarity();
6859
+ VectorStore = class {
6860
+ constructor(filePath, maxEntries = 500) {
6861
+ this.filePath = filePath;
6862
+ this.maxEntries = maxEntries;
6863
+ }
6864
+ documents = [];
6865
+ dirty = false;
6866
+ /* ── Lifecycle ─────────────────────────────────────────────────────────── */
6867
+ async load() {
6868
+ if (await (0, import_fs_extra15.pathExists)(this.filePath)) {
6869
+ try {
6870
+ const data = await (0, import_fs_extra15.readJson)(this.filePath);
6871
+ this.documents = data.documents ?? [];
6872
+ } catch {
6873
+ this.documents = [];
6874
+ }
6875
+ }
6876
+ }
6877
+ async persist() {
6878
+ if (!this.dirty) return;
6879
+ const data = { version: 1, documents: this.documents };
6880
+ await (0, import_fs_extra15.outputJson)(this.filePath, data, { spaces: 0 });
6881
+ this.dirty = false;
6882
+ }
6883
+ /* ── CRUD ──────────────────────────────────────────────────────────────── */
6884
+ /** Add a document. Auto-prunes if maxEntries exceeded. */
6885
+ add(id, text, vector, metadata) {
6886
+ this.documents = this.documents.filter((d) => d.id !== id);
6887
+ const now2 = Date.now();
6888
+ this.documents.push({ id, text, vector, metadata, createdAt: now2, updatedAt: now2 });
6889
+ this.dirty = true;
6890
+ if (this.documents.length > this.maxEntries) {
6891
+ this.prune();
6892
+ }
6893
+ }
6894
+ /** Update metadata for an existing document */
6895
+ update(id, metadata) {
6896
+ const doc = this.documents.find((d) => d.id === id);
6897
+ if (!doc) return false;
6898
+ doc.metadata = { ...doc.metadata, ...metadata };
6899
+ doc.updatedAt = Date.now();
6900
+ this.dirty = true;
6901
+ return true;
6902
+ }
6903
+ /** Remove a document by ID */
6904
+ remove(id) {
6905
+ const before = this.documents.length;
6906
+ this.documents = this.documents.filter((d) => d.id !== id);
6907
+ if (this.documents.length !== before) {
6908
+ this.dirty = true;
6909
+ return true;
6910
+ }
6911
+ return false;
6912
+ }
6913
+ /** Get a document by ID */
6914
+ get(id) {
6915
+ return this.documents.find((d) => d.id === id);
6916
+ }
6917
+ /** Check if a document exists */
6918
+ has(id) {
6919
+ return this.documents.some((d) => d.id === id);
6920
+ }
6921
+ /** Number of documents in the store */
6922
+ get size() {
6923
+ return this.documents.length;
6924
+ }
6925
+ /* ── Search ────────────────────────────────────────────────────────────── */
6926
+ /**
6927
+ * Find the top-K most similar documents to a query vector.
6928
+ * Only returns documents above the similarity threshold.
6929
+ */
6930
+ search(queryVector, topK = 5, threshold = 0.5) {
6931
+ const scored = [];
6932
+ for (const doc of this.documents) {
6933
+ const score = cosineSimilarity(queryVector, doc.vector);
6934
+ if (score >= threshold) {
6935
+ scored.push({
6936
+ id: doc.id,
6937
+ text: doc.text,
6938
+ metadata: doc.metadata,
6939
+ score
6940
+ });
6941
+ }
6942
+ }
6943
+ scored.sort((a, b) => {
6944
+ if (Math.abs(a.score - b.score) > 1e-3) return b.score - a.score;
6945
+ const docA = this.documents.find((d) => d.id === a.id);
6946
+ const docB = this.documents.find((d) => d.id === b.id);
6947
+ return docB.updatedAt - docA.updatedAt;
6948
+ });
6949
+ return scored.slice(0, topK);
6950
+ }
6951
+ /* ── Internal ──────────────────────────────────────────────────────────── */
6952
+ /** Remove oldest entries to stay within maxEntries limit */
6953
+ prune() {
6954
+ const sorted = [...this.documents].sort((a, b) => a.updatedAt - b.updatedAt);
6955
+ const excess = sorted.length - this.maxEntries;
6956
+ if (excess > 0) {
6957
+ const toRemove = new Set(sorted.slice(0, excess).map((d) => d.id));
6958
+ this.documents = this.documents.filter((d) => !toRemove.has(d.id));
6959
+ }
6960
+ }
6961
+ };
6962
+ }
6963
+ });
6964
+
6965
+ // src/rag/code-corpus.ts
6966
+ function generateId(instruction, url) {
6967
+ const normalized = `${instruction.toLowerCase().trim()}|${normalizeUrl2(url)}`;
6968
+ return (0, import_crypto5.createHash)("sha256").update(normalized).digest("hex").slice(0, 16);
6969
+ }
6970
+ function normalizeUrl2(url) {
6971
+ if (!url) return "";
6972
+ try {
6973
+ const u = new URL(url);
6974
+ const path37 = u.pathname.replace(/\/\d+/g, "/*");
6975
+ return `${u.hostname}${path37}`;
6976
+ } catch {
6977
+ return url;
6978
+ }
6979
+ }
6980
+ function boostBySuccess(score, success, fail) {
6981
+ const total = success + fail;
6982
+ if (total === 0) return score;
6983
+ const successRate = success / total;
6984
+ return score * (1 + 0.05 * successRate);
6985
+ }
6986
+ function toMatch(doc) {
6987
+ return {
6988
+ id: doc.id,
6989
+ instruction: doc.metadata.instruction,
6990
+ code: doc.metadata.code,
6991
+ urlPattern: doc.metadata.urlPattern,
6992
+ score: doc.score,
6993
+ successCount: doc.metadata.successCount,
6994
+ failCount: doc.metadata.failCount
6995
+ };
6996
+ }
6997
+ var import_crypto5, MAX_CONSECUTIVE_FAILS, CodeCorpus;
6998
+ var init_code_corpus = __esm({
6999
+ "src/rag/code-corpus.ts"() {
7000
+ "use strict";
7001
+ init_cjs_shims();
7002
+ import_crypto5 = require("crypto");
7003
+ MAX_CONSECUTIVE_FAILS = 2;
7004
+ CodeCorpus = class {
7005
+ constructor(store, embedder) {
7006
+ this.store = store;
7007
+ this.embedder = embedder;
7008
+ }
7009
+ /* ── Ingest ────────────────────────────────────────────────────────────── */
7010
+ /**
7011
+ * Add a successful instruction → code mapping to the corpus.
7012
+ * Called after a step passes execution.
7013
+ */
7014
+ ingest(instruction, code, url) {
7015
+ const id = generateId(instruction, url);
7016
+ const urlPattern = normalizeUrl2(url);
7017
+ const embedText = `${instruction} ${urlPattern}`;
7018
+ const existing = this.store.get(id);
7019
+ if (existing) {
7020
+ this.store.update(id, {
7021
+ ...existing.metadata,
7022
+ code,
7023
+ successCount: existing.metadata.successCount + 1,
7024
+ consecutiveFails: 0
7025
+ });
7026
+ return;
7027
+ }
7028
+ this.embedder.addDocument(embedText);
7029
+ const vector = this.embedder.embed(embedText);
7030
+ this.store.add(id, embedText, vector, {
7031
+ instruction,
7032
+ code,
7033
+ urlPattern,
7034
+ successCount: 1,
7035
+ failCount: 0,
7036
+ consecutiveFails: 0
7037
+ });
7038
+ }
7039
+ /* ── Retrieve ──────────────────────────────────────────────────────────── */
7040
+ /**
7041
+ * Find similar past instructions in the corpus.
7042
+ * Returns matches ranked by similarity score.
7043
+ */
7044
+ retrieve(instruction, url = "", topK = 3, threshold = 0.65) {
7045
+ if (this.store.size === 0) return [];
7046
+ const urlPattern = normalizeUrl2(url);
7047
+ const queryText = `${instruction} ${urlPattern}`;
7048
+ const queryVector = this.embedder.embed(queryText);
7049
+ const results = this.store.search(queryVector, topK * 2, threshold);
7050
+ const boosted = results.map((r) => ({
7051
+ ...r,
7052
+ score: boostBySuccess(r.score, r.metadata.successCount, r.metadata.failCount)
7053
+ }));
7054
+ boosted.sort((a, b) => b.score - a.score);
7055
+ return boosted.slice(0, topK).map(toMatch);
7056
+ }
7057
+ /* ── Feedback ──────────────────────────────────────────────────────────── */
7058
+ /**
7059
+ * Record the outcome of using a corpus entry.
7060
+ * Removes entries that fail too many times consecutively.
7061
+ */
7062
+ recordOutcome(id, passed) {
7063
+ const doc = this.store.get(id);
7064
+ if (!doc) return;
7065
+ if (passed) {
7066
+ this.store.update(id, {
7067
+ ...doc.metadata,
7068
+ successCount: doc.metadata.successCount + 1,
7069
+ consecutiveFails: 0
7070
+ });
7071
+ } else {
7072
+ const newConsecutive = doc.metadata.consecutiveFails + 1;
7073
+ if (newConsecutive >= MAX_CONSECUTIVE_FAILS) {
7074
+ this.store.remove(id);
7075
+ return;
7076
+ }
7077
+ this.store.update(id, {
7078
+ ...doc.metadata,
7079
+ failCount: doc.metadata.failCount + 1,
7080
+ consecutiveFails: newConsecutive
7081
+ });
7082
+ }
7083
+ }
7084
+ /** Number of entries in the corpus */
7085
+ get size() {
7086
+ return this.store.size;
7087
+ }
7088
+ };
7089
+ }
7090
+ });
7091
+
7092
+ // src/rag/healing-corpus.ts
7093
+ function generateId2(instruction, error) {
7094
+ const normalized = `${instruction.toLowerCase().trim()}|${normalizeError(error)}`;
7095
+ return (0, import_crypto6.createHash)("sha256").update(normalized).digest("hex").slice(0, 16);
7096
+ }
7097
+ function normalizeError(error) {
7098
+ return error.replace(/\d+ms/g, "Nms").replace(/\d+/g, "N").replace(/["'][^"']*["']/g, "'...'").replace(/\s+/g, " ").trim().slice(0, 200);
7099
+ }
7100
+ function normalizeUrl3(url) {
7101
+ if (!url) return "";
7102
+ try {
7103
+ const u = new URL(url);
7104
+ return `${u.hostname}${u.pathname.replace(/\/\d+/g, "/*")}`;
7105
+ } catch {
7106
+ return url;
7107
+ }
7108
+ }
7109
+ function toMatch2(doc) {
7110
+ return {
7111
+ id: doc.id,
7112
+ instruction: doc.metadata.instruction,
7113
+ error: doc.metadata.error,
7114
+ failedCode: doc.metadata.failedCode,
7115
+ healedCode: doc.metadata.healedCode,
7116
+ strategy: doc.metadata.strategy,
7117
+ level: doc.metadata.level,
7118
+ score: doc.score,
7119
+ successCount: doc.metadata.successCount
7120
+ };
7121
+ }
7122
+ var import_crypto6, HealingCorpus;
7123
+ var init_healing_corpus = __esm({
7124
+ "src/rag/healing-corpus.ts"() {
7125
+ "use strict";
7126
+ init_cjs_shims();
7127
+ import_crypto6 = require("crypto");
7128
+ HealingCorpus = class {
7129
+ constructor(store, embedder) {
7130
+ this.store = store;
7131
+ this.embedder = embedder;
7132
+ }
7133
+ /* ── Ingest ────────────────────────────────────────────────────────────── */
7134
+ /**
7135
+ * Record a successful healing event in the corpus.
7136
+ * Called when self-healing produces a working fix.
7137
+ */
7138
+ ingest(event) {
7139
+ const id = generateId2(event.instruction, event.error);
7140
+ const urlPattern = normalizeUrl3(event.url);
7141
+ const embedText = `${event.instruction} ${normalizeError(event.error)}`;
7142
+ const existing = this.store.get(id);
7143
+ if (existing) {
7144
+ this.store.update(id, {
7145
+ ...existing.metadata,
7146
+ healedCode: event.healedCode,
7147
+ strategy: event.strategy,
7148
+ level: event.level,
7149
+ successCount: existing.metadata.successCount + 1
7150
+ });
7151
+ return;
7152
+ }
7153
+ this.embedder.addDocument(embedText);
7154
+ const vector = this.embedder.embed(embedText);
7155
+ this.store.add(id, embedText, vector, {
7156
+ instruction: event.instruction,
7157
+ error: normalizeError(event.error),
7158
+ failedCode: event.failedCode,
7159
+ healedCode: event.healedCode,
7160
+ strategy: event.strategy,
7161
+ level: event.level,
7162
+ urlPattern,
7163
+ successCount: 1
7164
+ });
7165
+ }
7166
+ /* ── Retrieve ──────────────────────────────────────────────────────────── */
7167
+ /**
7168
+ * Find similar past healing events.
7169
+ * Searches by instruction + error pattern similarity.
7170
+ */
7171
+ retrieve(instruction, error, topK = 3, threshold = 0.6) {
7172
+ if (this.store.size === 0) return [];
7173
+ const queryText = `${instruction} ${normalizeError(error)}`;
7174
+ const queryVector = this.embedder.embed(queryText);
7175
+ const results = this.store.search(queryVector, topK, threshold);
7176
+ return results.map(toMatch2);
7177
+ }
7178
+ /** Number of entries */
7179
+ get size() {
7180
+ return this.store.size;
7181
+ }
7182
+ };
7183
+ }
7184
+ });
7185
+
7186
+ // src/rag/error-catalog.ts
7187
+ function generateId3(errorPattern, urlPattern) {
7188
+ const key = `${errorPattern}|${urlPattern}`;
7189
+ return (0, import_crypto7.createHash)("sha256").update(key).digest("hex").slice(0, 16);
7190
+ }
7191
+ function normalizeError2(error) {
7192
+ return error.replace(/\d+ms/g, "Nms").replace(/\d+/g, "N").replace(/["'][^"']*["']/g, "'...'").replace(/\s+/g, " ").trim().slice(0, 200);
7193
+ }
7194
+ function normalizeUrl4(url) {
7195
+ if (!url) return "";
7196
+ try {
7197
+ const u = new URL(url);
7198
+ return `${u.hostname}${u.pathname.replace(/\/\d+/g, "/*")}`;
7199
+ } catch {
7200
+ return url;
7201
+ }
7202
+ }
7203
+ function toWarning(doc) {
7204
+ const fixes = doc.metadata.commonFixes;
7205
+ return {
7206
+ errorPattern: doc.metadata.errorPattern,
7207
+ urlPattern: doc.metadata.urlPattern,
7208
+ frequency: doc.metadata.frequency,
7209
+ suggestedFix: fixes.length > 0 ? fixes[fixes.length - 1] : null,
7210
+ score: doc.score
7211
+ };
7212
+ }
7213
+ var import_crypto7, ErrorCatalog;
7214
+ var init_error_catalog = __esm({
7215
+ "src/rag/error-catalog.ts"() {
7216
+ "use strict";
7217
+ init_cjs_shims();
7218
+ import_crypto7 = require("crypto");
7219
+ ErrorCatalog = class {
7220
+ constructor(store, embedder) {
7221
+ this.store = store;
7222
+ this.embedder = embedder;
7223
+ }
7224
+ /* ── Record ────────────────────────────────────────────────────────────── */
7225
+ /**
7226
+ * Record a test failure. If the error pattern already exists,
7227
+ * increment frequency. If a fix is provided, add it to commonFixes.
7228
+ */
7229
+ record(error, url, fixCode) {
7230
+ const errorPattern = normalizeError2(error);
7231
+ const urlPattern = normalizeUrl4(url);
7232
+ const id = generateId3(errorPattern, urlPattern);
7233
+ const embedText = `${errorPattern} ${urlPattern}`;
7234
+ const existing = this.store.get(id);
7235
+ if (existing) {
7236
+ const fixes = [...existing.metadata.commonFixes];
7237
+ if (fixCode && !fixes.includes(fixCode)) {
7238
+ fixes.push(fixCode);
7239
+ if (fixes.length > 5) fixes.shift();
7240
+ }
7241
+ this.store.update(id, {
7242
+ ...existing.metadata,
7243
+ frequency: existing.metadata.frequency + 1,
7244
+ lastSeen: Date.now(),
7245
+ commonFixes: fixes
7246
+ });
7247
+ return;
7248
+ }
7249
+ this.embedder.addDocument(embedText);
7250
+ const vector = this.embedder.embed(embedText);
7251
+ this.store.add(id, embedText, vector, {
7252
+ errorPattern,
7253
+ urlPattern,
7254
+ frequency: 1,
7255
+ lastSeen: Date.now(),
7256
+ commonFixes: fixCode ? [fixCode] : []
7257
+ });
7258
+ }
7259
+ /* ── Retrieve ──────────────────────────────────────────────────────────── */
7260
+ /**
7261
+ * Get known error patterns for a URL.
7262
+ * Used during code generation to warn the AI about known pitfalls.
7263
+ */
7264
+ getWarnings(url, topK = 5) {
7265
+ if (this.store.size === 0) return [];
7266
+ const urlPattern = normalizeUrl4(url);
7267
+ const queryVector = this.embedder.embed(urlPattern);
7268
+ const results = this.store.search(queryVector, topK * 2, 0.3);
7269
+ const recurring = results.filter((r) => r.metadata.frequency >= 2);
7270
+ recurring.sort((a, b) => b.metadata.frequency - a.metadata.frequency);
7271
+ return recurring.slice(0, topK).map(toWarning);
7272
+ }
7273
+ /** Number of tracked patterns */
7274
+ get size() {
7275
+ return this.store.size;
7276
+ }
7277
+ };
7278
+ }
7279
+ });
7280
+
7281
+ // src/rag/rag-manager.ts
7282
+ var import_path19, import_fs_extra16, logger21, RagManager;
7283
+ var init_rag_manager = __esm({
7284
+ "src/rag/rag-manager.ts"() {
7285
+ "use strict";
7286
+ init_cjs_shims();
7287
+ import_path19 = require("path");
7288
+ import_fs_extra16 = require("fs-extra");
7289
+ init_embedder();
7290
+ init_vector_store();
7291
+ init_code_corpus();
7292
+ init_healing_corpus();
7293
+ init_error_catalog();
7294
+ init_logger();
7295
+ logger21 = createChildLogger("rag");
7296
+ RagManager = class _RagManager {
7297
+ codeCorpus;
7298
+ healingCorpus;
7299
+ errorCatalog;
7300
+ embedder;
7301
+ codeStore;
7302
+ healingStore;
7303
+ errorStore;
7304
+ constructor(embedder, codeStore, healingStore, errorStore) {
7305
+ this.embedder = embedder;
7306
+ this.codeStore = codeStore;
7307
+ this.healingStore = healingStore;
7308
+ this.errorStore = errorStore;
7309
+ this.codeCorpus = new CodeCorpus(codeStore, embedder);
7310
+ this.healingCorpus = new HealingCorpus(healingStore, embedder);
7311
+ this.errorCatalog = new ErrorCatalog(errorStore, embedder);
7312
+ }
7313
+ /* ── Factory ───────────────────────────────────────────────────────────── */
7314
+ /**
7315
+ * Create and initialize a RagManager.
7316
+ * Loads all corpora from disk. Safe to call if files don't exist yet.
7317
+ */
7318
+ static async create(rootDir, config) {
7319
+ const ragDir = (0, import_path19.join)(rootDir, "results", ".rag");
7320
+ await (0, import_fs_extra16.ensureDir)(ragDir);
7321
+ logger21.info({ ragDir }, "Initializing RAG manager");
7322
+ const embedder = new TfIdfEmbedder((0, import_path19.join)(ragDir, "idf-vocab.json"));
7323
+ await embedder.load();
7324
+ const codeStore = new VectorStore(
7325
+ (0, import_path19.join)(ragDir, "code-corpus.json"),
7326
+ config.codeCorpus.maxEntries
7327
+ );
7328
+ const healingStore = new VectorStore(
7329
+ (0, import_path19.join)(ragDir, "healing-corpus.json"),
7330
+ config.healingCorpus.maxEntries
7331
+ );
7332
+ const errorStore = new VectorStore(
7333
+ (0, import_path19.join)(ragDir, "error-catalog.json"),
7334
+ config.errorCatalog.maxEntries
7335
+ );
7336
+ await Promise.all([
7337
+ codeStore.load(),
7338
+ healingStore.load(),
7339
+ errorStore.load()
7340
+ ]);
7341
+ logger21.info(
7342
+ {
7343
+ codeCorpus: codeStore.size,
7344
+ healingCorpus: healingStore.size,
7345
+ errorCatalog: errorStore.size,
7346
+ idfDocuments: embedder.documentCount
7347
+ },
7348
+ "RAG corpora loaded"
7349
+ );
7350
+ return new _RagManager(embedder, codeStore, healingStore, errorStore);
7351
+ }
7352
+ /* ── Persistence ───────────────────────────────────────────────────────── */
7353
+ /** Persist all corpora and the embedder vocabulary to disk */
7354
+ async persist() {
7355
+ await Promise.all([
7356
+ this.embedder.save(),
7357
+ this.codeStore.persist(),
7358
+ this.healingStore.persist(),
7359
+ this.errorStore.persist()
7360
+ ]);
7361
+ logger21.debug("RAG corpora persisted");
7362
+ }
7363
+ /* ── Stats ─────────────────────────────────────────────────────────────── */
7364
+ getStats() {
7365
+ return {
7366
+ codeCorpus: this.codeStore.size,
7367
+ healingCorpus: this.healingStore.size,
7368
+ errorCatalog: this.errorStore.size,
7369
+ idfVocab: this.embedder.documentCount
7370
+ };
7371
+ }
7372
+ };
7373
+ }
7374
+ });
7375
+
6513
7376
  // src/mcp/proactive-checker.ts
6514
7377
  function extractSelectors(code) {
6515
7378
  const results = [];
@@ -6597,7 +7460,7 @@ async function checkSelectors(mcpSession, url, code) {
6597
7460
  await mcpSession.navigate(url);
6598
7461
  const raw = await mcpSession.snapshot();
6599
7462
  if (!raw) {
6600
- logger21.warn({ url }, "Proactive check: snapshot returned null \u2014 skipping validation");
7463
+ logger22.warn({ url }, "Proactive check: snapshot returned null \u2014 skipping validation");
6601
7464
  return { url, ok: true, total: selectors.length, matched: 0, missing: 0, checks: [] };
6602
7465
  }
6603
7466
  const parsed = parseSnapshot(raw);
@@ -6638,7 +7501,7 @@ async function checkSelectors(mcpSession, url, code) {
6638
7501
  }
6639
7502
  const matched = checks.filter((c) => c.found).length;
6640
7503
  const missing = checks.filter((c) => !c.found).length;
6641
- logger21.info(
7504
+ logger22.info(
6642
7505
  { url, total: checks.length, matched, missing },
6643
7506
  "Proactive selector check complete"
6644
7507
  );
@@ -6651,14 +7514,14 @@ async function checkSelectors(mcpSession, url, code) {
6651
7514
  checks
6652
7515
  };
6653
7516
  }
6654
- var logger21, SELECTOR_PATTERNS;
7517
+ var logger22, SELECTOR_PATTERNS;
6655
7518
  var init_proactive_checker = __esm({
6656
7519
  "src/mcp/proactive-checker.ts"() {
6657
7520
  "use strict";
6658
7521
  init_cjs_shims();
6659
7522
  init_snapshot_parser();
6660
7523
  init_logger();
6661
- logger21 = createChildLogger("proactive-checker");
7524
+ logger22 = createChildLogger("proactive-checker");
6662
7525
  SELECTOR_PATTERNS = [
6663
7526
  // getByRole('button', { name: 'Login' })
6664
7527
  { regex: /getByRole\(\s*['"](\w+)['"]\s*,\s*\{[^}]*name:\s*['"]([^'"]+)['"]/g, type: "getByRole" },
@@ -6675,14 +7538,14 @@ var init_proactive_checker = __esm({
6675
7538
  });
6676
7539
 
6677
7540
  // src/engine/browser-manager.ts
6678
- var import_playwright, logger22, BrowserManager;
7541
+ var import_playwright, logger23, BrowserManager;
6679
7542
  var init_browser_manager = __esm({
6680
7543
  "src/engine/browser-manager.ts"() {
6681
7544
  "use strict";
6682
7545
  init_cjs_shims();
6683
7546
  import_playwright = require("playwright");
6684
7547
  init_logger();
6685
- logger22 = createChildLogger("browser-manager");
7548
+ logger23 = createChildLogger("browser-manager");
6686
7549
  BrowserManager = class {
6687
7550
  browsers = /* @__PURE__ */ new Map();
6688
7551
  // ─── Launch ───────────────────────────────────────────────────────────────
@@ -6692,14 +7555,14 @@ var init_browser_manager = __esm({
6692
7555
  const launcher = browserName === "firefox" ? import_playwright.firefox : browserName === "webkit" ? import_playwright.webkit : import_playwright.chromium;
6693
7556
  const browser = await launcher.launch({ headless: config.headless });
6694
7557
  this.browsers.set(browserName, browser);
6695
- logger22.info({ browser: browserName, headless: config.headless }, "Browser launched");
7558
+ logger23.info({ browser: browserName, headless: config.headless }, "Browser launched");
6696
7559
  return browser;
6697
7560
  }
6698
7561
  // ─── Context ──────────────────────────────────────────────────────────────
6699
7562
  async newContext(browser, config, videosDir) {
6700
7563
  const deviceDescriptor = config.device && import_playwright.devices[config.device] ? import_playwright.devices[config.device] : null;
6701
7564
  if (config.device && !deviceDescriptor) {
6702
- logger22.warn({ device: config.device }, "Unknown Playwright device \u2014 falling back to configured viewport");
7565
+ logger23.warn({ device: config.device }, "Unknown Playwright device \u2014 falling back to configured viewport");
6703
7566
  }
6704
7567
  const contextOptions = {
6705
7568
  // Do NOT set baseURL here — it causes Playwright to pre-navigate the page
@@ -6812,7 +7675,7 @@ var init_browser_manager = __esm({
6812
7675
  close: async () => {
6813
7676
  }
6814
7677
  };
6815
- logger22.info("Created API-only request context (zero browser)");
7678
+ logger23.info("Created API-only request context (zero browser)");
6816
7679
  return fakePage;
6817
7680
  }
6818
7681
  /** Closes all API request contexts. */
@@ -6837,11 +7700,11 @@ var init_browser_manager = __esm({
6837
7700
  await context.tracing.stop();
6838
7701
  }
6839
7702
  } catch (err) {
6840
- logger22.debug({ err }, "Tracing stop failed \u2014 ignoring");
7703
+ logger23.debug({ err }, "Tracing stop failed \u2014 ignoring");
6841
7704
  }
6842
7705
  }
6843
7706
  await context.close().catch((err) => {
6844
- logger22.debug({ err }, "Context close failed \u2014 ignoring");
7707
+ logger23.debug({ err }, "Context close failed \u2014 ignoring");
6845
7708
  });
6846
7709
  }
6847
7710
  /** Closes all open browsers and API contexts. Called at the end of every run. */
@@ -6850,9 +7713,9 @@ var init_browser_manager = __esm({
6850
7713
  for (const [name, browser] of this.browsers) {
6851
7714
  try {
6852
7715
  await browser.close();
6853
- logger22.debug({ browser: name }, "Browser closed");
7716
+ logger23.debug({ browser: name }, "Browser closed");
6854
7717
  } catch (err) {
6855
- logger22.warn({ err, browser: name }, "Error closing browser");
7718
+ logger23.warn({ err, browser: name }, "Error closing browser");
6856
7719
  }
6857
7720
  }
6858
7721
  this.browsers.clear();
@@ -6865,13 +7728,13 @@ var init_browser_manager = __esm({
6865
7728
  });
6866
7729
 
6867
7730
  // src/engine/healing-budget.ts
6868
- var logger23, ENVIRONMENTAL_PATTERNS, HealingBudget;
7731
+ var logger24, ENVIRONMENTAL_PATTERNS, HealingBudget;
6869
7732
  var init_healing_budget = __esm({
6870
7733
  "src/engine/healing-budget.ts"() {
6871
7734
  "use strict";
6872
7735
  init_cjs_shims();
6873
7736
  init_logger();
6874
- logger23 = createChildLogger("healing-budget");
7737
+ logger24 = createChildLogger("healing-budget");
6875
7738
  ENVIRONMENTAL_PATTERNS = [
6876
7739
  /timeout/i,
6877
7740
  /ECONNREFUSED/i,
@@ -6894,7 +7757,7 @@ var init_healing_budget = __esm({
6894
7757
  // ─── Main decision ────────────────────────────────────────────────────────
6895
7758
  shouldHeal(failure) {
6896
7759
  if (this.spent >= this.dailyBudget) {
6897
- logger23.warn(
7760
+ logger24.warn(
6898
7761
  { spent: this.spent, budget: this.dailyBudget },
6899
7762
  "Daily healing budget exhausted"
6900
7763
  );
@@ -6928,7 +7791,7 @@ var init_healing_budget = __esm({
6928
7791
  /** Call after each AI healing call with the estimated cost. */
6929
7792
  recordSpend(amountUsd) {
6930
7793
  this.spent += amountUsd;
6931
- logger23.debug({ spend: amountUsd, total: this.spent }, "Healing budget spend recorded");
7794
+ logger24.debug({ spend: amountUsd, total: this.spent }, "Healing budget spend recorded");
6932
7795
  }
6933
7796
  /** Call when a healing attempt itself fails (AI returned bad code, etc.). */
6934
7797
  recordHealFailure(stepId) {
@@ -6950,21 +7813,21 @@ var init_healing_budget = __esm({
6950
7813
  });
6951
7814
 
6952
7815
  // src/engine/healing-report.ts
6953
- var import_crypto4, logger24, HealingReporter;
7816
+ var import_crypto8, logger25, HealingReporter;
6954
7817
  var init_healing_report = __esm({
6955
7818
  "src/engine/healing-report.ts"() {
6956
7819
  "use strict";
6957
7820
  init_cjs_shims();
6958
- import_crypto4 = require("crypto");
7821
+ import_crypto8 = require("crypto");
6959
7822
  init_healing_store();
6960
7823
  init_logger();
6961
- logger24 = createChildLogger("healing-report");
7824
+ logger25 = createChildLogger("healing-report");
6962
7825
  HealingReporter = class {
6963
7826
  events = [];
6964
7827
  /** Records a successful healing event (called by executor after each healed step). */
6965
7828
  record(input) {
6966
7829
  const event = {
6967
- id: (0, import_crypto4.randomUUID)(),
7830
+ id: (0, import_crypto8.randomUUID)(),
6968
7831
  runId: input.runId,
6969
7832
  suiteId: input.suiteId,
6970
7833
  caseId: input.caseId,
@@ -6980,7 +7843,7 @@ var init_healing_report = __esm({
6980
7843
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6981
7844
  };
6982
7845
  this.events.push(event);
6983
- logger24.info(
7846
+ logger25.info(
6984
7847
  { stepId: input.stepId, strategy: input.strategy, level: input.level },
6985
7848
  "Healing event recorded"
6986
7849
  );
@@ -7006,15 +7869,15 @@ var init_healing_report = __esm({
7006
7869
  };
7007
7870
  try {
7008
7871
  await writeHealingReport(rootDir, report);
7009
- logger24.info({ runId, count: this.events.length }, "Healing report written");
7872
+ logger25.info({ runId, count: this.events.length }, "Healing report written");
7010
7873
  } catch (err) {
7011
- logger24.error({ err, runId }, "Failed to write healing report");
7874
+ logger25.error({ err, runId }, "Failed to write healing report");
7012
7875
  }
7013
7876
  for (const event of this.events) {
7014
7877
  try {
7015
7878
  await appendPendingEvent(rootDir, event);
7016
7879
  } catch (err) {
7017
- logger24.warn({ err, eventId: event.id }, "Failed to append pending healing event");
7880
+ logger25.warn({ err, eventId: event.id }, "Failed to append pending healing event");
7018
7881
  }
7019
7882
  }
7020
7883
  }
@@ -7206,40 +8069,40 @@ function buildCategoryHtml(caseName, pageLoads, category) {
7206
8069
  </body>
7207
8070
  </html>`;
7208
8071
  }
7209
- var import_path19, import_crypto5, import_fs_extra14, logger25, AllureReporter;
8072
+ var import_path20, import_crypto9, import_fs_extra17, logger26, AllureReporter;
7210
8073
  var init_allure_reporter = __esm({
7211
8074
  "src/engine/allure-reporter.ts"() {
7212
8075
  "use strict";
7213
8076
  init_cjs_shims();
7214
- import_path19 = __toESM(require("path"));
7215
- import_crypto5 = require("crypto");
7216
- import_fs_extra14 = __toESM(require("fs-extra"));
8077
+ import_path20 = __toESM(require("path"));
8078
+ import_crypto9 = require("crypto");
8079
+ import_fs_extra17 = __toESM(require("fs-extra"));
7217
8080
  init_logger();
7218
- logger25 = createChildLogger("allure-reporter");
8081
+ logger26 = createChildLogger("allure-reporter");
7219
8082
  AllureReporter = class {
7220
8083
  resultsDir;
7221
8084
  environment;
7222
8085
  constructor(rootDir, runId, environment = "dev") {
7223
- this.resultsDir = import_path19.default.join(rootDir, "results", "allure-results", runId);
8086
+ this.resultsDir = import_path20.default.join(rootDir, "results", "allure-results", runId);
7224
8087
  this.environment = environment;
7225
8088
  }
7226
8089
  /** Writes one allure result JSON per test case + environment.properties. */
7227
8090
  async writeResults(runResult) {
7228
- await import_fs_extra14.default.ensureDir(this.resultsDir);
8091
+ await import_fs_extra17.default.ensureDir(this.resultsDir);
7229
8092
  const envProps = [
7230
8093
  `Environment=${this.environment.toUpperCase()}`,
7231
8094
  `Framework=Assuremind`
7232
8095
  ].join("\n");
7233
- await import_fs_extra14.default.writeFile(import_path19.default.join(this.resultsDir, "environment.properties"), envProps, "utf-8");
8096
+ await import_fs_extra17.default.writeFile(import_path20.default.join(this.resultsDir, "environment.properties"), envProps, "utf-8");
7234
8097
  let logFileName;
7235
8098
  if (runResult.logFilePath) {
7236
8099
  try {
7237
- logFileName = import_path19.default.basename(runResult.logFilePath);
8100
+ logFileName = import_path20.default.basename(runResult.logFilePath);
7238
8101
  await this.copyAttachment(runResult.logFilePath).catch(() => {
7239
8102
  });
7240
- logger25.debug({ logFile: logFileName }, "Log file copied to Allure results");
8103
+ logger26.debug({ logFile: logFileName }, "Log file copied to Allure results");
7241
8104
  } catch {
7242
- logger25.debug("Failed to copy log file to Allure results");
8105
+ logger26.debug("Failed to copy log file to Allure results");
7243
8106
  }
7244
8107
  }
7245
8108
  let written = 0;
@@ -7249,23 +8112,23 @@ var init_allure_reporter = __esm({
7249
8112
  written++;
7250
8113
  }
7251
8114
  }
7252
- logger25.info({ dir: this.resultsDir, written }, "Allure results written");
8115
+ logger26.info({ dir: this.resultsDir, written }, "Allure results written");
7253
8116
  }
7254
8117
  // ─── Internal ─────────────────────────────────────────────────────────────
7255
8118
  async writeTestResult(suite, tc, logFileName) {
7256
- const uuid = (0, import_crypto5.randomUUID)();
8119
+ const uuid = (0, import_crypto9.randomUUID)();
7257
8120
  const start = new Date(tc.startedAt).getTime();
7258
8121
  const stop2 = new Date(tc.finishedAt).getTime();
7259
8122
  const failedStep = tc.steps.find((s) => s.status === "failed");
7260
8123
  const testAttachments = [];
7261
8124
  if (tc.videoPath) {
7262
- const videoName = import_path19.default.basename(tc.videoPath);
7263
- const ext = import_path19.default.extname(videoName).toLowerCase();
8125
+ const videoName = import_path20.default.basename(tc.videoPath);
8126
+ const ext = import_path20.default.extname(videoName).toLowerCase();
7264
8127
  const mime = ext === ".mp4" ? "video/mp4" : "video/webm";
7265
8128
  testAttachments.push({ name: "Video Recording", source: videoName, type: mime });
7266
8129
  }
7267
8130
  if (tc.tracePath) {
7268
- const traceName = import_path19.default.basename(tc.tracePath);
8131
+ const traceName = import_path20.default.basename(tc.tracePath);
7269
8132
  testAttachments.push({ name: "Playwright Trace", source: traceName, type: "application/zip" });
7270
8133
  }
7271
8134
  if (logFileName) {
@@ -7279,19 +8142,19 @@ var init_allure_reporter = __esm({
7279
8142
  if (hasPerf) {
7280
8143
  const html = buildCategoryHtml(tc.caseName, pageLoads, "performance");
7281
8144
  const file = `${uuid}-perf.html`;
7282
- await import_fs_extra14.default.writeFile(import_path19.default.join(this.resultsDir, file), html, "utf-8");
8145
+ await import_fs_extra17.default.writeFile(import_path20.default.join(this.resultsDir, file), html, "utf-8");
7283
8146
  testAttachments.push({ name: "\u26A1 Performance Report", source: file, type: "text/html" });
7284
8147
  }
7285
8148
  if (hasA11y) {
7286
8149
  const html = buildCategoryHtml(tc.caseName, pageLoads, "accessibility");
7287
8150
  const file = `${uuid}-a11y.html`;
7288
- await import_fs_extra14.default.writeFile(import_path19.default.join(this.resultsDir, file), html, "utf-8");
8151
+ await import_fs_extra17.default.writeFile(import_path20.default.join(this.resultsDir, file), html, "utf-8");
7289
8152
  testAttachments.push({ name: "\u267F Accessibility Report", source: file, type: "text/html" });
7290
8153
  }
7291
8154
  if (hasSeo) {
7292
8155
  const html = buildCategoryHtml(tc.caseName, pageLoads, "seo");
7293
8156
  const file = `${uuid}-seo.html`;
7294
- await import_fs_extra14.default.writeFile(import_path19.default.join(this.resultsDir, file), html, "utf-8");
8157
+ await import_fs_extra17.default.writeFile(import_path20.default.join(this.resultsDir, file), html, "utf-8");
7295
8158
  testAttachments.push({ name: "\u{1F50D} SEO Report", source: file, type: "text/html" });
7296
8159
  }
7297
8160
  }
@@ -7319,8 +8182,8 @@ var init_allure_reporter = __esm({
7319
8182
  statusDetails: { message: failedStep.error }
7320
8183
  }
7321
8184
  };
7322
- const filePath = import_path19.default.join(this.resultsDir, `${uuid}-result.json`);
7323
- await import_fs_extra14.default.writeJson(filePath, result, { spaces: 2 });
8185
+ const filePath = import_path20.default.join(this.resultsDir, `${uuid}-result.json`);
8186
+ await import_fs_extra17.default.writeJson(filePath, result, { spaces: 2 });
7324
8187
  for (const step of tc.steps) {
7325
8188
  if (step.screenshotPath) {
7326
8189
  await this.copyAttachment(step.screenshotPath).catch(() => {
@@ -7341,7 +8204,7 @@ var init_allure_reporter = __esm({
7341
8204
  const stepStop = caseStart + step.duration;
7342
8205
  const attachments = [];
7343
8206
  if (step.screenshotPath) {
7344
- const name = import_path19.default.basename(step.screenshotPath);
8207
+ const name = import_path20.default.basename(step.screenshotPath);
7345
8208
  attachments.push({ name: "Screenshot", source: name, type: "image/png" });
7346
8209
  }
7347
8210
  return {
@@ -7356,8 +8219,8 @@ var init_allure_reporter = __esm({
7356
8219
  };
7357
8220
  }
7358
8221
  async copyAttachment(screenshotPath) {
7359
- const dest = import_path19.default.join(this.resultsDir, import_path19.default.basename(screenshotPath));
7360
- await import_fs_extra14.default.copy(screenshotPath, dest, { overwrite: true });
8222
+ const dest = import_path20.default.join(this.resultsDir, import_path20.default.basename(screenshotPath));
8223
+ await import_fs_extra17.default.copy(screenshotPath, dest, { overwrite: true });
7361
8224
  }
7362
8225
  };
7363
8226
  }
@@ -7365,15 +8228,15 @@ var init_allure_reporter = __esm({
7365
8228
 
7366
8229
  // src/engine/allure-generator.ts
7367
8230
  async function generateAllureReport(rootDir, runId) {
7368
- const resultsDir = import_path20.default.join(rootDir, "results", "allure-results", runId);
7369
- const reportDir = import_path20.default.join(rootDir, "results", "allure-report", runId);
7370
- if (!await import_fs_extra15.default.pathExists(resultsDir)) {
7371
- logger26.debug({ resultsDir }, "No allure-results dir for run \u2014 skipping report generation");
8231
+ const resultsDir = import_path21.default.join(rootDir, "results", "allure-results", runId);
8232
+ const reportDir = import_path21.default.join(rootDir, "results", "allure-report", runId);
8233
+ if (!await import_fs_extra18.default.pathExists(resultsDir)) {
8234
+ logger27.debug({ resultsDir }, "No allure-results dir for run \u2014 skipping report generation");
7372
8235
  return false;
7373
8236
  }
7374
- const files = await import_fs_extra15.default.readdir(resultsDir);
8237
+ const files = await import_fs_extra18.default.readdir(resultsDir);
7375
8238
  if (files.length === 0) {
7376
- logger26.debug({ runId }, "allure-results is empty \u2014 skipping report generation");
8239
+ logger27.debug({ runId }, "allure-results is empty \u2014 skipping report generation");
7377
8240
  return false;
7378
8241
  }
7379
8242
  try {
@@ -7382,34 +8245,34 @@ async function generateAllureReport(rootDir, runId) {
7382
8245
  ["generate", resultsDir, "--output", reportDir, "--clean", "--single-file"],
7383
8246
  isWindows ? { shell: true } : {}
7384
8247
  );
7385
- logger26.info({ reportDir, runId }, "Allure HTML report generated");
8248
+ logger27.info({ reportDir, runId }, "Allure HTML report generated");
7386
8249
  return true;
7387
8250
  } catch (err) {
7388
8251
  const msg = err instanceof Error ? err.message : String(err);
7389
8252
  if (msg.includes("java") || msg.includes("Java") || msg.includes("JAVA")) {
7390
- logger26.warn(
8253
+ logger27.warn(
7391
8254
  "Allure report generation skipped \u2014 Java is not installed. Install Java (https://adoptium.net) to enable Allure HTML reports."
7392
8255
  );
7393
8256
  } else {
7394
- logger26.warn({ err: msg, runId }, "Allure report generation failed");
8257
+ logger27.warn({ err: msg, runId }, "Allure report generation failed");
7395
8258
  }
7396
8259
  return false;
7397
8260
  }
7398
8261
  }
7399
- var import_path20, import_fs_extra15, import_child_process2, import_util, execFileAsync, logger26, isWindows, alluireBin;
8262
+ var import_path21, import_fs_extra18, import_child_process2, import_util, execFileAsync, logger27, isWindows, alluireBin;
7400
8263
  var init_allure_generator = __esm({
7401
8264
  "src/engine/allure-generator.ts"() {
7402
8265
  "use strict";
7403
8266
  init_cjs_shims();
7404
- import_path20 = __toESM(require("path"));
7405
- import_fs_extra15 = __toESM(require("fs-extra"));
8267
+ import_path21 = __toESM(require("path"));
8268
+ import_fs_extra18 = __toESM(require("fs-extra"));
7406
8269
  import_child_process2 = require("child_process");
7407
8270
  import_util = require("util");
7408
8271
  init_logger();
7409
8272
  execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
7410
- logger26 = createChildLogger("allure-generator");
8273
+ logger27 = createChildLogger("allure-generator");
7411
8274
  isWindows = process.platform === "win32";
7412
- alluireBin = import_path20.default.resolve(
8275
+ alluireBin = import_path21.default.resolve(
7413
8276
  __dirname,
7414
8277
  isWindows ? "../../node_modules/allure-commandline/dist/bin/allure.bat" : "../../node_modules/.bin/allure"
7415
8278
  );
@@ -7954,7 +8817,7 @@ function parseMultiStrategies(raw) {
7954
8817
  }
7955
8818
  return stripped.split("\n").map((s) => s.trim()).filter((s) => s.length > 0 && s.startsWith("await"));
7956
8819
  }
7957
- var logger27, SelfHealer;
8820
+ var logger28, SelfHealer;
7958
8821
  var init_self_healing = __esm({
7959
8822
  "src/engine/self-healing.ts"() {
7960
8823
  "use strict";
@@ -7966,11 +8829,12 @@ var init_self_healing = __esm({
7966
8829
  init_logger();
7967
8830
  init_code_runner();
7968
8831
  init_step_type_detector();
7969
- logger27 = createChildLogger("self-healing");
8832
+ logger28 = createChildLogger("self-healing");
7970
8833
  SelfHealer = class {
7971
- constructor(provider, maxLevel) {
8834
+ constructor(provider, maxLevel, ragManager) {
7972
8835
  this.provider = provider;
7973
8836
  this.maxLevel = maxLevel;
8837
+ this.ragManager = ragManager;
7974
8838
  }
7975
8839
  /**
7976
8840
  * Attempts to heal a failed step by cycling through levels 1–maxLevel.
@@ -7981,7 +8845,7 @@ var init_self_healing = __esm({
7981
8845
  */
7982
8846
  async heal(step, failure, previousSteps, page, variables, baseUrl) {
7983
8847
  if (failure.failureKind === "assertion") {
7984
- logger27.info(
8848
+ logger28.info(
7985
8849
  { stepId: step.id },
7986
8850
  "Assertion failure \u2014 skipping all healing levels (possible application bug)"
7987
8851
  );
@@ -7990,10 +8854,10 @@ var init_self_healing = __esm({
7990
8854
  const isApi = step.stepType === "api" || detectStepType(step.instruction) === "api";
7991
8855
  for (let level = 1; level <= this.maxLevel; level++) {
7992
8856
  if (isApi && (level === 3 || level === 4)) {
7993
- logger27.debug({ stepId: step.id, level }, `Skipping DOM-specific level ${level} for API step`);
8857
+ logger28.debug({ stepId: step.id, level }, `Skipping DOM-specific level ${level} for API step`);
7994
8858
  continue;
7995
8859
  }
7996
- logger27.debug({ stepId: step.id, level, isApi }, `Attempting heal level ${level}`);
8860
+ logger28.debug({ stepId: step.id, level, isApi }, `Attempting heal level ${level}`);
7997
8861
  try {
7998
8862
  const result = await this.tryLevel(
7999
8863
  level,
@@ -8006,14 +8870,14 @@ var init_self_healing = __esm({
8006
8870
  isApi
8007
8871
  );
8008
8872
  if (result !== null) {
8009
- logger27.info({ stepId: step.id, level, strategy: result.strategy }, "Healing succeeded");
8873
+ logger28.info({ stepId: step.id, level, strategy: result.strategy }, "Healing succeeded");
8010
8874
  return result;
8011
8875
  }
8012
8876
  } catch (err) {
8013
- logger27.debug({ err, level, stepId: step.id }, `Level ${level} healing attempt threw`);
8877
+ logger28.debug({ err, level, stepId: step.id }, `Level ${level} healing attempt threw`);
8014
8878
  }
8015
8879
  }
8016
- logger27.warn({ stepId: step.id, maxLevel: this.maxLevel }, "All healing levels exhausted");
8880
+ logger28.warn({ stepId: step.id, maxLevel: this.maxLevel }, "All healing levels exhausted");
8017
8881
  return null;
8018
8882
  }
8019
8883
  // ─── Level dispatcher ─────────────────────────────────────────────────────
@@ -8048,7 +8912,7 @@ var init_self_healing = __esm({
8048
8912
  if (this.provider.supportsVision) {
8049
8913
  try {
8050
8914
  const som = await captureSetOfMarks(page);
8051
- logger27.debug({ stepId: step.id }, "Level 2: using SoM vision healing");
8915
+ logger28.debug({ stepId: step.id }, "Level 2: using SoM vision healing");
8052
8916
  const code2 = await this.provider.analyzeWithSoM(
8053
8917
  som.screenshot,
8054
8918
  som.elementMap,
@@ -8058,13 +8922,27 @@ var init_self_healing = __esm({
8058
8922
  await executeCode(sanitized2, page);
8059
8923
  return { code: sanitized2, level: 2, strategy: "regenerate" };
8060
8924
  } catch (visionErr) {
8061
- logger27.debug({ stepId: step.id, err: visionErr }, "SoM vision failed, falling back to text regeneration");
8925
+ logger28.debug({ stepId: step.id, err: visionErr }, "SoM vision failed, falling back to text regeneration");
8062
8926
  await cleanupSoMOverlay(page);
8063
8927
  }
8064
8928
  }
8065
8929
  const context = await extractPageContext(page, previousSteps, variables);
8930
+ let healingHint = "";
8931
+ if (this.ragManager) {
8932
+ try {
8933
+ const matches = this.ragManager.healingCorpus.retrieve(step.instruction, failure.error, 2);
8934
+ if (matches.length > 0) {
8935
+ healingHint = "\n\nSimilar past fixes that worked:\n" + matches.map(
8936
+ (m, i) => ` ${i + 1}. Error: ${m.error}
8937
+ Fix: ${m.healedCode}`
8938
+ ).join("\n");
8939
+ logger28.debug({ stepId: step.id, matches: matches.length }, "RAG healing corpus matches found");
8940
+ }
8941
+ } catch {
8942
+ }
8943
+ }
8066
8944
  const code = await this.provider.healSelector(
8067
- step.instruction,
8945
+ step.instruction + healingHint,
8068
8946
  failure.failedCode,
8069
8947
  failure.error,
8070
8948
  context
@@ -8114,12 +8992,12 @@ var init_self_healing = __esm({
8114
8992
  // Produces inspectable Playwright code (not pixel coordinates).
8115
8993
  async level4Visual(step, page, variables, baseUrl) {
8116
8994
  if (!this.provider.supportsVision) {
8117
- logger27.debug({ stepId: step.id }, "Skipping SoM visual healing \u2014 provider lacks vision");
8995
+ logger28.debug({ stepId: step.id }, "Skipping SoM visual healing \u2014 provider lacks vision");
8118
8996
  return null;
8119
8997
  }
8120
8998
  try {
8121
8999
  const som = await captureSetOfMarks(page);
8122
- logger27.debug(
9000
+ logger28.debug(
8123
9001
  { stepId: step.id, elementCount: som.elements.length },
8124
9002
  "Level 4: SoM screenshot captured"
8125
9003
  );
@@ -8133,7 +9011,7 @@ var init_self_healing = __esm({
8133
9011
  return { code: sanitized, level: 4, strategy: "visual" };
8134
9012
  } catch (err) {
8135
9013
  await cleanupSoMOverlay(page);
8136
- logger27.debug({ stepId: step.id, err }, "Level 4 SoM failed");
9014
+ logger28.debug({ stepId: step.id, err }, "Level 4 SoM failed");
8137
9015
  return null;
8138
9016
  }
8139
9017
  }
@@ -8158,7 +9036,7 @@ var init_self_healing = __esm({
8158
9036
  }
8159
9037
  // ─── Level 6: Manual (human in the loop) ─────────────────────────────────
8160
9038
  level6Manual(step, failure) {
8161
- logger27.warn(
9039
+ logger28.warn(
8162
9040
  { stepId: step.id, instruction: step.instruction, error: failure.error },
8163
9041
  "Level 6: manual intervention required \u2014 marking step for human review"
8164
9042
  );
@@ -8183,17 +9061,17 @@ async function executeStep(step, page, ctx) {
8183
9061
  const mergedVars = { ...ctx.variables, ...Object.fromEntries(ctx.sharedVariables) };
8184
9062
  const hasTokens = step.generatedCode.includes("{{");
8185
9063
  if (hasTokens) {
8186
- logger28.warn(
9064
+ logger29.warn(
8187
9065
  { stepId: step.id, variableKeys: Object.keys(mergedVars), variableCount: Object.keys(mergedVars).length },
8188
9066
  "Interpolating variables into step code"
8189
9067
  );
8190
9068
  }
8191
9069
  const code = interpolate(step.generatedCode, mergedVars, ctx.config.baseUrl);
8192
9070
  if (hasTokens) {
8193
- logger28.warn({ stepId: step.id, before: step.generatedCode, after: code }, "Variable interpolation result");
9071
+ logger29.warn({ stepId: step.id, before: step.generatedCode, after: code }, "Variable interpolation result");
8194
9072
  }
8195
9073
  if (!code.trim() && step.stepType !== "mock") {
8196
- logger28.warn({ stepId: step.id }, "Step has no generated code \u2014 skipping");
9074
+ logger29.warn({ stepId: step.id }, "Step has no generated code \u2014 skipping");
8197
9075
  return buildResult(step, "skipped", code, Date.now() - start);
8198
9076
  }
8199
9077
  if (step.stepType === "mock" && step.mockUrl && step.mockResponse) {
@@ -8218,7 +9096,7 @@ async function executeStep(step, page, ctx) {
8218
9096
  const runtimeCtx = {
8219
9097
  setVariable: (key, value) => {
8220
9098
  ctx.sharedVariables.set(key, value);
8221
- logger28.info({ stepId: step.id, key, value: value.slice(0, 50) }, "Runtime variable set");
9099
+ logger29.info({ stepId: step.id, key, value: value.slice(0, 50) }, "Runtime variable set");
8222
9100
  },
8223
9101
  getVariable: (key) => ctx.sharedVariables.get(key)
8224
9102
  };
@@ -8250,7 +9128,13 @@ async function executeStep(step, page, ctx) {
8250
9128
  } catch {
8251
9129
  }
8252
9130
  }
8253
- if (ctx.config.screenshot === "on") {
9131
+ if (ctx.ragManager && code.trim()) {
9132
+ try {
9133
+ ctx.ragManager.codeCorpus.ingest(step.instruction, code, page.url());
9134
+ } catch {
9135
+ }
9136
+ }
9137
+ if (ctx.config.screenshot === "on") {
8254
9138
  const screenshotPath = await captureScreenshot(page, ctx, step.id, "pass");
8255
9139
  return { ...buildResult(step, "passed", code, Date.now() - start, void 0, screenshotPath), navigatedToUrl, auditUrl };
8256
9140
  }
@@ -8258,7 +9142,7 @@ async function executeStep(step, page, ctx) {
8258
9142
  } catch (err) {
8259
9143
  lastErr = err;
8260
9144
  if (attempt < stepRetries) {
8261
- logger28.debug({ stepId: step.id, attempt, stepRetries }, "Step failed \u2014 retrying");
9145
+ logger29.debug({ stepId: step.id, attempt, stepRetries }, "Step failed \u2014 retrying");
8262
9146
  continue;
8263
9147
  }
8264
9148
  }
@@ -8266,7 +9150,7 @@ async function executeStep(step, page, ctx) {
8266
9150
  const firstErr = lastErr;
8267
9151
  {
8268
9152
  const errorMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
8269
- logger28.debug({ stepId: step.id, error: errorMsg }, "Step failed on first attempt");
9153
+ logger29.debug({ stepId: step.id, error: errorMsg }, "Step failed on first attempt");
8270
9154
  const failAuditUrl = step.runAudit && step.stepType !== "api" ? (() => {
8271
9155
  try {
8272
9156
  return page.url() || void 0;
@@ -8280,7 +9164,7 @@ async function executeStep(step, page, ctx) {
8280
9164
  }
8281
9165
  const failureKind = classifyError(errorMsg);
8282
9166
  if (failureKind === "assertion") {
8283
- logger28.info(
9167
+ logger29.info(
8284
9168
  { stepId: step.id, error: errorMsg },
8285
9169
  "Assertion failure detected \u2014 NOT healing (possible application bug)"
8286
9170
  );
@@ -8289,7 +9173,13 @@ async function executeStep(step, page, ctx) {
8289
9173
  if (!ctx.config.healing.enabled) {
8290
9174
  return { ...buildResult(step, "failed", code, Date.now() - start, errorMsg, screenshotPath), ...failAuditUrl ? { auditUrl: failAuditUrl } : {} };
8291
9175
  }
8292
- logger28.debug({ stepId: step.id, failureKind }, "Infrastructure failure \u2014 attempting to heal");
9176
+ if (ctx.ragManager) {
9177
+ try {
9178
+ ctx.ragManager.errorCatalog.record(errorMsg, page.url());
9179
+ } catch {
9180
+ }
9181
+ }
9182
+ logger29.debug({ stepId: step.id, failureKind }, "Infrastructure failure \u2014 attempting to heal");
8293
9183
  const failure = {
8294
9184
  stepId: step.id,
8295
9185
  instruction: step.instruction,
@@ -8300,10 +9190,10 @@ async function executeStep(step, page, ctx) {
8300
9190
  };
8301
9191
  const decision = ctx.budget.shouldHeal(failure);
8302
9192
  if (!decision.heal) {
8303
- logger28.info({ stepId: step.id, reason: decision.reason }, "Healing skipped");
9193
+ logger29.info({ stepId: step.id, reason: decision.reason }, "Healing skipped");
8304
9194
  return { ...buildResult(step, "failed", code, Date.now() - start, errorMsg, screenshotPath), ...failAuditUrl ? { auditUrl: failAuditUrl } : {} };
8305
9195
  }
8306
- const healer = new SelfHealer(ctx.provider, ctx.config.healing.maxLevel);
9196
+ const healer = new SelfHealer(ctx.provider, ctx.config.healing.maxLevel, ctx.ragManager);
8307
9197
  try {
8308
9198
  const healResult = await healer.heal(
8309
9199
  step,
@@ -8317,6 +9207,21 @@ async function executeStep(step, page, ctx) {
8317
9207
  const codeChanged = healResult.code.trim() !== code.trim();
8318
9208
  if (codeChanged) {
8319
9209
  ctx.budget.recordSpend(5e-3);
9210
+ if (ctx.ragManager) {
9211
+ try {
9212
+ ctx.ragManager.healingCorpus.ingest({
9213
+ instruction: step.instruction,
9214
+ error: errorMsg,
9215
+ failedCode: code,
9216
+ healedCode: healResult.code,
9217
+ strategy: healResult.strategy,
9218
+ level: healResult.level,
9219
+ url: page.url()
9220
+ });
9221
+ ctx.ragManager.errorCatalog.record(errorMsg, page.url(), healResult.code);
9222
+ } catch {
9223
+ }
9224
+ }
8320
9225
  ctx.healingReporter.record({
8321
9226
  runId: ctx.runId,
8322
9227
  suiteId: ctx.suiteId,
@@ -8331,7 +9236,7 @@ async function executeStep(step, page, ctx) {
8331
9236
  pageUrl: page.url()
8332
9237
  });
8333
9238
  }
8334
- logger28.info(
9239
+ logger29.info(
8335
9240
  { stepId: step.id, level: healResult.level, strategy: healResult.strategy, codeChanged },
8336
9241
  codeChanged ? "Step passed after healing (code NOT saved \u2014 awaits human review)" : "Step passed after smart retry (no code change)"
8337
9242
  );
@@ -8347,7 +9252,7 @@ async function executeStep(step, page, ctx) {
8347
9252
  }
8348
9253
  } catch (healErr) {
8349
9254
  const healErrMsg = healErr instanceof Error ? healErr.message : String(healErr);
8350
- logger28.warn({ stepId: step.id, error: healErrMsg }, "Healing threw an error");
9255
+ logger29.warn({ stepId: step.id, error: healErrMsg }, "Healing threw an error");
8351
9256
  ctx.budget.recordHealFailure(step.id);
8352
9257
  }
8353
9258
  return { ...buildResult(step, "failed", code, Date.now() - start, errorMsg, screenshotPath), ...failAuditUrl ? { auditUrl: failAuditUrl } : {} };
@@ -8371,13 +9276,13 @@ async function captureScreenshot(page, ctx, stepId, suffix) {
8371
9276
  try {
8372
9277
  await page.waitForLoadState("load", { timeout: 5e3 }).catch(() => {
8373
9278
  });
8374
- const screenshotsDir = import_path21.default.join(ctx.rootDir, "results", "screenshots");
9279
+ const screenshotsDir = import_path22.default.join(ctx.rootDir, "results", "screenshots");
8375
9280
  const filename = `${ctx.runId}_${ctx.caseId}_${stepId}_${suffix}.png`;
8376
- const screenshotPath = import_path21.default.join(screenshotsDir, filename);
9281
+ const screenshotPath = import_path22.default.join(screenshotsDir, filename);
8377
9282
  await page.screenshot({ path: screenshotPath, fullPage: true });
8378
9283
  return screenshotPath;
8379
9284
  } catch (err) {
8380
- logger28.debug({ err }, "Screenshot capture failed");
9285
+ logger29.debug({ err }, "Screenshot capture failed");
8381
9286
  return void 0;
8382
9287
  }
8383
9288
  }
@@ -8389,19 +9294,19 @@ function withTimeout(promise, ms) {
8389
9294
  )
8390
9295
  ]);
8391
9296
  }
8392
- var import_path21, logger28, ASSERTION_PATTERNS, INFRA_PATTERNS;
9297
+ var import_path22, logger29, ASSERTION_PATTERNS, INFRA_PATTERNS;
8393
9298
  var init_executor = __esm({
8394
9299
  "src/engine/executor.ts"() {
8395
9300
  "use strict";
8396
9301
  init_cjs_shims();
8397
- import_path21 = __toESM(require("path"));
9302
+ import_path22 = __toESM(require("path"));
8398
9303
  init_variable_interpolator();
8399
9304
  init_code_runner();
8400
9305
  init_self_healing();
8401
9306
  init_step_type_detector();
8402
9307
  init_logger();
8403
9308
  init_code_runner();
8404
- logger28 = createChildLogger("executor");
9309
+ logger29 = createChildLogger("executor");
8405
9310
  ASSERTION_PATTERNS = [
8406
9311
  /expect\(.*\)\.(toBe|toEqual|toContain|toMatch|toHaveText|toHaveValue|toBeChecked|toBeVisible|toBeHidden|toBeEnabled|toBeDisabled|toHaveCount|toHaveAttribute|toHaveClass|toHaveCSS|toHaveURL|toHaveTitle)/i,
8407
9312
  /AssertionError/i,
@@ -8618,24 +9523,24 @@ async function loadDataRows(rootDir, dataSource) {
8618
9523
  return dataSource.data ?? [];
8619
9524
  case "json-file": {
8620
9525
  if (!dataSource.path) throw new Error("JSON data source requires a path");
8621
- const filePath = import_path22.default.resolve(rootDir, dataSource.path);
8622
- if (!await import_fs_extra16.default.pathExists(filePath)) {
9526
+ const filePath = import_path23.default.resolve(rootDir, dataSource.path);
9527
+ if (!await import_fs_extra19.default.pathExists(filePath)) {
8623
9528
  throw new Error(`Data file not found: ${filePath}`);
8624
9529
  }
8625
- const raw = await import_fs_extra16.default.readJson(filePath);
9530
+ const raw = await import_fs_extra19.default.readJson(filePath);
8626
9531
  if (!Array.isArray(raw)) throw new Error("JSON data file must contain an array of objects");
8627
- logger29.info({ path: filePath, rows: raw.length }, "Loaded JSON data rows");
9532
+ logger30.info({ path: filePath, rows: raw.length }, "Loaded JSON data rows");
8628
9533
  return raw.map(
8629
9534
  (row) => Object.fromEntries(Object.entries(row).map(([k, v]) => [k, String(v)]))
8630
9535
  );
8631
9536
  }
8632
9537
  case "csv-file": {
8633
9538
  if (!dataSource.path) throw new Error("CSV data source requires a path");
8634
- const filePath = import_path22.default.resolve(rootDir, dataSource.path);
8635
- if (!await import_fs_extra16.default.pathExists(filePath)) {
9539
+ const filePath = import_path23.default.resolve(rootDir, dataSource.path);
9540
+ if (!await import_fs_extra19.default.pathExists(filePath)) {
8636
9541
  throw new Error(`Data file not found: ${filePath}`);
8637
9542
  }
8638
- const content = await import_fs_extra16.default.readFile(filePath, "utf8");
9543
+ const content = await import_fs_extra19.default.readFile(filePath, "utf8");
8639
9544
  const lines = content.trim().split("\n").map((l) => l.trim()).filter(Boolean);
8640
9545
  if (lines.length < 2) return [];
8641
9546
  const headers = lines[0].split(",").map((h) => h.trim());
@@ -8647,22 +9552,22 @@ async function loadDataRows(rootDir, dataSource) {
8647
9552
  });
8648
9553
  return row;
8649
9554
  });
8650
- logger29.info({ path: filePath, rows: rows.length }, "Loaded CSV data rows");
9555
+ logger30.info({ path: filePath, rows: rows.length }, "Loaded CSV data rows");
8651
9556
  return rows;
8652
9557
  }
8653
9558
  default:
8654
9559
  return [];
8655
9560
  }
8656
9561
  }
8657
- var import_path22, import_fs_extra16, logger29;
9562
+ var import_path23, import_fs_extra19, logger30;
8658
9563
  var init_data_loader = __esm({
8659
9564
  "src/engine/data-loader.ts"() {
8660
9565
  "use strict";
8661
9566
  init_cjs_shims();
8662
- import_path22 = __toESM(require("path"));
8663
- import_fs_extra16 = __toESM(require("fs-extra"));
9567
+ import_path23 = __toESM(require("path"));
9568
+ import_fs_extra19 = __toESM(require("fs-extra"));
8664
9569
  init_logger();
8665
- logger29 = createChildLogger("data-loader");
9570
+ logger30 = createChildLogger("data-loader");
8666
9571
  }
8667
9572
  });
8668
9573
 
@@ -9922,16 +10827,16 @@ function makeUnixTmpDir() {
9922
10827
  function makeWin32TmpDir() {
9923
10828
  const winTmpPath = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
9924
10829
  const randomNumber = Math.floor(Math.random() * 9e7 + 1e7);
9925
- const tmpdir = (0, import_path23.join)(winTmpPath, "lighthouse." + randomNumber);
10830
+ const tmpdir = (0, import_path24.join)(winTmpPath, "lighthouse." + randomNumber);
9926
10831
  (0, import_fs.mkdirSync)(tmpdir, { recursive: true });
9927
10832
  return tmpdir;
9928
10833
  }
9929
- var import_path23, import_child_process3, import_fs, import_is_wsl, LauncherError, ChromePathNotSetError, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError;
10834
+ var import_path24, import_child_process3, import_fs, import_is_wsl, LauncherError, ChromePathNotSetError, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError;
9930
10835
  var init_utils3 = __esm({
9931
10836
  "node_modules/chrome-launcher/dist/utils.js"() {
9932
10837
  "use strict";
9933
10838
  init_cjs_shims();
9934
- import_path23 = require("path");
10839
+ import_path24 = require("path");
9935
10840
  import_child_process3 = __toESM(require("child_process"), 1);
9936
10841
  import_fs = require("fs");
9937
10842
  import_is_wsl = __toESM(require_is_wsl(), 1);
@@ -10007,7 +10912,7 @@ function darwin() {
10007
10912
  }
10008
10913
  (0, import_child_process4.execSync)(`${LSREGISTER} -dump | grep -i 'google chrome\\( canary\\)\\?\\.app' | awk '{$1=""; print $0}'`).toString().split(newLineRegex).forEach((inst) => {
10009
10914
  suffixes.forEach((suffix) => {
10010
- const execPath = import_path24.default.join(inst.substring(0, inst.indexOf(".app") + 4).trim(), suffix);
10915
+ const execPath = import_path25.default.join(inst.substring(0, inst.indexOf(".app") + 4).trim(), suffix);
10011
10916
  if (canAccess(execPath) && installations.indexOf(execPath) === -1) {
10012
10917
  installations.push(execPath);
10013
10918
  }
@@ -10047,7 +10952,7 @@ function linux() {
10047
10952
  installations.push(customChromePath);
10048
10953
  }
10049
10954
  const desktopInstallationFolders = [
10050
- import_path24.default.join((0, import_os.homedir)(), ".local/share/applications/"),
10955
+ import_path25.default.join((0, import_os.homedir)(), ".local/share/applications/"),
10051
10956
  "/usr/share/applications/"
10052
10957
  ];
10053
10958
  desktopInstallationFolders.forEach((folder) => {
@@ -10095,8 +11000,8 @@ function wsl() {
10095
11000
  function win32() {
10096
11001
  const installations = [];
10097
11002
  const suffixes = [
10098
- `${import_path24.default.sep}Google${import_path24.default.sep}Chrome SxS${import_path24.default.sep}Application${import_path24.default.sep}chrome.exe`,
10099
- `${import_path24.default.sep}Google${import_path24.default.sep}Chrome${import_path24.default.sep}Application${import_path24.default.sep}chrome.exe`
11003
+ `${import_path25.default.sep}Google${import_path25.default.sep}Chrome SxS${import_path25.default.sep}Application${import_path25.default.sep}chrome.exe`,
11004
+ `${import_path25.default.sep}Google${import_path25.default.sep}Chrome${import_path25.default.sep}Application${import_path25.default.sep}chrome.exe`
10100
11005
  ];
10101
11006
  const prefixes = [
10102
11007
  process.env.LOCALAPPDATA,
@@ -10108,7 +11013,7 @@ function win32() {
10108
11013
  installations.push(customChromePath);
10109
11014
  }
10110
11015
  prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
10111
- const chromePath = import_path24.default.join(prefix, suffix);
11016
+ const chromePath = import_path25.default.join(prefix, suffix);
10112
11017
  if (canAccess(chromePath)) {
10113
11018
  installations.push(chromePath);
10114
11019
  }
@@ -10156,13 +11061,13 @@ function findChromeExecutables(folder) {
10156
11061
  }
10157
11062
  return installations;
10158
11063
  }
10159
- var import_fs2, import_path24, import_os, import_child_process4, import_escape_string_regexp, newLineRegex;
11064
+ var import_fs2, import_path25, import_os, import_child_process4, import_escape_string_regexp, newLineRegex;
10160
11065
  var init_chrome_finder = __esm({
10161
11066
  "node_modules/chrome-launcher/dist/chrome-finder.js"() {
10162
11067
  "use strict";
10163
11068
  init_cjs_shims();
10164
11069
  import_fs2 = __toESM(require("fs"), 1);
10165
- import_path24 = __toESM(require("path"), 1);
11070
+ import_path25 = __toESM(require("path"), 1);
10166
11071
  import_os = require("os");
10167
11072
  import_child_process4 = require("child_process");
10168
11073
  import_escape_string_regexp = __toESM(require_escape_string_regexp(), 1);
@@ -10633,15 +11538,15 @@ function isSuiteAudit(suite) {
10633
11538
  return suite.type === "audit" || suite.type === "performance";
10634
11539
  }
10635
11540
  async function runTests(rootDir, runConfig, autotestConfig, ws) {
10636
- const runId = (0, import_crypto6.randomUUID)();
11541
+ const runId = (0, import_crypto10.randomUUID)();
10637
11542
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10638
- const testsDir = import_path25.default.join(rootDir, "tests");
11543
+ const testsDir = import_path26.default.join(rootDir, "tests");
10639
11544
  const activeEnv = autotestConfig.environment ?? "dev";
10640
11545
  const envUrl = autotestConfig.environmentUrls?.[activeEnv];
10641
11546
  if (envUrl) {
10642
11547
  autotestConfig = { ...autotestConfig, baseUrl: envUrl };
10643
11548
  }
10644
- logger30.info({ runId, runConfig, environment: activeEnv, baseUrl: autotestConfig.baseUrl }, "Run started");
11549
+ logger31.info({ runId, runConfig, environment: activeEnv, baseUrl: autotestConfig.baseUrl }, "Run started");
10645
11550
  const runLog = await createRunLogFile(rootDir, runId);
10646
11551
  const logToFile = (level, msg, data) => {
10647
11552
  const ts = (/* @__PURE__ */ new Date()).toISOString();
@@ -10655,7 +11560,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
10655
11560
  try {
10656
11561
  const pairs = await collectWork(testsDir, runConfig);
10657
11562
  if (pairs.length === 0) {
10658
- logger30.warn({ runConfig }, "No tests matched the run config");
11563
+ logger31.warn({ runConfig }, "No tests matched the run config");
10659
11564
  }
10660
11565
  const variables = await loadVariables(rootDir, runConfig.env);
10661
11566
  const sharedVariables = /* @__PURE__ */ new Map();
@@ -10666,18 +11571,18 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
10666
11571
  const rows = await loadDataRows(rootDir, pair.testCase.dataSource);
10667
11572
  if (rows.length > 0) {
10668
11573
  rows.forEach((row, idx) => expandedPairs.push({ ...pair, dataRow: row, dataRowIndex: idx }));
10669
- logger30.info({ caseId: pair.testCase.id, rows: rows.length }, "Expanded data-driven case");
11574
+ logger31.info({ caseId: pair.testCase.id, rows: rows.length }, "Expanded data-driven case");
10670
11575
  continue;
10671
11576
  }
10672
11577
  } catch (err) {
10673
- logger30.warn({ err, caseId: pair.testCase.id }, "Failed to load data rows \u2014 running case once");
11578
+ logger31.warn({ err, caseId: pair.testCase.id }, "Failed to load data rows \u2014 running case once");
10674
11579
  }
10675
11580
  }
10676
11581
  expandedPairs.push(pair);
10677
11582
  }
10678
11583
  ws?.broadcast("run:start", { runId, totalTests: expandedPairs.length });
10679
11584
  if (autotestConfig.mcp?.proactiveHealing && autotestConfig.mcp?.enabled) {
10680
- logger30.info("Proactive healing enabled \u2014 validating selectors before run");
11585
+ logger31.info("Proactive healing enabled \u2014 validating selectors before run");
10681
11586
  let mcpChecker = null;
10682
11587
  try {
10683
11588
  mcpChecker = new McpSession({
@@ -10700,7 +11605,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
10700
11605
  suggestion: c.suggestion
10701
11606
  }))
10702
11607
  });
10703
- logger30.warn(
11608
+ logger31.warn(
10704
11609
  { stepId: step.id, missing: result.missing },
10705
11610
  `Proactive check: ${result.missing} selector(s) may be broken`
10706
11611
  );
@@ -10708,12 +11613,22 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
10708
11613
  }
10709
11614
  }
10710
11615
  } catch (err) {
10711
- logger30.warn({ err }, "Proactive healing check failed \u2014 continuing with run");
11616
+ logger31.warn({ err }, "Proactive healing check failed \u2014 continuing with run");
10712
11617
  } finally {
10713
11618
  await mcpChecker?.disconnect();
10714
11619
  }
10715
11620
  }
10716
- const smartRouter = createSmartRouter(rootDir);
11621
+ let ragManager;
11622
+ if (autotestConfig.rag?.enabled) {
11623
+ try {
11624
+ ragManager = await RagManager.create(rootDir, autotestConfig.rag);
11625
+ logger31.info(ragManager.getStats(), "RAG memory loaded");
11626
+ logToFile("info", "RAG memory loaded", ragManager.getStats());
11627
+ } catch (err) {
11628
+ logger31.warn({ err }, "RAG initialization failed \u2014 continuing without memory");
11629
+ }
11630
+ }
11631
+ const smartRouter = createSmartRouter(rootDir, void 0, void 0, ragManager);
10717
11632
  const provider = smartRouter.getPrimaryProvider();
10718
11633
  const budget = new HealingBudget(autotestConfig.healing.dailyBudget);
10719
11634
  const browsers = autotestConfig.browsers;
@@ -10722,7 +11637,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
10722
11637
  ({ suite, testCase }) => (isSuiteApiOnly(suite) || isCaseApiOnly(testCase)) && !isSuiteAudit(suite)
10723
11638
  );
10724
11639
  if (allApiOnly) {
10725
- logger30.info("All test cases are API-only \u2014 no browser will be launched");
11640
+ logger31.info("All test cases are API-only \u2014 no browser will be launched");
10726
11641
  logToFile("info", "API-only run detected \u2014 skipping browser launch");
10727
11642
  }
10728
11643
  const browserInstances = /* @__PURE__ */ new Map();
@@ -10776,7 +11691,8 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
10776
11691
  browserManager,
10777
11692
  ws,
10778
11693
  logToFile,
10779
- compositeCaseId
11694
+ compositeCaseId,
11695
+ ragManager
10780
11696
  );
10781
11697
  result.suiteType = suite.type;
10782
11698
  if (dataRow !== void 0) {
@@ -10826,13 +11742,21 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
10826
11742
  generateAllureReport(rootDir, runId).catch(() => void 0);
10827
11743
  }
10828
11744
  if (autotestConfig.reporting.json) {
10829
- const jsonPath = import_path25.default.join(rootDir, "results", `run-${runId}.json`);
10830
- await import_fs_extra17.default.ensureDir(import_path25.default.dirname(jsonPath));
10831
- await import_fs_extra17.default.writeJson(jsonPath, runResult, { spaces: 2 });
11745
+ const jsonPath = import_path26.default.join(rootDir, "results", `run-${runId}.json`);
11746
+ await import_fs_extra20.default.ensureDir(import_path26.default.dirname(jsonPath));
11747
+ await import_fs_extra20.default.writeJson(jsonPath, runResult, { spaces: 2 });
10832
11748
  }
10833
11749
  await healingReporter.flush(rootDir, runId);
10834
11750
  await smartRouter.getCache().persist();
10835
- logger30.info(
11751
+ if (ragManager) {
11752
+ try {
11753
+ await ragManager.persist();
11754
+ logger31.info(ragManager.getStats(), "RAG memory persisted");
11755
+ } catch (err) {
11756
+ logger31.warn({ err }, "RAG persistence failed");
11757
+ }
11758
+ }
11759
+ logger31.info(
10836
11760
  {
10837
11761
  runId,
10838
11762
  passed: runResult.passed,
@@ -10847,22 +11771,22 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
10847
11771
  await browserManager.closeAll();
10848
11772
  }
10849
11773
  }
10850
- async function runCase(rootDir, runId, suite, testCase, browserName, browser, config, variables, sharedVariables, provider, budget, healingReporter, browserManager, ws, logToFile, wsCaseId) {
11774
+ async function runCase(rootDir, runId, suite, testCase, browserName, browser, config, variables, sharedVariables, provider, budget, healingReporter, browserManager, ws, logToFile, wsCaseId, ragManager) {
10851
11775
  const broadcastCaseId = wsCaseId ?? testCase.id;
10852
11776
  const caseStart = (/* @__PURE__ */ new Date()).toISOString();
10853
11777
  const stepResults = [];
10854
11778
  const previousSteps = [];
10855
11779
  const apiOnly = isSuiteApiOnly(suite) || isCaseApiOnly(testCase);
10856
11780
  const caseConfig = isSuiteAudit(suite) ? { ...config, screenshot: "off", video: "off", trace: "off" } : config;
10857
- const tracePath = !apiOnly && caseConfig.trace !== "off" ? import_path25.default.join(rootDir, "results", "traces", `${runId}_${testCase.id}_${browserName}.zip`) : void 0;
11781
+ const tracePath = !apiOnly && caseConfig.trace !== "off" ? import_path26.default.join(rootDir, "results", "traces", `${runId}_${testCase.id}_${browserName}.zip`) : void 0;
10858
11782
  let context = null;
10859
11783
  let page;
10860
11784
  if (apiOnly) {
10861
11785
  page = await browserManager.createApiOnlyPage(config.baseUrl);
10862
- logger30.debug({ caseName: testCase.name }, "API-only case \u2014 zero browser, using standalone HTTP client");
11786
+ logger31.debug({ caseName: testCase.name }, "API-only case \u2014 zero browser, using standalone HTTP client");
10863
11787
  } else {
10864
11788
  if (!browser) throw new Error("Browser required for UI test cases");
10865
- context = await browserManager.newContext(browser, caseConfig, import_path25.default.join(rootDir, "results", "videos"));
11789
+ context = await browserManager.newContext(browser, caseConfig, import_path26.default.join(rootDir, "results", "videos"));
10866
11790
  page = await browserManager.newPage(context);
10867
11791
  await installHighlighter(page);
10868
11792
  }
@@ -10932,7 +11856,8 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
10932
11856
  config: effectiveConfig,
10933
11857
  provider,
10934
11858
  budget,
10935
- healingReporter
11859
+ healingReporter,
11860
+ ragManager
10936
11861
  });
10937
11862
  ws?.broadcast("run:step", {
10938
11863
  runId,
@@ -10996,16 +11921,16 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
10996
11921
  const launch2 = chromeLauncher.launch ?? chromeLauncher.default?.launch;
10997
11922
  const { chromium: pwChromium } = await import("playwright");
10998
11923
  const chromePath = pwChromium.executablePath();
10999
- const lhBaseDir = import_path25.default.join(rootDir, "results", ".lh-profiles");
11000
- await import_fs_extra17.default.ensureDir(lhBaseDir);
11924
+ const lhBaseDir = import_path26.default.join(rootDir, "results", ".lh-profiles");
11925
+ await import_fs_extra20.default.ensureDir(lhBaseDir);
11001
11926
  for (let auditIdx = 0; auditIdx < navigatedSteps.length; auditIdx++) {
11002
11927
  const { s, idx } = navigatedSteps[auditIdx];
11003
11928
  const url = s.auditUrl ?? s.navigatedToUrl;
11004
11929
  logToFile?.("info", ` Auditing: ${url}`);
11005
11930
  let chromeInstance = null;
11006
- const userDataDir = import_path25.default.join(lhBaseDir, `audit-${Date.now()}-${auditIdx}`);
11931
+ const userDataDir = import_path26.default.join(lhBaseDir, `audit-${Date.now()}-${auditIdx}`);
11007
11932
  try {
11008
- await import_fs_extra17.default.ensureDir(userDataDir);
11933
+ await import_fs_extra20.default.ensureDir(userDataDir);
11009
11934
  chromeInstance = await launch2({
11010
11935
  chromePath,
11011
11936
  chromeFlags: ["--headless", "--no-sandbox", "--disable-gpu"],
@@ -11081,7 +12006,7 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
11081
12006
  logToFile?.("info", ` Perf: ${entry.score ?? "--"} | A11y: ${entry.a11yScore ?? "--"} | SEO: ${entry.seoScore ?? "--"} | FCP: ${entry.fcp != null ? (entry.fcp / 1e3).toFixed(2) + "s" : "--"} | LCP: ${entry.lcp != null ? (entry.lcp / 1e3).toFixed(2) + "s" : "--"} | CLS: ${entry.cls?.toFixed(3) ?? "--"} | TTFB: ${entry.ttfb != null ? (entry.ttfb / 1e3).toFixed(2) + "s" : "--"}`);
11082
12007
  } catch (auditErr) {
11083
12008
  const errMsg = auditErr instanceof Error ? auditErr.message : String(auditErr);
11084
- logger30.warn({ url, err: errMsg }, "Lighthouse audit failed for URL");
12009
+ logger31.warn({ url, err: errMsg }, "Lighthouse audit failed for URL");
11085
12010
  logToFile?.("warn", ` Lighthouse failed for ${url}: ${errMsg}`);
11086
12011
  pageLoads.push({
11087
12012
  url,
@@ -11095,14 +12020,14 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
11095
12020
  } catch {
11096
12021
  }
11097
12022
  try {
11098
- await import_fs_extra17.default.remove(userDataDir);
12023
+ await import_fs_extra20.default.remove(userDataDir);
11099
12024
  } catch {
11100
12025
  }
11101
12026
  }
11102
12027
  }
11103
12028
  } catch (lhErr) {
11104
12029
  const errMsg = lhErr instanceof Error ? lhErr.message : String(lhErr);
11105
- logger30.warn({ err: errMsg }, "Lighthouse import/init failed");
12030
+ logger31.warn({ err: errMsg }, "Lighthouse import/init failed");
11106
12031
  logToFile?.("warn", ` Lighthouse init failed: ${errMsg}`);
11107
12032
  const recordedIndices = new Set(pageLoads.map((p) => p.stepIndex));
11108
12033
  for (const { s, idx } of navigatedSteps) {
@@ -11148,11 +12073,11 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
11148
12073
  await browserManager.closeContext(context, caseConfig, tracePath);
11149
12074
  const videoDeleteOnPass = caseConfig.video === "retain-on-failure" || caseConfig.video === "on-first-retry";
11150
12075
  if (videoDeleteOnPass && caseStatus === "passed" && pendingVideoPath) {
11151
- await import_fs_extra17.default.remove(pendingVideoPath).catch(() => void 0);
12076
+ await import_fs_extra20.default.remove(pendingVideoPath).catch(() => void 0);
11152
12077
  }
11153
12078
  const traceDeleteOnPass = caseConfig.trace === "retain-on-failure" || caseConfig.trace === "on-first-retry";
11154
12079
  if (traceDeleteOnPass && caseStatus === "passed" && tracePath) {
11155
- await import_fs_extra17.default.remove(tracePath).catch(() => void 0);
12080
+ await import_fs_extra20.default.remove(tracePath).catch(() => void 0);
11156
12081
  }
11157
12082
  }
11158
12083
  }
@@ -11198,7 +12123,7 @@ async function collectWork(testsDir, runConfig) {
11198
12123
  }
11199
12124
  const suiteDir = await findSuiteDirById(testsDir, suite.id);
11200
12125
  if (!suiteDir) {
11201
- logger30.warn({ suiteId: suite.id }, "Suite directory not found \u2014 skipping");
12126
+ logger31.warn({ suiteId: suite.id }, "Suite directory not found \u2014 skipping");
11202
12127
  continue;
11203
12128
  }
11204
12129
  const cases = await listCases(suiteDir);
@@ -11220,10 +12145,10 @@ async function loadVariables(rootDir, env) {
11220
12145
  try {
11221
12146
  const resolved = await resolveVariables(rootDir, env ?? "dev");
11222
12147
  const count = Object.keys(resolved).length;
11223
- logger30.info({ count, env: env ?? "dev", rootDir }, "Variables loaded");
12148
+ logger31.info({ count, env: env ?? "dev", rootDir }, "Variables loaded");
11224
12149
  return resolved;
11225
12150
  } catch (err) {
11226
- logger30.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to load variables \u2014 running without variable substitution");
12151
+ logger31.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to load variables \u2014 running without variable substitution");
11227
12152
  return {};
11228
12153
  }
11229
12154
  }
@@ -11294,20 +12219,21 @@ function buildRunResult(runId, startedAt, finishedAt, suites, environment) {
11294
12219
  skipped
11295
12220
  };
11296
12221
  }
11297
- var import_path25, import_crypto6, import_fs_extra17, logger30;
12222
+ var import_path26, import_crypto10, import_fs_extra20, logger31;
11298
12223
  var init_runner = __esm({
11299
12224
  "src/engine/runner.ts"() {
11300
12225
  "use strict";
11301
12226
  init_cjs_shims();
11302
- import_path25 = __toESM(require("path"));
11303
- import_crypto6 = require("crypto");
11304
- import_fs_extra17 = __toESM(require("fs-extra"));
12227
+ import_path26 = __toESM(require("path"));
12228
+ import_crypto10 = require("crypto");
12229
+ import_fs_extra20 = __toESM(require("fs-extra"));
11305
12230
  init_suite_store();
11306
12231
  init_case_store();
11307
12232
  init_variable_store();
11308
12233
  init_result_store();
11309
12234
  init_router();
11310
12235
  init_mcp_session();
12236
+ init_rag_manager();
11311
12237
  init_proactive_checker();
11312
12238
  init_browser_manager();
11313
12239
  init_healing_budget();
@@ -11319,7 +12245,7 @@ var init_runner = __esm({
11319
12245
  init_data_loader();
11320
12246
  init_step_type_detector();
11321
12247
  init_logger();
11322
- logger30 = createChildLogger("runner");
12248
+ logger31 = createChildLogger("runner");
11323
12249
  }
11324
12250
  });
11325
12251
 
@@ -11353,7 +12279,7 @@ async function runRoutes(fastify) {
11353
12279
  };
11354
12280
  const wsManager = fastify.wsManager;
11355
12281
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11356
- logger31.info({ runConfig }, "Run requested via API");
12282
+ logger32.info({ runConfig }, "Run requested via API");
11357
12283
  runTests(rootDir, runConfig, autotestConfig, wsManager).then((result) => {
11358
12284
  wsManager.broadcast("run:complete", {
11359
12285
  runId: result.runId,
@@ -11362,9 +12288,9 @@ async function runRoutes(fastify) {
11362
12288
  skipped: result.skipped,
11363
12289
  duration: result.duration
11364
12290
  });
11365
- logger31.info({ runId: result.runId, status: result.status }, "API-triggered run complete");
12291
+ logger32.info({ runId: result.runId, status: result.status }, "API-triggered run complete");
11366
12292
  }).catch((err) => {
11367
- logger31.error({ err }, "API-triggered run failed");
12293
+ logger32.error({ err }, "API-triggered run failed");
11368
12294
  wsManager.broadcast("run:error", {
11369
12295
  error: err instanceof Error ? err.message : String(err)
11370
12296
  });
@@ -11395,7 +12321,7 @@ async function runRoutes(fastify) {
11395
12321
  }
11396
12322
  });
11397
12323
  }
11398
- var import_zod14, logger31, ScreenshotMode, VideoMode, TraceMode, StartRunBody, ListRunsQuery;
12324
+ var import_zod14, logger32, ScreenshotMode, VideoMode, TraceMode, StartRunBody, ListRunsQuery;
11399
12325
  var init_run2 = __esm({
11400
12326
  "src/server/routes/run.ts"() {
11401
12327
  "use strict";
@@ -11406,7 +12332,7 @@ var init_run2 = __esm({
11406
12332
  init_result_store();
11407
12333
  init_utils2();
11408
12334
  init_logger();
11409
- logger31 = createChildLogger("routes/run");
12335
+ logger32 = createChildLogger("routes/run");
11410
12336
  ScreenshotMode = import_zod14.z.enum(["off", "on", "only-on-failure"]);
11411
12337
  VideoMode = import_zod14.z.enum(["off", "on", "on-first-retry", "retain-on-failure"]);
11412
12338
  TraceMode = import_zod14.z.enum(["off", "on", "on-first-retry", "retain-on-failure"]);
@@ -11496,7 +12422,7 @@ async function parseSwaggerSpec(urlOrJson) {
11496
12422
  specUrl = new URL(specUrl, url).href;
11497
12423
  }
11498
12424
  urls.unshift(specUrl);
11499
- logger32.info({ specUrl, pattern: pattern.source }, "Extracted spec URL from HTML page");
12425
+ logger33.info({ specUrl, pattern: pattern.source }, "Extracted spec URL from HTML page");
11500
12426
  }
11501
12427
  }
11502
12428
  }
@@ -11508,7 +12434,7 @@ async function parseSwaggerSpec(urlOrJson) {
11508
12434
  const errors = [];
11509
12435
  for (const tryUrl of uniqueUrls) {
11510
12436
  try {
11511
- logger32.debug({ tryUrl }, "Trying to fetch OpenAPI spec");
12437
+ logger33.debug({ tryUrl }, "Trying to fetch OpenAPI spec");
11512
12438
  const res = await fetch(tryUrl, {
11513
12439
  headers: { Accept: "application/json, application/yaml, */*" },
11514
12440
  signal: AbortSignal.timeout(15e3)
@@ -11523,7 +12449,7 @@ async function parseSwaggerSpec(urlOrJson) {
11523
12449
  spec = isJson ? JSON.parse(text) : import_js_yaml.default.load(text);
11524
12450
  if (spec && (spec.openapi || spec.swagger || spec.paths)) {
11525
12451
  fetched = true;
11526
- logger32.info({ tryUrl, format: isYaml ? "YAML" : "JSON" }, "Successfully fetched OpenAPI spec");
12452
+ logger33.info({ tryUrl, format: isYaml ? "YAML" : "JSON" }, "Successfully fetched OpenAPI spec");
11527
12453
  break;
11528
12454
  }
11529
12455
  errors.push(`${tryUrl}: Parsed but not a valid OpenAPI spec`);
@@ -11537,7 +12463,7 @@ async function parseSwaggerSpec(urlOrJson) {
11537
12463
  if (parsed && typeof parsed === "object" && (parsed.openapi || parsed.swagger || parsed.paths)) {
11538
12464
  spec = parsed;
11539
12465
  fetched = true;
11540
- logger32.info({ tryUrl, format: "YAML (text/plain)" }, "Successfully fetched OpenAPI spec");
12466
+ logger33.info({ tryUrl, format: "YAML (text/plain)" }, "Successfully fetched OpenAPI spec");
11541
12467
  break;
11542
12468
  }
11543
12469
  } catch {
@@ -11626,17 +12552,17 @@ function parseSpec(spec) {
11626
12552
  });
11627
12553
  }
11628
12554
  }
11629
- logger32.info({ title, endpointCount: endpoints.length }, "Parsed OpenAPI spec");
12555
+ logger33.info({ title, endpointCount: endpoints.length }, "Parsed OpenAPI spec");
11630
12556
  return { title, baseUrl, endpoints };
11631
12557
  }
11632
- var import_js_yaml, logger32;
12558
+ var import_js_yaml, logger33;
11633
12559
  var init_swagger_parser = __esm({
11634
12560
  "src/ai/swagger-parser.ts"() {
11635
12561
  "use strict";
11636
12562
  init_cjs_shims();
11637
12563
  import_js_yaml = __toESM(require("js-yaml"));
11638
12564
  init_logger();
11639
- logger32 = createChildLogger("swagger-parser");
12565
+ logger33 = createChildLogger("swagger-parser");
11640
12566
  }
11641
12567
  });
11642
12568
 
@@ -11692,7 +12618,7 @@ async function fetchJiraIssue(issueKey) {
11692
12618
  }
11693
12619
  const authHeader = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
11694
12620
  const url = `${config.baseUrl}/rest/api/3/issue/${encodeURIComponent(issueKey)}?fields=summary,description,comment,issuetype,status,priority,labels,customfield_10020`;
11695
- logger33.info({ issueKey, url: url.replace(/\/rest.*/, "/...") }, "Fetching Jira issue");
12621
+ logger34.info({ issueKey, url: url.replace(/\/rest.*/, "/...") }, "Fetching Jira issue");
11696
12622
  const res = await fetch(url, {
11697
12623
  method: "GET",
11698
12624
  headers: {
@@ -11813,7 +12739,7 @@ async function storyRoutes(fastify) {
11813
12739
  return reply.status(400).send({ error: "Invalid request", details: parsed.error.issues });
11814
12740
  }
11815
12741
  try {
11816
- const testsDir = import_path26.default.join(rootDir, "tests");
12742
+ const testsDir = import_path27.default.join(rootDir, "tests");
11817
12743
  let storyText = parsed.data.story;
11818
12744
  if (parsed.data.jiraUrl) {
11819
12745
  const issueKey = extractIssueKey(parsed.data.jiraUrl);
@@ -11821,19 +12747,19 @@ async function storyRoutes(fastify) {
11821
12747
  try {
11822
12748
  const jiraContent = await fetchJiraIssue(issueKey);
11823
12749
  storyText = buildEnrichedStory(jiraContent, parsed.data.story || void 0);
11824
- logger33.info({ issueKey }, "Enriched story with Jira content");
12750
+ logger34.info({ issueKey }, "Enriched story with Jira content");
11825
12751
  } catch (jiraErr) {
11826
12752
  if (!storyText || storyText.length < 10) {
11827
12753
  return sendError(reply, 400, "Jira fetch failed and no user story provided", jiraErr);
11828
12754
  }
11829
- logger33.warn({ err: jiraErr }, "Failed to fetch Jira issue \u2014 proceeding with user story only");
12755
+ logger34.warn({ err: jiraErr }, "Failed to fetch Jira issue \u2014 proceeding with user story only");
11830
12756
  }
11831
12757
  }
11832
12758
  }
11833
12759
  if (!storyText || storyText.length < 10) {
11834
12760
  return reply.status(400).send({ error: "Not enough content to generate tests. Provide a story or a valid Jira link." });
11835
12761
  }
11836
- logger33.info({ suiteName: parsed.data.suiteName }, "Story generation requested");
12762
+ logger34.info({ suiteName: parsed.data.suiteName }, "Story generation requested");
11837
12763
  const { suiteId, suiteName } = await generateSuiteFromStory(storyText, {
11838
12764
  rootDir,
11839
12765
  outputDir: testsDir,
@@ -11859,9 +12785,9 @@ async function storyRoutes(fastify) {
11859
12785
  return reply.status(400).send({ error: "Invalid request", details: parsed.error.issues });
11860
12786
  }
11861
12787
  const { stories, concurrency } = parsed.data;
11862
- const testsDir = import_path26.default.join(rootDir, "tests");
12788
+ const testsDir = import_path27.default.join(rootDir, "tests");
11863
12789
  const wsManager = fastify.wsManager;
11864
- logger33.info({ total: stories.length, concurrency }, "Bulk story generation requested");
12790
+ logger34.info({ total: stories.length, concurrency }, "Bulk story generation requested");
11865
12791
  const enrichedStories = await Promise.all(
11866
12792
  stories.map(async (item) => {
11867
12793
  if (!item.jiraUrl) return { story: item.story, suiteName: item.suiteName };
@@ -11874,7 +12800,7 @@ async function storyRoutes(fastify) {
11874
12800
  suiteName: item.suiteName ?? jiraContent.summary
11875
12801
  };
11876
12802
  } catch {
11877
- logger33.warn({ issueKey }, "Failed to fetch Jira issue for bulk item \u2014 using raw story");
12803
+ logger34.warn({ issueKey }, "Failed to fetch Jira issue for bulk item \u2014 using raw story");
11878
12804
  return { story: item.story, suiteName: item.suiteName };
11879
12805
  }
11880
12806
  })
@@ -11885,7 +12811,7 @@ async function storyRoutes(fastify) {
11885
12811
  wsManager,
11886
12812
  concurrency
11887
12813
  ).catch((err) => {
11888
- logger33.error({ err }, "Bulk story generation error");
12814
+ logger34.error({ err }, "Bulk story generation error");
11889
12815
  });
11890
12816
  return reply.status(202).send({
11891
12817
  message: `Processing ${stories.length} stories in the background`,
@@ -11927,7 +12853,7 @@ async function storyRoutes(fastify) {
11927
12853
  }
11928
12854
  try {
11929
12855
  const spec = await parseSwaggerSpec(parsed.data.specUrl);
11930
- logger33.info({ title: spec.title, endpoints: spec.endpoints.length }, "Parsed OpenAPI spec");
12856
+ logger34.info({ title: spec.title, endpoints: spec.endpoints.length }, "Parsed OpenAPI spec");
11931
12857
  let endpoints = spec.endpoints;
11932
12858
  if (parsed.data.tags && parsed.data.tags.length > 0) {
11933
12859
  const tagSet = new Set(parsed.data.tags.map((t) => t.toLowerCase()));
@@ -11939,7 +12865,7 @@ async function storyRoutes(fastify) {
11939
12865
  const MAX_ENDPOINTS = 20;
11940
12866
  const totalEndpoints = endpoints.length;
11941
12867
  if (endpoints.length > MAX_ENDPOINTS) {
11942
- logger33.warn({ total: endpoints.length, max: MAX_ENDPOINTS }, "Capping endpoints to max");
12868
+ logger34.warn({ total: endpoints.length, max: MAX_ENDPOINTS }, "Capping endpoints to max");
11943
12869
  endpoints = endpoints.slice(0, MAX_ENDPOINTS);
11944
12870
  }
11945
12871
  const storyLines = [
@@ -11971,7 +12897,7 @@ async function storyRoutes(fastify) {
11971
12897
  }
11972
12898
  const storyText = storyLines.join("\n");
11973
12899
  const suiteName = parsed.data.suiteName ?? `${spec.title} API Tests`;
11974
- const testsDir = import_path26.default.join(rootDir, "tests");
12900
+ const testsDir = import_path27.default.join(rootDir, "tests");
11975
12901
  const { suiteId } = await generateSuiteFromStory(storyText, {
11976
12902
  rootDir,
11977
12903
  outputDir: testsDir,
@@ -11991,18 +12917,18 @@ async function storyRoutes(fastify) {
11991
12917
  }
11992
12918
  });
11993
12919
  }
11994
- var import_path26, import_zod15, logger33, GenerateStoryBody, FetchJiraBody;
12920
+ var import_path27, import_zod15, logger34, GenerateStoryBody, FetchJiraBody;
11995
12921
  var init_story = __esm({
11996
12922
  "src/server/routes/story.ts"() {
11997
12923
  "use strict";
11998
12924
  init_cjs_shims();
11999
- import_path26 = __toESM(require("path"));
12925
+ import_path27 = __toESM(require("path"));
12000
12926
  import_zod15 = require("zod");
12001
12927
  init_router();
12002
12928
  init_swagger_parser();
12003
12929
  init_utils2();
12004
12930
  init_logger();
12005
- logger33 = createChildLogger("routes/story");
12931
+ logger34 = createChildLogger("routes/story");
12006
12932
  GenerateStoryBody = import_zod15.z.object({
12007
12933
  story: import_zod15.z.string().default(""),
12008
12934
  suiteName: import_zod15.z.string().min(1).optional(),
@@ -12043,14 +12969,14 @@ async function reportRoutes(fastify) {
12043
12969
  fastify.delete("/report/:runId", async (req, reply) => {
12044
12970
  try {
12045
12971
  await deleteResult(rootDir, req.params.runId);
12046
- logger34.info({ runId: req.params.runId }, "Run result deleted");
12972
+ logger35.info({ runId: req.params.runId }, "Run result deleted");
12047
12973
  return reply.status(204).send();
12048
12974
  } catch {
12049
12975
  return reply.status(404).send({ error: `Report for run "${req.params.runId}" not found` });
12050
12976
  }
12051
12977
  });
12052
12978
  }
12053
- var import_zod16, logger34, ListReportsQuery;
12979
+ var import_zod16, logger35, ListReportsQuery;
12054
12980
  var init_report = __esm({
12055
12981
  "src/server/routes/report.ts"() {
12056
12982
  "use strict";
@@ -12059,7 +12985,7 @@ var init_report = __esm({
12059
12985
  init_result_store();
12060
12986
  init_utils2();
12061
12987
  init_logger();
12062
- logger34 = createChildLogger("routes/report");
12988
+ logger35 = createChildLogger("routes/report");
12063
12989
  ListReportsQuery = import_zod16.z.object({
12064
12990
  limit: import_zod16.z.coerce.number().int().positive().max(100).default(20)
12065
12991
  });
@@ -12068,26 +12994,26 @@ var init_report = __esm({
12068
12994
 
12069
12995
  // src/storage/baseline-store.ts
12070
12996
  function baselinesDir(rootDir) {
12071
- return import_path27.default.join(rootDir, "results", "baselines");
12997
+ return import_path28.default.join(rootDir, "results", "baselines");
12072
12998
  }
12073
12999
  function baselinePath(rootDir, stepId) {
12074
- return import_path27.default.join(baselinesDir(rootDir), `${stepId}.png`);
13000
+ return import_path28.default.join(baselinesDir(rootDir), `${stepId}.png`);
12075
13001
  }
12076
13002
  async function saveBaseline(rootDir, stepId, sourcePath) {
12077
13003
  const dir = baselinesDir(rootDir);
12078
- await import_fs_extra18.default.ensureDir(dir);
12079
- await import_fs_extra18.default.copy(sourcePath, baselinePath(rootDir, stepId), { overwrite: true });
13004
+ await import_fs_extra21.default.ensureDir(dir);
13005
+ await import_fs_extra21.default.copy(sourcePath, baselinePath(rootDir, stepId), { overwrite: true });
12080
13006
  }
12081
13007
  async function hasBaseline(rootDir, stepId) {
12082
- return import_fs_extra18.default.pathExists(baselinePath(rootDir, stepId));
13008
+ return import_fs_extra21.default.pathExists(baselinePath(rootDir, stepId));
12083
13009
  }
12084
- var import_path27, import_fs_extra18;
13010
+ var import_path28, import_fs_extra21;
12085
13011
  var init_baseline_store = __esm({
12086
13012
  "src/storage/baseline-store.ts"() {
12087
13013
  "use strict";
12088
13014
  init_cjs_shims();
12089
- import_path27 = __toESM(require("path"));
12090
- import_fs_extra18 = __toESM(require("fs-extra"));
13015
+ import_path28 = __toESM(require("path"));
13016
+ import_fs_extra21 = __toESM(require("fs-extra"));
12091
13017
  }
12092
13018
  });
12093
13019
 
@@ -12098,26 +13024,26 @@ async function mediaRoutes(fastify) {
12098
13024
  if (!["screenshots", "videos", "traces", "logs", "baselines"].includes(category)) {
12099
13025
  return reply.status(404).send({ error: "Unknown media category" });
12100
13026
  }
12101
- const safeName = import_path28.default.basename(filename);
12102
- const filePath = import_path28.default.join(fastify.rootDir, "results", category, safeName);
12103
- if (!await import_fs_extra19.default.pathExists(filePath)) {
13027
+ const safeName = import_path29.default.basename(filename);
13028
+ const filePath = import_path29.default.join(fastify.rootDir, "results", category, safeName);
13029
+ if (!await import_fs_extra22.default.pathExists(filePath)) {
12104
13030
  return reply.status(404).send({ error: `File not found: ${safeName}` });
12105
13031
  }
12106
- const ext = import_path28.default.extname(safeName).toLowerCase();
13032
+ const ext = import_path29.default.extname(safeName).toLowerCase();
12107
13033
  const mimeType = MIME[ext] ?? "application/octet-stream";
12108
13034
  if (ext === ".zip") {
12109
13035
  void reply.header("Content-Disposition", `attachment; filename="${safeName}"`);
12110
13036
  }
12111
- const stat = await import_fs_extra19.default.stat(filePath);
13037
+ const stat = await import_fs_extra22.default.stat(filePath);
12112
13038
  void reply.header("Content-Length", stat.size);
12113
- return reply.type(mimeType).send(import_fs_extra19.default.createReadStream(filePath));
13039
+ return reply.type(mimeType).send(import_fs_extra22.default.createReadStream(filePath));
12114
13040
  });
12115
13041
  fastify.post("/baselines/:stepId/capture", async (req, reply) => {
12116
13042
  try {
12117
13043
  const body = req.body;
12118
13044
  if (!body?.screenshotPath) return reply.status(400).send({ error: "screenshotPath required" });
12119
- const fullPath = import_path28.default.isAbsolute(body.screenshotPath) ? body.screenshotPath : import_path28.default.join(fastify.rootDir, body.screenshotPath);
12120
- if (!await import_fs_extra19.default.pathExists(fullPath)) return reply.status(404).send({ error: "Screenshot not found" });
13045
+ const fullPath = import_path29.default.isAbsolute(body.screenshotPath) ? body.screenshotPath : import_path29.default.join(fastify.rootDir, body.screenshotPath);
13046
+ if (!await import_fs_extra22.default.pathExists(fullPath)) return reply.status(404).send({ error: "Screenshot not found" });
12121
13047
  await saveBaseline(fastify.rootDir, req.params.stepId, fullPath);
12122
13048
  return reply.send({ saved: true });
12123
13049
  } catch (err) {
@@ -12128,21 +13054,21 @@ async function mediaRoutes(fastify) {
12128
13054
  try {
12129
13055
  const exists = await hasBaseline(fastify.rootDir, req.params.stepId);
12130
13056
  if (!exists) return reply.send({ hasBaseline: false });
12131
- const filePath = import_path28.default.join(fastify.rootDir, "results", "baselines", `${req.params.stepId}.png`);
12132
- const stat = await import_fs_extra19.default.stat(filePath).catch(() => null);
13057
+ const filePath = import_path29.default.join(fastify.rootDir, "results", "baselines", `${req.params.stepId}.png`);
13058
+ const stat = await import_fs_extra22.default.stat(filePath).catch(() => null);
12133
13059
  return reply.send({ hasBaseline: true, capturedAt: stat?.mtimeMs ?? Date.now() });
12134
13060
  } catch (err) {
12135
13061
  return sendError(reply, 500, "Failed to check baseline", err);
12136
13062
  }
12137
13063
  });
12138
13064
  }
12139
- var import_path28, import_fs_extra19, MIME;
13065
+ var import_path29, import_fs_extra22, MIME;
12140
13066
  var init_media = __esm({
12141
13067
  "src/server/routes/media.ts"() {
12142
13068
  "use strict";
12143
13069
  init_cjs_shims();
12144
- import_path28 = __toESM(require("path"));
12145
- import_fs_extra19 = __toESM(require("fs-extra"));
13070
+ import_path29 = __toESM(require("path"));
13071
+ import_fs_extra22 = __toESM(require("fs-extra"));
12146
13072
  init_baseline_store();
12147
13073
  init_utils2();
12148
13074
  MIME = {
@@ -12166,13 +13092,13 @@ function toAdf(text) {
12166
13092
  }));
12167
13093
  return { type: "doc", version: 1, content: paragraphs };
12168
13094
  }
12169
- var logger35, XrayClient;
13095
+ var logger36, XrayClient;
12170
13096
  var init_client = __esm({
12171
13097
  "src/xray/client.ts"() {
12172
13098
  "use strict";
12173
13099
  init_cjs_shims();
12174
13100
  init_logger();
12175
- logger35 = createChildLogger("xray-client");
13101
+ logger36 = createChildLogger("xray-client");
12176
13102
  XrayClient = class {
12177
13103
  config;
12178
13104
  authHeader;
@@ -12188,7 +13114,7 @@ var init_client = __esm({
12188
13114
  // ─── Jira REST API helpers ──────────────────────────────────────────────────
12189
13115
  async jiraRequest(method, path37, body) {
12190
13116
  const url = `${this.config.jiraBaseUrl}/rest/api/3${path37}`;
12191
- logger35.debug({ method, url }, "Jira API request");
13117
+ logger36.debug({ method, url }, "Jira API request");
12192
13118
  const headers = {
12193
13119
  "Authorization": this.authHeader,
12194
13120
  "Accept": "application/json"
@@ -12203,7 +13129,7 @@ var init_client = __esm({
12203
13129
  });
12204
13130
  if (!res.ok) {
12205
13131
  const text = await res.text();
12206
- logger35.error({ status: res.status, url, body: text }, "Jira API error");
13132
+ logger36.error({ status: res.status, url, body: text }, "Jira API error");
12207
13133
  throw new Error(`Jira API ${method} ${path37} \u2192 ${res.status}: ${text}`);
12208
13134
  }
12209
13135
  if (res.status === 204) return void 0;
@@ -12235,7 +13161,7 @@ var init_client = __esm({
12235
13161
  const token = await this.getXrayToken();
12236
13162
  const baseUrl = token ? "https://xray.cloud.getxray.app/api/v2" : `${this.config.jiraBaseUrl}/rest/raven/2.0/api`;
12237
13163
  const url = `${baseUrl}${path37}`;
12238
- logger35.debug({ method, url }, "Xray API request");
13164
+ logger36.debug({ method, url }, "Xray API request");
12239
13165
  const headers = {
12240
13166
  "Accept": "application/json"
12241
13167
  };
@@ -12254,7 +13180,7 @@ var init_client = __esm({
12254
13180
  });
12255
13181
  if (!res.ok) {
12256
13182
  const text = await res.text();
12257
- logger35.error({ status: res.status, url, body: text }, "Xray API error");
13183
+ logger36.error({ status: res.status, url, body: text }, "Xray API error");
12258
13184
  throw new Error(`Xray API ${method} ${path37} \u2192 ${res.status}: ${text}`);
12259
13185
  }
12260
13186
  if (res.status === 204) return void 0;
@@ -12323,7 +13249,7 @@ var init_client = __esm({
12323
13249
  warnings
12324
13250
  }
12325
13251
  }`;
12326
- logger35.info({ caseName: testCase.name, projectKey: this.config.projectKey, folderPath }, "Creating Test via Xray Cloud GraphQL");
13252
+ logger36.info({ caseName: testCase.name, projectKey: this.config.projectKey, folderPath }, "Creating Test via Xray Cloud GraphQL");
12327
13253
  const result = await this.xrayGraphQL(mutation);
12328
13254
  if (!result?.createTest?.test) {
12329
13255
  throw new Error("Xray createTest returned no test data");
@@ -12332,9 +13258,9 @@ var init_client = __esm({
12332
13258
  const jiraKey = test.jira?.key ?? "";
12333
13259
  const warnings = result.createTest.warnings;
12334
13260
  if (warnings && warnings.length > 0) {
12335
- logger35.warn({ warnings, jiraKey }, "Xray createTest warnings");
13261
+ logger36.warn({ warnings, jiraKey }, "Xray createTest warnings");
12336
13262
  }
12337
- logger35.info({ issueId: test.issueId, jiraKey, stepCount: testCase.steps.length, folderPath }, "Xray Test created via GraphQL");
13263
+ logger36.info({ issueId: test.issueId, jiraKey, stepCount: testCase.steps.length, folderPath }, "Xray Test created via GraphQL");
12338
13264
  return { id: test.issueId, key: jiraKey, self: "" };
12339
13265
  }
12340
13266
  /** Xray Server/DC: create via Jira REST API */
@@ -12347,7 +13273,7 @@ var init_client = __esm({
12347
13273
  issuetype: { name: issueType },
12348
13274
  labels: ["assuremind", ...testCase.tags]
12349
13275
  };
12350
- logger35.info({ caseName: testCase.name, projectKey: this.config.projectKey }, "Creating Xray Test via Jira REST");
13276
+ logger36.info({ caseName: testCase.name, projectKey: this.config.projectKey }, "Creating Xray Test via Jira REST");
12351
13277
  const result = await this.jiraRequest("POST", "/issue", { fields });
12352
13278
  if (testCase.steps.length > 0) {
12353
13279
  await this.addTestStepsServerDC(result.key, testCase.steps);
@@ -12363,7 +13289,7 @@ var init_client = __esm({
12363
13289
  description: toAdf(testCase.description || `Automated test case: ${testCase.name}`),
12364
13290
  labels: ["assuremind", ...testCase.tags]
12365
13291
  };
12366
- logger35.info({ issueKey: testIssue.key, caseName: testCase.name }, "Updating Xray Test");
13292
+ logger36.info({ issueKey: testIssue.key, caseName: testCase.name }, "Updating Xray Test");
12367
13293
  await this.jiraRequest("PUT", `/issue/${testIssue.key}`, { fields });
12368
13294
  if (testCase.steps.length > 0) {
12369
13295
  await this.replaceTestSteps(testIssue, testCase.steps);
@@ -12402,7 +13328,7 @@ var init_client = __esm({
12402
13328
  warnings
12403
13329
  }
12404
13330
  }`;
12405
- logger35.info({ suiteName: suite.name, projectKey: this.config.projectKey, testCount: testIssueIds?.length }, "Creating Test Set via Xray Cloud GraphQL");
13331
+ logger36.info({ suiteName: suite.name, projectKey: this.config.projectKey, testCount: testIssueIds?.length }, "Creating Test Set via Xray Cloud GraphQL");
12406
13332
  const result = await this.xrayGraphQL(mutation);
12407
13333
  if (!result?.createTestSet?.testSet) {
12408
13334
  throw new Error("Xray createTestSet returned no data");
@@ -12411,9 +13337,9 @@ var init_client = __esm({
12411
13337
  const jiraKey = testSet.jira?.key ?? "";
12412
13338
  const warnings = result.createTestSet.warnings;
12413
13339
  if (warnings && warnings.length > 0) {
12414
- logger35.warn({ warnings, jiraKey }, "Xray createTestSet warnings");
13340
+ logger36.warn({ warnings, jiraKey }, "Xray createTestSet warnings");
12415
13341
  }
12416
- logger35.info({ issueId: testSet.issueId, jiraKey }, "Xray Test Set created via GraphQL");
13342
+ logger36.info({ issueId: testSet.issueId, jiraKey }, "Xray Test Set created via GraphQL");
12417
13343
  return { id: testSet.issueId, key: jiraKey, self: "" };
12418
13344
  }
12419
13345
  /** Xray Server/DC: create via Jira REST API */
@@ -12426,7 +13352,7 @@ var init_client = __esm({
12426
13352
  issuetype: { name: issueType },
12427
13353
  labels: ["assuremind", ...suite.tags]
12428
13354
  };
12429
- logger35.info({ suiteName: suite.name, projectKey: this.config.projectKey }, "Creating Xray Test Set via Jira REST");
13355
+ logger36.info({ suiteName: suite.name, projectKey: this.config.projectKey }, "Creating Xray Test Set via Jira REST");
12430
13356
  const result = await this.jiraRequest("POST", "/issue", { fields });
12431
13357
  return { id: result.id, key: result.key, self: result.self };
12432
13358
  }
@@ -12439,7 +13365,7 @@ var init_client = __esm({
12439
13365
  description: toAdf(suite.description || `Automated test suite: ${suite.name}`),
12440
13366
  labels: ["assuremind", ...suite.tags]
12441
13367
  };
12442
- logger35.info({ issueKey, suiteName: suite.name }, "Updating Xray Test Set");
13368
+ logger36.info({ issueKey, suiteName: suite.name }, "Updating Xray Test Set");
12443
13369
  await this.jiraRequest("PUT", `/issue/${issueKey}`, { fields });
12444
13370
  }
12445
13371
  // ─── Test Steps ────────────────────────────────────────────────────────────
@@ -12456,7 +13382,7 @@ var init_client = __esm({
12456
13382
  if (this.isXrayCloud) {
12457
13383
  const xrayId = await this.resolveXrayIssueId(testIssue.key, "Test");
12458
13384
  if (!xrayId) {
12459
- logger35.warn({ testKey: testIssue.key }, "Cannot add steps \u2014 Test not found in Xray yet");
13385
+ logger36.warn({ testKey: testIssue.key }, "Cannot add steps \u2014 Test not found in Xray yet");
12460
13386
  return;
12461
13387
  }
12462
13388
  await this.addTestStepsCloud(xrayId, steps);
@@ -12491,16 +13417,16 @@ var init_client = __esm({
12491
13417
  } catch (err) {
12492
13418
  const errMsg = err instanceof Error ? err.message : String(err);
12493
13419
  if (errMsg.includes("not found")) {
12494
- logger35.warn({ xrayIssueId }, "Xray issue not found during step add \u2014 aborting");
13420
+ logger36.warn({ xrayIssueId }, "Xray issue not found during step add \u2014 aborting");
12495
13421
  break;
12496
13422
  }
12497
- logger35.warn({ xrayIssueId, step: action, err: errMsg }, "Failed to add test step");
13423
+ logger36.warn({ xrayIssueId, step: action, err: errMsg }, "Failed to add test step");
12498
13424
  }
12499
13425
  }
12500
13426
  if (added > 0) {
12501
- logger35.info({ xrayIssueId, added, total: steps.length }, "Test steps added via Xray Cloud GraphQL");
13427
+ logger36.info({ xrayIssueId, added, total: steps.length }, "Test steps added via Xray Cloud GraphQL");
12502
13428
  } else if (steps.length > 0) {
12503
- logger35.warn({ xrayIssueId }, "Failed to add any steps");
13429
+ logger36.warn({ xrayIssueId }, "Failed to add any steps");
12504
13430
  }
12505
13431
  }
12506
13432
  /** Xray Server/DC — use REST API /test/{key}/step */
@@ -12513,14 +13439,14 @@ var init_client = __esm({
12513
13439
  }));
12514
13440
  try {
12515
13441
  await this.xrayRequest("PUT", `/test/${testIssueKey}/step`, xraySteps);
12516
- logger35.debug({ testIssueKey, stepCount: steps.length }, "Test steps added");
13442
+ logger36.debug({ testIssueKey, stepCount: steps.length }, "Test steps added");
12517
13443
  } catch (err) {
12518
- logger35.warn({ testIssueKey, err }, "Bulk step add failed, trying one-by-one");
13444
+ logger36.warn({ testIssueKey, err }, "Bulk step add failed, trying one-by-one");
12519
13445
  for (const step of xraySteps) {
12520
13446
  try {
12521
13447
  await this.xrayRequest("POST", `/test/${testIssueKey}/step`, step);
12522
13448
  } catch (stepErr) {
12523
- logger35.warn({ testIssueKey, step: step.index, err: stepErr }, "Individual step add failed");
13449
+ logger36.warn({ testIssueKey, step: step.index, err: stepErr }, "Individual step add failed");
12524
13450
  }
12525
13451
  }
12526
13452
  }
@@ -12532,7 +13458,7 @@ var init_client = __esm({
12532
13458
  if (this.isXrayCloud) {
12533
13459
  const xrayId = await this.resolveXrayIssueId(testIssue.key, "Test");
12534
13460
  if (!xrayId) {
12535
- logger35.warn({ testKey: testIssue.key }, "Cannot replace steps \u2014 Test not found in Xray");
13461
+ logger36.warn({ testKey: testIssue.key }, "Cannot replace steps \u2014 Test not found in Xray");
12536
13462
  return;
12537
13463
  }
12538
13464
  await this.addTestStepsCloud(xrayId, steps);
@@ -12544,7 +13470,7 @@ var init_client = __esm({
12544
13470
  await this.xrayRequest("DELETE", `/test/${testIssue.key}/step/${step.id}`);
12545
13471
  }
12546
13472
  } catch {
12547
- logger35.debug({ testIssueKey: testIssue.key }, "No existing steps to delete (or endpoint unavailable)");
13473
+ logger36.debug({ testIssueKey: testIssue.key }, "No existing steps to delete (or endpoint unavailable)");
12548
13474
  }
12549
13475
  await this.addTestStepsServerDC(testIssue.key, steps);
12550
13476
  }
@@ -12563,9 +13489,9 @@ var init_client = __esm({
12563
13489
  await this.xrayRequest("POST", `/testset/${testSet.key}/test`, {
12564
13490
  add: tests.map((t) => t.key)
12565
13491
  });
12566
- logger35.info({ testSetKey: testSet.key, count: tests.length }, "Tests linked to Test Set");
13492
+ logger36.info({ testSetKey: testSet.key, count: tests.length }, "Tests linked to Test Set");
12567
13493
  } catch (err) {
12568
- logger35.warn({ testSetKey: testSet.key, err }, "Xray link API failed, trying Jira issue links");
13494
+ logger36.warn({ testSetKey: testSet.key, err }, "Xray link API failed, trying Jira issue links");
12569
13495
  await this.linkViaJiraIssueLinks(testSet.key, tests.map((t) => t.key));
12570
13496
  }
12571
13497
  }
@@ -12575,10 +13501,10 @@ var init_client = __esm({
12575
13501
  * Resolves Xray internal IDs first (they differ from Jira numeric IDs).
12576
13502
  */
12577
13503
  async linkTestsCloud(testSet, tests) {
12578
- logger35.info({ testSetKey: testSet.key, testKeys: tests.map((t) => t.key) }, "Resolving Xray IDs for linking");
13504
+ logger36.info({ testSetKey: testSet.key, testKeys: tests.map((t) => t.key) }, "Resolving Xray IDs for linking");
12579
13505
  const xrayTestSetId = await this.resolveXrayIssueId(testSet.key, "Test Set");
12580
13506
  if (!xrayTestSetId) {
12581
- logger35.error({ testSetKey: testSet.key }, "Cannot link \u2014 Test Set not found in Xray");
13507
+ logger36.error({ testSetKey: testSet.key }, "Cannot link \u2014 Test Set not found in Xray");
12582
13508
  return;
12583
13509
  }
12584
13510
  const xrayTestIds = [];
@@ -12587,11 +13513,11 @@ var init_client = __esm({
12587
13513
  if (xrayTestId) {
12588
13514
  xrayTestIds.push(xrayTestId);
12589
13515
  } else {
12590
- logger35.warn({ testKey: test.key }, "Could not resolve Xray ID for test \u2014 skipping");
13516
+ logger36.warn({ testKey: test.key }, "Could not resolve Xray ID for test \u2014 skipping");
12591
13517
  }
12592
13518
  }
12593
13519
  if (xrayTestIds.length === 0) {
12594
- logger35.warn({ testSetKey: testSet.key }, "No test IDs resolved \u2014 skipping link");
13520
+ logger36.warn({ testSetKey: testSet.key }, "No test IDs resolved \u2014 skipping link");
12595
13521
  return;
12596
13522
  }
12597
13523
  try {
@@ -12607,7 +13533,7 @@ var init_client = __esm({
12607
13533
  }`
12608
13534
  );
12609
13535
  if (result) {
12610
- logger35.info({
13536
+ logger36.info({
12611
13537
  testSetKey: testSet.key,
12612
13538
  xrayTestSetId,
12613
13539
  count: xrayTestIds.length,
@@ -12616,7 +13542,7 @@ var init_client = __esm({
12616
13542
  }, "Tests linked to Test Set via Xray Cloud");
12617
13543
  }
12618
13544
  } catch (err) {
12619
- logger35.error({ testSetKey: testSet.key, xrayTestSetId, err: err instanceof Error ? err.message : String(err) }, "linkTestsCloud failed");
13545
+ logger36.error({ testSetKey: testSet.key, xrayTestSetId, err: err instanceof Error ? err.message : String(err) }, "linkTestsCloud failed");
12620
13546
  }
12621
13547
  }
12622
13548
  sleep(ms) {
@@ -12632,7 +13558,7 @@ var init_client = __esm({
12632
13558
  outwardIssue: { key: testKey }
12633
13559
  });
12634
13560
  } catch (linkErr) {
12635
- logger35.error({ testSetKey, testKey, err: linkErr }, "Failed to link test to test set");
13561
+ logger36.error({ testSetKey, testKey, err: linkErr }, "Failed to link test to test set");
12636
13562
  }
12637
13563
  }
12638
13564
  }
@@ -12645,7 +13571,7 @@ var init_client = __esm({
12645
13571
  */
12646
13572
  async createFolderAndAddTests(folderName, tests) {
12647
13573
  if (!this.isXrayCloud) {
12648
- logger35.debug("Folder creation skipped \u2014 only supported on Xray Cloud");
13574
+ logger36.debug("Folder creation skipped \u2014 only supported on Xray Cloud");
12649
13575
  return;
12650
13576
  }
12651
13577
  if (tests.length === 0) return;
@@ -12667,11 +13593,11 @@ var init_client = __esm({
12667
13593
  }
12668
13594
  }`
12669
13595
  );
12670
- logger35.info({ folderPath, projectId }, "Test Repository folder created");
13596
+ logger36.info({ folderPath, projectId }, "Test Repository folder created");
12671
13597
  } catch (folderErr) {
12672
13598
  const errMsg = folderErr instanceof Error ? folderErr.message : String(folderErr);
12673
13599
  if (errMsg.includes("already exists")) {
12674
- logger35.info({ folderPath }, "Test Repository folder already exists \u2014 reusing");
13600
+ logger36.info({ folderPath }, "Test Repository folder already exists \u2014 reusing");
12675
13601
  } else {
12676
13602
  throw folderErr;
12677
13603
  }
@@ -12682,14 +13608,14 @@ var init_client = __esm({
12682
13608
  if (xrayId) {
12683
13609
  xrayTestIds.push(xrayId);
12684
13610
  } else {
12685
- logger35.warn({ testKey: test.key }, "Could not resolve Xray ID for folder \u2014 skipping");
13611
+ logger36.warn({ testKey: test.key }, "Could not resolve Xray ID for folder \u2014 skipping");
12686
13612
  }
12687
13613
  }
12688
13614
  if (xrayTestIds.length === 0) {
12689
- logger35.warn({ folderPath }, "No Xray IDs resolved \u2014 cannot add tests to folder");
13615
+ logger36.warn({ folderPath }, "No Xray IDs resolved \u2014 cannot add tests to folder");
12690
13616
  return;
12691
13617
  }
12692
- logger35.info({ folderPath, xrayTestIds }, "Adding tests to folder with resolved Xray IDs");
13618
+ logger36.info({ folderPath, xrayTestIds }, "Adding tests to folder with resolved Xray IDs");
12693
13619
  try {
12694
13620
  await this.xrayGraphQL(
12695
13621
  `mutation {
@@ -12703,12 +13629,12 @@ var init_client = __esm({
12703
13629
  }
12704
13630
  }`
12705
13631
  );
12706
- logger35.info({ folderPath, count: xrayTestIds.length }, "Tests added to folder");
13632
+ logger36.info({ folderPath, count: xrayTestIds.length }, "Tests added to folder");
12707
13633
  } catch (addErr) {
12708
- logger35.warn({ folderPath, err: addErr instanceof Error ? addErr.message : String(addErr) }, "Failed to add tests to folder");
13634
+ logger36.warn({ folderPath, err: addErr instanceof Error ? addErr.message : String(addErr) }, "Failed to add tests to folder");
12709
13635
  }
12710
13636
  } catch (err) {
12711
- logger35.error({ folderName, err: err instanceof Error ? err.message : String(err) }, "FAILED to create folder or add tests \u2014 tests exist but are unorganized");
13637
+ logger36.error({ folderName, err: err instanceof Error ? err.message : String(err) }, "FAILED to create folder or add tests \u2014 tests exist but are unorganized");
12712
13638
  }
12713
13639
  }
12714
13640
  // ─── Xray Cloud GraphQL helper ────────────────────────────────────────────
@@ -12726,12 +13652,12 @@ var init_client = __esm({
12726
13652
  });
12727
13653
  if (!res.ok) {
12728
13654
  const text = await res.text();
12729
- logger35.warn({ status: res.status, body: text }, "Xray Cloud GraphQL request failed");
13655
+ logger36.warn({ status: res.status, body: text }, "Xray Cloud GraphQL request failed");
12730
13656
  throw new Error(`Xray GraphQL \u2192 ${res.status}: ${text}`);
12731
13657
  }
12732
13658
  const result = await res.json();
12733
13659
  if (result.errors && result.errors.length > 0) {
12734
- logger35.warn({ errors: result.errors }, "Xray Cloud GraphQL returned errors");
13660
+ logger36.warn({ errors: result.errors }, "Xray Cloud GraphQL returned errors");
12735
13661
  throw new Error(`Xray GraphQL errors: ${result.errors.map((e) => e.message).join("; ")}`);
12736
13662
  }
12737
13663
  return result.data ?? null;
@@ -12749,7 +13675,7 @@ var init_client = __esm({
12749
13675
  const delays = [0, 5e3, 1e4, 2e4];
12750
13676
  for (let attempt = 0; attempt < delays.length; attempt++) {
12751
13677
  if (delays[attempt] > 0) {
12752
- logger35.info({ jiraKey, attempt, delay: delays[attempt] }, `Waiting ${delays[attempt] / 1e3}s before retry`);
13678
+ logger36.info({ jiraKey, attempt, delay: delays[attempt] }, `Waiting ${delays[attempt] / 1e3}s before retry`);
12753
13679
  await this.sleep(delays[attempt]);
12754
13680
  }
12755
13681
  try {
@@ -12762,21 +13688,21 @@ var init_client = __esm({
12762
13688
  }
12763
13689
  }
12764
13690
  }`;
12765
- logger35.info({ jiraKey, attempt, query: queryName }, `Querying Xray for ${type} issueId`);
13691
+ logger36.info({ jiraKey, attempt, query: queryName }, `Querying Xray for ${type} issueId`);
12766
13692
  const result = await this.xrayGraphQL(gql);
12767
13693
  const queryResult = result?.[resultField];
12768
- logger35.info({ jiraKey, attempt, total: queryResult?.total, resultCount: queryResult?.results?.length, raw: JSON.stringify(queryResult) }, `Xray ${queryName} response`);
13694
+ logger36.info({ jiraKey, attempt, total: queryResult?.total, resultCount: queryResult?.results?.length, raw: JSON.stringify(queryResult) }, `Xray ${queryName} response`);
12769
13695
  const items = queryResult?.results;
12770
13696
  if (items && items.length > 0 && items[0].issueId) {
12771
- logger35.info({ jiraKey, xrayIssueId: items[0].issueId, attempt }, `Resolved Xray issueId for ${type}`);
13697
+ logger36.info({ jiraKey, xrayIssueId: items[0].issueId, attempt }, `Resolved Xray issueId for ${type}`);
12772
13698
  return items[0].issueId;
12773
13699
  }
12774
- logger35.warn({ jiraKey, attempt, total: queryResult?.total }, `Xray has not indexed ${type} yet \u2014 no results`);
13700
+ logger36.warn({ jiraKey, attempt, total: queryResult?.total }, `Xray has not indexed ${type} yet \u2014 no results`);
12775
13701
  } catch (err) {
12776
- logger35.warn({ jiraKey, attempt, err: err instanceof Error ? err.message : String(err) }, `resolveXrayIssueId attempt ${attempt} failed`);
13702
+ logger36.warn({ jiraKey, attempt, err: err instanceof Error ? err.message : String(err) }, `resolveXrayIssueId attempt ${attempt} failed`);
12777
13703
  }
12778
13704
  }
12779
- logger35.error({ jiraKey }, `Could not resolve Xray issueId for ${type} after ${delays.length} attempts`);
13705
+ logger36.error({ jiraKey }, `Could not resolve Xray issueId for ${type} after ${delays.length} attempts`);
12780
13706
  return null;
12781
13707
  }
12782
13708
  // ─── Validation ────────────────────────────────────────────────────────────
@@ -12842,11 +13768,11 @@ var init_xray = __esm({
12842
13768
 
12843
13769
  // src/storage/xray-store.ts
12844
13770
  function mappingPath(rootDir) {
12845
- return import_path29.default.join(rootDir, "assuremind-data", MAPPING_FILE);
13771
+ return import_path30.default.join(rootDir, "assuremind-data", MAPPING_FILE);
12846
13772
  }
12847
13773
  async function readXrayMapping(rootDir) {
12848
13774
  const filePath = mappingPath(rootDir);
12849
- if (!await import_fs_extra20.default.pathExists(filePath)) {
13775
+ if (!await import_fs_extra23.default.pathExists(filePath)) {
12850
13776
  return {
12851
13777
  projectKey: "",
12852
13778
  suites: [],
@@ -12856,17 +13782,17 @@ async function readXrayMapping(rootDir) {
12856
13782
  const raw = await readJson(filePath);
12857
13783
  const result = XrayMappingFileSchema.safeParse(raw);
12858
13784
  if (!result.success) {
12859
- logger36.warn({ path: filePath, errors: result.error.issues }, "Invalid xray-mapping.json \u2014 returning empty");
13785
+ logger37.warn({ path: filePath, errors: result.error.issues }, "Invalid xray-mapping.json \u2014 returning empty");
12860
13786
  return { projectKey: "", suites: [], updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
12861
13787
  }
12862
13788
  return result.data;
12863
13789
  }
12864
13790
  async function writeXrayMapping(rootDir, mapping) {
12865
13791
  const filePath = mappingPath(rootDir);
12866
- await import_fs_extra20.default.ensureDir(import_path29.default.dirname(filePath));
13792
+ await import_fs_extra23.default.ensureDir(import_path30.default.dirname(filePath));
12867
13793
  const data = { ...mapping, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
12868
13794
  await atomicWriteJson(filePath, data);
12869
- logger36.debug({ path: filePath }, "Xray mapping written");
13795
+ logger37.debug({ path: filePath }, "Xray mapping written");
12870
13796
  }
12871
13797
  function findSuiteMapping(mapping, suiteId) {
12872
13798
  return mapping.suites.find((s) => s.suiteId === suiteId);
@@ -12898,24 +13824,24 @@ function upsertCaseMapping(suiteMapping, caseMapping) {
12898
13824
  }
12899
13825
  return { ...suiteMapping, cases };
12900
13826
  }
12901
- var import_path29, import_fs_extra20, logger36, MAPPING_FILE;
13827
+ var import_path30, import_fs_extra23, logger37, MAPPING_FILE;
12902
13828
  var init_xray_store = __esm({
12903
13829
  "src/storage/xray-store.ts"() {
12904
13830
  "use strict";
12905
13831
  init_cjs_shims();
12906
- import_path29 = __toESM(require("path"));
12907
- import_fs_extra20 = __toESM(require("fs-extra"));
13832
+ import_path30 = __toESM(require("path"));
13833
+ import_fs_extra23 = __toESM(require("fs-extra"));
12908
13834
  init_xray();
12909
13835
  init_utils();
12910
13836
  init_logger();
12911
- logger36 = createChildLogger("xray-store");
13837
+ logger37 = createChildLogger("xray-store");
12912
13838
  MAPPING_FILE = "xray-mapping.json";
12913
13839
  }
12914
13840
  });
12915
13841
 
12916
13842
  // src/xray/service.ts
12917
13843
  function contentHash(obj) {
12918
- return import_crypto7.default.createHash("sha256").update(JSON.stringify(obj)).digest("hex").slice(0, 16);
13844
+ return import_crypto11.default.createHash("sha256").update(JSON.stringify(obj)).digest("hex").slice(0, 16);
12919
13845
  }
12920
13846
  function suiteHash(suite) {
12921
13847
  return contentHash({ name: suite.name, description: suite.description, tags: suite.tags });
@@ -12929,18 +13855,18 @@ function caseHash(tc) {
12929
13855
  steps: tc.steps.map((s) => ({ instruction: s.instruction, code: s.generatedCode }))
12930
13856
  });
12931
13857
  }
12932
- var import_crypto7, logger37, XrayService;
13858
+ var import_crypto11, logger38, XrayService;
12933
13859
  var init_service = __esm({
12934
13860
  "src/xray/service.ts"() {
12935
13861
  "use strict";
12936
13862
  init_cjs_shims();
12937
- import_crypto7 = __toESM(require("crypto"));
13863
+ import_crypto11 = __toESM(require("crypto"));
12938
13864
  init_client();
12939
13865
  init_xray_store();
12940
13866
  init_suite_store();
12941
13867
  init_case_store();
12942
13868
  init_logger();
12943
- logger37 = createChildLogger("xray-service");
13869
+ logger38 = createChildLogger("xray-service");
12944
13870
  XrayService = class {
12945
13871
  client;
12946
13872
  rootDir;
@@ -13048,7 +13974,7 @@ var init_service = __esm({
13048
13974
  const results = [];
13049
13975
  let sm = findSuiteMapping(mapping, suiteId);
13050
13976
  if (sm) {
13051
- logger37.info({ suiteId }, "Suite already mapped in Xray \u2014 delegating to sync");
13977
+ logger38.info({ suiteId }, "Suite already mapped in Xray \u2014 delegating to sync");
13052
13978
  return this.syncSuite(suiteId);
13053
13979
  }
13054
13980
  const folderPath = `/${suite.name}`;
@@ -13077,7 +14003,7 @@ var init_service = __esm({
13077
14003
  await writeXrayMapping(this.rootDir, mapping);
13078
14004
  createdTestIssueIds.push(test.id);
13079
14005
  results.push({ success: true, caseId: tc.id, xrayKey: test.key, xrayId: test.id, action: "created" });
13080
- logger37.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
14006
+ logger38.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
13081
14007
  } catch (err) {
13082
14008
  results.push({
13083
14009
  success: false,
@@ -13097,9 +14023,9 @@ var init_service = __esm({
13097
14023
  if (allTestPairs.length > 0) {
13098
14024
  try {
13099
14025
  await this.client.createFolderAndAddTests(suite.name, allTestPairs);
13100
- logger37.info({ folderPath, testCount: allTestPairs.length }, "Tests organized into folder");
14026
+ logger38.info({ folderPath, testCount: allTestPairs.length }, "Tests organized into folder");
13101
14027
  } catch (err) {
13102
- logger37.warn({ folderPath, err: err instanceof Error ? err.message : String(err) }, "Folder organization failed (non-fatal)");
14028
+ logger38.warn({ folderPath, err: err instanceof Error ? err.message : String(err) }, "Folder organization failed (non-fatal)");
13103
14029
  }
13104
14030
  }
13105
14031
  if (!findSuiteMapping(mapping, suiteId)?.xrayTestSetId) {
@@ -13116,7 +14042,7 @@ var init_service = __esm({
13116
14042
  mapping = upsertSuiteMapping(mapping, sm);
13117
14043
  await writeXrayMapping(this.rootDir, mapping);
13118
14044
  results.push({ success: true, suiteId, xrayKey: testSet.key, action: "created" });
13119
- logger37.info({ suiteId, xrayKey: testSet.key, testCount: createdTestIssueIds.length }, "Test Set created with linked tests");
14045
+ logger38.info({ suiteId, xrayKey: testSet.key, testCount: createdTestIssueIds.length }, "Test Set created with linked tests");
13120
14046
  } catch (err) {
13121
14047
  results.push({
13122
14048
  success: false,
@@ -13185,7 +14111,7 @@ var init_service = __esm({
13185
14111
  { key: caseResult.result.xrayKey, id: caseResult.result.xrayId }
13186
14112
  ]);
13187
14113
  } catch (err) {
13188
- logger37.warn({ err: err instanceof Error ? err.message : String(err) }, "Folder organization failed for single case (non-fatal)");
14114
+ logger38.warn({ err: err instanceof Error ? err.message : String(err) }, "Folder organization failed for single case (non-fatal)");
13189
14115
  }
13190
14116
  }
13191
14117
  if (caseResult.result.xrayId && sm.xrayTestSetIssueId) {
@@ -13195,7 +14121,7 @@ var init_service = __esm({
13195
14121
  [{ key: caseResult.result.xrayKey ?? "", id: caseResult.result.xrayId }]
13196
14122
  );
13197
14123
  } catch (err) {
13198
- logger37.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to link test to test set");
14124
+ logger38.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to link test to test set");
13199
14125
  }
13200
14126
  }
13201
14127
  return this.summarizeResults([caseResult.result]);
@@ -13226,7 +14152,7 @@ var init_service = __esm({
13226
14152
  const updatedSm = upsertCaseMapping(sm, cm);
13227
14153
  const updatedMapping = upsertSuiteMapping(mapping, updatedSm);
13228
14154
  await writeXrayMapping(this.rootDir, updatedMapping);
13229
- logger37.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
14155
+ logger38.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
13230
14156
  return {
13231
14157
  result: { success: true, caseId: tc.id, xrayKey: test.key, xrayId: test.id, action: "created" },
13232
14158
  mapping: updatedMapping
@@ -13292,7 +14218,7 @@ var init_service = __esm({
13292
14218
  mapping = upsertSuiteMapping(mapping, sm);
13293
14219
  await writeXrayMapping(this.rootDir, mapping);
13294
14220
  results.push({ success: true, suiteId, xrayKey: testSet.key, action: "created" });
13295
- logger37.info({ suiteId, xrayKey: testSet.key }, "Test Set created during sync (was missing)");
14221
+ logger38.info({ suiteId, xrayKey: testSet.key }, "Test Set created during sync (was missing)");
13296
14222
  } catch (err) {
13297
14223
  results.push({
13298
14224
  success: false,
@@ -13388,11 +14314,11 @@ var init_service = __esm({
13388
14314
  try {
13389
14315
  await this.client.createFolderAndAddTests(suite.name, newTests);
13390
14316
  } catch (folderErr) {
13391
- logger37.warn({ err: folderErr instanceof Error ? folderErr.message : String(folderErr) }, "Folder organization failed for synced tests (non-fatal)");
14317
+ logger38.warn({ err: folderErr instanceof Error ? folderErr.message : String(folderErr) }, "Folder organization failed for synced tests (non-fatal)");
13392
14318
  }
13393
14319
  }
13394
14320
  } catch (err) {
13395
- logger37.warn({ err }, "Failed to link new tests to test set");
14321
+ logger38.warn({ err }, "Failed to link new tests to test set");
13396
14322
  }
13397
14323
  }
13398
14324
  }
@@ -13549,7 +14475,7 @@ async function xrayRoutes(fastify) {
13549
14475
  const service = requireService(reply);
13550
14476
  if (!service) return;
13551
14477
  try {
13552
- logger38.info("Creating all suites and cases in Xray");
14478
+ logger39.info("Creating all suites and cases in Xray");
13553
14479
  const result = await service.createAll();
13554
14480
  return reply.send(result);
13555
14481
  } catch (err) {
@@ -13560,7 +14486,7 @@ async function xrayRoutes(fastify) {
13560
14486
  const service = requireService(reply);
13561
14487
  if (!service) return;
13562
14488
  try {
13563
- logger38.info({ suiteId: req.params.suiteId }, "Creating suite in Xray");
14489
+ logger39.info({ suiteId: req.params.suiteId }, "Creating suite in Xray");
13564
14490
  const result = await service.createSuite(req.params.suiteId);
13565
14491
  return reply.send(result);
13566
14492
  } catch (err) {
@@ -13584,7 +14510,7 @@ async function xrayRoutes(fastify) {
13584
14510
  const service = requireService(reply);
13585
14511
  if (!service) return;
13586
14512
  try {
13587
- logger38.info("Syncing all suites and cases with Xray");
14513
+ logger39.info("Syncing all suites and cases with Xray");
13588
14514
  const result = await service.syncAll();
13589
14515
  return reply.send(result);
13590
14516
  } catch (err) {
@@ -13595,7 +14521,7 @@ async function xrayRoutes(fastify) {
13595
14521
  const service = requireService(reply);
13596
14522
  if (!service) return;
13597
14523
  try {
13598
- logger38.info({ suiteId: req.params.suiteId }, "Syncing suite with Xray");
14524
+ logger39.info({ suiteId: req.params.suiteId }, "Syncing suite with Xray");
13599
14525
  const result = await service.syncSuite(req.params.suiteId);
13600
14526
  return reply.send(result);
13601
14527
  } catch (err) {
@@ -13626,7 +14552,7 @@ async function xrayRoutes(fastify) {
13626
14552
  }
13627
14553
  });
13628
14554
  }
13629
- var logger38;
14555
+ var logger39;
13630
14556
  var init_xray2 = __esm({
13631
14557
  "src/server/routes/xray.ts"() {
13632
14558
  "use strict";
@@ -13634,21 +14560,21 @@ var init_xray2 = __esm({
13634
14560
  init_service();
13635
14561
  init_utils2();
13636
14562
  init_logger();
13637
- logger38 = createChildLogger("xray-routes");
14563
+ logger39 = createChildLogger("xray-routes");
13638
14564
  }
13639
14565
  });
13640
14566
 
13641
14567
  // src/git/service.ts
13642
- var import_simple_git, import_fs_extra21, import_path30, logger39, GitService;
14568
+ var import_simple_git, import_fs_extra24, import_path31, logger40, GitService;
13643
14569
  var init_service2 = __esm({
13644
14570
  "src/git/service.ts"() {
13645
14571
  "use strict";
13646
14572
  init_cjs_shims();
13647
14573
  import_simple_git = __toESM(require("simple-git"));
13648
- import_fs_extra21 = __toESM(require("fs-extra"));
13649
- import_path30 = __toESM(require("path"));
14574
+ import_fs_extra24 = __toESM(require("fs-extra"));
14575
+ import_path31 = __toESM(require("path"));
13650
14576
  init_logger();
13651
- logger39 = createChildLogger("git-service");
14577
+ logger40 = createChildLogger("git-service");
13652
14578
  GitService = class {
13653
14579
  git;
13654
14580
  rootDir;
@@ -13710,7 +14636,7 @@ var init_service2 = __esm({
13710
14636
  try {
13711
14637
  await this.git.checkout(branch);
13712
14638
  this.broadcastLog("Checkout branch", "done", branch);
13713
- logger39.info({ branch }, "Switched branch");
14639
+ logger40.info({ branch }, "Switched branch");
13714
14640
  } catch (err) {
13715
14641
  const msg = err instanceof Error ? err.message : String(err);
13716
14642
  this.broadcastLog("Checkout branch", "error", msg);
@@ -13728,7 +14654,7 @@ var init_service2 = __esm({
13728
14654
  const hash = result.commit || "unknown";
13729
14655
  this.broadcastLog("Create commit", "done", hash);
13730
14656
  this.broadcastDone("commit");
13731
- logger39.info({ hash, message }, "Commit created");
14657
+ logger40.info({ hash, message }, "Commit created");
13732
14658
  return hash;
13733
14659
  } catch (err) {
13734
14660
  const msg = err instanceof Error ? err.message : String(err);
@@ -13745,7 +14671,7 @@ var init_service2 = __esm({
13745
14671
  await this.git.pull(void 0, void 0, opts);
13746
14672
  this.broadcastLog(label, "done", rebase ? "Rebase successful" : "Pull successful");
13747
14673
  this.broadcastDone("pull");
13748
- logger39.info({ rebase }, "Pull completed");
14674
+ logger40.info({ rebase }, "Pull completed");
13749
14675
  } catch (err) {
13750
14676
  const msg = err instanceof Error ? err.message : String(err);
13751
14677
  if (msg.includes("CONFLICT") || msg.includes("conflict") || msg.includes("could not apply")) {
@@ -13767,7 +14693,7 @@ var init_service2 = __esm({
13767
14693
  await this.git.push();
13768
14694
  this.broadcastLog("Push to origin", "done");
13769
14695
  this.broadcastDone("push");
13770
- logger39.info("Push completed");
14696
+ logger40.info("Push completed");
13771
14697
  } catch (err) {
13772
14698
  const msg = err instanceof Error ? err.message : String(err);
13773
14699
  this.broadcastLog("Push to origin", "error", msg);
@@ -13814,7 +14740,7 @@ var init_service2 = __esm({
13814
14740
  throw pushErr;
13815
14741
  }
13816
14742
  this.broadcastDone("full-sync");
13817
- logger39.info("Full sync completed");
14743
+ logger40.info("Full sync completed");
13818
14744
  } catch (err) {
13819
14745
  this.broadcastDone("full-sync");
13820
14746
  throw err;
@@ -13846,8 +14772,8 @@ var init_service2 = __esm({
13846
14772
  return status.conflicted;
13847
14773
  }
13848
14774
  async getFileConflictContent(filePath) {
13849
- const fullPath = import_path30.default.join(this.rootDir, filePath);
13850
- const content = await import_fs_extra21.default.readFile(fullPath, "utf-8");
14775
+ const fullPath = import_path31.default.join(this.rootDir, filePath);
14776
+ const content = await import_fs_extra24.default.readFile(fullPath, "utf-8");
13851
14777
  let ours = "";
13852
14778
  let theirs = "";
13853
14779
  let section = "none";
@@ -13873,10 +14799,10 @@ var init_service2 = __esm({
13873
14799
  return { ours: ours.trimEnd(), theirs: theirs.trimEnd() };
13874
14800
  }
13875
14801
  async resolveConflict(filePath, resolvedContent) {
13876
- const fullPath = import_path30.default.join(this.rootDir, filePath);
13877
- await import_fs_extra21.default.writeFile(fullPath, resolvedContent, "utf-8");
14802
+ const fullPath = import_path31.default.join(this.rootDir, filePath);
14803
+ await import_fs_extra24.default.writeFile(fullPath, resolvedContent, "utf-8");
13878
14804
  await this.git.add(filePath);
13879
- logger39.info({ filePath }, "Conflict resolved");
14805
+ logger40.info({ filePath }, "Conflict resolved");
13880
14806
  }
13881
14807
  async continueRebase() {
13882
14808
  this.broadcastLog("Continuing rebase...", "running");
@@ -13951,10 +14877,10 @@ async function generateCommitMessage(diff) {
13951
14877
  cleaned = cleaned.replace(/^["'`]+|["'`]+$/g, "");
13952
14878
  cleaned = cleaned.replace(/^(commit message:?\s*)/i, "");
13953
14879
  cleaned = cleaned.split("\n")[0].trim();
13954
- logger40.info({ messageLength: cleaned.length }, "AI commit message generated");
14880
+ logger41.info({ messageLength: cleaned.length }, "AI commit message generated");
13955
14881
  return cleaned || "chore: update files";
13956
14882
  } catch (err) {
13957
- logger40.warn({ err: err instanceof Error ? err.message : String(err) }, "AI commit message generation failed");
14883
+ logger41.warn({ err: err instanceof Error ? err.message : String(err) }, "AI commit message generation failed");
13958
14884
  return "chore: update files";
13959
14885
  }
13960
14886
  }
@@ -13981,21 +14907,21 @@ ${theirs}`);
13981
14907
  const provider = createProvider(void 0, { temperature: 0.4 });
13982
14908
  let result = await provider.generateCode(instruction, context);
13983
14909
  result = result.replace(/^```[\w]*\n?/, "").replace(/\n?```$/, "");
13984
- logger40.info({ filePath, resultLength: result.length }, "AI conflict resolution generated");
14910
+ logger41.info({ filePath, resultLength: result.length }, "AI conflict resolution generated");
13985
14911
  return result;
13986
14912
  } catch (err) {
13987
- logger40.warn({ filePath, err: err instanceof Error ? err.message : String(err) }, "AI conflict resolution failed");
14913
+ logger41.warn({ filePath, err: err instanceof Error ? err.message : String(err) }, "AI conflict resolution failed");
13988
14914
  throw err;
13989
14915
  }
13990
14916
  }
13991
- var logger40;
14917
+ var logger41;
13992
14918
  var init_ai = __esm({
13993
14919
  "src/git/ai.ts"() {
13994
14920
  "use strict";
13995
14921
  init_cjs_shims();
13996
14922
  init_router();
13997
14923
  init_logger();
13998
- logger40 = createChildLogger("git-ai");
14924
+ logger41 = createChildLogger("git-ai");
13999
14925
  }
14000
14926
  });
14001
14927
 
@@ -14012,7 +14938,7 @@ async function gitRoutes(fastify) {
14012
14938
  _isGitRepo = true;
14013
14939
  } catch {
14014
14940
  _isGitRepo = false;
14015
- logger41.info("Project directory is not a git repository \u2014 git features disabled");
14941
+ logger42.info("Project directory is not a git repository \u2014 git features disabled");
14016
14942
  }
14017
14943
  return _isGitRepo;
14018
14944
  };
@@ -14188,7 +15114,7 @@ async function gitRoutes(fastify) {
14188
15114
  const changedFiles = stdout.split("\n").filter(Boolean);
14189
15115
  const { listSuiteDirs: listSuiteDirs2, readSuite: readSuite2 } = await Promise.resolve().then(() => (init_suite_store(), suite_store_exports));
14190
15116
  const { listCases: listCases2 } = await Promise.resolve().then(() => (init_case_store(), case_store_exports));
14191
- const testsDir = import_path31.default.join(rootDir, "tests");
15117
+ const testsDir = import_path32.default.join(rootDir, "tests");
14192
15118
  const suiteDirs = await listSuiteDirs2(testsDir);
14193
15119
  const impacted = [];
14194
15120
  for (const suiteDir of suiteDirs) {
@@ -14198,8 +15124,8 @@ async function gitRoutes(fastify) {
14198
15124
  for (const tc of cases) {
14199
15125
  const allText = [tc.name, tc.description ?? "", ...tc.steps.map((s) => s.instruction)].join(" ").toLowerCase();
14200
15126
  for (const file of changedFiles) {
14201
- const basename = import_path31.default.basename(file).toLowerCase();
14202
- const dirname = import_path31.default.dirname(file).toLowerCase();
15127
+ const basename = import_path32.default.basename(file).toLowerCase();
15128
+ const dirname = import_path32.default.dirname(file).toLowerCase();
14203
15129
  if (allText.includes(basename) || allText.includes(dirname)) {
14204
15130
  impacted.push({ caseId: tc.id, caseName: tc.name, suiteId: suite.id, suiteName: suite.name, reason: `Mentions "${file}"` });
14205
15131
  break;
@@ -14215,18 +15141,18 @@ async function gitRoutes(fastify) {
14215
15141
  }
14216
15142
  });
14217
15143
  }
14218
- var import_path31, import_zod18, logger41, CheckoutBody, CommitBody, PullBody, FullSyncBody, ResolveConflictBody, ResolveConflictAIBody;
15144
+ var import_path32, import_zod18, logger42, CheckoutBody, CommitBody, PullBody, FullSyncBody, ResolveConflictBody, ResolveConflictAIBody;
14219
15145
  var init_git = __esm({
14220
15146
  "src/server/routes/git.ts"() {
14221
15147
  "use strict";
14222
15148
  init_cjs_shims();
14223
- import_path31 = __toESM(require("path"));
15149
+ import_path32 = __toESM(require("path"));
14224
15150
  import_zod18 = require("zod");
14225
15151
  init_service2();
14226
15152
  init_ai();
14227
15153
  init_utils2();
14228
15154
  init_logger();
14229
- logger41 = createChildLogger("git-routes");
15155
+ logger42 = createChildLogger("git-routes");
14230
15156
  CheckoutBody = import_zod18.z.object({ branch: import_zod18.z.string().min(1) });
14231
15157
  CommitBody = import_zod18.z.object({ message: import_zod18.z.string().min(1) });
14232
15158
  PullBody = import_zod18.z.object({ rebase: import_zod18.z.boolean().optional() });
@@ -14271,12 +15197,12 @@ function parseCSV(content) {
14271
15197
  }
14272
15198
  async function dataFileRoutes(fastify) {
14273
15199
  const rootDir = fastify.rootDir;
14274
- const dataDir = import_path32.default.join(rootDir, "tests", "data");
14275
- await import_fs_extra22.default.ensureDir(dataDir);
15200
+ const dataDir = import_path33.default.join(rootDir, "tests", "data");
15201
+ await import_fs_extra25.default.ensureDir(dataDir);
14276
15202
  fastify.get("/data-files", async (_req, reply) => {
14277
15203
  try {
14278
- await import_fs_extra22.default.ensureDir(dataDir);
14279
- const entries = await import_fs_extra22.default.readdir(dataDir, { withFileTypes: true });
15204
+ await import_fs_extra25.default.ensureDir(dataDir);
15205
+ const entries = await import_fs_extra25.default.readdir(dataDir, { withFileTypes: true });
14280
15206
  const files = entries.filter((e) => e.isFile() && ALLOWED_EXTENSIONS.some((ext) => e.name.endsWith(ext))).map((e) => ({
14281
15207
  name: e.name,
14282
15208
  type: e.name.endsWith(".csv") ? "csv" : "json",
@@ -14289,12 +15215,12 @@ async function dataFileRoutes(fastify) {
14289
15215
  });
14290
15216
  fastify.get("/data-files/:filename", async (req, reply) => {
14291
15217
  try {
14292
- const filename = import_path32.default.basename(req.params.filename);
14293
- const filePath = import_path32.default.join(dataDir, filename);
14294
- if (!await import_fs_extra22.default.pathExists(filePath)) {
15218
+ const filename = import_path33.default.basename(req.params.filename);
15219
+ const filePath = import_path33.default.join(dataDir, filename);
15220
+ if (!await import_fs_extra25.default.pathExists(filePath)) {
14295
15221
  return reply.status(404).send({ error: `Data file "${filename}" not found` });
14296
15222
  }
14297
- const raw = await import_fs_extra22.default.readFile(filePath, "utf8");
15223
+ const raw = await import_fs_extra25.default.readFile(filePath, "utf8");
14298
15224
  let rows;
14299
15225
  if (filename.endsWith(".csv")) {
14300
15226
  rows = parseCSV(raw);
@@ -14315,14 +15241,14 @@ async function dataFileRoutes(fastify) {
14315
15241
  if (!filename) {
14316
15242
  return reply.status(400).send({ error: "filename is required" });
14317
15243
  }
14318
- const ext = import_path32.default.extname(filename).toLowerCase();
15244
+ const ext = import_path33.default.extname(filename).toLowerCase();
14319
15245
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
14320
15246
  return reply.status(400).send({ error: `Only ${ALLOWED_EXTENSIONS.join(", ")} files are allowed` });
14321
15247
  }
14322
- const safeName = import_path32.default.basename(filename);
14323
- const filePath = import_path32.default.join(dataDir, safeName);
14324
- await import_fs_extra22.default.ensureDir(dataDir);
14325
- await import_fs_extra22.default.writeFile(filePath, content, "utf8");
15248
+ const safeName = import_path33.default.basename(filename);
15249
+ const filePath = import_path33.default.join(dataDir, safeName);
15250
+ await import_fs_extra25.default.ensureDir(dataDir);
15251
+ await import_fs_extra25.default.writeFile(filePath, content, "utf8");
14326
15252
  let rowCount = 0;
14327
15253
  try {
14328
15254
  if (ext === ".csv") {
@@ -14333,7 +15259,7 @@ async function dataFileRoutes(fastify) {
14333
15259
  }
14334
15260
  } catch {
14335
15261
  }
14336
- logger42.info({ filename: safeName, rowCount }, "Data file uploaded");
15262
+ logger43.info({ filename: safeName, rowCount }, "Data file uploaded");
14337
15263
  return reply.status(201).send({
14338
15264
  filename: safeName,
14339
15265
  path: `data/${safeName}`,
@@ -14345,13 +15271,13 @@ async function dataFileRoutes(fastify) {
14345
15271
  });
14346
15272
  fastify.put("/data-files/:filename", async (req, reply) => {
14347
15273
  try {
14348
- const filename = import_path32.default.basename(req.params.filename);
14349
- const filePath = import_path32.default.join(dataDir, filename);
15274
+ const filename = import_path33.default.basename(req.params.filename);
15275
+ const filePath = import_path33.default.join(dataDir, filename);
14350
15276
  const body = req.body;
14351
15277
  const content = String(body.content ?? "");
14352
- await import_fs_extra22.default.ensureDir(dataDir);
14353
- await import_fs_extra22.default.writeFile(filePath, content, "utf8");
14354
- logger42.info({ filename }, "Data file updated");
15278
+ await import_fs_extra25.default.ensureDir(dataDir);
15279
+ await import_fs_extra25.default.writeFile(filePath, content, "utf8");
15280
+ logger43.info({ filename }, "Data file updated");
14355
15281
  return reply.send({ filename, path: `data/${filename}` });
14356
15282
  } catch (err) {
14357
15283
  return sendError(reply, 500, "Failed to update data file", err);
@@ -14359,29 +15285,29 @@ async function dataFileRoutes(fastify) {
14359
15285
  });
14360
15286
  fastify.delete("/data-files/:filename", async (req, reply) => {
14361
15287
  try {
14362
- const filename = import_path32.default.basename(req.params.filename);
14363
- const filePath = import_path32.default.join(dataDir, filename);
14364
- if (!await import_fs_extra22.default.pathExists(filePath)) {
15288
+ const filename = import_path33.default.basename(req.params.filename);
15289
+ const filePath = import_path33.default.join(dataDir, filename);
15290
+ if (!await import_fs_extra25.default.pathExists(filePath)) {
14365
15291
  return reply.status(404).send({ error: `Data file "${filename}" not found` });
14366
15292
  }
14367
- await import_fs_extra22.default.remove(filePath);
14368
- logger42.info({ filename }, "Data file deleted");
15293
+ await import_fs_extra25.default.remove(filePath);
15294
+ logger43.info({ filename }, "Data file deleted");
14369
15295
  return reply.status(204).send();
14370
15296
  } catch (err) {
14371
15297
  return sendError(reply, 500, "Failed to delete data file", err);
14372
15298
  }
14373
15299
  });
14374
15300
  }
14375
- var import_path32, import_fs_extra22, logger42, ALLOWED_EXTENSIONS;
15301
+ var import_path33, import_fs_extra25, logger43, ALLOWED_EXTENSIONS;
14376
15302
  var init_data_files = __esm({
14377
15303
  "src/server/routes/data-files.ts"() {
14378
15304
  "use strict";
14379
15305
  init_cjs_shims();
14380
- import_path32 = __toESM(require("path"));
14381
- import_fs_extra22 = __toESM(require("fs-extra"));
15306
+ import_path33 = __toESM(require("path"));
15307
+ import_fs_extra25 = __toESM(require("fs-extra"));
14382
15308
  init_utils2();
14383
15309
  init_logger();
14384
- logger42 = createChildLogger("routes/data-files");
15310
+ logger43 = createChildLogger("routes/data-files");
14385
15311
  ALLOWED_EXTENSIONS = [".json", ".csv"];
14386
15312
  }
14387
15313
  });
@@ -14400,7 +15326,7 @@ function matchWeight(matchedOn) {
14400
15326
  }
14401
15327
  }
14402
15328
  async function searchRoutes(fastify) {
14403
- const testsDir = import_path33.default.join(fastify.rootDir, "tests");
15329
+ const testsDir = import_path34.default.join(fastify.rootDir, "tests");
14404
15330
  fastify.get("/search", async (req, reply) => {
14405
15331
  const q = (req.query.q ?? "").trim();
14406
15332
  if (q.length < 2) {
@@ -14462,12 +15388,12 @@ async function searchRoutes(fastify) {
14462
15388
  }
14463
15389
  });
14464
15390
  }
14465
- var import_path33;
15391
+ var import_path34;
14466
15392
  var init_search = __esm({
14467
15393
  "src/server/routes/search.ts"() {
14468
15394
  "use strict";
14469
15395
  init_cjs_shims();
14470
- import_path33 = __toESM(require("path"));
15396
+ import_path34 = __toESM(require("path"));
14471
15397
  init_suite_store();
14472
15398
  init_case_store();
14473
15399
  init_utils2();
@@ -14748,13 +15674,13 @@ var init_flakiness = __esm({
14748
15674
 
14749
15675
  // src/storage/step-library-store.ts
14750
15676
  function libraryPath(rootDir) {
14751
- return import_path34.default.join(rootDir, "tests", ".step-library.json");
15677
+ return import_path35.default.join(rootDir, "tests", ".step-library.json");
14752
15678
  }
14753
15679
  async function readLibrary(rootDir) {
14754
15680
  const filePath = libraryPath(rootDir);
14755
- if (!await import_fs_extra23.default.pathExists(filePath)) return [];
15681
+ if (!await import_fs_extra26.default.pathExists(filePath)) return [];
14756
15682
  try {
14757
- const raw = await import_fs_extra23.default.readJson(filePath);
15683
+ const raw = await import_fs_extra26.default.readJson(filePath);
14758
15684
  if (!Array.isArray(raw)) return [];
14759
15685
  return raw.map((item) => LibraryStepSchema.parse(item));
14760
15686
  } catch {
@@ -14763,7 +15689,7 @@ async function readLibrary(rootDir) {
14763
15689
  }
14764
15690
  async function writeLibrary(rootDir, steps) {
14765
15691
  const filePath = libraryPath(rootDir);
14766
- await import_fs_extra23.default.ensureDir(import_path34.default.dirname(filePath));
15692
+ await import_fs_extra26.default.ensureDir(import_path35.default.dirname(filePath));
14767
15693
  await atomicWriteJson(filePath, steps);
14768
15694
  }
14769
15695
  async function addStep(rootDir, data) {
@@ -14773,6 +15699,7 @@ async function addStep(rootDir, data) {
14773
15699
  id: (0, import_uuid3.v4)(),
14774
15700
  name: data.name,
14775
15701
  instruction: data.instruction,
15702
+ generatedCode: data.generatedCode ?? "",
14776
15703
  tags: data.tags ?? [],
14777
15704
  createdAt: now2,
14778
15705
  updatedAt: now2
@@ -14794,13 +15721,13 @@ async function deleteStep(rootDir, id) {
14794
15721
  const filtered = steps.filter((s) => s.id !== id);
14795
15722
  await writeLibrary(rootDir, filtered);
14796
15723
  }
14797
- var import_path34, import_fs_extra23, import_uuid3, import_zod20, LibraryStepSchema;
15724
+ var import_path35, import_fs_extra26, import_uuid3, import_zod20, LibraryStepSchema;
14798
15725
  var init_step_library_store = __esm({
14799
15726
  "src/storage/step-library-store.ts"() {
14800
15727
  "use strict";
14801
15728
  init_cjs_shims();
14802
- import_path34 = __toESM(require("path"));
14803
- import_fs_extra23 = __toESM(require("fs-extra"));
15729
+ import_path35 = __toESM(require("path"));
15730
+ import_fs_extra26 = __toESM(require("fs-extra"));
14804
15731
  import_uuid3 = require("uuid");
14805
15732
  import_zod20 = require("zod");
14806
15733
  init_utils();
@@ -14808,6 +15735,7 @@ var init_step_library_store = __esm({
14808
15735
  id: import_zod20.z.string(),
14809
15736
  name: import_zod20.z.string().min(1),
14810
15737
  instruction: import_zod20.z.string().min(1),
15738
+ generatedCode: import_zod20.z.string().optional().default(""),
14811
15739
  tags: import_zod20.z.array(import_zod20.z.string()).default([]),
14812
15740
  createdAt: import_zod20.z.string(),
14813
15741
  updatedAt: import_zod20.z.string()
@@ -14854,6 +15782,40 @@ async function stepLibraryRoutes(fastify) {
14854
15782
  return sendError(reply, 500, "Failed to delete library step", err);
14855
15783
  }
14856
15784
  });
15785
+ fastify.post("/step-library/:id/generate", async (req, reply) => {
15786
+ let mcpSession;
15787
+ try {
15788
+ const steps = await readLibrary(rootDir);
15789
+ const step = steps.find((s) => s.id === req.params.id);
15790
+ if (!step) return reply.status(404).send({ error: `Library step "${req.params.id}" not found` });
15791
+ const config = await readConfig(rootDir);
15792
+ const variables = await resolveVariables(rootDir).catch(() => ({}));
15793
+ const useMcp = config.mcp?.enabled;
15794
+ if (useMcp) {
15795
+ mcpSession = new McpSession({
15796
+ headless: config.mcp?.headless ?? true,
15797
+ actionTimeout: config.mcp?.actionTimeout ?? 15e3,
15798
+ idleTimeout: config.mcp?.idleTimeout ?? 3e4
15799
+ });
15800
+ }
15801
+ const router = createSmartRouter(rootDir, mcpSession, useMcp ? config.mcp?.actThenScript : false);
15802
+ const result = await router.generate(step.instruction, {
15803
+ url: config.baseUrl,
15804
+ title: "",
15805
+ interactiveElements: "",
15806
+ htmlSnapshot: "",
15807
+ previousSteps: [],
15808
+ variables
15809
+ });
15810
+ await router.getCache().persist();
15811
+ const updated = await updateStep(rootDir, step.id, { generatedCode: result.code });
15812
+ return reply.send({ step: updated, strategy: result.strategy, cost: result.cost });
15813
+ } catch (err) {
15814
+ return sendError(reply, 500, "Failed to generate code for library step", err);
15815
+ } finally {
15816
+ mcpSession?.release();
15817
+ }
15818
+ });
14857
15819
  }
14858
15820
  var import_zod21, CreateBody, UpdateBody;
14859
15821
  var init_step_library = __esm({
@@ -14862,20 +15824,701 @@ var init_step_library = __esm({
14862
15824
  init_cjs_shims();
14863
15825
  import_zod21 = require("zod");
14864
15826
  init_step_library_store();
15827
+ init_config_store();
15828
+ init_variable_store();
15829
+ init_router();
15830
+ init_mcp_session();
14865
15831
  init_utils2();
14866
15832
  CreateBody = import_zod21.z.object({
14867
15833
  name: import_zod21.z.string().min(1),
14868
15834
  instruction: import_zod21.z.string().min(1),
15835
+ generatedCode: import_zod21.z.string().optional(),
14869
15836
  tags: import_zod21.z.array(import_zod21.z.string()).optional()
14870
15837
  });
14871
15838
  UpdateBody = import_zod21.z.object({
14872
15839
  name: import_zod21.z.string().min(1).optional(),
14873
15840
  instruction: import_zod21.z.string().min(1).optional(),
15841
+ generatedCode: import_zod21.z.string().optional(),
14874
15842
  tags: import_zod21.z.array(import_zod21.z.string()).optional()
14875
15843
  });
14876
15844
  }
14877
15845
  });
14878
15846
 
15847
+ // src/engine/recorder.ts
15848
+ function buildCaptureScript() {
15849
+ return `
15850
+ (function() {
15851
+ if (window.__amListenersAttached) return;
15852
+ window.__amListenersAttached = true;
15853
+
15854
+ // Safety wrapper \u2014 queue if binding not ready
15855
+ var sendQueue = [];
15856
+ function safeSend(json) {
15857
+ if (typeof window.__amSend === 'function') {
15858
+ while (sendQueue.length > 0) { try { window.__amSend(sendQueue.shift()); } catch(e) {} }
15859
+ try { window.__amSend(json); } catch(e) { sendQueue.push(json); }
15860
+ } else { sendQueue.push(json); }
15861
+ }
15862
+ var retryInterval = setInterval(function() {
15863
+ if (typeof window.__amSend === 'function' && sendQueue.length > 0) {
15864
+ while (sendQueue.length > 0) { try { window.__amSend(sendQueue.shift()); } catch(e) { break; } }
15865
+ }
15866
+ if (typeof window.__amSend === 'function') clearInterval(retryInterval);
15867
+ }, 200);
15868
+
15869
+ // \u2500\u2500 Detect iframe context \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
15870
+ // When running inside an iframe, compute a selector for the iframe element
15871
+ // as seen from the parent frame. This lets Node.js use page.frameLocator().
15872
+ var _frameSelector = '';
15873
+ try {
15874
+ if (window !== window.top && window.frameElement) {
15875
+ var iframe = window.frameElement;
15876
+ if (iframe.id) { _frameSelector = '#' + iframe.id; }
15877
+ else if (iframe.getAttribute('name')) { _frameSelector = 'iframe[name="' + iframe.getAttribute('name') + '"]'; }
15878
+ else if (iframe.getAttribute('data-testid')) { _frameSelector = 'iframe[data-testid="' + iframe.getAttribute('data-testid') + '"]'; }
15879
+ else if (iframe.getAttribute('src')) {
15880
+ // Use src but strip query params for stability
15881
+ var src = iframe.getAttribute('src').split('?')[0];
15882
+ _frameSelector = 'iframe[src*="' + src + '"]';
15883
+ }
15884
+ else { _frameSelector = 'iframe'; }
15885
+ }
15886
+ } catch(e) {
15887
+ // Cross-origin iframe \u2014 frameElement is null, try parent postMessage approach
15888
+ // For same-origin iframes this works; cross-origin will use fallback
15889
+ _frameSelector = '';
15890
+ }
15891
+
15892
+ // \u2500\u2500 Collect element metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
15893
+ function getMeta(el) {
15894
+ var tag = el.tagName.toLowerCase();
15895
+ var type = (el.getAttribute('type') || '').toLowerCase();
15896
+ var meta = {
15897
+ tag: tag,
15898
+ type: type,
15899
+ id: el.id || '',
15900
+ testId: el.getAttribute('data-testid') || el.getAttribute('data-test-id') || el.getAttribute('data-cy') || '',
15901
+ ariaLabel: el.getAttribute('aria-label') || '',
15902
+ ariaRole: el.getAttribute('role') || '',
15903
+ placeholder: el.getAttribute('placeholder') || '',
15904
+ labelText: getLabel(el) || '',
15905
+ innerText: visText(el),
15906
+ title: el.getAttribute('title') || '',
15907
+ name: el.getAttribute('name') || '',
15908
+ value: (el.value !== undefined ? el.value : '') || '',
15909
+ checked: !!el.checked,
15910
+ cssPath: cssPath(el),
15911
+ classes: Array.from(el.classList || []).join(' '),
15912
+ };
15913
+ if (_frameSelector) { meta.frameSelector = _frameSelector; }
15914
+ return meta;
15915
+ }
15916
+
15917
+ function getLabel(el) {
15918
+ if (!el.id) {
15919
+ var wrap = el.closest('label');
15920
+ if (wrap) {
15921
+ var clone = wrap.cloneNode(true);
15922
+ clone.querySelectorAll('input,select,textarea').forEach(function(i) { i.remove(); });
15923
+ var t = clone.textContent.trim();
15924
+ if (t) return t;
15925
+ }
15926
+ return null;
15927
+ }
15928
+ var lbl = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
15929
+ return lbl ? lbl.textContent.trim() : null;
15930
+ }
15931
+
15932
+ function visText(el) {
15933
+ var t = el.innerText || el.textContent || '';
15934
+ return t.trim().replace(/\\s+/g, ' ').slice(0, 80);
15935
+ }
15936
+
15937
+ function cssPath(el) {
15938
+ if (el.id) return '#' + CSS.escape(el.id);
15939
+ var parts = [], cur = el;
15940
+ for (var i = 0; i < 4 && cur && cur !== document.body; i++) {
15941
+ var sel = cur.tagName.toLowerCase();
15942
+ if (cur.id) { parts.unshift('#' + CSS.escape(cur.id)); break; }
15943
+ var cls = Array.from(cur.classList).filter(function(c) { return !/^(ng-|_|css-|sc-|jsx-|emotion)/.test(c); }).slice(0, 2);
15944
+ if (cls.length) sel += '.' + cls.map(function(c) { return CSS.escape(c); }).join('.');
15945
+ var p = cur.parentElement;
15946
+ if (p) {
15947
+ var sibs = Array.from(p.children).filter(function(s) { return s.tagName === cur.tagName; });
15948
+ if (sibs.length > 1) sel += ':nth-child(' + (sibs.indexOf(cur) + 1) + ')';
15949
+ }
15950
+ parts.unshift(sel);
15951
+ cur = cur.parentElement;
15952
+ }
15953
+ return parts.join(' > ');
15954
+ }
15955
+
15956
+ // Human-readable element description for instructions
15957
+ function descEl(meta) {
15958
+ var t = meta.tag, ty = meta.type;
15959
+ var nm = meta.ariaLabel || meta.labelText || meta.innerText || meta.placeholder || meta.title || meta.testId;
15960
+ if (t === 'a') return 'the "' + (nm || 'link') + '" link';
15961
+ if (t === 'button' || (t === 'input' && ty === 'submit')) return 'the "' + (nm || 'button') + '" button';
15962
+ if (t === 'input') {
15963
+ if (ty === 'checkbox') return 'the "' + (nm || 'checkbox') + '" checkbox';
15964
+ if (ty === 'radio') return 'the "' + (nm || 'radio button') + '" radio button';
15965
+ var lb = meta.labelText || meta.placeholder || nm;
15966
+ return 'the "' + (lb || 'input') + '" field';
15967
+ }
15968
+ if (t === 'textarea') return 'the "' + (meta.labelText || meta.placeholder || nm || 'text area') + '" field';
15969
+ if (t === 'select') return 'the "' + (meta.labelText || nm || 'dropdown') + '" dropdown';
15970
+ if (t === 'img') return 'the "' + (nm || 'image') + '" image';
15971
+ if (['h1','h2','h3','h4','h5','h6'].indexOf(t) >= 0) return 'the "' + (nm || 'heading') + '" heading';
15972
+ return 'the "' + (nm || t) + '" element';
15973
+ }
15974
+
15975
+ // Track the currently active input \u2014 only record its value when the user LEAVES the field
15976
+ var activeInput = null; // the DOM element being typed into
15977
+ var pendingInput = null; // the action payload to send
15978
+ function flushInput() {
15979
+ if (pendingInput && activeInput) {
15980
+ // Grab the FINAL value at flush time, not the value when input event fired
15981
+ var meta = getMeta(activeInput);
15982
+ var desc = descEl(meta);
15983
+ pendingInput.instruction = 'Enter "' + activeInput.value + '" in ' + desc;
15984
+ pendingInput.meta = meta;
15985
+ pendingInput.value = activeInput.value;
15986
+ safeSend(JSON.stringify(pendingInput));
15987
+ }
15988
+ pendingInput = null;
15989
+ activeInput = null;
15990
+ }
15991
+
15992
+ // Banner + styles (only in main frame \u2014 iframes don't need the banner)
15993
+ function ensureBanner() {
15994
+ if (window !== window.top) return;
15995
+ if (document.getElementById('__am-banner')) return;
15996
+ var s = document.createElement('style');
15997
+ s.textContent = '@keyframes __amp{0%,100%{opacity:1}50%{opacity:0.3}}.__am-ah{outline:3px solid #22c55e!important;outline-offset:2px;background:rgba(34,197,94,0.08)!important;}.__am-hh{outline:2px dashed #a78bfa!important;outline-offset:1px;}';
15998
+ document.head.appendChild(s);
15999
+ var b = document.createElement('div');
16000
+ b.id = '__am-banner';
16001
+ b.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:2147483647;background:linear-gradient(90deg,#1e1e2e,#2d1b3d);color:#fff;font:13px system-ui,sans-serif;padding:6px 16px;display:flex;align-items:center;box-shadow:0 2px 8px rgba(0,0,0,0.3);gap:12px;';
16002
+ b.innerHTML = '<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:8px;height:8px;border-radius:50%;background:#ef4444;animation:__amp 1.5s infinite"></span><b>Recording</b></span><span style="opacity:.65;font-size:12px">Shift+Click = assert &nbsp;|&nbsp; Ctrl+Shift+Click = soft assert &nbsp;|&nbsp; Ctrl+Shift+U = URL &nbsp;|&nbsp; Ctrl+Shift+T = title</span>';
16003
+ document.body.appendChild(b);
16004
+ }
16005
+ if (document.body) ensureBanner();
16006
+ else document.addEventListener('DOMContentLoaded', ensureBanner);
16007
+
16008
+ // Hover highlight
16009
+ var lastH = null;
16010
+ document.addEventListener('mouseover', function(e) {
16011
+ var el = e.target;
16012
+ if (!el || !el.tagName || !el.classList) return;
16013
+ if (el.id === '__am-banner' || el.closest('#__am-banner')) return;
16014
+ if (lastH && lastH !== el && lastH.classList) lastH.classList.remove('__am-hh');
16015
+ el.classList.add('__am-hh');
16016
+ lastH = el;
16017
+ }, true);
16018
+ document.addEventListener('mouseout', function(e) {
16019
+ if (e.target && e.target.classList) e.target.classList.remove('__am-hh');
16020
+ }, true);
16021
+
16022
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
16023
+ // EVENT LISTENERS \u2014 send raw metadata, Node.js resolves locators
16024
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
16025
+
16026
+ // Click (normal = action, Shift = assert)
16027
+ document.addEventListener('click', function(e) {
16028
+ flushInput();
16029
+ var el = e.target;
16030
+ if (!el || !el.tagName) return;
16031
+ if (el.id === '__am-banner' || el.closest('#__am-banner')) return;
16032
+
16033
+ var meta = getMeta(el);
16034
+ var desc = descEl(meta);
16035
+
16036
+ // Shift+Click = ASSERTION (Ctrl+Shift+Click = SOFT assertion)
16037
+ if (e.shiftKey) {
16038
+ e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
16039
+ var isSoft = e.ctrlKey || e.metaKey;
16040
+ var prefix = isSoft ? 'Soft verify ' : 'Verify ';
16041
+ if (el.classList) { el.classList.add('__am-ah'); setTimeout(function() { el.classList.remove('__am-ah'); }, 1200); }
16042
+
16043
+ var inst = '';
16044
+ if (meta.tag === 'input' || meta.tag === 'textarea') {
16045
+ inst = meta.value ? prefix + desc + ' has value "' + meta.value + '"' : prefix + desc + ' is visible';
16046
+ } else if (meta.type === 'checkbox' || meta.type === 'radio') {
16047
+ inst = prefix + desc + ' is ' + (meta.checked ? 'checked' : 'unchecked');
16048
+ } else {
16049
+ inst = (meta.innerText && meta.innerText.length <= 60) ? prefix + '"' + meta.innerText + '" text is visible' : prefix + desc + ' is visible';
16050
+ }
16051
+ safeSend(JSON.stringify({ action: 'assert', instruction: inst, meta: meta, url: location.href, soft: isSoft }));
16052
+ return;
16053
+ }
16054
+
16055
+ // Normal click
16056
+ if (meta.tag === 'input' && ['submit','button','checkbox','radio','file'].indexOf(meta.type) < 0) return;
16057
+ if (meta.tag === 'textarea' || meta.tag === 'option') return;
16058
+
16059
+ var action = 'click', instruction = 'Click ' + desc;
16060
+ if (meta.type === 'checkbox') { action = meta.checked ? 'check' : 'uncheck'; instruction = (meta.checked ? 'Check' : 'Uncheck') + ' ' + desc; }
16061
+ else if (meta.type === 'radio') { action = 'check'; instruction = 'Select ' + desc; }
16062
+ safeSend(JSON.stringify({ action: action, instruction: instruction, meta: meta, url: location.href }));
16063
+ }, true);
16064
+
16065
+ // Input \u2014 just mark this element as actively being typed into (NO timer, NO intermediate captures)
16066
+ document.addEventListener('input', function(e) {
16067
+ var el = e.target; if (!el || !el.tagName) return;
16068
+ if (el.tagName.toLowerCase() !== 'input' && el.tagName.toLowerCase() !== 'textarea') return;
16069
+ var ty = (el.getAttribute('type') || '').toLowerCase();
16070
+ if (['checkbox','radio','submit','button','file'].indexOf(ty) >= 0) return;
16071
+ // Just mark as active \u2014 we'll capture the FINAL value on blur/click/Enter
16072
+ activeInput = el;
16073
+ pendingInput = { action: 'fill', instruction: '', meta: null, value: '', url: location.href };
16074
+ }, true);
16075
+
16076
+ // Blur \u2014 user left the field \u2192 capture the FINAL value
16077
+ document.addEventListener('blur', function(e) {
16078
+ var el = e.target;
16079
+ if (el && el === activeInput && pendingInput) {
16080
+ flushInput();
16081
+ }
16082
+ }, true);
16083
+
16084
+ // Select change
16085
+ document.addEventListener('change', function(e) {
16086
+ var el = e.target; if (!el || el.tagName.toLowerCase() !== 'select') return;
16087
+ flushInput();
16088
+ var meta = getMeta(el), desc = descEl(meta);
16089
+ var selText = el.options[el.selectedIndex] ? el.options[el.selectedIndex].text : el.value;
16090
+ safeSend(JSON.stringify({ action: 'select', instruction: 'Select "' + selText + '" from ' + desc, meta: meta, value: el.value, url: location.href }));
16091
+ }, true);
16092
+
16093
+ // Keyboard
16094
+ document.addEventListener('keydown', function(e) {
16095
+ if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'u') {
16096
+ e.preventDefault(); e.stopPropagation(); flushInput();
16097
+ var b = document.getElementById('__am-banner');
16098
+ if (b) { b.style.background = 'linear-gradient(90deg,#064e3b,#065f46)'; setTimeout(function() { b.style.background = 'linear-gradient(90deg,#1e1e2e,#2d1b3d)'; }, 800); }
16099
+ var path = location.pathname + location.search + location.hash;
16100
+ safeSend(JSON.stringify({ action: 'assert', instruction: 'Verify the current URL contains "' + (path || '/') + '"', meta: null, value: location.href, url: location.href }));
16101
+ return;
16102
+ }
16103
+ if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 't') {
16104
+ e.preventDefault(); e.stopPropagation(); flushInput();
16105
+ var b2 = document.getElementById('__am-banner');
16106
+ if (b2) { b2.style.background = 'linear-gradient(90deg,#064e3b,#065f46)'; setTimeout(function() { b2.style.background = 'linear-gradient(90deg,#1e1e2e,#2d1b3d)'; }, 800); }
16107
+ safeSend(JSON.stringify({ action: 'assert', instruction: 'Verify the page title is "' + document.title + '"', meta: null, value: document.title, url: location.href }));
16108
+ return;
16109
+ }
16110
+ if (e.key === 'Enter') {
16111
+ flushInput();
16112
+ var el = e.target;
16113
+ if (el && el.tagName) {
16114
+ var t = el.tagName.toLowerCase();
16115
+ if (t === 'input' || t === 'textarea') {
16116
+ var meta = getMeta(el), desc = descEl(meta);
16117
+ safeSend(JSON.stringify({ action: 'press', instruction: 'Press Enter on ' + desc, meta: meta, value: 'Enter', url: location.href }));
16118
+ }
16119
+ }
16120
+ } else if (e.key === 'Escape') {
16121
+ flushInput();
16122
+ safeSend(JSON.stringify({ action: 'press', instruction: 'Press Escape', meta: null, value: 'Escape', url: location.href }));
16123
+ } else if (e.key === 'Tab') {
16124
+ flushInput();
16125
+ }
16126
+ }, true);
16127
+ })();
16128
+ `;
16129
+ }
16130
+ async function resolveLocator(page, meta) {
16131
+ const candidates = [];
16132
+ const inIframe = !!meta.frameSelector;
16133
+ const framePrefix = inIframe ? `frameLocator('${esc(meta.frameSelector)}').` : "";
16134
+ const base = inIframe ? page.frameLocator(meta.frameSelector) : page;
16135
+ try {
16136
+ const role = meta.ariaRole || getImplicitRole(meta.tag, meta.type);
16137
+ const bestName = meta.ariaLabel || meta.labelText || meta.innerText || meta.title || "";
16138
+ const headingLevel = getHeadingLevel(meta.tag);
16139
+ if (meta.testId) {
16140
+ candidates.push({
16141
+ name: "testid",
16142
+ locator: base.getByTestId(meta.testId),
16143
+ pw: `${framePrefix}getByTestId('${esc(meta.testId)}')`
16144
+ });
16145
+ }
16146
+ if (role && bestName && bestName.length <= 60) {
16147
+ if (headingLevel) {
16148
+ candidates.push({
16149
+ name: "role+name+level",
16150
+ locator: base.getByRole(role, { name: bestName, exact: true, level: headingLevel }),
16151
+ pw: `${framePrefix}getByRole('${role}', { name: '${esc(bestName)}', level: ${headingLevel} })`
16152
+ });
16153
+ }
16154
+ candidates.push({
16155
+ name: "role+name-exact",
16156
+ locator: base.getByRole(role, { name: bestName, exact: true }),
16157
+ pw: `${framePrefix}getByRole('${role}', { name: '${esc(bestName)}' })`
16158
+ });
16159
+ candidates.push({
16160
+ name: "role+name-fuzzy",
16161
+ locator: base.getByRole(role, { name: bestName, exact: false }),
16162
+ pw: `${framePrefix}getByRole('${role}', { name: '${esc(bestName)}' })`
16163
+ });
16164
+ }
16165
+ if (meta.labelText) {
16166
+ candidates.push({
16167
+ name: "label",
16168
+ locator: base.getByLabel(meta.labelText, { exact: true }),
16169
+ pw: `${framePrefix}getByLabel('${esc(meta.labelText)}')`
16170
+ });
16171
+ candidates.push({
16172
+ name: "label-fuzzy",
16173
+ locator: base.getByLabel(meta.labelText, { exact: false }),
16174
+ pw: `${framePrefix}getByLabel('${esc(meta.labelText)}')`
16175
+ });
16176
+ }
16177
+ if (meta.placeholder) {
16178
+ candidates.push({
16179
+ name: "placeholder",
16180
+ locator: base.getByPlaceholder(meta.placeholder, { exact: true }),
16181
+ pw: `${framePrefix}getByPlaceholder('${esc(meta.placeholder)}')`
16182
+ });
16183
+ }
16184
+ if (meta.innerText && meta.innerText.length <= 60) {
16185
+ candidates.push({
16186
+ name: "text-exact",
16187
+ locator: base.getByText(meta.innerText, { exact: true }),
16188
+ pw: `${framePrefix}getByText('${esc(meta.innerText)}')`
16189
+ });
16190
+ candidates.push({
16191
+ name: "text-fuzzy",
16192
+ locator: base.getByText(meta.innerText, { exact: false }),
16193
+ pw: `${framePrefix}getByText('${esc(meta.innerText)}')`
16194
+ });
16195
+ }
16196
+ if (meta.cssPath) {
16197
+ candidates.push({
16198
+ name: "css",
16199
+ locator: base.locator(meta.cssPath),
16200
+ pw: `${framePrefix}locator('${esc(meta.cssPath)}')`
16201
+ });
16202
+ }
16203
+ for (const c of candidates) {
16204
+ try {
16205
+ const count = await c.locator.count();
16206
+ if (count === 1) {
16207
+ logger44.debug({ strategy: c.name, pw: c.pw, inIframe }, "Locator resolved");
16208
+ return c.pw;
16209
+ }
16210
+ } catch {
16211
+ }
16212
+ }
16213
+ if (meta.cssPath) return `${framePrefix}locator('${esc(meta.cssPath)}')`;
16214
+ if (meta.id) return `${framePrefix}locator('#${esc(meta.id)}')`;
16215
+ return `${framePrefix}locator('${esc(meta.tag)}')`;
16216
+ } catch (err) {
16217
+ logger44.warn({ err, meta: meta.cssPath }, "Locator resolution failed \u2014 using CSS fallback");
16218
+ return meta.cssPath ? `${framePrefix}locator('${esc(meta.cssPath)}')` : `${framePrefix}locator('${esc(meta.tag)}')`;
16219
+ }
16220
+ }
16221
+ function getImplicitRole(tag, type) {
16222
+ if (tag === "button" || tag === "input" && type === "submit") return "button";
16223
+ if (tag === "a") return "link";
16224
+ if (tag === "input") {
16225
+ if (type === "checkbox") return "checkbox";
16226
+ if (type === "radio") return "radio";
16227
+ if (["text", "email", "password", "search", "tel", "url", "number", ""].includes(type)) return "textbox";
16228
+ }
16229
+ if (tag === "textarea") return "textbox";
16230
+ if (tag === "select") return "combobox";
16231
+ if (tag === "img") return "img";
16232
+ if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) return "heading";
16233
+ return null;
16234
+ }
16235
+ function getHeadingLevel(tag) {
16236
+ const match = tag.match(/^h([1-6])$/);
16237
+ return match ? parseInt(match[1], 10) : null;
16238
+ }
16239
+ function esc(s) {
16240
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
16241
+ }
16242
+ var import_playwright2, logger44, TestRecorder;
16243
+ var init_recorder = __esm({
16244
+ "src/engine/recorder.ts"() {
16245
+ "use strict";
16246
+ init_cjs_shims();
16247
+ import_playwright2 = require("playwright");
16248
+ init_logger();
16249
+ logger44 = createChildLogger("recorder");
16250
+ TestRecorder = class {
16251
+ browser = null;
16252
+ context = null;
16253
+ page = null;
16254
+ actions = [];
16255
+ actionCounter = 0;
16256
+ running = false;
16257
+ options;
16258
+ _setLastUserActionTime = null;
16259
+ constructor(options) {
16260
+ this.options = options;
16261
+ }
16262
+ get isRunning() {
16263
+ return this.running;
16264
+ }
16265
+ get recordedActions() {
16266
+ return [...this.actions];
16267
+ }
16268
+ async start() {
16269
+ if (this.running) throw new Error("Recorder is already running");
16270
+ logger44.info({ url: this.options.startUrl }, "Starting recorder");
16271
+ this.actions = [];
16272
+ this.actionCounter = 0;
16273
+ this.running = true;
16274
+ try {
16275
+ this.browser = await import_playwright2.chromium.launch({
16276
+ headless: false,
16277
+ args: ["--start-maximized"]
16278
+ });
16279
+ this.context = await this.browser.newContext({
16280
+ viewport: this.options.viewport || { width: 1280, height: 720 },
16281
+ ignoreHTTPSErrors: true
16282
+ });
16283
+ await this.context.exposeBinding("__amSend", (_source, raw) => {
16284
+ void this.handleRawAction(raw);
16285
+ });
16286
+ this.page = await this.context.newPage();
16287
+ this.page.on("pageerror", (err) => {
16288
+ logger44.warn({ error: err.message }, "Page error during recording");
16289
+ });
16290
+ const captureCode = buildCaptureScript();
16291
+ const injectIntoFrame = async (frame) => {
16292
+ try {
16293
+ await frame.evaluate(captureCode);
16294
+ logger44.debug({ url: frame.url(), name: frame.name() }, "Capture script injected into frame");
16295
+ } catch (err) {
16296
+ logger44.debug({ err, url: frame.url() }, "Could not inject into frame (likely cross-origin)");
16297
+ }
16298
+ };
16299
+ const injectAllFrames = async (p) => {
16300
+ await injectIntoFrame(p.mainFrame());
16301
+ for (const frame of p.frames()) {
16302
+ if (frame !== p.mainFrame()) {
16303
+ await injectIntoFrame(frame);
16304
+ }
16305
+ }
16306
+ };
16307
+ this.page.on("load", () => {
16308
+ if (!this.running || !this.page) return;
16309
+ setTimeout(() => injectAllFrames(this.page), 150);
16310
+ });
16311
+ this.page.on("frameattached", (frame) => {
16312
+ if (!this.running) return;
16313
+ frame.waitForLoadState("load").then(() => {
16314
+ injectIntoFrame(frame);
16315
+ }).catch(() => {
16316
+ });
16317
+ });
16318
+ this.page.on("framenavigated", (frame) => {
16319
+ if (!this.running || !this.page) return;
16320
+ if (frame === this.page.mainFrame()) return;
16321
+ setTimeout(() => injectIntoFrame(frame), 150);
16322
+ });
16323
+ let lastUrl = "";
16324
+ let lastUserActionTime = 0;
16325
+ const REDIRECT_WINDOW_MS = 3e3;
16326
+ this.page.on("framenavigated", (frame) => {
16327
+ if (!this.running || !this.page) return;
16328
+ if (frame !== this.page.mainFrame()) return;
16329
+ const url = frame.url();
16330
+ if (url === lastUrl || url === "about:blank") return;
16331
+ lastUrl = url;
16332
+ const isInitialNav = this.actionCounter === 0;
16333
+ if (!isInitialNav && lastUserActionTime > 0) {
16334
+ const elapsed = Date.now() - lastUserActionTime;
16335
+ if (elapsed < REDIRECT_WINDOW_MS) {
16336
+ logger44.debug({ url, elapsed }, "Skipping redirect navigation after user action");
16337
+ return;
16338
+ }
16339
+ }
16340
+ const action = {
16341
+ id: `rec-${++this.actionCounter}`,
16342
+ order: this.actionCounter,
16343
+ action: "navigate",
16344
+ instruction: isInitialNav ? `Go to ${url}` : `Navigate to ${url}`,
16345
+ locator: "",
16346
+ url,
16347
+ timestamp: Date.now()
16348
+ };
16349
+ this.actions.push(action);
16350
+ this.options.onAction(action);
16351
+ logger44.debug({ action: action.instruction }, "Recorded navigation");
16352
+ });
16353
+ this._setLastUserActionTime = (t) => {
16354
+ lastUserActionTime = t;
16355
+ };
16356
+ this.page.on("close", () => {
16357
+ if (this.running) {
16358
+ logger44.info("Browser closed by user \u2014 stopping recorder");
16359
+ this.running = false;
16360
+ this.options.onStop?.(this.actions);
16361
+ }
16362
+ });
16363
+ await this.page.goto(this.options.startUrl, { waitUntil: "load", timeout: 3e4 });
16364
+ await injectAllFrames(this.page);
16365
+ logger44.info("Recorder started \u2014 browser is open for interaction");
16366
+ } catch (err) {
16367
+ logger44.error({ err }, "Failed to start recorder");
16368
+ await this.cleanup();
16369
+ throw err;
16370
+ }
16371
+ }
16372
+ async stop() {
16373
+ if (!this.running) return this.actions;
16374
+ logger44.info({ actionCount: this.actions.length }, "Stopping recorder");
16375
+ this.running = false;
16376
+ const result = [...this.actions];
16377
+ await this.cleanup();
16378
+ this.options.onStop?.(result);
16379
+ return result;
16380
+ }
16381
+ /**
16382
+ * Handle a raw action from the browser capture script.
16383
+ * Resolves the best Playwright locator using the live page's DOM + Accessibility tree.
16384
+ */
16385
+ async handleRawAction(raw) {
16386
+ if (!this.running || !this.page) return;
16387
+ try {
16388
+ const parsed = JSON.parse(raw);
16389
+ const meta = parsed.meta;
16390
+ let locator = "";
16391
+ if (meta && this.page) {
16392
+ try {
16393
+ locator = await resolveLocator(this.page, meta);
16394
+ } catch (err) {
16395
+ logger44.warn({ err }, "Locator resolution failed \u2014 using CSS fallback");
16396
+ locator = meta.cssPath ? `locator('${esc(meta.cssPath)}')` : "";
16397
+ }
16398
+ }
16399
+ const userActions = ["click", "fill", "check", "uncheck", "select", "press"];
16400
+ if (userActions.includes(parsed.action) && this._setLastUserActionTime) {
16401
+ this._setLastUserActionTime(Date.now());
16402
+ }
16403
+ const action = {
16404
+ id: `rec-${++this.actionCounter}`,
16405
+ order: this.actionCounter,
16406
+ action: parsed.action,
16407
+ instruction: parsed.instruction,
16408
+ locator,
16409
+ value: parsed.value,
16410
+ url: parsed.url,
16411
+ timestamp: Date.now(),
16412
+ ...parsed.soft ? { soft: true } : {},
16413
+ ...meta?.frameSelector ? { frameSelector: meta.frameSelector } : {}
16414
+ };
16415
+ this.actions.push(action);
16416
+ this.options.onAction(action);
16417
+ logger44.debug({ action: action.instruction, locator, strategy: "resolved" }, "Recorded action");
16418
+ } catch (err) {
16419
+ logger44.warn({ err, raw }, "Failed to parse recorded action");
16420
+ }
16421
+ }
16422
+ async cleanup() {
16423
+ try {
16424
+ if (this.context) await this.context.close().catch(() => {
16425
+ });
16426
+ if (this.browser) await this.browser.close().catch(() => {
16427
+ });
16428
+ } catch {
16429
+ }
16430
+ this.browser = null;
16431
+ this.context = null;
16432
+ this.page = null;
16433
+ }
16434
+ };
16435
+ }
16436
+ });
16437
+
16438
+ // src/server/routes/recorder.ts
16439
+ async function recorderRoutes(fastify) {
16440
+ const rootDir = fastify.rootDir;
16441
+ const wsManager = fastify.wsManager;
16442
+ fastify.post("/recorder/start", async (req, reply) => {
16443
+ try {
16444
+ if (activeRecorder?.isRunning) {
16445
+ return reply.status(409).send({ error: "A recording session is already active. Stop it first." });
16446
+ }
16447
+ const body = StartBody.parse(req.body || {});
16448
+ const config = await readConfig(rootDir);
16449
+ const startUrl = body.url || config.baseUrl || "https://www.google.com";
16450
+ activeRecorder = new TestRecorder({
16451
+ startUrl,
16452
+ viewport: body.viewport,
16453
+ onAction: (action) => {
16454
+ wsManager.broadcast("recorder:action", action);
16455
+ },
16456
+ onStop: (actions) => {
16457
+ wsManager.broadcast("recorder:stopped", { actionCount: actions.length });
16458
+ logger45.info({ actionCount: actions.length }, "Recording stopped (browser closed)");
16459
+ }
16460
+ });
16461
+ await activeRecorder.start();
16462
+ return reply.send({
16463
+ status: "recording",
16464
+ url: startUrl,
16465
+ message: "Browser launched \u2014 interact with your app. Actions are streamed in real time."
16466
+ });
16467
+ } catch (err) {
16468
+ activeRecorder = null;
16469
+ return sendError(reply, 500, "Failed to start recorder", err);
16470
+ }
16471
+ });
16472
+ fastify.post("/recorder/stop", async (_req, reply) => {
16473
+ try {
16474
+ if (!activeRecorder?.isRunning) {
16475
+ return reply.status(404).send({ error: "No active recording session." });
16476
+ }
16477
+ const actions = await activeRecorder.stop();
16478
+ activeRecorder = null;
16479
+ wsManager.broadcast("recorder:stopped", { actionCount: actions.length });
16480
+ return reply.send({
16481
+ status: "stopped",
16482
+ actions,
16483
+ actionCount: actions.length
16484
+ });
16485
+ } catch (err) {
16486
+ activeRecorder = null;
16487
+ return sendError(reply, 500, "Failed to stop recorder", err);
16488
+ }
16489
+ });
16490
+ fastify.get("/recorder/status", async (_req, reply) => {
16491
+ const isRecording = activeRecorder?.isRunning ?? false;
16492
+ const actionCount = activeRecorder?.recordedActions.length ?? 0;
16493
+ return reply.send({
16494
+ recording: isRecording,
16495
+ actionCount,
16496
+ actions: isRecording ? activeRecorder.recordedActions : []
16497
+ });
16498
+ });
16499
+ }
16500
+ var import_zod22, logger45, StartBody, activeRecorder;
16501
+ var init_recorder2 = __esm({
16502
+ "src/server/routes/recorder.ts"() {
16503
+ "use strict";
16504
+ init_cjs_shims();
16505
+ import_zod22 = require("zod");
16506
+ init_recorder();
16507
+ init_config_store();
16508
+ init_utils2();
16509
+ init_logger();
16510
+ logger45 = createChildLogger("recorder-routes");
16511
+ StartBody = import_zod22.z.object({
16512
+ url: import_zod22.z.string().url().optional(),
16513
+ viewport: import_zod22.z.object({
16514
+ width: import_zod22.z.number().int().positive(),
16515
+ height: import_zod22.z.number().int().positive()
16516
+ }).optional()
16517
+ });
16518
+ activeRecorder = null;
16519
+ }
16520
+ });
16521
+
14879
16522
  // src/server/index.ts
14880
16523
  var server_exports = {};
14881
16524
  __export(server_exports, {
@@ -14917,17 +16560,18 @@ async function startServer(options) {
14917
16560
  await fastify.register(templatesRoutes, { prefix: "/api" });
14918
16561
  await fastify.register(flakinessRoutes, { prefix: "/api" });
14919
16562
  await fastify.register(stepLibraryRoutes, { prefix: "/api" });
16563
+ await fastify.register(recorderRoutes, { prefix: "/api" });
14920
16564
  await registerStatic(fastify, rootDir);
14921
16565
  await fastify.listen({ port, host: "127.0.0.1" });
14922
- logger43.info({ port, rootDir }, `Assuremind Studio listening on http://127.0.0.1:${port}`);
16566
+ logger46.info({ port, rootDir }, `Assuremind Studio listening on http://127.0.0.1:${port}`);
14923
16567
  return {
14924
16568
  async close() {
14925
16569
  await fastify.close();
14926
- logger43.info("Server closed");
16570
+ logger46.info("Server closed");
14927
16571
  }
14928
16572
  };
14929
16573
  }
14930
- var import_fastify, import_cors, import_websocket, logger43;
16574
+ var import_fastify, import_cors, import_websocket, logger46;
14931
16575
  var init_server = __esm({
14932
16576
  "src/server/index.ts"() {
14933
16577
  "use strict";
@@ -14957,8 +16601,9 @@ var init_server = __esm({
14957
16601
  init_templates();
14958
16602
  init_flakiness();
14959
16603
  init_step_library();
16604
+ init_recorder2();
14960
16605
  init_logger();
14961
- logger43 = createChildLogger("server");
16606
+ logger46 = createChildLogger("server");
14962
16607
  }
14963
16608
  });
14964
16609
 
@@ -15261,14 +16906,14 @@ async function loadStoryText(options) {
15261
16906
  return options.story;
15262
16907
  }
15263
16908
  if (options.storyFile) {
15264
- const filePath = import_path35.default.resolve(options.storyFile);
15265
- if (!await import_fs_extra24.default.pathExists(filePath)) {
16909
+ const filePath = import_path36.default.resolve(options.storyFile);
16910
+ if (!await import_fs_extra27.default.pathExists(filePath)) {
15266
16911
  throw new ConfigError(
15267
16912
  `Story file not found: "${filePath}"`,
15268
16913
  "STORY_FILE_NOT_FOUND"
15269
16914
  );
15270
16915
  }
15271
- return import_fs_extra24.default.readFile(filePath, "utf8");
16916
+ return import_fs_extra27.default.readFile(filePath, "utf8");
15272
16917
  }
15273
16918
  throw new ConfigError(
15274
16919
  'Provide a story with --story "<text>" or --story-file <path>',
@@ -15291,7 +16936,7 @@ async function runGenerate(options) {
15291
16936
  process.exit(1);
15292
16937
  }
15293
16938
  const rootDir = process.cwd();
15294
- const outputDir = options.output ? import_path35.default.resolve(options.output) : import_path35.default.join(rootDir, "tests");
16939
+ const outputDir = options.output ? import_path36.default.resolve(options.output) : import_path36.default.join(rootDir, "tests");
15295
16940
  printInfo(`Story: ${colors.dim(`${storyText.slice(0, 80)}${storyText.length > 80 ? "\u2026" : ""}`)}`);
15296
16941
  if (options.suite) {
15297
16942
  printInfo(`Suite name: ${options.suite}`);
@@ -15323,8 +16968,8 @@ async function runGenerate(options) {
15323
16968
  suiteName: options.suite
15324
16969
  });
15325
16970
  genSpinner.succeed("Suite generated");
15326
- const suiteOutputDir = options.output ?? import_path35.default.join(rootDir, "tests");
15327
- printSuccess(`Test suite created in: ${import_path35.default.relative(rootDir, suiteOutputDir)}`);
16971
+ const suiteOutputDir = options.output ?? import_path36.default.join(rootDir, "tests");
16972
+ printSuccess(`Test suite created in: ${import_path36.default.relative(rootDir, suiteOutputDir)}`);
15328
16973
  printInfo("Open Studio or use the Test Editor to review and generate Playwright code:");
15329
16974
  process.stdout.write(` ${colors.accent("npx assuremind studio")}
15330
16975
 
@@ -15335,13 +16980,13 @@ async function runGenerate(options) {
15335
16980
  process.exit(1);
15336
16981
  }
15337
16982
  }
15338
- var import_path35, import_fs_extra24;
16983
+ var import_path36, import_fs_extra27;
15339
16984
  var init_generate = __esm({
15340
16985
  "src/cli/generate.ts"() {
15341
16986
  "use strict";
15342
16987
  init_cjs_shims();
15343
- import_path35 = __toESM(require("path"));
15344
- import_fs_extra24 = __toESM(require("fs-extra"));
16988
+ import_path36 = __toESM(require("path"));
16989
+ import_fs_extra27 = __toESM(require("fs-extra"));
15345
16990
  init_ui();
15346
16991
  init_env();
15347
16992
  init_errors();
@@ -15355,7 +17000,7 @@ __export(validate_exports, {
15355
17000
  });
15356
17001
  async function checkConfig(rootDir, result) {
15357
17002
  printInfo("Checking configuration\u2026");
15358
- if (!await import_fs_extra25.default.pathExists(import_path36.default.join(rootDir, "autotest.config.json")) && !await import_fs_extra25.default.pathExists(import_path36.default.join(rootDir, "autotest.config.ts"))) {
17003
+ if (!await import_fs_extra28.default.pathExists(import_path37.default.join(rootDir, "autotest.config.json")) && !await import_fs_extra28.default.pathExists(import_path37.default.join(rootDir, "autotest.config.ts"))) {
15359
17004
  printError(' autotest.config.ts not found \u2014 run "npx assuremind init" to create it');
15360
17005
  result.errors++;
15361
17006
  return;
@@ -15371,7 +17016,7 @@ async function checkConfig(rootDir, result) {
15371
17016
  }
15372
17017
  async function checkEnv(result) {
15373
17018
  printInfo("Checking environment\u2026");
15374
- if (!await import_fs_extra25.default.pathExists(".env")) {
17019
+ if (!await import_fs_extra28.default.pathExists(".env")) {
15375
17020
  printWarn(" .env file not found \u2014 copy .env.example to .env and fill in your API key");
15376
17021
  result.warnings++;
15377
17022
  return;
@@ -15387,8 +17032,8 @@ async function checkEnv(result) {
15387
17032
  }
15388
17033
  async function checkTests(rootDir, result) {
15389
17034
  printInfo("Scanning test files\u2026");
15390
- const testsDir = import_path36.default.join(rootDir, "tests");
15391
- if (!await import_fs_extra25.default.pathExists(testsDir)) {
17035
+ const testsDir = import_path37.default.join(rootDir, "tests");
17036
+ if (!await import_fs_extra28.default.pathExists(testsDir)) {
15392
17037
  printWarn(" tests/ directory not found \u2014 no tests to validate");
15393
17038
  result.warnings++;
15394
17039
  return;
@@ -15408,7 +17053,7 @@ async function checkTests(rootDir, result) {
15408
17053
  try {
15409
17054
  await readCase(casePath);
15410
17055
  } catch {
15411
- printError(` Invalid: ${import_path36.default.relative(rootDir, casePath)}`);
17056
+ printError(` Invalid: ${import_path37.default.relative(rootDir, casePath)}`);
15412
17057
  invalidCases++;
15413
17058
  result.errors++;
15414
17059
  }
@@ -15436,7 +17081,7 @@ async function checkVariables(rootDir, result) {
15436
17081
  try {
15437
17082
  await readVariables(file);
15438
17083
  } catch {
15439
- printError(` Invalid: ${import_path36.default.relative(rootDir, file)}`);
17084
+ printError(` Invalid: ${import_path37.default.relative(rootDir, file)}`);
15440
17085
  invalid++;
15441
17086
  result.errors++;
15442
17087
  }
@@ -15473,13 +17118,13 @@ async function runValidate() {
15473
17118
  process.exit(1);
15474
17119
  }
15475
17120
  }
15476
- var import_path36, import_fs_extra25;
17121
+ var import_path37, import_fs_extra28;
15477
17122
  var init_validate = __esm({
15478
17123
  "src/cli/validate.ts"() {
15479
17124
  "use strict";
15480
17125
  init_cjs_shims();
15481
- import_path36 = __toESM(require("path"));
15482
- import_fs_extra25 = __toESM(require("fs-extra"));
17126
+ import_path37 = __toESM(require("path"));
17127
+ import_fs_extra28 = __toESM(require("fs-extra"));
15483
17128
  init_ui();
15484
17129
  init_config_store();
15485
17130
  init_suite_store();
@@ -15544,7 +17189,7 @@ async function checkPlaywright() {
15544
17189
  }
15545
17190
  }
15546
17191
  async function checkEnvFile() {
15547
- if (!await import_fs_extra26.default.pathExists(".env")) {
17192
+ if (!await import_fs_extra29.default.pathExists(".env")) {
15548
17193
  return check(
15549
17194
  ".env file",
15550
17195
  false,
@@ -15582,7 +17227,7 @@ async function checkConfig2() {
15582
17227
  }
15583
17228
  }
15584
17229
  async function checkPackageJson() {
15585
- if (!await import_fs_extra26.default.pathExists("package.json")) {
17230
+ if (!await import_fs_extra29.default.pathExists("package.json")) {
15586
17231
  return check(
15587
17232
  "package.json",
15588
17233
  false,
@@ -15590,7 +17235,7 @@ async function checkPackageJson() {
15590
17235
  );
15591
17236
  }
15592
17237
  try {
15593
- const pkg2 = await import_fs_extra26.default.readJson("package.json");
17238
+ const pkg2 = await import_fs_extra29.default.readJson("package.json");
15594
17239
  return check("package.json", true, `${pkg2.name ?? "unnamed"} v${pkg2.version ?? "?"}`);
15595
17240
  } catch {
15596
17241
  return check("package.json", false, "Exists but is not valid JSON");
@@ -15670,13 +17315,13 @@ async function runDoctor() {
15670
17315
  process.exit(1);
15671
17316
  }
15672
17317
  }
15673
- var import_child_process6, import_fs_extra26;
17318
+ var import_child_process6, import_fs_extra29;
15674
17319
  var init_doctor = __esm({
15675
17320
  "src/cli/doctor.ts"() {
15676
17321
  "use strict";
15677
17322
  init_cjs_shims();
15678
17323
  import_child_process6 = require("child_process");
15679
- import_fs_extra26 = __toESM(require("fs-extra"));
17324
+ import_fs_extra29 = __toESM(require("fs-extra"));
15680
17325
  init_ui();
15681
17326
  init_env();
15682
17327
  init_config_store();
@@ -15703,7 +17348,7 @@ async function findCaseFilePath(testsDir, caseId) {
15703
17348
  return null;
15704
17349
  }
15705
17350
  async function applyHealToFile(rootDir, event) {
15706
- const testsDir = import_path37.default.join(rootDir, "tests");
17351
+ const testsDir = import_path38.default.join(rootDir, "tests");
15707
17352
  const casePath = await findCaseFilePath(testsDir, event.caseId);
15708
17353
  if (!casePath) {
15709
17354
  throw new Error(
@@ -15725,7 +17370,7 @@ async function applyHealToFile(rootDir, event) {
15725
17370
  testCase.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
15726
17371
  await writeCase(casePath, testCase);
15727
17372
  printSuccess(
15728
- ` Updated: ${import_path37.default.relative(rootDir, casePath)} \u203A step "${testCase.steps[stepIdx].instruction}"`
17373
+ ` Updated: ${import_path38.default.relative(rootDir, casePath)} \u203A step "${testCase.steps[stepIdx].instruction}"`
15729
17374
  );
15730
17375
  }
15731
17376
  function ask(rl, question) {
@@ -15867,21 +17512,21 @@ Review the changes and merge if the fixes look correct.
15867
17512
  }
15868
17513
  async function runApplyHealing(options) {
15869
17514
  const rootDir = process.cwd();
15870
- (0, import_dotenv2.config)({ path: import_path37.default.join(rootDir, ".env") });
17515
+ (0, import_dotenv2.config)({ path: import_path38.default.join(rootDir, ".env") });
15871
17516
  process.stdout.write("\n");
15872
17517
  let events;
15873
17518
  if (options.from) {
15874
- const reportPath = import_path37.default.resolve(options.from);
15875
- if (!await import_fs_extra27.default.pathExists(reportPath)) {
17519
+ const reportPath = import_path38.default.resolve(options.from);
17520
+ if (!await import_fs_extra30.default.pathExists(reportPath)) {
15876
17521
  printError(`Healing report not found: ${reportPath}`);
15877
17522
  process.exit(1);
15878
17523
  }
15879
17524
  try {
15880
- const rawRunId = import_path37.default.basename(reportPath, ".json").replace("healing-report-", "");
17525
+ const rawRunId = import_path38.default.basename(reportPath, ".json").replace("healing-report-", "");
15881
17526
  const report = await readHealingReport(rootDir, rawRunId);
15882
17527
  events = report.events.filter((e) => e.status === "pending");
15883
17528
  } catch {
15884
- const raw = await import_fs_extra27.default.readJson(reportPath);
17529
+ const raw = await import_fs_extra30.default.readJson(reportPath);
15885
17530
  events = Array.isArray(raw) ? raw.filter((e) => e.status === "pending") : (raw.events ?? []).filter((e) => e.status === "pending");
15886
17531
  }
15887
17532
  } else {
@@ -15910,13 +17555,13 @@ async function runApplyHealing(options) {
15910
17555
  }
15911
17556
  }
15912
17557
  }
15913
- var import_path37, import_fs_extra27, import_readline, import_dotenv2;
17558
+ var import_path38, import_fs_extra30, import_readline, import_dotenv2;
15914
17559
  var init_apply_healing = __esm({
15915
17560
  "src/cli/apply-healing.ts"() {
15916
17561
  "use strict";
15917
17562
  init_cjs_shims();
15918
- import_path37 = __toESM(require("path"));
15919
- import_fs_extra27 = __toESM(require("fs-extra"));
17563
+ import_path38 = __toESM(require("path"));
17564
+ import_fs_extra30 = __toESM(require("fs-extra"));
15920
17565
  import_readline = __toESM(require("readline"));
15921
17566
  import_dotenv2 = require("dotenv");
15922
17567
  init_healing_store();