@chemx/create-chemx 26.9.12-272 → 26.9.13-272
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 +13 -10
- package/cli/audit/reporter-grades.js +28 -16
- package/cli/audit/reporter-markdown.js +31 -16
- package/cli/audit/reporter-sections.js +14 -4
- package/cli/audit/reporter-utils.js +75 -0
- package/cli/audit/reporter.d.ts +13 -4
- package/cli/audit/reporter.js +29 -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/build/detector.js +68 -0
- package/cli/build/executor.js +58 -0
- package/cli/build/grouper.js +78 -0
- package/cli/build/parser-matchers.js +116 -0
- package/cli/build/parser.js +80 -0
- package/cli/build/reporter.js +66 -0
- package/cli/build/types.d.ts +58 -0
- package/cli/build/types.js +1 -0
- package/cli/build.d.ts +16 -0
- package/cli/build.js +55 -0
- package/cli/help.js +14 -0
- package/cli/index.js +38 -19
- package/cli/installer-templates.js +42 -3
- 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/docs/CHANGELOG.md +36 -0
- package/package.json +3 -2
|
@@ -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
|
@@ -90,11 +90,12 @@ export const buildGradeFPrompt = (report, options = {}) => {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
lines.push('### STRICT EXECUTION RULES:');
|
|
93
|
-
lines.push('1.
|
|
94
|
-
lines.push('2.
|
|
95
|
-
lines.push('3.
|
|
96
|
-
lines.push('4. Zero
|
|
97
|
-
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.');
|
|
98
99
|
|
|
99
100
|
return lines.join('\n');
|
|
100
101
|
};
|
|
@@ -240,11 +241,13 @@ export const buildHotspotsPrompt = (report) => {
|
|
|
240
241
|
lines.push('');
|
|
241
242
|
|
|
242
243
|
lines.push('### STRICT EXECUTION RULES:');
|
|
243
|
-
lines.push('1.
|
|
244
|
-
lines.push('2.
|
|
245
|
-
lines.push('3.
|
|
246
|
-
lines.push('4.
|
|
247
|
-
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.');
|
|
248
251
|
|
|
249
252
|
return lines.join('\n');
|
|
250
253
|
};
|
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
import { getReportCardAsciiLines } from './reporter-ascii.js';
|
|
20
20
|
import { renderGroupedViolationsTerminal } from './reporter-grouping.js';
|
|
21
21
|
|
|
22
|
-
export const formatGradeFSection = (report) => {
|
|
22
|
+
export const formatGradeFSection = (report, options = {}) => {
|
|
23
|
+
const { includePrompt = true } = options;
|
|
23
24
|
const { violations, hotspots, pillars } = report;
|
|
24
25
|
const { critical } = groupViolationsBySeverity(violations);
|
|
25
26
|
const extremeMonoliths = hotspots.filter((h) => h.lineCount >= 2000);
|
|
@@ -67,16 +68,19 @@ export const formatGradeFSection = (report) => {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
if (includePrompt) {
|
|
72
|
+
const promptF = buildGradeFPrompt(report);
|
|
73
|
+
if (promptF) {
|
|
74
|
+
lines.push(formatPromptBox('🤖 AI AGENT REFACTORING PROMPT (GRADE F HAZARDS)', promptF));
|
|
75
|
+
}
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
lines.push(`${RED}======================================================================${RESET}\n`);
|
|
76
79
|
return lines.join('\n');
|
|
77
80
|
};
|
|
78
81
|
|
|
79
|
-
export const formatGradeDSection = (report) => {
|
|
82
|
+
export const formatGradeDSection = (report, options = {}) => {
|
|
83
|
+
const { includePrompt = true } = options;
|
|
80
84
|
const { violations, hotspots, pillars } = report;
|
|
81
85
|
const { high } = groupViolationsBySeverity(violations);
|
|
82
86
|
const severeMonoliths = hotspots.filter((h) => h.lineCount >= 1000 && h.lineCount < 2000);
|
|
@@ -123,16 +127,19 @@ export const formatGradeDSection = (report) => {
|
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
if (includePrompt) {
|
|
131
|
+
const promptD = buildGradeDPrompt(report);
|
|
132
|
+
if (promptD) {
|
|
133
|
+
lines.push(formatPromptBox('🤖 AI AGENT REFACTORING PROMPT (GRADE D DEBTS)', promptD));
|
|
134
|
+
}
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
lines.push(`${ORANGE}======================================================================${RESET}\n`);
|
|
132
138
|
return lines.join('\n');
|
|
133
139
|
};
|
|
134
140
|
|
|
135
|
-
export const formatGradeCSection = (report) => {
|
|
141
|
+
export const formatGradeCSection = (report, options = {}) => {
|
|
142
|
+
const { includePrompt = true } = options;
|
|
136
143
|
const { violations, hotspots } = report;
|
|
137
144
|
const { medium } = groupViolationsBySeverity(violations);
|
|
138
145
|
const warningMonoliths = hotspots.filter((h) => h.lineCount >= 500 && h.lineCount < 1000);
|
|
@@ -169,16 +176,19 @@ export const formatGradeCSection = (report) => {
|
|
|
169
176
|
}
|
|
170
177
|
}
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
if (includePrompt) {
|
|
180
|
+
const promptC = buildGradeCPrompt(report);
|
|
181
|
+
if (promptC) {
|
|
182
|
+
lines.push(formatPromptBox('🤖 AI AGENT REFACTORING PROMPT (GRADE C DEBTS)', promptC));
|
|
183
|
+
}
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
lines.push(`${CYAN}======================================================================${RESET}\n`);
|
|
178
187
|
return lines.join('\n');
|
|
179
188
|
};
|
|
180
189
|
|
|
181
|
-
export const formatGradeBSection = (report) => {
|
|
190
|
+
export const formatGradeBSection = (report, options = {}) => {
|
|
191
|
+
const { includePrompt = true } = options;
|
|
182
192
|
const { violations } = report;
|
|
183
193
|
const { low } = groupViolationsBySeverity(violations);
|
|
184
194
|
const lines = [];
|
|
@@ -202,9 +212,11 @@ export const formatGradeBSection = (report) => {
|
|
|
202
212
|
lines.push(renderGroupedViolationsTerminal(low));
|
|
203
213
|
}
|
|
204
214
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
215
|
+
if (includePrompt) {
|
|
216
|
+
const promptB = buildGradeBPrompt(report);
|
|
217
|
+
if (promptB) {
|
|
218
|
+
lines.push(formatPromptBox('🤖 AI AGENT REFACTORING PROMPT (GRADE B HYGIENE)', promptB));
|
|
219
|
+
}
|
|
208
220
|
}
|
|
209
221
|
|
|
210
222
|
lines.push(`${YELLOW}======================================================================${RESET}\n`);
|