@yasserkhanorg/e2e-agents 1.3.2 → 1.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/README.md +40 -9
- package/dist/cli/commands/train.d.ts +3 -0
- package/dist/cli/commands/train.d.ts.map +1 -0
- package/dist/cli/commands/train.js +307 -0
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +7 -1
- package/dist/cli/types.d.ts +6 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/usage.d.ts.map +1 -1
- package/dist/cli/usage.js +7 -1
- package/dist/cli.js +5 -0
- package/dist/esm/cli/commands/train.js +271 -0
- package/dist/esm/cli/parse_args.js +7 -1
- package/dist/esm/cli/usage.js +7 -1
- package/dist/esm/cli.js +5 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/knowledge/route_families.js +2 -2
- package/dist/esm/training/enricher.js +273 -0
- package/dist/esm/training/merger.js +137 -0
- package/dist/esm/training/scanner.js +386 -0
- package/dist/esm/training/types.js +6 -0
- package/dist/esm/training/validator.js +153 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/knowledge/route_families.d.ts +2 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +2 -0
- package/dist/training/enricher.d.ts +15 -0
- package/dist/training/enricher.d.ts.map +1 -0
- package/dist/training/enricher.js +278 -0
- package/dist/training/merger.d.ts +5 -0
- package/dist/training/merger.d.ts.map +1 -0
- package/dist/training/merger.js +141 -0
- package/dist/training/scanner.d.ts +5 -0
- package/dist/training/scanner.d.ts.map +1 -0
- package/dist/training/scanner.js +391 -0
- package/dist/training/types.d.ts +109 -0
- package/dist/training/types.d.ts.map +1 -0
- package/dist/training/types.js +9 -0
- package/dist/training/validator.d.ts +16 -0
- package/dist/training/validator.d.ts.map +1 -0
- package/dist/training/validator.js +160 -0
- package/package.json +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
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.discoverSourceDirs = discoverSourceDirs;
|
|
6
|
+
exports.discoverTestDirs = discoverTestDirs;
|
|
7
|
+
exports.scanProject = scanProject;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const SOURCE_MAX_DEPTH = 3;
|
|
11
|
+
// One deeper than source to account for test framework wrapper dirs (e2e/, integration/)
|
|
12
|
+
const TEST_MAX_DEPTH = 5;
|
|
13
|
+
const SPEC_FILES_MAX_DEPTH = 10;
|
|
14
|
+
const SOURCE_ROOTS = ['src', 'app', 'pages', 'components', 'features', 'modules'];
|
|
15
|
+
const SERVER_ROOTS = ['server', 'api', 'cmd', 'model', 'services'];
|
|
16
|
+
const SKIP_DIRS = new Set([
|
|
17
|
+
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build',
|
|
18
|
+
'coverage', '__pycache__', '.e2e-ai-agents', '.cache',
|
|
19
|
+
'vendor', 'third_party',
|
|
20
|
+
]);
|
|
21
|
+
const TEST_EXTENSIONS = ['.spec.ts', '.test.ts', '.spec.js', '.test.js', '.spec.tsx', '.test.tsx'];
|
|
22
|
+
const GO_TEST_SUFFIX = '_test.go';
|
|
23
|
+
/** Type-safe includes check for readonly arrays */
|
|
24
|
+
const includes = (arr, v) => arr.includes(v);
|
|
25
|
+
function isSkipped(name) {
|
|
26
|
+
return name.startsWith('.') || SKIP_DIRS.has(name);
|
|
27
|
+
}
|
|
28
|
+
function normalizeId(name) {
|
|
29
|
+
return name
|
|
30
|
+
.replace(/[A-Z]/g, (c, idx) => (idx > 0 ? `_${c.toLowerCase()}` : c.toLowerCase()))
|
|
31
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
32
|
+
.replace(/_+/g, '_')
|
|
33
|
+
.replace(/^_|_$/g, '');
|
|
34
|
+
}
|
|
35
|
+
function extractFamilyHint(dirPath, projectRoot) {
|
|
36
|
+
const rel = (0, path_1.relative)(projectRoot, dirPath).replace(/\\/g, '/');
|
|
37
|
+
const parts = rel.split('/').filter(Boolean);
|
|
38
|
+
// Skip the root category dir (src/, server/, tests/, etc.)
|
|
39
|
+
// Return the first meaningful subdirectory name
|
|
40
|
+
for (let i = 1; i < parts.length; i++) {
|
|
41
|
+
const part = parts[i];
|
|
42
|
+
if (!isSkipped(part) && part !== 'e2e' && part !== 'integration' && part !== 'functional') {
|
|
43
|
+
return normalizeId(part);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return normalizeId(parts[parts.length - 1] || (0, path_1.basename)(dirPath));
|
|
47
|
+
}
|
|
48
|
+
function walkDirs(root, projectRoot, category, maxDepth, results, depth = 0) {
|
|
49
|
+
if (depth > maxDepth || !(0, fs_1.existsSync)(root)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = (0, fs_1.readdirSync)(root);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const hasSourceFiles = entries.some((e) => {
|
|
61
|
+
const ext = e.slice(e.lastIndexOf('.'));
|
|
62
|
+
return ['.ts', '.tsx', '.js', '.jsx', '.go', '.py', '.rs'].includes(ext);
|
|
63
|
+
});
|
|
64
|
+
const subdirs = entries.filter((e) => {
|
|
65
|
+
if (isSkipped(e))
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
const stat = (0, fs_1.lstatSync)((0, path_1.join)(root, e));
|
|
69
|
+
if (stat.isSymbolicLink())
|
|
70
|
+
return false;
|
|
71
|
+
return stat.isDirectory();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
if (hasSourceFiles && depth >= 1) {
|
|
79
|
+
results.push({
|
|
80
|
+
path: (0, path_1.resolve)(root),
|
|
81
|
+
relativePath: (0, path_1.relative)(projectRoot, root).replace(/\\/g, '/'),
|
|
82
|
+
category,
|
|
83
|
+
familyHint: extractFamilyHint(root, projectRoot),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
for (const sub of subdirs) {
|
|
87
|
+
walkDirs((0, path_1.join)(root, sub), projectRoot, category, maxDepth, results, depth + 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function discoverSourceDirs(projectRoot) {
|
|
91
|
+
const results = [];
|
|
92
|
+
const resolved = (0, path_1.resolve)(projectRoot);
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = (0, fs_1.readdirSync)(resolved);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (isSkipped(entry))
|
|
103
|
+
continue;
|
|
104
|
+
const fullPath = (0, path_1.join)(resolved, entry);
|
|
105
|
+
try {
|
|
106
|
+
const stat = (0, fs_1.lstatSync)(fullPath);
|
|
107
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (includes(SOURCE_ROOTS, entry)) {
|
|
115
|
+
walkDirs(fullPath, resolved, 'webapp', SOURCE_MAX_DEPTH, results);
|
|
116
|
+
}
|
|
117
|
+
else if (includes(SERVER_ROOTS, entry)) {
|
|
118
|
+
walkDirs(fullPath, resolved, 'server', SOURCE_MAX_DEPTH, results);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
function discoverTestDirs(projectRoot) {
|
|
124
|
+
const results = [];
|
|
125
|
+
const resolved = (0, path_1.resolve)(projectRoot);
|
|
126
|
+
function walk(dir, category, depth) {
|
|
127
|
+
if (depth > TEST_MAX_DEPTH || !(0, fs_1.existsSync)(dir))
|
|
128
|
+
return;
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = (0, fs_1.readdirSync)(dir);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const hasTests = entries.some((e) => {
|
|
138
|
+
return TEST_EXTENSIONS.some((ext) => e.endsWith(ext)) || e.endsWith(GO_TEST_SUFFIX);
|
|
139
|
+
});
|
|
140
|
+
if (hasTests) {
|
|
141
|
+
results.push({
|
|
142
|
+
path: (0, path_1.resolve)(dir),
|
|
143
|
+
relativePath: (0, path_1.relative)(resolved, dir).replace(/\\/g, '/'),
|
|
144
|
+
category,
|
|
145
|
+
familyHint: extractFamilyHint(dir, resolved),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
if (isSkipped(entry))
|
|
150
|
+
continue;
|
|
151
|
+
const full = (0, path_1.join)(dir, entry);
|
|
152
|
+
try {
|
|
153
|
+
const stat = (0, fs_1.lstatSync)(full);
|
|
154
|
+
if (stat.isSymbolicLink())
|
|
155
|
+
continue;
|
|
156
|
+
if (stat.isDirectory()) {
|
|
157
|
+
walk(full, category, depth + 1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const testRoots = ['tests', 'test', 'e2e-tests', 'e2e', 'specs', 'spec'];
|
|
166
|
+
const cypressRoots = ['cypress/e2e', 'cypress/integration'];
|
|
167
|
+
for (const root of testRoots) {
|
|
168
|
+
walk((0, path_1.join)(resolved, root), 'test', 0);
|
|
169
|
+
}
|
|
170
|
+
for (const root of cypressRoots) {
|
|
171
|
+
walk((0, path_1.join)(resolved, root), 'cypress', 0);
|
|
172
|
+
}
|
|
173
|
+
// Also scan server dirs for Go test files
|
|
174
|
+
for (const root of SERVER_ROOTS) {
|
|
175
|
+
const serverPath = (0, path_1.join)(resolved, root);
|
|
176
|
+
if ((0, fs_1.existsSync)(serverPath)) {
|
|
177
|
+
walk(serverPath, 'test', 0);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
function extractTags(specFiles) {
|
|
183
|
+
const tags = new Set();
|
|
184
|
+
for (const file of specFiles) {
|
|
185
|
+
try {
|
|
186
|
+
const content = (0, fs_1.readFileSync)(file, 'utf-8');
|
|
187
|
+
const matches = content.match(/@[a-zA-Z][a-zA-Z0-9_-]*/g);
|
|
188
|
+
if (matches) {
|
|
189
|
+
for (const m of matches) {
|
|
190
|
+
if (!m.startsWith('@playwright') && !m.startsWith('@param') && !m.startsWith('@returns')) {
|
|
191
|
+
tags.add(m);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// ENOENT or EACCES — skip unreadable files
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return Array.from(tags);
|
|
201
|
+
}
|
|
202
|
+
function getSpecFiles(dir, depth = 0) {
|
|
203
|
+
if (depth > SPEC_FILES_MAX_DEPTH)
|
|
204
|
+
return [];
|
|
205
|
+
const files = [];
|
|
206
|
+
try {
|
|
207
|
+
for (const entry of (0, fs_1.readdirSync)(dir)) {
|
|
208
|
+
const full = (0, path_1.join)(dir, entry);
|
|
209
|
+
try {
|
|
210
|
+
const stat = (0, fs_1.lstatSync)(full);
|
|
211
|
+
if (stat.isSymbolicLink())
|
|
212
|
+
continue;
|
|
213
|
+
if (stat.isDirectory()) {
|
|
214
|
+
files.push(...getSpecFiles(full, depth + 1));
|
|
215
|
+
}
|
|
216
|
+
else if (TEST_EXTENSIONS.some((ext) => entry.endsWith(ext))) {
|
|
217
|
+
files.push(full);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// ENOENT or EACCES — skip inaccessible directories
|
|
227
|
+
}
|
|
228
|
+
return files;
|
|
229
|
+
}
|
|
230
|
+
function buildGlobPattern(relativePath) {
|
|
231
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
232
|
+
return `${normalized}/*`;
|
|
233
|
+
}
|
|
234
|
+
function groupByFamily(dirs) {
|
|
235
|
+
const groups = new Map();
|
|
236
|
+
for (const dir of dirs) {
|
|
237
|
+
const key = normalizeId(dir.familyHint);
|
|
238
|
+
if (!groups.has(key)) {
|
|
239
|
+
groups.set(key, { webapp: [], server: [], test: [], cypress: [] });
|
|
240
|
+
}
|
|
241
|
+
const group = groups.get(key);
|
|
242
|
+
if (dir.category === 'webapp')
|
|
243
|
+
group.webapp.push(dir);
|
|
244
|
+
else if (dir.category === 'server')
|
|
245
|
+
group.server.push(dir);
|
|
246
|
+
else if (dir.category === 'cypress')
|
|
247
|
+
group.cypress.push(dir);
|
|
248
|
+
else
|
|
249
|
+
group.test.push(dir);
|
|
250
|
+
}
|
|
251
|
+
return groups;
|
|
252
|
+
}
|
|
253
|
+
function detectFeatures(familyId, group, projectRoot) {
|
|
254
|
+
const features = [];
|
|
255
|
+
const webappSubdirs = new Map();
|
|
256
|
+
for (const dir of group.webapp) {
|
|
257
|
+
try {
|
|
258
|
+
for (const entry of (0, fs_1.readdirSync)(dir.path)) {
|
|
259
|
+
if (isSkipped(entry))
|
|
260
|
+
continue;
|
|
261
|
+
const full = (0, path_1.join)(dir.path, entry);
|
|
262
|
+
try {
|
|
263
|
+
const stat = (0, fs_1.lstatSync)(full);
|
|
264
|
+
if (stat.isSymbolicLink())
|
|
265
|
+
continue;
|
|
266
|
+
if (stat.isDirectory()) {
|
|
267
|
+
const hint = normalizeId(entry);
|
|
268
|
+
if (!webappSubdirs.has(hint))
|
|
269
|
+
webappSubdirs.set(hint, []);
|
|
270
|
+
webappSubdirs.get(hint).push({
|
|
271
|
+
path: full,
|
|
272
|
+
relativePath: (0, path_1.relative)(projectRoot, full).replace(/\\/g, '/'),
|
|
273
|
+
category: 'webapp',
|
|
274
|
+
familyHint: entry,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// ENOENT or EACCES — skip inaccessible directories
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const testDir of group.test) {
|
|
288
|
+
try {
|
|
289
|
+
for (const entry of (0, fs_1.readdirSync)(testDir.path)) {
|
|
290
|
+
if (isSkipped(entry))
|
|
291
|
+
continue;
|
|
292
|
+
const full = (0, path_1.join)(testDir.path, entry);
|
|
293
|
+
try {
|
|
294
|
+
const stat = (0, fs_1.lstatSync)(full);
|
|
295
|
+
if (stat.isSymbolicLink())
|
|
296
|
+
continue;
|
|
297
|
+
if (!stat.isDirectory())
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const hint = normalizeId(entry);
|
|
305
|
+
if (webappSubdirs.has(hint)) {
|
|
306
|
+
const webDirs = webappSubdirs.get(hint);
|
|
307
|
+
features.push({
|
|
308
|
+
id: `${familyId}/${hint}`,
|
|
309
|
+
webappPaths: webDirs.map((d) => buildGlobPattern(d.relativePath)),
|
|
310
|
+
serverPaths: [],
|
|
311
|
+
specDirs: [(0, path_1.relative)(projectRoot, full).replace(/\\/g, '/') + '/'],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// ENOENT or EACCES — skip inaccessible directories
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return features;
|
|
321
|
+
}
|
|
322
|
+
function scanProject(projectRoot) {
|
|
323
|
+
const resolved = (0, path_1.resolve)(projectRoot);
|
|
324
|
+
const sourceDirs = discoverSourceDirs(resolved);
|
|
325
|
+
const testDirs = discoverTestDirs(resolved);
|
|
326
|
+
const allDirs = [...sourceDirs, ...testDirs];
|
|
327
|
+
const groups = groupByFamily(allDirs);
|
|
328
|
+
const families = [];
|
|
329
|
+
for (const [familyId, group] of groups) {
|
|
330
|
+
const hasSrc = group.webapp.length > 0 || group.server.length > 0;
|
|
331
|
+
const hasTests = group.test.length > 0 || group.cypress.length > 0;
|
|
332
|
+
if (!hasSrc && !hasTests)
|
|
333
|
+
continue;
|
|
334
|
+
const allSpecFiles = [];
|
|
335
|
+
for (const td of [...group.test, ...group.cypress]) {
|
|
336
|
+
allSpecFiles.push(...getSpecFiles(td.path));
|
|
337
|
+
}
|
|
338
|
+
const features = detectFeatures(familyId, group, resolved);
|
|
339
|
+
families.push({
|
|
340
|
+
id: familyId,
|
|
341
|
+
routes: [`/${familyId}`],
|
|
342
|
+
webappPaths: group.webapp.map((d) => buildGlobPattern(d.relativePath)),
|
|
343
|
+
serverPaths: group.server.map((d) => buildGlobPattern(d.relativePath)),
|
|
344
|
+
specDirs: group.test.map((d) => d.relativePath + '/'),
|
|
345
|
+
cypressSpecDirs: group.cypress.map((d) => d.relativePath + '/'),
|
|
346
|
+
tags: extractTags(allSpecFiles),
|
|
347
|
+
features,
|
|
348
|
+
routesGuessed: true,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const familyIds = new Set(families.map((f) => f.id));
|
|
352
|
+
const unmatchedSourceDirs = sourceDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
|
|
353
|
+
const unmatchedTestDirs = testDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
|
|
354
|
+
let totalSourceFiles = 0;
|
|
355
|
+
let totalTestFiles = 0;
|
|
356
|
+
for (const dir of sourceDirs) {
|
|
357
|
+
try {
|
|
358
|
+
totalSourceFiles += (0, fs_1.readdirSync)(dir.path).filter((e) => {
|
|
359
|
+
try {
|
|
360
|
+
const stat = (0, fs_1.lstatSync)((0, path_1.join)(dir.path, e));
|
|
361
|
+
return !stat.isSymbolicLink() && !stat.isDirectory();
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// ENOENT or EACCES — skip inaccessible entries
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}).length;
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// ENOENT or EACCES — skip inaccessible directories
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const dir of testDirs) {
|
|
374
|
+
try {
|
|
375
|
+
totalTestFiles += getSpecFiles(dir.path).length;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// ENOENT or EACCES — skip inaccessible directories
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
families,
|
|
383
|
+
unmatchedSourceDirs,
|
|
384
|
+
unmatchedTestDirs,
|
|
385
|
+
stats: {
|
|
386
|
+
totalSourceFiles,
|
|
387
|
+
totalTestFiles,
|
|
388
|
+
familyCount: families.length,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { RouteFamily, RouteFamilyManifest } from '../knowledge/route_families.js';
|
|
2
|
+
/** A source directory discovered by the scanner */
|
|
3
|
+
export interface DiscoveredDir {
|
|
4
|
+
/** Absolute path to the directory */
|
|
5
|
+
path: string;
|
|
6
|
+
/** Relative path from project root */
|
|
7
|
+
relativePath: string;
|
|
8
|
+
/** Category: frontend source, backend source, or test */
|
|
9
|
+
category: 'webapp' | 'server' | 'test' | 'cypress';
|
|
10
|
+
/** Deepest meaningful directory name (e.g., 'channels' from 'src/channels/') */
|
|
11
|
+
familyHint: string;
|
|
12
|
+
}
|
|
13
|
+
/** A family proposed by the deterministic scanner */
|
|
14
|
+
export interface ScannedFamily {
|
|
15
|
+
id: string;
|
|
16
|
+
routes: string[];
|
|
17
|
+
webappPaths: string[];
|
|
18
|
+
serverPaths: string[];
|
|
19
|
+
specDirs: string[];
|
|
20
|
+
cypressSpecDirs: string[];
|
|
21
|
+
tags: string[];
|
|
22
|
+
features: ScannedFeature[];
|
|
23
|
+
/** True if routes are guesses (directory-name-based) */
|
|
24
|
+
routesGuessed: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** A nested feature proposed by the scanner */
|
|
27
|
+
export interface ScannedFeature {
|
|
28
|
+
id: string;
|
|
29
|
+
webappPaths: string[];
|
|
30
|
+
serverPaths: string[];
|
|
31
|
+
specDirs: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Output of the deterministic scanner */
|
|
34
|
+
export interface ScanResult {
|
|
35
|
+
families: ScannedFamily[];
|
|
36
|
+
unmatchedSourceDirs: DiscoveredDir[];
|
|
37
|
+
unmatchedTestDirs: DiscoveredDir[];
|
|
38
|
+
stats: {
|
|
39
|
+
totalSourceFiles: number;
|
|
40
|
+
totalTestFiles: number;
|
|
41
|
+
familyCount: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Output of LLM enrichment */
|
|
45
|
+
export interface EnrichmentResult {
|
|
46
|
+
enrichedFamilies: RouteFamily[];
|
|
47
|
+
tokensUsed: number;
|
|
48
|
+
costUSD: number;
|
|
49
|
+
skippedFamilies: string[];
|
|
50
|
+
}
|
|
51
|
+
/** A single commit's validation result */
|
|
52
|
+
export interface CommitValidation {
|
|
53
|
+
hash: string;
|
|
54
|
+
message: string;
|
|
55
|
+
changedFiles: string[];
|
|
56
|
+
boundFiles: number;
|
|
57
|
+
unboundFiles: string[];
|
|
58
|
+
familiesHit: string[];
|
|
59
|
+
}
|
|
60
|
+
/** Output of validation mode */
|
|
61
|
+
export interface ValidationReport {
|
|
62
|
+
totalCommits: number;
|
|
63
|
+
totalFiles: number;
|
|
64
|
+
boundFiles: number;
|
|
65
|
+
unboundFiles: number;
|
|
66
|
+
coveragePercent: number;
|
|
67
|
+
commits: CommitValidation[];
|
|
68
|
+
familyHits: Record<string, number>;
|
|
69
|
+
neverHitFamilies: string[];
|
|
70
|
+
unboundFileClusters: Array<{
|
|
71
|
+
pattern: string;
|
|
72
|
+
count: number;
|
|
73
|
+
suggestedFamily: string;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
/** Output of smart merge */
|
|
77
|
+
export interface MergeResult {
|
|
78
|
+
manifest: RouteFamilyManifest;
|
|
79
|
+
newFamilies: string[];
|
|
80
|
+
updatedFamilies: string[];
|
|
81
|
+
staleFamilies: string[];
|
|
82
|
+
summary: string;
|
|
83
|
+
}
|
|
84
|
+
/** Options for the train command */
|
|
85
|
+
export interface TrainOptions {
|
|
86
|
+
/** Path to the application root */
|
|
87
|
+
appPath: string;
|
|
88
|
+
/** Path to tests root (may differ from appPath) */
|
|
89
|
+
testsRoot: string;
|
|
90
|
+
/** Enable LLM enrichment (default: true) */
|
|
91
|
+
enrich: boolean;
|
|
92
|
+
/** Run validation against git history */
|
|
93
|
+
validate: boolean;
|
|
94
|
+
/** Git ref for validation (e.g., 'HEAD~20') */
|
|
95
|
+
since: string;
|
|
96
|
+
/** GitHub PR number for validation */
|
|
97
|
+
pr?: number;
|
|
98
|
+
/** Output path for route-families.json */
|
|
99
|
+
outputPath: string;
|
|
100
|
+
/** Dry run — print without writing */
|
|
101
|
+
dryRun: boolean;
|
|
102
|
+
/** Non-interactive mode */
|
|
103
|
+
yes: boolean;
|
|
104
|
+
/** Max LLM spend in USD */
|
|
105
|
+
budgetUSD: number;
|
|
106
|
+
}
|
|
107
|
+
/** Routes that look like bare "/<id>" are scanner-generated guesses */
|
|
108
|
+
export declare function isGuessedRoute(routes: string[]): boolean;
|
|
109
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/training/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAE,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AAErF,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC1B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,QAAQ,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;IACnD,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,wDAAwD;IACxD,aAAa,EAAE,OAAO,CAAC;CAC1B;AAED,+CAA+C;AAC/C,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,0CAA0C;AAC1C,MAAM,WAAW,UAAU;IACvB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,mBAAmB,EAAE,aAAa,EAAE,CAAC;IACrC,iBAAiB,EAAE,aAAa,EAAE,CAAC;IACnC,KAAK,EAAE;QACH,gBAAgB,EAAE,MAAM,CAAC;QACzB,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;KACvB,CAAC;CACL;AAED,+BAA+B;AAC/B,MAAM,WAAW,gBAAgB;IAC7B,gBAAgB,EAAE,WAAW,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,gCAAgC;AAChC,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,mBAAmB,EAAE,KAAK,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,eAAe,EAAE,MAAM,CAAC;KAC3B,CAAC,CAAC;CACN;AAED,4BAA4B;AAC5B,MAAM,WAAW,WAAW;IACxB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,oCAAoC;AACpC,MAAM,WAAW,YAAY;IACzB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,MAAM,EAAE,OAAO,CAAC;IAChB,yCAAyC;IACzC,QAAQ,EAAE,OAAO,CAAC;IAClB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,MAAM,EAAE,OAAO,CAAC;IAChB,2BAA2B;IAC3B,GAAG,EAAE,OAAO,CAAC;IACb,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAExD"}
|
|
@@ -0,0 +1,9 @@
|
|
|
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.isGuessedRoute = isGuessedRoute;
|
|
6
|
+
/** Routes that look like bare "/<id>" are scanner-generated guesses */
|
|
7
|
+
function isGuessedRoute(routes) {
|
|
8
|
+
return routes.every((r) => /^\/[a-z][a-z0-9_]*$/.test(r));
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RouteFamilyManifest } from '../knowledge/route_families.js';
|
|
2
|
+
import type { CommitValidation, ValidationReport } from './types.js';
|
|
3
|
+
export declare function parseGitLog(log: string): Array<{
|
|
4
|
+
hash: string;
|
|
5
|
+
message: string;
|
|
6
|
+
files: string[];
|
|
7
|
+
}>;
|
|
8
|
+
export declare function getCommitFiles(projectRoot: string, since: string): Array<{
|
|
9
|
+
hash: string;
|
|
10
|
+
message: string;
|
|
11
|
+
files: string[];
|
|
12
|
+
}>;
|
|
13
|
+
export declare function validateCommit(manifest: RouteFamilyManifest, files: string[], hash: string, message: string): CommitValidation;
|
|
14
|
+
export declare function buildValidationReport(commits: CommitValidation[], manifest: RouteFamilyManifest): ValidationReport;
|
|
15
|
+
export declare function formatValidationReport(report: ValidationReport): string;
|
|
16
|
+
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/training/validator.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AAExE,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,YAAY,CAAC;AAEnE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC,CA6BhG;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC,CAgB1H;AAED,wBAAgB,cAAc,CAC1B,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAChB,gBAAgB,CA6BlB;AAED,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,QAAQ,EAAE,mBAAmB,GAC9B,gBAAgB,CAkDlB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAgCvE"}
|