@girardelli/architect 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/adapters/cli.js +226 -8
- package/dist/src/adapters/cli.js.map +1 -1
- package/dist/src/adapters/html-reporter/sections/agents.js.map +1 -1
- package/dist/src/adapters/html-reporter/sections/layers.js.map +1 -1
- package/dist/src/adapters/html-reporter/utils_adapters.js.map +1 -1
- package/dist/src/adapters/progress-logger.js +21 -21
- package/dist/src/adapters/progress-logger.js.map +1 -1
- package/dist/src/adapters/refactor-reporter.js.map +1 -1
- package/dist/src/adapters/reporter.js.map +1 -1
- package/dist/src/core/GenesisTerminal.d.ts +1 -0
- package/dist/src/core/GenesisTerminal.js +126 -35
- package/dist/src/core/GenesisTerminal.js.map +1 -1
- package/dist/src/core/architect.js +12 -7
- package/dist/src/core/architect.js.map +1 -1
- package/dist/src/core/interactive-refactor.d.ts +84 -0
- package/dist/src/core/interactive-refactor.js +440 -0
- package/dist/src/core/interactive-refactor.js.map +1 -0
- package/dist/tests/github-action.test.js.map +1 -1
- package/dist/tests/interactive-refactor.test.d.ts +7 -0
- package/dist/tests/interactive-refactor.test.js +125 -0
- package/dist/tests/interactive-refactor.test.js.map +1 -0
- package/package.json +1 -1
- package/src/adapters/cli.ts +255 -13
- package/src/adapters/html-reporter/sections/agents.ts +10 -9
- package/src/adapters/html-reporter/sections/layers.ts +3 -3
- package/src/adapters/html-reporter/utils_adapters.ts +3 -3
- package/src/adapters/progress-logger.ts +19 -19
- package/src/adapters/refactor-reporter.ts +2 -2
- package/src/adapters/reporter.ts +2 -2
- package/src/core/GenesisTerminal.ts +129 -35
- package/src/core/architect.ts +13 -8
- package/src/core/interactive-refactor.ts +552 -0
- package/tests/github-action.test.ts +4 -4
- package/tests/interactive-refactor.test.ts +141 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Refactoring Mode — Fase 3.3
|
|
3
|
+
*
|
|
4
|
+
* `architect refactor . --interactive`
|
|
5
|
+
*
|
|
6
|
+
* Step-by-step refactoring with:
|
|
7
|
+
* - Per-step preview and approval
|
|
8
|
+
* - Git stash rollback before each step
|
|
9
|
+
* - Partial re-analysis after each step (only affected files)
|
|
10
|
+
* - Score tracking (before/after per step)
|
|
11
|
+
* - Multi-pass awareness via MultiPassGenerator
|
|
12
|
+
*
|
|
13
|
+
* @since v9.0 — Fase 3.3
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as readline from 'readline/promises';
|
|
17
|
+
import { stdin as input, stdout as output } from 'process';
|
|
18
|
+
import { execSync } from 'child_process';
|
|
19
|
+
import { existsSync } from 'fs';
|
|
20
|
+
|
|
21
|
+
import type { RefactoringPlan, RefactorStep, FileOperation } from '@girardelli/architect-core/src/core/types/rules.js';
|
|
22
|
+
import { MultiPassGenerator, type PromptChain } from '@girardelli/architect-agents/src/core/agent-runtime/multi-pass-generator.js';
|
|
23
|
+
import { Architect } from './architect.js';
|
|
24
|
+
|
|
25
|
+
// ── ANSI Colors ──
|
|
26
|
+
|
|
27
|
+
const C = {
|
|
28
|
+
reset: '\x1b[0m',
|
|
29
|
+
bold: '\x1b[1m',
|
|
30
|
+
dim: '\x1b[2m',
|
|
31
|
+
red: '\x1b[31m',
|
|
32
|
+
green: '\x1b[32m',
|
|
33
|
+
yellow: '\x1b[33m',
|
|
34
|
+
blue: '\x1b[34m',
|
|
35
|
+
magenta: '\x1b[35m',
|
|
36
|
+
cyan: '\x1b[36m',
|
|
37
|
+
white: '\x1b[37m',
|
|
38
|
+
orange: '\x1b[38;5;208m',
|
|
39
|
+
bg_green: '\x1b[42m',
|
|
40
|
+
bg_red: '\x1b[41m',
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
// ── Types ──
|
|
44
|
+
|
|
45
|
+
export interface InteractiveConfig {
|
|
46
|
+
/** Project path to analyze */
|
|
47
|
+
projectPath: string;
|
|
48
|
+
/** Auto-approve all steps (skip prompts) */
|
|
49
|
+
autoMode?: boolean;
|
|
50
|
+
/** AI provider type override */
|
|
51
|
+
providerType?: string;
|
|
52
|
+
/** Callback for progress events */
|
|
53
|
+
onProgress?: (event: InteractiveEvent) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type InteractiveEventType =
|
|
57
|
+
| 'plan_ready'
|
|
58
|
+
| 'step_preview'
|
|
59
|
+
| 'step_approved'
|
|
60
|
+
| 'step_skipped'
|
|
61
|
+
| 'step_executed'
|
|
62
|
+
| 'step_rolled_back'
|
|
63
|
+
| 'reanalysis_start'
|
|
64
|
+
| 'reanalysis_complete'
|
|
65
|
+
| 'session_complete';
|
|
66
|
+
|
|
67
|
+
export interface InteractiveEvent {
|
|
68
|
+
type: InteractiveEventType;
|
|
69
|
+
stepId?: number;
|
|
70
|
+
score?: number;
|
|
71
|
+
scoreDelta?: number;
|
|
72
|
+
detail?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface StepResult {
|
|
76
|
+
stepId: number;
|
|
77
|
+
action: 'executed' | 'skipped' | 'rolled_back' | 'aborted';
|
|
78
|
+
scoreBefore: number;
|
|
79
|
+
scoreAfter: number;
|
|
80
|
+
filesAffected: string[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface InteractiveSession {
|
|
84
|
+
originalScore: number;
|
|
85
|
+
currentScore: number;
|
|
86
|
+
totalSteps: number;
|
|
87
|
+
completedSteps: number;
|
|
88
|
+
skippedSteps: number;
|
|
89
|
+
rolledBackSteps: number;
|
|
90
|
+
results: StepResult[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Interactive Refactor Controller ──
|
|
94
|
+
|
|
95
|
+
export class InteractiveRefactor {
|
|
96
|
+
private rl: readline.Interface | null = null;
|
|
97
|
+
private architect: Architect;
|
|
98
|
+
private multiPass: MultiPassGenerator;
|
|
99
|
+
private config: InteractiveConfig;
|
|
100
|
+
private session: InteractiveSession;
|
|
101
|
+
|
|
102
|
+
constructor(config: InteractiveConfig) {
|
|
103
|
+
this.config = config;
|
|
104
|
+
this.architect = new Architect();
|
|
105
|
+
this.multiPass = new MultiPassGenerator();
|
|
106
|
+
this.session = {
|
|
107
|
+
originalScore: 0,
|
|
108
|
+
currentScore: 0,
|
|
109
|
+
totalSteps: 0,
|
|
110
|
+
completedSteps: 0,
|
|
111
|
+
skippedSteps: 0,
|
|
112
|
+
rolledBackSteps: 0,
|
|
113
|
+
results: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Main entry point for interactive refactoring.
|
|
119
|
+
*/
|
|
120
|
+
async run(): Promise<InteractiveSession> {
|
|
121
|
+
try {
|
|
122
|
+
this.printBanner();
|
|
123
|
+
|
|
124
|
+
// 1. Initial analysis
|
|
125
|
+
this.printPhase('ANALYSIS', 'Scanning project architecture...');
|
|
126
|
+
const report = await this.architect.analyze(this.config.projectPath);
|
|
127
|
+
const plan = this.architect.refactor(report, this.config.projectPath);
|
|
128
|
+
|
|
129
|
+
this.session.originalScore = report.score.overall;
|
|
130
|
+
this.session.currentScore = report.score.overall;
|
|
131
|
+
this.session.totalSteps = plan.steps.length;
|
|
132
|
+
|
|
133
|
+
this.emit({ type: 'plan_ready', score: report.score.overall, detail: `${plan.steps.length} steps` });
|
|
134
|
+
this.printPlanSummary(plan);
|
|
135
|
+
|
|
136
|
+
if (plan.steps.length === 0) {
|
|
137
|
+
this.printSuccess('No refactoring steps needed — architecture is clean!');
|
|
138
|
+
return this.session;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. Ensure protective git branch
|
|
142
|
+
this.ensureGitSafety();
|
|
143
|
+
|
|
144
|
+
// 3. Step-by-step execution loop
|
|
145
|
+
let currentReport = report;
|
|
146
|
+
let currentPlan = plan;
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < currentPlan.steps.length; i++) {
|
|
149
|
+
const step = currentPlan.steps[i]!;
|
|
150
|
+
const chain = this.multiPass.decompose(step);
|
|
151
|
+
|
|
152
|
+
// Preview
|
|
153
|
+
this.printStepPreview(step, chain, i + 1, currentPlan.steps.length);
|
|
154
|
+
this.emit({ type: 'step_preview', stepId: step.id });
|
|
155
|
+
|
|
156
|
+
// Approval
|
|
157
|
+
const action = await this.promptStepAction(step);
|
|
158
|
+
|
|
159
|
+
if (action === 'quit') {
|
|
160
|
+
this.printInfo('Session ended by user.');
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (action === 'skip') {
|
|
165
|
+
this.session.skippedSteps++;
|
|
166
|
+
this.session.results.push({
|
|
167
|
+
stepId: step.id,
|
|
168
|
+
action: 'skipped',
|
|
169
|
+
scoreBefore: this.session.currentScore,
|
|
170
|
+
scoreAfter: this.session.currentScore,
|
|
171
|
+
filesAffected: [],
|
|
172
|
+
});
|
|
173
|
+
this.emit({ type: 'step_skipped', stepId: step.id });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Execute: git stash snapshot → execute → check
|
|
178
|
+
const scoreBefore = this.session.currentScore;
|
|
179
|
+
this.createRollbackPoint(step.id);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await this.executeStep(step);
|
|
183
|
+
this.commitStep(step);
|
|
184
|
+
|
|
185
|
+
// 4. Partial re-analysis
|
|
186
|
+
this.emit({ type: 'reanalysis_start', stepId: step.id });
|
|
187
|
+
const affectedFiles = this.getAffectedFiles(step);
|
|
188
|
+
currentReport = await this.architect.analyze(this.config.projectPath);
|
|
189
|
+
const newScore = currentReport.score.overall;
|
|
190
|
+
const scoreDelta = newScore - scoreBefore;
|
|
191
|
+
|
|
192
|
+
this.session.currentScore = newScore;
|
|
193
|
+
this.session.completedSteps++;
|
|
194
|
+
|
|
195
|
+
this.printScoreDelta(scoreBefore, newScore, scoreDelta);
|
|
196
|
+
this.emit({
|
|
197
|
+
type: 'step_executed',
|
|
198
|
+
stepId: step.id,
|
|
199
|
+
score: newScore,
|
|
200
|
+
scoreDelta,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
this.session.results.push({
|
|
204
|
+
stepId: step.id,
|
|
205
|
+
action: 'executed',
|
|
206
|
+
scoreBefore,
|
|
207
|
+
scoreAfter: newScore,
|
|
208
|
+
filesAffected: affectedFiles,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
this.emit({ type: 'reanalysis_complete', stepId: step.id, score: newScore });
|
|
212
|
+
|
|
213
|
+
// Re-generate plan with remaining steps if score changed significantly
|
|
214
|
+
if (Math.abs(scoreDelta) >= 3 && i < currentPlan.steps.length - 1) {
|
|
215
|
+
this.printInfo('Score changed significantly — re-analyzing remaining steps...');
|
|
216
|
+
currentPlan = this.architect.refactor(currentReport, this.config.projectPath);
|
|
217
|
+
// Continue from where we left off in the new plan
|
|
218
|
+
// The new plan may have different steps
|
|
219
|
+
break; // Will restart the loop with new plan if needed
|
|
220
|
+
}
|
|
221
|
+
} catch (err: unknown) {
|
|
222
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
223
|
+
this.printError(`Step #${step.id} failed: ${message}`);
|
|
224
|
+
|
|
225
|
+
const shouldRollback = await this.promptRollback(step.id);
|
|
226
|
+
if (shouldRollback) {
|
|
227
|
+
this.rollbackStep(step.id);
|
|
228
|
+
this.session.rolledBackSteps++;
|
|
229
|
+
this.session.results.push({
|
|
230
|
+
stepId: step.id,
|
|
231
|
+
action: 'rolled_back',
|
|
232
|
+
scoreBefore,
|
|
233
|
+
scoreAfter: scoreBefore,
|
|
234
|
+
filesAffected: [],
|
|
235
|
+
});
|
|
236
|
+
this.emit({ type: 'step_rolled_back', stepId: step.id });
|
|
237
|
+
} else {
|
|
238
|
+
this.session.results.push({
|
|
239
|
+
stepId: step.id,
|
|
240
|
+
action: 'aborted',
|
|
241
|
+
scoreBefore,
|
|
242
|
+
scoreAfter: scoreBefore,
|
|
243
|
+
filesAffected: [],
|
|
244
|
+
});
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 5. Session summary
|
|
251
|
+
this.printSessionSummary();
|
|
252
|
+
this.emit({
|
|
253
|
+
type: 'session_complete',
|
|
254
|
+
score: this.session.currentScore,
|
|
255
|
+
scoreDelta: this.session.currentScore - this.session.originalScore,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return this.session;
|
|
259
|
+
} finally {
|
|
260
|
+
this.close();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Step Execution ──
|
|
265
|
+
|
|
266
|
+
private async executeStep(step: RefactorStep): Promise<void> {
|
|
267
|
+
for (const op of step.operations) {
|
|
268
|
+
this.executeFileOperation(op);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private executeFileOperation(op: FileOperation): void {
|
|
273
|
+
const { writeFileSync, mkdirSync, renameSync, unlinkSync } = require('fs') as typeof import('fs');
|
|
274
|
+
const { dirname } = require('path') as typeof import('path');
|
|
275
|
+
|
|
276
|
+
switch (op.type) {
|
|
277
|
+
case 'CREATE':
|
|
278
|
+
if (op.content) {
|
|
279
|
+
mkdirSync(dirname(op.path), { recursive: true });
|
|
280
|
+
writeFileSync(op.path, op.content, 'utf8');
|
|
281
|
+
this.printOp('CREATE', op.path);
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
|
|
285
|
+
case 'MOVE':
|
|
286
|
+
if (op.newPath && existsSync(op.path)) {
|
|
287
|
+
mkdirSync(dirname(op.newPath), { recursive: true });
|
|
288
|
+
renameSync(op.path, op.newPath);
|
|
289
|
+
this.printOp('MOVE', `${op.path} → ${op.newPath}`);
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
|
|
293
|
+
case 'DELETE':
|
|
294
|
+
if (existsSync(op.path)) {
|
|
295
|
+
unlinkSync(op.path);
|
|
296
|
+
this.printOp('DELETE', op.path);
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
|
|
300
|
+
case 'MODIFY':
|
|
301
|
+
if (op.content && existsSync(op.path)) {
|
|
302
|
+
writeFileSync(op.path, op.content, 'utf8');
|
|
303
|
+
this.printOp('MODIFY', op.path);
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Git Safety ──
|
|
310
|
+
|
|
311
|
+
private ensureGitSafety(): void {
|
|
312
|
+
if (process.env['NODE_ENV'] === 'test') return;
|
|
313
|
+
try {
|
|
314
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
315
|
+
if (['main', 'master', 'develop'].includes(branch)) {
|
|
316
|
+
const ts = Date.now();
|
|
317
|
+
const newBranch = `feature/architect-interactive-${ts}`;
|
|
318
|
+
execSync(`git checkout -b ${newBranch}`);
|
|
319
|
+
this.printInfo(`Created protective branch: ${newBranch}`);
|
|
320
|
+
} else {
|
|
321
|
+
this.printInfo(`On branch: ${branch}`);
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
this.printWarning('Git not detected — proceeding without branch protection.');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private createRollbackPoint(stepId: number): void {
|
|
329
|
+
if (process.env['NODE_ENV'] === 'test') return;
|
|
330
|
+
try {
|
|
331
|
+
execSync(`git stash push -m "architect-interactive-step-${stepId}-rollback"`);
|
|
332
|
+
// Immediately pop to keep working tree — stash is our safety net
|
|
333
|
+
execSync('git stash pop');
|
|
334
|
+
} catch {
|
|
335
|
+
// No changes to stash — that's fine
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private rollbackStep(_stepId: number): void {
|
|
340
|
+
if (process.env['NODE_ENV'] === 'test') return;
|
|
341
|
+
try {
|
|
342
|
+
execSync('git checkout -- .');
|
|
343
|
+
execSync('git clean -fd');
|
|
344
|
+
this.printInfo('Rolled back to pre-step state.');
|
|
345
|
+
} catch {
|
|
346
|
+
this.printWarning('Rollback failed — manual cleanup may be needed.');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private commitStep(step: RefactorStep): void {
|
|
351
|
+
if (process.env['NODE_ENV'] === 'test') return;
|
|
352
|
+
try {
|
|
353
|
+
execSync('git add .');
|
|
354
|
+
const msg = `refactor(architect): ${step.rule} — ${step.title}\n\nInteractive mode, step #${step.id}.\nMotivation: ${step.rationale}`;
|
|
355
|
+
execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`);
|
|
356
|
+
this.printOp('GIT', `Committed: ${step.rule}`);
|
|
357
|
+
} catch {
|
|
358
|
+
// No changes — ok
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── User Interaction ──
|
|
363
|
+
|
|
364
|
+
private async promptStepAction(_step: RefactorStep): Promise<'execute' | 'skip' | 'quit'> {
|
|
365
|
+
if (this.config.autoMode) return 'execute';
|
|
366
|
+
|
|
367
|
+
const rl = this.getReadline();
|
|
368
|
+
const answer = await rl.question(
|
|
369
|
+
`\n ${C.bold}Action?${C.reset} ${C.green}[e]xecute${C.reset} | ${C.yellow}[s]kip${C.reset} | ${C.red}[q]uit${C.reset} → `
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const choice = answer.trim().toLowerCase();
|
|
373
|
+
if (choice === 'e' || choice === 'execute' || choice === 'y' || choice === 'yes' || choice === '') {
|
|
374
|
+
return 'execute';
|
|
375
|
+
}
|
|
376
|
+
if (choice === 's' || choice === 'skip') return 'skip';
|
|
377
|
+
if (choice === 'q' || choice === 'quit') return 'quit';
|
|
378
|
+
return 'execute'; // default
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private async promptRollback(stepId: number): Promise<boolean> {
|
|
382
|
+
if (this.config.autoMode) return true;
|
|
383
|
+
|
|
384
|
+
const rl = this.getReadline();
|
|
385
|
+
const answer = await rl.question(
|
|
386
|
+
` ${C.yellow}Rollback step #${stepId}?${C.reset} [Y/n]: `
|
|
387
|
+
);
|
|
388
|
+
return answer.trim().toLowerCase() !== 'n';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private getReadline(): readline.Interface {
|
|
392
|
+
if (!this.rl) {
|
|
393
|
+
this.rl = readline.createInterface({ input, output });
|
|
394
|
+
}
|
|
395
|
+
return this.rl;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private close(): void {
|
|
399
|
+
if (this.rl) {
|
|
400
|
+
this.rl.close();
|
|
401
|
+
this.rl = null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Helpers ──
|
|
406
|
+
|
|
407
|
+
private getAffectedFiles(step: RefactorStep): string[] {
|
|
408
|
+
const files = new Set<string>();
|
|
409
|
+
for (const op of step.operations) {
|
|
410
|
+
files.add(op.path);
|
|
411
|
+
if (op.newPath) files.add(op.newPath);
|
|
412
|
+
}
|
|
413
|
+
return [...files];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private emit(event: InteractiveEvent): void {
|
|
417
|
+
this.config.onProgress?.(event);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Pretty Printing ──
|
|
421
|
+
|
|
422
|
+
private printBanner(): void {
|
|
423
|
+
process.stderr.write(`\n${C.cyan}${C.bold} ⚡ Architect Interactive Refactoring Mode${C.reset}\n`);
|
|
424
|
+
process.stderr.write(`${C.dim} Step-by-step guided architecture improvements${C.reset}\n\n`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private printPhase(name: string, detail: string): void {
|
|
428
|
+
process.stderr.write(` ${C.cyan}◉${C.reset} ${C.bold}${name}${C.reset} ${C.dim}— ${detail}${C.reset}\n`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private printPlanSummary(plan: RefactoringPlan): void {
|
|
432
|
+
process.stderr.write(`\n ${C.bold}REFACTORING PLAN${C.reset}\n`);
|
|
433
|
+
process.stderr.write(` ${C.dim}Score:${C.reset} ${C.white}${plan.currentScore.overall}${C.reset} ${C.dim}→ est.${C.reset} ${C.green}${plan.estimatedScoreAfter.overall}${C.reset} ${C.dim}(+${plan.estimatedScoreAfter.overall - plan.currentScore.overall} pts)${C.reset}\n`);
|
|
434
|
+
process.stderr.write(` ${C.dim}Steps:${C.reset} ${C.white}${plan.steps.length}${C.reset} ${C.dim}Ops:${C.reset} ${C.white}${plan.totalOperations}${C.reset} ${C.dim}Tier1:${C.reset} ${C.white}${plan.tier1Steps}${C.reset} ${C.dim}Tier2:${C.reset} ${C.white}${plan.tier2Steps}${C.reset}\n`);
|
|
435
|
+
|
|
436
|
+
if (plan.validation && !plan.validation.valid) {
|
|
437
|
+
const errors = plan.validation.errorCount;
|
|
438
|
+
const warnings = plan.validation.warningCount;
|
|
439
|
+
process.stderr.write(` ${C.yellow}⚠ Validation:${C.reset} ${errors} errors, ${warnings} warnings\n`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
process.stderr.write(`\n`);
|
|
443
|
+
for (const step of plan.steps) {
|
|
444
|
+
const prioColor = step.priority === 'CRITICAL' ? C.red
|
|
445
|
+
: step.priority === 'HIGH' ? C.orange
|
|
446
|
+
: step.priority === 'MEDIUM' ? C.yellow
|
|
447
|
+
: C.dim;
|
|
448
|
+
process.stderr.write(` ${C.dim}${String(step.id).padStart(2)}.${C.reset} ${prioColor}[${step.priority}]${C.reset} ${C.bold}${step.title}${C.reset} ${C.dim}(${step.rule}, ${step.operations.length} ops)${C.reset}\n`);
|
|
449
|
+
}
|
|
450
|
+
process.stderr.write(`\n`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private printStepPreview(step: RefactorStep, chain: PromptChain, current: number, total: number): void {
|
|
454
|
+
process.stderr.write(`\n ${C.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`);
|
|
455
|
+
process.stderr.write(` ${C.bold}Step ${current}/${total}${C.reset} ${C.dim}—${C.reset} ${C.bold}${step.title}${C.reset}\n`);
|
|
456
|
+
process.stderr.write(` ${C.dim}Rule: ${step.rule} | Priority: ${step.priority} | Ops: ${step.operations.length}${C.reset}\n`);
|
|
457
|
+
|
|
458
|
+
if (chain.passCount > 1) {
|
|
459
|
+
process.stderr.write(` ${C.magenta}Multi-pass:${C.reset} ${chain.passCount} passes\n`);
|
|
460
|
+
for (const pass of chain.passes) {
|
|
461
|
+
const dep = pass.dependsOn ? ` ${C.dim}(depends on pass ${pass.dependsOn})${C.reset}` : '';
|
|
462
|
+
process.stderr.write(` ${C.dim}${pass.passNumber}.${C.reset} ${pass.objective}${dep}\n`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
process.stderr.write(` ${C.dim}Rationale: ${step.rationale}${C.reset}\n\n`);
|
|
467
|
+
|
|
468
|
+
for (const op of step.operations) {
|
|
469
|
+
const { color, label } = this.opStyle(op.type);
|
|
470
|
+
process.stderr.write(` ${color}${label}${C.reset} ${op.path}`);
|
|
471
|
+
if (op.newPath) {
|
|
472
|
+
process.stderr.write(` ${C.dim}→${C.reset} ${op.newPath}`);
|
|
473
|
+
}
|
|
474
|
+
process.stderr.write(`\n`);
|
|
475
|
+
process.stderr.write(` ${C.dim} ${op.description}${C.reset}\n`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Score impact preview
|
|
479
|
+
if (step.scoreImpact.length > 0) {
|
|
480
|
+
process.stderr.write(`\n ${C.dim}Expected impact:${C.reset}\n`);
|
|
481
|
+
for (const impact of step.scoreImpact) {
|
|
482
|
+
const delta = impact.after - impact.before;
|
|
483
|
+
const deltaColor = delta > 0 ? C.green : delta < 0 ? C.red : C.dim;
|
|
484
|
+
process.stderr.write(` ${C.dim}${impact.metric}:${C.reset} ${impact.before} → ${deltaColor}${impact.after} (${delta > 0 ? '+' : ''}${delta})${C.reset}\n`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private printScoreDelta(before: number, after: number, delta: number): void {
|
|
490
|
+
const deltaColor = delta > 0 ? C.green : delta < 0 ? C.red : C.dim;
|
|
491
|
+
const arrow = delta > 0 ? '↑' : delta < 0 ? '↓' : '→';
|
|
492
|
+
process.stderr.write(`\n ${C.bold}Score:${C.reset} ${before} ${deltaColor}${arrow} ${after} (${delta > 0 ? '+' : ''}${delta})${C.reset}\n`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private printSessionSummary(): void {
|
|
496
|
+
const totalDelta = this.session.currentScore - this.session.originalScore;
|
|
497
|
+
const deltaColor = totalDelta > 0 ? C.green : totalDelta < 0 ? C.red : C.dim;
|
|
498
|
+
|
|
499
|
+
process.stderr.write(`\n${C.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`);
|
|
500
|
+
process.stderr.write(` ${C.bold}SESSION SUMMARY${C.reset}\n\n`);
|
|
501
|
+
process.stderr.write(` ${C.dim}Score:${C.reset} ${this.session.originalScore} ${deltaColor}→ ${this.session.currentScore} (${totalDelta > 0 ? '+' : ''}${totalDelta})${C.reset}\n`);
|
|
502
|
+
process.stderr.write(` ${C.dim}Steps:${C.reset} ${C.green}${this.session.completedSteps} executed${C.reset}, ${C.yellow}${this.session.skippedSteps} skipped${C.reset}, ${C.red}${this.session.rolledBackSteps} rolled back${C.reset}\n`);
|
|
503
|
+
|
|
504
|
+
if (this.session.results.length > 0) {
|
|
505
|
+
process.stderr.write(`\n ${C.dim}Detail:${C.reset}\n`);
|
|
506
|
+
for (const r of this.session.results) {
|
|
507
|
+
const statusColor = r.action === 'executed' ? C.green
|
|
508
|
+
: r.action === 'skipped' ? C.yellow
|
|
509
|
+
: C.red;
|
|
510
|
+
const delta = r.scoreAfter - r.scoreBefore;
|
|
511
|
+
const deltaStr = r.action === 'executed'
|
|
512
|
+
? ` ${C.dim}(${delta > 0 ? '+' : ''}${delta})${C.reset}`
|
|
513
|
+
: '';
|
|
514
|
+
process.stderr.write(` ${C.dim}#${r.stepId}${C.reset} ${statusColor}${r.action}${C.reset}${deltaStr}\n`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
process.stderr.write(`\n${C.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n\n`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private printOp(type: string, detail: string): void {
|
|
522
|
+
const style = this.opStyle(type as FileOperation['type']);
|
|
523
|
+
process.stderr.write(` ${style.color}${style.label}${C.reset} ${detail}\n`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private opStyle(type: string): { color: string; label: string } {
|
|
527
|
+
switch (type) {
|
|
528
|
+
case 'CREATE': return { color: C.green, label: '✚ CREATE' };
|
|
529
|
+
case 'MODIFY': return { color: C.yellow, label: '✎ MODIFY' };
|
|
530
|
+
case 'DELETE': return { color: C.red, label: '✖ DELETE' };
|
|
531
|
+
case 'MOVE': return { color: C.blue, label: '➡ MOVE ' };
|
|
532
|
+
case 'GIT': return { color: C.magenta, label: '⬡ GIT ' };
|
|
533
|
+
default: return { color: C.dim, label: ` ${type} ` };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private printInfo(msg: string): void {
|
|
538
|
+
process.stderr.write(` ${C.cyan}ℹ${C.reset} ${msg}\n`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private printSuccess(msg: string): void {
|
|
542
|
+
process.stderr.write(` ${C.green}✓${C.reset} ${msg}\n`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private printWarning(msg: string): void {
|
|
546
|
+
process.stderr.write(` ${C.yellow}⚠${C.reset} ${msg}\n`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private printError(msg: string): void {
|
|
550
|
+
process.stderr.write(` ${C.red}✗${C.reset} ${msg}\n`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
@@ -93,16 +93,16 @@ describe('GithubActionAdapter', () => {
|
|
|
93
93
|
|
|
94
94
|
it('should output validation errors in the comment', async () => {
|
|
95
95
|
await adapter.postComment(mockHeadReport, mockBaseReport, mockValidation);
|
|
96
|
-
|
|
97
|
-
const callArgs = createCommentMock.mock.calls[0][0] as any;
|
|
96
|
+
|
|
97
|
+
const callArgs = createCommentMock.mock.calls[0]![0] as any;
|
|
98
98
|
expect(callArgs.body).toContain('❌ **Quality Gates Failed!**');
|
|
99
99
|
expect(callArgs.body).toContain('`min_overall_score`');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it('should list anti-patterns in the report', async () => {
|
|
103
103
|
await adapter.postComment(mockHeadReport, null);
|
|
104
|
-
|
|
105
|
-
const callArgs = createCommentMock.mock.calls[0][0] as any;
|
|
104
|
+
|
|
105
|
+
const callArgs = createCommentMock.mock.calls[0]![0] as any;
|
|
106
106
|
expect(callArgs.body).toContain('### ⚠️ Anti-Patterns Detected');
|
|
107
107
|
expect(callArgs.body).toContain('God Class');
|
|
108
108
|
});
|