@yasserkhanorg/e2e-agents 1.7.4 → 1.7.6

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,5 +1,7 @@
1
1
  export interface GitChangeResult {
2
2
  files: string[];
3
+ /** Test/spec files from the diff that were filtered by isRelevantFile(). Only includes files matching TEST_FILE_PATTERNS — not config, docs, or other non-test filtered files. */
4
+ filteredTestFiles: string[];
3
5
  error?: string;
4
6
  baseRef?: string;
5
7
  baseStrategy?: 'merge-base' | 'direct';
@@ -1 +1 @@
1
- {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/agent/git.ts"],"names":[],"mappings":"AAkHA,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;CAC1C;AAED,MAAM,WAAW,gBAAgB;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAChC;AA8CD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe,CA2C3G"}
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/agent/git.ts"],"names":[],"mappings":"AAkHA,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,kLAAkL;IAClL,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;CAC1C;AAED,MAAM,WAAW,gBAAgB;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAChC;AA8CD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe,CA0D3G"}
package/dist/agent/git.js CHANGED
@@ -167,7 +167,7 @@ function getChangedFiles(appRoot, since, options) {
167
167
  const repoRoot = runGitRaw(['rev-parse', '--show-toplevel'], appRoot)?.trim() || appRoot;
168
168
  const diffFiles = runGit(['diff', '--name-only', `${baseRef}..HEAD`], repoRoot);
169
169
  if (!diffFiles) {
170
- return { files: [], error: 'git diff failed' };
170
+ return { files: [], filteredTestFiles: [], error: 'git diff failed' };
171
171
  }
172
172
  diffFiles.forEach((file) => files.add(file));
173
173
  if (options?.includeUncommitted) {
@@ -181,9 +181,25 @@ function getChangedFiles(appRoot, since, options) {
181
181
  parseStatusLines(statusLines).forEach((file) => files.add(file));
182
182
  }
183
183
  }
184
- return { files: Array.from(files).filter(isRelevantFile), baseRef, baseStrategy };
184
+ const allFiles = Array.from(files);
185
+ const relevant = [];
186
+ const filteredTestFiles = [];
187
+ for (const f of allFiles) {
188
+ if (isRelevantFile(f)) {
189
+ relevant.push(f);
190
+ }
191
+ else {
192
+ // Only capture files that were filtered because they match test patterns.
193
+ // Config, docs, workflow files etc. are not useful for PR-test detection.
194
+ const basename = f.split('/').pop() || f;
195
+ if (TEST_FILE_PATTERNS.some((p) => p.test(basename))) {
196
+ filteredTestFiles.push(f);
197
+ }
198
+ }
199
+ }
200
+ return { files: relevant, filteredTestFiles, baseRef, baseStrategy };
185
201
  }
186
202
  catch {
187
- return { files: [], error: 'git diff failed' };
203
+ return { files: [], filteredTestFiles: [], error: 'git diff failed' };
188
204
  }
189
205
  }
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAIA,OAAO,EAAgB,KAAK,eAAe,EAAC,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAEH,KAAK,UAAU,EAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAmC,KAAK,YAAY,EAAC,MAAM,2BAA2B,CAAC;AAU9F,OAAO,EAAqB,KAAK,kBAAkB,EAAC,MAAM,2BAA2B,CAAC;AAEtF,OAAO,EAAyB,KAAK,6BAA6B,EAAE,KAAK,4BAA4B,EAAC,MAAM,oBAAoB,CAAC;AACjI,OAAO,EAEH,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAChC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAGH,KAAK,yBAAyB,EACjC,MAAM,iCAAiC,CAAC;AAEzC,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,4BAA4B;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,yBAAyB,CAAC;CACvC;AAED,MAAM,WAAW,6BAA6B;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAcD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,6BAA6B,GAAG,4BAA4B,CAE1G;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,4BAA4B,GAAG,wBAAwB,CASlG;AAED,MAAM,WAAW,sBAAsB;IACnC,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,eAAoB,GAAG,YAAY,CAQtF;AAED,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,eAAoB,GAAG,sBAAsB,CAejG;AAED,wBAAsB,gBAAgB,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,sBAAsB,GAAG;IAAE,YAAY,CAAC,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAiD7I;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,6BAA6B,GAAG,yBAAyB,CAkBrG"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAIA,OAAO,EAAgB,KAAK,eAAe,EAAC,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAEH,KAAK,UAAU,EAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAmC,KAAK,YAAY,EAAC,MAAM,2BAA2B,CAAC;AAU9F,OAAO,EAAqB,KAAK,kBAAkB,EAAC,MAAM,2BAA2B,CAAC;AAEtF,OAAO,EAAyB,KAAK,6BAA6B,EAAE,KAAK,4BAA4B,EAAC,MAAM,oBAAoB,CAAC;AACjI,OAAO,EAEH,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAChC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAGH,KAAK,yBAAyB,EACjC,MAAM,iCAAiC,CAAC;AAEzC,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,4BAA4B;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,yBAAyB,CAAC;CACvC;AAED,MAAM,WAAW,6BAA6B;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAcD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,6BAA6B,GAAG,4BAA4B,CAE1G;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,4BAA4B,GAAG,wBAAwB,CASlG;AAED,MAAM,WAAW,sBAAsB;IACnC,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,eAAoB,GAAG,YAAY,CAStF;AAED,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,eAAoB,GAAG,sBAAsB,CAgBjG;AAED,wBAAsB,gBAAgB,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,sBAAsB,GAAG;IAAE,YAAY,CAAC,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAkD7I;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,6BAA6B,GAAG,yBAAyB,CAkBrG"}
package/dist/api.js CHANGED
@@ -51,6 +51,7 @@ function analyzeImpactDeterministic(options = {}) {
51
51
  return (0, impact_engine_js_1.analyzeImpact)(gitResult.files, {
52
52
  testsRoot: reportRoot,
53
53
  routeFamilies: config.routeFamilies,
54
+ filteredTestFiles: gitResult.filteredTestFiles,
54
55
  });
55
56
  }
56
57
  function recommendTestsDeterministic(options = {}) {
@@ -60,6 +61,7 @@ function recommendTestsDeterministic(options = {}) {
60
61
  const impact = (0, impact_engine_js_1.analyzeImpact)(gitResult.files, {
61
62
  testsRoot: reportRoot,
62
63
  routeFamilies: config.routeFamilies,
64
+ filteredTestFiles: gitResult.filteredTestFiles,
63
65
  });
64
66
  const adaptive = (0, feedback_js_1.getAdaptiveThresholds)(reportRoot);
65
67
  const plan = (0, plan_builder_js_1.buildPlanFromImpact)(impact, config.policy, undefined, adaptive);
@@ -76,6 +78,7 @@ async function recommendTestsAI(options = {}) {
76
78
  const impact = (0, impact_engine_js_1.analyzeImpact)(gitResult.files, {
77
79
  testsRoot: reportRoot,
78
80
  routeFamilies: config.routeFamilies,
81
+ filteredTestFiles: gitResult.filteredTestFiles,
79
82
  });
80
83
  const apiKey = process.env.ANTHROPIC_API_KEY;
81
84
  let aiEnrichment;
@@ -1 +1 @@
1
- {"version":3,"file":"ai_enrichment.d.ts","sourceRoot":"","sources":["../../src/engine/ai_enrichment.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,YAAY,EAAmB,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AACzF,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gCAAgC,CAAC;AAGpE,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IAC/B,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,mBAAmB,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IAClF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;CAC/C;AAED,MAAM,WAAW,mBAAmB;IAChC,mBAAmB,EAAE,YAAY,CAAC;IAClC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAmLD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkIlG"}
1
+ {"version":3,"file":"ai_enrichment.d.ts","sourceRoot":"","sources":["../../src/engine/ai_enrichment.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,YAAY,EAAmB,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AACzF,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gCAAgC,CAAC;AAGpE,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IAC/B,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,mBAAmB,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IAClF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;CAC/C;AAED,MAAM,WAAW,mBAAmB;IAChC,mBAAmB,EAAE,YAAY,CAAC;IAClC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAoLD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkIlG"}
@@ -41,7 +41,8 @@ function buildPrompt(options) {
41
41
  const specCount = feature.playwrightSpecs.length + feature.cypressSpecs.length;
42
42
  const specList2 = [...feature.playwrightSpecs, ...feature.cypressSpecs];
43
43
  const specsDisplay = specList2.length > 0 ? specList2.join(', ') : 'none';
44
- lines.push(`- familyId=${feature.familyId} ${featureIdPart} (${feature.priority}): ${specCount} files, coverage=${feature.coverageStatus}, specs=[${specsDisplay}]`);
44
+ const userFlowsDisplay = feature.userFlows.length > 0 ? ` userFlows=[${feature.userFlows.slice(0, 5).join(', ')}]` : '';
45
+ lines.push(`- familyId=${feature.familyId} ${featureIdPart} (${feature.priority}): ${specCount} specs, coverage=${feature.coverageStatus}, specs=[${specsDisplay}]${userFlowsDisplay}`);
45
46
  }
46
47
  lines.push('');
47
48
  // Unbound files
@@ -36,6 +36,8 @@ export interface ImpactEngineOptions {
36
36
  cypressRoot?: string;
37
37
  routeFamilies?: RouteFamiliesConfig;
38
38
  expandedFiles?: string[];
39
+ /** Test files that were filtered by the caller (e.g. isRelevantFile in git.ts). Used to detect PR-included E2E specs. */
40
+ filteredTestFiles?: string[];
39
41
  }
40
42
  /**
41
43
  * Extract describe/test/it titles from a spec file using regex.
@@ -43,13 +45,24 @@ export interface ImpactEngineOptions {
43
45
  */
44
46
  export declare function extractScenarios(filePath: string, framework: 'playwright' | 'cypress'): string[];
45
47
  export declare function analyzeImpact(changedFiles: string[], options: ImpactEngineOptions): ImpactResult;
48
+ export interface GapResult {
49
+ /** Active gaps that should be reported/enforced. */
50
+ gaps: ImpactedFeature[];
51
+ /** Family-level gaps suppressed because all their files are covered by specific feature matches. These should be promoted to advisory. */
52
+ suppressedGaps: ImpactedFeature[];
53
+ }
46
54
  /**
47
55
  * Get gaps: P0/P1 features with 'uncovered' status.
48
56
  *
49
57
  * Suppresses family-level (generic) gaps when ALL their changed files are
50
58
  * already covered by feature-level (specific) matches in other families.
51
- * This prevents double-counting when a file like `policies.tsx` matches both
52
- * a generic family (`config`) and a specific feature (`system_console/permissions`).
59
+ * Suppressed gaps are returned separately so the plan builder can promote
60
+ * them to advisory ("new behavior detected") on covered flows.
61
+ */
62
+ export declare function getGapsWithSuppressed(result: ImpactResult): GapResult;
63
+ /**
64
+ * Get gaps: P0/P1 features with 'uncovered' status.
65
+ * Convenience wrapper that returns only active gaps (backward-compatible).
53
66
  */
54
67
  export declare function getGaps(result: ImpactResult): ImpactedFeature[];
55
68
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"impact_engine.d.ts","sourceRoot":"","sources":["../../src/engine/impact_engine.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGR,eAAe,EAClB,MAAM,gCAAgC,CAAC;AASxC,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,oBAAoB,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,qBAAqB,EAAE,iBAAiB,EAAE,CAAC;IAC3C,kBAAkB,EAAE,iBAAiB,EAAE,CAAC;IACxC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5E,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oFAAoF;IACpF,mBAAmB,EAAE,UAAU,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AA+CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,EAAE,CAgBhG;AA0GD,wBAAgB,aAAa,CACzB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,mBAAmB,GAC7B,YAAY,CAoFd;AAYD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAuB/D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAItE"}
1
+ {"version":3,"file":"impact_engine.d.ts","sourceRoot":"","sources":["../../src/engine/impact_engine.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGR,eAAe,EAClB,MAAM,gCAAgC,CAAC;AASxC,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,oBAAoB,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,qBAAqB,EAAE,iBAAiB,EAAE,CAAC;IAC3C,kBAAkB,EAAE,iBAAiB,EAAE,CAAC;IACxC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5E,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oFAAoF;IACpF,mBAAmB,EAAE,UAAU,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,yHAAyH;IACzH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AA+CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,EAAE,CAgBhG;AA0GD,wBAAgB,aAAa,CACzB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,mBAAmB,GAC7B,YAAY,CAuFd;AAYD,MAAM,WAAW,SAAS;IACtB,oDAAoD;IACpD,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,0IAA0I;IAC1I,cAAc,EAAE,eAAe,EAAE,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,CA2BrE;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAE/D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAItE"}
@@ -4,6 +4,7 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.extractScenarios = extractScenarios;
6
6
  exports.analyzeImpact = analyzeImpact;
7
+ exports.getGapsWithSuppressed = getGapsWithSuppressed;
7
8
  exports.getGaps = getGaps;
8
9
  exports.getPartialGaps = getPartialGaps;
9
10
  const fs_1 = require("fs");
@@ -179,8 +180,11 @@ function classifyPrTestFiles(allFiles, sourceFiles) {
179
180
  function analyzeImpact(changedFiles, options) {
180
181
  const { testsRoot, routeFamilies } = options;
181
182
  const warnings = [];
182
- // Partition into source files and test files
183
- const allOriginalFiles = [...changedFiles];
183
+ // Partition into source files and test files.
184
+ // Combine: (a) test files already in changedFiles that isTestFile catches, and
185
+ // (b) test files pre-filtered by the caller (filteredTestFiles from git.ts).
186
+ const preFilteredTests = options.filteredTestFiles ?? [];
187
+ const allOriginalFiles = [...new Set([...changedFiles, ...preFilteredTests])];
184
188
  changedFiles = changedFiles.filter((f) => !isTestFile(f));
185
189
  const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
186
190
  // Load manifest
@@ -263,10 +267,10 @@ function inferCypressRoot(testsRoot) {
263
267
  *
264
268
  * Suppresses family-level (generic) gaps when ALL their changed files are
265
269
  * already covered by feature-level (specific) matches in other families.
266
- * This prevents double-counting when a file like `policies.tsx` matches both
267
- * a generic family (`config`) and a specific feature (`system_console/permissions`).
270
+ * Suppressed gaps are returned separately so the plan builder can promote
271
+ * them to advisory ("new behavior detected") on covered flows.
268
272
  */
269
- function getGaps(result) {
273
+ function getGapsWithSuppressed(result) {
270
274
  // Collect files that are covered via feature-level matches (more specific)
271
275
  const filesCoveredByFeatures = new Set();
272
276
  for (const f of result.impactedFeatures) {
@@ -276,18 +280,29 @@ function getGaps(result) {
276
280
  }
277
281
  }
278
282
  }
279
- return result.impactedFeatures.filter((f) => {
283
+ const gaps = [];
284
+ const suppressedGaps = [];
285
+ for (const f of result.impactedFeatures) {
280
286
  if (f.priority !== 'P0' && f.priority !== 'P1')
281
- return false;
287
+ continue;
282
288
  if (f.coverageStatus !== 'uncovered')
283
- return false;
289
+ continue;
284
290
  // Only suppress FAMILY-level gaps (no featureId = generic match).
285
- // If it's a feature-level gap, keep it — it's specific and intentional.
286
291
  if (!f.featureId && f.changedFiles.every((file) => filesCoveredByFeatures.has(file))) {
287
- return false;
292
+ suppressedGaps.push(f);
288
293
  }
289
- return true;
290
- });
294
+ else {
295
+ gaps.push(f);
296
+ }
297
+ }
298
+ return { gaps, suppressedGaps };
299
+ }
300
+ /**
301
+ * Get gaps: P0/P1 features with 'uncovered' status.
302
+ * Convenience wrapper that returns only active gaps (backward-compatible).
303
+ */
304
+ function getGaps(result) {
305
+ return getGapsWithSuppressed(result).gaps;
291
306
  }
292
307
  /**
293
308
  * Get partial gaps: P0/P1 features with 'partial' status (advisory).
@@ -1 +1 @@
1
- {"version":3,"file":"plan_builder.d.ts","sourceRoot":"","sources":["../../src/engine/plan_builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAErD,OAAO,KAAK,EAAC,YAAY,EAAkB,MAAM,oBAAoB,CAAC;AAEtE,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EACR,UAAU,EACV,SAAS,EACT,kBAAkB,EAIrB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAC,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAC,CAAC;AAgQxD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,YAAY,EACpB,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EACtC,YAAY,CAAC,EAAE,kBAAkB,EACjC,kBAAkB,CAAC,EAAE,kBAAkB,GACxC,UAAU,CAyKZ;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAMzE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA4IhE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,SAAiC,GAAG,MAAM,CAMvH"}
1
+ {"version":3,"file":"plan_builder.d.ts","sourceRoot":"","sources":["../../src/engine/plan_builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAErD,OAAO,KAAK,EAAC,YAAY,EAA8B,MAAM,oBAAoB,CAAC;AAGlF,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EACR,UAAU,EACV,SAAS,EACT,kBAAkB,EAIrB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAC,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAC,CAAC;AAsUxD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,YAAY,EACpB,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EACtC,YAAY,CAAC,EAAE,kBAAkB,EACjC,kBAAkB,CAAC,EAAE,kBAAkB,GACxC,UAAU,CA6LZ;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAMzE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA4IhE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,SAAiC,GAAG,MAAM,CAMvH"}
@@ -11,6 +11,7 @@ const path_1 = require("path");
11
11
  const minimatch_1 = require("minimatch");
12
12
  const test_path_js_1 = require("../agent/test_path.js");
13
13
  const impact_engine_js_1 = require("./impact_engine.js");
14
+ const route_families_js_1 = require("../knowledge/route_families.js");
14
15
  const DEFAULT_POLICY = {
15
16
  minConfidenceForTargeted: 60,
16
17
  safeMergeMinConfidence: 85,
@@ -111,17 +112,73 @@ function pickRunSet(impact, confidence, policy) {
111
112
  riskyFiles,
112
113
  };
113
114
  }
115
+ /**
116
+ * Check which gaps have matching PR-included E2E spec files by binding
117
+ * spec files to families via the manifest. Returns familyIds that are covered.
118
+ */
119
+ function matchPrSpecsToGaps(prTestFiles, gaps, testsRoot) {
120
+ const coveredFamilies = new Set();
121
+ const prE2ESpecs = prTestFiles.filter((t) => t.type === 'playwright' || t.type === 'cypress');
122
+ if (prE2ESpecs.length === 0 || !testsRoot) {
123
+ return coveredFamilies;
124
+ }
125
+ // Try to bind PR spec files to families via the manifest
126
+ const manifest = (0, route_families_js_1.loadRouteFamilyManifest)(testsRoot);
127
+ if (manifest) {
128
+ const specBindings = (0, route_families_js_1.bindFilesToFamilies)(prE2ESpecs.map((s) => s.file), manifest);
129
+ for (const sb of specBindings) {
130
+ for (const binding of sb.bindings) {
131
+ coveredFamilies.add(binding.family);
132
+ }
133
+ }
134
+ }
135
+ // Fallback heuristic: if manifest binding didn't match (common for Cypress specs
136
+ // in directories not mapped in the manifest), check path-based keyword overlap.
137
+ if (coveredFamilies.size === 0) {
138
+ const gapFamilyIds = new Set(gaps.map((g) => g.familyId));
139
+ for (const spec of prE2ESpecs) {
140
+ const specLower = spec.file.toLowerCase().replace(/[_\-/\\]/g, ' ');
141
+ for (const familyId of gapFamilyIds) {
142
+ // Check if the spec path contains the family name or related terms
143
+ if (specLower.includes(familyId.toLowerCase())) {
144
+ coveredFamilies.add(familyId);
145
+ }
146
+ }
147
+ }
148
+ }
149
+ return coveredFamilies;
150
+ }
114
151
  function buildDecision(impact, runSet, confidence, policy) {
115
- const gaps = (0, impact_engine_js_1.getGaps)(impact);
152
+ const gaps = (0, impact_engine_js_1.getGapsWithSuppressed)(impact).gaps;
116
153
  if (gaps.length > 0) {
117
- // Check if PR already includes E2E test files that likely cover the gaps
118
- const prE2ESpecCount = (impact.prIncludedTestFiles ?? [])
119
- .filter((t) => t.type === 'playwright' || t.type === 'cypress').length;
120
- if (prE2ESpecCount > 0) {
154
+ const prE2ESpecs = (impact.prIncludedTestFiles ?? [])
155
+ .filter((t) => t.type === 'playwright' || t.type === 'cypress');
156
+ if (prE2ESpecs.length > 0) {
157
+ // Bind PR specs to families — only soften gaps that have matching specs
158
+ const coveredFamilies = matchPrSpecsToGaps(impact.prIncludedTestFiles ?? [], gaps);
159
+ const uncoveredGaps = gaps.filter((g) => !coveredFamilies.has(g.familyId));
160
+ if (uncoveredGaps.length === 0) {
161
+ // ALL gaps have matching PR specs
162
+ return {
163
+ action: 'run-now',
164
+ title: 'Run now',
165
+ summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecs.length} E2E test file(s) covering them. Verify the new tests cover impacted flows.`,
166
+ };
167
+ }
168
+ if (uncoveredGaps.length < gaps.length) {
169
+ // SOME gaps covered by PR specs, others not
170
+ return {
171
+ action: 'must-add-tests',
172
+ title: 'Must add tests',
173
+ summary: `Detected ${gaps.length} coverage gap(s). PR includes E2E tests for ${gaps.length - uncoveredGaps.length}, but ${uncoveredGaps.length} flow(s) still need coverage.`,
174
+ };
175
+ }
176
+ // No gaps matched by PR specs — but PR still has E2E files.
177
+ // Soften to run-now since the developer is actively writing tests.
121
178
  return {
122
179
  action: 'run-now',
123
180
  title: 'Run now',
124
- summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecCount} E2E test file(s). Verify the new tests cover impacted flows.`,
181
+ summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecs.length} E2E test file(s). Verify the new tests cover impacted flows.`,
125
182
  };
126
183
  }
127
184
  return {
@@ -239,7 +296,7 @@ function buildPlanFromImpact(impact, policyOverride, aiEnrichment, adaptiveThres
239
296
  const runSetResult = pickRunSet(impact, confidence, policy);
240
297
  const decision = buildDecision(impact, runSetResult.runSet, confidence, policy);
241
298
  const enforcement = evaluateEnforcement(decision, policy);
242
- const gaps = (0, impact_engine_js_1.getGaps)(impact);
299
+ const { gaps, suppressedGaps } = (0, impact_engine_js_1.getGapsWithSuppressed)(impact);
243
300
  const partialGaps = (0, impact_engine_js_1.getPartialGaps)(impact);
244
301
  // Build two separate lookup maps from aiEnrichment: one by featureId, one by familyId.
245
302
  // The familyId map stores only the FIRST feature encountered to avoid last-write-wins collisions.
@@ -324,9 +381,27 @@ function buildPlanFromImpact(impact, policyOverride, aiEnrichment, adaptiveThres
324
381
  ? (aiFeatureByFeatureId.get(f.featureId) ?? aiFeatureByFamilyId.get(f.familyId))
325
382
  : aiFeatureByFamilyId.get(f.familyId);
326
383
  // Only surface advisory scenarios when AI found new behavior in this diff
327
- const advisoryScenarios = aiFeature?.aiMissingScenarios?.length
328
- ? aiFeature.aiMissingScenarios
384
+ let advisoryScenarios = aiFeature?.aiMissingScenarios?.length
385
+ ? [...aiFeature.aiMissingScenarios]
329
386
  : undefined;
387
+ // Promote suppressed gaps to advisory on covered flows that share changed files.
388
+ // When a family-level gap is suppressed (e.g. "post" because post.go is also in
389
+ // a covered feature like "channels/threads"), the behavioral change should appear
390
+ // here as "new behavior detected" instead of vanishing.
391
+ for (const sg of suppressedGaps) {
392
+ const sharedFiles = sg.changedFiles.filter((file) => f.changedFiles.includes(file));
393
+ if (sharedFiles.length > 0) {
394
+ const sgAi = sg.featureId
395
+ ? (aiFeatureByFeatureId.get(sg.featureId) ?? aiFeatureByFamilyId.get(sg.familyId))
396
+ : aiFeatureByFamilyId.get(sg.familyId);
397
+ const sgScenarios = sgAi?.aiMissingScenarios?.length
398
+ ? sgAi.aiMissingScenarios
399
+ : sg.userFlows.slice(0, 3);
400
+ if (sgScenarios.length > 0) {
401
+ advisoryScenarios = [...(advisoryScenarios || []), ...sgScenarios];
402
+ }
403
+ }
404
+ }
330
405
  return {
331
406
  id: featureLabel(f),
332
407
  name: featureLabel(f),
@@ -164,7 +164,7 @@ export function getChangedFiles(appRoot, since, options) {
164
164
  const repoRoot = runGitRaw(['rev-parse', '--show-toplevel'], appRoot)?.trim() || appRoot;
165
165
  const diffFiles = runGit(['diff', '--name-only', `${baseRef}..HEAD`], repoRoot);
166
166
  if (!diffFiles) {
167
- return { files: [], error: 'git diff failed' };
167
+ return { files: [], filteredTestFiles: [], error: 'git diff failed' };
168
168
  }
169
169
  diffFiles.forEach((file) => files.add(file));
170
170
  if (options?.includeUncommitted) {
@@ -178,9 +178,25 @@ export function getChangedFiles(appRoot, since, options) {
178
178
  parseStatusLines(statusLines).forEach((file) => files.add(file));
179
179
  }
180
180
  }
181
- return { files: Array.from(files).filter(isRelevantFile), baseRef, baseStrategy };
181
+ const allFiles = Array.from(files);
182
+ const relevant = [];
183
+ const filteredTestFiles = [];
184
+ for (const f of allFiles) {
185
+ if (isRelevantFile(f)) {
186
+ relevant.push(f);
187
+ }
188
+ else {
189
+ // Only capture files that were filtered because they match test patterns.
190
+ // Config, docs, workflow files etc. are not useful for PR-test detection.
191
+ const basename = f.split('/').pop() || f;
192
+ if (TEST_FILE_PATTERNS.some((p) => p.test(basename))) {
193
+ filteredTestFiles.push(f);
194
+ }
195
+ }
196
+ }
197
+ return { files: relevant, filteredTestFiles, baseRef, baseStrategy };
182
198
  }
183
199
  catch {
184
- return { files: [], error: 'git diff failed' };
200
+ return { files: [], filteredTestFiles: [], error: 'git diff failed' };
185
201
  }
186
202
  }
package/dist/esm/api.js CHANGED
@@ -43,6 +43,7 @@ export function analyzeImpactDeterministic(options = {}) {
43
43
  return analyzeImpactV2(gitResult.files, {
44
44
  testsRoot: reportRoot,
45
45
  routeFamilies: config.routeFamilies,
46
+ filteredTestFiles: gitResult.filteredTestFiles,
46
47
  });
47
48
  }
48
49
  export function recommendTestsDeterministic(options = {}) {
@@ -52,6 +53,7 @@ export function recommendTestsDeterministic(options = {}) {
52
53
  const impact = analyzeImpactV2(gitResult.files, {
53
54
  testsRoot: reportRoot,
54
55
  routeFamilies: config.routeFamilies,
56
+ filteredTestFiles: gitResult.filteredTestFiles,
55
57
  });
56
58
  const adaptive = getAdaptiveThresholds(reportRoot);
57
59
  const plan = buildPlanFromImpact(impact, config.policy, undefined, adaptive);
@@ -68,6 +70,7 @@ export async function recommendTestsAI(options = {}) {
68
70
  const impact = analyzeImpactV2(gitResult.files, {
69
71
  testsRoot: reportRoot,
70
72
  routeFamilies: config.routeFamilies,
73
+ filteredTestFiles: gitResult.filteredTestFiles,
71
74
  });
72
75
  const apiKey = process.env.ANTHROPIC_API_KEY;
73
76
  let aiEnrichment;
@@ -38,7 +38,8 @@ function buildPrompt(options) {
38
38
  const specCount = feature.playwrightSpecs.length + feature.cypressSpecs.length;
39
39
  const specList2 = [...feature.playwrightSpecs, ...feature.cypressSpecs];
40
40
  const specsDisplay = specList2.length > 0 ? specList2.join(', ') : 'none';
41
- lines.push(`- familyId=${feature.familyId} ${featureIdPart} (${feature.priority}): ${specCount} files, coverage=${feature.coverageStatus}, specs=[${specsDisplay}]`);
41
+ const userFlowsDisplay = feature.userFlows.length > 0 ? ` userFlows=[${feature.userFlows.slice(0, 5).join(', ')}]` : '';
42
+ lines.push(`- familyId=${feature.familyId} ${featureIdPart} (${feature.priority}): ${specCount} specs, coverage=${feature.coverageStatus}, specs=[${specsDisplay}]${userFlowsDisplay}`);
42
43
  }
43
44
  lines.push('');
44
45
  // Unbound files
@@ -173,8 +173,11 @@ function classifyPrTestFiles(allFiles, sourceFiles) {
173
173
  export function analyzeImpact(changedFiles, options) {
174
174
  const { testsRoot, routeFamilies } = options;
175
175
  const warnings = [];
176
- // Partition into source files and test files
177
- const allOriginalFiles = [...changedFiles];
176
+ // Partition into source files and test files.
177
+ // Combine: (a) test files already in changedFiles that isTestFile catches, and
178
+ // (b) test files pre-filtered by the caller (filteredTestFiles from git.ts).
179
+ const preFilteredTests = options.filteredTestFiles ?? [];
180
+ const allOriginalFiles = [...new Set([...changedFiles, ...preFilteredTests])];
178
181
  changedFiles = changedFiles.filter((f) => !isTestFile(f));
179
182
  const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
180
183
  // Load manifest
@@ -257,10 +260,10 @@ function inferCypressRoot(testsRoot) {
257
260
  *
258
261
  * Suppresses family-level (generic) gaps when ALL their changed files are
259
262
  * already covered by feature-level (specific) matches in other families.
260
- * This prevents double-counting when a file like `policies.tsx` matches both
261
- * a generic family (`config`) and a specific feature (`system_console/permissions`).
263
+ * Suppressed gaps are returned separately so the plan builder can promote
264
+ * them to advisory ("new behavior detected") on covered flows.
262
265
  */
263
- export function getGaps(result) {
266
+ export function getGapsWithSuppressed(result) {
264
267
  // Collect files that are covered via feature-level matches (more specific)
265
268
  const filesCoveredByFeatures = new Set();
266
269
  for (const f of result.impactedFeatures) {
@@ -270,18 +273,29 @@ export function getGaps(result) {
270
273
  }
271
274
  }
272
275
  }
273
- return result.impactedFeatures.filter((f) => {
276
+ const gaps = [];
277
+ const suppressedGaps = [];
278
+ for (const f of result.impactedFeatures) {
274
279
  if (f.priority !== 'P0' && f.priority !== 'P1')
275
- return false;
280
+ continue;
276
281
  if (f.coverageStatus !== 'uncovered')
277
- return false;
282
+ continue;
278
283
  // Only suppress FAMILY-level gaps (no featureId = generic match).
279
- // If it's a feature-level gap, keep it — it's specific and intentional.
280
284
  if (!f.featureId && f.changedFiles.every((file) => filesCoveredByFeatures.has(file))) {
281
- return false;
285
+ suppressedGaps.push(f);
282
286
  }
283
- return true;
284
- });
287
+ else {
288
+ gaps.push(f);
289
+ }
290
+ }
291
+ return { gaps, suppressedGaps };
292
+ }
293
+ /**
294
+ * Get gaps: P0/P1 features with 'uncovered' status.
295
+ * Convenience wrapper that returns only active gaps (backward-compatible).
296
+ */
297
+ export function getGaps(result) {
298
+ return getGapsWithSuppressed(result).gaps;
285
299
  }
286
300
  /**
287
301
  * Get partial gaps: P0/P1 features with 'partial' status (advisory).
@@ -4,7 +4,8 @@ import { mkdirSync, writeFileSync } from 'fs';
4
4
  import { dirname, join } from 'path';
5
5
  import { minimatch } from 'minimatch';
6
6
  import { inferSubsystemFromTestPath } from '../agent/test_path.js';
7
- import { getGaps, getPartialGaps } from './impact_engine.js';
7
+ import { getGaps, getGapsWithSuppressed, getPartialGaps } from './impact_engine.js';
8
+ import { bindFilesToFamilies, loadRouteFamilyManifest } from '../knowledge/route_families.js';
8
9
  const DEFAULT_POLICY = {
9
10
  minConfidenceForTargeted: 60,
10
11
  safeMergeMinConfidence: 85,
@@ -105,17 +106,73 @@ function pickRunSet(impact, confidence, policy) {
105
106
  riskyFiles,
106
107
  };
107
108
  }
109
+ /**
110
+ * Check which gaps have matching PR-included E2E spec files by binding
111
+ * spec files to families via the manifest. Returns familyIds that are covered.
112
+ */
113
+ function matchPrSpecsToGaps(prTestFiles, gaps, testsRoot) {
114
+ const coveredFamilies = new Set();
115
+ const prE2ESpecs = prTestFiles.filter((t) => t.type === 'playwright' || t.type === 'cypress');
116
+ if (prE2ESpecs.length === 0 || !testsRoot) {
117
+ return coveredFamilies;
118
+ }
119
+ // Try to bind PR spec files to families via the manifest
120
+ const manifest = loadRouteFamilyManifest(testsRoot);
121
+ if (manifest) {
122
+ const specBindings = bindFilesToFamilies(prE2ESpecs.map((s) => s.file), manifest);
123
+ for (const sb of specBindings) {
124
+ for (const binding of sb.bindings) {
125
+ coveredFamilies.add(binding.family);
126
+ }
127
+ }
128
+ }
129
+ // Fallback heuristic: if manifest binding didn't match (common for Cypress specs
130
+ // in directories not mapped in the manifest), check path-based keyword overlap.
131
+ if (coveredFamilies.size === 0) {
132
+ const gapFamilyIds = new Set(gaps.map((g) => g.familyId));
133
+ for (const spec of prE2ESpecs) {
134
+ const specLower = spec.file.toLowerCase().replace(/[_\-/\\]/g, ' ');
135
+ for (const familyId of gapFamilyIds) {
136
+ // Check if the spec path contains the family name or related terms
137
+ if (specLower.includes(familyId.toLowerCase())) {
138
+ coveredFamilies.add(familyId);
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return coveredFamilies;
144
+ }
108
145
  function buildDecision(impact, runSet, confidence, policy) {
109
- const gaps = getGaps(impact);
146
+ const gaps = getGapsWithSuppressed(impact).gaps;
110
147
  if (gaps.length > 0) {
111
- // Check if PR already includes E2E test files that likely cover the gaps
112
- const prE2ESpecCount = (impact.prIncludedTestFiles ?? [])
113
- .filter((t) => t.type === 'playwright' || t.type === 'cypress').length;
114
- if (prE2ESpecCount > 0) {
148
+ const prE2ESpecs = (impact.prIncludedTestFiles ?? [])
149
+ .filter((t) => t.type === 'playwright' || t.type === 'cypress');
150
+ if (prE2ESpecs.length > 0) {
151
+ // Bind PR specs to families — only soften gaps that have matching specs
152
+ const coveredFamilies = matchPrSpecsToGaps(impact.prIncludedTestFiles ?? [], gaps);
153
+ const uncoveredGaps = gaps.filter((g) => !coveredFamilies.has(g.familyId));
154
+ if (uncoveredGaps.length === 0) {
155
+ // ALL gaps have matching PR specs
156
+ return {
157
+ action: 'run-now',
158
+ title: 'Run now',
159
+ summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecs.length} E2E test file(s) covering them. Verify the new tests cover impacted flows.`,
160
+ };
161
+ }
162
+ if (uncoveredGaps.length < gaps.length) {
163
+ // SOME gaps covered by PR specs, others not
164
+ return {
165
+ action: 'must-add-tests',
166
+ title: 'Must add tests',
167
+ summary: `Detected ${gaps.length} coverage gap(s). PR includes E2E tests for ${gaps.length - uncoveredGaps.length}, but ${uncoveredGaps.length} flow(s) still need coverage.`,
168
+ };
169
+ }
170
+ // No gaps matched by PR specs — but PR still has E2E files.
171
+ // Soften to run-now since the developer is actively writing tests.
115
172
  return {
116
173
  action: 'run-now',
117
174
  title: 'Run now',
118
- summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecCount} E2E test file(s). Verify the new tests cover impacted flows.`,
175
+ summary: `Detected ${gaps.length} coverage gap(s), but the PR includes ${prE2ESpecs.length} E2E test file(s). Verify the new tests cover impacted flows.`,
119
176
  };
120
177
  }
121
178
  return {
@@ -233,7 +290,7 @@ export function buildPlanFromImpact(impact, policyOverride, aiEnrichment, adapti
233
290
  const runSetResult = pickRunSet(impact, confidence, policy);
234
291
  const decision = buildDecision(impact, runSetResult.runSet, confidence, policy);
235
292
  const enforcement = evaluateEnforcement(decision, policy);
236
- const gaps = getGaps(impact);
293
+ const { gaps, suppressedGaps } = getGapsWithSuppressed(impact);
237
294
  const partialGaps = getPartialGaps(impact);
238
295
  // Build two separate lookup maps from aiEnrichment: one by featureId, one by familyId.
239
296
  // The familyId map stores only the FIRST feature encountered to avoid last-write-wins collisions.
@@ -318,9 +375,27 @@ export function buildPlanFromImpact(impact, policyOverride, aiEnrichment, adapti
318
375
  ? (aiFeatureByFeatureId.get(f.featureId) ?? aiFeatureByFamilyId.get(f.familyId))
319
376
  : aiFeatureByFamilyId.get(f.familyId);
320
377
  // Only surface advisory scenarios when AI found new behavior in this diff
321
- const advisoryScenarios = aiFeature?.aiMissingScenarios?.length
322
- ? aiFeature.aiMissingScenarios
378
+ let advisoryScenarios = aiFeature?.aiMissingScenarios?.length
379
+ ? [...aiFeature.aiMissingScenarios]
323
380
  : undefined;
381
+ // Promote suppressed gaps to advisory on covered flows that share changed files.
382
+ // When a family-level gap is suppressed (e.g. "post" because post.go is also in
383
+ // a covered feature like "channels/threads"), the behavioral change should appear
384
+ // here as "new behavior detected" instead of vanishing.
385
+ for (const sg of suppressedGaps) {
386
+ const sharedFiles = sg.changedFiles.filter((file) => f.changedFiles.includes(file));
387
+ if (sharedFiles.length > 0) {
388
+ const sgAi = sg.featureId
389
+ ? (aiFeatureByFeatureId.get(sg.featureId) ?? aiFeatureByFamilyId.get(sg.familyId))
390
+ : aiFeatureByFamilyId.get(sg.familyId);
391
+ const sgScenarios = sgAi?.aiMissingScenarios?.length
392
+ ? sgAi.aiMissingScenarios
393
+ : sg.userFlows.slice(0, 3);
394
+ if (sgScenarios.length > 0) {
395
+ advisoryScenarios = [...(advisoryScenarios || []), ...sgScenarios];
396
+ }
397
+ }
398
+ }
324
399
  return {
325
400
  id: featureLabel(f),
326
401
  name: featureLabel(f),
package/dist/esm/index.js CHANGED
@@ -11,7 +11,7 @@ export { LLMProviderFactory, validateProviderSetup } from './provider_factory.js
11
11
  // Agent API (deterministic impact + plan, traceability)
12
12
  export { analyzeImpactDeterministic, recommendTestsDeterministic, handoffGeneratedTests, ingestTraceability, captureTraceability } from './api.js';
13
13
  // V2 Engine (deterministic impact + plan)
14
- export { analyzeImpact as analyzeImpactV2, getGaps, getPartialGaps } from './engine/impact_engine.js';
14
+ export { analyzeImpact as analyzeImpactV2, getGaps, getGapsWithSuppressed, getPartialGaps } from './engine/impact_engine.js';
15
15
  export { extractScenarios } from './engine/impact_engine.js';
16
16
  export { buildPlanFromImpact } from './engine/plan_builder.js';
17
17
  export { appendFeedbackAndRecompute, readCalibration, readFlakyTests, getAdaptiveThresholds } from './agent/feedback.js';
package/dist/index.d.ts CHANGED
@@ -20,8 +20,8 @@ export { LLMProviderFactory, validateProviderSetup } from './provider_factory.js
20
20
  export type { HybridConfig } from './provider_factory.js';
21
21
  export { analyzeImpactDeterministic, recommendTestsDeterministic, handoffGeneratedTests, ingestTraceability, captureTraceability } from './api.js';
22
22
  export type { AgentApiOptions, RecommendTestsV2Result, TraceabilityIngestApiOptions, TraceabilityCaptureApiOptions, } from './api.js';
23
- export { analyzeImpact as analyzeImpactV2, getGaps, getPartialGaps } from './engine/impact_engine.js';
24
- export type { ImpactResult, ImpactedFeature, CoverageStatus, ImpactEngineOptions, SpecWithScenarios, PrTestFile, PrTestFileType } from './engine/impact_engine.js';
23
+ export { analyzeImpact as analyzeImpactV2, getGaps, getGapsWithSuppressed, getPartialGaps } from './engine/impact_engine.js';
24
+ export type { ImpactResult, ImpactedFeature, CoverageStatus, ImpactEngineOptions, SpecWithScenarios, PrTestFile, PrTestFileType, GapResult } from './engine/impact_engine.js';
25
25
  export { extractScenarios } from './engine/impact_engine.js';
26
26
  export { buildPlanFromImpact } from './engine/plan_builder.js';
27
27
  export { appendFeedbackAndRecompute, readCalibration, readFlakyTests, getAdaptiveThresholds } from './agent/feedback.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
3
  // See LICENSE.txt for license information.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.scanProject = exports.runAgenticGeneration = exports.getSpecsForFamily = exports.buildSpecIndex = exports.loadOrBuildApiSurface = exports.buildApiSurface = exports.getUserFlowsForBinding = exports.getPriorityForBinding = exports.getCypressSpecDirsForBinding = exports.bindFilesToFamilies = exports.loadRouteFamilyManifest = exports.buildQualityFixPrompt = exports.buildHealPrompt = exports.renderHealMarkdown = exports.resolveHealTargets = exports.healFromReport = exports.runHealStage = exports.detectHallucinatedMethods = exports.parseGenerationResponse = exports.buildGenerationPrompt = exports.runGenerationStage = exports.runPipeline = exports.captureTraceabilityInput = exports.ingestTraceabilityInput = exports.finalizeGeneratedTests = exports.getAdaptiveThresholds = exports.readFlakyTests = exports.readCalibration = exports.appendFeedbackAndRecompute = exports.buildPlanFromImpact = exports.extractScenarios = exports.getPartialGaps = exports.getGaps = exports.analyzeImpactV2 = exports.captureTraceability = exports.ingestTraceability = exports.handoffGeneratedTests = exports.recommendTestsDeterministic = exports.analyzeImpactDeterministic = exports.validateProviderSetup = exports.LLMProviderFactory = exports.CustomProvider = exports.checkOpenAISetup = exports.OpenAIProvider = exports.checkOllamaSetup = exports.OllamaProvider = exports.checkAnthropicSetup = exports.AnthropicProvider = exports.UnsupportedCapabilityError = exports.LLMProviderError = void 0;
6
- exports.formatValidationReport = exports.buildValidationReport = exports.validateCommit = exports.getCommitFiles = exports.enrichFamilies = exports.detectStaleFamilies = exports.mergeFamilies = void 0;
5
+ exports.runAgenticGeneration = exports.getSpecsForFamily = exports.buildSpecIndex = exports.loadOrBuildApiSurface = exports.buildApiSurface = exports.getUserFlowsForBinding = exports.getPriorityForBinding = exports.getCypressSpecDirsForBinding = exports.bindFilesToFamilies = exports.loadRouteFamilyManifest = exports.buildQualityFixPrompt = exports.buildHealPrompt = exports.renderHealMarkdown = exports.resolveHealTargets = exports.healFromReport = exports.runHealStage = exports.detectHallucinatedMethods = exports.parseGenerationResponse = exports.buildGenerationPrompt = exports.runGenerationStage = exports.runPipeline = exports.captureTraceabilityInput = exports.ingestTraceabilityInput = exports.finalizeGeneratedTests = exports.getAdaptiveThresholds = exports.readFlakyTests = exports.readCalibration = exports.appendFeedbackAndRecompute = exports.buildPlanFromImpact = exports.extractScenarios = exports.getPartialGaps = exports.getGapsWithSuppressed = exports.getGaps = exports.analyzeImpactV2 = exports.captureTraceability = exports.ingestTraceability = exports.handoffGeneratedTests = exports.recommendTestsDeterministic = exports.analyzeImpactDeterministic = exports.validateProviderSetup = exports.LLMProviderFactory = exports.CustomProvider = exports.checkOpenAISetup = exports.OpenAIProvider = exports.checkOllamaSetup = exports.OllamaProvider = exports.checkAnthropicSetup = exports.AnthropicProvider = exports.UnsupportedCapabilityError = exports.LLMProviderError = void 0;
6
+ exports.formatValidationReport = exports.buildValidationReport = exports.validateCommit = exports.getCommitFiles = exports.enrichFamilies = exports.detectStaleFamilies = exports.mergeFamilies = exports.scanProject = void 0;
7
7
  var provider_interface_js_1 = require("./provider_interface.js");
8
8
  Object.defineProperty(exports, "LLMProviderError", { enumerable: true, get: function () { return provider_interface_js_1.LLMProviderError; } });
9
9
  Object.defineProperty(exports, "UnsupportedCapabilityError", { enumerable: true, get: function () { return provider_interface_js_1.UnsupportedCapabilityError; } });
@@ -34,6 +34,7 @@ Object.defineProperty(exports, "captureTraceability", { enumerable: true, get: f
34
34
  var impact_engine_js_1 = require("./engine/impact_engine.js");
35
35
  Object.defineProperty(exports, "analyzeImpactV2", { enumerable: true, get: function () { return impact_engine_js_1.analyzeImpact; } });
36
36
  Object.defineProperty(exports, "getGaps", { enumerable: true, get: function () { return impact_engine_js_1.getGaps; } });
37
+ Object.defineProperty(exports, "getGapsWithSuppressed", { enumerable: true, get: function () { return impact_engine_js_1.getGapsWithSuppressed; } });
37
38
  Object.defineProperty(exports, "getPartialGaps", { enumerable: true, get: function () { return impact_engine_js_1.getPartialGaps; } });
38
39
  var impact_engine_js_2 = require("./engine/impact_engine.js");
39
40
  Object.defineProperty(exports, "extractScenarios", { enumerable: true, get: function () { return impact_engine_js_2.extractScenarios; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasserkhanorg/e2e-agents",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "description": "AI-powered E2E test impact analysis, generation, and healing. Analyzes code changes to identify affected Playwright tests, detects coverage gaps, and generates or repairs specs using pluggable LLM providers (Claude, OpenAI, Ollama). Includes MCP server, traceability, and CI/CD integration.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",