@yasserkhanorg/e2e-agents 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/agent/feedback.d.ts +16 -0
  2. package/dist/agent/feedback.d.ts.map +1 -1
  3. package/dist/agent/feedback.js +62 -0
  4. package/dist/agent/process_runner.d.ts +1 -1
  5. package/dist/agent/process_runner.d.ts.map +1 -1
  6. package/dist/agent/process_runner.js +3 -3
  7. package/dist/api.d.ts.map +1 -1
  8. package/dist/api.js +5 -2
  9. package/dist/cli/commands/train.d.ts.map +1 -1
  10. package/dist/cli/commands/train.js +31 -4
  11. package/dist/cli/parse_args.d.ts.map +1 -1
  12. package/dist/cli/parse_args.js +1 -0
  13. package/dist/cli/types.d.ts +1 -0
  14. package/dist/cli/types.d.ts.map +1 -1
  15. package/dist/engine/plan_builder.d.ts +2 -1
  16. package/dist/engine/plan_builder.d.ts.map +1 -1
  17. package/dist/engine/plan_builder.js +22 -9
  18. package/dist/esm/agent/feedback.js +61 -0
  19. package/dist/esm/agent/process_runner.js +3 -3
  20. package/dist/esm/api.js +5 -2
  21. package/dist/esm/cli/commands/train.js +31 -4
  22. package/dist/esm/cli/parse_args.js +1 -0
  23. package/dist/esm/engine/plan_builder.js +22 -9
  24. package/dist/esm/index.js +1 -1
  25. package/dist/esm/pipeline/spec_verifier.js +75 -0
  26. package/dist/esm/pipeline/stage3_generation.js +122 -4
  27. package/dist/esm/pipeline/stage4_heal.js +146 -3
  28. package/dist/esm/prompts/heal.js +4 -0
  29. package/dist/esm/qa-agent/phase2/agent_loop.js +60 -24
  30. package/dist/esm/qa-agent/phase2/exploration_state.js +21 -0
  31. package/dist/esm/qa-agent/phase2/tools.js +99 -1
  32. package/dist/esm/qa-agent/phase3/reporter.js +31 -4
  33. package/dist/esm/training/enricher.js +71 -7
  34. package/dist/esm/training/merger.js +77 -10
  35. package/dist/esm/training/scanner.js +368 -2
  36. package/dist/esm/validation/guardrails.js +1 -0
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +3 -2
  40. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  41. package/dist/pipeline/spec_verifier.d.ts +20 -0
  42. package/dist/pipeline/spec_verifier.d.ts.map +1 -0
  43. package/dist/pipeline/spec_verifier.js +79 -0
  44. package/dist/pipeline/stage3_generation.d.ts +10 -0
  45. package/dist/pipeline/stage3_generation.d.ts.map +1 -1
  46. package/dist/pipeline/stage3_generation.js +120 -2
  47. package/dist/pipeline/stage4_heal.d.ts +4 -0
  48. package/dist/pipeline/stage4_heal.d.ts.map +1 -1
  49. package/dist/pipeline/stage4_heal.js +145 -2
  50. package/dist/prompts/heal.d.ts +2 -0
  51. package/dist/prompts/heal.d.ts.map +1 -1
  52. package/dist/prompts/heal.js +4 -0
  53. package/dist/qa-agent/phase2/agent_loop.d.ts.map +1 -1
  54. package/dist/qa-agent/phase2/agent_loop.js +60 -24
  55. package/dist/qa-agent/phase2/exploration_state.d.ts.map +1 -1
  56. package/dist/qa-agent/phase2/exploration_state.js +21 -0
  57. package/dist/qa-agent/phase2/tools.d.ts.map +1 -1
  58. package/dist/qa-agent/phase2/tools.js +99 -1
  59. package/dist/qa-agent/phase3/reporter.js +31 -4
  60. package/dist/qa-agent/types.d.ts +9 -1
  61. package/dist/qa-agent/types.d.ts.map +1 -1
  62. package/dist/training/enricher.d.ts +3 -1
  63. package/dist/training/enricher.d.ts.map +1 -1
  64. package/dist/training/enricher.js +71 -7
  65. package/dist/training/merger.d.ts +11 -1
  66. package/dist/training/merger.d.ts.map +1 -1
  67. package/dist/training/merger.js +77 -10
  68. package/dist/training/scanner.d.ts +15 -2
  69. package/dist/training/scanner.d.ts.map +1 -1
  70. package/dist/training/scanner.js +370 -2
  71. package/dist/training/types.d.ts +4 -0
  72. package/dist/training/types.d.ts.map +1 -1
  73. package/dist/validation/guardrails.d.ts +2 -0
  74. package/dist/validation/guardrails.d.ts.map +1 -1
  75. package/dist/validation/guardrails.js +4 -1
  76. package/package.json +1 -1
@@ -98,7 +98,7 @@ exports.TOOL_DEFINITIONS = [
98
98
  },
99
99
  {
100
100
  name: 'report_finding',
101
- description: 'Report a bug, visual issue, UX problem, or gap you discovered. Always include current URL and repro steps.',
101
+ description: 'Report a bug, visual issue, UX problem, or gap you discovered. Include expected/actual behavior and repro steps. Take before/after screenshots before calling this.',
102
102
  input_schema: {
103
103
  type: 'object',
104
104
  properties: {
@@ -110,6 +110,13 @@ exports.TOOL_DEFINITIONS = [
110
110
  items: { type: 'string' },
111
111
  description: 'Steps to reproduce',
112
112
  },
113
+ screenshot_refs: {
114
+ type: 'array',
115
+ items: { type: 'string' },
116
+ description: 'Paths to before/after screenshots (from take_screenshot)',
117
+ },
118
+ expected_behavior: { type: 'string', description: 'What should have happened' },
119
+ actual_behavior: { type: 'string', description: 'What actually happened' },
113
120
  },
114
121
  required: ['type', 'severity', 'summary', 'repro_steps'],
115
122
  },
@@ -137,6 +144,23 @@ exports.TOOL_DEFINITIONS = [
137
144
  required: ['role'],
138
145
  },
139
146
  },
147
+ {
148
+ name: 'wait_for',
149
+ description: 'Wait for an element condition or page state. Use after actions that trigger async changes (navigation, API calls, animations).',
150
+ input_schema: {
151
+ type: 'object',
152
+ properties: {
153
+ condition: {
154
+ type: 'string',
155
+ enum: ['visible', 'hidden', 'stable', 'networkidle'],
156
+ description: 'What to wait for: visible/hidden (element state), stable (no DOM changes for 1s), networkidle (no pending requests)',
157
+ },
158
+ ref: { type: 'string', description: 'Accessibility ref for element conditions (visible/hidden). Not needed for stable/networkidle.' },
159
+ timeout_ms: { type: 'number', description: 'Max wait time in ms (default 5000, max 15000)' },
160
+ },
161
+ required: ['condition'],
162
+ },
163
+ },
140
164
  ];
141
165
  function executeTool(ctx, name, input) {
142
166
  switch (name) {
@@ -208,6 +232,36 @@ function executeTool(ctx, name, input) {
208
232
  if (!Array.isArray(input.repro_steps)) {
209
233
  return { output: `Invalid repro_steps: expected an array of strings.` };
210
234
  }
235
+ // Auto-capture console errors at time of finding
236
+ let autoConsoleErrors;
237
+ try {
238
+ const raw = ctx.browser.evaluateInternal('JSON.stringify(window.__consoleErrors || [])');
239
+ const parsed = JSON.parse(raw);
240
+ if (Array.isArray(parsed) && parsed.length > 0) {
241
+ autoConsoleErrors = parsed.map(String).slice(-10);
242
+ }
243
+ }
244
+ catch {
245
+ // Console error capture not available
246
+ }
247
+ // Auto-take screenshot if none provided
248
+ let autoScreenshot;
249
+ const screenshotRefs = Array.isArray(input.screenshot_refs)
250
+ ? input.screenshot_refs.map(String)
251
+ : undefined;
252
+ if (!screenshotRefs || screenshotRefs.length === 0) {
253
+ try {
254
+ const nextCount = ctx.screenshotCounter + 1;
255
+ const filename = `${String(nextCount).padStart(3, '0')}-finding-auto.png`;
256
+ const screenshotPath = `${ctx.screenshotDir}/${filename}`;
257
+ ctx.browser.screenshot(screenshotPath);
258
+ ctx.screenshotCounter = nextCount;
259
+ autoScreenshot = screenshotPath;
260
+ }
261
+ catch {
262
+ autoScreenshot = undefined;
263
+ }
264
+ }
211
265
  const finding = {
212
266
  id: `f-${crypto.randomUUID()}`,
213
267
  type: rawType,
@@ -217,6 +271,11 @@ function executeTool(ctx, name, input) {
217
271
  evidence: {
218
272
  url: ctx.currentUrl,
219
273
  reproSteps: input.repro_steps.map(String),
274
+ screenshotRefs: screenshotRefs || (autoScreenshot ? [autoScreenshot] : undefined),
275
+ screenshotPath: autoScreenshot || (screenshotRefs ? screenshotRefs[0] : undefined),
276
+ consoleErrors: autoConsoleErrors,
277
+ expectedBehavior: input.expected_behavior ? String(input.expected_behavior) : undefined,
278
+ actualBehavior: input.actual_behavior ? String(input.actual_behavior) : undefined,
220
279
  },
221
280
  timestamp: Date.now(),
222
281
  };
@@ -233,6 +292,45 @@ function executeTool(ctx, name, input) {
233
292
  flowDone: { flowId, status: rawStatus },
234
293
  };
235
294
  }
295
+ case 'wait_for': {
296
+ const condition = String(input.condition || '');
297
+ const VALID_CONDITIONS = new Set(['visible', 'hidden', 'stable', 'networkidle']);
298
+ if (!VALID_CONDITIONS.has(condition)) {
299
+ return { output: `Invalid condition "${condition}". Must be one of: ${[...VALID_CONDITIONS].join(', ')}.` };
300
+ }
301
+ const timeoutMs = Math.min(Math.max(Number(input.timeout_ms) || 5000, 500), 15000);
302
+ try {
303
+ if (condition === 'stable' || condition === 'networkidle') {
304
+ const waitMs = condition === 'networkidle' ? Math.min(timeoutMs, 3000) : 1000;
305
+ ctx.browser.evaluateInternal(`new Promise(r => setTimeout(r, ${waitMs}))`);
306
+ return { output: `Waited ${waitMs}ms for ${condition} (heuristic delay)` };
307
+ }
308
+ // Element-level wait: poll snapshot for ref presence/absence
309
+ const ref = input.ref ? String(input.ref) : undefined;
310
+ if (!ref) {
311
+ return { output: `Element condition "${condition}" requires a ref parameter.` };
312
+ }
313
+ const start = Date.now();
314
+ const wantVisible = condition === 'visible';
315
+ // Use word-boundary regex to avoid false positives (@e1 matching @e10)
316
+ const refPattern = new RegExp(`(?<![\\w@])${ref.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w])`);
317
+ const pollIntervalMs = 300;
318
+ while (Date.now() - start < timeoutMs) {
319
+ const snap = ctx.browser.snapshot();
320
+ const found = refPattern.test(snap);
321
+ if ((wantVisible && found) || (!wantVisible && !found)) {
322
+ return { output: `Element ${ref} is now ${condition} (took ${Date.now() - start}ms)` };
323
+ }
324
+ // Synchronous in-process sleep via Atomics.wait (available in Node.js 8.10+)
325
+ const buf = new SharedArrayBuffer(4);
326
+ Atomics.wait(new Int32Array(buf), 0, 0, pollIntervalMs);
327
+ }
328
+ return { output: `Timeout: element ${ref} did not become ${condition} within ${timeoutMs}ms` };
329
+ }
330
+ catch (err) {
331
+ return { output: `wait_for error: ${String(err)}` };
332
+ }
333
+ }
236
334
  case 'switch_user': {
237
335
  const role = String(input.role);
238
336
  const user = ctx.users?.find((u) => u.role === role);
@@ -83,16 +83,43 @@ function renderMarkdown(report) {
83
83
  if (report.phase2.findings.length > 0) {
84
84
  lines.push(`## Findings`, '');
85
85
  for (const f of report.phase2.findings) {
86
- lines.push(`### [${f.severity.toUpperCase()}] ${f.summary}`);
86
+ const dupNote = f.duplicateCount && f.duplicateCount > 1
87
+ ? ` (seen ${f.duplicateCount} times)`
88
+ : '';
89
+ lines.push(`### [${f.severity.toUpperCase()}] ${f.summary}${dupNote}`);
87
90
  lines.push('');
88
91
  lines.push(`- **Type:** ${f.type}`);
89
92
  lines.push(`- **Flow:** ${f.flow}`);
90
93
  lines.push(`- **URL:** ${f.evidence.url}`);
91
- if (f.evidence.screenshotPath) {
92
- lines.push(`- **Screenshot:** ${f.evidence.screenshotPath}`);
94
+ // Expected vs actual behavior
95
+ if (f.evidence.expectedBehavior || f.evidence.actualBehavior) {
96
+ const escapePipe = (s) => s.replace(/\|/g, '\\|');
97
+ lines.push('');
98
+ lines.push(`| Expected | Actual |`);
99
+ lines.push(`|----------|--------|`);
100
+ lines.push(`| ${escapePipe(f.evidence.expectedBehavior || '—')} | ${escapePipe(f.evidence.actualBehavior || '—')} |`);
101
+ lines.push('');
102
+ }
103
+ // Screenshot evidence (inline images)
104
+ if (f.evidence.screenshotRefs && f.evidence.screenshotRefs.length > 0) {
105
+ for (const ref of f.evidence.screenshotRefs) {
106
+ lines.push(`![Evidence](${ref})`);
107
+ }
108
+ }
109
+ else if (f.evidence.screenshotPath) {
110
+ lines.push(`![Evidence](${f.evidence.screenshotPath})`);
111
+ }
112
+ // Console errors
113
+ if (f.evidence.consoleErrors && f.evidence.consoleErrors.length > 0) {
114
+ lines.push('');
115
+ lines.push('**Console errors:**');
116
+ for (const err of f.evidence.consoleErrors.slice(0, 5)) {
117
+ lines.push(`- \`${err.replace(/`/g, '\\`')}\``);
118
+ }
93
119
  }
94
120
  if (f.evidence.reproSteps.length > 0) {
95
- lines.push('- **Repro steps:**');
121
+ lines.push('');
122
+ lines.push('**Repro steps:**');
96
123
  for (const step of f.evidence.reproSteps) {
97
124
  lines.push(` 1. ${step}`);
98
125
  }
@@ -20,7 +20,7 @@ export interface UserCredentials {
20
20
  username: string;
21
21
  password: string;
22
22
  }
23
- export type BrowserActionType = 'navigate' | 'click' | 'fill' | 'type' | 'press' | 'scroll' | 'back' | 'screenshot' | 'snapshot' | 'get_url' | 'get_title' | 'get_text' | 'eval' | 'compressed';
23
+ export type BrowserActionType = 'navigate' | 'click' | 'fill' | 'type' | 'press' | 'press_key' | 'scroll' | 'back' | 'go_back' | 'screenshot' | 'take_screenshot' | 'snapshot' | 'get_url' | 'get_title' | 'get_text' | 'eval' | 'report_finding' | 'mark_flow_done' | 'switch_user' | 'wait_for' | 'compressed';
24
24
  export interface BrowserAction {
25
25
  type: BrowserActionType;
26
26
  target?: string;
@@ -37,12 +37,18 @@ export interface Finding {
37
37
  flow: string;
38
38
  evidence: FindingEvidence;
39
39
  timestamp: number;
40
+ /** Number of duplicate findings collapsed into this one */
41
+ duplicateCount?: number;
40
42
  }
41
43
  export interface FindingEvidence {
42
44
  screenshotPath?: string;
45
+ /** Multiple screenshot references (e.g. before/after) */
46
+ screenshotRefs?: string[];
43
47
  url: string;
44
48
  reproSteps: string[];
45
49
  consoleErrors?: string[];
50
+ expectedBehavior?: string;
51
+ actualBehavior?: string;
46
52
  }
47
53
  export interface TargetFlow {
48
54
  id: string;
@@ -55,6 +61,8 @@ export interface ExplorationState {
55
61
  flowsExplored: string[];
56
62
  currentFlow: string | null;
57
63
  findings: Finding[];
64
+ /** Dedup index: maps finding hash key → index in findings array. Runtime-only — not serializable to JSON. */
65
+ findingDedupIndex: Record<string, number>;
58
66
  actionsLog: BrowserAction[];
59
67
  recentActions: BrowserAction[];
60
68
  tokensUsed: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/qa-agent/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/qa-agent/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAC;AAMpD,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAMxD,MAAM,WAAW,QAAQ;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAMD,MAAM,MAAM,iBAAiB,GACvB,UAAU,GACV,OAAO,GACP,MAAM,GACN,MAAM,GACN,OAAO,GACP,WAAW,GACX,QAAQ,GACR,MAAM,GACN,SAAS,GACT,YAAY,GACZ,iBAAiB,GACjB,UAAU,GACV,SAAS,GACT,WAAW,GACX,UAAU,GACV,MAAM,GACN,gBAAgB,GAChB,gBAAgB,GAChB,aAAa,GACb,UAAU,GACV,YAAY,CAAC;AAEnB,MAAM,WAAW,aAAa;IAC1B,IAAI,EAAE,iBAAiB,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACrB;AAMD,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,mBAAmB,GAAG,UAAU,GAAG,KAAK,GAAG,aAAa,CAAC;AAC3F,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAE9E,MAAM,WAAW,OAAO;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,eAAe,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAMD,MAAM,WAAW,UAAU;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,cAAc,EAAE,UAAU,EAAE,CAAC;IAC7B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,6GAA6G;IAC7G,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,aAAa,EAAE,aAAa,EAAE,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACrB;AAMD,MAAM,WAAW,UAAU;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IACzB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC5B;AAMD,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,OAAO,GAAG,aAAa,CAAC;AAE7D,MAAM,WAAW,WAAW;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,YAAY,CAAC;IAC3C,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC3B,QAAQ,EAAE,eAAe,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACvB;AAMD,MAAM,WAAW,QAAQ;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,gBAAgB,EAAE,MAAM,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,cAAc,CAAC;CAC3B"}
@@ -8,8 +8,10 @@ export interface EnrichedEntry {
8
8
  routes?: string[];
9
9
  pageObjects?: string[];
10
10
  components?: string[];
11
+ webappPaths?: string[];
12
+ serverPaths?: string[];
11
13
  }
12
14
  export declare function validateEntries(parsed: unknown[]): EnrichedEntry[];
13
15
  export declare function parseEnrichResponse(response: string): EnrichedEntry[];
14
- export declare function enrichFamilies(families: RouteFamily[], scanned: ScannedFamily[], projectRoot: string, provider: LLMProvider, budgetUSD: number): Promise<EnrichmentResult>;
16
+ export declare function enrichFamilies(families: RouteFamily[], scanned: ScannedFamily[], projectRoot: string, provider: LLMProvider, budgetUSD: number, testsRoot?: string): Promise<EnrichmentResult>;
15
17
  //# sourceMappingURL=enricher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"enricher.d.ts","sourceRoot":"","sources":["../../src/training/enricher.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAGhE,OAAO,KAAK,EAAC,gBAAgB,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAmIhE,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,EAAE,CAiBlE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,EAAE,CAwBrE;AA2BD,wBAAsB,cAAc,CAChC,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,EAAE,aAAa,EAAE,EACxB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAgF3B"}
1
+ {"version":3,"file":"enricher.d.ts","sourceRoot":"","sources":["../../src/training/enricher.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAGhE,OAAO,KAAK,EAAC,gBAAgB,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAkLhE,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,EAAE,CAmBlE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,EAAE,CAwBrE;AAkCD,wBAAsB,cAAc,CAChC,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,EAAE,aAAa,EAAE,EACxB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAgF3B"}
@@ -64,9 +64,47 @@ function sampleFiles(dir, maxFiles) {
64
64
  walk(dir);
65
65
  return files;
66
66
  }
67
- function buildEnrichPrompt(families, projectRoot) {
67
+ /**
68
+ * Build a shallow directory listing of the source tree (depth 2-3) so the LLM
69
+ * can suggest accurate webappPaths / serverPaths for test-derived families.
70
+ */
71
+ function getSourceTreeListing(projectRoot, maxDepth = 3) {
72
+ const lines = [];
73
+ function walk(dir, depth, prefix) {
74
+ if (depth > maxDepth || lines.length > 200)
75
+ return;
76
+ let entries;
77
+ try {
78
+ entries = (0, fs_1.readdirSync)(dir).sort();
79
+ }
80
+ catch {
81
+ return;
82
+ }
83
+ const dirs = entries.filter((e) => {
84
+ if (e.startsWith('.') || SKIP_DIRS.has(e))
85
+ return false;
86
+ try {
87
+ const stat = (0, fs_1.lstatSync)((0, path_1.join)(dir, e));
88
+ return !stat.isSymbolicLink() && stat.isDirectory();
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ });
94
+ for (const d of dirs) {
95
+ lines.push(`${prefix}${d}/`);
96
+ walk((0, path_1.join)(dir, d), depth + 1, prefix + ' ');
97
+ }
98
+ }
99
+ walk((0, path_1.resolve)(projectRoot), 0, '');
100
+ return lines.join('\n');
101
+ }
102
+ function buildEnrichPrompt(families, projectRoot, testsRoot) {
68
103
  const sections = [];
104
+ const hasTestOnlyFamilies = families.some((f) => f.webappPaths.length === 0 && f.serverPaths.length === 0);
105
+ const resolvedTestsRoot = testsRoot ? (0, path_1.resolve)(testsRoot) : (0, path_1.resolve)(projectRoot);
69
106
  for (const family of families) {
107
+ const isTestOnly = family.webappPaths.length === 0 && family.serverPaths.length === 0;
70
108
  const allDirs = [
71
109
  ...family.webappPaths.map((p) => p.replace(/\/?\*.*$/, '')),
72
110
  ...family.serverPaths.map((p) => p.replace(/\/?\*.*$/, '')),
@@ -80,10 +118,19 @@ function buildEnrichPrompt(families, projectRoot) {
80
118
  if (samples.length >= MAX_FILES_PER_FAMILY)
81
119
  break;
82
120
  }
121
+ // For test-only families, sample the test files themselves for richer context
122
+ if (isTestOnly) {
123
+ for (const specDir of family.specDirs) {
124
+ if (samples.length >= MAX_FILES_PER_FAMILY)
125
+ break;
126
+ const fullDir = (0, path_1.join)(resolvedTestsRoot, specDir);
127
+ samples.push(...sampleFiles(fullDir, MAX_FILES_PER_FAMILY - samples.length));
128
+ }
129
+ }
83
130
  // Sample spec descriptions
84
131
  const specSamples = [];
85
132
  for (const specDir of family.specDirs) {
86
- const fullDir = (0, path_1.join)((0, path_1.resolve)(projectRoot), specDir);
133
+ const fullDir = (0, path_1.join)(resolvedTestsRoot, specDir);
87
134
  const specFiles = sampleFiles(fullDir, 5);
88
135
  for (const sf of specFiles) {
89
136
  const matches = sf.content.match(/(?:test|it|describe)\s*\(\s*['"`]([^'"`]+)/g);
@@ -92,7 +139,7 @@ function buildEnrichPrompt(families, projectRoot) {
92
139
  }
93
140
  }
94
141
  }
95
- sections.push(`## Family: ${family.id}
142
+ sections.push(`## Family: ${family.id}${isTestOnly ? ' [TEST-ONLY — needs webappPaths/serverPaths]' : ''}
96
143
  Routes (guessed): ${JSON.stringify(family.routes)}
97
144
  Webapp paths: ${JSON.stringify(family.webappPaths)}
98
145
  Server paths: ${JSON.stringify(family.serverPaths)}
@@ -107,6 +154,10 @@ Test descriptions:
107
154
  ${specSamples.length > 0 ? specSamples.map((d) => `- ${d}`).join('\n') : '(none found)'}
108
155
  `);
109
156
  }
157
+ // Include source tree listing when we have test-only families
158
+ const sourceTreeSection = hasTestOnlyFamilies
159
+ ? `\n## Source Directory Structure\nUse this to suggest accurate webappPaths and serverPaths for test-only families:\n\`\`\`\n${getSourceTreeListing(projectRoot)}\n\`\`\`\n`
160
+ : '';
110
161
  return `You are analyzing a codebase to enrich route-family definitions for an E2E test impact analysis tool.
111
162
 
112
163
  For each family below, provide:
@@ -115,6 +166,8 @@ For each family below, provide:
115
166
  3. **routes**: Improved URL patterns (e.g., "/{team}/channels/{channel}" instead of "/channels")
116
167
  4. **pageObjects**: Array of page object class names found in the code
117
168
  5. **components**: Array of UI component names relevant to this family
169
+ 6. **webappPaths**: Array of glob patterns for frontend source directories (e.g., "src/components/drafts/**"). REQUIRED for families marked [TEST-ONLY].
170
+ 7. **serverPaths**: Array of glob patterns for backend source directories. REQUIRED for families marked [TEST-ONLY].
118
171
 
119
172
  Respond in JSON format:
120
173
  \`\`\`json
@@ -125,11 +178,13 @@ Respond in JSON format:
125
178
  "userFlows": ["Flow name 1", "Flow name 2"],
126
179
  "routes": ["/improved/route/{param}"],
127
180
  "pageObjects": ["PageName"],
128
- "components": ["ComponentName"]
181
+ "components": ["ComponentName"],
182
+ "webappPaths": ["src/components/feature_name/**"],
183
+ "serverPaths": ["server/channels/api4/feature.go"]
129
184
  }
130
185
  ]
131
186
  \`\`\`
132
-
187
+ ${sourceTreeSection}
133
188
  ${sections.join('\n---\n')}`;
134
189
  }
135
190
  function validateEntries(parsed) {
@@ -148,6 +203,8 @@ function validateEntries(parsed) {
148
203
  userFlows: filterStrings(entry.userFlows, 500),
149
204
  pageObjects: filterStrings(entry.pageObjects, 200),
150
205
  components: filterStrings(entry.components, 200),
206
+ webappPaths: filterStrings(entry.webappPaths, 300),
207
+ serverPaths: filterStrings(entry.serverPaths, 300),
151
208
  }));
152
209
  }
153
210
  function parseEnrichResponse(response) {
@@ -197,9 +254,16 @@ function applyEnrichment(family, enriched) {
197
254
  if (enriched.components && (!family.components || family.components.length === 0)) {
198
255
  result.components = enriched.components;
199
256
  }
257
+ // Only fill source paths when the family has none (test-derived families)
258
+ if (enriched.webappPaths && (!family.webappPaths || family.webappPaths.length === 0)) {
259
+ result.webappPaths = enriched.webappPaths;
260
+ }
261
+ if (enriched.serverPaths && (!family.serverPaths || family.serverPaths.length === 0)) {
262
+ result.serverPaths = enriched.serverPaths;
263
+ }
200
264
  return result;
201
265
  }
202
- async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD) {
266
+ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD, testsRoot) {
203
267
  const scannedMap = new Map(scanned.map((s) => [s.id, s]));
204
268
  const enriched = [];
205
269
  let totalTokens = 0;
@@ -223,7 +287,7 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
223
287
  enriched.push(...chunk);
224
288
  continue;
225
289
  }
226
- let prompt = buildEnrichPrompt(scannedChunk, projectRoot);
290
+ let prompt = buildEnrichPrompt(scannedChunk, projectRoot, testsRoot);
227
291
  if (prompt.length > MAX_PROMPT_CHARS) {
228
292
  // Truncate at the last complete section boundary to avoid malformed input
229
293
  const lastSectionEnd = prompt.lastIndexOf('\n---\n', MAX_PROMPT_CHARS);
@@ -1,5 +1,15 @@
1
1
  import type { RouteFamilyManifest } from '../knowledge/route_families.js';
2
2
  import type { MergeResult, ScannedFamily } from './types.js';
3
3
  export declare function mergeFamilies(existing: RouteFamilyManifest | null, scanned: ScannedFamily[]): MergeResult;
4
- export declare function detectStaleFamilies(manifest: RouteFamilyManifest, projectRoot: string): string[];
4
+ /**
5
+ * Detect families whose paths no longer exist on disk.
6
+ *
7
+ * Paths in the manifest may be relative to different roots:
8
+ * - webappPaths / serverPaths are typically relative to the repo root
9
+ * - specDirs may be relative to the tests root
10
+ *
11
+ * We try each pattern against all provided roots (and the git repo root
12
+ * if discoverable) to avoid false positives from path-prefix mismatches.
13
+ */
14
+ export declare function detectStaleFamilies(manifest: RouteFamilyManifest, projectRoot: string, testsRoot?: string): string[];
5
15
  //# sourceMappingURL=merger.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"merger.d.ts","sourceRoot":"","sources":["../../src/training/merger.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAc,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AAGrF,OAAO,KAAK,EAAC,WAAW,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAqE3D,wBAAgB,aAAa,CACzB,QAAQ,EAAE,mBAAmB,GAAG,IAAI,EACpC,OAAO,EAAE,aAAa,EAAE,GACzB,WAAW,CAyCb;AAED,wBAAgB,mBAAmB,CAC/B,QAAQ,EAAE,mBAAmB,EAC7B,WAAW,EAAE,MAAM,GACpB,MAAM,EAAE,CA8BV"}
1
+ {"version":3,"file":"merger.d.ts","sourceRoot":"","sources":["../../src/training/merger.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AAGrF,OAAO,KAAK,EAAC,WAAW,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAkF3D,wBAAgB,aAAa,CACzB,QAAQ,EAAE,mBAAmB,GAAG,IAAI,EACpC,OAAO,EAAE,aAAa,EAAE,GACzB,WAAW,CA+Cb;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAC/B,QAAQ,EAAE,mBAAmB,EAC7B,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACnB,MAAM,EAAE,CA6DV"}
@@ -4,6 +4,7 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.mergeFamilies = mergeFamilies;
6
6
  exports.detectStaleFamilies = detectStaleFamilies;
7
+ const child_process_1 = require("child_process");
7
8
  const fs_1 = require("fs");
8
9
  const path_1 = require("path");
9
10
  const types_js_1 = require("./types.js");
@@ -71,6 +72,21 @@ function scannedToRouteFamily(scanned) {
71
72
  }
72
73
  return family;
73
74
  }
75
+ /**
76
+ * Try to find a matching family ID with singular/plural normalization.
77
+ * "team" matches "teams", "emoji" matches "emoji", etc.
78
+ */
79
+ function findFuzzyMatch(id, idMap) {
80
+ if (idMap.has(id))
81
+ return id;
82
+ // Try adding 's'
83
+ if (!id.endsWith('s') && idMap.has(id + 's'))
84
+ return id + 's';
85
+ // Try removing 's'
86
+ if (id.endsWith('s') && idMap.has(id.slice(0, -1)))
87
+ return id.slice(0, -1);
88
+ return undefined;
89
+ }
74
90
  function mergeFamilies(existing, scanned) {
75
91
  const existingFamilies = existing?.families || [];
76
92
  const existingMap = new Map(existingFamilies.map((f) => [f.id, f]));
@@ -78,9 +94,15 @@ function mergeFamilies(existing, scanned) {
78
94
  const newFamilies = [];
79
95
  const updatedFamilies = [];
80
96
  const mergedFamilies = [];
81
- // Process existing families
97
+ // Process existing families — match scanned by exact or fuzzy ID
82
98
  for (const ef of existingFamilies) {
83
- const sf = scannedMap.get(ef.id);
99
+ let sf = scannedMap.get(ef.id);
100
+ // Try singular/plural match if exact match failed
101
+ if (!sf) {
102
+ const fuzzyId = findFuzzyMatch(ef.id, scannedMap);
103
+ if (fuzzyId)
104
+ sf = scannedMap.get(fuzzyId);
105
+ }
84
106
  if (sf) {
85
107
  mergedFamilies.push(mergeFamily(ef, sf));
86
108
  updatedFamilies.push(ef.id);
@@ -90,9 +112,10 @@ function mergeFamilies(existing, scanned) {
90
112
  mergedFamilies.push({ ...ef });
91
113
  }
92
114
  }
93
- // Add new families from scanner
115
+ // Add new families from scanner (if no existing family matched)
94
116
  for (const sf of scanned) {
95
- if (!existingMap.has(sf.id)) {
117
+ const matchedExisting = findFuzzyMatch(sf.id, existingMap);
118
+ if (!matchedExisting) {
96
119
  mergedFamilies.push(scannedToRouteFamily(sf));
97
120
  newFamilies.push(sf.id);
98
121
  }
@@ -112,8 +135,33 @@ function mergeFamilies(existing, scanned) {
112
135
  summary: parts.join(', '),
113
136
  };
114
137
  }
115
- function detectStaleFamilies(manifest, projectRoot) {
116
- const resolved = (0, path_1.resolve)(projectRoot);
138
+ /**
139
+ * Detect families whose paths no longer exist on disk.
140
+ *
141
+ * Paths in the manifest may be relative to different roots:
142
+ * - webappPaths / serverPaths are typically relative to the repo root
143
+ * - specDirs may be relative to the tests root
144
+ *
145
+ * We try each pattern against all provided roots (and the git repo root
146
+ * if discoverable) to avoid false positives from path-prefix mismatches.
147
+ */
148
+ function detectStaleFamilies(manifest, projectRoot, testsRoot) {
149
+ const roots = new Set([(0, path_1.resolve)(projectRoot)]);
150
+ if (testsRoot)
151
+ roots.add((0, path_1.resolve)(testsRoot));
152
+ // Also try to discover the git repo root — manifest paths may be repo-relative
153
+ try {
154
+ const gitRoot = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], {
155
+ cwd: projectRoot,
156
+ encoding: 'utf-8',
157
+ stdio: ['pipe', 'pipe', 'pipe'],
158
+ }).trim();
159
+ if (gitRoot)
160
+ roots.add((0, path_1.resolve)(gitRoot));
161
+ }
162
+ catch {
163
+ // Not a git repo or git not available — that's fine
164
+ }
117
165
  const stale = [];
118
166
  for (const family of manifest.families) {
119
167
  const allPatterns = [
@@ -123,15 +171,34 @@ function detectStaleFamilies(manifest, projectRoot) {
123
171
  ];
124
172
  if (allPatterns.length === 0)
125
173
  continue;
126
- // Check if any pattern resolves to existing files/dirs
174
+ // Check if any pattern resolves to existing files/dirs in any root
127
175
  let hasAny = false;
128
176
  for (const pattern of allPatterns) {
129
177
  // Strip trailing glob (* or **) to get the directory
130
178
  const dirPart = pattern.replace(/\/?\*.*$/, '');
131
- if (dirPart && (0, fs_1.existsSync)((0, path_1.join)(resolved, dirPart))) {
132
- hasAny = true;
133
- break;
179
+ if (!dirPart)
180
+ continue;
181
+ // For file-level patterns like "server/channels/api4/draft*.go",
182
+ // dirPart is "server/channels/api4/draft" — check the parent dir instead
183
+ const isFileGlob = /\.\w+$/.test(pattern);
184
+ const pathsToCheck = [dirPart];
185
+ if (isFileGlob) {
186
+ const parentDir = dirPart.split('/').slice(0, -1).join('/');
187
+ if (parentDir)
188
+ pathsToCheck.push(parentDir);
189
+ }
190
+ for (const checkPath of pathsToCheck) {
191
+ for (const root of roots) {
192
+ if ((0, fs_1.existsSync)((0, path_1.join)(root, checkPath))) {
193
+ hasAny = true;
194
+ break;
195
+ }
196
+ }
197
+ if (hasAny)
198
+ break;
134
199
  }
200
+ if (hasAny)
201
+ break;
135
202
  }
136
203
  if (!hasAny) {
137
204
  stale.push(family.id);
@@ -1,5 +1,18 @@
1
- import type { DiscoveredDir, ScanResult } from './types.js';
1
+ import type { DiscoveredDir, ScannedFamily, ScanResult } from './types.js';
2
2
  export declare function discoverSourceDirs(projectRoot: string): DiscoveredDir[];
3
3
  export declare function discoverTestDirs(projectRoot: string): DiscoveredDir[];
4
- export declare function scanProject(projectRoot: string): ScanResult;
4
+ /**
5
+ * Discover families by scanning server Go source files.
6
+ *
7
+ * The backend follows a three-tier pattern:
8
+ * api4/draft.go + app/draft.go + store/sqlstore/draft_store.go
9
+ *
10
+ * Related files are grouped under parent domains:
11
+ * channel.go, channel_bookmark.go, channel_category.go → "channel" family
12
+ *
13
+ * Each domain becomes a candidate family with precise serverPaths.
14
+ */
15
+ export declare function discoverServerDerivedFamilies(serverRoot: string): ScannedFamily[];
16
+ export declare function discoverTestDerivedFamilies(testsRoot: string): ScannedFamily[];
17
+ export declare function scanProject(projectRoot: string, testsRoot?: string, serverRoot?: string): ScanResult;
5
18
  //# sourceMappingURL=scanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/training/scanner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAiC,UAAU,EAAC,MAAM,YAAY,CAAC;AAgGzF,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA+BvE;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA6DrE;AA6ID,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CA+E3D"}
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/training/scanner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAE,aAAa,EAAkB,UAAU,EAAC,MAAM,YAAY,CAAC;AAgJzF,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA+BvE;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA6DrE;AAuLD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,EAAE,CA0HjF;AAED,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,EAAE,CAiG9E;AAED,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,UAAU,CA6IpG"}