@yasserkhanorg/e2e-agents 1.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/train.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,aAAa,CAAC;AA6H5C,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6K1F"}
1
+ {"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/train.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,aAAa,CAAC;AAqJ5C,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqL1F"}
@@ -36,6 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  })();
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.runTrainCommand = runTrainCommand;
39
+ const child_process_1 = require("child_process");
39
40
  const fs_1 = require("fs");
40
41
  const path_1 = require("path");
41
42
  const readline = __importStar(require("readline"));
@@ -111,9 +112,32 @@ function resolveTrainOptions(args, autoConfig) {
111
112
  if (!inApp && !inTests) {
112
113
  throw new TrainError(`Output path must be within the project root or tests root: ${resolvedOutputPath}`);
113
114
  }
115
+ // Discover git repo root for monorepo-aware scanning and validation
116
+ let gitRepoRoot;
117
+ try {
118
+ gitRepoRoot = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], {
119
+ cwd: resolvedAppPath,
120
+ encoding: 'utf-8',
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ }).trim();
123
+ }
124
+ catch {
125
+ // Not a git repo or git not available
126
+ }
127
+ // Resolve serverRoot: explicit flag, or auto-detect from git repo root
128
+ let serverRoot = args.serverPath;
129
+ if (!serverRoot && gitRepoRoot) {
130
+ const serverDir = (0, path_1.join)(gitRepoRoot, 'server');
131
+ if ((0, fs_1.existsSync)(serverDir)) {
132
+ serverRoot = serverDir;
133
+ }
134
+ }
135
+ const resolvedServerRoot = serverRoot ? (0, path_1.resolve)(serverRoot) : undefined;
114
136
  return {
115
137
  appPath: resolvedAppPath,
116
138
  testsRoot: resolvedTestsRoot,
139
+ serverRoot: resolvedServerRoot,
140
+ gitRepoRoot: gitRepoRoot ? (0, path_1.resolve)(gitRepoRoot) : undefined,
117
141
  enrich: args.trainEnrich !== false,
118
142
  validate: args.trainValidate || false,
119
143
  since,
@@ -158,7 +182,10 @@ async function runTrainCommand(args, autoConfig) {
158
182
  console.log('');
159
183
  // ---------- Phase 1: Deterministic scan ----------
160
184
  console.log(' Scanning project structure...');
161
- const scanResult = (0, scanner_js_1.scanProject)(opts.appPath);
185
+ if (opts.serverRoot) {
186
+ console.log(` Server root: ${opts.serverRoot}`);
187
+ }
188
+ const scanResult = (0, scanner_js_1.scanProject)(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot);
162
189
  console.log(` Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
163
190
  console.log(` Discovered ${scanResult.families.length} candidate families`);
164
191
  if (scanResult.families.length === 0) {
@@ -177,7 +204,7 @@ async function runTrainCommand(args, autoConfig) {
177
204
  console.log(` Merge: ${mergeResult.summary}`);
178
205
  // ---------- Phase 3: Stale detection ----------
179
206
  if (mergeResult.manifest.families.length > 0) {
180
- const stale = (0, merger_js_1.detectStaleFamilies)(mergeResult.manifest, opts.appPath);
207
+ const stale = (0, merger_js_1.detectStaleFamilies)(mergeResult.manifest, opts.appPath, opts.testsRoot);
181
208
  if (stale.length > 0) {
182
209
  console.log('');
183
210
  console.log(` Stale families detected (${stale.length}):`);
@@ -207,7 +234,7 @@ async function runTrainCommand(args, autoConfig) {
207
234
  console.log(' Enriching with LLM...');
208
235
  try {
209
236
  const provider = await provider_factory_js_1.LLMProviderFactory.createFromEnv();
210
- const enrichResult = await (0, enricher_js_1.enrichFamilies)(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD);
237
+ const enrichResult = await (0, enricher_js_1.enrichFamilies)(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined);
211
238
  mergeResult.manifest.families = enrichResult.enrichedFamilies;
212
239
  console.log(` Enriched ${enrichResult.enrichedFamilies.length} families (${enrichResult.tokensUsed} tokens, ~$${enrichResult.costUSD})`);
213
240
  if (enrichResult.skippedFamilies.length > 0) {
@@ -291,7 +318,7 @@ async function runTrainCommand(args, autoConfig) {
291
318
  else {
292
319
  console.log('');
293
320
  console.log(` Validating against git history (${opts.since})...`);
294
- const commits = (0, validator_js_1.getCommitFiles)(opts.appPath, opts.since);
321
+ const commits = (0, validator_js_1.getCommitFiles)(opts.gitRepoRoot || opts.appPath, opts.since);
295
322
  if (commits.length === 0) {
296
323
  console.log(' No commits found in range.');
297
324
  }
@@ -1 +1 @@
1
- {"version":3,"file":"parse_args.d.ts","sourceRoot":"","sources":["../../src/cli/parse_args.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,UAAU,EAAC,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,iBAAiB,UAA8D,CAAC;AAE7F,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAmBlF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAmBtE;AA2ID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA4EpD"}
1
+ {"version":3,"file":"parse_args.d.ts","sourceRoot":"","sources":["../../src/cli/parse_args.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,UAAU,EAAC,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,iBAAiB,UAA8D,CAAC;AAE7F,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAmBlF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAmBtE;AA4ID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA4EpD"}
@@ -101,6 +101,7 @@ const FLAGS = {
101
101
  '--heal-report': { key: 'analyzeHealReport', type: 'string' },
102
102
  '--flow-catalog': { key: 'flowCatalogPath', type: 'string' },
103
103
  '--output': { key: 'trainOutput', type: 'string' },
104
+ '--server-path': { key: 'serverPath', type: 'string' },
104
105
  // -- number flags (with isFinite guard) --
105
106
  '--pipeline-scenarios': { key: 'pipelineScenarios', type: 'number' },
106
107
  '--time': { key: 'timeLimitMinutes', type: 'number' },
@@ -71,5 +71,6 @@ export interface ParsedArgs {
71
71
  trainPr?: number;
72
72
  trainOutput?: string;
73
73
  trainYes?: boolean;
74
+ serverPath?: string;
74
75
  }
75
76
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
+ import { execFileSync } from 'child_process';
3
4
  import { existsSync, mkdirSync, renameSync, writeFileSync } from 'fs';
4
5
  import { dirname, join, resolve } from 'path';
5
6
  import * as readline from 'readline';
@@ -75,9 +76,32 @@ function resolveTrainOptions(args, autoConfig) {
75
76
  if (!inApp && !inTests) {
76
77
  throw new TrainError(`Output path must be within the project root or tests root: ${resolvedOutputPath}`);
77
78
  }
79
+ // Discover git repo root for monorepo-aware scanning and validation
80
+ let gitRepoRoot;
81
+ try {
82
+ gitRepoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
83
+ cwd: resolvedAppPath,
84
+ encoding: 'utf-8',
85
+ stdio: ['pipe', 'pipe', 'pipe'],
86
+ }).trim();
87
+ }
88
+ catch {
89
+ // Not a git repo or git not available
90
+ }
91
+ // Resolve serverRoot: explicit flag, or auto-detect from git repo root
92
+ let serverRoot = args.serverPath;
93
+ if (!serverRoot && gitRepoRoot) {
94
+ const serverDir = join(gitRepoRoot, 'server');
95
+ if (existsSync(serverDir)) {
96
+ serverRoot = serverDir;
97
+ }
98
+ }
99
+ const resolvedServerRoot = serverRoot ? resolve(serverRoot) : undefined;
78
100
  return {
79
101
  appPath: resolvedAppPath,
80
102
  testsRoot: resolvedTestsRoot,
103
+ serverRoot: resolvedServerRoot,
104
+ gitRepoRoot: gitRepoRoot ? resolve(gitRepoRoot) : undefined,
81
105
  enrich: args.trainEnrich !== false,
82
106
  validate: args.trainValidate || false,
83
107
  since,
@@ -122,7 +146,10 @@ export async function runTrainCommand(args, autoConfig) {
122
146
  console.log('');
123
147
  // ---------- Phase 1: Deterministic scan ----------
124
148
  console.log(' Scanning project structure...');
125
- const scanResult = scanProject(opts.appPath);
149
+ if (opts.serverRoot) {
150
+ console.log(` Server root: ${opts.serverRoot}`);
151
+ }
152
+ const scanResult = scanProject(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot);
126
153
  console.log(` Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
127
154
  console.log(` Discovered ${scanResult.families.length} candidate families`);
128
155
  if (scanResult.families.length === 0) {
@@ -141,7 +168,7 @@ export async function runTrainCommand(args, autoConfig) {
141
168
  console.log(` Merge: ${mergeResult.summary}`);
142
169
  // ---------- Phase 3: Stale detection ----------
143
170
  if (mergeResult.manifest.families.length > 0) {
144
- const stale = detectStaleFamilies(mergeResult.manifest, opts.appPath);
171
+ const stale = detectStaleFamilies(mergeResult.manifest, opts.appPath, opts.testsRoot);
145
172
  if (stale.length > 0) {
146
173
  console.log('');
147
174
  console.log(` Stale families detected (${stale.length}):`);
@@ -171,7 +198,7 @@ export async function runTrainCommand(args, autoConfig) {
171
198
  console.log(' Enriching with LLM...');
172
199
  try {
173
200
  const provider = await LLMProviderFactory.createFromEnv();
174
- const enrichResult = await enrichFamilies(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD);
201
+ const enrichResult = await enrichFamilies(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined);
175
202
  mergeResult.manifest.families = enrichResult.enrichedFamilies;
176
203
  console.log(` Enriched ${enrichResult.enrichedFamilies.length} families (${enrichResult.tokensUsed} tokens, ~$${enrichResult.costUSD})`);
177
204
  if (enrichResult.skippedFamilies.length > 0) {
@@ -255,7 +282,7 @@ export async function runTrainCommand(args, autoConfig) {
255
282
  else {
256
283
  console.log('');
257
284
  console.log(` Validating against git history (${opts.since})...`);
258
- const commits = getCommitFiles(opts.appPath, opts.since);
285
+ const commits = getCommitFiles(opts.gitRepoRoot || opts.appPath, opts.since);
259
286
  if (commits.length === 0) {
260
287
  console.log(' No commits found in range.');
261
288
  }
@@ -95,6 +95,7 @@ const FLAGS = {
95
95
  '--heal-report': { key: 'analyzeHealReport', type: 'string' },
96
96
  '--flow-catalog': { key: 'flowCatalogPath', type: 'string' },
97
97
  '--output': { key: 'trainOutput', type: 'string' },
98
+ '--server-path': { key: 'serverPath', type: 'string' },
98
99
  // -- number flags (with isFinite guard) --
99
100
  '--pipeline-scenarios': { key: 'pipelineScenarios', type: 'number' },
100
101
  '--time': { key: 'timeLimitMinutes', type: 'number' },
@@ -59,9 +59,47 @@ function sampleFiles(dir, maxFiles) {
59
59
  walk(dir);
60
60
  return files;
61
61
  }
62
- function buildEnrichPrompt(families, projectRoot) {
62
+ /**
63
+ * Build a shallow directory listing of the source tree (depth 2-3) so the LLM
64
+ * can suggest accurate webappPaths / serverPaths for test-derived families.
65
+ */
66
+ function getSourceTreeListing(projectRoot, maxDepth = 3) {
67
+ const lines = [];
68
+ function walk(dir, depth, prefix) {
69
+ if (depth > maxDepth || lines.length > 200)
70
+ return;
71
+ let entries;
72
+ try {
73
+ entries = readdirSync(dir).sort();
74
+ }
75
+ catch {
76
+ return;
77
+ }
78
+ const dirs = entries.filter((e) => {
79
+ if (e.startsWith('.') || SKIP_DIRS.has(e))
80
+ return false;
81
+ try {
82
+ const stat = lstatSync(join(dir, e));
83
+ return !stat.isSymbolicLink() && stat.isDirectory();
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ });
89
+ for (const d of dirs) {
90
+ lines.push(`${prefix}${d}/`);
91
+ walk(join(dir, d), depth + 1, prefix + ' ');
92
+ }
93
+ }
94
+ walk(resolve(projectRoot), 0, '');
95
+ return lines.join('\n');
96
+ }
97
+ function buildEnrichPrompt(families, projectRoot, testsRoot) {
63
98
  const sections = [];
99
+ const hasTestOnlyFamilies = families.some((f) => f.webappPaths.length === 0 && f.serverPaths.length === 0);
100
+ const resolvedTestsRoot = testsRoot ? resolve(testsRoot) : resolve(projectRoot);
64
101
  for (const family of families) {
102
+ const isTestOnly = family.webappPaths.length === 0 && family.serverPaths.length === 0;
65
103
  const allDirs = [
66
104
  ...family.webappPaths.map((p) => p.replace(/\/?\*.*$/, '')),
67
105
  ...family.serverPaths.map((p) => p.replace(/\/?\*.*$/, '')),
@@ -75,10 +113,19 @@ function buildEnrichPrompt(families, projectRoot) {
75
113
  if (samples.length >= MAX_FILES_PER_FAMILY)
76
114
  break;
77
115
  }
116
+ // For test-only families, sample the test files themselves for richer context
117
+ if (isTestOnly) {
118
+ for (const specDir of family.specDirs) {
119
+ if (samples.length >= MAX_FILES_PER_FAMILY)
120
+ break;
121
+ const fullDir = join(resolvedTestsRoot, specDir);
122
+ samples.push(...sampleFiles(fullDir, MAX_FILES_PER_FAMILY - samples.length));
123
+ }
124
+ }
78
125
  // Sample spec descriptions
79
126
  const specSamples = [];
80
127
  for (const specDir of family.specDirs) {
81
- const fullDir = join(resolve(projectRoot), specDir);
128
+ const fullDir = join(resolvedTestsRoot, specDir);
82
129
  const specFiles = sampleFiles(fullDir, 5);
83
130
  for (const sf of specFiles) {
84
131
  const matches = sf.content.match(/(?:test|it|describe)\s*\(\s*['"`]([^'"`]+)/g);
@@ -87,7 +134,7 @@ function buildEnrichPrompt(families, projectRoot) {
87
134
  }
88
135
  }
89
136
  }
90
- sections.push(`## Family: ${family.id}
137
+ sections.push(`## Family: ${family.id}${isTestOnly ? ' [TEST-ONLY — needs webappPaths/serverPaths]' : ''}
91
138
  Routes (guessed): ${JSON.stringify(family.routes)}
92
139
  Webapp paths: ${JSON.stringify(family.webappPaths)}
93
140
  Server paths: ${JSON.stringify(family.serverPaths)}
@@ -102,6 +149,10 @@ Test descriptions:
102
149
  ${specSamples.length > 0 ? specSamples.map((d) => `- ${d}`).join('\n') : '(none found)'}
103
150
  `);
104
151
  }
152
+ // Include source tree listing when we have test-only families
153
+ const sourceTreeSection = hasTestOnlyFamilies
154
+ ? `\n## Source Directory Structure\nUse this to suggest accurate webappPaths and serverPaths for test-only families:\n\`\`\`\n${getSourceTreeListing(projectRoot)}\n\`\`\`\n`
155
+ : '';
105
156
  return `You are analyzing a codebase to enrich route-family definitions for an E2E test impact analysis tool.
106
157
 
107
158
  For each family below, provide:
@@ -110,6 +161,8 @@ For each family below, provide:
110
161
  3. **routes**: Improved URL patterns (e.g., "/{team}/channels/{channel}" instead of "/channels")
111
162
  4. **pageObjects**: Array of page object class names found in the code
112
163
  5. **components**: Array of UI component names relevant to this family
164
+ 6. **webappPaths**: Array of glob patterns for frontend source directories (e.g., "src/components/drafts/**"). REQUIRED for families marked [TEST-ONLY].
165
+ 7. **serverPaths**: Array of glob patterns for backend source directories. REQUIRED for families marked [TEST-ONLY].
113
166
 
114
167
  Respond in JSON format:
115
168
  \`\`\`json
@@ -120,11 +173,13 @@ Respond in JSON format:
120
173
  "userFlows": ["Flow name 1", "Flow name 2"],
121
174
  "routes": ["/improved/route/{param}"],
122
175
  "pageObjects": ["PageName"],
123
- "components": ["ComponentName"]
176
+ "components": ["ComponentName"],
177
+ "webappPaths": ["src/components/feature_name/**"],
178
+ "serverPaths": ["server/channels/api4/feature.go"]
124
179
  }
125
180
  ]
126
181
  \`\`\`
127
-
182
+ ${sourceTreeSection}
128
183
  ${sections.join('\n---\n')}`;
129
184
  }
130
185
  export function validateEntries(parsed) {
@@ -143,6 +198,8 @@ export function validateEntries(parsed) {
143
198
  userFlows: filterStrings(entry.userFlows, 500),
144
199
  pageObjects: filterStrings(entry.pageObjects, 200),
145
200
  components: filterStrings(entry.components, 200),
201
+ webappPaths: filterStrings(entry.webappPaths, 300),
202
+ serverPaths: filterStrings(entry.serverPaths, 300),
146
203
  }));
147
204
  }
148
205
  export function parseEnrichResponse(response) {
@@ -192,9 +249,16 @@ function applyEnrichment(family, enriched) {
192
249
  if (enriched.components && (!family.components || family.components.length === 0)) {
193
250
  result.components = enriched.components;
194
251
  }
252
+ // Only fill source paths when the family has none (test-derived families)
253
+ if (enriched.webappPaths && (!family.webappPaths || family.webappPaths.length === 0)) {
254
+ result.webappPaths = enriched.webappPaths;
255
+ }
256
+ if (enriched.serverPaths && (!family.serverPaths || family.serverPaths.length === 0)) {
257
+ result.serverPaths = enriched.serverPaths;
258
+ }
195
259
  return result;
196
260
  }
197
- export async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD) {
261
+ export async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD, testsRoot) {
198
262
  const scannedMap = new Map(scanned.map((s) => [s.id, s]));
199
263
  const enriched = [];
200
264
  let totalTokens = 0;
@@ -218,7 +282,7 @@ export async function enrichFamilies(families, scanned, projectRoot, provider, b
218
282
  enriched.push(...chunk);
219
283
  continue;
220
284
  }
221
- let prompt = buildEnrichPrompt(scannedChunk, projectRoot);
285
+ let prompt = buildEnrichPrompt(scannedChunk, projectRoot, testsRoot);
222
286
  if (prompt.length > MAX_PROMPT_CHARS) {
223
287
  // Truncate at the last complete section boundary to avoid malformed input
224
288
  const lastSectionEnd = prompt.lastIndexOf('\n---\n', MAX_PROMPT_CHARS);
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
+ import { execFileSync } from 'child_process';
3
4
  import { existsSync } from 'fs';
4
5
  import { join, resolve } from 'path';
5
6
  import { isGuessedRoute } from './types.js';
@@ -67,6 +68,21 @@ function scannedToRouteFamily(scanned) {
67
68
  }
68
69
  return family;
69
70
  }
71
+ /**
72
+ * Try to find a matching family ID with singular/plural normalization.
73
+ * "team" matches "teams", "emoji" matches "emoji", etc.
74
+ */
75
+ function findFuzzyMatch(id, idMap) {
76
+ if (idMap.has(id))
77
+ return id;
78
+ // Try adding 's'
79
+ if (!id.endsWith('s') && idMap.has(id + 's'))
80
+ return id + 's';
81
+ // Try removing 's'
82
+ if (id.endsWith('s') && idMap.has(id.slice(0, -1)))
83
+ return id.slice(0, -1);
84
+ return undefined;
85
+ }
70
86
  export function mergeFamilies(existing, scanned) {
71
87
  const existingFamilies = existing?.families || [];
72
88
  const existingMap = new Map(existingFamilies.map((f) => [f.id, f]));
@@ -74,9 +90,15 @@ export function mergeFamilies(existing, scanned) {
74
90
  const newFamilies = [];
75
91
  const updatedFamilies = [];
76
92
  const mergedFamilies = [];
77
- // Process existing families
93
+ // Process existing families — match scanned by exact or fuzzy ID
78
94
  for (const ef of existingFamilies) {
79
- const sf = scannedMap.get(ef.id);
95
+ let sf = scannedMap.get(ef.id);
96
+ // Try singular/plural match if exact match failed
97
+ if (!sf) {
98
+ const fuzzyId = findFuzzyMatch(ef.id, scannedMap);
99
+ if (fuzzyId)
100
+ sf = scannedMap.get(fuzzyId);
101
+ }
80
102
  if (sf) {
81
103
  mergedFamilies.push(mergeFamily(ef, sf));
82
104
  updatedFamilies.push(ef.id);
@@ -86,9 +108,10 @@ export function mergeFamilies(existing, scanned) {
86
108
  mergedFamilies.push({ ...ef });
87
109
  }
88
110
  }
89
- // Add new families from scanner
111
+ // Add new families from scanner (if no existing family matched)
90
112
  for (const sf of scanned) {
91
- if (!existingMap.has(sf.id)) {
113
+ const matchedExisting = findFuzzyMatch(sf.id, existingMap);
114
+ if (!matchedExisting) {
92
115
  mergedFamilies.push(scannedToRouteFamily(sf));
93
116
  newFamilies.push(sf.id);
94
117
  }
@@ -108,8 +131,33 @@ export function mergeFamilies(existing, scanned) {
108
131
  summary: parts.join(', '),
109
132
  };
110
133
  }
111
- export function detectStaleFamilies(manifest, projectRoot) {
112
- const resolved = resolve(projectRoot);
134
+ /**
135
+ * Detect families whose paths no longer exist on disk.
136
+ *
137
+ * Paths in the manifest may be relative to different roots:
138
+ * - webappPaths / serverPaths are typically relative to the repo root
139
+ * - specDirs may be relative to the tests root
140
+ *
141
+ * We try each pattern against all provided roots (and the git repo root
142
+ * if discoverable) to avoid false positives from path-prefix mismatches.
143
+ */
144
+ export function detectStaleFamilies(manifest, projectRoot, testsRoot) {
145
+ const roots = new Set([resolve(projectRoot)]);
146
+ if (testsRoot)
147
+ roots.add(resolve(testsRoot));
148
+ // Also try to discover the git repo root — manifest paths may be repo-relative
149
+ try {
150
+ const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
151
+ cwd: projectRoot,
152
+ encoding: 'utf-8',
153
+ stdio: ['pipe', 'pipe', 'pipe'],
154
+ }).trim();
155
+ if (gitRoot)
156
+ roots.add(resolve(gitRoot));
157
+ }
158
+ catch {
159
+ // Not a git repo or git not available — that's fine
160
+ }
113
161
  const stale = [];
114
162
  for (const family of manifest.families) {
115
163
  const allPatterns = [
@@ -119,15 +167,34 @@ export function detectStaleFamilies(manifest, projectRoot) {
119
167
  ];
120
168
  if (allPatterns.length === 0)
121
169
  continue;
122
- // Check if any pattern resolves to existing files/dirs
170
+ // Check if any pattern resolves to existing files/dirs in any root
123
171
  let hasAny = false;
124
172
  for (const pattern of allPatterns) {
125
173
  // Strip trailing glob (* or **) to get the directory
126
174
  const dirPart = pattern.replace(/\/?\*.*$/, '');
127
- if (dirPart && existsSync(join(resolved, dirPart))) {
128
- hasAny = true;
129
- break;
175
+ if (!dirPart)
176
+ continue;
177
+ // For file-level patterns like "server/channels/api4/draft*.go",
178
+ // dirPart is "server/channels/api4/draft" — check the parent dir instead
179
+ const isFileGlob = /\.\w+$/.test(pattern);
180
+ const pathsToCheck = [dirPart];
181
+ if (isFileGlob) {
182
+ const parentDir = dirPart.split('/').slice(0, -1).join('/');
183
+ if (parentDir)
184
+ pathsToCheck.push(parentDir);
185
+ }
186
+ for (const checkPath of pathsToCheck) {
187
+ for (const root of roots) {
188
+ if (existsSync(join(root, checkPath))) {
189
+ hasAny = true;
190
+ break;
191
+ }
192
+ }
193
+ if (hasAny)
194
+ break;
130
195
  }
196
+ if (hasAny)
197
+ break;
131
198
  }
132
199
  if (!hasAny) {
133
200
  stale.push(family.id);