@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.
- package/dist/agent/feedback.d.ts +16 -0
- package/dist/agent/feedback.d.ts.map +1 -1
- package/dist/agent/feedback.js +62 -0
- package/dist/agent/process_runner.d.ts +1 -1
- package/dist/agent/process_runner.d.ts.map +1 -1
- package/dist/agent/process_runner.js +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -2
- package/dist/cli/commands/train.d.ts.map +1 -1
- package/dist/cli/commands/train.js +31 -4
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +1 -0
- package/dist/cli/types.d.ts +1 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/engine/plan_builder.d.ts +2 -1
- package/dist/engine/plan_builder.d.ts.map +1 -1
- package/dist/engine/plan_builder.js +22 -9
- package/dist/esm/agent/feedback.js +61 -0
- package/dist/esm/agent/process_runner.js +3 -3
- package/dist/esm/api.js +5 -2
- package/dist/esm/cli/commands/train.js +31 -4
- package/dist/esm/cli/parse_args.js +1 -0
- package/dist/esm/engine/plan_builder.js +22 -9
- package/dist/esm/index.js +1 -1
- package/dist/esm/pipeline/spec_verifier.js +75 -0
- package/dist/esm/pipeline/stage3_generation.js +122 -4
- package/dist/esm/pipeline/stage4_heal.js +146 -3
- package/dist/esm/prompts/heal.js +4 -0
- package/dist/esm/qa-agent/phase2/agent_loop.js +60 -24
- package/dist/esm/qa-agent/phase2/exploration_state.js +21 -0
- package/dist/esm/qa-agent/phase2/tools.js +99 -1
- package/dist/esm/qa-agent/phase3/reporter.js +31 -4
- package/dist/esm/training/enricher.js +71 -7
- package/dist/esm/training/merger.js +77 -10
- package/dist/esm/training/scanner.js +368 -2
- package/dist/esm/validation/guardrails.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/spec_verifier.d.ts +20 -0
- package/dist/pipeline/spec_verifier.d.ts.map +1 -0
- package/dist/pipeline/spec_verifier.js +79 -0
- package/dist/pipeline/stage3_generation.d.ts +10 -0
- package/dist/pipeline/stage3_generation.d.ts.map +1 -1
- package/dist/pipeline/stage3_generation.js +120 -2
- package/dist/pipeline/stage4_heal.d.ts +4 -0
- package/dist/pipeline/stage4_heal.d.ts.map +1 -1
- package/dist/pipeline/stage4_heal.js +145 -2
- package/dist/prompts/heal.d.ts +2 -0
- package/dist/prompts/heal.d.ts.map +1 -1
- package/dist/prompts/heal.js +4 -0
- package/dist/qa-agent/phase2/agent_loop.d.ts.map +1 -1
- package/dist/qa-agent/phase2/agent_loop.js +60 -24
- package/dist/qa-agent/phase2/exploration_state.d.ts.map +1 -1
- package/dist/qa-agent/phase2/exploration_state.js +21 -0
- package/dist/qa-agent/phase2/tools.d.ts.map +1 -1
- package/dist/qa-agent/phase2/tools.js +99 -1
- package/dist/qa-agent/phase3/reporter.js +31 -4
- package/dist/qa-agent/types.d.ts +9 -1
- package/dist/qa-agent/types.d.ts.map +1 -1
- package/dist/training/enricher.d.ts +3 -1
- package/dist/training/enricher.d.ts.map +1 -1
- package/dist/training/enricher.js +71 -7
- package/dist/training/merger.d.ts +11 -1
- package/dist/training/merger.d.ts.map +1 -1
- package/dist/training/merger.js +77 -10
- package/dist/training/scanner.d.ts +15 -2
- package/dist/training/scanner.d.ts.map +1 -1
- package/dist/training/scanner.js +370 -2
- package/dist/training/types.d.ts +4 -0
- package/dist/training/types.d.ts.map +1 -1
- package/dist/validation/guardrails.d.ts +2 -0
- package/dist/validation/guardrails.d.ts.map +1 -1
- package/dist/validation/guardrails.js +4 -1
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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(``);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (f.evidence.screenshotPath) {
|
|
110
|
+
lines.push(``);
|
|
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('
|
|
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
|
}
|
package/dist/qa-agent/types.d.ts
CHANGED
|
@@ -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":"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,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,UAAU,GACV,SAAS,GACT,WAAW,GACX,UAAU,GACV,MAAM,GACN,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;
|
|
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;
|
|
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
|
-
|
|
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)(
|
|
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
|
-
|
|
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":"
|
|
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"}
|
package/dist/training/merger.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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,
|
|
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"}
|