@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.
- package/dist/agent/config.d.ts +12 -0
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/config.js +32 -0
- package/dist/anthropic_provider.d.ts.map +1 -1
- package/dist/anthropic_provider.js +1 -0
- package/dist/cli.js +74 -0
- package/dist/esm/agent/config.js +32 -0
- package/dist/esm/anthropic_provider.js +1 -0
- package/dist/esm/cli.js +74 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/knowledge/api_surface.js +177 -0
- package/dist/esm/knowledge/context_loader.js +85 -0
- package/dist/esm/knowledge/route_families.js +211 -0
- package/dist/esm/knowledge/spec_index.js +122 -0
- package/dist/esm/pipeline/orchestrator.js +199 -0
- package/dist/esm/pipeline/stage0_preprocess.js +109 -0
- package/dist/esm/pipeline/stage1_impact.js +124 -0
- package/dist/esm/pipeline/stage2_coverage.js +131 -0
- package/dist/esm/pipeline/stage3_generation.js +146 -0
- package/dist/esm/prompts/coverage.js +64 -0
- package/dist/esm/prompts/generation.js +141 -0
- package/dist/esm/prompts/impact.js +82 -0
- package/dist/esm/validation/guardrails.js +95 -0
- package/dist/esm/validation/output_schema.js +80 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/knowledge/api_surface.d.ts +25 -0
- package/dist/knowledge/api_surface.d.ts.map +1 -0
- package/dist/knowledge/api_surface.js +184 -0
- package/dist/knowledge/context_loader.d.ts +13 -0
- package/dist/knowledge/context_loader.d.ts.map +1 -0
- package/dist/knowledge/context_loader.js +90 -0
- package/dist/knowledge/route_families.d.ts +48 -0
- package/dist/knowledge/route_families.d.ts.map +1 -0
- package/dist/knowledge/route_families.js +220 -0
- package/dist/knowledge/spec_index.d.ts +18 -0
- package/dist/knowledge/spec_index.d.ts.map +1 -0
- package/dist/knowledge/spec_index.js +128 -0
- package/dist/pipeline/orchestrator.d.ts +26 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -0
- package/dist/pipeline/orchestrator.js +202 -0
- package/dist/pipeline/stage0_preprocess.d.ts +31 -0
- package/dist/pipeline/stage0_preprocess.d.ts.map +1 -0
- package/dist/pipeline/stage0_preprocess.js +112 -0
- package/dist/pipeline/stage1_impact.d.ts +19 -0
- package/dist/pipeline/stage1_impact.d.ts.map +1 -0
- package/dist/pipeline/stage1_impact.js +127 -0
- package/dist/pipeline/stage2_coverage.d.ts +17 -0
- package/dist/pipeline/stage2_coverage.d.ts.map +1 -0
- package/dist/pipeline/stage2_coverage.js +134 -0
- package/dist/pipeline/stage3_generation.d.ts +31 -0
- package/dist/pipeline/stage3_generation.d.ts.map +1 -0
- package/dist/pipeline/stage3_generation.js +149 -0
- package/dist/prompts/coverage.d.ts +37 -0
- package/dist/prompts/coverage.d.ts.map +1 -0
- package/dist/prompts/coverage.js +68 -0
- package/dist/prompts/generation.d.ts +23 -0
- package/dist/prompts/generation.d.ts.map +1 -0
- package/dist/prompts/generation.js +146 -0
- package/dist/prompts/impact.d.ts +30 -0
- package/dist/prompts/impact.d.ts.map +1 -0
- package/dist/prompts/impact.js +86 -0
- package/dist/validation/guardrails.d.ts +27 -0
- package/dist/validation/guardrails.d.ts.map +1 -0
- package/dist/validation/guardrails.js +104 -0
- package/dist/validation/output_schema.d.ts +64 -0
- package/dist/validation/output_schema.d.ts.map +1 -0
- package/dist/validation/output_schema.js +84 -0
- package/package.json +3 -1
- package/schemas/flow-decision.schema.json +83 -0
- 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
|
+
}
|