@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.
- package/cli/audit/ast-visitors.js +243 -0
- package/cli/audit/discussion-store.js +39 -0
- package/cli/audit/history.d.ts +63 -0
- package/cli/audit/history.js +352 -0
- package/cli/audit/metrics.js +179 -0
- package/cli/audit/prompts.js +174 -0
- package/cli/audit/reporter-grades.js +259 -0
- package/cli/audit/reporter-markdown.js +136 -0
- package/cli/audit/reporter-utils.js +107 -0
- package/cli/audit/reporter.js +459 -0
- package/cli/audit/rules-helpers.js +109 -0
- package/cli/audit/rules-registry.js +97 -0
- package/cli/audit/rules.js +123 -0
- package/cli/audit/social-constants.js +4 -0
- package/cli/audit/social-gh.js +126 -0
- package/cli/audit/social-git.js +214 -0
- package/cli/audit/social-http.js +222 -0
- package/cli/audit/social-publisher.js +193 -0
- package/cli/audit/social.d.ts +66 -0
- package/cli/audit/social.js +254 -0
- package/cli/audit/types.d.ts +131 -0
- package/cli/audit.d.ts +3 -24
- package/cli/audit.js +195 -210
- package/cli/help.d.ts +1 -0
- package/cli/help.js +136 -0
- package/cli/index.js +128 -566
- package/cli/installer-templates.js +80 -0
- package/cli/installer.d.ts +20 -0
- package/cli/installer.js +56 -0
- package/cli/license.js +320 -0
- package/cli/navigator-conversion.js +100 -0
- package/cli/navigator-grades.js +70 -0
- package/cli/navigator-paged.js +86 -0
- package/cli/navigator-share.js +129 -0
- package/cli/navigator.d.ts +24 -0
- package/cli/navigator.js +400 -0
- package/cli/scaffold.js +148 -0
- package/cli/terminal.js +126 -0
- package/docs/CHANGELOG.md +73 -0
- 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
|
+
};
|