@yasserkhanorg/e2e-agents 0.3.8 → 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/cli.js +74 -0
- package/dist/esm/agent/config.js +32 -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,48 @@
|
|
|
1
|
+
export interface RouteFeature {
|
|
2
|
+
id: string;
|
|
3
|
+
routes?: string[];
|
|
4
|
+
webappPaths?: string[];
|
|
5
|
+
serverPaths?: string[];
|
|
6
|
+
specDirs?: string[];
|
|
7
|
+
tags?: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface RouteFamily {
|
|
10
|
+
id: string;
|
|
11
|
+
routes: string[];
|
|
12
|
+
pageObjects?: string[];
|
|
13
|
+
components?: string[];
|
|
14
|
+
webappPaths?: string[];
|
|
15
|
+
serverPaths?: string[];
|
|
16
|
+
specDirs?: string[];
|
|
17
|
+
tags?: string[];
|
|
18
|
+
features?: RouteFeature[];
|
|
19
|
+
}
|
|
20
|
+
export interface RouteFamilyManifest {
|
|
21
|
+
families: RouteFamily[];
|
|
22
|
+
source: string;
|
|
23
|
+
}
|
|
24
|
+
export interface FileBinding {
|
|
25
|
+
file: string;
|
|
26
|
+
bindings: Array<{
|
|
27
|
+
family: string;
|
|
28
|
+
feature?: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export interface RouteFamilyConfig {
|
|
32
|
+
manifestPath?: string;
|
|
33
|
+
strict?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function loadRouteFamilyManifest(testsRoot: string, config?: RouteFamilyConfig): RouteFamilyManifest | null;
|
|
36
|
+
export declare function bindFilesToFamilies(changedFiles: string[], manifest: RouteFamilyManifest): FileBinding[];
|
|
37
|
+
export declare function getFamilyById(manifest: RouteFamilyManifest, familyId: string): RouteFamily | undefined;
|
|
38
|
+
export declare function getFeatureById(family: RouteFamily, featureId: string): RouteFeature | undefined;
|
|
39
|
+
export declare function getSpecDirsForBinding(manifest: RouteFamilyManifest, binding: {
|
|
40
|
+
family: string;
|
|
41
|
+
feature?: string;
|
|
42
|
+
}): string[];
|
|
43
|
+
export declare function getRoutesForBinding(manifest: RouteFamilyManifest, binding: {
|
|
44
|
+
family: string;
|
|
45
|
+
feature?: string;
|
|
46
|
+
}): string[];
|
|
47
|
+
export declare function clearManifestCache(): void;
|
|
48
|
+
//# sourceMappingURL=route_families.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route_families.d.ts","sourceRoot":"","sources":["../../src/knowledge/route_families.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAChC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,iBAAiB;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AA6GD,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,IAAI,CAyCjH;AAED,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,mBAAmB,GAAG,WAAW,EAAE,CAsCxG;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEtG;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAE/F;AAED,wBAAgB,qBAAqB,CACjC,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAC,GAC5C,MAAM,EAAE,CAYV;AAED,wBAAgB,mBAAmB,CAC/B,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAC,GAC5C,MAAM,EAAE,CAYV;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -0,0 +1,220 @@
|
|
|
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.loadRouteFamilyManifest = loadRouteFamilyManifest;
|
|
6
|
+
exports.bindFilesToFamilies = bindFilesToFamilies;
|
|
7
|
+
exports.getFamilyById = getFamilyById;
|
|
8
|
+
exports.getFeatureById = getFeatureById;
|
|
9
|
+
exports.getSpecDirsForBinding = getSpecDirsForBinding;
|
|
10
|
+
exports.getRoutesForBinding = getRoutesForBinding;
|
|
11
|
+
exports.clearManifestCache = clearManifestCache;
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const manifestCache = new Map();
|
|
15
|
+
function matchesGlob(filePath, pattern) {
|
|
16
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
17
|
+
const parts = pattern.replace(/\\/g, '/').split('*');
|
|
18
|
+
if (parts.length === 1) {
|
|
19
|
+
return normalized === parts[0] || normalized.startsWith(parts[0]);
|
|
20
|
+
}
|
|
21
|
+
let pos = 0;
|
|
22
|
+
for (let i = 0; i < parts.length; i++) {
|
|
23
|
+
const part = parts[i];
|
|
24
|
+
if (part === '') {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const idx = normalized.indexOf(part, pos);
|
|
28
|
+
if (idx < 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (i === 0 && idx !== 0) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
pos = idx + part.length;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
function matchesAnyPattern(filePath, patterns) {
|
|
39
|
+
return patterns.some((pattern) => matchesGlob(filePath, pattern));
|
|
40
|
+
}
|
|
41
|
+
function validateFamily(family) {
|
|
42
|
+
if (!family || typeof family !== 'object') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const obj = family;
|
|
46
|
+
if (typeof obj.id !== 'string' || !obj.id.trim()) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(obj.routes) || obj.routes.length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const routes = obj.routes.filter((r) => typeof r === 'string');
|
|
53
|
+
if (routes.length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const result = {
|
|
57
|
+
id: obj.id.trim(),
|
|
58
|
+
routes,
|
|
59
|
+
};
|
|
60
|
+
if (Array.isArray(obj.pageObjects)) {
|
|
61
|
+
result.pageObjects = obj.pageObjects.filter((v) => typeof v === 'string');
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(obj.components)) {
|
|
64
|
+
result.components = obj.components.filter((v) => typeof v === 'string');
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(obj.webappPaths)) {
|
|
67
|
+
result.webappPaths = obj.webappPaths.filter((v) => typeof v === 'string');
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(obj.serverPaths)) {
|
|
70
|
+
result.serverPaths = obj.serverPaths.filter((v) => typeof v === 'string');
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(obj.specDirs)) {
|
|
73
|
+
result.specDirs = obj.specDirs.filter((v) => typeof v === 'string');
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(obj.tags)) {
|
|
76
|
+
result.tags = obj.tags.filter((v) => typeof v === 'string');
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(obj.features)) {
|
|
79
|
+
result.features = obj.features
|
|
80
|
+
.map((f) => validateFeature(f))
|
|
81
|
+
.filter((f) => f !== null);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
function validateFeature(feature) {
|
|
86
|
+
if (!feature || typeof feature !== 'object') {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const obj = feature;
|
|
90
|
+
if (typeof obj.id !== 'string' || !obj.id.trim()) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const result = { id: obj.id.trim() };
|
|
94
|
+
if (Array.isArray(obj.routes)) {
|
|
95
|
+
result.routes = obj.routes.filter((v) => typeof v === 'string');
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(obj.webappPaths)) {
|
|
98
|
+
result.webappPaths = obj.webappPaths.filter((v) => typeof v === 'string');
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(obj.serverPaths)) {
|
|
101
|
+
result.serverPaths = obj.serverPaths.filter((v) => typeof v === 'string');
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(obj.specDirs)) {
|
|
104
|
+
result.specDirs = obj.specDirs.filter((v) => typeof v === 'string');
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(obj.tags)) {
|
|
107
|
+
result.tags = obj.tags.filter((v) => typeof v === 'string');
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
function loadRouteFamilyManifest(testsRoot, config) {
|
|
112
|
+
const candidates = [];
|
|
113
|
+
if (config?.manifestPath) {
|
|
114
|
+
candidates.push(config.manifestPath);
|
|
115
|
+
}
|
|
116
|
+
candidates.push((0, path_1.join)(testsRoot, '.e2e-ai-agents', 'route-families.json'));
|
|
117
|
+
for (const candidate of candidates) {
|
|
118
|
+
try {
|
|
119
|
+
if (!(0, fs_1.existsSync)(candidate)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const mtimeMs = (0, fs_1.statSync)(candidate).mtimeMs;
|
|
123
|
+
const cached = manifestCache.get(candidate);
|
|
124
|
+
if (cached && cached.mtimeMs === mtimeMs && cached.manifest) {
|
|
125
|
+
return cached.manifest;
|
|
126
|
+
}
|
|
127
|
+
const raw = JSON.parse((0, fs_1.readFileSync)(candidate, 'utf-8'));
|
|
128
|
+
if (!raw.families || !Array.isArray(raw.families)) {
|
|
129
|
+
manifestCache.set(candidate, { mtimeMs, manifest: null });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const families = raw.families
|
|
133
|
+
.map((f) => validateFamily(f))
|
|
134
|
+
.filter((f) => f !== null);
|
|
135
|
+
if (families.length === 0) {
|
|
136
|
+
manifestCache.set(candidate, { mtimeMs, manifest: null });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const manifest = { families, source: candidate };
|
|
140
|
+
manifestCache.set(candidate, { mtimeMs, manifest });
|
|
141
|
+
return manifest;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (config?.strict) {
|
|
148
|
+
throw new Error('Route family manifest is required but not found. Create .e2e-ai-agents/route-families.json');
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function bindFilesToFamilies(changedFiles, manifest) {
|
|
153
|
+
return changedFiles.map((file) => {
|
|
154
|
+
const normalized = file.replace(/\\/g, '/');
|
|
155
|
+
const bindings = [];
|
|
156
|
+
for (const family of manifest.families) {
|
|
157
|
+
const featureBindings = [];
|
|
158
|
+
// Check feature-level first (more specific)
|
|
159
|
+
if (family.features) {
|
|
160
|
+
for (const feature of family.features) {
|
|
161
|
+
const featurePatterns = [
|
|
162
|
+
...(feature.webappPaths || []),
|
|
163
|
+
...(feature.serverPaths || []),
|
|
164
|
+
];
|
|
165
|
+
if (featurePatterns.length > 0 && matchesAnyPattern(normalized, featurePatterns)) {
|
|
166
|
+
featureBindings.push({ family: family.id, feature: feature.id });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (featureBindings.length > 0) {
|
|
171
|
+
bindings.push(...featureBindings);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Fall back to family-level patterns
|
|
175
|
+
const familyPatterns = [
|
|
176
|
+
...(family.webappPaths || []),
|
|
177
|
+
...(family.serverPaths || []),
|
|
178
|
+
];
|
|
179
|
+
if (familyPatterns.length > 0 && matchesAnyPattern(normalized, familyPatterns)) {
|
|
180
|
+
bindings.push({ family: family.id });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { file: normalized, bindings };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function getFamilyById(manifest, familyId) {
|
|
187
|
+
return manifest.families.find((f) => f.id === familyId);
|
|
188
|
+
}
|
|
189
|
+
function getFeatureById(family, featureId) {
|
|
190
|
+
return family.features?.find((f) => f.id === featureId);
|
|
191
|
+
}
|
|
192
|
+
function getSpecDirsForBinding(manifest, binding) {
|
|
193
|
+
const family = getFamilyById(manifest, binding.family);
|
|
194
|
+
if (!family) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
if (binding.feature) {
|
|
198
|
+
const feature = getFeatureById(family, binding.feature);
|
|
199
|
+
if (feature?.specDirs && feature.specDirs.length > 0) {
|
|
200
|
+
return feature.specDirs;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return family.specDirs || [];
|
|
204
|
+
}
|
|
205
|
+
function getRoutesForBinding(manifest, binding) {
|
|
206
|
+
const family = getFamilyById(manifest, binding.family);
|
|
207
|
+
if (!family) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
if (binding.feature) {
|
|
211
|
+
const feature = getFeatureById(family, binding.feature);
|
|
212
|
+
if (feature?.routes && feature.routes.length > 0) {
|
|
213
|
+
return feature.routes;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return family.routes;
|
|
217
|
+
}
|
|
218
|
+
function clearManifestCache() {
|
|
219
|
+
manifestCache.clear();
|
|
220
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RouteFamilyManifest } from './route_families.js';
|
|
2
|
+
export interface SpecEntry {
|
|
3
|
+
path: string;
|
|
4
|
+
relativePath: string;
|
|
5
|
+
testTitles: string[];
|
|
6
|
+
tags: string[];
|
|
7
|
+
familyId?: string;
|
|
8
|
+
featureId?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SpecIndex {
|
|
11
|
+
specs: SpecEntry[];
|
|
12
|
+
indexedAt: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildSpecIndex(testsRoot: string, _specPatterns?: string[], manifest?: RouteFamilyManifest | null): SpecIndex;
|
|
15
|
+
export declare function getSpecsForFamily(index: SpecIndex, familyId: string, featureId?: string): SpecEntry[];
|
|
16
|
+
export declare function getSpecByPath(index: SpecIndex, relativePath: string): SpecEntry | undefined;
|
|
17
|
+
export declare function formatSpecsForPrompt(specs: SpecEntry[]): string;
|
|
18
|
+
//# sourceMappingURL=spec_index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec_index.d.ts","sourceRoot":"","sources":["../../src/knowledge/spec_index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAE7D,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACtB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACrB;AAwFD,wBAAgB,cAAc,CAC1B,SAAS,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,QAAQ,CAAC,EAAE,mBAAmB,GAAG,IAAI,GACtC,SAAS,CAcX;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CAUrG;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE3F;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,CAQ/D"}
|
|
@@ -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
|
+
}
|