@yasserkhanorg/e2e-agents 1.7.3 → 1.7.5

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
+ /** All files from the diff before relevance filtering (includes test files, config, etc.). */
4
+ allFiles?: 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,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,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,CA4C3G"}
package/dist/agent/git.js CHANGED
@@ -181,7 +181,8 @@ 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
+ return { files: allFiles.filter(isRelevantFile), allFiles, baseRef, baseStrategy };
185
186
  }
186
187
  catch {
187
188
  return { files: [], error: 'git diff failed' };
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
+ allFiles: gitResult.allFiles,
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
+ allFiles: gitResult.allFiles,
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
+ allFiles: gitResult.allFiles,
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;AAkLD;;;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;AAmLD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkIlG"}
@@ -89,8 +89,9 @@ function buildPrompt(options) {
89
89
  lines.push('');
90
90
  lines.push('Rules for missingScenarios:');
91
91
  lines.push('- Cross-reference the scenario titles in Existing Test Coverage. If a scenario already exists that covers the behavior, do NOT suggest it — instead list it in coveredBy.');
92
- lines.push('- For coverage=uncovered: list all scenarios the feature needs.');
92
+ lines.push('- For coverage=uncovered: list ONLY scenarios that test the SPECIFIC behavior change in this diff — NOT generic scenarios for the entire feature. A permission check fix needs permission-denial test scenarios, not general CRUD tests.');
93
93
  lines.push('- For coverage=covered or coverage=partial: ONLY list scenarios introduced by THIS diff that have NO matching scenario in existing coverage. If the diff adds no new user-visible behavior, return []. Do not pad with generic scenarios.');
94
+ lines.push('- If the source code diff is trivial (single-line fix, type change, field rename, or the PR is primarily adding tests), return missingScenarios=[] and explain in reasons that no new user-visible behavior was introduced.');
94
95
  lines.push('');
95
96
  lines.push(JSON.stringify({
96
97
  impactedFlows: [
@@ -99,10 +100,10 @@ function buildPrompt(options) {
99
100
  name: '<human-readable flow name>',
100
101
  priority: 'P0|P1|P2',
101
102
  reasons: [
102
- '<EXACTLY 1-2 sentences describing user-visible behavioral impact. Focus on what a user would observe or do differently — NOT file names, NOT implementation details.>',
103
+ '<EXACTLY 1-2 sentences explaining what SPECIFICALLY changed for users in THIS diff. Reference the actual behavior modification — NOT a general description of what the feature does. BAD: "Users can create and view posts." GOOD: "Users editing posts now require create_post permission, which may block edits for restricted roles.">',
103
104
  ],
104
105
  coveredBy: ['<spec file paths that cover this flow>'],
105
- missingScenarios: ['<concrete scenario title for a new or changed behavior introduced by THIS diff. E.g. "Thread popout preserves scroll position on reload">'],
106
+ missingScenarios: ['<concrete scenario title for a NEW or CHANGED behavior in THIS diff. Must be SPECIFIC to the code change — never generic feature tests. BAD: "User can create a new post". GOOD: "User without create_post permission sees error when editing a post". If the diff is trivial (typo, field rename, test-only) return [].>'],
106
107
  },
107
108
  ],
108
109
  unboundFileAnalysis: [
@@ -17,18 +17,27 @@ export interface ImpactedFeature {
17
17
  userFlows: string[];
18
18
  coverageStatus: CoverageStatus;
19
19
  }
20
+ export type PrTestFileType = 'playwright' | 'cypress' | 'unit' | 'snapshot';
21
+ export interface PrTestFile {
22
+ file: string;
23
+ type: PrTestFileType;
24
+ }
20
25
  export interface ImpactResult {
21
26
  changedFiles: string[];
22
27
  expandedFiles: string[];
23
28
  impactedFeatures: ImpactedFeature[];
24
29
  unboundFiles: string[];
25
30
  warnings: string[];
31
+ /** Test files that were in the original PR changeset but filtered from analysis. */
32
+ prIncludedTestFiles: PrTestFile[];
26
33
  }
27
34
  export interface ImpactEngineOptions {
28
35
  testsRoot: string;
29
36
  cypressRoot?: string;
30
37
  routeFamilies?: RouteFamiliesConfig;
31
38
  expandedFiles?: string[];
39
+ /** Full unfiltered file list from git diff (includes test files). Used to detect PR-included E2E specs. */
40
+ allFiles?: string[];
32
41
  }
33
42
  /**
34
43
  * Extract describe/test/it titles from a spec file using regex.
@@ -38,6 +47,11 @@ export declare function extractScenarios(filePath: string, framework: 'playwrigh
38
47
  export declare function analyzeImpact(changedFiles: string[], options: ImpactEngineOptions): ImpactResult;
39
48
  /**
40
49
  * Get gaps: P0/P1 features with 'uncovered' status.
50
+ *
51
+ * Suppresses family-level (generic) gaps when ALL their changed files are
52
+ * already covered by feature-level (specific) matches in other families.
53
+ * This prevents double-counting when a file like `policies.tsx` matches both
54
+ * a generic family (`config`) and a specific feature (`system_console/permissions`).
41
55
  */
42
56
  export declare function getGaps(result: ImpactResult): ImpactedFeature[];
43
57
  /**
@@ -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,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;CACtB;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;AAoFD,wBAAgB,aAAa,CACzB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,mBAAmB,GAC7B,YAAY,CAgFd;AAYD;;GAEG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAI/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,2GAA2G;IAC3G,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;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,CAwFd;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"}
@@ -150,16 +150,43 @@ function groupBindings(fileBindings) {
150
150
  function isTestFile(file) {
151
151
  const normalized = file.replace(/\\/g, '/');
152
152
  return /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(normalized) ||
153
+ /\.snap$/.test(normalized) ||
153
154
  /_test\.go$/.test(normalized) ||
154
155
  normalized.includes('__tests__/') ||
156
+ normalized.includes('__snapshots__/') ||
155
157
  normalized.includes('/tests/') ||
156
158
  normalized.includes('/test/');
157
159
  }
160
+ /** Classify filtered test files by type for downstream decision-making. */
161
+ function classifyPrTestFiles(allFiles, sourceFiles) {
162
+ const sourceSet = new Set(sourceFiles);
163
+ return allFiles
164
+ .filter((f) => !sourceSet.has(f))
165
+ .map((f) => {
166
+ const n = f.replace(/\\/g, '/');
167
+ if (/\.snap$/.test(n) || n.includes('__snapshots__/')) {
168
+ return { file: f, type: 'snapshot' };
169
+ }
170
+ if (/\.spec\.(ts|tsx|js|jsx)$/.test(n)) {
171
+ return { file: f, type: 'playwright' };
172
+ }
173
+ if (n.includes('/cypress/') && /\.(js|ts)$/.test(n)) {
174
+ return { file: f, type: 'cypress' };
175
+ }
176
+ return { file: f, type: 'unit' };
177
+ });
178
+ }
158
179
  function analyzeImpact(changedFiles, options) {
159
180
  const { testsRoot, routeFamilies } = options;
160
181
  const warnings = [];
161
- // Filter out test files before analysis
182
+ // Partition into source files and test files.
183
+ // When allFiles is provided (from git.ts before relevance filtering), use it
184
+ // to capture test files that were pre-filtered by the caller.
185
+ const allOriginalFiles = options.allFiles && options.allFiles.length > 0
186
+ ? [...new Set([...options.allFiles, ...changedFiles])]
187
+ : [...changedFiles];
162
188
  changedFiles = changedFiles.filter((f) => !isTestFile(f));
189
+ const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
163
190
  // Load manifest
164
191
  const manifest = (0, route_families_js_1.loadRouteFamilyManifest)(testsRoot, routeFamilies);
165
192
  if (!manifest) {
@@ -169,6 +196,7 @@ function analyzeImpact(changedFiles, options) {
169
196
  impactedFeatures: [],
170
197
  unboundFiles: [...changedFiles],
171
198
  warnings: ['Route family manifest not found. All files are unbound.'],
199
+ prIncludedTestFiles,
172
200
  };
173
201
  }
174
202
  // Combine original + expanded files
@@ -222,6 +250,7 @@ function analyzeImpact(changedFiles, options) {
222
250
  impactedFeatures,
223
251
  unboundFiles,
224
252
  warnings,
253
+ prIncludedTestFiles,
225
254
  };
226
255
  }
227
256
  function inferCypressRoot(testsRoot) {
@@ -235,9 +264,34 @@ function inferCypressRoot(testsRoot) {
235
264
  }
236
265
  /**
237
266
  * Get gaps: P0/P1 features with 'uncovered' status.
267
+ *
268
+ * Suppresses family-level (generic) gaps when ALL their changed files are
269
+ * already covered by feature-level (specific) matches in other families.
270
+ * This prevents double-counting when a file like `policies.tsx` matches both
271
+ * a generic family (`config`) and a specific feature (`system_console/permissions`).
238
272
  */
239
273
  function getGaps(result) {
240
- return result.impactedFeatures.filter((f) => (f.priority === 'P0' || f.priority === 'P1') && f.coverageStatus === 'uncovered');
274
+ // Collect files that are covered via feature-level matches (more specific)
275
+ const filesCoveredByFeatures = new Set();
276
+ for (const f of result.impactedFeatures) {
277
+ if (f.featureId && f.coverageStatus !== 'uncovered') {
278
+ for (const file of f.changedFiles) {
279
+ filesCoveredByFeatures.add(file);
280
+ }
281
+ }
282
+ }
283
+ return result.impactedFeatures.filter((f) => {
284
+ if (f.priority !== 'P0' && f.priority !== 'P1')
285
+ return false;
286
+ if (f.coverageStatus !== 'uncovered')
287
+ return false;
288
+ // Only suppress FAMILY-level gaps (no featureId = generic match).
289
+ // If it's a feature-level gap, keep it — it's specific and intentional.
290
+ if (!f.featureId && f.changedFiles.every((file) => filesCoveredByFeatures.has(file))) {
291
+ return false;
292
+ }
293
+ return true;
294
+ });
241
295
  }
242
296
  /**
243
297
  * 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;AAoPxD,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,CAwHhE;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,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"}
@@ -114,6 +114,16 @@ function pickRunSet(impact, confidence, policy) {
114
114
  function buildDecision(impact, runSet, confidence, policy) {
115
115
  const gaps = (0, impact_engine_js_1.getGaps)(impact);
116
116
  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) {
121
+ return {
122
+ action: 'run-now',
123
+ 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.`,
125
+ };
126
+ }
117
127
  return {
118
128
  action: 'must-add-tests',
119
129
  title: 'Must add tests',
@@ -381,6 +391,7 @@ function renderCiSummaryMarkdown(plan) {
381
391
  const lines = [];
382
392
  const { uncoveredP0P1Flows, changedFiles, impactedFlows, coveredFlows: coveredCount, partialFlows: partialCount, unboundFiles: unboundCount } = plan.metrics;
383
393
  const mustAddTests = plan.decision.action === 'must-add-tests';
394
+ const hasGapsButPrHasSpecs = !mustAddTests && plan.gapDetails.filter((g) => !g.name.includes('(partial)')).length > 0;
384
395
  const flowsWithAdvisory = plan.coveredFlows.filter((f) => f.advisoryScenarios && f.advisoryScenarios.length > 0);
385
396
  const cleanFlows = plan.coveredFlows.filter((f) => !f.advisoryScenarios || f.advisoryScenarios.length === 0);
386
397
  const statusEmoji = mustAddTests ? '🔴' : plan.decision.action === 'safe-to-merge' ? '🟢' : '🟡';
@@ -432,6 +443,24 @@ function renderCiSummaryMarkdown(plan) {
432
443
  }
433
444
  }
434
445
  }
446
+ // ── Informational gaps (PR includes E2E specs) ─────────────────────────────
447
+ if (hasGapsButPrHasSpecs) {
448
+ const infoGaps = plan.gapDetails.filter((g) => !g.name.includes('(partial)'));
449
+ lines.push('');
450
+ lines.push(`### ℹ️ Coverage gaps detected (PR includes E2E tests)`);
451
+ lines.push('');
452
+ lines.push('> The PR adds E2E test files. Verify they cover these flows:');
453
+ lines.push('');
454
+ for (const gap of infoGaps) {
455
+ const aiLabel = gap.source === 'ai+deterministic' ? ' ✦ AI-enriched' : '';
456
+ lines.push(`- **${gap.name}** · ${gap.priority}${aiLabel}`);
457
+ const aiReasons = gap.reasons.slice(1);
458
+ if (aiReasons.length > 0) {
459
+ lines.push(` ${aiReasons.join(' ')}`);
460
+ }
461
+ }
462
+ lines.push('');
463
+ }
435
464
  // ── Advisory: covered flows with new behavior ─────────────────────────────
436
465
  if (flowsWithAdvisory.length > 0) {
437
466
  lines.push('');
@@ -178,7 +178,8 @@ 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
+ return { files: allFiles.filter(isRelevantFile), allFiles, baseRef, baseStrategy };
182
183
  }
183
184
  catch {
184
185
  return { files: [], error: 'git diff failed' };
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
+ allFiles: gitResult.allFiles,
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
+ allFiles: gitResult.allFiles,
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
+ allFiles: gitResult.allFiles,
71
74
  });
72
75
  const apiKey = process.env.ANTHROPIC_API_KEY;
73
76
  let aiEnrichment;
@@ -86,8 +86,9 @@ function buildPrompt(options) {
86
86
  lines.push('');
87
87
  lines.push('Rules for missingScenarios:');
88
88
  lines.push('- Cross-reference the scenario titles in Existing Test Coverage. If a scenario already exists that covers the behavior, do NOT suggest it — instead list it in coveredBy.');
89
- lines.push('- For coverage=uncovered: list all scenarios the feature needs.');
89
+ lines.push('- For coverage=uncovered: list ONLY scenarios that test the SPECIFIC behavior change in this diff — NOT generic scenarios for the entire feature. A permission check fix needs permission-denial test scenarios, not general CRUD tests.');
90
90
  lines.push('- For coverage=covered or coverage=partial: ONLY list scenarios introduced by THIS diff that have NO matching scenario in existing coverage. If the diff adds no new user-visible behavior, return []. Do not pad with generic scenarios.');
91
+ lines.push('- If the source code diff is trivial (single-line fix, type change, field rename, or the PR is primarily adding tests), return missingScenarios=[] and explain in reasons that no new user-visible behavior was introduced.');
91
92
  lines.push('');
92
93
  lines.push(JSON.stringify({
93
94
  impactedFlows: [
@@ -96,10 +97,10 @@ function buildPrompt(options) {
96
97
  name: '<human-readable flow name>',
97
98
  priority: 'P0|P1|P2',
98
99
  reasons: [
99
- '<EXACTLY 1-2 sentences describing user-visible behavioral impact. Focus on what a user would observe or do differently — NOT file names, NOT implementation details.>',
100
+ '<EXACTLY 1-2 sentences explaining what SPECIFICALLY changed for users in THIS diff. Reference the actual behavior modification — NOT a general description of what the feature does. BAD: "Users can create and view posts." GOOD: "Users editing posts now require create_post permission, which may block edits for restricted roles.">',
100
101
  ],
101
102
  coveredBy: ['<spec file paths that cover this flow>'],
102
- missingScenarios: ['<concrete scenario title for a new or changed behavior introduced by THIS diff. E.g. "Thread popout preserves scroll position on reload">'],
103
+ missingScenarios: ['<concrete scenario title for a NEW or CHANGED behavior in THIS diff. Must be SPECIFIC to the code change — never generic feature tests. BAD: "User can create a new post". GOOD: "User without create_post permission sees error when editing a post". If the diff is trivial (typo, field rename, test-only) return [].>'],
103
104
  },
104
105
  ],
105
106
  unboundFileAnalysis: [
@@ -144,16 +144,43 @@ function groupBindings(fileBindings) {
144
144
  function isTestFile(file) {
145
145
  const normalized = file.replace(/\\/g, '/');
146
146
  return /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(normalized) ||
147
+ /\.snap$/.test(normalized) ||
147
148
  /_test\.go$/.test(normalized) ||
148
149
  normalized.includes('__tests__/') ||
150
+ normalized.includes('__snapshots__/') ||
149
151
  normalized.includes('/tests/') ||
150
152
  normalized.includes('/test/');
151
153
  }
154
+ /** Classify filtered test files by type for downstream decision-making. */
155
+ function classifyPrTestFiles(allFiles, sourceFiles) {
156
+ const sourceSet = new Set(sourceFiles);
157
+ return allFiles
158
+ .filter((f) => !sourceSet.has(f))
159
+ .map((f) => {
160
+ const n = f.replace(/\\/g, '/');
161
+ if (/\.snap$/.test(n) || n.includes('__snapshots__/')) {
162
+ return { file: f, type: 'snapshot' };
163
+ }
164
+ if (/\.spec\.(ts|tsx|js|jsx)$/.test(n)) {
165
+ return { file: f, type: 'playwright' };
166
+ }
167
+ if (n.includes('/cypress/') && /\.(js|ts)$/.test(n)) {
168
+ return { file: f, type: 'cypress' };
169
+ }
170
+ return { file: f, type: 'unit' };
171
+ });
172
+ }
152
173
  export function analyzeImpact(changedFiles, options) {
153
174
  const { testsRoot, routeFamilies } = options;
154
175
  const warnings = [];
155
- // Filter out test files before analysis
176
+ // Partition into source files and test files.
177
+ // When allFiles is provided (from git.ts before relevance filtering), use it
178
+ // to capture test files that were pre-filtered by the caller.
179
+ const allOriginalFiles = options.allFiles && options.allFiles.length > 0
180
+ ? [...new Set([...options.allFiles, ...changedFiles])]
181
+ : [...changedFiles];
156
182
  changedFiles = changedFiles.filter((f) => !isTestFile(f));
183
+ const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
157
184
  // Load manifest
158
185
  const manifest = loadRouteFamilyManifest(testsRoot, routeFamilies);
159
186
  if (!manifest) {
@@ -163,6 +190,7 @@ export function analyzeImpact(changedFiles, options) {
163
190
  impactedFeatures: [],
164
191
  unboundFiles: [...changedFiles],
165
192
  warnings: ['Route family manifest not found. All files are unbound.'],
193
+ prIncludedTestFiles,
166
194
  };
167
195
  }
168
196
  // Combine original + expanded files
@@ -216,6 +244,7 @@ export function analyzeImpact(changedFiles, options) {
216
244
  impactedFeatures,
217
245
  unboundFiles,
218
246
  warnings,
247
+ prIncludedTestFiles,
219
248
  };
220
249
  }
221
250
  function inferCypressRoot(testsRoot) {
@@ -229,9 +258,34 @@ function inferCypressRoot(testsRoot) {
229
258
  }
230
259
  /**
231
260
  * Get gaps: P0/P1 features with 'uncovered' status.
261
+ *
262
+ * Suppresses family-level (generic) gaps when ALL their changed files are
263
+ * already covered by feature-level (specific) matches in other families.
264
+ * This prevents double-counting when a file like `policies.tsx` matches both
265
+ * a generic family (`config`) and a specific feature (`system_console/permissions`).
232
266
  */
233
267
  export function getGaps(result) {
234
- return result.impactedFeatures.filter((f) => (f.priority === 'P0' || f.priority === 'P1') && f.coverageStatus === 'uncovered');
268
+ // Collect files that are covered via feature-level matches (more specific)
269
+ const filesCoveredByFeatures = new Set();
270
+ for (const f of result.impactedFeatures) {
271
+ if (f.featureId && f.coverageStatus !== 'uncovered') {
272
+ for (const file of f.changedFiles) {
273
+ filesCoveredByFeatures.add(file);
274
+ }
275
+ }
276
+ }
277
+ return result.impactedFeatures.filter((f) => {
278
+ if (f.priority !== 'P0' && f.priority !== 'P1')
279
+ return false;
280
+ if (f.coverageStatus !== 'uncovered')
281
+ return false;
282
+ // Only suppress FAMILY-level gaps (no featureId = generic match).
283
+ // If it's a feature-level gap, keep it — it's specific and intentional.
284
+ if (!f.featureId && f.changedFiles.every((file) => filesCoveredByFeatures.has(file))) {
285
+ return false;
286
+ }
287
+ return true;
288
+ });
235
289
  }
236
290
  /**
237
291
  * Get partial gaps: P0/P1 features with 'partial' status (advisory).
@@ -108,6 +108,16 @@ function pickRunSet(impact, confidence, policy) {
108
108
  function buildDecision(impact, runSet, confidence, policy) {
109
109
  const gaps = getGaps(impact);
110
110
  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) {
115
+ return {
116
+ action: 'run-now',
117
+ 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.`,
119
+ };
120
+ }
111
121
  return {
112
122
  action: 'must-add-tests',
113
123
  title: 'Must add tests',
@@ -375,6 +385,7 @@ export function renderCiSummaryMarkdown(plan) {
375
385
  const lines = [];
376
386
  const { uncoveredP0P1Flows, changedFiles, impactedFlows, coveredFlows: coveredCount, partialFlows: partialCount, unboundFiles: unboundCount } = plan.metrics;
377
387
  const mustAddTests = plan.decision.action === 'must-add-tests';
388
+ const hasGapsButPrHasSpecs = !mustAddTests && plan.gapDetails.filter((g) => !g.name.includes('(partial)')).length > 0;
378
389
  const flowsWithAdvisory = plan.coveredFlows.filter((f) => f.advisoryScenarios && f.advisoryScenarios.length > 0);
379
390
  const cleanFlows = plan.coveredFlows.filter((f) => !f.advisoryScenarios || f.advisoryScenarios.length === 0);
380
391
  const statusEmoji = mustAddTests ? '🔴' : plan.decision.action === 'safe-to-merge' ? '🟢' : '🟡';
@@ -426,6 +437,24 @@ export function renderCiSummaryMarkdown(plan) {
426
437
  }
427
438
  }
428
439
  }
440
+ // ── Informational gaps (PR includes E2E specs) ─────────────────────────────
441
+ if (hasGapsButPrHasSpecs) {
442
+ const infoGaps = plan.gapDetails.filter((g) => !g.name.includes('(partial)'));
443
+ lines.push('');
444
+ lines.push(`### ℹ️ Coverage gaps detected (PR includes E2E tests)`);
445
+ lines.push('');
446
+ lines.push('> The PR adds E2E test files. Verify they cover these flows:');
447
+ lines.push('');
448
+ for (const gap of infoGaps) {
449
+ const aiLabel = gap.source === 'ai+deterministic' ? ' ✦ AI-enriched' : '';
450
+ lines.push(`- **${gap.name}** · ${gap.priority}${aiLabel}`);
451
+ const aiReasons = gap.reasons.slice(1);
452
+ if (aiReasons.length > 0) {
453
+ lines.push(` ${aiReasons.join(' ')}`);
454
+ }
455
+ }
456
+ lines.push('');
457
+ }
429
458
  // ── Advisory: covered flows with new behavior ─────────────────────────────
430
459
  if (flowsWithAdvisory.length > 0) {
431
460
  lines.push('');
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@ 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
23
  export { analyzeImpact as analyzeImpactV2, getGaps, getPartialGaps } from './engine/impact_engine.js';
24
- export type { ImpactResult, ImpactedFeature, CoverageStatus, ImpactEngineOptions, SpecWithScenarios } from './engine/impact_engine.js';
24
+ export type { ImpactResult, ImpactedFeature, CoverageStatus, ImpactEngineOptions, SpecWithScenarios, PrTestFile, PrTestFileType } 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasserkhanorg/e2e-agents",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
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",