@yasserkhanorg/e2e-agents 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/agent/config.d.ts +12 -0
  2. package/dist/agent/config.d.ts.map +1 -1
  3. package/dist/agent/config.js +32 -0
  4. package/dist/anthropic_provider.d.ts.map +1 -1
  5. package/dist/anthropic_provider.js +1 -0
  6. package/dist/cli.js +74 -0
  7. package/dist/esm/agent/config.js +32 -0
  8. package/dist/esm/anthropic_provider.js +1 -0
  9. package/dist/esm/cli.js +74 -0
  10. package/dist/esm/index.js +8 -0
  11. package/dist/esm/knowledge/api_surface.js +177 -0
  12. package/dist/esm/knowledge/context_loader.js +85 -0
  13. package/dist/esm/knowledge/route_families.js +211 -0
  14. package/dist/esm/knowledge/spec_index.js +122 -0
  15. package/dist/esm/pipeline/orchestrator.js +199 -0
  16. package/dist/esm/pipeline/stage0_preprocess.js +109 -0
  17. package/dist/esm/pipeline/stage1_impact.js +124 -0
  18. package/dist/esm/pipeline/stage2_coverage.js +131 -0
  19. package/dist/esm/pipeline/stage3_generation.js +146 -0
  20. package/dist/esm/prompts/coverage.js +64 -0
  21. package/dist/esm/prompts/generation.js +141 -0
  22. package/dist/esm/prompts/impact.js +82 -0
  23. package/dist/esm/validation/guardrails.js +95 -0
  24. package/dist/esm/validation/output_schema.js +80 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +20 -1
  28. package/dist/knowledge/api_surface.d.ts +25 -0
  29. package/dist/knowledge/api_surface.d.ts.map +1 -0
  30. package/dist/knowledge/api_surface.js +184 -0
  31. package/dist/knowledge/context_loader.d.ts +13 -0
  32. package/dist/knowledge/context_loader.d.ts.map +1 -0
  33. package/dist/knowledge/context_loader.js +90 -0
  34. package/dist/knowledge/route_families.d.ts +48 -0
  35. package/dist/knowledge/route_families.d.ts.map +1 -0
  36. package/dist/knowledge/route_families.js +220 -0
  37. package/dist/knowledge/spec_index.d.ts +18 -0
  38. package/dist/knowledge/spec_index.d.ts.map +1 -0
  39. package/dist/knowledge/spec_index.js +128 -0
  40. package/dist/pipeline/orchestrator.d.ts +26 -0
  41. package/dist/pipeline/orchestrator.d.ts.map +1 -0
  42. package/dist/pipeline/orchestrator.js +202 -0
  43. package/dist/pipeline/stage0_preprocess.d.ts +31 -0
  44. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -0
  45. package/dist/pipeline/stage0_preprocess.js +112 -0
  46. package/dist/pipeline/stage1_impact.d.ts +19 -0
  47. package/dist/pipeline/stage1_impact.d.ts.map +1 -0
  48. package/dist/pipeline/stage1_impact.js +127 -0
  49. package/dist/pipeline/stage2_coverage.d.ts +17 -0
  50. package/dist/pipeline/stage2_coverage.d.ts.map +1 -0
  51. package/dist/pipeline/stage2_coverage.js +134 -0
  52. package/dist/pipeline/stage3_generation.d.ts +31 -0
  53. package/dist/pipeline/stage3_generation.d.ts.map +1 -0
  54. package/dist/pipeline/stage3_generation.js +149 -0
  55. package/dist/prompts/coverage.d.ts +37 -0
  56. package/dist/prompts/coverage.d.ts.map +1 -0
  57. package/dist/prompts/coverage.js +68 -0
  58. package/dist/prompts/generation.d.ts +23 -0
  59. package/dist/prompts/generation.d.ts.map +1 -0
  60. package/dist/prompts/generation.js +146 -0
  61. package/dist/prompts/impact.d.ts +30 -0
  62. package/dist/prompts/impact.d.ts.map +1 -0
  63. package/dist/prompts/impact.js +86 -0
  64. package/dist/validation/guardrails.d.ts +27 -0
  65. package/dist/validation/guardrails.d.ts.map +1 -0
  66. package/dist/validation/guardrails.js +104 -0
  67. package/dist/validation/output_schema.d.ts +64 -0
  68. package/dist/validation/output_schema.d.ts.map +1 -0
  69. package/dist/validation/output_schema.js +84 -0
  70. package/package.json +3 -1
  71. package/schemas/flow-decision.schema.json +83 -0
  72. package/schemas/route-families.schema.json +107 -0
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ // See LICENSE.txt for license information.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.buildSpecIndex = buildSpecIndex;
6
+ exports.getSpecsForFamily = getSpecsForFamily;
7
+ exports.getSpecByPath = getSpecByPath;
8
+ exports.formatSpecsForPrompt = formatSpecsForPrompt;
9
+ const fs_1 = require("fs");
10
+ const path_1 = require("path");
11
+ function extractTestTitles(content) {
12
+ const titles = [];
13
+ const testRe = /\btest\s*\(\s*(['"`])((?:(?!\1).|\\.)*)\1/g;
14
+ let match;
15
+ while ((match = testRe.exec(content)) !== null) {
16
+ const title = match[2].trim();
17
+ if (title) {
18
+ titles.push(title);
19
+ }
20
+ }
21
+ return titles;
22
+ }
23
+ function extractTags(content) {
24
+ const tags = new Set();
25
+ const singleTagRe = /\btag:\s*['"`](@[\w-]+)['"`]/g;
26
+ let match;
27
+ while ((match = singleTagRe.exec(content)) !== null) {
28
+ tags.add(match[1]);
29
+ }
30
+ const arrayTagRe = /\btag:\s*\[([^\]]*)\]/g;
31
+ while ((match = arrayTagRe.exec(content)) !== null) {
32
+ const inner = match[1];
33
+ const tagRe = /['"`](@[\w-]+)['"`]/g;
34
+ let tagMatch;
35
+ while ((tagMatch = tagRe.exec(inner)) !== null) {
36
+ tags.add(tagMatch[1]);
37
+ }
38
+ }
39
+ return Array.from(tags);
40
+ }
41
+ function scanSpecDir(dir, testsRoot) {
42
+ const entries = [];
43
+ if (!(0, fs_1.existsSync)(dir)) {
44
+ return entries;
45
+ }
46
+ const items = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
47
+ for (const item of items) {
48
+ const fullPath = (0, path_1.join)(dir, item.name);
49
+ if (item.isDirectory()) {
50
+ entries.push(...scanSpecDir(fullPath, testsRoot));
51
+ continue;
52
+ }
53
+ if (!item.name.endsWith('.spec.ts') && !item.name.endsWith('.spec.tsx')) {
54
+ continue;
55
+ }
56
+ try {
57
+ const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
58
+ entries.push({
59
+ path: fullPath,
60
+ relativePath: (0, path_1.relative)(testsRoot, fullPath).replace(/\\/g, '/'),
61
+ testTitles: extractTestTitles(content),
62
+ tags: extractTags(content),
63
+ });
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ }
69
+ return entries;
70
+ }
71
+ function bindSpecToFamily(spec, manifest) {
72
+ const specPath = spec.relativePath;
73
+ for (const family of manifest.families) {
74
+ if (family.features) {
75
+ for (const feature of family.features) {
76
+ if (feature.specDirs?.some((dir) => specPath.startsWith(dir))) {
77
+ spec.familyId = family.id;
78
+ spec.featureId = feature.id;
79
+ return;
80
+ }
81
+ }
82
+ }
83
+ if (family.specDirs?.some((dir) => specPath.startsWith(dir))) {
84
+ spec.familyId = family.id;
85
+ return;
86
+ }
87
+ if (family.tags && spec.tags.some((t) => family.tags.includes(t))) {
88
+ spec.familyId = family.id;
89
+ return;
90
+ }
91
+ }
92
+ }
93
+ function buildSpecIndex(testsRoot, _specPatterns, manifest) {
94
+ const specsDir = (0, path_1.join)(testsRoot, 'specs');
95
+ const specs = scanSpecDir(specsDir, testsRoot);
96
+ if (manifest) {
97
+ for (const spec of specs) {
98
+ bindSpecToFamily(spec, manifest);
99
+ }
100
+ }
101
+ return {
102
+ specs,
103
+ indexedAt: new Date().toISOString(),
104
+ };
105
+ }
106
+ function getSpecsForFamily(index, familyId, featureId) {
107
+ return index.specs.filter((s) => {
108
+ if (s.familyId !== familyId) {
109
+ return false;
110
+ }
111
+ if (featureId && s.featureId && s.featureId !== featureId) {
112
+ return false;
113
+ }
114
+ return true;
115
+ });
116
+ }
117
+ function getSpecByPath(index, relativePath) {
118
+ return index.specs.find((s) => s.relativePath === relativePath);
119
+ }
120
+ function formatSpecsForPrompt(specs) {
121
+ return specs
122
+ .map((s) => {
123
+ const titles = s.testTitles.map((t) => ` - ${t}`).join('\n');
124
+ const tagsStr = s.tags.length > 0 ? ` [${s.tags.join(', ')}]` : '';
125
+ return `${s.relativePath}${tagsStr}\n${titles}`;
126
+ })
127
+ .join('\n\n');
128
+ }
@@ -0,0 +1,26 @@
1
+ import { type ImpactConfig } from './stage1_impact.js';
2
+ import { type CoverageConfig } from './stage2_coverage.js';
3
+ import { type GenerationConfig, type GeneratedSpec } from './stage3_generation.js';
4
+ import { type FlowDecisionReport } from '../validation/output_schema.js';
5
+ import type { RouteFamilyConfig } from '../knowledge/route_families.js';
6
+ import type { ApiSurfaceConfig } from '../knowledge/api_surface.js';
7
+ export interface PipelineConfig {
8
+ appPath: string;
9
+ testsRoot: string;
10
+ gitSince: string;
11
+ gitIncludeUncommitted?: boolean;
12
+ routeFamilies?: RouteFamilyConfig;
13
+ apiSurface?: ApiSurfaceConfig;
14
+ impact?: ImpactConfig;
15
+ coverage?: CoverageConfig;
16
+ generation?: GenerationConfig;
17
+ stages?: Array<'preprocess' | 'impact' | 'coverage' | 'generation'>;
18
+ }
19
+ export interface PipelineResult {
20
+ report: FlowDecisionReport;
21
+ reportPath: string;
22
+ warnings: string[];
23
+ generated?: GeneratedSpec[];
24
+ }
25
+ export declare function runPipeline(config: PipelineConfig): Promise<PipelineResult>;
26
+ //# sourceMappingURL=orchestrator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/pipeline/orchestrator.ts"],"names":[],"mappings":"AAOA,OAAO,EAAiB,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAmB,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAqB,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAAC,MAAM,wBAAwB,CAAC;AACrG,OAAO,EAAe,KAAK,kBAAkB,EAAoB,MAAM,gCAAgC,CAAC;AAExG,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,6BAA6B,CAAC;AAElE,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;CAC/B;AAoBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CA6GjF"}
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ // See LICENSE.txt for license information.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.runPipeline = runPipeline;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const git_js_1 = require("../agent/git.js");
9
+ const stage0_preprocess_js_1 = require("./stage0_preprocess.js");
10
+ const stage1_impact_js_1 = require("./stage1_impact.js");
11
+ const stage2_coverage_js_1 = require("./stage2_coverage.js");
12
+ const stage3_generation_js_1 = require("./stage3_generation.js");
13
+ const output_schema_js_1 = require("../validation/output_schema.js");
14
+ const guardrails_js_1 = require("../validation/guardrails.js");
15
+ function createRunId() {
16
+ const ciRunId = process.env.GITHUB_RUN_ID;
17
+ const entropy = Math.random().toString(36).slice(2, 8);
18
+ const ts = Date.now().toString(36);
19
+ if (ciRunId) {
20
+ return `pipeline-gh-${ciRunId}-${ts}-${entropy}`;
21
+ }
22
+ return `pipeline-local-${ts}-${entropy}`;
23
+ }
24
+ function isTestFile(file) {
25
+ const normalized = file.replace(/\\/g, '/');
26
+ return /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(normalized) ||
27
+ normalized.includes('__tests__/') ||
28
+ normalized.includes('/tests/') ||
29
+ normalized.includes('/test/');
30
+ }
31
+ async function runPipeline(config) {
32
+ const runId = createRunId();
33
+ const startedAt = new Date().toISOString();
34
+ const allWarnings = [];
35
+ const stages = config.stages || ['preprocess', 'impact', 'coverage'];
36
+ let generatedSpecs;
37
+ // Step 1: Get changed files
38
+ const gitResult = (0, git_js_1.getChangedFiles)(config.appPath, config.gitSince, {
39
+ includeUncommitted: config.gitIncludeUncommitted,
40
+ });
41
+ if (gitResult.error) {
42
+ allWarnings.push(`Git diff warning: ${gitResult.error}`);
43
+ }
44
+ const changedFiles = gitResult.files
45
+ .map((f) => f.replace(/\\/g, '/'))
46
+ .filter((f) => !isTestFile(f));
47
+ if (changedFiles.length === 0) {
48
+ allWarnings.push('No changed application files detected.');
49
+ const emptyReport = {
50
+ runId,
51
+ timestamp: startedAt,
52
+ gitRef: config.gitSince,
53
+ summary: (0, output_schema_js_1.buildSummary)([]),
54
+ decisions: [],
55
+ warnings: allWarnings,
56
+ model: {},
57
+ };
58
+ const reportPath = writeReport(config.testsRoot, emptyReport);
59
+ return { report: emptyReport, reportPath, warnings: allWarnings };
60
+ }
61
+ // Step 2: Preprocess — deterministic file classification + route family binding
62
+ const preprocessResult = (0, stage0_preprocess_js_1.preprocess)(changedFiles, {
63
+ appPath: config.appPath,
64
+ testsRoot: config.testsRoot,
65
+ routeFamilies: config.routeFamilies,
66
+ apiSurface: config.apiSurface,
67
+ });
68
+ allWarnings.push(...preprocessResult.warnings);
69
+ let decisions = [];
70
+ // Step 3: Impact stage — AI-powered flow identification per family
71
+ if (stages.includes('impact')) {
72
+ const impactResult = await (0, stage1_impact_js_1.runImpactStage)(preprocessResult.familyGroups, preprocessResult.manifest, preprocessResult.specIndex, preprocessResult.apiSurface, preprocessResult.context, config.impact || {});
73
+ decisions = impactResult.decisions;
74
+ allWarnings.push(...impactResult.warnings);
75
+ // Check cannot_determine ratio
76
+ const cannotDetermineRatio = (0, guardrails_js_1.computeCannotDetermineRatio)(decisions);
77
+ if (cannotDetermineRatio > 0.3) {
78
+ allWarnings.push(`High cannot_determine ratio (${(cannotDetermineRatio * 100).toFixed(0)}%). Consider updating route-families.json or running with MCP exploration.`);
79
+ }
80
+ }
81
+ // Step 4: Coverage stage — AI-powered spec coverage evaluation
82
+ if (stages.includes('coverage') && decisions.length > 0) {
83
+ const coverageResult = await (0, stage2_coverage_js_1.runCoverageStage)(decisions, preprocessResult.specIndex, preprocessResult.context, config.testsRoot, config.coverage || {});
84
+ decisions = coverageResult.decisions;
85
+ allWarnings.push(...coverageResult.warnings);
86
+ }
87
+ // Step 5: Generation stage — AI-powered spec generation for create_spec / add_scenarios
88
+ if (stages.includes('generation') && decisions.length > 0) {
89
+ const generationResult = await (0, stage3_generation_js_1.runGenerationStage)(decisions, preprocessResult.apiSurface, config.testsRoot, config.generation || {});
90
+ generatedSpecs = generationResult.generated;
91
+ allWarnings.push(...generationResult.warnings);
92
+ }
93
+ // Build report
94
+ const report = {
95
+ runId,
96
+ timestamp: startedAt,
97
+ gitRef: config.gitSince,
98
+ summary: (0, output_schema_js_1.buildSummary)(decisions),
99
+ decisions,
100
+ warnings: allWarnings,
101
+ model: {
102
+ impactAgent: config.impact?.provider || 'auto',
103
+ coverageAgent: config.coverage?.provider || 'auto',
104
+ generationAgent: stages.includes('generation') ? (config.generation?.provider || 'auto') : undefined,
105
+ },
106
+ };
107
+ const reportPath = writeReport(config.testsRoot, report);
108
+ return { report, reportPath, warnings: allWarnings, generated: generatedSpecs };
109
+ }
110
+ function writeReport(testsRoot, report) {
111
+ const outputDir = (0, path_1.join)(testsRoot, '.e2e-ai-agents');
112
+ if (!(0, fs_1.existsSync)(outputDir)) {
113
+ (0, fs_1.mkdirSync)(outputDir, { recursive: true });
114
+ }
115
+ const jsonPath = (0, path_1.join)(outputDir, 'pipeline-report.json');
116
+ (0, fs_1.writeFileSync)(jsonPath, JSON.stringify(report, null, 2), 'utf-8');
117
+ const mdPath = (0, path_1.join)(outputDir, 'pipeline-report.md');
118
+ (0, fs_1.writeFileSync)(mdPath, renderMarkdown(report), 'utf-8');
119
+ return jsonPath;
120
+ }
121
+ function renderMarkdown(report) {
122
+ const lines = [
123
+ `# Impact Analysis Pipeline Report`,
124
+ '',
125
+ `**Run ID:** ${report.runId}`,
126
+ `**Timestamp:** ${report.timestamp}`,
127
+ `**Git Ref:** ${report.gitRef}`,
128
+ '',
129
+ `## Summary`,
130
+ '',
131
+ `| Metric | Value |`,
132
+ `|--------|-------|`,
133
+ `| Changed Files | ${report.summary.changedFiles} |`,
134
+ `| Route Families Impacted | ${report.summary.routeFamiliesImpacted.join(', ') || 'none'} |`,
135
+ `| Flows Identified | ${report.summary.flowsIdentified} |`,
136
+ `| Covered | ${report.summary.flowsCovered} |`,
137
+ `| Partial | ${report.summary.flowsPartial} |`,
138
+ `| Uncovered | ${report.summary.flowsUncovered} |`,
139
+ `| Cannot Determine | ${report.summary.actionsRequired.cannot_determine} |`,
140
+ `| Overall Confidence | ${report.summary.overallConfidence} |`,
141
+ '',
142
+ ];
143
+ if (report.decisions.length > 0) {
144
+ lines.push('## Decisions', '');
145
+ for (const d of report.decisions) {
146
+ lines.push(`### ${d.flowName} (${d.priority})`);
147
+ lines.push('');
148
+ lines.push(`- **Action:** ${d.action}`);
149
+ lines.push(`- **Route Family:** ${d.routeFamily}${d.featureId ? ` / ${d.featureId}` : ''}`);
150
+ if (d.specificRoute) {
151
+ lines.push(`- **Route:** ${d.specificRoute}`);
152
+ }
153
+ lines.push(`- **Confidence:** ${d.confidence}%`);
154
+ lines.push(`- **Evidence:** ${d.evidence}`);
155
+ lines.push(`- **Changed Files:** ${d.changedFiles.join(', ')}`);
156
+ if (d.userActions.length > 0) {
157
+ lines.push(`- **User Actions:** ${d.userActions.join('; ')}`);
158
+ }
159
+ if (d.existingSpecs.length > 0) {
160
+ lines.push('- **Existing Coverage:**');
161
+ for (const spec of d.existingSpecs) {
162
+ lines.push(` - ${spec.path} (${spec.coverageLevel})`);
163
+ if (spec.testTitles.length > 0) {
164
+ for (const title of spec.testTitles) {
165
+ lines.push(` - \`${title}\``);
166
+ }
167
+ }
168
+ if (spec.missingScenarios && spec.missingScenarios.length > 0) {
169
+ lines.push(' - Missing:');
170
+ for (const scenario of spec.missingScenarios) {
171
+ lines.push(` - ${scenario}`);
172
+ }
173
+ }
174
+ }
175
+ }
176
+ if (d.scenariosToAdd && d.scenariosToAdd.length > 0) {
177
+ lines.push('- **Scenarios to Add:**');
178
+ for (const s of d.scenariosToAdd) {
179
+ lines.push(` - ${s}`);
180
+ }
181
+ }
182
+ if (d.targetSpec) {
183
+ lines.push(`- **Target Spec:** ${d.targetSpec}`);
184
+ }
185
+ if (d.newSpecPath) {
186
+ lines.push(`- **New Spec Path:** ${d.newSpecPath}`);
187
+ }
188
+ if (d.blockingReason) {
189
+ lines.push(`- **Blocking Reason:** ${d.blockingReason}`);
190
+ }
191
+ lines.push('');
192
+ }
193
+ }
194
+ if (report.warnings.length > 0) {
195
+ lines.push('## Warnings', '');
196
+ for (const w of report.warnings) {
197
+ lines.push(`- ${w}`);
198
+ }
199
+ lines.push('');
200
+ }
201
+ return lines.join('\n');
202
+ }
@@ -0,0 +1,31 @@
1
+ import { type FileBinding, type RouteFamilyConfig, type RouteFamilyManifest } from '../knowledge/route_families.js';
2
+ import { type ApiSurfaceCatalog, type ApiSurfaceConfig } from '../knowledge/api_surface.js';
3
+ import { type SpecIndex } from '../knowledge/spec_index.js';
4
+ import { type LoadedContext } from '../knowledge/context_loader.js';
5
+ export interface PreprocessConfig {
6
+ appPath: string;
7
+ testsRoot: string;
8
+ routeFamilies?: RouteFamilyConfig;
9
+ apiSurface?: ApiSurfaceConfig;
10
+ }
11
+ export interface FamilyGroup {
12
+ familyId: string;
13
+ featureId?: string;
14
+ files: Array<{
15
+ path: string;
16
+ snippet?: string;
17
+ }>;
18
+ }
19
+ export interface PreprocessResult {
20
+ changedFiles: string[];
21
+ fileBindings: FileBinding[];
22
+ unboundFiles: string[];
23
+ familyGroups: FamilyGroup[];
24
+ manifest: RouteFamilyManifest | null;
25
+ apiSurface: ApiSurfaceCatalog;
26
+ specIndex: SpecIndex;
27
+ context: LoadedContext;
28
+ warnings: string[];
29
+ }
30
+ export declare function preprocess(changedFiles: string[], config: PreprocessConfig): PreprocessResult;
31
+ //# sourceMappingURL=stage0_preprocess.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage0_preprocess.d.ts","sourceRoot":"","sources":["../../src/pipeline/stage0_preprocess.ts"],"names":[],"mappings":"AAKA,OAAO,EAGH,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAwB,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAC,MAAM,6BAA6B,CAAC;AACjH,OAAO,EAAiB,KAAK,SAAS,EAAC,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAuB,KAAK,aAAa,EAAC,MAAM,gCAAgC,CAAC;AAExF,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACrC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,SAAS,EAAE,SAAS,CAAC;IACrB,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AA0BD,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAsF7F"}
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ // See LICENSE.txt for license information.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.preprocess = preprocess;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const route_families_js_1 = require("../knowledge/route_families.js");
9
+ const api_surface_js_1 = require("../knowledge/api_surface.js");
10
+ const spec_index_js_1 = require("../knowledge/spec_index.js");
11
+ const context_loader_js_1 = require("../knowledge/context_loader.js");
12
+ const MAX_SNIPPET_CHARS = 3000;
13
+ const MAX_FILES_PER_GROUP = 30;
14
+ function loadFileSnippet(appPath, filePath) {
15
+ const candidates = [
16
+ (0, path_1.join)(appPath, filePath),
17
+ filePath,
18
+ ];
19
+ for (const candidate of candidates) {
20
+ if ((0, fs_1.existsSync)(candidate)) {
21
+ try {
22
+ const content = (0, fs_1.readFileSync)(candidate, 'utf-8');
23
+ if (content.length <= MAX_SNIPPET_CHARS) {
24
+ return content;
25
+ }
26
+ return content.slice(0, MAX_SNIPPET_CHARS) + '\n// ... truncated';
27
+ }
28
+ catch {
29
+ continue;
30
+ }
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+ function preprocess(changedFiles, config) {
36
+ const warnings = [];
37
+ // Load route family manifest
38
+ const manifest = (0, route_families_js_1.loadRouteFamilyManifest)(config.testsRoot, config.routeFamilies);
39
+ if (!manifest) {
40
+ warnings.push('Route family manifest not found. File-to-family binding will be skipped; AI will operate without route constraints.');
41
+ }
42
+ // Load API surface catalog
43
+ const apiSurface = (0, api_surface_js_1.loadOrBuildApiSurface)(config.testsRoot, config.apiSurface);
44
+ if (apiSurface.pageObjects.length === 0) {
45
+ warnings.push('API surface catalog is empty. Generated test validation will be limited.');
46
+ }
47
+ // Build spec index
48
+ const specIndex = (0, spec_index_js_1.buildSpecIndex)(config.testsRoot, undefined, manifest);
49
+ // Load context documents
50
+ const context = (0, context_loader_js_1.loadContextDocuments)(config.testsRoot, config.appPath);
51
+ warnings.push(...context.warnings);
52
+ // Bind files to families
53
+ let fileBindings = [];
54
+ let unboundFiles = [];
55
+ if (manifest) {
56
+ fileBindings = (0, route_families_js_1.bindFilesToFamilies)(changedFiles, manifest);
57
+ unboundFiles = fileBindings
58
+ .filter((fb) => fb.bindings.length === 0)
59
+ .map((fb) => fb.file);
60
+ if (unboundFiles.length > 0) {
61
+ warnings.push(`${unboundFiles.length} changed file(s) did not match any route family: ${unboundFiles.slice(0, 5).join(', ')}${unboundFiles.length > 5 ? '...' : ''}`);
62
+ }
63
+ }
64
+ else {
65
+ fileBindings = changedFiles.map((f) => ({ file: f, bindings: [] }));
66
+ unboundFiles = changedFiles;
67
+ }
68
+ // Group files by family+feature
69
+ const groupMap = new Map();
70
+ for (const binding of fileBindings) {
71
+ if (binding.bindings.length === 0) {
72
+ // Unbound files go into a special "unbound" group
73
+ const key = '__unbound__';
74
+ if (!groupMap.has(key)) {
75
+ groupMap.set(key, { familyId: '__unbound__', files: [] });
76
+ }
77
+ const group = groupMap.get(key);
78
+ if (group.files.length < MAX_FILES_PER_GROUP) {
79
+ group.files.push({
80
+ path: binding.file,
81
+ snippet: loadFileSnippet(config.appPath, binding.file),
82
+ });
83
+ }
84
+ continue;
85
+ }
86
+ for (const b of binding.bindings) {
87
+ const key = b.feature ? `${b.family}::${b.feature}` : b.family;
88
+ if (!groupMap.has(key)) {
89
+ groupMap.set(key, { familyId: b.family, featureId: b.feature, files: [] });
90
+ }
91
+ const group = groupMap.get(key);
92
+ if (group.files.length < MAX_FILES_PER_GROUP) {
93
+ group.files.push({
94
+ path: binding.file,
95
+ snippet: loadFileSnippet(config.appPath, binding.file),
96
+ });
97
+ }
98
+ }
99
+ }
100
+ const familyGroups = Array.from(groupMap.values()).filter((g) => g.familyId !== '__unbound__');
101
+ return {
102
+ changedFiles,
103
+ fileBindings,
104
+ unboundFiles,
105
+ familyGroups,
106
+ manifest,
107
+ apiSurface,
108
+ specIndex,
109
+ context,
110
+ warnings,
111
+ };
112
+ }
@@ -0,0 +1,19 @@
1
+ import { type RouteFamilyManifest } from '../knowledge/route_families.js';
2
+ import { type SpecIndex } from '../knowledge/spec_index.js';
3
+ import type { ApiSurfaceCatalog } from '../knowledge/api_surface.js';
4
+ import type { LoadedContext } from '../knowledge/context_loader.js';
5
+ import type { FamilyGroup } from './stage0_preprocess.js';
6
+ import type { FlowDecision } from '../validation/output_schema.js';
7
+ export interface ImpactConfig {
8
+ provider?: string;
9
+ maxTokens?: number;
10
+ temperature?: number;
11
+ timeout?: number;
12
+ }
13
+ export interface ImpactResult {
14
+ decisions: FlowDecision[];
15
+ warnings: string[];
16
+ providerName: string;
17
+ }
18
+ export declare function runImpactStage(familyGroups: FamilyGroup[], manifest: RouteFamilyManifest | null, specIndex: SpecIndex, apiSurface: ApiSurfaceCatalog, context: LoadedContext, config: ImpactConfig): Promise<ImpactResult>;
19
+ //# sourceMappingURL=stage1_impact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage1_impact.d.ts","sourceRoot":"","sources":["../../src/pipeline/stage1_impact.ts"],"names":[],"mappings":"AAOA,OAAO,EAAgB,KAAK,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AACvF,OAAO,EAAoB,KAAK,SAAS,EAAC,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gCAAgC,CAAC;AAClE,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAC,YAAY,EAA+B,MAAM,gCAAgC,CAAC;AAG/F,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACxB;AAgBD,wBAAsB,cAAc,CAChC,YAAY,EAAE,WAAW,EAAE,EAC3B,QAAQ,EAAE,mBAAmB,GAAG,IAAI,EACpC,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,iBAAiB,EAC7B,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY,GACrB,OAAO,CAAC,YAAY,CAAC,CAmHvB"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ // See LICENSE.txt for license information.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.runImpactStage = runImpactStage;
6
+ const provider_factory_js_1 = require("../provider_factory.js");
7
+ const impact_js_1 = require("../prompts/impact.js");
8
+ const context_loader_js_1 = require("../knowledge/context_loader.js");
9
+ const route_families_js_1 = require("../knowledge/route_families.js");
10
+ const spec_index_js_1 = require("../knowledge/spec_index.js");
11
+ const guardrails_js_1 = require("../validation/guardrails.js");
12
+ function normalizePriority(value) {
13
+ if (value === 'P0' || value === 'P1' || value === 'P2') {
14
+ return value;
15
+ }
16
+ return 'P2';
17
+ }
18
+ async function getProvider(config) {
19
+ if (config.provider && config.provider !== 'auto') {
20
+ return provider_factory_js_1.LLMProviderFactory.createFromString(config.provider);
21
+ }
22
+ return provider_factory_js_1.LLMProviderFactory.createFromEnv();
23
+ }
24
+ async function runImpactStage(familyGroups, manifest, specIndex, apiSurface, context, config) {
25
+ const warnings = [];
26
+ const allDecisions = [];
27
+ if (familyGroups.length === 0) {
28
+ warnings.push('No family groups to analyze. All changed files were unbound.');
29
+ return { decisions: [], warnings, providerName: 'none' };
30
+ }
31
+ let provider;
32
+ try {
33
+ provider = await getProvider(config);
34
+ }
35
+ catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ warnings.push(`Impact agent unavailable: ${message}`);
38
+ return { decisions: [], warnings, providerName: 'none' };
39
+ }
40
+ const contextBlock = (0, context_loader_js_1.formatContextForPrompt)(context);
41
+ for (const group of familyGroups) {
42
+ const family = manifest ? (0, route_families_js_1.getFamilyById)(manifest, group.familyId) : null;
43
+ if (!family) {
44
+ // For unbound groups, create cannot_determine decisions
45
+ for (const file of group.files) {
46
+ allDecisions.push({
47
+ flowId: `unbound_${file.path.replace(/[^a-zA-Z0-9]/g, '_')}`,
48
+ flowName: `Unbound file: ${file.path}`,
49
+ routeFamily: '__unbound__',
50
+ changedFiles: [file.path],
51
+ evidence: 'File does not match any known route family in the manifest.',
52
+ evidenceSource: 'deterministic',
53
+ confidence: 0,
54
+ existingSpecs: [],
55
+ action: 'cannot_determine',
56
+ blockingReason: 'File not mapped to any route family. Update route-families.json to include this file path.',
57
+ priority: 'P2',
58
+ userActions: [],
59
+ });
60
+ }
61
+ continue;
62
+ }
63
+ const specs = (0, spec_index_js_1.getSpecsForFamily)(specIndex, group.familyId, group.featureId);
64
+ const promptCtx = {
65
+ family,
66
+ featureId: group.featureId,
67
+ changedFiles: group.files,
68
+ existingSpecs: specs,
69
+ apiSurface,
70
+ contextBlock,
71
+ };
72
+ const prompt = (0, impact_js_1.buildImpactPrompt)(promptCtx);
73
+ try {
74
+ const response = await provider.generateText(prompt, {
75
+ maxTokens: config.maxTokens || 4000,
76
+ temperature: config.temperature ?? 0,
77
+ timeout: config.timeout || 45000,
78
+ systemPrompt: 'Return only valid JSON. Do not include markdown fences unless necessary.',
79
+ });
80
+ const parsed = (0, impact_js_1.parseImpactResponse)(response.text);
81
+ if (!parsed || parsed.flows.length === 0) {
82
+ warnings.push(`Impact agent returned no flows for family ${group.familyId}.`);
83
+ continue;
84
+ }
85
+ for (const flow of parsed.flows) {
86
+ if (!flow.id || !flow.changedFiles || !Array.isArray(flow.changedFiles)) {
87
+ continue;
88
+ }
89
+ const confidence = typeof flow.confidence === 'number'
90
+ ? Math.max(0, Math.min(100, flow.confidence))
91
+ : (0, guardrails_js_1.computeConfidence)({
92
+ hasRouteFamily: true,
93
+ hasSpecificRoute: Boolean(flow.route),
94
+ hasPageObject: Boolean(flow.pageObjects && flow.pageObjects.length > 0),
95
+ hasUserAction: Boolean(flow.userActions && flow.userActions.length > 0),
96
+ hasExistingSpecCited: false,
97
+ });
98
+ const decision = {
99
+ flowId: flow.id,
100
+ flowName: flow.name || flow.id,
101
+ routeFamily: group.familyId,
102
+ featureId: group.featureId,
103
+ specificRoute: flow.route,
104
+ changedFiles: flow.changedFiles.filter((f) => typeof f === 'string'),
105
+ evidence: flow.evidence || 'AI identified this flow as impacted.',
106
+ evidenceSource: 'ai',
107
+ confidence,
108
+ existingSpecs: [],
109
+ action: (0, guardrails_js_1.shouldForceCannotDetermine)(confidence) ? 'cannot_determine' : 'run_existing',
110
+ blockingReason: (0, guardrails_js_1.shouldForceCannotDetermine)(confidence) ? 'Confidence too low to determine action.' : undefined,
111
+ priority: normalizePriority(flow.priority),
112
+ userActions: Array.isArray(flow.userActions) ? flow.userActions.filter((a) => typeof a === 'string') : [],
113
+ };
114
+ allDecisions.push(decision);
115
+ }
116
+ }
117
+ catch (error) {
118
+ const message = error instanceof Error ? error.message : String(error);
119
+ warnings.push(`Impact agent failed for family ${group.familyId}: ${message}`);
120
+ }
121
+ }
122
+ return {
123
+ decisions: allDecisions,
124
+ warnings,
125
+ providerName: provider.name,
126
+ };
127
+ }