@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.
@@ -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
+ };
@@ -1,36 +1,19 @@
1
- import { groupViolationsByRule } from './reporter-grouping.js';
2
-
3
- const CYAN = '\x1b[38;2;98;201;255m';
4
- const BOLD = '\x1b[1m';
5
- const DIM = '\x1b[2m';
6
- const RESET = '\x1b[0m';
7
-
8
- const buildPathTree = (violations) => {
9
- const root = { dirs: new Map(), files: new Map() };
10
-
11
- for (const v of violations) {
12
- const rawPath = v.filePath || '';
13
- const normalized = rawPath.replace(/\\/g, '/').replace(/^\.\//, '');
14
- const segments = normalized.split('/');
15
- const fileName = segments.pop();
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}* ${dirName}`);
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: ${rg.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. Every new molecule component must stay under 100 lines.');
110
- lines.push('2. Top-level page views must be 10-20 line declarative Table-of-Contents templates assembling components via named slots.');
111
- lines.push('3. Zero synthetic or mock data: return live data or explicit empty states.');
112
- lines.push('4. Zero render-hack setTimeout: replace with await nextTick() or flush: post.');
113
- lines.push('5. Output surgical diffs or modular replacements. Preserve existing external exports.');
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. Molecular Limits: Molecule capsules must strictly remain under 100 lines per file.');
260
- lines.push('2. Table-of-Contents Views: Top-level page views must be 10 to 20 line declarative Table of Contents assembling molecules via named slots.');
261
- lines.push('3. Composable Return Contracts: Custom hooks/composables must strictly limit returns to 3 to 5 properties (State + Status + Actions).');
262
- lines.push('4. Type Co-location: Co-locate granular types/*.d.ts files inside each feature capsule (< 100 lines per type file) instead of creating type monoliths.');
263
- lines.push('5. Zero breaking changes to external component APIs, route exports, or existing props.');
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(' \x1b[1m\x1b[37mThe Secret Sauce to \x1b[38;2;98;201;255mVibe Coding\x1b[0m');
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
- let r;
39
- let g;
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) {