@chemx/starter-kit 26.9.9-786 → 26.9.11-362

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 (40) hide show
  1. package/cli/audit/ast-visitors.js +243 -0
  2. package/cli/audit/discussion-store.js +39 -0
  3. package/cli/audit/history.d.ts +63 -0
  4. package/cli/audit/history.js +352 -0
  5. package/cli/audit/metrics.js +179 -0
  6. package/cli/audit/prompts.js +174 -0
  7. package/cli/audit/reporter-grades.js +259 -0
  8. package/cli/audit/reporter-markdown.js +136 -0
  9. package/cli/audit/reporter-utils.js +107 -0
  10. package/cli/audit/reporter.js +459 -0
  11. package/cli/audit/rules-helpers.js +109 -0
  12. package/cli/audit/rules-registry.js +97 -0
  13. package/cli/audit/rules.js +123 -0
  14. package/cli/audit/social-constants.js +4 -0
  15. package/cli/audit/social-gh.js +126 -0
  16. package/cli/audit/social-git.js +214 -0
  17. package/cli/audit/social-http.js +222 -0
  18. package/cli/audit/social-publisher.js +193 -0
  19. package/cli/audit/social.d.ts +66 -0
  20. package/cli/audit/social.js +254 -0
  21. package/cli/audit/types.d.ts +131 -0
  22. package/cli/audit.d.ts +3 -24
  23. package/cli/audit.js +195 -210
  24. package/cli/help.d.ts +1 -0
  25. package/cli/help.js +136 -0
  26. package/cli/index.js +128 -566
  27. package/cli/installer-templates.js +80 -0
  28. package/cli/installer.d.ts +20 -0
  29. package/cli/installer.js +56 -0
  30. package/cli/license.js +320 -0
  31. package/cli/navigator-conversion.js +100 -0
  32. package/cli/navigator-grades.js +70 -0
  33. package/cli/navigator-paged.js +86 -0
  34. package/cli/navigator-share.js +129 -0
  35. package/cli/navigator.d.ts +24 -0
  36. package/cli/navigator.js +400 -0
  37. package/cli/scaffold.js +148 -0
  38. package/cli/terminal.js +126 -0
  39. package/docs/CHANGELOG.md +73 -0
  40. package/package.json +11 -10
@@ -0,0 +1,243 @@
1
+ import * as t from '@babel/types';
2
+ import { RULE_REGISTRY } from './rules-registry.js';
3
+ import { countLogicalOperators } from './rules-helpers.js';
4
+
5
+ export const createAstVisitors = ({ relativePath, violations }) => {
6
+ return {
7
+ Function(astPath) {
8
+ const isCustomHook = (
9
+ (astPath.node.id && /^use[A-Z0-9]/.test(astPath.node.id.name)) ||
10
+ (astPath.parentPath?.node?.id && /^use[A-Z0-9]/.test(astPath.parentPath.node.id.name))
11
+ );
12
+
13
+ // Pillar 3: Hook Saturation
14
+ let hookCount = 0;
15
+ astPath.traverse({
16
+ CallExpression(callPath) {
17
+ const callee = callPath.node.callee;
18
+ if (t.isIdentifier(callee) && /^use[A-Z0-9]/.test(callee.name)) {
19
+ if (callPath.getFunctionParent() === astPath) {
20
+ hookCount += 1;
21
+ }
22
+ }
23
+ }
24
+ });
25
+
26
+ if (hookCount > 5) {
27
+ const line = astPath.node.loc?.start.line || 1;
28
+ const meta = RULE_REGISTRY.HOOK_SATURATION;
29
+ violations.push({
30
+ filePath: relativePath,
31
+ line,
32
+ column: astPath.node.loc?.start.column || 1,
33
+ hazard: `Hook saturation detected (${hookCount} hooks > 5 limit)`,
34
+ rule: 'HOOK_SATURATION',
35
+ severity: meta.severity,
36
+ pillar: meta.pillar,
37
+ directive: meta.directive
38
+ });
39
+ }
40
+
41
+ // Pillar 3: Hook Return Overload (3 to 5 limit)
42
+ if (isCustomHook) {
43
+ astPath.traverse({
44
+ ReturnStatement(retPath) {
45
+ if (retPath.getFunctionParent() === astPath && t.isObjectExpression(retPath.node.argument)) {
46
+ const propCount = retPath.node.argument.properties.length;
47
+ if (propCount > 5) {
48
+ const line = retPath.node.loc?.start.line || 1;
49
+ const meta = RULE_REGISTRY.HOOK_RETURN_OVERLOAD;
50
+ violations.push({
51
+ filePath: relativePath,
52
+ line,
53
+ column: retPath.node.loc?.start.column || 1,
54
+ hazard: `Hook return saturation (${propCount} properties > 5 limit)`,
55
+ rule: 'HOOK_RETURN_OVERLOAD',
56
+ severity: meta.severity,
57
+ pillar: meta.pillar,
58
+ directive: meta.directive
59
+ });
60
+ }
61
+ }
62
+ }
63
+ });
64
+ }
65
+ },
66
+
67
+ // Pillar 2: Control Flow Complexity
68
+ JSXExpressionContainer(astPath) {
69
+ const expr = astPath.node.expression;
70
+ if (t.isLogicalExpression(expr) || t.isUnaryExpression(expr)) {
71
+ const opCount = countLogicalOperators(expr);
72
+ if (opCount > 2) {
73
+ const line = expr.loc?.start.line || astPath.node.loc?.start.line || 1;
74
+ const meta = RULE_REGISTRY.CONTROL_FLOW_INLINE_BOOLEAN;
75
+ violations.push({
76
+ filePath: relativePath,
77
+ line,
78
+ column: expr.loc?.start.column || 1,
79
+ hazard: `Inline boolean complexity (${opCount} logical operators > 2 limit)`,
80
+ rule: 'CONTROL_FLOW_INLINE_BOOLEAN',
81
+ severity: meta.severity,
82
+ pillar: meta.pillar,
83
+ directive: meta.directive
84
+ });
85
+ }
86
+ }
87
+ },
88
+
89
+ ConditionalExpression(astPath) {
90
+ if (t.isConditionalExpression(astPath.node.consequent) || t.isConditionalExpression(astPath.node.alternate)) {
91
+ const line = astPath.node.loc?.start.line || 1;
92
+ const meta = RULE_REGISTRY.CONTROL_FLOW_NESTED_TERNARY;
93
+ violations.push({
94
+ filePath: relativePath,
95
+ line,
96
+ column: astPath.node.loc?.start.column || 1,
97
+ hazard: 'Nested ternary operator detected',
98
+ rule: 'CONTROL_FLOW_NESTED_TERNARY',
99
+ severity: meta.severity,
100
+ pillar: meta.pillar,
101
+ directive: meta.directive
102
+ });
103
+ }
104
+ },
105
+
106
+ // Pillar 5: Design System & Inline Styles
107
+ JSXAttribute(astPath) {
108
+ const attrName = astPath.node.name?.name;
109
+ if (attrName === 'style') {
110
+ const value = astPath.node.value;
111
+ if (t.isJSXExpressionContainer(value) && t.isObjectExpression(value.expression)) {
112
+ const line = astPath.node.loc?.start.line || 1;
113
+ const meta = RULE_REGISTRY.RAW_INLINE_STYLE;
114
+ violations.push({
115
+ filePath: relativePath,
116
+ line,
117
+ column: astPath.node.loc?.start.column || 1,
118
+ hazard: 'Raw inline style attribute detected in JSX',
119
+ rule: 'RAW_INLINE_STYLE',
120
+ severity: meta.severity,
121
+ pillar: meta.pillar,
122
+ directive: meta.directive
123
+ });
124
+ }
125
+ } else if (attrName === 'className' || attrName === 'class') {
126
+ const value = astPath.node.value;
127
+ const strVal = t.isStringLiteral(value) ? value.value : '';
128
+ if (strVal && (strVal.includes('fa-') || strVal.includes('fa '))) {
129
+ if (/\btext-(primary|secondary|danger|warning|success|info|light|dark|\w+)\b/.test(strVal)) {
130
+ const line = astPath.node.loc?.start.line || 1;
131
+ const meta = RULE_REGISTRY.ICON_SVG_STYLE_LEAK;
132
+ violations.push({
133
+ filePath: relativePath,
134
+ line,
135
+ column: astPath.node.loc?.start.column || 1,
136
+ hazard: 'FontAwesome icon with text-* class breaks SVG fill',
137
+ rule: 'ICON_SVG_STYLE_LEAK',
138
+ severity: meta.severity,
139
+ pillar: meta.pillar,
140
+ directive: meta.directive
141
+ });
142
+ }
143
+ }
144
+ }
145
+ },
146
+
147
+ // Pillar 6 & Pillar 7: Call Expressions (Timers & Logging)
148
+ CallExpression(astPath) {
149
+ const callee = astPath.node.callee;
150
+
151
+ // Timer Discipline
152
+ if (t.isIdentifier(callee) && (callee.name === 'setInterval' || callee.name === 'setTimeout')) {
153
+ const args = astPath.node.arguments;
154
+ const delayArg = args[1];
155
+
156
+ // Render-hack check: setTimeout(fn, 0)
157
+ if (callee.name === 'setTimeout' && delayArg && t.isNumericLiteral(delayArg) && delayArg.value === 0) {
158
+ const line = astPath.node.loc?.start.line || 1;
159
+ const meta = RULE_REGISTRY.RENDER_HACK_TIMEOUT;
160
+ violations.push({
161
+ filePath: relativePath,
162
+ line,
163
+ column: astPath.node.loc?.start.column || 1,
164
+ hazard: 'Zero-delay render hack setTimeout(..., 0) detected',
165
+ rule: 'RENDER_HACK_TIMEOUT',
166
+ severity: meta.severity,
167
+ pillar: meta.pillar,
168
+ directive: meta.directive
169
+ });
170
+ }
171
+
172
+ const fnParent = astPath.getFunctionParent();
173
+ let hasCleanup = false;
174
+ if (fnParent) {
175
+ fnParent.traverse({
176
+ ReturnStatement(retPath) {
177
+ if (retPath.node.argument) {
178
+ hasCleanup = true;
179
+ }
180
+ }
181
+ });
182
+ }
183
+
184
+ if (!hasCleanup) {
185
+ const line = astPath.node.loc?.start.line || 1;
186
+ const meta = RULE_REGISTRY.TIMER_DISCIPLINE;
187
+ violations.push({
188
+ filePath: relativePath,
189
+ line,
190
+ column: astPath.node.loc?.start.column || 1,
191
+ hazard: `Raw ${callee.name} lacking lifecycle scope disposal`,
192
+ rule: 'TIMER_DISCIPLINE',
193
+ severity: meta.severity,
194
+ pillar: meta.pillar,
195
+ directive: meta.directive
196
+ });
197
+ }
198
+ }
199
+
200
+ // Unguarded Logging
201
+ if (
202
+ t.isMemberExpression(callee) &&
203
+ t.isIdentifier(callee.object) &&
204
+ callee.object.name === 'console' &&
205
+ t.isIdentifier(callee.property) &&
206
+ ['log', 'info', 'warn'].includes(callee.property.name)
207
+ ) {
208
+ const line = astPath.node.loc?.start.line || 1;
209
+ const meta = RULE_REGISTRY.UNGUARDED_LOGGING;
210
+ violations.push({
211
+ filePath: relativePath,
212
+ line,
213
+ column: astPath.node.loc?.start.column || 1,
214
+ hazard: `Unguarded console.${callee.property.name} statement`,
215
+ rule: 'UNGUARDED_LOGGING',
216
+ severity: meta.severity,
217
+ pillar: meta.pillar,
218
+ directive: meta.directive
219
+ });
220
+ }
221
+ },
222
+
223
+ // Pillar 4: Type Co-location
224
+ TSTypeLiteral(astPath) {
225
+ if (astPath.node.members.length > 3) {
226
+ if (!astPath.findParent((p) => p.isTSTypeAliasDeclaration() || p.isTSInterfaceDeclaration())) {
227
+ const line = astPath.node.loc?.start.line || 1;
228
+ const meta = RULE_REGISTRY.TYPE_COLOCATION;
229
+ violations.push({
230
+ filePath: relativePath,
231
+ line,
232
+ column: astPath.node.loc?.start.column || 1,
233
+ hazard: `Inlined anonymous complex type (${astPath.node.members.length} members)`,
234
+ rule: 'TYPE_COLOCATION',
235
+ severity: meta.severity,
236
+ pillar: meta.pillar,
237
+ directive: meta.directive
238
+ });
239
+ }
240
+ }
241
+ }
242
+ };
243
+ };
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ensureChemxDir } from './history.js';
4
+
5
+ const DISCUSSION_FILE = 'discussion.json';
6
+
7
+ export const getStoredDiscussion = (cwd = process.cwd()) => {
8
+ try {
9
+ const filePath = path.resolve(cwd, '.chemx', DISCUSSION_FILE);
10
+ if (!fs.existsSync(filePath)) return null;
11
+ const content = fs.readFileSync(filePath, 'utf-8');
12
+ return JSON.parse(content);
13
+ } catch {
14
+ return null;
15
+ }
16
+ };
17
+
18
+ export const saveStoredDiscussion = (data, cwd = process.cwd()) => {
19
+ try {
20
+ const dir = ensureChemxDir(cwd);
21
+ const filePath = path.resolve(dir, DISCUSSION_FILE);
22
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ };
28
+
29
+ export const clearStoredDiscussion = (cwd = process.cwd()) => {
30
+ try {
31
+ const filePath = path.resolve(cwd, '.chemx', DISCUSSION_FILE);
32
+ if (fs.existsSync(filePath)) {
33
+ fs.unlinkSync(filePath);
34
+ }
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ };
@@ -0,0 +1,63 @@
1
+ import type { MolecularHealthScore, CodebaseMetrics, PillarStatus, ContextTokenAnalysis, AuditReport } from './types';
2
+
3
+ export interface AuditSnapshot {
4
+ readonly id: string;
5
+ readonly timestamp: string;
6
+ readonly health: MolecularHealthScore;
7
+ readonly metrics: CodebaseMetrics;
8
+ readonly violations: {
9
+ readonly total: number;
10
+ readonly critical: number;
11
+ readonly high: number;
12
+ readonly medium: number;
13
+ readonly low: number;
14
+ };
15
+ readonly monoliths: {
16
+ readonly total: number;
17
+ readonly extreme: number;
18
+ readonly severe: number;
19
+ readonly warning: number;
20
+ };
21
+ readonly tokens: ContextTokenAnalysis;
22
+ readonly pillars: Record<string, {
23
+ readonly status: PillarStatus;
24
+ readonly violations: number;
25
+ readonly critical: number;
26
+ }>;
27
+ }
28
+
29
+ export interface PillarDelta {
30
+ readonly beforeStatus: PillarStatus;
31
+ readonly afterStatus: PillarStatus;
32
+ readonly beforeViolations: number;
33
+ readonly afterViolations: number;
34
+ readonly improved: boolean;
35
+ }
36
+
37
+ export interface TransformationDelta {
38
+ readonly scoreDelta: number;
39
+ readonly critDelta: number;
40
+ readonly totalDelta: number;
41
+ readonly monolithDelta: number;
42
+ readonly tokensDelta: number;
43
+ readonly pillarDeltas: Record<string, PillarDelta>;
44
+ readonly isImproved: boolean;
45
+ }
46
+
47
+ export interface SaveSnapshotResult {
48
+ readonly snapshot: AuditSnapshot;
49
+ readonly history: readonly AuditSnapshot[];
50
+ readonly baseline: AuditSnapshot | null;
51
+ readonly isNewBaseline: boolean;
52
+ readonly totalAudits: number;
53
+ }
54
+
55
+ export declare function ensureChemxDir(cwd?: string): string;
56
+ export declare function createSnapshotFromReport(report: AuditReport): AuditSnapshot;
57
+ export declare function getAuditHistory(cwd?: string): AuditSnapshot[];
58
+ export declare function getAuditBaseline(cwd?: string): AuditSnapshot | null;
59
+ export declare function setAuditBaseline(snapshot: AuditSnapshot, cwd?: string): AuditSnapshot;
60
+ export declare function saveAuditSnapshot(report: AuditReport, cwd?: string): SaveSnapshotResult;
61
+ export declare function calculateTransformationDelta(beforeSnapshot: AuditSnapshot, afterSnapshot: AuditSnapshot): TransformationDelta;
62
+ export declare function formatTransformationTerminal(beforeSnapshot: AuditSnapshot, afterSnapshot: AuditSnapshot): string;
63
+ export declare function formatHistoryTimelineTerminal(history: readonly AuditSnapshot[]): string;
@@ -0,0 +1,352 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { groupViolationsBySeverity } from './reporter.js';
4
+
5
+ const RESET = '\x1b[0m';
6
+ const BOLD = '\x1b[1m';
7
+ const DIM = '\x1b[2m';
8
+ const CYAN = '\x1b[36m';
9
+ const GREEN = '\x1b[32m';
10
+ const YELLOW = '\x1b[33m';
11
+ const RED = '\x1b[31m';
12
+
13
+ export const ensureChemxDir = (cwd = process.cwd()) => {
14
+ const dir = path.resolve(cwd, '.chemx');
15
+ if (!fs.existsSync(dir)) {
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+
19
+ // Ensure .chemx is gitignored if git repo exists
20
+ const gitDir = path.resolve(cwd, '.git');
21
+ if (fs.existsSync(gitDir)) {
22
+ const gitignorePath = path.resolve(cwd, '.gitignore');
23
+ try {
24
+ let content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
25
+ if (!content.includes('.chemx')) {
26
+ const trailingNewline = content.endsWith('\n') || content.length === 0 ? '' : '\n';
27
+ fs.appendFileSync(gitignorePath, `${trailingNewline}# Chemical X local telemetry & audit history\n.chemx/\n`, 'utf-8');
28
+ }
29
+ } catch {}
30
+ }
31
+
32
+ return dir;
33
+ };
34
+
35
+ export const createSnapshotFromReport = (report) => {
36
+ const { metrics, health, hotspots = [], violations = [], pillars = {}, contextAnalysis = {} } = report;
37
+ const { critical, high, medium, low } = groupViolationsBySeverity(violations);
38
+
39
+ const isExtremeMonolith = (h) => h.lineCount >= 2000;
40
+ const isSevereMonolith = (h) => h.lineCount >= 1000 && h.lineCount < 2000;
41
+ const isWarningMonolith = (h) => h.lineCount >= 500 && h.lineCount < 1000;
42
+
43
+ const extremeMonoliths = hotspots.filter(isExtremeMonolith).length;
44
+ const severeMonoliths = hotspots.filter(isSevereMonolith).length;
45
+ const warningMonoliths = hotspots.filter(isWarningMonolith).length;
46
+ const totalMonoliths = extremeMonoliths + severeMonoliths + warningMonoliths;
47
+
48
+ const pillarSummaries = {};
49
+ for (const [key, data] of Object.entries(pillars)) {
50
+ pillarSummaries[key] = {
51
+ status: data.status,
52
+ violations: data.violations,
53
+ critical: data.critical
54
+ };
55
+ }
56
+
57
+ return {
58
+ id: `audit-${Date.now()}`,
59
+ timestamp: new Date().toISOString(),
60
+ health: {
61
+ score: health.score,
62
+ grade: health.grade,
63
+ label: health.label
64
+ },
65
+ metrics: {
66
+ scannedFiles: metrics.scannedFiles,
67
+ totalLoc: metrics.totalLoc,
68
+ avgLoc: metrics.avgLoc,
69
+ moleculeCount: metrics.moleculeCount,
70
+ moleculeCompliantCount: metrics.moleculeCompliantCount,
71
+ moleculeCompliantPct: metrics.moleculeCompliantPct,
72
+ hookCount: metrics.hookCount
73
+ },
74
+ violations: {
75
+ total: violations.length,
76
+ critical: critical.length,
77
+ high: high.length,
78
+ medium: medium.length,
79
+ low: low.length
80
+ },
81
+ monoliths: {
82
+ total: totalMonoliths,
83
+ extreme: extremeMonoliths,
84
+ severe: severeMonoliths,
85
+ warning: warningMonoliths
86
+ },
87
+ tokens: {
88
+ estimatedTokens: contextAnalysis.estimatedTokens || 0,
89
+ estimatedExcessTokens: contextAnalysis.estimatedExcessTokens || 0,
90
+ potentialSavingsPct: contextAnalysis.potentialSavingsPct || 0,
91
+ riskLevel: contextAnalysis.riskLevel || 'LOW'
92
+ },
93
+ pillars: pillarSummaries
94
+ };
95
+ };
96
+
97
+ export const getAuditHistory = (cwd = process.cwd()) => {
98
+ const historyPath = path.resolve(cwd, '.chemx', 'history.json');
99
+ if (!fs.existsSync(historyPath)) return [];
100
+ try {
101
+ const raw = fs.readFileSync(historyPath, 'utf-8');
102
+ const parsed = JSON.parse(raw);
103
+ return Array.isArray(parsed) ? parsed : [];
104
+ } catch {
105
+ return [];
106
+ }
107
+ };
108
+
109
+ export const getAuditBaseline = (cwd = process.cwd()) => {
110
+ const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
111
+ if (fs.existsSync(baselinePath)) {
112
+ try {
113
+ const raw = fs.readFileSync(baselinePath, 'utf-8');
114
+ return JSON.parse(raw);
115
+ } catch {}
116
+ }
117
+ const history = getAuditHistory(cwd);
118
+ return history.length > 0 ? history[0] : null;
119
+ };
120
+
121
+ export const setAuditBaseline = (snapshot, cwd = process.cwd()) => {
122
+ ensureChemxDir(cwd);
123
+ const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
124
+ fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
125
+ return snapshot;
126
+ };
127
+
128
+ export const saveAuditSnapshot = (report, cwd = process.cwd()) => {
129
+ ensureChemxDir(cwd);
130
+ const snapshot = createSnapshotFromReport(report);
131
+ const history = getAuditHistory(cwd);
132
+
133
+ // Prevent duplicate snapshots if called multiple times within 5 seconds with same metrics
134
+ if (history.length > 0) {
135
+ const last = history[history.length - 1];
136
+ const timeDiff = Math.abs(Date.now() - new Date(last.timestamp).getTime());
137
+ const isSameMetrics =
138
+ last.health?.score === snapshot.health?.score &&
139
+ last.metrics?.totalLoc === snapshot.metrics?.totalLoc &&
140
+ last.violations?.total === snapshot.violations?.total;
141
+
142
+ if (timeDiff < 5000 && isSameMetrics) {
143
+ return {
144
+ snapshot: last,
145
+ history,
146
+ baseline: getAuditBaseline(cwd),
147
+ isNewBaseline: false,
148
+ totalAudits: history.length
149
+ };
150
+ }
151
+ }
152
+
153
+ history.push(snapshot);
154
+
155
+ // Keep last 50 snapshots
156
+ const trimmed = history.slice(-50);
157
+ const historyPath = path.resolve(cwd, '.chemx', 'history.json');
158
+ fs.writeFileSync(historyPath, JSON.stringify(trimmed, null, 2), 'utf-8');
159
+
160
+ // Auto-establish first run as baseline if none exists
161
+ const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
162
+ let isNewBaseline = false;
163
+ if (!fs.existsSync(baselinePath)) {
164
+ fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
165
+ isNewBaseline = true;
166
+ }
167
+
168
+ const baseline = getAuditBaseline(cwd);
169
+ return {
170
+ snapshot,
171
+ history: trimmed,
172
+ baseline,
173
+ isNewBaseline,
174
+ totalAudits: trimmed.length
175
+ };
176
+ };
177
+
178
+ export const calculateTransformationDelta = (beforeSnapshot, afterSnapshot) => {
179
+ const scoreBefore = beforeSnapshot.health.score;
180
+ const scoreAfter = afterSnapshot.health.score;
181
+ const scoreDelta = scoreAfter - scoreBefore;
182
+
183
+ const critBefore = beforeSnapshot.violations.critical;
184
+ const critAfter = afterSnapshot.violations.critical;
185
+ const critDelta = critAfter - critBefore;
186
+
187
+ const totalBefore = beforeSnapshot.violations.total;
188
+ const totalAfter = afterSnapshot.violations.total;
189
+ const totalDelta = totalAfter - totalBefore;
190
+
191
+ const monolithBefore = beforeSnapshot.monoliths.total;
192
+ const monolithAfter = afterSnapshot.monoliths.total;
193
+ const monolithDelta = monolithAfter - monolithBefore;
194
+
195
+ const tokensBefore = beforeSnapshot.tokens.estimatedExcessTokens;
196
+ const tokensAfter = afterSnapshot.tokens.estimatedExcessTokens;
197
+ const tokensDelta = tokensAfter - tokensBefore;
198
+
199
+ const pillarDeltas = {};
200
+ const allPillars = new Set([
201
+ ...Object.keys(beforeSnapshot.pillars || {}),
202
+ ...Object.keys(afterSnapshot.pillars || {})
203
+ ]);
204
+
205
+ for (const pillar of allPillars) {
206
+ const bPillar = beforeSnapshot.pillars?.[pillar] || { status: 'UNKNOWN', violations: 0 };
207
+ const aPillar = afterSnapshot.pillars?.[pillar] || { status: 'UNKNOWN', violations: 0 };
208
+ pillarDeltas[pillar] = {
209
+ beforeStatus: bPillar.status,
210
+ afterStatus: aPillar.status,
211
+ beforeViolations: bPillar.violations,
212
+ afterViolations: aPillar.violations,
213
+ improved: aPillar.violations < bPillar.violations || (bPillar.status !== 'PASSED' && aPillar.status === 'PASSED')
214
+ };
215
+ }
216
+
217
+ return {
218
+ scoreDelta,
219
+ critDelta,
220
+ totalDelta,
221
+ monolithDelta,
222
+ tokensDelta,
223
+ pillarDeltas,
224
+ isImproved: scoreDelta > 0 || critDelta < 0 || totalDelta < 0
225
+ };
226
+ };
227
+
228
+ export const formatTransformationTerminal = (beforeSnapshot, afterSnapshot) => {
229
+ const delta = calculateTransformationDelta(beforeSnapshot, afterSnapshot);
230
+ const lines = [];
231
+
232
+ const formatDeltaNumber = (val, invertPositiveGood = false) => {
233
+ if (val === 0) return `${DIM}0 (No change)${RESET}`;
234
+ const isGood = invertPositiveGood ? val < 0 : val > 0;
235
+ const sign = val > 0 ? `+${val}` : `${val}`;
236
+ const color = isGood ? GREEN : RED;
237
+ return `${color}${BOLD}${sign}${RESET}`;
238
+ };
239
+
240
+ lines.push('');
241
+ lines.push(`${CYAN}======================================================================${RESET}`);
242
+ lines.push(`${BOLD}${CYAN} ARCHITECTURAL TRANSFORMATION : BEFORE & AFTER PROGRESSION${RESET}`);
243
+ lines.push(`${DIM} Comparing baseline snapshot vs. latest refactored audit${RESET}`);
244
+ lines.push(`${CYAN}======================================================================${RESET}`);
245
+ lines.push('');
246
+
247
+ const dateBefore = new Date(beforeSnapshot.timestamp).toLocaleDateString();
248
+ const dateAfter = new Date(afterSnapshot.timestamp).toLocaleDateString();
249
+
250
+ lines.push(` ${BOLD}Baseline Audit:${RESET} ${DIM}${dateBefore} [${beforeSnapshot.id}]${RESET}`);
251
+ lines.push(` ${BOLD}Latest Audit:${RESET} ${GREEN}${dateAfter} [${afterSnapshot.id}]${RESET}`);
252
+ lines.push('');
253
+
254
+ lines.push(`${BOLD} METRIC COMPARISON TABLE${RESET}`);
255
+ lines.push(` ------------------------------------------------------------------`);
256
+ lines.push(` ${'Metric'.padEnd(26)} ${'Before'.padEnd(16)} ${'After'.padEnd(16)} Delta`);
257
+ lines.push(` ------------------------------------------------------------------`);
258
+
259
+ const scoreBeforeStr = `${beforeSnapshot.health.score} (${beforeSnapshot.health.grade})`;
260
+ const scoreAfterStr = `${afterSnapshot.health.score} (${afterSnapshot.health.grade})`;
261
+ lines.push(` ${'Molecular Health (MHI)'.padEnd(26)} ${scoreBeforeStr.padEnd(16)} ${scoreAfterStr.padEnd(16)} ${formatDeltaNumber(delta.scoreDelta)}`);
262
+
263
+ const critBeforeStr = `${beforeSnapshot.violations.critical}`;
264
+ const critAfterStr = `${afterSnapshot.violations.critical}`;
265
+ lines.push(` ${'Critical Hazards'.padEnd(26)} ${critBeforeStr.padEnd(16)} ${critAfterStr.padEnd(16)} ${formatDeltaNumber(delta.critDelta, true)}`);
266
+
267
+ const totalBeforeStr = `${beforeSnapshot.violations.total}`;
268
+ const totalAfterStr = `${afterSnapshot.violations.total}`;
269
+ lines.push(` ${'Total Violations'.padEnd(26)} ${totalBeforeStr.padEnd(16)} ${totalAfterStr.padEnd(16)} ${formatDeltaNumber(delta.totalDelta, true)}`);
270
+
271
+ const monoBeforeStr = `${beforeSnapshot.monoliths.total}`;
272
+ const monoAfterStr = `${afterSnapshot.monoliths.total}`;
273
+ lines.push(` ${'Monolith Files (>500 LOC)'.padEnd(26)} ${monoBeforeStr.padEnd(16)} ${monoAfterStr.padEnd(16)} ${formatDeltaNumber(delta.monolithDelta, true)}`);
274
+
275
+ const excessBeforeStr = `${beforeSnapshot.tokens.estimatedExcessTokens.toLocaleString()} tok`;
276
+ const excessAfterStr = `${afterSnapshot.tokens.estimatedExcessTokens.toLocaleString()} tok`;
277
+ lines.push(` ${'Excess Token Burn'.padEnd(26)} ${excessBeforeStr.padEnd(16)} ${excessAfterStr.padEnd(16)} ${formatDeltaNumber(delta.tokensDelta, true)}`);
278
+
279
+ lines.push(` ------------------------------------------------------------------`);
280
+ lines.push('');
281
+
282
+ const resolvePillarDeltaArrow = (pDelta) => {
283
+ if (pDelta.improved) {
284
+ return `${GREEN}▲ RESOLVED${RESET}`;
285
+ }
286
+ if (pDelta.beforeStatus === pDelta.afterStatus) {
287
+ return `${DIM}━ UNCHANGED${RESET}`;
288
+ }
289
+ return `${RED}▼ DEGRADED${RESET}`;
290
+ };
291
+
292
+ lines.push(`${BOLD} 7-PILLAR PROGRESSION${RESET}`);
293
+ lines.push(` ------------------------------------------------------------------`);
294
+ for (const [pillar, pDelta] of Object.entries(delta.pillarDeltas)) {
295
+ const arrow = resolvePillarDeltaArrow(pDelta);
296
+ lines.push(` ${pillar.padEnd(28)} ${pDelta.beforeStatus.padEnd(10)} -> ${pDelta.afterStatus.padEnd(10)} ${arrow}`);
297
+ }
298
+ lines.push(` ------------------------------------------------------------------`);
299
+ lines.push('');
300
+
301
+ if (delta.isImproved) {
302
+ lines.push(` ${GREEN}${BOLD}✔ CODEBASE SIGNIFICANTLY IMPROVED!${RESET}`);
303
+ lines.push(` Post your transformation story to GitHub Discussions using the Share menu.`);
304
+ } else {
305
+ lines.push(` ${YELLOW}ℹ Codebase metrics are stable or currently under refactoring.${RESET}`);
306
+ }
307
+ lines.push('');
308
+
309
+ return lines.join('\n');
310
+ };
311
+
312
+ export const formatHistoryTimelineTerminal = (history) => {
313
+ const lines = [];
314
+ lines.push('');
315
+ lines.push(`${CYAN}======================================================================${RESET}`);
316
+ lines.push(`${BOLD}${CYAN} LOCAL AUDIT HISTORY TIMELINE (${history.length} RUNS RECORDED)${RESET}`);
317
+ lines.push(`${DIM} Persistent snapshots stored in .chemx/history.json${RESET}`);
318
+ lines.push(`${CYAN}======================================================================${RESET}`);
319
+ lines.push('');
320
+
321
+ if (history.length === 0) {
322
+ lines.push(' No historical audits recorded yet.');
323
+ lines.push('');
324
+ return lines.join('\n');
325
+ }
326
+
327
+ lines.push(` ${'#'.padEnd(4)} ${'Date/Time'.padEnd(22)} ${'Score'.padEnd(12)} ${'Grade'.padEnd(10)} ${'Monoliths'.padEnd(12)} Hazards`);
328
+ lines.push(` ------------------------------------------------------------------`);
329
+
330
+ const resolveScoreGradeColor = (score) => {
331
+ if (score >= 90) return GREEN;
332
+ if (score >= 70) return YELLOW;
333
+ return RED;
334
+ };
335
+
336
+ const renderTimelineRow = (snap, idx) => {
337
+ const num = `${idx + 1}`.padEnd(4);
338
+ const dateStr = new Date(snap.timestamp).toLocaleString().slice(0, 20).padEnd(22);
339
+ const scoreStr = `${snap.health.score}/100`.padEnd(12);
340
+ const gradeColor = resolveScoreGradeColor(snap.health.score);
341
+ const gradeStr = `${gradeColor}${snap.health.grade.padEnd(10)}${RESET}`;
342
+ const monoStr = `${snap.monoliths.total} files`.padEnd(12);
343
+ const hazStr = `${snap.violations.total} (Crit: ${snap.violations.critical})`;
344
+ lines.push(` ${num} ${dateStr} ${scoreStr} ${gradeStr} ${monoStr} ${hazStr}`);
345
+ };
346
+
347
+ history.forEach(renderTimelineRow);
348
+
349
+ lines.push(` ------------------------------------------------------------------`);
350
+ lines.push('');
351
+ return lines.join('\n');
352
+ };