@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.
Files changed (34) hide show
  1. package/dist/src/adapters/cli.js +226 -8
  2. package/dist/src/adapters/cli.js.map +1 -1
  3. package/dist/src/adapters/html-reporter/sections/agents.js.map +1 -1
  4. package/dist/src/adapters/html-reporter/sections/layers.js.map +1 -1
  5. package/dist/src/adapters/html-reporter/utils_adapters.js.map +1 -1
  6. package/dist/src/adapters/progress-logger.js +21 -21
  7. package/dist/src/adapters/progress-logger.js.map +1 -1
  8. package/dist/src/adapters/refactor-reporter.js.map +1 -1
  9. package/dist/src/adapters/reporter.js.map +1 -1
  10. package/dist/src/core/GenesisTerminal.d.ts +1 -0
  11. package/dist/src/core/GenesisTerminal.js +126 -35
  12. package/dist/src/core/GenesisTerminal.js.map +1 -1
  13. package/dist/src/core/architect.js +12 -7
  14. package/dist/src/core/architect.js.map +1 -1
  15. package/dist/src/core/interactive-refactor.d.ts +84 -0
  16. package/dist/src/core/interactive-refactor.js +440 -0
  17. package/dist/src/core/interactive-refactor.js.map +1 -0
  18. package/dist/tests/github-action.test.js.map +1 -1
  19. package/dist/tests/interactive-refactor.test.d.ts +7 -0
  20. package/dist/tests/interactive-refactor.test.js +125 -0
  21. package/dist/tests/interactive-refactor.test.js.map +1 -0
  22. package/package.json +1 -1
  23. package/src/adapters/cli.ts +255 -13
  24. package/src/adapters/html-reporter/sections/agents.ts +10 -9
  25. package/src/adapters/html-reporter/sections/layers.ts +3 -3
  26. package/src/adapters/html-reporter/utils_adapters.ts +3 -3
  27. package/src/adapters/progress-logger.ts +19 -19
  28. package/src/adapters/refactor-reporter.ts +2 -2
  29. package/src/adapters/reporter.ts +2 -2
  30. package/src/core/GenesisTerminal.ts +129 -35
  31. package/src/core/architect.ts +13 -8
  32. package/src/core/interactive-refactor.ts +552 -0
  33. package/tests/github-action.test.ts +4 -4
  34. 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
  });