@chemx/create-chemx 26.9.13-272 → 26.9.13-383
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 +7 -7
- package/cli/audit/extended-visitors.js +10 -4
- package/cli/audit/history.js +2 -4
- package/cli/audit/prompts.js +37 -0
- package/cli/audit/reporter-sections.d.ts +1 -0
- package/cli/audit/reporter-sections.js +55 -1
- package/cli/audit/reporter-utils.js +28 -0
- package/cli/audit/reporter.js +2 -0
- package/cli/audit/rules-predicates.d.ts +5 -0
- package/cli/audit/rules-predicates.js +35 -3
- package/cli/audit.js +13 -7
- package/cli/build.js +5 -1
- package/cli/generator.js +4 -1
- package/cli/index.js +4 -1
- package/cli/license.js +50 -0
- package/cli/navigator-actions.js +94 -98
- package/cli/navigator-grades.js +28 -0
- package/cli/navigator-menu.js +18 -17
- package/cli/navigator.d.ts +2 -0
- package/cli/navigator.js +2 -0
- package/cli/scaffold.js +24 -9
- package/docs/CHANGELOG.md +22 -0
- package/package.json +1 -1
|
@@ -222,12 +222,13 @@ export const createAiSlopVisitors = ({ relativePath, violations }) => {
|
|
|
222
222
|
VariableDeclarator(astPath) {
|
|
223
223
|
if (!isComponentFile) return;
|
|
224
224
|
const idName = astPath.node.id?.name?.toLowerCase();
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
225
|
+
if (!idName || !REINVENTED_UTILS.has(idName)) return;
|
|
226
|
+
|
|
227
|
+
const initNode = astPath.node.init;
|
|
228
|
+
const isFunction = t.isArrowFunctionExpression(initNode) || t.isFunctionExpression(initNode);
|
|
229
|
+
if (!isFunction) return;
|
|
230
|
+
|
|
231
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
231
232
|
const meta = RULE_REGISTRY.AI_SLOP_UTILITY_REINVENTION;
|
|
232
233
|
violations.push({
|
|
233
234
|
filePath: relativePath,
|
|
@@ -240,7 +241,6 @@ export const createAiSlopVisitors = ({ relativePath, violations }) => {
|
|
|
240
241
|
directive: meta.directive,
|
|
241
242
|
isAiSlop: true
|
|
242
243
|
});
|
|
243
|
-
}
|
|
244
244
|
}
|
|
245
245
|
};
|
|
246
246
|
};
|
|
@@ -3,6 +3,11 @@ import path from 'node:path';
|
|
|
3
3
|
import * as t from '@babel/types';
|
|
4
4
|
import { RULE_REGISTRY } from './rules-registry.js';
|
|
5
5
|
import { toResultSync } from './rules-helpers.js';
|
|
6
|
+
import {
|
|
7
|
+
isComponentExtension,
|
|
8
|
+
isTemplateExtension,
|
|
9
|
+
isCommentLine
|
|
10
|
+
} from './rules-predicates.js';
|
|
6
11
|
|
|
7
12
|
const BARE_BOOLEANS = new Set([
|
|
8
13
|
'loading', 'valid', 'active', 'visible', 'open', 'disabled',
|
|
@@ -37,11 +42,12 @@ const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
|
|
|
37
42
|
|
|
38
43
|
// Stage 1: Atomic Concept Declarations
|
|
39
44
|
const isMoleculePath = relativePath.includes('molecules') || baseName.startsWith('m-');
|
|
40
|
-
const isComponentExt = ext
|
|
45
|
+
const isComponentExt = isComponentExtension(ext);
|
|
41
46
|
const isTestOrSpecFile = baseName.includes('.spec.') || baseName.includes('.test.');
|
|
42
47
|
|
|
43
48
|
// Stage 2: Unified Decision Variable & Early Guard Clause
|
|
44
|
-
const
|
|
49
|
+
const isCandidateComponent = isMoleculePath && isComponentExt;
|
|
50
|
+
const isMoleculeComponent = isCandidateComponent && !isTestOrSpecFile;
|
|
45
51
|
if (!isMoleculeComponent) return;
|
|
46
52
|
|
|
47
53
|
const dir = path.dirname(filePath);
|
|
@@ -75,12 +81,12 @@ export const checkExtendedTextPatterns = (content, lines, relativePath, filePath
|
|
|
75
81
|
|
|
76
82
|
const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath);
|
|
77
83
|
const ext = path.extname(filePath);
|
|
78
|
-
const isTemplateFile = ext
|
|
84
|
+
const isTemplateFile = isTemplateExtension(ext);
|
|
79
85
|
|
|
80
86
|
lines.forEach((lineText, idx) => {
|
|
81
87
|
const lineNum = idx + 1;
|
|
82
88
|
const trimmed = lineText.trim();
|
|
83
|
-
const isComment =
|
|
89
|
+
const isComment = isCommentLine(trimmed);
|
|
84
90
|
|
|
85
91
|
// Pillar 9: Hardcoded Secret Detection
|
|
86
92
|
const hasEnvReference = trimmed.includes('process.env') || trimmed.includes('import.meta.env');
|
package/cli/audit/history.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { groupViolationsBySeverity } from './reporter.js';
|
|
4
|
+
import { hasMatchingAuditMetrics } from './rules-predicates.js';
|
|
4
5
|
|
|
5
6
|
const RESET = '\x1b[0m';
|
|
6
7
|
const BOLD = '\x1b[1m';
|
|
@@ -177,10 +178,7 @@ export const saveAuditSnapshot = (report, cwd = process.cwd()) => {
|
|
|
177
178
|
if (history.length > 0) {
|
|
178
179
|
const last = history[history.length - 1];
|
|
179
180
|
const timeDiff = Math.abs(Date.now() - new Date(last.timestamp).getTime());
|
|
180
|
-
const isSameMetrics =
|
|
181
|
-
last.health?.score === snapshot.health?.score &&
|
|
182
|
-
last.metrics?.totalLoc === snapshot.metrics?.totalLoc &&
|
|
183
|
-
last.violations?.total === snapshot.violations?.total;
|
|
181
|
+
const isSameMetrics = hasMatchingAuditMetrics(last, snapshot);
|
|
184
182
|
|
|
185
183
|
if (timeDiff < 5000 && isSameMetrics) {
|
|
186
184
|
return {
|
package/cli/audit/prompts.js
CHANGED
|
@@ -252,6 +252,43 @@ export const buildHotspotsPrompt = (report) => {
|
|
|
252
252
|
return lines.join('\n');
|
|
253
253
|
};
|
|
254
254
|
|
|
255
|
+
export const buildPillarPrompt = (report, pillarName) => {
|
|
256
|
+
const { violations = [], hotspots = [] } = report;
|
|
257
|
+
const pillarViolations = violations.filter((v) => v.pillar === pillarName);
|
|
258
|
+
const isPillar1 = pillarName === 'Line Budgets & Monolith Decomposition';
|
|
259
|
+
const relevantHotspots = isPillar1 ? hotspots.filter((h) => h.isMonolith || h.lineCount > 500) : [];
|
|
260
|
+
|
|
261
|
+
const hasNoViolations = pillarViolations.length === 0;
|
|
262
|
+
const hasNoHotspots = relevantHotspots.length === 0;
|
|
263
|
+
if (hasNoViolations && hasNoHotspots) return '';
|
|
264
|
+
|
|
265
|
+
const lines = [];
|
|
266
|
+
lines.push(`Act as a Principal Systems Architect. Surgically refactor the following ${pillarName} violations according to Chemical X Molecular Architecture Standards:\n`);
|
|
267
|
+
|
|
268
|
+
if (relevantHotspots.length > 0) {
|
|
269
|
+
lines.push('### MONOLITHIC REFACTORING HOTSPOTS');
|
|
270
|
+
lines.push('Action: Decompose into crystalline molecule capsules (< 100 lines) and declarative Table-of-Contents views.\n');
|
|
271
|
+
relevantHotspots.forEach((h, i) => {
|
|
272
|
+
lines.push(`${i + 1}. File: \`${h.filePath}\` (${h.lineCount} lines, ${h.violationCount} hazards)`);
|
|
273
|
+
});
|
|
274
|
+
lines.push('');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (pillarViolations.length > 0) {
|
|
278
|
+
lines.push(`### ${pillarName.toUpperCase()} HAZARD VIOLATIONS`);
|
|
279
|
+
lines.push(...formatGroupedPromptViolations(pillarViolations));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
lines.push('### STRICT EXECUTION RULES:');
|
|
283
|
+
lines.push('1. Branch First: Change to a new branch prefixed with `chem-x/NAMEOFIMPROVEMENT` before making changes.');
|
|
284
|
+
lines.push('2. Pre-Split Pattern Discovery: Survey cross-file patterns before slicing; extract canonical shared capsules first.');
|
|
285
|
+
lines.push('3. Molecular Capsule Limit: Maximum 100 lines per molecule capsule file.');
|
|
286
|
+
lines.push('4. Table-of-Contents Views: Top-level page views must be 10 to 20 line declarative templates assembling components via named slots.');
|
|
287
|
+
lines.push('5. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
288
|
+
|
|
289
|
+
return lines.join('\n');
|
|
290
|
+
};
|
|
291
|
+
|
|
255
292
|
export const buildMasterPrompt = (report) => {
|
|
256
293
|
const sections = [
|
|
257
294
|
buildGradeFPrompt(report, { excludeAiSlop: true }),
|
|
@@ -5,6 +5,7 @@ export declare function formatCriticalSection(report: AuditReport): string;
|
|
|
5
5
|
export declare function formatHighMediumSection(report: AuditReport): string;
|
|
6
6
|
export declare function formatLowSection(report: AuditReport): string;
|
|
7
7
|
export declare function formatPillarsSection(report: AuditReport, themeColor?: string | null): string;
|
|
8
|
+
export declare function formatSinglePillarSection(report: AuditReport, pillarName: string, themeColor?: string | null): string;
|
|
8
9
|
export declare function formatHotspotsSection(report: AuditReport, themeColor?: string | null): string;
|
|
9
10
|
export declare function formatContextAnalysisSection(report: AuditReport, themeColor?: string | null): string;
|
|
10
11
|
export declare function formatAiSlopSection(report: AuditReport, themeColor?: string | null): string;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
resolveRiskColor,
|
|
15
15
|
resolveHotspotBadge,
|
|
16
16
|
resolveTopSectionColor,
|
|
17
|
+
resolveIndividualPillarGrade,
|
|
17
18
|
PILLAR_EMOJIS,
|
|
18
19
|
formatPillarReactionBadgesTerminal
|
|
19
20
|
} from './reporter-utils.js';
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
buildHotspotsPrompt,
|
|
25
26
|
formatPromptBox
|
|
26
27
|
} from './prompts.js';
|
|
28
|
+
import { resolveFirstDefined } from './rules-predicates.js';
|
|
27
29
|
|
|
28
30
|
const isSlopViolation = (v) => Boolean(v.isAiSlop);
|
|
29
31
|
|
|
@@ -35,7 +37,8 @@ const formatHotspotItem = (h, idx) => {
|
|
|
35
37
|
export const formatScorecardSection = (report, themeColor = null) => {
|
|
36
38
|
const { metrics, health } = report;
|
|
37
39
|
const sectionColor = themeColor || resolveTopSectionColor(report);
|
|
38
|
-
const
|
|
40
|
+
const gradeTarget = resolveFirstDefined(health?.score, health?.grade, 'A');
|
|
41
|
+
const gradeColor = resolveGradeColor(gradeTarget);
|
|
39
42
|
const lines = [];
|
|
40
43
|
|
|
41
44
|
lines.push('');
|
|
@@ -176,6 +179,57 @@ export const formatPillarsSection = (report, themeColor = null) => {
|
|
|
176
179
|
return lines.join('\n');
|
|
177
180
|
};
|
|
178
181
|
|
|
182
|
+
export const formatSinglePillarSection = (report, pillarName, themeColor = null) => {
|
|
183
|
+
const pillarData = report?.pillars?.[pillarName] || {
|
|
184
|
+
violations: 0,
|
|
185
|
+
critical: 0,
|
|
186
|
+
high: 0,
|
|
187
|
+
medium: 0,
|
|
188
|
+
low: 0,
|
|
189
|
+
status: 'PASSED'
|
|
190
|
+
};
|
|
191
|
+
const grade = resolveIndividualPillarGrade(pillarData);
|
|
192
|
+
const gradeColor = resolveGradeColor(grade);
|
|
193
|
+
const sectionColor = themeColor || gradeColor;
|
|
194
|
+
const icon = PILLAR_EMOJIS[pillarName] || '🏛️';
|
|
195
|
+
const lines = [];
|
|
196
|
+
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push(`${sectionColor}======================================================================${RESET}`);
|
|
199
|
+
lines.push(`${BOLD}${sectionColor} [ ${icon} ] PILLAR ARCHITECTURAL AUDIT : ${pillarName.toUpperCase()}${RESET}`);
|
|
200
|
+
lines.push(`${sectionColor}======================================================================${RESET}`);
|
|
201
|
+
lines.push(` Pillar Grade: ${gradeColor}${BOLD}Grade: ${grade}${RESET}`);
|
|
202
|
+
lines.push(` Compliance Status: ${getStatusBadge(pillarData.status)}`);
|
|
203
|
+
lines.push(` Violations: ${pillarData.violations} total (${pillarData.critical} Critical, ${pillarData.high} High, ${pillarData.medium} Medium, ${pillarData.low} Low)`);
|
|
204
|
+
lines.push(`${sectionColor}----------------------------------------------------------------------${RESET}`);
|
|
205
|
+
|
|
206
|
+
if (pillarData.violations === 0) {
|
|
207
|
+
lines.push(`\n ${GREEN}✔ Outstanding! Zero violations detected for this pillar.${RESET}`);
|
|
208
|
+
lines.push(` ${DIM}All inspected modules adhere strictly to Chemical X standards for ${pillarName}.${RESET}\n`);
|
|
209
|
+
} else {
|
|
210
|
+
const pillarViolations = (report.violations || []).filter((v) => v.pillar === pillarName);
|
|
211
|
+
if (pillarViolations.length > 0) {
|
|
212
|
+
lines.push(`\n ${BOLD}${sectionColor}🚨 DETECTED VIOLATIONS (${pillarViolations.length}):${RESET}\n`);
|
|
213
|
+
lines.push(renderGroupedViolationsTerminal(pillarViolations));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const isPillar1 = pillarName === 'Line Budgets & Monolith Decomposition';
|
|
218
|
+
if (isPillar1) {
|
|
219
|
+
const monolithHotspots = (report?.hotspots || []).filter((h) => h.isMonolith || h.lineCount > 500);
|
|
220
|
+
if (monolithHotspots.length > 0) {
|
|
221
|
+
lines.push(` ${BOLD}${sectionColor}🔥 MONOLITHIC HOTSPOTS (${monolithHotspots.length}):${RESET}`);
|
|
222
|
+
monolithHotspots.forEach((h, idx) => {
|
|
223
|
+
lines.push(formatHotspotItem(h, idx));
|
|
224
|
+
});
|
|
225
|
+
lines.push('');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
lines.push(`${sectionColor}======================================================================${RESET}\n`);
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
};
|
|
232
|
+
|
|
179
233
|
export const formatHotspotsSection = (report, themeColor = null, options = {}) => {
|
|
180
234
|
const { includePrompt = true } = typeof options === 'boolean' ? { includePrompt: options } : options;
|
|
181
235
|
const { hotspots } = report;
|
|
@@ -63,6 +63,34 @@ export const getStatusBadge = (status) => {
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
export const resolveIndividualPillarGrade = (pillarData) => {
|
|
67
|
+
const hasNoViolations = !pillarData || pillarData.violations === 0;
|
|
68
|
+
if (hasNoViolations) return 'A+';
|
|
69
|
+
|
|
70
|
+
const hasCritical = (pillarData.critical || 0) > 0;
|
|
71
|
+
if (hasCritical) return 'F';
|
|
72
|
+
|
|
73
|
+
const hasHigh = (pillarData.high || 0) > 0;
|
|
74
|
+
if (hasHigh) return 'D';
|
|
75
|
+
|
|
76
|
+
const hasMedium = (pillarData.medium || 0) > 0;
|
|
77
|
+
if (hasMedium) return 'C';
|
|
78
|
+
|
|
79
|
+
const hasLow = (pillarData.low || 0) > 0;
|
|
80
|
+
if (hasLow) return 'B';
|
|
81
|
+
|
|
82
|
+
return 'A+';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const resolvePillarRiskWeight = (pillarData) => {
|
|
86
|
+
if (!pillarData) return 0;
|
|
87
|
+
const critical = pillarData.critical || 0;
|
|
88
|
+
const high = pillarData.high || 0;
|
|
89
|
+
const medium = pillarData.medium || 0;
|
|
90
|
+
const low = pillarData.low || 0;
|
|
91
|
+
return critical * 8 + high * 4 + medium * 2 + low * 1;
|
|
92
|
+
};
|
|
93
|
+
|
|
66
94
|
export const resolveGradeColor = (scoreOrGrade) => {
|
|
67
95
|
if (typeof scoreOrGrade === 'string') {
|
|
68
96
|
const g = scoreOrGrade.toUpperCase();
|
package/cli/audit/reporter.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
formatHighMediumSection,
|
|
17
17
|
formatLowSection,
|
|
18
18
|
formatPillarsSection,
|
|
19
|
+
formatSinglePillarSection,
|
|
19
20
|
formatHotspotsSection,
|
|
20
21
|
formatContextAnalysisSection,
|
|
21
22
|
formatAiSlopSection
|
|
@@ -108,6 +109,7 @@ export {
|
|
|
108
109
|
formatHighMediumSection,
|
|
109
110
|
formatLowSection,
|
|
110
111
|
formatPillarsSection,
|
|
112
|
+
formatSinglePillarSection,
|
|
111
113
|
formatHotspotsSection,
|
|
112
114
|
formatContextAnalysisSection,
|
|
113
115
|
formatAiSlopSection
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type * as babelTypes from '@babel/types';
|
|
2
2
|
|
|
3
3
|
export declare function isComponentPath(path: string): boolean;
|
|
4
|
+
export declare function isComponentExtension(ext: string): boolean;
|
|
5
|
+
export declare function isTemplateExtension(ext: string): boolean;
|
|
6
|
+
export declare function isCommentLine(line: string): boolean;
|
|
4
7
|
export declare function isCodeLine(line: string): boolean;
|
|
5
8
|
export declare function isConsoleCallStatement(stmt: unknown, t: typeof babelTypes): boolean;
|
|
6
9
|
export declare function isShallowCatchBody(body: readonly unknown[], t: typeof babelTypes): boolean;
|
|
@@ -9,6 +12,8 @@ export declare function isRedundantPassthroughReturn(curr: unknown, next: unknow
|
|
|
9
12
|
export declare function isHookIdentifier(idNode: unknown): boolean;
|
|
10
13
|
export declare function isCustomHookFunction(astPath: unknown): boolean;
|
|
11
14
|
export declare function resolveStartLine(primaryNode?: unknown, fallbackNode?: unknown, defaultLine?: number): number;
|
|
15
|
+
export declare function resolveFirstDefined<T>(...candidates: readonly (T | null | undefined)[]): T | undefined;
|
|
16
|
+
export declare function hasMatchingAuditMetrics(a: Record<string, any>, b: Record<string, any>): boolean;
|
|
12
17
|
export declare function isZeroDelayTimeout(callee: unknown, delayArg: unknown, t: typeof babelTypes): boolean;
|
|
13
18
|
export declare function isUnguardedConsoleCall(callee: unknown, t: typeof babelTypes): boolean;
|
|
14
19
|
export declare function matchesDiscussionTitle(titleLower: string, targetLower: string): boolean;
|
|
@@ -4,14 +4,27 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const COMPONENT_PATH_SEGMENTS = ['molecules', 'components', '/m-', '/views/', '/pages/'];
|
|
7
|
+
const COMPONENT_EXTENSIONS = new Set(['.vue', '.tsx', '.jsx']);
|
|
8
|
+
const TEMPLATE_EXTENSIONS = new Set(['.vue', '.html', '.svelte']);
|
|
9
|
+
const COMMENT_PREFIXES = ['//', '*', '/*'];
|
|
7
10
|
const CONSOLE_LOG_METHODS = ['log', 'info', 'warn'];
|
|
8
11
|
const NON_INTERACTIVE_FLAGS = ['--non-interactive', '--no-interactive', '--ci'];
|
|
12
|
+
const FORMAT_FLAGS = ['--markdown', '--md', '--json'];
|
|
9
13
|
const GRADE_RANKS = { 'A+': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1, 'F': 0 };
|
|
10
14
|
|
|
11
15
|
export const isComponentPath = (path) => {
|
|
12
16
|
return COMPONENT_PATH_SEGMENTS.some((segment) => path.includes(segment));
|
|
13
17
|
};
|
|
14
18
|
|
|
19
|
+
export const isComponentExtension = (ext) => COMPONENT_EXTENSIONS.has(ext);
|
|
20
|
+
|
|
21
|
+
export const isTemplateExtension = (ext) => TEMPLATE_EXTENSIONS.has(ext);
|
|
22
|
+
|
|
23
|
+
export const isCommentLine = (line) => {
|
|
24
|
+
if (!line) return false;
|
|
25
|
+
return COMMENT_PREFIXES.some((prefix) => line.startsWith(prefix));
|
|
26
|
+
};
|
|
27
|
+
|
|
15
28
|
export const isCodeLine = (line) => {
|
|
16
29
|
if (!line) return false;
|
|
17
30
|
const isLineComment = line.startsWith('//');
|
|
@@ -60,7 +73,26 @@ export const isCustomHookFunction = (astPath) => {
|
|
|
60
73
|
};
|
|
61
74
|
|
|
62
75
|
export const resolveStartLine = (primaryNode, fallbackNode, defaultLine = 1) => {
|
|
63
|
-
|
|
76
|
+
const primaryLine = primaryNode?.loc?.start?.line;
|
|
77
|
+
if (primaryLine !== undefined) return primaryLine;
|
|
78
|
+
const fallbackLine = fallbackNode?.loc?.start?.line;
|
|
79
|
+
if (fallbackLine !== undefined) return fallbackLine;
|
|
80
|
+
return defaultLine;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const resolveFirstDefined = (...candidates) => {
|
|
84
|
+
for (const val of candidates) {
|
|
85
|
+
if (val !== null && val !== undefined) return val;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const hasMatchingAuditMetrics = (a, b) => {
|
|
91
|
+
const isScoreMatch = a.health?.score === b.health?.score;
|
|
92
|
+
if (!isScoreMatch) return false;
|
|
93
|
+
const isLocMatch = a.metrics?.totalLoc === b.metrics?.totalLoc;
|
|
94
|
+
if (!isLocMatch) return false;
|
|
95
|
+
return a.violations?.total === b.violations?.total;
|
|
64
96
|
};
|
|
65
97
|
|
|
66
98
|
export const isZeroDelayTimeout = (callee, delayArg, t) => {
|
|
@@ -101,7 +133,7 @@ export const evaluateAuditFailure = (conditions = []) => {
|
|
|
101
133
|
export const isNonInteractiveSession = (rawArgs, env = process.env) => {
|
|
102
134
|
const hasCliFlag = NON_INTERACTIVE_FLAGS.some((flag) => rawArgs.includes(flag));
|
|
103
135
|
const hasOutputFlag = rawArgs.some((arg) => arg.startsWith('--output=') || arg.startsWith('-o='));
|
|
104
|
-
const hasFormatFlag =
|
|
136
|
+
const hasFormatFlag = FORMAT_FLAGS.some((flag) => rawArgs.includes(flag));
|
|
105
137
|
const hasCiEnv = Boolean(env.CI || env.GIT_DIR);
|
|
106
|
-
return hasCliFlag
|
|
138
|
+
return [hasCliFlag, hasOutputFlag, hasFormatFlag, hasCiEnv].some(Boolean);
|
|
107
139
|
};
|
package/cli/audit.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
formatHighMediumSection,
|
|
33
33
|
formatLowSection,
|
|
34
34
|
formatPillarsSection,
|
|
35
|
+
formatSinglePillarSection,
|
|
35
36
|
formatHotspotsSection,
|
|
36
37
|
formatContextAnalysisSection,
|
|
37
38
|
formatFailuresSection,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
formatPillarReactionBadgesMarkdown,
|
|
51
52
|
formatPillarShieldBadges,
|
|
52
53
|
resolveBadgeColor,
|
|
54
|
+
PILLAR_EMOJIS,
|
|
53
55
|
PILLAR_SHORT_NAMES
|
|
54
56
|
} from './audit/reporter.js';
|
|
55
57
|
import {
|
|
@@ -59,6 +61,7 @@ import {
|
|
|
59
61
|
buildGradeBPrompt,
|
|
60
62
|
buildAiSlopPrompt,
|
|
61
63
|
buildHotspotsPrompt,
|
|
64
|
+
buildPillarPrompt,
|
|
62
65
|
buildMasterPrompt,
|
|
63
66
|
formatPromptBox,
|
|
64
67
|
formatGroupedPromptViolations
|
|
@@ -78,14 +81,14 @@ const IGNORED_DIRS = new Set([
|
|
|
78
81
|
'out'
|
|
79
82
|
]);
|
|
80
83
|
|
|
84
|
+
const EXCLUDED_NAME_PATTERNS = ['.test.', '.spec.', '.min.'];
|
|
85
|
+
|
|
81
86
|
const isSourceFile = (name) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
!name.includes('.min.')
|
|
88
|
-
);
|
|
87
|
+
const isExtensionValid = /\.(tsx|ts|jsx|js|vue)$/.test(name);
|
|
88
|
+
if (!isExtensionValid) return false;
|
|
89
|
+
if (name.endsWith('.d.ts')) return false;
|
|
90
|
+
const isExcluded = EXCLUDED_NAME_PATTERNS.some((pat) => name.includes(pat));
|
|
91
|
+
return !isExcluded;
|
|
89
92
|
};
|
|
90
93
|
|
|
91
94
|
export const auditFile = (filePath, relativePath) => {
|
|
@@ -249,6 +252,7 @@ export {
|
|
|
249
252
|
formatHighMediumSection,
|
|
250
253
|
formatLowSection,
|
|
251
254
|
formatPillarsSection,
|
|
255
|
+
formatSinglePillarSection,
|
|
252
256
|
formatHotspotsSection,
|
|
253
257
|
formatContextAnalysisSection,
|
|
254
258
|
formatFailuresSection,
|
|
@@ -267,6 +271,7 @@ export {
|
|
|
267
271
|
formatPillarReactionBadgesMarkdown,
|
|
268
272
|
formatPillarShieldBadges,
|
|
269
273
|
resolveBadgeColor,
|
|
274
|
+
PILLAR_EMOJIS,
|
|
270
275
|
PILLAR_SHORT_NAMES,
|
|
271
276
|
buildGradeFPrompt,
|
|
272
277
|
buildGradeDPrompt,
|
|
@@ -274,6 +279,7 @@ export {
|
|
|
274
279
|
buildGradeBPrompt,
|
|
275
280
|
buildAiSlopPrompt,
|
|
276
281
|
buildHotspotsPrompt,
|
|
282
|
+
buildPillarPrompt,
|
|
277
283
|
buildMasterPrompt,
|
|
278
284
|
formatPromptBox,
|
|
279
285
|
formatGroupedPromptViolations,
|
package/cli/build.js
CHANGED
|
@@ -5,6 +5,10 @@ import { groupBuildDiagnostics } from './build/grouper.js';
|
|
|
5
5
|
import { formatTerminalBuildReport, formatJsonBuildReport } from './build/reporter.js';
|
|
6
6
|
import { ANSI } from './theme.js';
|
|
7
7
|
|
|
8
|
+
const IGNORED_COMMAND_TOKENS = new Set(['build', 'run', 'wrap']);
|
|
9
|
+
|
|
10
|
+
const isCommandCandidate = (arg) => !arg.startsWith('-') && !IGNORED_COMMAND_TOKENS.has(arg);
|
|
11
|
+
|
|
8
12
|
const parseCommandFromArgs = (args) => {
|
|
9
13
|
const dashDashIndex = args.indexOf('--');
|
|
10
14
|
if (dashDashIndex !== -1) {
|
|
@@ -12,7 +16,7 @@ const parseCommandFromArgs = (args) => {
|
|
|
12
16
|
if (afterDash.length > 0) return afterDash;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
const candidate = args.find(
|
|
19
|
+
const candidate = args.find(isCommandCandidate);
|
|
16
20
|
return candidate ? candidate.trim() : null;
|
|
17
21
|
};
|
|
18
22
|
|
package/cli/generator.js
CHANGED
|
@@ -34,6 +34,9 @@ const detectBaseDir = () => {
|
|
|
34
34
|
return '.';
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
const IGNORED_NAME_TOKENS = new Set(['generate', 'capsule', 'add']);
|
|
38
|
+
const isCapsuleNameArg = (arg) => !arg.startsWith('-') && !IGNORED_NAME_TOKENS.has(arg);
|
|
39
|
+
|
|
37
40
|
export const runGenerateWizard = async (rawArgs = []) => {
|
|
38
41
|
renderBanner('Chemical X: Molecular Capsule Wizard');
|
|
39
42
|
await checkOrPromptEvaluation('generate capsule');
|
|
@@ -41,7 +44,7 @@ export const runGenerateWizard = async (rawArgs = []) => {
|
|
|
41
44
|
const useGum = hasGum();
|
|
42
45
|
const isYes = rawArgs.includes('-y') || rawArgs.includes('--yes');
|
|
43
46
|
|
|
44
|
-
const nameArg = rawArgs.find(
|
|
47
|
+
const nameArg = rawArgs.find(isCapsuleNameArg);
|
|
45
48
|
const frameworkArg = (rawArgs.find((a) => a.startsWith('--framework=')) || '').split('=')[1]
|
|
46
49
|
|| (rawArgs.includes('-f') ? rawArgs[rawArgs.indexOf('-f') + 1] : null);
|
|
47
50
|
const tierArg = (rawArgs.find((a) => a.startsWith('--tier=')) || '').split('=')[1];
|
package/cli/index.js
CHANGED
|
@@ -36,6 +36,9 @@ import { runBuildAudit } from './build.js';
|
|
|
36
36
|
|
|
37
37
|
const rawArgs = process.argv.slice(2);
|
|
38
38
|
const invokedBin = path.basename(process.argv[1] || '');
|
|
39
|
+
const CAPSULE_PREFIXES = ['m-', 'a-', 'o-', 't-'];
|
|
40
|
+
|
|
41
|
+
const isCapsulePrefix = (arg) => CAPSULE_PREFIXES.some((prefix) => arg.startsWith(prefix));
|
|
39
42
|
|
|
40
43
|
const loadProjectConfig = () => {
|
|
41
44
|
const cfgPath = path.resolve(process.cwd(), '.chemx', 'config.json');
|
|
@@ -212,7 +215,7 @@ const main = async () => {
|
|
|
212
215
|
printHelp();
|
|
213
216
|
break;
|
|
214
217
|
default:
|
|
215
|
-
if (firstArg && (firstArg
|
|
218
|
+
if (firstArg && isCapsulePrefix(firstArg)) {
|
|
216
219
|
await runGenerateWizard(rawArgs);
|
|
217
220
|
} else if (firstArg && !firstArg.startsWith('-')) {
|
|
218
221
|
await runScaffold(firstArg, rawArgs, runAudit);
|
package/cli/license.js
CHANGED
|
@@ -16,11 +16,28 @@ export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
|
16
16
|
export const DEVICE_FILE = path.join(CONFIG_DIR, 'device_id');
|
|
17
17
|
|
|
18
18
|
export const API_BASE = process.env.CHEMICAL_X_API_URL || 'https://chemicalx.xophz.com';
|
|
19
|
+
export const GATEKEEPER_API_URL = process.env.COMPASS_GATEKEEPER_URL || 'https://mycompassconsulting.com/wp-json/compass/v1/gatekeeper';
|
|
19
20
|
export const URL_LEARN = 'https://chemicalx.xophz.com';
|
|
20
21
|
export const URL_SPONSOR = 'https://github.com/sponsors/Chemical-X-Protocol';
|
|
21
22
|
export const URL_STANDARD = 'https://mycompassconsulting.com/buy/chemical-x/standard';
|
|
22
23
|
export const URL_MASTER = 'https://mycompassconsulting.com/buy/chemical-x/master';
|
|
23
24
|
|
|
25
|
+
export const verifyWithGatekeeper = async (keyOrUser, deviceId = null) => {
|
|
26
|
+
const effectiveDeviceId = deviceId || getOrCreateDeviceId();
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${GATEKEEPER_API_URL}/licenses/validate`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ key: keyOrUser, deviceId: effectiveDeviceId })
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) return { valid: false };
|
|
34
|
+
return await res.json();
|
|
35
|
+
} catch {
|
|
36
|
+
return { valid: false };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
|
|
24
41
|
export const ensureConfigDir = () => {
|
|
25
42
|
if (fs.existsSync(CONFIG_DIR)) return true;
|
|
26
43
|
const [, err] = toResultSync(() => fs.mkdirSync(CONFIG_DIR, { recursive: true }));
|
|
@@ -328,9 +345,42 @@ export const fetchStarterKitFiles = async (licenseKey) => {
|
|
|
328
345
|
);
|
|
329
346
|
return responseData.files || {};
|
|
330
347
|
} catch (err) {
|
|
348
|
+
// Fallback: Verify with Gatekeeper or local blueprints
|
|
349
|
+
const gatekeeperCheck = await verifyWithGatekeeper(normalizedKey, deviceId);
|
|
350
|
+
if (gatekeeperCheck.valid) {
|
|
351
|
+
saveLicenseKey(normalizedKey);
|
|
352
|
+
process.stdout.write(
|
|
353
|
+
`\x1b[32m✔ Verified via Gatekeeper for @${gatekeeperCheck.github_user || 'sponsor'} [Tier: ${gatekeeperCheck.tier}]\x1b[0m\n\n`
|
|
354
|
+
);
|
|
355
|
+
const localFiles = loadLocalBlueprintFiles();
|
|
356
|
+
if (Object.keys(localFiles).length > 0) return localFiles;
|
|
357
|
+
}
|
|
358
|
+
|
|
331
359
|
process.stderr.write(
|
|
332
360
|
`\x1b[31m✕ Network Error: Failed to reach edge server (${err.message}).\x1b[0m\n`
|
|
333
361
|
);
|
|
334
362
|
process.exit(1);
|
|
335
363
|
}
|
|
336
364
|
};
|
|
365
|
+
|
|
366
|
+
export const loadLocalBlueprintFiles = () => {
|
|
367
|
+
const blueprintsDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../blueprints');
|
|
368
|
+
const files = {};
|
|
369
|
+
if (!fs.existsSync(blueprintsDir)) return files;
|
|
370
|
+
|
|
371
|
+
const walk = (dir, base = '') => {
|
|
372
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
373
|
+
for (const entry of entries) {
|
|
374
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
375
|
+
const full = path.join(dir, entry.name);
|
|
376
|
+
if (entry.isDirectory()) {
|
|
377
|
+
walk(full, rel);
|
|
378
|
+
} else {
|
|
379
|
+
files[rel] = fs.readFileSync(full, 'utf-8');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
walk(blueprintsDir);
|
|
384
|
+
return files;
|
|
385
|
+
};
|
|
386
|
+
|
package/cli/navigator-actions.js
CHANGED
|
@@ -27,13 +27,21 @@ import {
|
|
|
27
27
|
buildGradeCPrompt,
|
|
28
28
|
buildGradeBPrompt,
|
|
29
29
|
buildAiSlopPrompt,
|
|
30
|
+
formatSinglePillarSection,
|
|
31
|
+
buildPillarPrompt,
|
|
32
|
+
PILLAR_EMOJIS,
|
|
33
|
+
PILLAR_SHORT_NAMES,
|
|
30
34
|
buildHotspotsPrompt,
|
|
31
35
|
buildMasterPrompt,
|
|
32
36
|
formatRoadmapSection,
|
|
33
37
|
buildSelfHealingRoadmapPrompt,
|
|
34
38
|
copyToClipboard
|
|
35
39
|
} from "./audit.js";
|
|
36
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
resolveGradeBadge,
|
|
42
|
+
resolveIndividualPillarGrade,
|
|
43
|
+
resolvePillarRiskWeight
|
|
44
|
+
} from "./navigator-grades.js";
|
|
37
45
|
import { showPagedContent } from "./navigator-paged.js";
|
|
38
46
|
import { showConversionMenu } from "./navigator-conversion.js";
|
|
39
47
|
import { handleShareToDiscussions } from "./navigator-share.js";
|
|
@@ -41,111 +49,99 @@ import { runInstallWizard } from "./installer.js";
|
|
|
41
49
|
import { formatButtonTag } from "./navigator-menu.js";
|
|
42
50
|
import { runBadgeCommand } from "./badge.js";
|
|
43
51
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const GRADE_RISK_RANKS = {
|
|
53
|
+
F: 5,
|
|
54
|
+
D: 4,
|
|
55
|
+
C: 3,
|
|
56
|
+
B: 2,
|
|
57
|
+
A: 1,
|
|
58
|
+
'A+': 0
|
|
59
|
+
};
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
61
|
+
const comparePillarsByRisk = (a, b) => {
|
|
62
|
+
const rankA = GRADE_RISK_RANKS[a.grade] ?? 0;
|
|
63
|
+
const rankB = GRADE_RISK_RANKS[b.grade] ?? 0;
|
|
64
|
+
if (rankA !== rankB) {
|
|
65
|
+
return rankB - rankA;
|
|
66
|
+
}
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
if (b.riskWeight !== a.riskWeight) {
|
|
69
|
+
return b.riskWeight - a.riskWeight;
|
|
70
|
+
}
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
if (b.violations !== a.violations) {
|
|
73
|
+
return b.violations - a.violations;
|
|
74
|
+
}
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const gradeCCount = medium.length + warningMonoliths.length;
|
|
66
|
-
const gradeBCount = low.length;
|
|
67
|
-
const gradeACount = passedPillarsCount;
|
|
76
|
+
return a.originalIndex - b.originalIndex;
|
|
77
|
+
};
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
export const buildActiveGrades = (report) => {
|
|
80
|
+
const { pillars = {}, violations = [], aiSlop } = report;
|
|
81
|
+
const pillarEntries = Object.entries(pillars);
|
|
82
|
+
const items = [];
|
|
83
|
+
|
|
84
|
+
pillarEntries.forEach(([pillarName, data], index) => {
|
|
85
|
+
const grade = resolveIndividualPillarGrade(data);
|
|
86
|
+
const riskWeight = resolvePillarRiskWeight(data);
|
|
87
|
+
const icon = PILLAR_EMOJIS[pillarName] || '🏛️';
|
|
88
|
+
const shortName = PILLAR_SHORT_NAMES[pillarName] || pillarName;
|
|
89
|
+
const isClean = data.violations === 0;
|
|
90
|
+
const countLabel = isClean
|
|
91
|
+
? '0 items'
|
|
92
|
+
: `${data.violations} ${data.violations === 1 ? 'item' : 'items'}`;
|
|
93
|
+
|
|
94
|
+
const handlePillarAction = async () => {
|
|
95
|
+
await showPagedContent(
|
|
96
|
+
formatSinglePillarSection(report, pillarName),
|
|
97
|
+
buildPillarPrompt(report, pillarName)
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
items.push({
|
|
102
|
+
key: `pillar_${data.pillarKey || index + 1}`,
|
|
103
|
+
name: pillarName,
|
|
104
|
+
shortName,
|
|
105
|
+
grade,
|
|
106
|
+
riskWeight,
|
|
107
|
+
violations: data.violations || 0,
|
|
108
|
+
originalIndex: index,
|
|
109
|
+
tag: resolveGradeBadge(grade, 11),
|
|
110
|
+
label: `${icon} ${pillarName} (${countLabel})`,
|
|
111
|
+
action: handlePillarAction
|
|
112
|
+
});
|
|
113
|
+
});
|
|
88
114
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
if (aiSlop) {
|
|
116
|
+
const slopViolations = violations.filter((v) => v.isAiSlop);
|
|
117
|
+
const slopGrade = aiSlop.grade || (slopViolations.length > 0 ? 'D' : 'A+');
|
|
118
|
+
const slopRiskWeight = resolvePillarRiskWeight(aiSlop.breakdown);
|
|
119
|
+
const slopCount = slopViolations.length;
|
|
120
|
+
const isSlopClean = slopCount === 0;
|
|
121
|
+
const slopCountLabel = isSlopClean
|
|
122
|
+
? '0 items'
|
|
123
|
+
: `${slopCount} ${slopCount === 1 ? 'item' : 'items'}`;
|
|
124
|
+
|
|
125
|
+
const handleSlopAction = async () => {
|
|
126
|
+
await showPagedContent(formatAiSlopSection(report), buildAiSlopPrompt(report));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
items.push({
|
|
130
|
+
key: 'grade_slop',
|
|
131
|
+
name: 'AI Slop & Code Authenticity',
|
|
132
|
+
shortName: 'Slop',
|
|
133
|
+
grade: slopGrade,
|
|
134
|
+
riskWeight: slopRiskWeight,
|
|
135
|
+
violations: slopCount,
|
|
136
|
+
originalIndex: pillarEntries.length,
|
|
137
|
+
tag: resolveGradeBadge(slopGrade, 11),
|
|
138
|
+
label: `🤖 AI Slop & Authenticity (${aiSlop.score ?? 100}/100 - ${aiSlop.label ?? 'Pure Artisanal'}) (${slopCountLabel})`,
|
|
104
139
|
action: handleSlopAction
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
key: "grade_b",
|
|
108
|
-
grade: "B",
|
|
109
|
-
count: gradeBCount,
|
|
110
|
-
icon: "💣",
|
|
111
|
-
description: "Low Hygiene Issues & Minor Debts",
|
|
112
|
-
action: handleGradeBAction
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
key: "grade_c",
|
|
116
|
-
grade: "C",
|
|
117
|
-
count: gradeCCount,
|
|
118
|
-
icon: "⚡",
|
|
119
|
-
description: "Medium Severity Debts & Monolith Drift",
|
|
120
|
-
action: handleGradeCAction
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
key: "grade_d",
|
|
124
|
-
grade: "D",
|
|
125
|
-
count: gradeDCount,
|
|
126
|
-
icon: "🔥",
|
|
127
|
-
description: "High Severity Debts & Severe Monoliths",
|
|
128
|
-
action: handleGradeDAction
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
key: "grade_f",
|
|
132
|
-
grade: "F",
|
|
133
|
-
count: gradeFCount,
|
|
134
|
-
icon: "💥",
|
|
135
|
-
description: "Critical Hazards & Extreme Monoliths",
|
|
136
|
-
action: handleGradeFAction
|
|
137
|
-
}
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
const createActiveGradeItem = (g) => ({
|
|
141
|
-
key: g.key || `grade_${g.grade.toLowerCase()}`,
|
|
142
|
-
grade: g.grade.toLowerCase(),
|
|
143
|
-
tag: resolveGradeBadge(g.grade, 11),
|
|
144
|
-
label: `${g.icon} Grade ${g.grade}: ${g.description} (${g.count} ${g.grade === "A" ? "clean" : "items"})`,
|
|
145
|
-
action: g.action
|
|
146
|
-
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
147
142
|
|
|
148
|
-
|
|
143
|
+
items.sort(comparePillarsByRisk);
|
|
144
|
+
return items;
|
|
149
145
|
};
|
|
150
146
|
|
|
151
147
|
export const buildDashboardActionGroups = ({ report, onScaffold = null, onRerun = null }) => {
|
package/cli/navigator-grades.js
CHANGED
|
@@ -53,6 +53,34 @@ export const resolveLowGrade = (count) => {
|
|
|
53
53
|
const isFailedPillar = (p) => p.status === 'FAILED';
|
|
54
54
|
const isWarnPillar = (p) => p.status === 'WARN';
|
|
55
55
|
|
|
56
|
+
export const resolveIndividualPillarGrade = (pillarData) => {
|
|
57
|
+
const hasNoViolations = !pillarData || pillarData.violations === 0;
|
|
58
|
+
if (hasNoViolations) return 'A+';
|
|
59
|
+
|
|
60
|
+
const hasCritical = (pillarData.critical || 0) > 0;
|
|
61
|
+
if (hasCritical) return 'F';
|
|
62
|
+
|
|
63
|
+
const hasHigh = (pillarData.high || 0) > 0;
|
|
64
|
+
if (hasHigh) return 'D';
|
|
65
|
+
|
|
66
|
+
const hasMedium = (pillarData.medium || 0) > 0;
|
|
67
|
+
if (hasMedium) return 'C';
|
|
68
|
+
|
|
69
|
+
const hasLow = (pillarData.low || 0) > 0;
|
|
70
|
+
if (hasLow) return 'B';
|
|
71
|
+
|
|
72
|
+
return 'A+';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const resolvePillarRiskWeight = (pillarData) => {
|
|
76
|
+
if (!pillarData) return 0;
|
|
77
|
+
const critical = pillarData.critical || 0;
|
|
78
|
+
const high = pillarData.high || 0;
|
|
79
|
+
const medium = pillarData.medium || 0;
|
|
80
|
+
const low = pillarData.low || 0;
|
|
81
|
+
return critical * 8 + high * 4 + medium * 2 + low * 1;
|
|
82
|
+
};
|
|
83
|
+
|
|
56
84
|
export const resolvePillarGrade = (pillars) => {
|
|
57
85
|
const values = Object.values(pillars || {});
|
|
58
86
|
const failed = values.filter(isFailedPillar).length;
|
package/cli/navigator-menu.js
CHANGED
|
@@ -30,6 +30,12 @@ export const buildNavigatorMenu = (topActions, activeGrades, midActions, bottomA
|
|
|
30
30
|
return { menuItems, menuOptions };
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
const ROADMAP_KEYWORDS = ['Roadmap', 'Healing', 'Self-Healing'];
|
|
34
|
+
const RERUN_KEYWORDS = ['Re-Run', 'Rerun', 'Re-run', 'Re-Audit'];
|
|
35
|
+
const SHARE_KEYWORDS = ['Share', 'Discussions', 'Plug your'];
|
|
36
|
+
|
|
37
|
+
const isAnyKeywordPresent = (text, keywords) => keywords.some((kw) => text.includes(kw));
|
|
38
|
+
|
|
33
39
|
const isChoiceMatchingItem = (cleanChoice) => (item) => {
|
|
34
40
|
const prefix = `${item.index}.`;
|
|
35
41
|
if (cleanChoice.startsWith(prefix) || cleanChoice === String(item.index)) return true;
|
|
@@ -51,19 +57,10 @@ const isChoiceMatchingItem = (cleanChoice) => (item) => {
|
|
|
51
57
|
) {
|
|
52
58
|
return true;
|
|
53
59
|
}
|
|
54
|
-
if (
|
|
55
|
-
item.key === "roadmap" &&
|
|
56
|
-
(cleanChoice.includes("Roadmap") || cleanChoice.includes("Healing") || cleanChoice.includes("Self-Healing"))
|
|
57
|
-
) {
|
|
60
|
+
if (item.key === "roadmap" && isAnyKeywordPresent(cleanChoice, ROADMAP_KEYWORDS)) {
|
|
58
61
|
return true;
|
|
59
62
|
}
|
|
60
|
-
if (
|
|
61
|
-
item.key === "rerun" &&
|
|
62
|
-
(cleanChoice.includes("Re-Run") ||
|
|
63
|
-
cleanChoice.includes("Rerun") ||
|
|
64
|
-
cleanChoice.includes("Re-run") ||
|
|
65
|
-
cleanChoice.includes("Re-Audit"))
|
|
66
|
-
) {
|
|
63
|
+
if (item.key === "rerun" && isAnyKeywordPresent(cleanChoice, RERUN_KEYWORDS)) {
|
|
67
64
|
return true;
|
|
68
65
|
}
|
|
69
66
|
if (
|
|
@@ -73,12 +70,7 @@ const isChoiceMatchingItem = (cleanChoice) => (item) => {
|
|
|
73
70
|
return true;
|
|
74
71
|
}
|
|
75
72
|
if (item.key === "progress" && cleanChoice.includes("Progress")) return true;
|
|
76
|
-
if (
|
|
77
|
-
item.key === "share" &&
|
|
78
|
-
(cleanChoice.includes("Share") ||
|
|
79
|
-
cleanChoice.includes("Discussions") ||
|
|
80
|
-
cleanChoice.includes("Plug your"))
|
|
81
|
-
) {
|
|
73
|
+
if (item.key === "share" && isAnyKeywordPresent(cleanChoice, SHARE_KEYWORDS)) {
|
|
82
74
|
return true;
|
|
83
75
|
}
|
|
84
76
|
if (item.key === "export" && cleanChoice.includes("Export")) return true;
|
|
@@ -111,6 +103,12 @@ const isChoiceMatchingItem = (cleanChoice) => (item) => {
|
|
|
111
103
|
) {
|
|
112
104
|
return true;
|
|
113
105
|
}
|
|
106
|
+
if (item.name && cleanChoice.toLowerCase().includes(item.name.toLowerCase())) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (item.shortName && cleanChoice.toLowerCase().includes(item.shortName.toLowerCase())) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
114
112
|
if (
|
|
115
113
|
item.grade &&
|
|
116
114
|
(cleanChoice.toLowerCase().includes(`grade ${item.grade}`) ||
|
|
@@ -168,6 +166,9 @@ const isEffectiveMatchingItem = (effective, exitIndex) => (item) => {
|
|
|
168
166
|
) {
|
|
169
167
|
return true;
|
|
170
168
|
}
|
|
169
|
+
if (item.name && effective === item.name.toLowerCase()) return true;
|
|
170
|
+
if (item.shortName && effective === item.shortName.toLowerCase()) return true;
|
|
171
|
+
if (item.key && (effective === item.key.toLowerCase() || effective === item.key.replace(/^pillar_/, '').toLowerCase())) return true;
|
|
171
172
|
if (item.key === effective) return true;
|
|
172
173
|
if (item.grade && (effective === item.grade || effective === `grade ${item.grade}`)) {
|
|
173
174
|
return true;
|
package/cli/navigator.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare function resolveCriticalGrade(count: number): string;
|
|
|
7
7
|
export declare function resolveHighMedGrade(count: number): string;
|
|
8
8
|
export declare function resolveLowGrade(count: number): string;
|
|
9
9
|
export declare function resolvePillarGrade(pillars: Record<string, unknown>): string;
|
|
10
|
+
export declare function resolveIndividualPillarGrade(pillarData: unknown): string;
|
|
11
|
+
export declare function resolvePillarRiskWeight(pillarData: unknown): number;
|
|
10
12
|
export declare function resolveHotspotGrade(hotspots: readonly { readonly lineCount: number }[]): string;
|
|
11
13
|
export declare function resolveContextGrade(riskTier: string): string;
|
|
12
14
|
|
package/cli/navigator.js
CHANGED
package/cli/scaffold.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { hasGum, gumInput, promptQuestion, renderBanner } from './terminal.js';
|
|
4
|
-
import { obtainLicenseKey, fetchStarterKitFiles } from './license.js';
|
|
4
|
+
import { obtainLicenseKey, fetchStarterKitFiles, loadLocalBlueprintFiles } from './license.js';
|
|
5
5
|
|
|
6
6
|
export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null) => {
|
|
7
7
|
renderBanner('Chemical X: Molecular Architecture Scaffolder (npm create chemx)');
|
|
8
8
|
|
|
9
9
|
const licenseKey = await obtainLicenseKey(rawArgs, onRunAudit);
|
|
10
|
+
let isCommunity = false;
|
|
11
|
+
let files = {};
|
|
12
|
+
|
|
10
13
|
if (!licenseKey) {
|
|
11
|
-
process.
|
|
12
|
-
|
|
14
|
+
process.stdout.write(
|
|
15
|
+
'\n\x1b[38;2;98;201;255m⚡ Chemical X: Community Edition (Free)\x1b[0m\n' +
|
|
16
|
+
'The Community version is free because you tell us about your project\n' +
|
|
17
|
+
'and the app will automatically post your scorecard to our GitHub Discussions.\n' +
|
|
18
|
+
'You get a free plug, we get data to prove Chemical X works and brings real value.\n' +
|
|
19
|
+
'Everybody wins! Thank you for supporting our Community version.\n\n'
|
|
20
|
+
);
|
|
21
|
+
files = loadLocalBlueprintFiles();
|
|
22
|
+
isCommunity = true;
|
|
23
|
+
} else {
|
|
24
|
+
files = await fetchStarterKitFiles(licenseKey);
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
let targetName = projectName;
|
|
@@ -29,8 +41,6 @@ export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null)
|
|
|
29
41
|
process.exit(1);
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
const files = await fetchStarterKitFiles(licenseKey);
|
|
33
|
-
|
|
34
44
|
process.stdout.write(`Scaffolding Molecular Architecture into: \x1b[36m${finalDirName}/\x1b[0m\n`);
|
|
35
45
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
36
46
|
|
|
@@ -66,13 +76,18 @@ export const runInit = async (targetSubDir = 'src/chemical-x', rawArgs = [], onR
|
|
|
66
76
|
|
|
67
77
|
const targetDir = path.resolve(process.cwd(), targetSubDir);
|
|
68
78
|
const licenseKey = await obtainLicenseKey(rawArgs, onRunAudit);
|
|
79
|
+
let files = {};
|
|
80
|
+
|
|
69
81
|
if (!licenseKey) {
|
|
70
|
-
process.
|
|
71
|
-
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
'\n\x1b[38;2;98;201;255m⚡ Chemical X: Community Drop-in (Free)\x1b[0m\n' +
|
|
84
|
+
'Unpacking standard blueprints for your project. Support community standards: chemicalx.xophz.com\n\n'
|
|
85
|
+
);
|
|
86
|
+
files = loadLocalBlueprintFiles();
|
|
87
|
+
} else {
|
|
88
|
+
files = await fetchStarterKitFiles(licenseKey);
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
const files = await fetchStarterKitFiles(licenseKey);
|
|
75
|
-
|
|
76
91
|
process.stdout.write(`Unpacking blueprints and hooks into: \x1b[36m${targetSubDir}/\x1b[0m\n`);
|
|
77
92
|
|
|
78
93
|
let count = 0;
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,20 @@ All notable changes to the Chemical X Starter Kit repository will be documented
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
6
|
|
|
7
|
+
## [2026-09-13]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Gatekeeper SaaS & Sponsor Verification**: Integrated client license validation in `cli/license.js` with `Xophz Gatekeeper` (`/wp-json/compass/v1/gatekeeper/licenses/validate`), allowing verification of both license keys (`CX-ST-`, `CX-SP-`) and GitHub Sponsor handles.
|
|
11
|
+
- **Community Edition Flow**: Added reciprocal free Community Edition scaffolding in `cli/scaffold.js` allowing developers to unpack blueprints locally while sharing scorecards with community GitHub Discussions.
|
|
12
|
+
- **Local Blueprint Fallback**: Added `loadLocalBlueprintFiles` in `cli/license.js` enabling offline and community blueprint scaffolding without remote edge download dependencies.
|
|
13
|
+
|
|
7
14
|
## [2026-09-12]
|
|
8
15
|
|
|
9
16
|
### Added
|
|
17
|
+
- Added individual pillar grading and risk-ordered navigation menu (`cli/navigator-actions.js`, `cli/navigator-menu.js`, `cli/navigator-grades.js`):
|
|
18
|
+
- Replaced generic letter-grade buckets with individual architectural pillar categories, dynamically displaying each pillar's specific grade tag (`A+` to `F`) and item counts.
|
|
19
|
+
- Sorted pillar categories dynamically from highest to lowest risk based on severity weight (`critical * 8 + high * 4 + medium * 2 + low * 1`), while preserving canonical 1 to 11 order when all pillars pass with Grade A+.
|
|
20
|
+
- Added dedicated individual pillar report view (`formatSinglePillarSection` in `cli/audit/reporter-sections.js`) and pillar AI refactoring prompt generator (`buildPillarPrompt` in `cli/audit/prompts.js`).
|
|
10
21
|
- Added Build Auditor wrapper command (`chemx build`, aliases `run`, `wrap`) with token-optimized silent execution (`cli/build.js`, `cli/build/`):
|
|
11
22
|
- Wraps arbitrary or auto-detected build scripts (`pnpm run build`, `yarn build`, `bun run build`, `npm run build`, `vite build`, `tsc --noEmit`).
|
|
12
23
|
- Automatically parses and catalogs TypeScript (`TS*`), Rollup/Vite resolution, Sass/PostCSS syntax, and bundle budget errors into structured diagnostic categories with 1-line remediation hints.
|
|
@@ -20,6 +31,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
20
31
|
- Preserved clean stdout in workflows by writing markdown reports to `--output` files without dumping raw markdown to the terminal.
|
|
21
32
|
- Replaced top-level synchronous config directory creation in `cli/license.js` with lazy `ensureConfigDir` and `toResultSync` Result Tuple pattern.
|
|
22
33
|
|
|
34
|
+
### Refactored
|
|
35
|
+
- Harmonized multi-clause boolean predicate topologies across CLI and audit modules:
|
|
36
|
+
- Extracted canonical extension helpers (`isComponentExtension`, `isTemplateExtension`, `isCommentLine`), metric comparison (`hasMatchingAuditMetrics`), and fallback resolution (`resolveFirstDefined`) in `cli/audit/rules-predicates.js` and `cli/audit/rules-predicates.d.ts`.
|
|
37
|
+
- Refactored `checkMoleculeCoLocatedTest` and `checkExtendedTextPatterns` in `cli/audit/extended-visitors.js` to use canonical predicate helpers and 2-stage atomic booleans.
|
|
38
|
+
- Refactored `VariableDeclarator` in `cli/audit/ai-slop-detector.js` to use sequential guard clauses for reinvented utility detection.
|
|
39
|
+
- Refactored `saveAuditSnapshot` in `cli/audit/history.js` using `hasMatchingAuditMetrics`.
|
|
40
|
+
- Refactored capsule prefix detection in `cli/index.js` and CLI build/generate argument parsing in `cli/build.js` and `cli/generator.js` using predicate helpers.
|
|
41
|
+
- Refactored menu choice matching in `cli/navigator-menu.js` using `isAnyKeywordPresent` and keyword catalogs.
|
|
42
|
+
- Refactored scorecard grade target resolution in `cli/audit/reporter-sections.js` using `resolveFirstDefined`.
|
|
43
|
+
- Refactored source file detection in `cli/audit.js` using early return guard clauses and exclusion arrays.
|
|
44
|
+
|
|
23
45
|
## [2026-09-08]
|
|
24
46
|
|
|
25
47
|
### Added
|