@chemx/starter-kit 26.9.9-786 → 26.9.11-481
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 +69 -0
- package/cli/audit/history.js +397 -0
- package/cli/audit/metrics.js +179 -0
- package/cli/audit/prompts.js +174 -0
- package/cli/audit/reporter-ascii.d.ts +4 -0
- package/cli/audit/reporter-ascii.js +99 -0
- package/cli/audit/reporter-grades.js +275 -0
- package/cli/audit/reporter-markdown.js +137 -0
- package/cli/audit/reporter-utils.js +125 -0
- package/cli/audit/reporter.js +497 -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 +69 -0
- package/cli/audit/social.js +303 -0
- package/cli/audit/types.d.ts +140 -0
- package/cli/audit.d.ts +3 -24
- package/cli/audit.js +207 -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 +91 -0
- package/cli/installer.d.ts +21 -0
- package/cli/installer.js +104 -0
- package/cli/license.js +320 -0
- package/cli/navigator-actions.js +274 -0
- package/cli/navigator-banner.js +98 -0
- package/cli/navigator-conversion.js +100 -0
- package/cli/navigator-grades.js +87 -0
- package/cli/navigator-menu.js +127 -0
- package/cli/navigator-paged.js +86 -0
- package/cli/navigator-share.js +129 -0
- package/cli/navigator.d.ts +43 -0
- package/cli/navigator.js +126 -0
- package/cli/scaffold.js +148 -0
- package/cli/terminal.js +126 -0
- package/docs/CHANGELOG.md +101 -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,69 @@
|
|
|
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 costPassBefore?: number;
|
|
44
|
+
readonly costPassAfter?: number;
|
|
45
|
+
readonly costPassDelta?: number;
|
|
46
|
+
readonly monthlyTaxBefore?: number;
|
|
47
|
+
readonly monthlyTaxAfter?: number;
|
|
48
|
+
readonly monthlyTaxDelta?: number;
|
|
49
|
+
readonly pillarDeltas: Record<string, PillarDelta>;
|
|
50
|
+
readonly isImproved: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SaveSnapshotResult {
|
|
54
|
+
readonly snapshot: AuditSnapshot;
|
|
55
|
+
readonly history: readonly AuditSnapshot[];
|
|
56
|
+
readonly baseline: AuditSnapshot | null;
|
|
57
|
+
readonly isNewBaseline: boolean;
|
|
58
|
+
readonly totalAudits: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export declare function ensureChemxDir(cwd?: string): string;
|
|
62
|
+
export declare function createSnapshotFromReport(report: AuditReport): AuditSnapshot;
|
|
63
|
+
export declare function getAuditHistory(cwd?: string): AuditSnapshot[];
|
|
64
|
+
export declare function getAuditBaseline(cwd?: string): AuditSnapshot | null;
|
|
65
|
+
export declare function setAuditBaseline(snapshot: AuditSnapshot, cwd?: string): AuditSnapshot;
|
|
66
|
+
export declare function saveAuditSnapshot(report: AuditReport, cwd?: string): SaveSnapshotResult;
|
|
67
|
+
export declare function calculateTransformationDelta(beforeSnapshot: AuditSnapshot, afterSnapshot: AuditSnapshot): TransformationDelta;
|
|
68
|
+
export declare function formatTransformationTerminal(beforeSnapshot: AuditSnapshot, afterSnapshot: AuditSnapshot): string;
|
|
69
|
+
export declare function formatHistoryTimelineTerminal(history: readonly AuditSnapshot[]): string;
|