@chemx/create-chemx 26.9.12-93 → 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.
Files changed (49) hide show
  1. package/cli/audit/ai-slop-detector.js +25 -41
  2. package/cli/audit/ast-visitors.js +10 -13
  3. package/cli/audit/history.js +45 -11
  4. package/cli/audit/pattern-detector.js +182 -0
  5. package/cli/audit/prompts.js +129 -72
  6. package/cli/audit/reporter-banner.js +4 -16
  7. package/cli/audit/reporter-grades.js +28 -16
  8. package/cli/audit/reporter-grouping.d.ts +6 -0
  9. package/cli/audit/reporter-grouping.js +48 -5
  10. package/cli/audit/reporter-markdown.js +31 -16
  11. package/cli/audit/reporter-sections.js +28 -12
  12. package/cli/audit/reporter-utils.js +88 -1
  13. package/cli/audit/reporter.d.ts +13 -4
  14. package/cli/audit/reporter.js +32 -5
  15. package/cli/audit/roadmap.js +236 -0
  16. package/cli/audit/rules-predicates.d.ts +17 -0
  17. package/cli/audit/rules-predicates.js +107 -0
  18. package/cli/audit/rules.js +6 -2
  19. package/cli/audit/social-gh.js +2 -6
  20. package/cli/audit/social-publisher.js +1 -1
  21. package/cli/audit/social.js +25 -9
  22. package/cli/audit/types.d.ts +52 -5
  23. package/cli/audit.js +76 -7
  24. package/cli/badge.js +9 -2
  25. package/cli/build/detector.js +68 -0
  26. package/cli/build/executor.js +58 -0
  27. package/cli/build/grouper.js +78 -0
  28. package/cli/build/parser-matchers.js +116 -0
  29. package/cli/build/parser.js +80 -0
  30. package/cli/build/reporter.js +66 -0
  31. package/cli/build/types.d.ts +58 -0
  32. package/cli/build/types.js +1 -0
  33. package/cli/build.d.ts +16 -0
  34. package/cli/build.js +55 -0
  35. package/cli/help.js +14 -0
  36. package/cli/index.js +39 -14
  37. package/cli/installer-templates.js +43 -4
  38. package/cli/license.js +10 -7
  39. package/cli/navigator-actions.js +34 -6
  40. package/cli/navigator-banner.js +4 -2
  41. package/cli/navigator-menu.js +18 -0
  42. package/cli/navigator-share.js +1 -1
  43. package/cli/navigator.d.ts +8 -1
  44. package/cli/navigator.js +3 -3
  45. package/cli/terminal.js +6 -5
  46. package/cli/theme.d.ts +41 -0
  47. package/cli/theme.js +84 -0
  48. package/docs/CHANGELOG.md +46 -0
  49. 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 (nextTrim && !nextTrim.startsWith('//') && !nextTrim.startsWith('/*')) {
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
- const param = astPath.node.param;
166
- if (param && t.isIdentifier(param) && param.typeAnnotation) {
167
- if (t.isTSTypeAnnotation(param.typeAnnotation) && t.isTSAnyKeyword(param.typeAnnotation.typeAnnotation)) {
168
- const line = astPath.node.loc?.start.line || 1;
169
- const meta = RULE_REGISTRY.AI_SLOP_LAZY_ANY;
170
- violations.push({
171
- filePath: relativePath,
172
- line,
173
- column: astPath.node.loc?.start.column || 1,
174
- hazard: 'Lazy any catch parameter widening detected',
175
- rule: 'AI_SLOP_LAZY_ANY',
176
- severity: meta.severity,
177
- pillar: meta.pillar,
178
- directive: meta.directive,
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.loc?.start.line || astPath.node.loc?.start.line || 1;
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.name === 'setTimeout' && delayArg && t.isNumericLiteral(delayArg) && delayArg.value === 0) {
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({
@@ -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
- return JSON.parse(raw);
135
+ explicitBaseline = JSON.parse(raw);
134
136
  } catch (err) {
135
137
  const error = err instanceof Error ? err : new Error(String(err));
136
- return null;
138
+ explicitBaseline = null;
137
139
  }
138
140
  }
139
- const history = getAuditHistory(cwd);
140
- return history.length > 0 ? history[0] : null;
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 if none exists
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
- if (!fs.existsSync(baselinePath)) {
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} ARCHITECTURAL TRANSFORMATION : BEFORE & AFTER PROGRESSION${RESET}`);
301
- lines.push(`${DIM} Comparing baseline snapshot vs. latest refactored audit${RESET}`);
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}Baseline Audit:${RESET} ${DIM}${dateBefore} [${beforeSnapshot.id}]${RESET}`);
309
- lines.push(` ${BOLD}Latest Audit:${RESET} ${GREEN}${dateAfter} [${afterSnapshot.id}]${RESET}`);
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
+ };