@chemx/starter-kit 26.9.12-107 → 26.9.12-1140
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/ai-slop-detector.js +25 -41
- package/cli/audit/ast-visitors.js +10 -13
- package/cli/audit/history.js +45 -11
- package/cli/audit/pattern-detector.js +182 -0
- package/cli/audit/prompts.js +35 -48
- package/cli/audit/reporter-banner.js +4 -16
- package/cli/audit/reporter-grades.js +28 -16
- package/cli/audit/reporter-grouping.d.ts +6 -0
- package/cli/audit/reporter-grouping.js +48 -5
- package/cli/audit/reporter-markdown.js +31 -16
- package/cli/audit/reporter-sections.js +14 -4
- package/cli/audit/reporter-utils.js +88 -1
- package/cli/audit/reporter.d.ts +13 -4
- package/cli/audit/reporter.js +30 -3
- package/cli/audit/roadmap.js +236 -0
- package/cli/audit/rules-predicates.d.ts +17 -0
- package/cli/audit/rules-predicates.js +107 -0
- package/cli/audit/rules.js +6 -2
- package/cli/audit/social-gh.js +2 -6
- package/cli/audit/social-publisher.js +1 -1
- package/cli/audit/social.js +25 -9
- package/cli/audit/types.d.ts +43 -1
- package/cli/audit.js +47 -7
- package/cli/badge.js +9 -2
- package/cli/index.js +32 -13
- package/cli/installer-templates.js +43 -4
- package/cli/license.js +10 -7
- package/cli/navigator-actions.js +34 -6
- package/cli/navigator-banner.js +4 -2
- package/cli/navigator-menu.js +18 -0
- package/cli/navigator-share.js +1 -1
- package/cli/navigator.d.ts +8 -1
- package/cli/navigator.js +3 -3
- package/cli/terminal.js +6 -5
- package/cli/theme.d.ts +41 -0
- package/cli/theme.js +84 -0
- package/docs/CHANGELOG.md +39 -1
- package/package.json +1 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as t from '@babel/types';
|
|
2
2
|
import { RULE_REGISTRY } from './rules-registry.js';
|
|
3
|
+
import {
|
|
4
|
+
isComponentPath,
|
|
5
|
+
isCodeLine,
|
|
6
|
+
isShallowCatchBody,
|
|
7
|
+
hasAnyTypeAnnotation,
|
|
8
|
+
isRedundantPassthroughReturn
|
|
9
|
+
} from './rules-predicates.js';
|
|
3
10
|
|
|
4
11
|
const RESIDUE_PATTERNS = [
|
|
5
12
|
['hope this', 'helps'].join(' '),
|
|
@@ -95,7 +102,7 @@ export const checkSlopTextPatterns = (content, lines, relativePath, violations)
|
|
|
95
102
|
let nextIdx = idx + 1;
|
|
96
103
|
while (nextIdx < lines.length) {
|
|
97
104
|
const nextTrim = lines[nextIdx].trim();
|
|
98
|
-
if (
|
|
105
|
+
if (isCodeLine(nextTrim)) {
|
|
99
106
|
const nextWords = new Set(normalizeWords(nextTrim));
|
|
100
107
|
let matchCount = 0;
|
|
101
108
|
for (const cw of commentWords) {
|
|
@@ -126,25 +133,12 @@ export const checkSlopTextPatterns = (content, lines, relativePath, violations)
|
|
|
126
133
|
};
|
|
127
134
|
|
|
128
135
|
export const createAiSlopVisitors = ({ relativePath, violations }) => {
|
|
129
|
-
const isComponentFile = (
|
|
130
|
-
relativePath.includes('molecules') ||
|
|
131
|
-
relativePath.includes('components') ||
|
|
132
|
-
relativePath.includes('/m-') ||
|
|
133
|
-
relativePath.includes('/views/') ||
|
|
134
|
-
relativePath.includes('/pages/')
|
|
135
|
-
);
|
|
136
|
+
const isComponentFile = isComponentPath(relativePath);
|
|
136
137
|
|
|
137
138
|
return {
|
|
138
139
|
CatchClause(astPath) {
|
|
139
140
|
const body = astPath.node.body?.body || [];
|
|
140
|
-
const isShallowCatch = (
|
|
141
|
-
body.length === 0 ||
|
|
142
|
-
(body.length === 1 &&
|
|
143
|
-
t.isExpressionStatement(body[0]) &&
|
|
144
|
-
t.isCallExpression(body[0].expression) &&
|
|
145
|
-
t.isMemberExpression(body[0].expression.callee) &&
|
|
146
|
-
t.isIdentifier(body[0].expression.callee.object, { name: 'console' }))
|
|
147
|
-
);
|
|
141
|
+
const isShallowCatch = isShallowCatchBody(body, t);
|
|
148
142
|
|
|
149
143
|
if (isShallowCatch) {
|
|
150
144
|
const line = astPath.node.loc?.start.line || 1;
|
|
@@ -162,23 +156,20 @@ export const createAiSlopVisitors = ({ relativePath, violations }) => {
|
|
|
162
156
|
});
|
|
163
157
|
}
|
|
164
158
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
isAiSlop: true
|
|
180
|
-
});
|
|
181
|
-
}
|
|
159
|
+
if (hasAnyTypeAnnotation(astPath.node.param, t)) {
|
|
160
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
161
|
+
const meta = RULE_REGISTRY.AI_SLOP_LAZY_ANY;
|
|
162
|
+
violations.push({
|
|
163
|
+
filePath: relativePath,
|
|
164
|
+
line,
|
|
165
|
+
column: astPath.node.loc?.start.column || 1,
|
|
166
|
+
hazard: 'Lazy any catch parameter widening detected',
|
|
167
|
+
rule: 'AI_SLOP_LAZY_ANY',
|
|
168
|
+
severity: meta.severity,
|
|
169
|
+
pillar: meta.pillar,
|
|
170
|
+
directive: meta.directive,
|
|
171
|
+
isAiSlop: true
|
|
172
|
+
});
|
|
182
173
|
}
|
|
183
174
|
},
|
|
184
175
|
|
|
@@ -188,14 +179,7 @@ export const createAiSlopVisitors = ({ relativePath, violations }) => {
|
|
|
188
179
|
for (let i = 0; i < stmts.length - 1; i++) {
|
|
189
180
|
const curr = stmts[i];
|
|
190
181
|
const next = stmts[i + 1];
|
|
191
|
-
if (
|
|
192
|
-
t.isVariableDeclaration(curr) &&
|
|
193
|
-
curr.declarations.length === 1 &&
|
|
194
|
-
t.isIdentifier(curr.declarations[0].id) &&
|
|
195
|
-
t.isReturnStatement(next) &&
|
|
196
|
-
t.isIdentifier(next.argument) &&
|
|
197
|
-
curr.declarations[0].id.name === next.argument.name
|
|
198
|
-
) {
|
|
182
|
+
if (isRedundantPassthroughReturn(curr, next, t)) {
|
|
199
183
|
const varName = curr.declarations[0].id.name;
|
|
200
184
|
const line = curr.loc?.start.line || 1;
|
|
201
185
|
const meta = RULE_REGISTRY.AI_SLOP_REDUNDANT_PASSTHROUGH;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import * as t from '@babel/types';
|
|
2
2
|
import { RULE_REGISTRY } from './rules-registry.js';
|
|
3
3
|
import { countLogicalOperators } from './rules-helpers.js';
|
|
4
|
+
import {
|
|
5
|
+
isCustomHookFunction,
|
|
6
|
+
resolveStartLine,
|
|
7
|
+
isZeroDelayTimeout,
|
|
8
|
+
isUnguardedConsoleCall
|
|
9
|
+
} from './rules-predicates.js';
|
|
4
10
|
|
|
5
11
|
export const createAstVisitors = ({ relativePath, violations }) => {
|
|
6
12
|
return {
|
|
7
13
|
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
|
-
);
|
|
14
|
+
const isCustomHook = isCustomHookFunction(astPath);
|
|
12
15
|
|
|
13
16
|
// Pillar 3: Hook Saturation
|
|
14
17
|
let hookCount = 0;
|
|
@@ -70,7 +73,7 @@ export const createAstVisitors = ({ relativePath, violations }) => {
|
|
|
70
73
|
if (t.isLogicalExpression(expr) || t.isUnaryExpression(expr)) {
|
|
71
74
|
const opCount = countLogicalOperators(expr);
|
|
72
75
|
if (opCount > 2) {
|
|
73
|
-
const line = expr
|
|
76
|
+
const line = resolveStartLine(expr, astPath.node, 1);
|
|
74
77
|
const meta = RULE_REGISTRY.CONTROL_FLOW_INLINE_BOOLEAN;
|
|
75
78
|
violations.push({
|
|
76
79
|
filePath: relativePath,
|
|
@@ -154,7 +157,7 @@ export const createAstVisitors = ({ relativePath, violations }) => {
|
|
|
154
157
|
const delayArg = args[1];
|
|
155
158
|
|
|
156
159
|
// Render-hack check: setTimeout(fn, 0)
|
|
157
|
-
if (callee
|
|
160
|
+
if (isZeroDelayTimeout(callee, delayArg, t)) {
|
|
158
161
|
const line = astPath.node.loc?.start.line || 1;
|
|
159
162
|
const meta = RULE_REGISTRY.RENDER_HACK_TIMEOUT;
|
|
160
163
|
violations.push({
|
|
@@ -198,13 +201,7 @@ export const createAstVisitors = ({ relativePath, violations }) => {
|
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
// 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
|
-
) {
|
|
204
|
+
if (isUnguardedConsoleCall(callee, t)) {
|
|
208
205
|
const line = astPath.node.loc?.start.line || 1;
|
|
209
206
|
const meta = RULE_REGISTRY.UNGUARDED_LOGGING;
|
|
210
207
|
violations.push({
|
package/cli/audit/history.js
CHANGED
|
@@ -126,18 +126,39 @@ export const getAuditHistory = (cwd = process.cwd()) => {
|
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
export const getAuditBaseline = (cwd = process.cwd()) => {
|
|
129
|
+
const history = getAuditHistory(cwd);
|
|
129
130
|
const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
|
|
131
|
+
let explicitBaseline = null;
|
|
130
132
|
if (fs.existsSync(baselinePath)) {
|
|
131
133
|
try {
|
|
132
134
|
const raw = fs.readFileSync(baselinePath, 'utf-8');
|
|
133
|
-
|
|
135
|
+
explicitBaseline = JSON.parse(raw);
|
|
134
136
|
} catch (err) {
|
|
135
137
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
136
|
-
|
|
138
|
+
explicitBaseline = null;
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
|
|
142
|
+
if (history.length === 0) {
|
|
143
|
+
return explicitBaseline;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Find the lowest scoring snapshot in history (the true debt floor)
|
|
147
|
+
let lowestSnapshot = history[0];
|
|
148
|
+
for (const s of history) {
|
|
149
|
+
const currentScore = s.health?.score ?? 100;
|
|
150
|
+
const lowestScore = lowestSnapshot.health?.score ?? 100;
|
|
151
|
+
if (currentScore < lowestScore) {
|
|
152
|
+
lowestSnapshot = s;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!explicitBaseline) return lowestSnapshot;
|
|
157
|
+
|
|
158
|
+
// Use the lowest score between explicit baseline and history floor
|
|
159
|
+
const explicitScore = explicitBaseline.health?.score ?? 100;
|
|
160
|
+
const minScore = lowestSnapshot.health?.score ?? 100;
|
|
161
|
+
return minScore < explicitScore ? lowestSnapshot : explicitBaseline;
|
|
141
162
|
};
|
|
142
163
|
|
|
143
164
|
export const setAuditBaseline = (snapshot, cwd = process.cwd()) => {
|
|
@@ -179,10 +200,14 @@ export const saveAuditSnapshot = (report, cwd = process.cwd()) => {
|
|
|
179
200
|
const historyPath = path.resolve(cwd, '.chemx', 'history.json');
|
|
180
201
|
fs.writeFileSync(historyPath, JSON.stringify(trimmed, null, 2), 'utf-8');
|
|
181
202
|
|
|
182
|
-
// Auto-establish first run as baseline
|
|
203
|
+
// Auto-establish first run or lower score as baseline floor
|
|
183
204
|
const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
|
|
184
205
|
let isNewBaseline = false;
|
|
185
|
-
|
|
206
|
+
const currentBaseline = getAuditBaseline(cwd);
|
|
207
|
+
const currentScore = snapshot.health?.score ?? 100;
|
|
208
|
+
const baselineScore = currentBaseline?.health?.score ?? 101;
|
|
209
|
+
|
|
210
|
+
if (!currentBaseline || currentScore < baselineScore) {
|
|
186
211
|
fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
187
212
|
isNewBaseline = true;
|
|
188
213
|
}
|
|
@@ -275,7 +300,8 @@ export const calculateTransformationDelta = (beforeSnapshot, afterSnapshot) => {
|
|
|
275
300
|
};
|
|
276
301
|
};
|
|
277
302
|
|
|
278
|
-
export const formatTransformationTerminal = (beforeSnapshot, afterSnapshot) => {
|
|
303
|
+
export const formatTransformationTerminal = (beforeSnapshot, afterSnapshot, options = {}) => {
|
|
304
|
+
const { isStepDelta = false } = options;
|
|
279
305
|
const delta = calculateTransformationDelta(beforeSnapshot, afterSnapshot);
|
|
280
306
|
const lines = [];
|
|
281
307
|
|
|
@@ -295,18 +321,26 @@ export const formatTransformationTerminal = (beforeSnapshot, afterSnapshot) => {
|
|
|
295
321
|
return `${color}${BOLD}${sign}${perUnit}${RESET}`;
|
|
296
322
|
};
|
|
297
323
|
|
|
324
|
+
const title = isStepDelta
|
|
325
|
+
? 'STEP PROGRESSION : INCREMENTAL CHECKPOINT DELTA'
|
|
326
|
+
: 'ARCHITECTURAL TRANSFORMATION : BEFORE & AFTER PROGRESSION';
|
|
327
|
+
const subtitle = isStepDelta
|
|
328
|
+
? 'Comparing immediately preceding audit vs. latest refactored audit'
|
|
329
|
+
: 'Comparing baseline floor snapshot vs. latest refactored audit';
|
|
330
|
+
const beforeLabel = isStepDelta ? 'Previous Audit:' : 'Baseline Floor:';
|
|
331
|
+
|
|
298
332
|
lines.push('');
|
|
299
333
|
lines.push(`${CYAN}======================================================================${RESET}`);
|
|
300
|
-
lines.push(`${BOLD}${CYAN}
|
|
301
|
-
lines.push(`${DIM}
|
|
334
|
+
lines.push(`${BOLD}${CYAN} ${title}${RESET}`);
|
|
335
|
+
lines.push(`${DIM} ${subtitle}${RESET}`);
|
|
302
336
|
lines.push(`${CYAN}======================================================================${RESET}`);
|
|
303
337
|
lines.push('');
|
|
304
338
|
|
|
305
339
|
const dateBefore = new Date(beforeSnapshot.timestamp).toLocaleDateString();
|
|
306
340
|
const dateAfter = new Date(afterSnapshot.timestamp).toLocaleDateString();
|
|
307
341
|
|
|
308
|
-
lines.push(` ${BOLD}
|
|
309
|
-
lines.push(` ${BOLD}Latest Audit
|
|
342
|
+
lines.push(` ${BOLD}${beforeLabel.padEnd(18)}${RESET} ${DIM}${dateBefore} [${beforeSnapshot.id}]${RESET}`);
|
|
343
|
+
lines.push(` ${BOLD}${'Latest Audit:'.padEnd(18)}${RESET} ${GREEN}${dateAfter} [${afterSnapshot.id}]${RESET}`);
|
|
310
344
|
lines.push('');
|
|
311
345
|
|
|
312
346
|
lines.push(`${BOLD} METRIC COMPARISON TABLE${RESET}`);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chemical X Protocol: Structural Pattern Harmonization Detector
|
|
3
|
+
* Single-pass AST fingerprinting to discover cross-file clones and extraction candidates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MIN_UNION_MEMBERS = 3;
|
|
7
|
+
const MIN_CHILD_NODES = 2;
|
|
8
|
+
|
|
9
|
+
export const createPatternRegistry = () => {
|
|
10
|
+
const buckets = new Map();
|
|
11
|
+
|
|
12
|
+
const record = (type, signature, location) => {
|
|
13
|
+
if (!signature) return;
|
|
14
|
+
const key = `${type}::${signature}`;
|
|
15
|
+
let bucket = buckets.get(key);
|
|
16
|
+
if (!bucket) {
|
|
17
|
+
bucket = {
|
|
18
|
+
type,
|
|
19
|
+
signature,
|
|
20
|
+
detail: location.detail || signature,
|
|
21
|
+
occurrences: []
|
|
22
|
+
};
|
|
23
|
+
buckets.set(key, bucket);
|
|
24
|
+
}
|
|
25
|
+
bucket.occurrences.push({
|
|
26
|
+
filePath: location.filePath,
|
|
27
|
+
line: location.line || 1,
|
|
28
|
+
column: location.column || 1
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const resolveHarmonizationCandidates = (hotspots = []) => {
|
|
33
|
+
const hotspotFiles = new Set(hotspots.map((h) => h.filePath));
|
|
34
|
+
const candidates = [];
|
|
35
|
+
|
|
36
|
+
for (const [key, bucket] of buckets.entries()) {
|
|
37
|
+
const uniqueFiles = Array.from(new Set(bucket.occurrences.map((o) => o.filePath)));
|
|
38
|
+
if (uniqueFiles.length < 2) continue;
|
|
39
|
+
|
|
40
|
+
const hasHotspot = uniqueFiles.some((f) => hotspotFiles.has(f));
|
|
41
|
+
const impactScore = uniqueFiles.length * (hasHotspot ? 3 : 1.5) + bucket.occurrences.length;
|
|
42
|
+
|
|
43
|
+
let label = 'Recurring Pattern';
|
|
44
|
+
let suggestedCapsule = 'm-shared-capsule';
|
|
45
|
+
let recommendation = 'Extract canonical capsule before decomposing consumers';
|
|
46
|
+
|
|
47
|
+
if (bucket.type === 'STATE_UNION') {
|
|
48
|
+
label = `Shared State Machine (${bucket.detail})`;
|
|
49
|
+
suggestedCapsule = 'types/state.d.ts';
|
|
50
|
+
recommendation = 'Extract shared discriminated union into domain types to eliminate duplicate definitions';
|
|
51
|
+
} else if (bucket.type === 'UI_STRUCTURE') {
|
|
52
|
+
label = `Shared UI Layout Structure (${bucket.detail})`;
|
|
53
|
+
suggestedCapsule = 'm-feature-card';
|
|
54
|
+
recommendation = 'Extract canonical molecule capsule before slicing consumer monoliths';
|
|
55
|
+
} else if (bucket.type === 'PREDICATE_LOGIC') {
|
|
56
|
+
label = `Duplicated Boolean Predicate Topology (${bucket.detail})`;
|
|
57
|
+
suggestedCapsule = 'usePredicateFilter.ts';
|
|
58
|
+
recommendation = 'Extract named predicate callback or higher-order filter function';
|
|
59
|
+
} else if (bucket.type === 'HOOK_SIGNATURE') {
|
|
60
|
+
label = `Parallel State Controller Return (${bucket.detail})`;
|
|
61
|
+
suggestedCapsule = 'useSharedController.ts';
|
|
62
|
+
recommendation = 'Unify ad-hoc local state returns into a canonical domain composable';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
candidates.push({
|
|
66
|
+
id: key,
|
|
67
|
+
type: bucket.type,
|
|
68
|
+
label,
|
|
69
|
+
detail: bucket.detail,
|
|
70
|
+
suggestedCapsule,
|
|
71
|
+
recommendation,
|
|
72
|
+
fileCount: uniqueFiles.length,
|
|
73
|
+
totalHits: bucket.occurrences.length,
|
|
74
|
+
hasHotspot,
|
|
75
|
+
impactScore,
|
|
76
|
+
occurrences: bucket.occurrences
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return candidates.sort((a, b) => b.impactScore - a.impactScore).slice(0, 12);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return { record, resolveHarmonizationCandidates };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const createPatternVisitors = (registry, relativePath) => {
|
|
87
|
+
if (!registry) return {};
|
|
88
|
+
|
|
89
|
+
const getJsxTagName = (node) => {
|
|
90
|
+
if (!node || !node.openingElement) return null;
|
|
91
|
+
const nameNode = node.openingElement.name;
|
|
92
|
+
if (nameNode.type === 'JSXIdentifier') return nameNode.name;
|
|
93
|
+
if (nameNode.type === 'JSXMemberExpression') return `${nameNode.object.name}.${nameNode.property.name}`;
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getJsxHierarchy = (node, depth = 0) => {
|
|
98
|
+
if (depth > 2 || !node || node.type !== 'JSXElement') return '';
|
|
99
|
+
const tag = getJsxTagName(node);
|
|
100
|
+
if (!tag) return '';
|
|
101
|
+
|
|
102
|
+
const elementChildren = (node.children || [])
|
|
103
|
+
.filter((c) => c.type === 'JSXElement')
|
|
104
|
+
.map((c) => getJsxHierarchy(c, depth + 1))
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
|
|
107
|
+
if (elementChildren.length === 0) return tag;
|
|
108
|
+
return `${tag}>(${elementChildren.join('+')})`;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
TSTypeAliasDeclaration(path) {
|
|
113
|
+
const typeAnnotation = path.node.typeAnnotation;
|
|
114
|
+
if (typeAnnotation && typeAnnotation.type === 'TSUnionType') {
|
|
115
|
+
const literals = (typeAnnotation.types || [])
|
|
116
|
+
.filter((t) => t.type === 'TSLiteralType' && typeof t.literal?.value === 'string')
|
|
117
|
+
.map((t) => `'${t.literal.value}'`);
|
|
118
|
+
|
|
119
|
+
if (literals.length >= MIN_UNION_MEMBERS) {
|
|
120
|
+
const sorted = Array.from(new Set(literals)).sort().join(' | ');
|
|
121
|
+
registry.record('STATE_UNION', sorted, {
|
|
122
|
+
filePath: relativePath,
|
|
123
|
+
line: path.node.loc?.start?.line,
|
|
124
|
+
detail: sorted
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
JSXElement(path) {
|
|
131
|
+
const directElementChildren = (path.node.children || []).filter((c) => c.type === 'JSXElement');
|
|
132
|
+
if (directElementChildren.length >= MIN_CHILD_NODES) {
|
|
133
|
+
const hierarchy = getJsxHierarchy(path.node);
|
|
134
|
+
if (hierarchy && hierarchy.includes('>')) {
|
|
135
|
+
registry.record('UI_STRUCTURE', hierarchy, {
|
|
136
|
+
filePath: relativePath,
|
|
137
|
+
line: path.node.loc?.start?.line,
|
|
138
|
+
detail: hierarchy
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
LogicalExpression(path) {
|
|
145
|
+
let clauseCount = 1;
|
|
146
|
+
let curr = path.node;
|
|
147
|
+
while (curr && curr.type === 'LogicalExpression') {
|
|
148
|
+
clauseCount++;
|
|
149
|
+
curr = curr.left;
|
|
150
|
+
}
|
|
151
|
+
if (clauseCount >= 3) {
|
|
152
|
+
const sig = `CLAUSES_${clauseCount}_OP_${path.node.operator}`;
|
|
153
|
+
registry.record('PREDICATE_LOGIC', sig, {
|
|
154
|
+
filePath: relativePath,
|
|
155
|
+
line: path.node.loc?.start?.line,
|
|
156
|
+
detail: `${clauseCount}-clause ${path.node.operator} condition`
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
ReturnStatement(path) {
|
|
162
|
+
const functionParent = path.getFunctionParent();
|
|
163
|
+
const fnName = functionParent?.node?.id?.name;
|
|
164
|
+
const isHook = fnName && /^use[A-Z]/.test(fnName);
|
|
165
|
+
|
|
166
|
+
if (isHook && path.node.argument && path.node.argument.type === 'ObjectExpression') {
|
|
167
|
+
const props = (path.node.argument.properties || [])
|
|
168
|
+
.filter((p) => p.type === 'ObjectProperty' && p.key?.name)
|
|
169
|
+
.map((p) => p.key.name);
|
|
170
|
+
|
|
171
|
+
if (props.length >= 3) {
|
|
172
|
+
const sorted = props.sort().join(',');
|
|
173
|
+
registry.record('HOOK_SIGNATURE', sorted, {
|
|
174
|
+
filePath: relativePath,
|
|
175
|
+
line: path.node.loc?.start?.line,
|
|
176
|
+
detail: `{ ${sorted.replace(/,/g, ', ')} }`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
};
|
package/cli/audit/prompts.js
CHANGED
|
@@ -1,36 +1,19 @@
|
|
|
1
|
-
import { groupViolationsByRule } from './reporter-grouping.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let current = root;
|
|
18
|
-
for (const seg of segments) {
|
|
19
|
-
const segName = seg.endsWith('/') ? seg : `${seg}/`;
|
|
20
|
-
if (!current.dirs.has(segName)) {
|
|
21
|
-
current.dirs.set(segName, { dirs: new Map(), files: new Map() });
|
|
22
|
-
}
|
|
23
|
-
current = current.dirs.get(segName);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!current.files.has(fileName)) {
|
|
27
|
-
current.files.set(fileName, []);
|
|
28
|
-
}
|
|
29
|
-
const loc = v.column ? `${v.line}:${v.column}` : `${v.line}`;
|
|
30
|
-
current.files.get(fileName).push(loc);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return root;
|
|
1
|
+
import { groupViolationsByRule, buildPathTree } from './reporter-grouping.js';
|
|
2
|
+
import {
|
|
3
|
+
CYAN,
|
|
4
|
+
YELLOW,
|
|
5
|
+
RED,
|
|
6
|
+
ORANGE,
|
|
7
|
+
DIM,
|
|
8
|
+
BOLD,
|
|
9
|
+
RESET
|
|
10
|
+
} from './reporter-utils.js';
|
|
11
|
+
|
|
12
|
+
const SEVERITY_COLORS = {
|
|
13
|
+
CRITICAL: RED,
|
|
14
|
+
HIGH: ORANGE,
|
|
15
|
+
MEDIUM: YELLOW,
|
|
16
|
+
LOW: DIM
|
|
34
17
|
};
|
|
35
18
|
|
|
36
19
|
const renderTreeLines = (node, depth = 0) => {
|
|
@@ -39,7 +22,7 @@ const renderTreeLines = (node, depth = 0) => {
|
|
|
39
22
|
|
|
40
23
|
const sortedDirs = Array.from(node.dirs.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
41
24
|
for (const [dirName, childNode] of sortedDirs) {
|
|
42
|
-
lines.push(`${indent}
|
|
25
|
+
lines.push(`${indent}📁 ${BOLD}${dirName}${RESET}`);
|
|
43
26
|
const childLines = renderTreeLines(childNode, depth + 1);
|
|
44
27
|
lines.push(...childLines);
|
|
45
28
|
}
|
|
@@ -47,7 +30,7 @@ const renderTreeLines = (node, depth = 0) => {
|
|
|
47
30
|
const sortedFiles = Array.from(node.files.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
48
31
|
for (const [fileName, locs] of sortedFiles) {
|
|
49
32
|
const uniqueLocs = Array.from(new Set(locs)).join(', ');
|
|
50
|
-
lines.push(`${indent}- \`${fileName}:${uniqueLocs}\``);
|
|
33
|
+
lines.push(`${indent}- \`${YELLOW}${fileName}:${uniqueLocs}${RESET}\``);
|
|
51
34
|
}
|
|
52
35
|
|
|
53
36
|
return lines;
|
|
@@ -61,10 +44,11 @@ export const formatGroupedPromptViolations = (violations = []) => {
|
|
|
61
44
|
const lines = [];
|
|
62
45
|
|
|
63
46
|
ruleGroups.forEach((rg, idx) => {
|
|
47
|
+
const sevColor = SEVERITY_COLORS[rg.severity] || YELLOW;
|
|
64
48
|
const countLabel = rg.total === 1 ? '1 item' : `${rg.total} items`;
|
|
65
|
-
lines.push(`${idx + 1}. [${rg.rule}] (${countLabel})`);
|
|
66
|
-
lines.push(` Hazard:
|
|
67
|
-
lines.push(` Directive: ${rg.directive}`);
|
|
49
|
+
lines.push(`${idx + 1}. ${sevColor}[${rg.rule}]${RESET} ${BOLD}(${countLabel})${RESET}`);
|
|
50
|
+
lines.push(` Hazard: ${rg.hazard}`);
|
|
51
|
+
lines.push(` Directive: ${CYAN}${rg.directive}${RESET}`);
|
|
68
52
|
lines.push(' Locations:');
|
|
69
53
|
|
|
70
54
|
const tree = buildPathTree(rg.violations);
|
|
@@ -106,11 +90,12 @@ export const buildGradeFPrompt = (report, options = {}) => {
|
|
|
106
90
|
}
|
|
107
91
|
|
|
108
92
|
lines.push('### STRICT EXECUTION RULES:');
|
|
109
|
-
lines.push('1.
|
|
110
|
-
lines.push('2.
|
|
111
|
-
lines.push('3.
|
|
112
|
-
lines.push('4. Zero
|
|
113
|
-
lines.push('5.
|
|
93
|
+
lines.push('1. Pre-Split Pattern Discovery: Survey cross-file patterns before slicing; extract canonical shared capsules first to avoid proliferating duplicate one-off patterns.');
|
|
94
|
+
lines.push('2. Every new molecule component must stay under 100 lines.');
|
|
95
|
+
lines.push('3. Top-level page views must be 10-20 line declarative Table-of-Contents templates assembling components via named slots.');
|
|
96
|
+
lines.push('4. Zero synthetic or mock data: return live data or explicit empty states.');
|
|
97
|
+
lines.push('5. Zero render-hack setTimeout: replace with await nextTick() or flush: post.');
|
|
98
|
+
lines.push('6. Output surgical diffs or modular replacements. Preserve existing external exports.');
|
|
114
99
|
|
|
115
100
|
return lines.join('\n');
|
|
116
101
|
};
|
|
@@ -256,11 +241,13 @@ export const buildHotspotsPrompt = (report) => {
|
|
|
256
241
|
lines.push('');
|
|
257
242
|
|
|
258
243
|
lines.push('### STRICT EXECUTION RULES:');
|
|
259
|
-
lines.push('1.
|
|
260
|
-
lines.push('2.
|
|
261
|
-
lines.push('3.
|
|
262
|
-
lines.push('4.
|
|
263
|
-
lines.push('5.
|
|
244
|
+
lines.push('1. Branch First: Change to a new branch prefixed with `chem-x/NAMEOFIMPROVEMENT` before making changes, then commit the branch and create a PR to main.');
|
|
245
|
+
lines.push('2. Pre-Split Pattern Discovery: Audit recurring UI layouts, state machines, and predicates across monoliths first; extract canonical shared capsules before slicing to prevent bespoke pattern duplication.');
|
|
246
|
+
lines.push('3. Molecular Limits: Molecule capsules must strictly remain under 100 lines per file.');
|
|
247
|
+
lines.push('4. Table-of-Contents Views: Top-level page views must be 10 to 20 line declarative Table of Contents assembling molecules via named slots.');
|
|
248
|
+
lines.push('5. Composable Return Contracts: Custom hooks/composables must strictly limit returns to 3 to 5 properties (State + Status + Actions).');
|
|
249
|
+
lines.push('6. Type Co-location: Co-locate granular types/*.d.ts files inside each feature capsule (< 100 lines per type file) instead of creating type monoliths.');
|
|
250
|
+
lines.push('7. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
264
251
|
|
|
265
252
|
return lines.join('\n');
|
|
266
253
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveGradeColor } from './reporter-utils.js';
|
|
2
2
|
import { getAsciiGradeLines } from './reporter-ascii.js';
|
|
3
|
+
import { getChemicalXGradientColor, ANSI } from '../theme.js';
|
|
3
4
|
|
|
4
5
|
const BANNER_ART = [
|
|
5
6
|
' ██████╗██╗ ██╗███████╗███╗ ███╗██╗ ██████╗ █████╗ ██╗ ██╗ ██╗',
|
|
@@ -18,7 +19,7 @@ export const getChemicalXAsciiBanner = (gradeOrReport = null) => {
|
|
|
18
19
|
: gradeOrReport;
|
|
19
20
|
|
|
20
21
|
const lines = [];
|
|
21
|
-
lines.push(
|
|
22
|
+
lines.push(` ${ANSI.BOLD}${ANSI.GOLD}The Secret Sauce to Vibe Coding!${ANSI.RESET}`);
|
|
22
23
|
|
|
23
24
|
const gColor = grade ? resolveGradeColor(grade) : '';
|
|
24
25
|
const gradeLines = grade ? getAsciiGradeLines(grade, gColor, true) : [];
|
|
@@ -35,21 +36,8 @@ export const getChemicalXAsciiBanner = (gradeOrReport = null) => {
|
|
|
35
36
|
continue;
|
|
36
37
|
}
|
|
37
38
|
const t = i / (MAX_BANNER_LEN - 1);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
let b;
|
|
41
|
-
if (t < 0.5) {
|
|
42
|
-
const factor = t * 2;
|
|
43
|
-
r = Math.round(244 * (1 - factor) + 192 * factor);
|
|
44
|
-
g = Math.round(114 * (1 - factor) + 132 * factor);
|
|
45
|
-
b = Math.round(182 * (1 - factor) + 252 * factor);
|
|
46
|
-
} else {
|
|
47
|
-
const factor = (t - 0.5) * 2;
|
|
48
|
-
r = Math.round(192 * (1 - factor) + 129 * factor);
|
|
49
|
-
g = Math.round(132 * (1 - factor) + 140 * factor);
|
|
50
|
-
b = Math.round(252 * (1 - factor) + 248 * factor);
|
|
51
|
-
}
|
|
52
|
-
out += `\x1b[38;2;${r};${g};${b}m\x1b[1m${ch}\x1b[0m`;
|
|
39
|
+
const { ansi } = getChemicalXGradientColor(t);
|
|
40
|
+
out += `${ansi}${ANSI.BOLD}${ch}${ANSI.RESET}`;
|
|
53
41
|
}
|
|
54
42
|
|
|
55
43
|
if (isSideBySide && gradeLines.length > lineIdx) {
|