claude-flow-novice 1.5.17 → 1.5.19
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/.claude-flow-novice/dist/config/hooks/post-edit-pipeline.js +1837 -0
- package/.claude-flow-novice/dist/src/hooks/communication-integrated-post-edit.js +673 -0
- package/.claude-flow-novice/dist/src/hooks/enhanced/experience-adaptation-hooks.js +347 -0
- package/.claude-flow-novice/dist/src/hooks/enhanced/personalization-hooks.js +118 -0
- package/.claude-flow-novice/dist/src/hooks/enhanced-post-edit-pipeline.js +2044 -0
- package/.claude-flow-novice/dist/src/hooks/filter-integration.js +542 -0
- package/.claude-flow-novice/dist/src/hooks/guidance-hooks.js +629 -0
- package/.claude-flow-novice/dist/src/hooks/index.ts +239 -0
- package/.claude-flow-novice/dist/src/hooks/managers/enhanced-hook-manager.js +200 -0
- package/.claude-flow-novice/dist/src/hooks/resilient-hook-system.js +812 -0
- package/CHANGELOG.md +22 -0
- package/config/hooks/post-edit-pipeline.js +30 -0
- package/package.json +2 -1
- package/src/cli/simple-commands/init/templates/CLAUDE.md +38 -6
- package/src/hooks/communication-integrated-post-edit.js +673 -0
- package/src/hooks/enhanced/experience-adaptation-hooks.js +347 -0
- package/src/hooks/enhanced/personalization-hooks.js +118 -0
- package/src/hooks/enhanced-hooks-cli.js +168 -0
- package/src/hooks/enhanced-post-edit-pipeline.js +2044 -0
- package/src/hooks/filter-integration.js +542 -0
- package/src/hooks/guidance-hooks.js +629 -0
- package/src/hooks/index.ts +239 -0
- package/src/hooks/managers/enhanced-hook-manager.js +200 -0
- package/src/hooks/resilient-hook-system.js +812 -0
|
@@ -0,0 +1,1837 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified Post-Edit Validation Pipeline
|
|
5
|
+
* Combines comprehensive validation with TDD enforcement
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Progressive validation (syntax → interface → integration → full)
|
|
9
|
+
* - TDD enforcement with test-first compliance
|
|
10
|
+
* - Single-file testing without full system compilation
|
|
11
|
+
* - Real-time coverage analysis
|
|
12
|
+
* - Rust-specific quality enforcements
|
|
13
|
+
* - Multi-language support
|
|
14
|
+
* - Security scanning
|
|
15
|
+
* - Agent coordination
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import fsSync from 'fs';
|
|
21
|
+
import { exec, spawn } from 'child_process';
|
|
22
|
+
import { promisify } from 'util';
|
|
23
|
+
|
|
24
|
+
const execAsync = promisify(exec);
|
|
25
|
+
|
|
26
|
+
// Enhanced logging utilities
|
|
27
|
+
class Logger {
|
|
28
|
+
static success(msg, data = {}) {
|
|
29
|
+
console.log(`✅ ${msg}`);
|
|
30
|
+
return { level: 'success', message: msg, data };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static error(msg, data = {}) {
|
|
34
|
+
console.log(`❌ ${msg}`);
|
|
35
|
+
return { level: 'error', message: msg, data };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static warning(msg, data = {}) {
|
|
39
|
+
console.log(`⚠️ ${msg}`);
|
|
40
|
+
return { level: 'warning', message: msg, data };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static info(msg, data = {}) {
|
|
44
|
+
console.log(`ℹ️ ${msg}`);
|
|
45
|
+
return { level: 'info', message: msg, data };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static test(msg, data = {}) {
|
|
49
|
+
console.log(`🧪 ${msg}`);
|
|
50
|
+
return { level: 'test', message: msg, data };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static coverage(msg, data = {}) {
|
|
54
|
+
console.log(`📊 ${msg}`);
|
|
55
|
+
return { level: 'coverage', message: msg, data };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static tdd(msg, data = {}) {
|
|
59
|
+
console.log(`🔴🟢♻️ ${msg}`);
|
|
60
|
+
return { level: 'tdd', message: msg, data };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Single-file test execution engine
|
|
65
|
+
class SingleFileTestEngine {
|
|
66
|
+
constructor() {
|
|
67
|
+
this.testRunners = {
|
|
68
|
+
'.js': this.runJavaScriptTests.bind(this),
|
|
69
|
+
'.jsx': this.runJavaScriptTests.bind(this),
|
|
70
|
+
'.ts': this.runTypeScriptTests.bind(this),
|
|
71
|
+
'.tsx': this.runTypeScriptTests.bind(this),
|
|
72
|
+
'.py': this.runPythonTests.bind(this),
|
|
73
|
+
'.go': this.runGoTests.bind(this),
|
|
74
|
+
'.rs': this.runRustTests.bind(this),
|
|
75
|
+
'.java': this.runJavaTests.bind(this),
|
|
76
|
+
'.cpp': this.runCPPTests.bind(this),
|
|
77
|
+
'.c': this.runCTests.bind(this)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async executeTests(file, content) {
|
|
82
|
+
const ext = path.extname(file).toLowerCase();
|
|
83
|
+
const runner = this.testRunners[ext];
|
|
84
|
+
|
|
85
|
+
if (!runner) {
|
|
86
|
+
return {
|
|
87
|
+
executed: false,
|
|
88
|
+
reason: `No test runner available for ${ext} files`,
|
|
89
|
+
framework: null,
|
|
90
|
+
results: null,
|
|
91
|
+
coverage: null,
|
|
92
|
+
tddCompliance: null
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const isTestFile = this.isTestFile(file);
|
|
97
|
+
const relatedFile = isTestFile ? this.findSourceFile(file) : this.findTestFile(file);
|
|
98
|
+
|
|
99
|
+
return await runner(file, content, { isTestFile, relatedFile });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async runJavaScriptTests(file, content, options = {}) {
|
|
103
|
+
const { isTestFile, relatedFile } = options;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const framework = await this.detectJSTestFramework();
|
|
107
|
+
|
|
108
|
+
if (!framework) {
|
|
109
|
+
return {
|
|
110
|
+
executed: false,
|
|
111
|
+
reason: 'No JavaScript test framework detected (jest, mocha, etc.)',
|
|
112
|
+
framework: null,
|
|
113
|
+
results: null,
|
|
114
|
+
coverage: null,
|
|
115
|
+
tddCompliance: this.checkTDDCompliance(file, relatedFile, null)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let testResults = null;
|
|
120
|
+
let coverage = null;
|
|
121
|
+
|
|
122
|
+
if (framework === 'jest') {
|
|
123
|
+
testResults = await this.runJestSingleFile(file, isTestFile);
|
|
124
|
+
coverage = await this.getJestCoverage(file);
|
|
125
|
+
} else if (framework === 'mocha') {
|
|
126
|
+
testResults = await this.runMochaSingleFile(file, isTestFile);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const tddCompliance = this.checkTDDCompliance(file, relatedFile, testResults);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
executed: true,
|
|
133
|
+
framework,
|
|
134
|
+
results: testResults,
|
|
135
|
+
coverage,
|
|
136
|
+
tddCompliance,
|
|
137
|
+
singleFileMode: true
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
executed: false,
|
|
143
|
+
reason: `Test execution failed: ${error.message}`,
|
|
144
|
+
framework: null,
|
|
145
|
+
results: null,
|
|
146
|
+
coverage: null,
|
|
147
|
+
tddCompliance: null,
|
|
148
|
+
error: error.message
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async runTypeScriptTests(file, content, options = {}) {
|
|
154
|
+
const jsResult = await this.runJavaScriptTests(file, content, options);
|
|
155
|
+
if (jsResult.executed) {
|
|
156
|
+
jsResult.language = 'typescript';
|
|
157
|
+
jsResult.compiled = true;
|
|
158
|
+
}
|
|
159
|
+
return jsResult;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async runPythonTests(file, content, options = {}) {
|
|
163
|
+
const { isTestFile, relatedFile } = options;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const framework = await this.detectPythonTestFramework();
|
|
167
|
+
|
|
168
|
+
if (!framework) {
|
|
169
|
+
return {
|
|
170
|
+
executed: false,
|
|
171
|
+
reason: 'No Python test framework detected (pytest, unittest)',
|
|
172
|
+
framework: null,
|
|
173
|
+
results: null,
|
|
174
|
+
coverage: null,
|
|
175
|
+
tddCompliance: this.checkTDDCompliance(file, relatedFile, null)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let testResults = null;
|
|
180
|
+
let coverage = null;
|
|
181
|
+
|
|
182
|
+
if (framework === 'pytest') {
|
|
183
|
+
testResults = await this.runPytestSingleFile(file, isTestFile);
|
|
184
|
+
coverage = await this.getPytestCoverage(file);
|
|
185
|
+
} else if (framework === 'unittest') {
|
|
186
|
+
testResults = await this.runUnittestSingleFile(file, isTestFile);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const tddCompliance = this.checkTDDCompliance(file, relatedFile, testResults);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
executed: true,
|
|
193
|
+
framework,
|
|
194
|
+
results: testResults,
|
|
195
|
+
coverage,
|
|
196
|
+
tddCompliance,
|
|
197
|
+
singleFileMode: true
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return {
|
|
202
|
+
executed: false,
|
|
203
|
+
reason: `Python test execution failed: ${error.message}`,
|
|
204
|
+
framework: null,
|
|
205
|
+
results: null,
|
|
206
|
+
coverage: null,
|
|
207
|
+
tddCompliance: null,
|
|
208
|
+
error: error.message
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async runGoTests(file, content, options = {}) {
|
|
214
|
+
const { isTestFile, relatedFile } = options;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
if (!isTestFile) {
|
|
218
|
+
const testFile = this.findTestFile(file);
|
|
219
|
+
if (!testFile || !await this.fileExists(testFile)) {
|
|
220
|
+
return {
|
|
221
|
+
executed: false,
|
|
222
|
+
reason: 'No corresponding test file found for Go source',
|
|
223
|
+
framework: 'go test',
|
|
224
|
+
results: null,
|
|
225
|
+
coverage: null,
|
|
226
|
+
tddCompliance: this.checkTDDCompliance(file, null, null)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
file = testFile;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const testResults = await this.runGoTestSingleFile(file);
|
|
233
|
+
const coverage = await this.getGoCoverage(file);
|
|
234
|
+
const tddCompliance = this.checkTDDCompliance(file, relatedFile, testResults);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
executed: true,
|
|
238
|
+
framework: 'go test',
|
|
239
|
+
results: testResults,
|
|
240
|
+
coverage,
|
|
241
|
+
tddCompliance,
|
|
242
|
+
singleFileMode: true
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return {
|
|
247
|
+
executed: false,
|
|
248
|
+
reason: `Go test execution failed: ${error.message}`,
|
|
249
|
+
framework: 'go test',
|
|
250
|
+
results: null,
|
|
251
|
+
coverage: null,
|
|
252
|
+
tddCompliance: null,
|
|
253
|
+
error: error.message
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async runRustTests(file, content, options = {}) {
|
|
259
|
+
const { isTestFile, relatedFile } = options;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const testResults = await this.runCargoTestSingleFile(file);
|
|
263
|
+
const coverage = await this.getRustCoverage(file);
|
|
264
|
+
const tddCompliance = this.checkTDDCompliance(file, relatedFile, testResults);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
executed: true,
|
|
268
|
+
framework: 'cargo test',
|
|
269
|
+
results: testResults,
|
|
270
|
+
coverage,
|
|
271
|
+
tddCompliance,
|
|
272
|
+
singleFileMode: true
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return {
|
|
277
|
+
executed: false,
|
|
278
|
+
reason: `Rust test execution failed: ${error.message}`,
|
|
279
|
+
framework: 'cargo test',
|
|
280
|
+
results: null,
|
|
281
|
+
coverage: null,
|
|
282
|
+
tddCompliance: null,
|
|
283
|
+
error: error.message
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async runJavaTests(file, content, options = {}) {
|
|
289
|
+
const { isTestFile, relatedFile } = options;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const framework = await this.detectJavaTestFramework();
|
|
293
|
+
|
|
294
|
+
if (!framework) {
|
|
295
|
+
return {
|
|
296
|
+
executed: false,
|
|
297
|
+
reason: 'No Java test framework detected (JUnit, TestNG)',
|
|
298
|
+
framework: null,
|
|
299
|
+
results: null,
|
|
300
|
+
coverage: null,
|
|
301
|
+
tddCompliance: this.checkTDDCompliance(file, relatedFile, null)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const testResults = await this.runJavaTestSingleFile(file, framework);
|
|
306
|
+
const tddCompliance = this.checkTDDCompliance(file, relatedFile, testResults);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
executed: true,
|
|
310
|
+
framework,
|
|
311
|
+
results: testResults,
|
|
312
|
+
coverage: null,
|
|
313
|
+
tddCompliance,
|
|
314
|
+
singleFileMode: true
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {
|
|
319
|
+
executed: false,
|
|
320
|
+
reason: `Java test execution failed: ${error.message}`,
|
|
321
|
+
framework: null,
|
|
322
|
+
results: null,
|
|
323
|
+
coverage: null,
|
|
324
|
+
tddCompliance: null,
|
|
325
|
+
error: error.message
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async runCPPTests(file, content, options = {}) {
|
|
331
|
+
return this.runCTests(file, content, options);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async runCTests(file, content, options = {}) {
|
|
335
|
+
const { isTestFile, relatedFile } = options;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const framework = await this.detectCTestFramework();
|
|
339
|
+
|
|
340
|
+
if (!framework) {
|
|
341
|
+
return {
|
|
342
|
+
executed: false,
|
|
343
|
+
reason: 'No C/C++ test framework detected (gtest, catch2)',
|
|
344
|
+
framework: null,
|
|
345
|
+
results: null,
|
|
346
|
+
coverage: null,
|
|
347
|
+
tddCompliance: this.checkTDDCompliance(file, relatedFile, null)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const testResults = await this.runCTestSingleFile(file, framework);
|
|
352
|
+
const tddCompliance = this.checkTDDCompliance(file, relatedFile, testResults);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
executed: true,
|
|
356
|
+
framework,
|
|
357
|
+
results: testResults,
|
|
358
|
+
coverage: null,
|
|
359
|
+
tddCompliance,
|
|
360
|
+
singleFileMode: true
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return {
|
|
365
|
+
executed: false,
|
|
366
|
+
reason: `C/C++ test execution failed: ${error.message}`,
|
|
367
|
+
framework: null,
|
|
368
|
+
results: null,
|
|
369
|
+
coverage: null,
|
|
370
|
+
tddCompliance: null,
|
|
371
|
+
error: error.message
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Framework detection methods
|
|
377
|
+
async detectJSTestFramework() {
|
|
378
|
+
try {
|
|
379
|
+
const possiblePaths = [
|
|
380
|
+
path.join(process.cwd(), 'package.json'),
|
|
381
|
+
path.join(path.dirname(process.cwd()), 'package.json'),
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
for (const packagePath of possiblePaths) {
|
|
385
|
+
try {
|
|
386
|
+
const packageContent = await fs.promises.readFile(packagePath, 'utf8');
|
|
387
|
+
const packageJson = JSON.parse(packageContent);
|
|
388
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
389
|
+
|
|
390
|
+
if (deps.jest) return 'jest';
|
|
391
|
+
if (deps.mocha) return 'mocha';
|
|
392
|
+
if (deps.ava) return 'ava';
|
|
393
|
+
if (deps.tap) return 'tap';
|
|
394
|
+
} catch {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return null;
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async detectPythonTestFramework() {
|
|
406
|
+
try {
|
|
407
|
+
await execAsync('pytest --version');
|
|
408
|
+
return 'pytest';
|
|
409
|
+
} catch {
|
|
410
|
+
try {
|
|
411
|
+
await execAsync('python -m unittest --help');
|
|
412
|
+
return 'unittest';
|
|
413
|
+
} catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async detectJavaTestFramework() {
|
|
420
|
+
try {
|
|
421
|
+
const buildFiles = ['pom.xml', 'build.gradle', 'build.gradle.kts'];
|
|
422
|
+
|
|
423
|
+
for (const buildFile of buildFiles) {
|
|
424
|
+
if (await this.fileExists(buildFile)) {
|
|
425
|
+
const content = await fs.promises.readFile(buildFile, 'utf8');
|
|
426
|
+
if (content.includes('junit')) return 'junit';
|
|
427
|
+
if (content.includes('testng')) return 'testng';
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return null;
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async detectCTestFramework() {
|
|
438
|
+
try {
|
|
439
|
+
await execAsync('pkg-config --exists gtest');
|
|
440
|
+
return 'gtest';
|
|
441
|
+
} catch {
|
|
442
|
+
try {
|
|
443
|
+
await execAsync('pkg-config --exists catch2');
|
|
444
|
+
return 'catch2';
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Test execution implementations
|
|
452
|
+
async runJestSingleFile(file, isTestFile) {
|
|
453
|
+
try {
|
|
454
|
+
const testPattern = isTestFile ? file : this.findTestFile(file);
|
|
455
|
+
|
|
456
|
+
if (!testPattern || !await this.fileExists(testPattern)) {
|
|
457
|
+
return {
|
|
458
|
+
passed: false,
|
|
459
|
+
reason: 'No test file found',
|
|
460
|
+
tests: [],
|
|
461
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const result = await execAsync(`npx jest "${path.basename(testPattern)}" --json --coverage=false`, {
|
|
466
|
+
cwd: path.dirname(testPattern)
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const jestOutput = JSON.parse(result.stdout);
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
passed: jestOutput.success,
|
|
473
|
+
tests: jestOutput.testResults[0]?.assertionResults || [],
|
|
474
|
+
summary: {
|
|
475
|
+
total: jestOutput.numTotalTests,
|
|
476
|
+
passed: jestOutput.numPassedTests,
|
|
477
|
+
failed: jestOutput.numFailedTests,
|
|
478
|
+
skipped: jestOutput.numPendingTests
|
|
479
|
+
},
|
|
480
|
+
duration: jestOutput.testResults[0]?.endTime - jestOutput.testResults[0]?.startTime
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
} catch (error) {
|
|
484
|
+
return {
|
|
485
|
+
passed: false,
|
|
486
|
+
error: error.message,
|
|
487
|
+
tests: [],
|
|
488
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async runMochaSingleFile(file, isTestFile) {
|
|
494
|
+
try {
|
|
495
|
+
const testFile = isTestFile ? file : this.findTestFile(file);
|
|
496
|
+
|
|
497
|
+
if (!testFile || !await this.fileExists(testFile)) {
|
|
498
|
+
return {
|
|
499
|
+
passed: false,
|
|
500
|
+
reason: 'No test file found',
|
|
501
|
+
tests: [],
|
|
502
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const result = await execAsync(`npx mocha "${testFile}" --reporter json`);
|
|
507
|
+
const mochaOutput = JSON.parse(result.stdout);
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
passed: mochaOutput.failures === 0,
|
|
511
|
+
tests: mochaOutput.tests || [],
|
|
512
|
+
summary: {
|
|
513
|
+
total: mochaOutput.tests.length,
|
|
514
|
+
passed: mochaOutput.passes,
|
|
515
|
+
failed: mochaOutput.failures,
|
|
516
|
+
skipped: mochaOutput.pending
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
} catch (error) {
|
|
521
|
+
return {
|
|
522
|
+
passed: false,
|
|
523
|
+
error: error.message,
|
|
524
|
+
tests: [],
|
|
525
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async runPytestSingleFile(file, isTestFile) {
|
|
531
|
+
try {
|
|
532
|
+
const testFile = isTestFile ? file : this.findTestFile(file);
|
|
533
|
+
|
|
534
|
+
if (!testFile || !await this.fileExists(testFile)) {
|
|
535
|
+
return {
|
|
536
|
+
passed: false,
|
|
537
|
+
reason: 'No test file found',
|
|
538
|
+
tests: [],
|
|
539
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const result = await execAsync(`pytest "${testFile}" -v`);
|
|
544
|
+
|
|
545
|
+
// Parse pytest output (simplified)
|
|
546
|
+
return {
|
|
547
|
+
passed: result.code === 0,
|
|
548
|
+
tests: [],
|
|
549
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
} catch (error) {
|
|
553
|
+
return {
|
|
554
|
+
passed: false,
|
|
555
|
+
error: error.message,
|
|
556
|
+
tests: [],
|
|
557
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async runUnittestSingleFile(file, isTestFile) {
|
|
563
|
+
try {
|
|
564
|
+
const testFile = isTestFile ? file : this.findTestFile(file);
|
|
565
|
+
|
|
566
|
+
if (!testFile || !await this.fileExists(testFile)) {
|
|
567
|
+
return {
|
|
568
|
+
passed: false,
|
|
569
|
+
reason: 'No test file found',
|
|
570
|
+
tests: [],
|
|
571
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const result = await execAsync(`python -m unittest "${testFile}" -v`);
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
passed: result.code === 0,
|
|
579
|
+
tests: [],
|
|
580
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
} catch (error) {
|
|
584
|
+
return {
|
|
585
|
+
passed: false,
|
|
586
|
+
error: error.message,
|
|
587
|
+
tests: [],
|
|
588
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async runGoTestSingleFile(file) {
|
|
594
|
+
try {
|
|
595
|
+
const result = await execAsync(`go test "${file}" -json`);
|
|
596
|
+
|
|
597
|
+
const lines = result.stdout.trim().split('\n');
|
|
598
|
+
const tests = [];
|
|
599
|
+
let passed = 0, failed = 0, skipped = 0;
|
|
600
|
+
|
|
601
|
+
for (const line of lines) {
|
|
602
|
+
try {
|
|
603
|
+
const testResult = JSON.parse(line);
|
|
604
|
+
if (testResult.Action === 'pass' || testResult.Action === 'fail' || testResult.Action === 'skip') {
|
|
605
|
+
tests.push(testResult);
|
|
606
|
+
if (testResult.Action === 'pass') passed++;
|
|
607
|
+
else if (testResult.Action === 'fail') failed++;
|
|
608
|
+
else if (testResult.Action === 'skip') skipped++;
|
|
609
|
+
}
|
|
610
|
+
} catch {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
passed: failed === 0,
|
|
617
|
+
tests,
|
|
618
|
+
summary: {
|
|
619
|
+
total: passed + failed + skipped,
|
|
620
|
+
passed,
|
|
621
|
+
failed,
|
|
622
|
+
skipped
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
} catch (error) {
|
|
627
|
+
return {
|
|
628
|
+
passed: false,
|
|
629
|
+
error: error.message,
|
|
630
|
+
tests: [],
|
|
631
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async runCargoTestSingleFile(file) {
|
|
637
|
+
try {
|
|
638
|
+
const result = await execAsync(`cargo test --message-format=json`, {
|
|
639
|
+
cwd: path.dirname(file)
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const lines = result.stdout.trim().split('\n');
|
|
643
|
+
const tests = [];
|
|
644
|
+
let passed = 0, failed = 0;
|
|
645
|
+
|
|
646
|
+
for (const line of lines) {
|
|
647
|
+
try {
|
|
648
|
+
const testResult = JSON.parse(line);
|
|
649
|
+
if (testResult.type === 'test') {
|
|
650
|
+
tests.push(testResult);
|
|
651
|
+
if (testResult.event === 'ok') passed++;
|
|
652
|
+
else if (testResult.event === 'failed') failed++;
|
|
653
|
+
}
|
|
654
|
+
} catch {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
passed: failed === 0,
|
|
661
|
+
tests,
|
|
662
|
+
summary: {
|
|
663
|
+
total: passed + failed,
|
|
664
|
+
passed,
|
|
665
|
+
failed,
|
|
666
|
+
skipped: 0
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
} catch (error) {
|
|
671
|
+
return {
|
|
672
|
+
passed: false,
|
|
673
|
+
error: error.message,
|
|
674
|
+
tests: [],
|
|
675
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async runJavaTestSingleFile(file, framework) {
|
|
681
|
+
// Simplified - would need actual implementation
|
|
682
|
+
return {
|
|
683
|
+
passed: false,
|
|
684
|
+
reason: 'Java test execution not fully implemented',
|
|
685
|
+
tests: [],
|
|
686
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async runCTestSingleFile(file, framework) {
|
|
691
|
+
// Simplified - would need actual implementation
|
|
692
|
+
return {
|
|
693
|
+
passed: false,
|
|
694
|
+
reason: 'C/C++ test execution not fully implemented',
|
|
695
|
+
tests: [],
|
|
696
|
+
summary: { total: 0, passed: 0, failed: 0, skipped: 0 }
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Coverage analysis
|
|
701
|
+
async getJestCoverage(file) {
|
|
702
|
+
try {
|
|
703
|
+
await execAsync(`npx jest "${file}" --coverage --coverageReporters=json --silent`);
|
|
704
|
+
|
|
705
|
+
const coveragePath = path.join(process.cwd(), 'coverage', 'coverage-final.json');
|
|
706
|
+
const coverageData = JSON.parse(await fs.promises.readFile(coveragePath, 'utf8'));
|
|
707
|
+
|
|
708
|
+
const fileCoverage = coverageData[path.resolve(file)] || {};
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
lines: {
|
|
712
|
+
total: Object.keys(fileCoverage.s || {}).length,
|
|
713
|
+
covered: Object.values(fileCoverage.s || {}).filter(v => v > 0).length,
|
|
714
|
+
percentage: this.calculatePercentage(fileCoverage.s)
|
|
715
|
+
},
|
|
716
|
+
functions: {
|
|
717
|
+
total: Object.keys(fileCoverage.f || {}).length,
|
|
718
|
+
covered: Object.values(fileCoverage.f || {}).filter(v => v > 0).length,
|
|
719
|
+
percentage: this.calculatePercentage(fileCoverage.f)
|
|
720
|
+
},
|
|
721
|
+
branches: {
|
|
722
|
+
total: Object.keys(fileCoverage.b || {}).length,
|
|
723
|
+
covered: Object.values(fileCoverage.b || {}).flat().filter(v => v > 0).length,
|
|
724
|
+
percentage: this.calculatePercentage(fileCoverage.b, true)
|
|
725
|
+
},
|
|
726
|
+
statements: {
|
|
727
|
+
total: Object.keys(fileCoverage.s || {}).length,
|
|
728
|
+
covered: Object.values(fileCoverage.s || {}).filter(v => v > 0).length,
|
|
729
|
+
percentage: this.calculatePercentage(fileCoverage.s)
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
} catch (error) {
|
|
734
|
+
return {
|
|
735
|
+
error: error.message,
|
|
736
|
+
available: false
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async getPytestCoverage(file) {
|
|
742
|
+
try {
|
|
743
|
+
const result = await execAsync(`pytest "${file}" --cov="${path.dirname(file)}" --cov-report=json:/tmp/coverage.json`);
|
|
744
|
+
|
|
745
|
+
const coverageData = JSON.parse(await fs.promises.readFile('/tmp/coverage.json', 'utf8'));
|
|
746
|
+
const fileCoverage = coverageData.files[path.resolve(file)] || {};
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
lines: {
|
|
750
|
+
total: fileCoverage.summary?.num_statements || 0,
|
|
751
|
+
covered: fileCoverage.summary?.covered_lines || 0,
|
|
752
|
+
percentage: fileCoverage.summary?.percent_covered || 0
|
|
753
|
+
},
|
|
754
|
+
missing: fileCoverage.missing_lines || [],
|
|
755
|
+
executed: fileCoverage.executed_lines || []
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
} catch (error) {
|
|
759
|
+
return {
|
|
760
|
+
error: error.message,
|
|
761
|
+
available: false
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async getRustCoverage(file) {
|
|
767
|
+
try {
|
|
768
|
+
// Try to check if cargo-tarpaulin is available
|
|
769
|
+
try {
|
|
770
|
+
await execAsync('cargo tarpaulin --version');
|
|
771
|
+
} catch {
|
|
772
|
+
return {
|
|
773
|
+
error: 'cargo-tarpaulin not installed',
|
|
774
|
+
available: false,
|
|
775
|
+
suggestion: 'Install with: cargo install cargo-tarpaulin'
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const projectDir = path.dirname(file);
|
|
780
|
+
let rootDir = projectDir;
|
|
781
|
+
|
|
782
|
+
// Find Cargo.toml root
|
|
783
|
+
while (rootDir !== path.dirname(rootDir)) {
|
|
784
|
+
if (fsSync.existsSync(path.join(rootDir, 'Cargo.toml'))) {
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
rootDir = path.dirname(rootDir);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const result = await execAsync(`cargo tarpaulin --out Json --output-dir /tmp`, {
|
|
791
|
+
cwd: rootDir
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
const coverageData = JSON.parse(await fs.promises.readFile('/tmp/tarpaulin-report.json', 'utf8'));
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
lines: {
|
|
798
|
+
total: coverageData.files[file]?.total_lines || 0,
|
|
799
|
+
covered: coverageData.files[file]?.covered_lines || 0,
|
|
800
|
+
percentage: coverageData.files[file]?.coverage || 0
|
|
801
|
+
},
|
|
802
|
+
overall: {
|
|
803
|
+
percentage: coverageData.coverage || 0
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
} catch (error) {
|
|
808
|
+
return {
|
|
809
|
+
error: error.message,
|
|
810
|
+
available: false
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async getGoCoverage(file) {
|
|
816
|
+
try {
|
|
817
|
+
const result = await execAsync(`go test -coverprofile=/tmp/coverage.out "${file}"`);
|
|
818
|
+
|
|
819
|
+
const coverageOutput = await fs.promises.readFile('/tmp/coverage.out', 'utf8');
|
|
820
|
+
const lines = coverageOutput.split('\n').filter(line => line.includes(file));
|
|
821
|
+
|
|
822
|
+
let totalStatements = 0;
|
|
823
|
+
let coveredStatements = 0;
|
|
824
|
+
|
|
825
|
+
lines.forEach(line => {
|
|
826
|
+
const match = line.match(/(\d+)\s+(\d+)\s+(\d+)/);
|
|
827
|
+
if (match) {
|
|
828
|
+
const count = parseInt(match[3]);
|
|
829
|
+
totalStatements++;
|
|
830
|
+
if (count > 0) coveredStatements++;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
const percentage = totalStatements > 0 ? Math.round((coveredStatements / totalStatements) * 100) : 0;
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
lines: {
|
|
838
|
+
total: totalStatements,
|
|
839
|
+
covered: coveredStatements,
|
|
840
|
+
percentage
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
} catch (error) {
|
|
845
|
+
return {
|
|
846
|
+
error: error.message,
|
|
847
|
+
available: false
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// TDD compliance checking
|
|
853
|
+
checkTDDCompliance(sourceFile, testFile, testResults) {
|
|
854
|
+
const compliance = {
|
|
855
|
+
hasTests: false,
|
|
856
|
+
testFirst: false,
|
|
857
|
+
redGreenRefactor: false,
|
|
858
|
+
coverage: 0,
|
|
859
|
+
testsPassed: 0,
|
|
860
|
+
testsFailed: 0,
|
|
861
|
+
recommendations: []
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
// Check if tests exist
|
|
865
|
+
if (testFile && this.fileExistsSync(testFile)) {
|
|
866
|
+
compliance.hasTests = true;
|
|
867
|
+
} else {
|
|
868
|
+
compliance.recommendations.push({
|
|
869
|
+
type: 'tdd_violation',
|
|
870
|
+
priority: 'high',
|
|
871
|
+
message: 'No test file found - TDD requires tests first',
|
|
872
|
+
action: `Create test file: ${this.suggestTestFileName(sourceFile)}`
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Check test results
|
|
877
|
+
if (testResults && testResults.summary) {
|
|
878
|
+
const { total, passed, failed } = testResults.summary;
|
|
879
|
+
|
|
880
|
+
compliance.testsPassed = passed;
|
|
881
|
+
compliance.testsFailed = failed;
|
|
882
|
+
|
|
883
|
+
if (total === 0) {
|
|
884
|
+
compliance.recommendations.push({
|
|
885
|
+
type: 'tdd_violation',
|
|
886
|
+
priority: 'high',
|
|
887
|
+
message: 'No tests found in test file',
|
|
888
|
+
action: 'Write tests before implementing functionality'
|
|
889
|
+
});
|
|
890
|
+
} else if (failed > 0) {
|
|
891
|
+
compliance.redGreenRefactor = true;
|
|
892
|
+
compliance.recommendations.push({
|
|
893
|
+
type: 'tdd_red_phase',
|
|
894
|
+
priority: 'medium',
|
|
895
|
+
message: `${failed} failing tests - in RED phase of TDD`,
|
|
896
|
+
action: 'Implement minimal code to make tests pass'
|
|
897
|
+
});
|
|
898
|
+
} else if (passed > 0) {
|
|
899
|
+
compliance.redGreenRefactor = true;
|
|
900
|
+
compliance.recommendations.push({
|
|
901
|
+
type: 'tdd_green_phase',
|
|
902
|
+
priority: 'low',
|
|
903
|
+
message: 'All tests passing - in GREEN phase of TDD',
|
|
904
|
+
action: 'Consider refactoring for better design'
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return compliance;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Utility methods
|
|
913
|
+
isTestFile(file) {
|
|
914
|
+
const fileName = path.basename(file);
|
|
915
|
+
return fileName.includes('.test.') ||
|
|
916
|
+
fileName.includes('.spec.') ||
|
|
917
|
+
fileName.includes('_test') ||
|
|
918
|
+
fileName.endsWith('Test.java') ||
|
|
919
|
+
fileName.endsWith('Test.cpp') ||
|
|
920
|
+
file.includes('/test/') ||
|
|
921
|
+
file.includes('/tests/');
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
findTestFile(sourceFile) {
|
|
925
|
+
const ext = path.extname(sourceFile);
|
|
926
|
+
const base = path.basename(sourceFile, ext);
|
|
927
|
+
const dir = path.dirname(sourceFile);
|
|
928
|
+
|
|
929
|
+
const testPatterns = [
|
|
930
|
+
`${base}.test${ext}`,
|
|
931
|
+
`${base}.spec${ext}`,
|
|
932
|
+
`${base}_test${ext}`,
|
|
933
|
+
`test_${base}${ext}`,
|
|
934
|
+
`${base}Test${ext}`
|
|
935
|
+
];
|
|
936
|
+
|
|
937
|
+
for (const pattern of testPatterns) {
|
|
938
|
+
const testPath = path.join(dir, pattern);
|
|
939
|
+
if (this.fileExistsSync(testPath)) return testPath;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const testDirs = ['test', 'tests', '__tests__', 'spec'];
|
|
943
|
+
for (const testDir of testDirs) {
|
|
944
|
+
for (const pattern of testPatterns) {
|
|
945
|
+
const testPath = path.join(dir, testDir, pattern);
|
|
946
|
+
if (this.fileExistsSync(testPath)) return testPath;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
findSourceFile(testFile) {
|
|
954
|
+
const ext = path.extname(testFile);
|
|
955
|
+
let base = path.basename(testFile, ext);
|
|
956
|
+
|
|
957
|
+
base = base.replace(/\.(test|spec)$/, '')
|
|
958
|
+
.replace(/_test$/, '')
|
|
959
|
+
.replace(/^test_/, '')
|
|
960
|
+
.replace(/Test$/, '');
|
|
961
|
+
|
|
962
|
+
const dir = path.dirname(testFile);
|
|
963
|
+
const sourcePatterns = [`${base}${ext}`];
|
|
964
|
+
|
|
965
|
+
const parentDir = path.dirname(dir);
|
|
966
|
+
for (const pattern of sourcePatterns) {
|
|
967
|
+
const sourcePath = path.join(parentDir, pattern);
|
|
968
|
+
if (this.fileExistsSync(sourcePath)) return sourcePath;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
for (const pattern of sourcePatterns) {
|
|
972
|
+
const sourcePath = path.join(dir, pattern);
|
|
973
|
+
if (this.fileExistsSync(sourcePath)) return sourcePath;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
suggestTestFileName(sourceFile) {
|
|
980
|
+
const ext = path.extname(sourceFile);
|
|
981
|
+
const base = path.basename(sourceFile, ext);
|
|
982
|
+
const dir = path.dirname(sourceFile);
|
|
983
|
+
|
|
984
|
+
if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
|
|
985
|
+
return path.join(dir, `${base}.test${ext}`);
|
|
986
|
+
} else if (ext === '.py') {
|
|
987
|
+
return path.join(dir, `test_${base}${ext}`);
|
|
988
|
+
} else if (ext === '.go') {
|
|
989
|
+
return path.join(dir, `${base}_test${ext}`);
|
|
990
|
+
} else if (ext === '.java') {
|
|
991
|
+
return path.join(dir, `${base}Test${ext}`);
|
|
992
|
+
} else {
|
|
993
|
+
return path.join(dir, `${base}_test${ext}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
calculatePercentage(coverage, isBranch = false) {
|
|
998
|
+
if (!coverage) return 0;
|
|
999
|
+
|
|
1000
|
+
const values = isBranch ? Object.values(coverage).flat() : Object.values(coverage);
|
|
1001
|
+
const total = values.length;
|
|
1002
|
+
const covered = values.filter(v => v > 0).length;
|
|
1003
|
+
|
|
1004
|
+
return total > 0 ? Math.round((covered / total) * 100) : 0;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
fileExistsSync(filePath) {
|
|
1008
|
+
try {
|
|
1009
|
+
fsSync.accessSync(filePath, fsSync.constants.F_OK);
|
|
1010
|
+
return true;
|
|
1011
|
+
} catch {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
async fileExists(filePath) {
|
|
1017
|
+
try {
|
|
1018
|
+
await fs.promises.access(filePath, fs.constants.F_OK);
|
|
1019
|
+
return true;
|
|
1020
|
+
} catch {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Rust-specific quality enforcement with enhanced validation
|
|
1027
|
+
class RustQualityEnforcer {
|
|
1028
|
+
constructor(options = {}) {
|
|
1029
|
+
this.config = {
|
|
1030
|
+
allowUnwrap: options.allowUnwrap !== undefined ? options.allowUnwrap : false,
|
|
1031
|
+
allowExpect: options.allowExpect !== undefined ? options.allowExpect : true,
|
|
1032
|
+
allowPanic: options.allowPanic !== undefined ? options.allowPanic : false,
|
|
1033
|
+
allowTodo: options.allowTodo !== undefined ? options.allowTodo : false,
|
|
1034
|
+
allowUnimplemented: options.allowUnimplemented !== undefined ? options.allowUnimplemented : false
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async analyzeFile(filePath, content) {
|
|
1039
|
+
const issues = [];
|
|
1040
|
+
const lines = content.split('\n');
|
|
1041
|
+
|
|
1042
|
+
// Regex patterns that skip comments
|
|
1043
|
+
const unwrapPattern = /(?<!\/\/.*)\.unwrap\(\)/;
|
|
1044
|
+
const expectPattern = /(?<!\/\/.*)\.expect\(/;
|
|
1045
|
+
const panicPattern = /(?<!\/\/)panic!\(/;
|
|
1046
|
+
const todoPattern = /(?<!\/\/)todo!\(/;
|
|
1047
|
+
const unimplementedPattern = /(?<!\/\/)unimplemented!\(/;
|
|
1048
|
+
|
|
1049
|
+
lines.forEach((line, index) => {
|
|
1050
|
+
// Remove comments from the line for accurate analysis
|
|
1051
|
+
const cleanedLine = line.replace(/\/\/.*$/, '').replace(/\/\*[\s\S]*?\*\//, '');
|
|
1052
|
+
|
|
1053
|
+
if (unwrapPattern.test(cleanedLine)) {
|
|
1054
|
+
if (!this.config.allowUnwrap) {
|
|
1055
|
+
issues.push({
|
|
1056
|
+
type: 'rust_unwrap',
|
|
1057
|
+
severity: 'critical',
|
|
1058
|
+
priority: 'high',
|
|
1059
|
+
message: 'Use of .unwrap() in production code - may panic',
|
|
1060
|
+
line: index + 1,
|
|
1061
|
+
code: line.trim(),
|
|
1062
|
+
suggestion: 'Use match, if let, or ? operator instead',
|
|
1063
|
+
action: 'Replace .unwrap() with proper error handling'
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (expectPattern.test(cleanedLine)) {
|
|
1069
|
+
if (!this.config.allowExpect) {
|
|
1070
|
+
issues.push({
|
|
1071
|
+
type: 'rust_expect',
|
|
1072
|
+
severity: 'warning',
|
|
1073
|
+
priority: 'medium',
|
|
1074
|
+
message: 'Use of .expect() may panic at runtime',
|
|
1075
|
+
line: index + 1,
|
|
1076
|
+
code: line.trim(),
|
|
1077
|
+
suggestion: 'Consider propagating errors with ?',
|
|
1078
|
+
action: 'Replace .expect() with proper error handling'
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (panicPattern.test(cleanedLine)) {
|
|
1084
|
+
if (!this.config.allowPanic) {
|
|
1085
|
+
issues.push({
|
|
1086
|
+
type: 'rust_panic',
|
|
1087
|
+
severity: 'critical',
|
|
1088
|
+
priority: 'high',
|
|
1089
|
+
message: 'panic!() will crash the program',
|
|
1090
|
+
line: index + 1,
|
|
1091
|
+
code: line.trim(),
|
|
1092
|
+
suggestion: 'Return Result<T, E> and handle errors properly',
|
|
1093
|
+
action: 'Replace panic!() with error propagation'
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (todoPattern.test(cleanedLine)) {
|
|
1099
|
+
if (!this.config.allowTodo) {
|
|
1100
|
+
issues.push({
|
|
1101
|
+
type: 'rust_todo',
|
|
1102
|
+
severity: 'error',
|
|
1103
|
+
priority: 'high',
|
|
1104
|
+
message: 'todo!() detected - incomplete code',
|
|
1105
|
+
line: index + 1,
|
|
1106
|
+
code: line.trim(),
|
|
1107
|
+
suggestion: 'Implement the missing functionality',
|
|
1108
|
+
action: 'Complete the implementation'
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (unimplementedPattern.test(cleanedLine)) {
|
|
1114
|
+
if (!this.config.allowUnimplemented) {
|
|
1115
|
+
issues.push({
|
|
1116
|
+
type: 'rust_unimplemented',
|
|
1117
|
+
severity: 'error',
|
|
1118
|
+
priority: 'high',
|
|
1119
|
+
message: 'unimplemented!() detected - incomplete code',
|
|
1120
|
+
line: index + 1,
|
|
1121
|
+
code: line.trim(),
|
|
1122
|
+
suggestion: 'Implement the missing functionality',
|
|
1123
|
+
action: 'Complete the implementation'
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
return {
|
|
1130
|
+
passed: issues.filter(i => i.severity === 'error' || i.severity === 'critical').length === 0,
|
|
1131
|
+
issues,
|
|
1132
|
+
suggestions: issues.map(i => i.suggestion).filter((v, i, a) => a.indexOf(v) === i),
|
|
1133
|
+
coverage: 'advanced',
|
|
1134
|
+
summary: {
|
|
1135
|
+
unwrap: issues.filter(i => i.type === 'rust_unwrap').length,
|
|
1136
|
+
expect: issues.filter(i => i.type === 'rust_expect').length,
|
|
1137
|
+
panic: issues.filter(i => i.type === 'rust_panic').length,
|
|
1138
|
+
todo: issues.filter(i => i.type === 'rust_todo').length,
|
|
1139
|
+
unimplemented: issues.filter(i => i.type === 'rust_unimplemented').length
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Main unified pipeline
|
|
1146
|
+
class UnifiedPostEditPipeline {
|
|
1147
|
+
constructor(options = {}) {
|
|
1148
|
+
this.tddMode = options.tddMode || false;
|
|
1149
|
+
this.minimumCoverage = options.minimumCoverage || 80;
|
|
1150
|
+
this.blockOnTDDViolations = options.blockOnTDDViolations || false;
|
|
1151
|
+
this.rustStrict = options.rustStrict || false;
|
|
1152
|
+
this.config = this.loadConfig();
|
|
1153
|
+
this.testEngine = new SingleFileTestEngine();
|
|
1154
|
+
this.rustEnforcer = new RustQualityEnforcer({
|
|
1155
|
+
allowUnwrap: !this.rustStrict,
|
|
1156
|
+
allowExpect: !this.rustStrict, // In strict mode, also warn about .expect()
|
|
1157
|
+
allowPanic: !this.rustStrict,
|
|
1158
|
+
allowTodo: false,
|
|
1159
|
+
allowUnimplemented: false
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
this.languageDetectors = {
|
|
1163
|
+
'.js': 'javascript',
|
|
1164
|
+
'.jsx': 'javascript',
|
|
1165
|
+
'.ts': 'typescript',
|
|
1166
|
+
'.tsx': 'typescript',
|
|
1167
|
+
'.py': 'python',
|
|
1168
|
+
'.rs': 'rust',
|
|
1169
|
+
'.go': 'go',
|
|
1170
|
+
'.java': 'java',
|
|
1171
|
+
'.cpp': 'cpp',
|
|
1172
|
+
'.c': 'c',
|
|
1173
|
+
'.php': 'php',
|
|
1174
|
+
'.rb': 'ruby',
|
|
1175
|
+
'.cs': 'csharp',
|
|
1176
|
+
'.json': 'json',
|
|
1177
|
+
'.yaml': 'yaml',
|
|
1178
|
+
'.yml': 'yaml',
|
|
1179
|
+
'.toml': 'toml',
|
|
1180
|
+
'.md': 'markdown',
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
loadConfig() {
|
|
1185
|
+
const configPath = path.join(process.cwd(), 'config', 'hooks', 'pipeline-config.json');
|
|
1186
|
+
try {
|
|
1187
|
+
if (fs.existsSync(configPath)) {
|
|
1188
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1189
|
+
}
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
console.warn('Using default pipeline configuration');
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return {
|
|
1195
|
+
formatters: {
|
|
1196
|
+
javascript: ['prettier', '--write'],
|
|
1197
|
+
typescript: ['prettier', '--write'],
|
|
1198
|
+
python: ['black', '--quiet'],
|
|
1199
|
+
rust: ['rustfmt'],
|
|
1200
|
+
go: ['gofmt', '-w'],
|
|
1201
|
+
json: ['prettier', '--write'],
|
|
1202
|
+
yaml: ['prettier', '--write'],
|
|
1203
|
+
markdown: ['prettier', '--write']
|
|
1204
|
+
},
|
|
1205
|
+
linters: {
|
|
1206
|
+
javascript: ['eslint', '--fix'],
|
|
1207
|
+
typescript: ['eslint', '--fix'],
|
|
1208
|
+
python: ['flake8'],
|
|
1209
|
+
rust: ['clippy'],
|
|
1210
|
+
go: ['golint']
|
|
1211
|
+
},
|
|
1212
|
+
typeCheckers: {
|
|
1213
|
+
typescript: ['tsc', '--noEmit'],
|
|
1214
|
+
python: ['mypy'],
|
|
1215
|
+
rust: ['cargo', 'check'],
|
|
1216
|
+
go: ['go', 'vet']
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
detectLanguage(filePath) {
|
|
1222
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1223
|
+
return this.languageDetectors[ext] || 'unknown';
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async runCommand(command, args, cwd = process.cwd()) {
|
|
1227
|
+
return new Promise((resolve) => {
|
|
1228
|
+
const proc = spawn(command, args, {
|
|
1229
|
+
cwd,
|
|
1230
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
let stdout = '';
|
|
1234
|
+
let stderr = '';
|
|
1235
|
+
|
|
1236
|
+
proc.stdout.on('data', (data) => stdout += data.toString());
|
|
1237
|
+
proc.stderr.on('data', (data) => stderr += data.toString());
|
|
1238
|
+
|
|
1239
|
+
proc.on('close', (code) => {
|
|
1240
|
+
resolve({ code, stdout, stderr });
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
proc.on('error', (error) => {
|
|
1244
|
+
resolve({ code: 1, stdout: '', stderr: error.message });
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
async checkToolAvailable(tool) {
|
|
1250
|
+
try {
|
|
1251
|
+
const { code } = await this.runCommand('which', [tool]);
|
|
1252
|
+
return code === 0;
|
|
1253
|
+
} catch {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
async formatFile(filePath, language) {
|
|
1259
|
+
const formatters = this.config.formatters[language];
|
|
1260
|
+
if (!formatters) return { success: true, message: 'No formatter configured' };
|
|
1261
|
+
|
|
1262
|
+
const [tool, ...args] = formatters;
|
|
1263
|
+
if (!(await this.checkToolAvailable(tool))) {
|
|
1264
|
+
return { success: false, message: `Formatter ${tool} not available` };
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const result = await this.runCommand(tool, [...args, filePath]);
|
|
1268
|
+
return {
|
|
1269
|
+
success: result.code === 0,
|
|
1270
|
+
message: result.code === 0 ? 'Formatted successfully' : result.stderr,
|
|
1271
|
+
output: result.stdout
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
async lintFile(filePath, language) {
|
|
1276
|
+
const linters = this.config.linters[language];
|
|
1277
|
+
if (!linters) return { success: true, message: 'No linter configured' };
|
|
1278
|
+
|
|
1279
|
+
const [tool, ...args] = linters;
|
|
1280
|
+
if (!(await this.checkToolAvailable(tool))) {
|
|
1281
|
+
return { success: false, message: `Linter ${tool} not available` };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const result = await this.runCommand(tool, [...args, filePath]);
|
|
1285
|
+
return {
|
|
1286
|
+
success: result.code === 0,
|
|
1287
|
+
message: result.code === 0 ? 'Linting passed' : 'Linting issues found',
|
|
1288
|
+
output: result.stdout + result.stderr,
|
|
1289
|
+
issues: result.code !== 0 ? result.stderr : ''
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
async typeCheck(filePath, language) {
|
|
1294
|
+
const typeCheckers = this.config.typeCheckers[language];
|
|
1295
|
+
if (!typeCheckers) return { success: true, message: 'No type checker configured' };
|
|
1296
|
+
|
|
1297
|
+
const [tool, ...args] = typeCheckers;
|
|
1298
|
+
if (!(await this.checkToolAvailable(tool))) {
|
|
1299
|
+
return { success: false, message: `Type checker ${tool} not available` };
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const projectDir = this.findProjectRoot(filePath);
|
|
1303
|
+
const result = await this.runCommand(tool, args, projectDir);
|
|
1304
|
+
|
|
1305
|
+
return {
|
|
1306
|
+
success: result.code === 0,
|
|
1307
|
+
message: result.code === 0 ? 'Type checking passed' : 'Type errors found',
|
|
1308
|
+
output: result.stdout + result.stderr,
|
|
1309
|
+
errors: result.code !== 0 ? result.stderr : ''
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
findProjectRoot(filePath) {
|
|
1314
|
+
const markers = ['package.json', 'Cargo.toml', 'go.mod', 'pyproject.toml', 'setup.py'];
|
|
1315
|
+
let dir = path.dirname(filePath);
|
|
1316
|
+
|
|
1317
|
+
while (dir !== path.dirname(dir)) {
|
|
1318
|
+
if (markers.some(marker => fs.existsSync(path.join(dir, marker)))) {
|
|
1319
|
+
return dir;
|
|
1320
|
+
}
|
|
1321
|
+
dir = path.dirname(dir);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
return process.cwd();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
extractAgentContext(options = {}) {
|
|
1328
|
+
const context = {
|
|
1329
|
+
memoryKey: options.memoryKey || process.env.MEMORY_KEY || null,
|
|
1330
|
+
agentType: options.agentType || process.env.AGENT_TYPE || null,
|
|
1331
|
+
agentName: options.agentName || process.env.AGENT_NAME || null,
|
|
1332
|
+
swarmId: options.swarmId || process.env.SWARM_ID || null,
|
|
1333
|
+
taskId: options.taskId || process.env.TASK_ID || null,
|
|
1334
|
+
sessionId: options.sessionId || process.env.SESSION_ID || null
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
if (context.memoryKey && !context.agentType) {
|
|
1338
|
+
const keyParts = context.memoryKey.split('/');
|
|
1339
|
+
if (keyParts.length >= 2) {
|
|
1340
|
+
context.agentType = keyParts[1];
|
|
1341
|
+
}
|
|
1342
|
+
if (keyParts.length >= 3) {
|
|
1343
|
+
context.taskStep = keyParts[2];
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
return context;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
formatTimestamp(isoTimestamp) {
|
|
1351
|
+
const date = new Date(isoTimestamp);
|
|
1352
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
1353
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
1354
|
+
const year = date.getFullYear();
|
|
1355
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
1356
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
1357
|
+
|
|
1358
|
+
return `${month}/${day}/${year} ${hours}:${minutes}`;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
async logToRootFile(results) {
|
|
1362
|
+
const logPath = path.join(process.cwd(), 'post-edit-pipeline.log');
|
|
1363
|
+
const MAX_ENTRIES = 500;
|
|
1364
|
+
|
|
1365
|
+
const logEntry = {
|
|
1366
|
+
timestamp: results.timestamp,
|
|
1367
|
+
displayTimestamp: this.formatTimestamp(results.timestamp),
|
|
1368
|
+
file: results.file,
|
|
1369
|
+
editId: results.editId || 'N/A',
|
|
1370
|
+
language: results.language,
|
|
1371
|
+
agent: results.agentContext,
|
|
1372
|
+
status: results.summary.success ? 'PASSED' : (results.blocking ? 'BLOCKED' : 'FAILED'),
|
|
1373
|
+
tddMode: this.tddMode,
|
|
1374
|
+
tddPhase: results.tddPhase || 'N/A',
|
|
1375
|
+
errors: results.summary.errors.length,
|
|
1376
|
+
warnings: results.summary.warnings.length,
|
|
1377
|
+
steps: results.steps || {},
|
|
1378
|
+
testing: results.testing || {},
|
|
1379
|
+
coverage: results.coverage || {},
|
|
1380
|
+
tddCompliance: results.tddCompliance || {},
|
|
1381
|
+
rustQuality: results.rustQuality || {},
|
|
1382
|
+
recommendations: results.recommendations || [],
|
|
1383
|
+
details: {
|
|
1384
|
+
errors: results.summary.errors,
|
|
1385
|
+
warnings: results.summary.warnings,
|
|
1386
|
+
suggestions: results.summary.suggestions
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
const logText = [
|
|
1391
|
+
'═'.repeat(80),
|
|
1392
|
+
`TIMESTAMP: ${logEntry.displayTimestamp}`,
|
|
1393
|
+
`FILE: ${logEntry.file}`,
|
|
1394
|
+
`EDIT ID: ${logEntry.editId}`,
|
|
1395
|
+
`LANGUAGE: ${logEntry.language}`,
|
|
1396
|
+
`STATUS: ${logEntry.status}`,
|
|
1397
|
+
`TDD MODE: ${logEntry.tddMode ? 'ENABLED' : 'DISABLED'}`,
|
|
1398
|
+
`TDD PHASE: ${logEntry.tddPhase}`,
|
|
1399
|
+
'',
|
|
1400
|
+
'AGENT CONTEXT:',
|
|
1401
|
+
` Memory Key: ${logEntry.agent.memoryKey || 'N/A'}`,
|
|
1402
|
+
` Agent Type: ${logEntry.agent.agentType || 'N/A'}`,
|
|
1403
|
+
'',
|
|
1404
|
+
'JSON:',
|
|
1405
|
+
JSON.stringify(logEntry, null, 2),
|
|
1406
|
+
'═'.repeat(80),
|
|
1407
|
+
'',
|
|
1408
|
+
''
|
|
1409
|
+
].join('\n');
|
|
1410
|
+
|
|
1411
|
+
try {
|
|
1412
|
+
let existingEntries = [];
|
|
1413
|
+
if (fs.existsSync(logPath)) {
|
|
1414
|
+
const existingLog = fs.readFileSync(logPath, 'utf8');
|
|
1415
|
+
const entrySections = existingLog.split('═'.repeat(80)).filter(s => s.trim());
|
|
1416
|
+
|
|
1417
|
+
for (const section of entrySections) {
|
|
1418
|
+
const jsonStart = section.indexOf('JSON:');
|
|
1419
|
+
if (jsonStart !== -1) {
|
|
1420
|
+
const jsonText = section.substring(jsonStart + 5).trim();
|
|
1421
|
+
let braceCount = 0;
|
|
1422
|
+
let jsonEnd = 0;
|
|
1423
|
+
let inString = false;
|
|
1424
|
+
let escapeNext = false;
|
|
1425
|
+
|
|
1426
|
+
for (let i = 0; i < jsonText.length; i++) {
|
|
1427
|
+
const char = jsonText[i];
|
|
1428
|
+
|
|
1429
|
+
if (escapeNext) {
|
|
1430
|
+
escapeNext = false;
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (char === '\\') {
|
|
1435
|
+
escapeNext = true;
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (char === '"') {
|
|
1440
|
+
inString = !inString;
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (!inString) {
|
|
1445
|
+
if (char === '{') braceCount++;
|
|
1446
|
+
if (char === '}') {
|
|
1447
|
+
braceCount--;
|
|
1448
|
+
if (braceCount === 0) {
|
|
1449
|
+
jsonEnd = i + 1;
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (jsonEnd > 0) {
|
|
1457
|
+
try {
|
|
1458
|
+
const entry = JSON.parse(jsonText.substring(0, jsonEnd));
|
|
1459
|
+
existingEntries.push(entry);
|
|
1460
|
+
} catch (e) {
|
|
1461
|
+
console.error(`Failed to parse JSON entry: ${e.message}`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
existingEntries.unshift(logEntry);
|
|
1469
|
+
|
|
1470
|
+
if (existingEntries.length > MAX_ENTRIES) {
|
|
1471
|
+
existingEntries = existingEntries.slice(0, MAX_ENTRIES);
|
|
1472
|
+
console.log(`\n🗑️ Trimmed log to ${MAX_ENTRIES} most recent entries`);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const rebuiltLog = existingEntries.map(entry => {
|
|
1476
|
+
return [
|
|
1477
|
+
'═'.repeat(80),
|
|
1478
|
+
`TIMESTAMP: ${entry.displayTimestamp}`,
|
|
1479
|
+
`FILE: ${entry.file}`,
|
|
1480
|
+
`EDIT ID: ${entry.editId || 'N/A'}`,
|
|
1481
|
+
`LANGUAGE: ${entry.language}`,
|
|
1482
|
+
`STATUS: ${entry.status}`,
|
|
1483
|
+
`TDD MODE: ${entry.tddMode ? 'ENABLED' : 'DISABLED'}`,
|
|
1484
|
+
`TDD PHASE: ${entry.tddPhase || 'N/A'}`,
|
|
1485
|
+
'',
|
|
1486
|
+
'AGENT CONTEXT:',
|
|
1487
|
+
` Memory Key: ${entry.agent?.memoryKey || 'N/A'}`,
|
|
1488
|
+
` Agent Type: ${entry.agent?.agentType || 'N/A'}`,
|
|
1489
|
+
'',
|
|
1490
|
+
'JSON:',
|
|
1491
|
+
JSON.stringify(entry, null, 2),
|
|
1492
|
+
'═'.repeat(80),
|
|
1493
|
+
'',
|
|
1494
|
+
''
|
|
1495
|
+
].join('\n');
|
|
1496
|
+
}).join('');
|
|
1497
|
+
|
|
1498
|
+
fs.writeFileSync(logPath, rebuiltLog, 'utf8');
|
|
1499
|
+
|
|
1500
|
+
console.log(`\n📝 Logged to: ${logPath} (${existingEntries.length}/${MAX_ENTRIES} entries)`);
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
console.error(`⚠️ Failed to write log: ${error.message}`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
async run(filePath, options = {}) {
|
|
1507
|
+
const language = this.detectLanguage(filePath);
|
|
1508
|
+
|
|
1509
|
+
// Bypass non-code files (config/documentation)
|
|
1510
|
+
const bypassExtensions = ['.toml', '.md', '.txt', '.json', '.yaml', '.yml'];
|
|
1511
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1512
|
+
if (bypassExtensions.includes(ext)) {
|
|
1513
|
+
console.log(`\n⏭️ BYPASSED: ${ext} files don't require validation`);
|
|
1514
|
+
return {
|
|
1515
|
+
file: filePath,
|
|
1516
|
+
language,
|
|
1517
|
+
timestamp: new Date().toISOString(),
|
|
1518
|
+
editId: `edit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
1519
|
+
agentContext: this.extractAgentContext(options),
|
|
1520
|
+
status: 'BYPASSED',
|
|
1521
|
+
bypassed: true,
|
|
1522
|
+
reason: `${ext} files are configuration/documentation and don't require validation`,
|
|
1523
|
+
summary: {
|
|
1524
|
+
success: true,
|
|
1525
|
+
warnings: [],
|
|
1526
|
+
errors: [],
|
|
1527
|
+
suggestions: []
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Auto-enable Rust strict mode for .rs files
|
|
1533
|
+
if (language === 'rust' && !this.rustStrict) {
|
|
1534
|
+
this.rustStrict = true;
|
|
1535
|
+
console.log('🦀 Auto-enabled Rust strict mode for .rs file');
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const results = {
|
|
1539
|
+
file: filePath,
|
|
1540
|
+
language,
|
|
1541
|
+
timestamp: new Date().toISOString(),
|
|
1542
|
+
editId: `edit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
1543
|
+
agentContext: this.extractAgentContext(options),
|
|
1544
|
+
steps: {},
|
|
1545
|
+
testing: null,
|
|
1546
|
+
coverage: null,
|
|
1547
|
+
tddCompliance: null,
|
|
1548
|
+
tddPhase: 'unknown',
|
|
1549
|
+
rustQuality: null,
|
|
1550
|
+
recommendations: [],
|
|
1551
|
+
blocking: false,
|
|
1552
|
+
summary: {
|
|
1553
|
+
success: true,
|
|
1554
|
+
warnings: [],
|
|
1555
|
+
errors: [],
|
|
1556
|
+
suggestions: []
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
console.log(`\n🔍 UNIFIED POST-EDIT PIPELINE`);
|
|
1561
|
+
console.log(`📄 File: ${path.basename(filePath)}`);
|
|
1562
|
+
console.log(`📋 Language: ${language.toUpperCase()}`);
|
|
1563
|
+
console.log(`🧪 TDD Mode: ${this.tddMode ? 'ENABLED' : 'DISABLED'}`);
|
|
1564
|
+
|
|
1565
|
+
let content = '';
|
|
1566
|
+
try {
|
|
1567
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
results.summary.errors.push(`Cannot read file: ${error.message}`);
|
|
1570
|
+
results.summary.success = false;
|
|
1571
|
+
await this.logToRootFile(results);
|
|
1572
|
+
return results;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Step 1: Format
|
|
1576
|
+
console.log('\n📝 FORMATTING...');
|
|
1577
|
+
results.steps.formatting = await this.formatFile(filePath, language);
|
|
1578
|
+
this.logStepResult('Format', results.steps.formatting);
|
|
1579
|
+
|
|
1580
|
+
// Step 2: Lint
|
|
1581
|
+
console.log('\n🔍 LINTING...');
|
|
1582
|
+
results.steps.linting = await this.lintFile(filePath, language);
|
|
1583
|
+
this.logStepResult('Lint', results.steps.linting);
|
|
1584
|
+
if (!results.steps.linting.success) {
|
|
1585
|
+
results.summary.warnings.push(`Linting issues in ${path.basename(filePath)}`);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// Step 3: Type Check
|
|
1589
|
+
console.log('\n🎯 TYPE CHECKING...');
|
|
1590
|
+
results.steps.typeCheck = await this.typeCheck(filePath, language);
|
|
1591
|
+
this.logStepResult('Type Check', results.steps.typeCheck);
|
|
1592
|
+
if (!results.steps.typeCheck.success) {
|
|
1593
|
+
results.summary.errors.push(`Type errors in ${path.basename(filePath)}`);
|
|
1594
|
+
results.summary.success = false;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// Step 4: Rust Quality Enforcement (if Rust and strict mode)
|
|
1598
|
+
if (language === 'rust' && this.rustStrict) {
|
|
1599
|
+
console.log('\n🦀 RUST QUALITY ENFORCEMENT...');
|
|
1600
|
+
results.rustQuality = await this.rustEnforcer.analyzeFile(filePath, content);
|
|
1601
|
+
|
|
1602
|
+
if (!results.rustQuality.passed) {
|
|
1603
|
+
console.log(` ❌ Rust quality issues found`);
|
|
1604
|
+
results.rustQuality.issues.forEach(issue => {
|
|
1605
|
+
console.log(` [${issue.severity.toUpperCase()}] ${issue.message}`);
|
|
1606
|
+
results.recommendations.push(issue);
|
|
1607
|
+
|
|
1608
|
+
if (issue.severity === 'error') {
|
|
1609
|
+
results.summary.errors.push(issue.message);
|
|
1610
|
+
results.summary.success = false;
|
|
1611
|
+
} else {
|
|
1612
|
+
results.summary.warnings.push(issue.message);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
} else {
|
|
1616
|
+
console.log(` ✅ Rust quality checks passed`);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Step 5: TDD Testing (if enabled)
|
|
1621
|
+
if (this.tddMode) {
|
|
1622
|
+
console.log('\n🧪 TDD TESTING...');
|
|
1623
|
+
results.testing = await this.testEngine.executeTests(filePath, content);
|
|
1624
|
+
|
|
1625
|
+
if (results.testing.executed) {
|
|
1626
|
+
Logger.success(`Tests executed with ${results.testing.framework}`);
|
|
1627
|
+
|
|
1628
|
+
if (results.testing.results) {
|
|
1629
|
+
const { total, passed, failed } = results.testing.results.summary;
|
|
1630
|
+
Logger.test(`Test results: ${passed}/${total} passed, ${failed} failed`);
|
|
1631
|
+
|
|
1632
|
+
if (failed > 0) {
|
|
1633
|
+
results.tddPhase = 'red';
|
|
1634
|
+
Logger.tdd('TDD Phase: RED (failing tests)');
|
|
1635
|
+
} else if (passed > 0) {
|
|
1636
|
+
results.tddPhase = 'green';
|
|
1637
|
+
Logger.tdd('TDD Phase: GREEN (passing tests)');
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
} else {
|
|
1641
|
+
Logger.warning(`Tests not executed: ${results.testing.reason}`);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Step 6: Coverage Analysis
|
|
1645
|
+
if (results.testing.coverage) {
|
|
1646
|
+
results.coverage = results.testing.coverage;
|
|
1647
|
+
|
|
1648
|
+
if (results.coverage.lines) {
|
|
1649
|
+
const coveragePercent = results.coverage.lines.percentage;
|
|
1650
|
+
Logger.coverage(`Line coverage: ${coveragePercent}%`);
|
|
1651
|
+
|
|
1652
|
+
if (coveragePercent < this.minimumCoverage) {
|
|
1653
|
+
Logger.warning(`Coverage below minimum (${this.minimumCoverage}%)`);
|
|
1654
|
+
results.recommendations.push({
|
|
1655
|
+
type: 'coverage',
|
|
1656
|
+
priority: 'medium',
|
|
1657
|
+
message: `Increase test coverage from ${coveragePercent}% to ${this.minimumCoverage}%`,
|
|
1658
|
+
action: 'Add tests for uncovered lines and branches'
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Step 7: TDD Compliance
|
|
1665
|
+
results.tddCompliance = results.testing.tddCompliance;
|
|
1666
|
+
|
|
1667
|
+
if (results.tddCompliance) {
|
|
1668
|
+
if (!results.tddCompliance.hasTests) {
|
|
1669
|
+
Logger.error('TDD Violation: No tests found');
|
|
1670
|
+
|
|
1671
|
+
if (this.blockOnTDDViolations) {
|
|
1672
|
+
results.blocking = true;
|
|
1673
|
+
results.summary.success = false;
|
|
1674
|
+
Logger.error('BLOCKING: TDD requires tests first');
|
|
1675
|
+
}
|
|
1676
|
+
} else {
|
|
1677
|
+
Logger.success('TDD Compliance: Tests exist');
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (results.tddCompliance.recommendations) {
|
|
1681
|
+
results.recommendations.push(...results.tddCompliance.recommendations);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Generate summary
|
|
1687
|
+
this.printSummary(results);
|
|
1688
|
+
|
|
1689
|
+
// Log to root file
|
|
1690
|
+
await this.logToRootFile(results);
|
|
1691
|
+
|
|
1692
|
+
return results;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
logStepResult(step, result) {
|
|
1696
|
+
if (result.success) {
|
|
1697
|
+
console.log(` ✅ ${step}: ${result.message}`);
|
|
1698
|
+
} else {
|
|
1699
|
+
console.log(` ❌ ${step}: ${result.message}`);
|
|
1700
|
+
if (result.issues || result.errors) {
|
|
1701
|
+
console.log(` ${(result.issues || result.errors).slice(0, 200)}...`);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
printSummary(results) {
|
|
1707
|
+
console.log('\n' + '='.repeat(60));
|
|
1708
|
+
console.log('📊 VALIDATION SUMMARY');
|
|
1709
|
+
console.log('='.repeat(60));
|
|
1710
|
+
|
|
1711
|
+
if (results.blocking) {
|
|
1712
|
+
console.log('🚫 Overall Status: BLOCKED');
|
|
1713
|
+
} else if (results.summary.success) {
|
|
1714
|
+
console.log('✅ Overall Status: PASSED');
|
|
1715
|
+
} else {
|
|
1716
|
+
console.log('❌ Overall Status: FAILED');
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
if (this.tddMode && results.testing) {
|
|
1720
|
+
console.log(`\n🧪 TDD Phase: ${results.tddPhase.toUpperCase()}`);
|
|
1721
|
+
if (results.testing.executed && results.testing.results) {
|
|
1722
|
+
const { total, passed, failed } = results.testing.results.summary;
|
|
1723
|
+
console.log(` Tests: ${passed}/${total} passed, ${failed} failed`);
|
|
1724
|
+
}
|
|
1725
|
+
if (results.coverage && results.coverage.lines) {
|
|
1726
|
+
console.log(` Coverage: ${results.coverage.lines.percentage}%`);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (results.summary.errors.length > 0) {
|
|
1731
|
+
console.log('\n🚨 ERRORS:');
|
|
1732
|
+
results.summary.errors.forEach(error => console.log(` • ${error}`));
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (results.summary.warnings.length > 0) {
|
|
1736
|
+
console.log('\n⚠️ WARNINGS:');
|
|
1737
|
+
results.summary.warnings.forEach(warning => console.log(` • ${warning}`));
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
if (results.recommendations.length > 0) {
|
|
1741
|
+
console.log('\n💡 RECOMMENDATIONS:');
|
|
1742
|
+
results.recommendations.slice(0, 5).forEach((rec, i) => {
|
|
1743
|
+
console.log(` ${i + 1}. [${rec.priority.toUpperCase()}] ${rec.message}`);
|
|
1744
|
+
if (rec.action) console.log(` Action: ${rec.action}`);
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
console.log('='.repeat(60));
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// CLI execution
|
|
1753
|
+
async function main() {
|
|
1754
|
+
const filePath = process.argv[2];
|
|
1755
|
+
|
|
1756
|
+
if (!filePath) {
|
|
1757
|
+
console.log(`
|
|
1758
|
+
🔴🟢♻️ UNIFIED POST-EDIT PIPELINE - v3.0.0
|
|
1759
|
+
|
|
1760
|
+
Usage: post-edit-pipeline.js <file> [options]
|
|
1761
|
+
|
|
1762
|
+
Options:
|
|
1763
|
+
--memory-key <key> Store results with specific memory key
|
|
1764
|
+
--tdd-mode Enable TDD testing and enforcement
|
|
1765
|
+
--minimum-coverage <percent> Minimum coverage threshold (default: 80)
|
|
1766
|
+
--block-on-tdd-violations Block execution on TDD violations
|
|
1767
|
+
--rust-strict Enable strict Rust quality checks
|
|
1768
|
+
--structured Return structured JSON data
|
|
1769
|
+
|
|
1770
|
+
Examples:
|
|
1771
|
+
node post-edit-pipeline.js src/app.js --tdd-mode --minimum-coverage 90
|
|
1772
|
+
node post-edit-pipeline.js src/lib.rs --rust-strict
|
|
1773
|
+
node post-edit-pipeline.js src/test.ts --tdd-mode --block-on-tdd-violations
|
|
1774
|
+
|
|
1775
|
+
Features:
|
|
1776
|
+
✅ Progressive validation (syntax → interface → integration → full)
|
|
1777
|
+
✅ TDD enforcement with Red-Green-Refactor detection
|
|
1778
|
+
✅ Single-file testing without full compilation
|
|
1779
|
+
✅ Real-time coverage analysis
|
|
1780
|
+
✅ Rust quality enforcement (.unwrap(), panic!, todo! detection)
|
|
1781
|
+
✅ Multi-language support (JS, TS, Python, Rust, Go, Java, C/C++)
|
|
1782
|
+
✅ Security scanning and dependency analysis
|
|
1783
|
+
✅ Agent coordination and memory storage
|
|
1784
|
+
`);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
if (!fs.existsSync(filePath)) {
|
|
1789
|
+
console.error(`File not found: ${filePath}`);
|
|
1790
|
+
process.exit(1);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const args = process.argv.slice(3);
|
|
1794
|
+
const options = {};
|
|
1795
|
+
|
|
1796
|
+
// Parse command-line options
|
|
1797
|
+
for (let i = 0; i < args.length; i++) {
|
|
1798
|
+
if (args[i] === '--memory-key' && args[i + 1]) {
|
|
1799
|
+
options.memoryKey = args[i + 1];
|
|
1800
|
+
i++;
|
|
1801
|
+
} else if (args[i] === '--agent-type' && args[i + 1]) {
|
|
1802
|
+
options.agentType = args[i + 1];
|
|
1803
|
+
i++;
|
|
1804
|
+
} else if (args[i] === '--minimum-coverage' && args[i + 1]) {
|
|
1805
|
+
options.minimumCoverage = parseInt(args[i + 1]) || 80;
|
|
1806
|
+
i++;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
const pipelineOptions = {
|
|
1811
|
+
tddMode: args.includes('--tdd-mode'),
|
|
1812
|
+
minimumCoverage: options.minimumCoverage || 80,
|
|
1813
|
+
blockOnTDDViolations: args.includes('--block-on-tdd-violations'),
|
|
1814
|
+
rustStrict: args.includes('--rust-strict'),
|
|
1815
|
+
structured: args.includes('--structured')
|
|
1816
|
+
};
|
|
1817
|
+
|
|
1818
|
+
const pipeline = new UnifiedPostEditPipeline(pipelineOptions);
|
|
1819
|
+
const results = await pipeline.run(filePath, options);
|
|
1820
|
+
|
|
1821
|
+
if (pipelineOptions.structured) {
|
|
1822
|
+
console.log('\n📋 STRUCTURED OUTPUT:');
|
|
1823
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
process.exit(results.summary.success && !results.blocking ? 0 : 1);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// Run if called directly
|
|
1830
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1831
|
+
main().catch(error => {
|
|
1832
|
+
console.error('Pipeline error:', error);
|
|
1833
|
+
process.exit(1);
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
export { UnifiedPostEditPipeline, SingleFileTestEngine, RustQualityEnforcer };
|