@chemx/starter-kit 26.9.16-668 → 26.9.17-606
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/README.md +67 -0
- package/assets/chemx-starter-kit-logo.gif +0 -0
- package/blueprints/_view-template.scss +24 -0
- package/blueprints/atoms/a-button.vue +43 -0
- package/blueprints/molecule-capsule/_m-sample-card.scss +75 -0
- package/blueprints/molecule-capsule/index.ts +2 -0
- package/blueprints/molecule-capsule/m-chemx-badge/_m-chemx-badge.scss +76 -0
- package/blueprints/molecule-capsule/m-chemx-badge/index.ts +3 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.tsx +39 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.vue +42 -0
- package/blueprints/molecule-capsule/m-chemx-badge/types.d.ts +12 -0
- package/blueprints/molecule-capsule/m-sample-card.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-sample-card.tsx +72 -0
- package/blueprints/molecule-capsule/m-tab-button/_m-tab-button.scss +49 -0
- package/blueprints/molecule-capsule/m-tab-button/index.ts +3 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +40 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.spec.ts +40 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.vue +38 -0
- package/blueprints/molecule-capsule/m-tab-button/types.d.ts +14 -0
- package/blueprints/molecule-capsule/types.d.ts +12 -0
- package/blueprints/shims-vue.d.ts +4 -0
- package/blueprints/view-template.tsx +20 -0
- package/blueprints/workflows/chemx-audit.yml +79 -0
- package/cli/audit/ast-visitors.js +73 -0
- package/cli/audit/autofix.js +4 -5
- package/cli/audit/extended-visitors.js +239 -0
- package/cli/audit/pattern-detector.js +90 -3
- package/cli/audit/pattern-detector.spec.js +36 -1
- package/cli/audit/reporter-utils.js +13 -22
- package/cli/audit/rules-predicates.js +3 -2
- package/cli/audit/rules-registry.js +25 -0
- package/cli/audit/rules.spec.js +205 -0
- package/cli/audit-preflight.spec.js +11 -0
- package/cli/build.d.ts +2 -1
- package/cli/build.js +8 -5
- package/cli/columnar.d.ts +14 -0
- package/cli/columnar.js +52 -0
- package/cli/columnar.spec.js +49 -0
- package/cli/generator-templates/controller.js +36 -0
- package/cli/generator-templates/hooks.js +60 -0
- package/cli/generator-templates/naming.js +14 -0
- package/cli/generator-templates/react.js +63 -0
- package/cli/generator-templates/specs.js +38 -0
- package/cli/generator-templates/styles.js +64 -0
- package/cli/generator-templates/svelte.js +66 -0
- package/cli/generator-templates/types.js +29 -0
- package/cli/generator-templates/views.js +76 -0
- package/cli/generator-templates/vue.js +62 -0
- package/cli/generator-templates.d.ts +35 -0
- package/cli/generator-templates.js +28 -480
- package/cli/generator-templates.spec.js +108 -0
- package/cli/generator.js +2 -2
- package/cli/generator.spec.js +61 -1
- package/cli/help.js +29 -4
- package/cli/index.js +54 -7
- package/cli/installer-templates.js +29 -8
- package/cli/installer.js +23 -6
- package/cli/license.js +22 -1
- package/cli/mcp/antigravity.js +59 -0
- package/cli/mcp/index.js +2 -1
- package/cli/mcp/installer.js +54 -22
- package/cli/mcp/installer.spec.js +23 -1
- package/cli/mcp/manifests.js +251 -0
- package/cli/mcp/prompts.js +28 -0
- package/cli/mcp/resources.js +175 -64
- package/cli/mcp/server.js +13 -0
- package/cli/mcp/server.spec.js +247 -1
- package/cli/mcp/tools.js +266 -193
- package/cli/mcp/types.d.ts +37 -0
- package/cli/navigator-actions.js +23 -10
- package/cli/navigator-banner.js +2 -3
- package/cli/navigator-guide.js +73 -0
- package/cli/navigator-menu.js +20 -0
- package/cli/navigator.js +1 -0
- package/cli/patcher.d.ts +73 -0
- package/cli/patcher.js +249 -4
- package/cli/patcher.spec.js +148 -0
- package/cli/postinstall.js +27 -0
- package/cli/project-detector.js +20 -2
- package/cli/project-detector.spec.js +19 -0
- package/cli/reader.js +18 -4
- package/cli/scaffold.js +26 -4
- package/cli/search-commands.js +10 -8
- package/cli/search-queries.js +8 -3
- package/cli/search-schema.js +1 -0
- package/cli/search.d.ts +16 -0
- package/cli/search.js +103 -11
- package/cli/silence-warnings.js +17 -0
- package/cli/terminal.js +36 -1
- package/cli/verify.js +377 -0
- package/cli/verify.spec.js +98 -0
- package/docs/CHANGELOG.md +10 -0
- package/package.json +10 -5
- package/scripts/postinstall.mjs +27 -0
- package/scripts/pre-commit.sh +135 -0
- package/scripts/publish-both.mjs +108 -0
- package/cli/mcp.js +0 -326
|
@@ -49,9 +49,10 @@ export const createPatternRegistry = () => {
|
|
|
49
49
|
suggestedCapsule = 'types/state.d.ts';
|
|
50
50
|
recommendation = 'Extract shared discriminated union into domain types to eliminate duplicate definitions';
|
|
51
51
|
} else if (bucket.type === 'UI_STRUCTURE') {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const categorization = categorizeUiStructure(bucket.detail);
|
|
53
|
+
label = categorization.label;
|
|
54
|
+
suggestedCapsule = categorization.suggestedCapsule;
|
|
55
|
+
recommendation = categorization.recommendation;
|
|
55
56
|
} else if (bucket.type === 'PREDICATE_LOGIC') {
|
|
56
57
|
label = `Duplicated Boolean Predicate Topology (${bucket.detail})`;
|
|
57
58
|
suggestedCapsule = 'usePredicateFilter.ts';
|
|
@@ -84,6 +85,92 @@ export const createPatternRegistry = () => {
|
|
|
84
85
|
return { record, resolveHarmonizationCandidates };
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
export const categorizeUiStructure = (signature = '') => {
|
|
89
|
+
const normalized = signature.toLowerCase();
|
|
90
|
+
|
|
91
|
+
const isSvg =
|
|
92
|
+
normalized.startsWith('svg>') ||
|
|
93
|
+
normalized.includes('path+path') ||
|
|
94
|
+
normalized.includes('polygon') ||
|
|
95
|
+
normalized.includes('circle');
|
|
96
|
+
if (isSvg) {
|
|
97
|
+
const isMultiLayer = (normalized.match(/path|circle|rect|polygon|g/g) || []).length > 3;
|
|
98
|
+
return {
|
|
99
|
+
label: `Shared Vector Glyph (${signature})`,
|
|
100
|
+
suggestedCapsule: isMultiLayer ? 'm-vector-glyph' : 'a-icon',
|
|
101
|
+
recommendation: 'Extract foundational icon atom or vector glyph capsule'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const hasButton =
|
|
106
|
+
normalized.includes('btn') ||
|
|
107
|
+
normalized.includes('button');
|
|
108
|
+
if (hasButton) {
|
|
109
|
+
const hasTextOrTitle =
|
|
110
|
+
normalized.includes('span') ||
|
|
111
|
+
normalized.includes('title') ||
|
|
112
|
+
normalized.includes('h1') ||
|
|
113
|
+
normalized.includes('h2') ||
|
|
114
|
+
normalized.includes('h3') ||
|
|
115
|
+
normalized.includes('h4') ||
|
|
116
|
+
normalized.includes('p');
|
|
117
|
+
|
|
118
|
+
if (hasTextOrTitle) {
|
|
119
|
+
return {
|
|
120
|
+
label: `Shared Action Header (${signature})`,
|
|
121
|
+
suggestedCapsule: 'm-action-header',
|
|
122
|
+
recommendation: 'Extract action header molecule capsule to standardize button actions'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
label: `Shared Button Row (${signature})`,
|
|
128
|
+
suggestedCapsule: 'm-button-row',
|
|
129
|
+
recommendation: 'Extract button row molecule capsule to consolidate action bar'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hasChipOrBadge =
|
|
134
|
+
normalized.includes('chip') ||
|
|
135
|
+
normalized.includes('badge') ||
|
|
136
|
+
normalized.includes('pill') ||
|
|
137
|
+
normalized.includes('tag');
|
|
138
|
+
if (hasChipOrBadge) {
|
|
139
|
+
const chipCount = (normalized.match(/chip|badge|pill|tag/g) || []).length;
|
|
140
|
+
if (chipCount > 1) {
|
|
141
|
+
return {
|
|
142
|
+
label: `Shared Pill Row (${signature})`,
|
|
143
|
+
suggestedCapsule: 'm-pill-row',
|
|
144
|
+
recommendation: 'Extract pill row molecule capsule to group status indicators'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
label: `Shared Status Badge (${signature})`,
|
|
150
|
+
suggestedCapsule: 'm-status-badge',
|
|
151
|
+
recommendation: 'Extract status badge atom or capsule to unify state tags'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const hasFormInput =
|
|
156
|
+
normalized.includes('input') ||
|
|
157
|
+
normalized.includes('textarea') ||
|
|
158
|
+
normalized.includes('select');
|
|
159
|
+
if (hasFormInput) {
|
|
160
|
+
return {
|
|
161
|
+
label: `Shared Form Field (${signature})`,
|
|
162
|
+
suggestedCapsule: 'm-form-field',
|
|
163
|
+
recommendation: 'Extract form field molecule encapsulating label and input atom'
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
label: `Shared UI Layout Structure (${signature})`,
|
|
169
|
+
suggestedCapsule: 'm-feature-card',
|
|
170
|
+
recommendation: 'Extract canonical molecule capsule before slicing consumer monoliths'
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
87
174
|
const VOID_ELEMENTS = new Set([
|
|
88
175
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
89
176
|
]);
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
createPatternRegistry,
|
|
5
5
|
extractTemplateTokens,
|
|
6
6
|
buildTagTree,
|
|
7
|
-
recordTemplatePatterns
|
|
7
|
+
recordTemplatePatterns,
|
|
8
|
+
categorizeUiStructure
|
|
8
9
|
} from './pattern-detector.js';
|
|
9
10
|
|
|
10
11
|
test('extractTemplateTokens: extracts tags, ignoring comments and handling quotes', () => {
|
|
@@ -56,6 +57,40 @@ test('buildTagTree and recordTemplatePatterns: captures UI_STRUCTURE from Vue te
|
|
|
56
57
|
|
|
57
58
|
assert.ok(uiCandidate, 'Expected UI_STRUCTURE candidate to be found');
|
|
58
59
|
assert.equal(uiCandidate.detail, 'v-sheet>(x-btn+x-btn)');
|
|
60
|
+
assert.equal(uiCandidate.suggestedCapsule, 'm-button-row');
|
|
59
61
|
assert.equal(uiCandidate.fileCount, 2);
|
|
60
62
|
assert.deepEqual(uiCandidate.uniqueFiles.sort(), ['src/components/A.vue', 'src/components/B.vue']);
|
|
61
63
|
});
|
|
64
|
+
|
|
65
|
+
test('categorizeUiStructure: maps semantic AST topologies to specialized capsules', () => {
|
|
66
|
+
// SVG dual path -> a-icon
|
|
67
|
+
const svgRes = categorizeUiStructure('svg>(path+path)');
|
|
68
|
+
assert.equal(svgRes.suggestedCapsule, 'a-icon');
|
|
69
|
+
assert.ok(svgRes.label.includes('Vector Glyph'));
|
|
70
|
+
|
|
71
|
+
// Action header -> m-action-header
|
|
72
|
+
const actionRes = categorizeUiStructure('div>(span+v-btn)');
|
|
73
|
+
assert.equal(actionRes.suggestedCapsule, 'm-action-header');
|
|
74
|
+
assert.ok(actionRes.label.includes('Action Header'));
|
|
75
|
+
|
|
76
|
+
// Button row -> m-button-row
|
|
77
|
+
const btnRowRes = categorizeUiStructure('div>(v-btn+v-btn)');
|
|
78
|
+
assert.equal(btnRowRes.suggestedCapsule, 'm-button-row');
|
|
79
|
+
|
|
80
|
+
// Status badge -> m-status-badge
|
|
81
|
+
const badgeRes = categorizeUiStructure('div>(span+v-chip)');
|
|
82
|
+
assert.equal(badgeRes.suggestedCapsule, 'm-status-badge');
|
|
83
|
+
|
|
84
|
+
// Pill row -> m-pill-row
|
|
85
|
+
const pillRowRes = categorizeUiStructure('div>(v-chip+v-chip)');
|
|
86
|
+
assert.equal(pillRowRes.suggestedCapsule, 'm-pill-row');
|
|
87
|
+
|
|
88
|
+
// Form field -> m-form-field
|
|
89
|
+
const formRes = categorizeUiStructure('div>(label+input)');
|
|
90
|
+
assert.equal(formRes.suggestedCapsule, 'm-form-field');
|
|
91
|
+
|
|
92
|
+
// Fallback -> m-feature-card
|
|
93
|
+
const fallbackRes = categorizeUiStructure('div>(div+div)');
|
|
94
|
+
assert.equal(fallbackRes.suggestedCapsule, 'm-feature-card');
|
|
95
|
+
});
|
|
96
|
+
|
|
@@ -36,31 +36,22 @@ export const groupViolationsBySeverity = (violations) => {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
export const getSeverityBadge = (severity) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
case 'LOW':
|
|
47
|
-
return `${YELLOW}[LOW]${RESET}`;
|
|
48
|
-
default:
|
|
49
|
-
return `[${severity}]`;
|
|
50
|
-
}
|
|
39
|
+
const SEVERITY_BADGES = {
|
|
40
|
+
CRITICAL: `${RED}[CRITICAL]${RESET}`,
|
|
41
|
+
HIGH: `${ORANGE}[HIGH]${RESET}`,
|
|
42
|
+
MEDIUM: `${ORANGE}[MEDIUM]${RESET}`,
|
|
43
|
+
LOW: `${YELLOW}[LOW]${RESET}`
|
|
44
|
+
};
|
|
45
|
+
return SEVERITY_BADGES[severity] || `[${severity}]`;
|
|
51
46
|
};
|
|
52
47
|
|
|
53
48
|
export const getStatusBadge = (status) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return `${RED}✕ FAIL${RESET}`;
|
|
61
|
-
default:
|
|
62
|
-
return status;
|
|
63
|
-
}
|
|
49
|
+
const STATUS_BADGES = {
|
|
50
|
+
PASSED: `${GREEN}✔ PASS${RESET}`,
|
|
51
|
+
WARN: `${YELLOW}⚠ WARN${RESET}`,
|
|
52
|
+
FAILED: `${RED}✕ FAIL${RESET}`
|
|
53
|
+
};
|
|
54
|
+
return STATUS_BADGES[status] || status;
|
|
64
55
|
};
|
|
65
56
|
|
|
66
57
|
export const resolveIndividualPillarGrade = (pillarData) => {
|
|
@@ -8,7 +8,7 @@ const COMPONENT_EXTENSIONS = new Set(['.vue', '.tsx', '.jsx']);
|
|
|
8
8
|
const TEMPLATE_EXTENSIONS = new Set(['.vue', '.html', '.svelte']);
|
|
9
9
|
const COMMENT_PREFIXES = ['//', '*', '/*'];
|
|
10
10
|
const CONSOLE_LOG_METHODS = ['log', 'info', 'warn'];
|
|
11
|
-
const NON_INTERACTIVE_FLAGS = ['--non-interactive', '--no-interactive', '--ci'];
|
|
11
|
+
const NON_INTERACTIVE_FLAGS = ['--non-interactive', '--no-interactive', '--ci', '--headless', '--yes', '-y'];
|
|
12
12
|
const FORMAT_FLAGS = ['--markdown', '--md', '--json'];
|
|
13
13
|
const GRADE_RANKS = { 'A+': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1, 'F': 0 };
|
|
14
14
|
|
|
@@ -135,5 +135,6 @@ export const isNonInteractiveSession = (rawArgs, env = process.env) => {
|
|
|
135
135
|
const hasOutputFlag = rawArgs.some((arg) => arg.startsWith('--output=') || arg.startsWith('-o='));
|
|
136
136
|
const hasFormatFlag = FORMAT_FLAGS.some((flag) => rawArgs.includes(flag));
|
|
137
137
|
const hasCiEnv = Boolean(env.CI || env.GIT_DIR);
|
|
138
|
-
|
|
138
|
+
const isNotTTY = Boolean(process.stdout && process.stdout.isTTY === false);
|
|
139
|
+
return [hasCliFlag, hasOutputFlag, hasFormatFlag, hasCiEnv, isNotTTY].some(Boolean);
|
|
139
140
|
};
|
|
@@ -36,6 +36,26 @@ export const RULE_REGISTRY = {
|
|
|
36
36
|
severity: 'CRITICAL',
|
|
37
37
|
directive: 'Extract hardcoded API keys and secrets into environment variables or secrets manager'
|
|
38
38
|
},
|
|
39
|
+
SECURITY_REVERSE_TABNABBING: {
|
|
40
|
+
pillar: PILLARS.PILLAR_9,
|
|
41
|
+
severity: 'MEDIUM',
|
|
42
|
+
directive: 'Add rel="noopener noreferrer" to external links with target="_blank" to prevent tab hijacking'
|
|
43
|
+
},
|
|
44
|
+
SECURITY_JAVASCRIPT_URL: {
|
|
45
|
+
pillar: PILLARS.PILLAR_9,
|
|
46
|
+
severity: 'CRITICAL',
|
|
47
|
+
directive: 'Do not use javascript: pseudo-protocol in links or action attributes; use event handlers'
|
|
48
|
+
},
|
|
49
|
+
SECURITY_DYNAMIC_CODE_EXECUTION: {
|
|
50
|
+
pillar: PILLARS.PILLAR_9,
|
|
51
|
+
severity: 'CRITICAL',
|
|
52
|
+
directive: 'Avoid eval(), new Function(), and string-based setTimeout/setInterval code execution'
|
|
53
|
+
},
|
|
54
|
+
SECURITY_SENSITIVE_LOGGING: {
|
|
55
|
+
pillar: PILLARS.PILLAR_9,
|
|
56
|
+
severity: 'HIGH',
|
|
57
|
+
directive: 'Never log sensitive credentials, tokens, or passwords to console diagnostics'
|
|
58
|
+
},
|
|
39
59
|
|
|
40
60
|
// Pillar 10: Testing Discipline
|
|
41
61
|
TEST_FAKE_GREEN: {
|
|
@@ -86,6 +106,11 @@ export const RULE_REGISTRY = {
|
|
|
86
106
|
severity: 'CRITICAL',
|
|
87
107
|
directive: 'Extract display states into computed descriptor objects or early returns'
|
|
88
108
|
},
|
|
109
|
+
CONTROL_FLOW_DISPATCH_SWITCH: {
|
|
110
|
+
pillar: PILLARS.PILLAR_2,
|
|
111
|
+
severity: 'MEDIUM',
|
|
112
|
+
directive: 'Replace repetitive dispatch switch with an O(1) keyed dictionary or method map (Directive 3.E)'
|
|
113
|
+
},
|
|
89
114
|
HOOK_SATURATION: {
|
|
90
115
|
pillar: PILLARS.PILLAR_3,
|
|
91
116
|
severity: 'HIGH',
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { auditCode } from "./rules.js";
|
|
4
|
+
|
|
5
|
+
test("Audit Rules: flags repetitive dispatch switch statement as CONTROL_FLOW_DISPATCH_SWITCH", () => {
|
|
6
|
+
const code = `
|
|
7
|
+
export const resolveColor = (type) => {
|
|
8
|
+
switch (type) {
|
|
9
|
+
case "primary":
|
|
10
|
+
return "blue";
|
|
11
|
+
case "secondary":
|
|
12
|
+
return "gray";
|
|
13
|
+
case "danger":
|
|
14
|
+
return "red";
|
|
15
|
+
case "warning":
|
|
16
|
+
return "yellow";
|
|
17
|
+
default:
|
|
18
|
+
return "white";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const violations = auditCode(code, "src/utils/colors.js", "src/utils/colors.js");
|
|
24
|
+
const dispatchViolation = violations.find((v) => v.rule === "CONTROL_FLOW_DISPATCH_SWITCH");
|
|
25
|
+
assert.ok(dispatchViolation, "Expected CONTROL_FLOW_DISPATCH_SWITCH violation");
|
|
26
|
+
assert.strictEqual(dispatchViolation.severity, "MEDIUM");
|
|
27
|
+
assert.ok(dispatchViolation.hazard.includes("Dispatch switch smell detected"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("Audit Rules: does NOT flag non-dispatch complex switch statements", () => {
|
|
31
|
+
const code = `
|
|
32
|
+
export const processOrder = (status, order) => {
|
|
33
|
+
switch (status) {
|
|
34
|
+
case "pending": {
|
|
35
|
+
validatePayment(order);
|
|
36
|
+
sendNotification(order.userId);
|
|
37
|
+
return queueForFulfillment(order);
|
|
38
|
+
}
|
|
39
|
+
case "completed": {
|
|
40
|
+
archiveOrder(order);
|
|
41
|
+
emitAnalytics("order_completed", order);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
throw new Error("Invalid status");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const violations = auditCode(code, "src/order.js", "src/order.js");
|
|
51
|
+
const dispatchViolation = violations.find((v) => v.rule === "CONTROL_FLOW_DISPATCH_SWITCH");
|
|
52
|
+
assert.strictEqual(dispatchViolation, undefined, "Complex switch should not be flagged as dispatch smell");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("Audit Rules: clean keyed map implementation produces 0 violations", () => {
|
|
56
|
+
const code = `
|
|
57
|
+
const COLOR_MAP = {
|
|
58
|
+
primary: "blue",
|
|
59
|
+
secondary: "gray",
|
|
60
|
+
danger: "red",
|
|
61
|
+
warning: "yellow"
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const resolveColor = (type) => {
|
|
65
|
+
return Object.prototype.hasOwnProperty.call(COLOR_MAP, type) ? COLOR_MAP[type] : "white";
|
|
66
|
+
};
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const violations = auditCode(code, "src/utils/colors.js", "src/utils/colors.js");
|
|
70
|
+
const dispatchViolation = violations.find((v) => v.rule === "CONTROL_FLOW_DISPATCH_SWITCH");
|
|
71
|
+
assert.strictEqual(dispatchViolation, undefined);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("Audit Security: flags external link target=_blank missing rel=noopener noreferrer in JSX", () => {
|
|
75
|
+
const code = `
|
|
76
|
+
export const ExternalLink = () => (
|
|
77
|
+
<a href="https://example.com" target="_blank">External</a>
|
|
78
|
+
);
|
|
79
|
+
`;
|
|
80
|
+
const violations = auditCode(code, "src/Link.tsx", "src/Link.tsx");
|
|
81
|
+
const violation = violations.find((v) => v.rule === "SECURITY_REVERSE_TABNABBING");
|
|
82
|
+
assert.ok(violation, "Expected SECURITY_REVERSE_TABNABBING violation in JSX");
|
|
83
|
+
assert.strictEqual(violation.severity, "MEDIUM");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("Audit Security: passes external link with rel=noopener noreferrer in JSX", () => {
|
|
87
|
+
const code = `
|
|
88
|
+
export const ExternalLink = () => (
|
|
89
|
+
<a href="https://example.com" target="_blank" rel="noopener noreferrer">External</a>
|
|
90
|
+
);
|
|
91
|
+
`;
|
|
92
|
+
const violations = auditCode(code, "src/Link.tsx", "src/Link.tsx");
|
|
93
|
+
const violation = violations.find((v) => v.rule === "SECURITY_REVERSE_TABNABBING");
|
|
94
|
+
assert.strictEqual(violation, undefined);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("Audit Security: flags target=_blank missing rel in Vue template", () => {
|
|
98
|
+
const code = `
|
|
99
|
+
<template>
|
|
100
|
+
<a href="https://example.com" target="_blank">External</a>
|
|
101
|
+
</template>
|
|
102
|
+
`;
|
|
103
|
+
const violations = auditCode(code, "src/Link.vue", "src/Link.vue");
|
|
104
|
+
const violation = violations.find((v) => v.rule === "SECURITY_REVERSE_TABNABBING");
|
|
105
|
+
assert.ok(violation, "Expected SECURITY_REVERSE_TABNABBING in Vue template");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("Audit Security: flags javascript: pseudo-protocol in JSX href", () => {
|
|
109
|
+
const code = `
|
|
110
|
+
export const BadLink = () => (
|
|
111
|
+
<a href="javascript:alert(1)">Click Me</a>
|
|
112
|
+
);
|
|
113
|
+
`;
|
|
114
|
+
const violations = auditCode(code, "src/BadLink.tsx", "src/BadLink.tsx");
|
|
115
|
+
const violation = violations.find((v) => v.rule === "SECURITY_JAVASCRIPT_URL");
|
|
116
|
+
assert.ok(violation, "Expected SECURITY_JAVASCRIPT_URL violation");
|
|
117
|
+
assert.strictEqual(violation.severity, "CRITICAL");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("Audit Security: flags javascript: in template attributes", () => {
|
|
121
|
+
const code = `
|
|
122
|
+
<template>
|
|
123
|
+
<form action="javascript:void(0)">
|
|
124
|
+
<button type="submit">Submit</button>
|
|
125
|
+
</form>
|
|
126
|
+
</template>
|
|
127
|
+
`;
|
|
128
|
+
const violations = auditCode(code, "src/Form.vue", "src/Form.vue");
|
|
129
|
+
const violation = violations.find((v) => v.rule === "SECURITY_JAVASCRIPT_URL");
|
|
130
|
+
assert.ok(violation, "Expected SECURITY_JAVASCRIPT_URL in template");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("Audit Security: flags dynamic code execution via eval()", () => {
|
|
134
|
+
const code = `
|
|
135
|
+
export const runDynamic = (str: string) => {
|
|
136
|
+
return eval(str);
|
|
137
|
+
};
|
|
138
|
+
`;
|
|
139
|
+
const violations = auditCode(code, "src/eval.ts", "src/eval.ts");
|
|
140
|
+
const violation = violations.find((v) => v.rule === "SECURITY_DYNAMIC_CODE_EXECUTION");
|
|
141
|
+
assert.ok(violation, "Expected SECURITY_DYNAMIC_CODE_EXECUTION for eval()");
|
|
142
|
+
assert.strictEqual(violation.severity, "CRITICAL");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("Audit Security: flags dynamic code execution via new Function()", () => {
|
|
146
|
+
const code = `
|
|
147
|
+
export const buildFn = (code: string) => {
|
|
148
|
+
return new Function("return " + code);
|
|
149
|
+
};
|
|
150
|
+
`;
|
|
151
|
+
const violations = auditCode(code, "src/fn.ts", "src/fn.ts");
|
|
152
|
+
const violation = violations.find((v) => v.rule === "SECURITY_DYNAMIC_CODE_EXECUTION");
|
|
153
|
+
assert.ok(violation, "Expected SECURITY_DYNAMIC_CODE_EXECUTION for new Function()");
|
|
154
|
+
assert.strictEqual(violation.severity, "CRITICAL");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("Audit Security: flags string-based execution in setTimeout", () => {
|
|
158
|
+
const code = `
|
|
159
|
+
export const scheduleCode = () => {
|
|
160
|
+
setTimeout("alert('hello')", 1000);
|
|
161
|
+
};
|
|
162
|
+
`;
|
|
163
|
+
const violations = auditCode(code, "src/timer.ts", "src/timer.ts");
|
|
164
|
+
const violation = violations.find((v) => v.rule === "SECURITY_DYNAMIC_CODE_EXECUTION");
|
|
165
|
+
assert.ok(violation, "Expected SECURITY_DYNAMIC_CODE_EXECUTION for string setTimeout");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("Audit Security: does NOT flag function callback setTimeout", () => {
|
|
169
|
+
const code = `
|
|
170
|
+
export const scheduleCode = () => {
|
|
171
|
+
const timer = setTimeout(() => {
|
|
172
|
+
// legitimate callback
|
|
173
|
+
}, 1000);
|
|
174
|
+
return () => clearTimeout(timer);
|
|
175
|
+
};
|
|
176
|
+
`;
|
|
177
|
+
const violations = auditCode(code, "src/timer.ts", "src/timer.ts");
|
|
178
|
+
const violation = violations.find((v) => v.rule === "SECURITY_DYNAMIC_CODE_EXECUTION");
|
|
179
|
+
assert.strictEqual(violation, undefined);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("Audit Security: flags sensitive variable logging in console", () => {
|
|
183
|
+
const code = `
|
|
184
|
+
export const debugAuth = (authToken: string, user: any) => {
|
|
185
|
+
console.log("Authenticated with token:", authToken);
|
|
186
|
+
console.info("User details:", { password: user.password });
|
|
187
|
+
};
|
|
188
|
+
`;
|
|
189
|
+
const violations = auditCode(code, "src/auth.ts", "src/auth.ts");
|
|
190
|
+
const loggingViolations = violations.filter((v) => v.rule === "SECURITY_SENSITIVE_LOGGING");
|
|
191
|
+
assert.strictEqual(loggingViolations.length >= 1, true, "Expected SECURITY_SENSITIVE_LOGGING violation");
|
|
192
|
+
assert.strictEqual(loggingViolations[0].severity, "HIGH");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("Audit Security: does NOT flag normal non-sensitive console logging", () => {
|
|
196
|
+
const code = `
|
|
197
|
+
export const debugCount = (itemCount: number) => {
|
|
198
|
+
console.log("Items rendered:", itemCount);
|
|
199
|
+
};
|
|
200
|
+
`;
|
|
201
|
+
const violations = auditCode(code, "src/items.ts", "src/items.ts");
|
|
202
|
+
const violation = violations.find((v) => v.rule === "SECURITY_SENSITIVE_LOGGING");
|
|
203
|
+
assert.strictEqual(violation, undefined);
|
|
204
|
+
});
|
|
205
|
+
|
|
@@ -102,3 +102,14 @@ test('runAudit: fast mode returns valid health metrics with zero AST traversal',
|
|
|
102
102
|
assert.ok(typeof report.health.score === 'number');
|
|
103
103
|
assert.ok(typeof report.health.grade === 'string');
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
test('runAuditPreflight: headless flag resolves immediately without interactive prompts', async () => {
|
|
107
|
+
const result = await runAuditPreflight(['--headless'], {
|
|
108
|
+
defaultDir: 'cli',
|
|
109
|
+
cwd: process.cwd()
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(result.targetDir, 'cli');
|
|
113
|
+
assert.strictEqual(result.fast, false);
|
|
114
|
+
assert.strictEqual(result.fileList, null);
|
|
115
|
+
});
|
package/cli/build.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { BuildAuditReport, BuildOptions } from './build/types.js';
|
|
|
2
2
|
|
|
3
3
|
export function runBuildAudit(
|
|
4
4
|
rawArgs?: readonly string[],
|
|
5
|
-
isCli?: boolean
|
|
5
|
+
isCli?: boolean,
|
|
6
|
+
options?: { readonly print?: boolean; readonly silent?: boolean }
|
|
6
7
|
): Promise<BuildAuditReport>;
|
|
7
8
|
|
|
8
9
|
export {
|
package/cli/build.js
CHANGED
|
@@ -20,16 +20,17 @@ const parseCommandFromArgs = (args) => {
|
|
|
20
20
|
return candidate ? candidate.trim() : null;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export const runBuildAudit = async (rawArgs = [], isCli = false) => {
|
|
23
|
+
export const runBuildAudit = async (rawArgs = [], isCli = false, options = {}) => {
|
|
24
24
|
const isJson = rawArgs.includes('--json');
|
|
25
|
-
const isSilent = rawArgs.includes('--silent');
|
|
25
|
+
const isSilent = rawArgs.includes('--silent') || options.silent === true;
|
|
26
26
|
const isRaw = rawArgs.includes('--raw');
|
|
27
27
|
const isSummary = rawArgs.includes('--summary');
|
|
28
|
+
const shouldPrint = options.print !== false;
|
|
28
29
|
|
|
29
30
|
const customCommand = parseCommandFromArgs(rawArgs);
|
|
30
31
|
const command = detectProjectBuildCommand(customCommand, process.cwd());
|
|
31
32
|
|
|
32
|
-
const shouldPrintStart = !isJson && !isSilent && !isRaw;
|
|
33
|
+
const shouldPrintStart = shouldPrint && !isJson && !isSilent && !isRaw;
|
|
33
34
|
if (shouldPrintStart) {
|
|
34
35
|
process.stdout.write(`${ANSI.CYAN}Auditing build:${ANSI.RESET} ${ANSI.DIM}${command}${ANSI.RESET}\n`);
|
|
35
36
|
}
|
|
@@ -39,13 +40,15 @@ export const runBuildAudit = async (rawArgs = [], isCli = false) => {
|
|
|
39
40
|
const report = groupBuildDiagnostics(rawDiagnostics, executionResult);
|
|
40
41
|
|
|
41
42
|
if (isJson) {
|
|
42
|
-
|
|
43
|
+
if (shouldPrint) {
|
|
44
|
+
process.stdout.write(formatJsonBuildReport(report) + '\n');
|
|
45
|
+
}
|
|
43
46
|
if (isCli) process.exit(report.exitCode);
|
|
44
47
|
return report;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
const terminalOutput = formatTerminalBuildReport(report, { silent: isSilent, summary: isSummary });
|
|
48
|
-
if (terminalOutput) {
|
|
51
|
+
if (terminalOutput && shouldPrint) {
|
|
49
52
|
process.stdout.write(terminalOutput);
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ColumnarData<T = any> {
|
|
2
|
+
readonly cols: readonly string[];
|
|
3
|
+
readonly rows: readonly T[][];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export declare function toColumnar<T extends Record<string, any>>(
|
|
7
|
+
items?: readonly T[],
|
|
8
|
+
cols?: readonly string[],
|
|
9
|
+
accessors?: Record<string, (item: T) => any>
|
|
10
|
+
): ColumnarData;
|
|
11
|
+
|
|
12
|
+
export declare function fromColumnar<T extends Record<string, any>>(
|
|
13
|
+
columnar?: ColumnarData
|
|
14
|
+
): readonly T[];
|
package/cli/columnar.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chemical X Protocol: Columnar Serialization Utility
|
|
3
|
+
* Converts object collections into compact single-schema matrices
|
|
4
|
+
* to eliminate repeated dictionary keys and slash token consumption.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Transforms an array of objects into a single-schema columnar structure.
|
|
9
|
+
*
|
|
10
|
+
* @param {readonly object[]} items Array of record objects.
|
|
11
|
+
* @param {readonly string[]} cols Column names to extract.
|
|
12
|
+
* @param {Record<string, (item: any) => any>} [accessors={}] Optional field transformers.
|
|
13
|
+
* @returns {{ cols: readonly string[], rows: readonly any[][] }}
|
|
14
|
+
*/
|
|
15
|
+
export const toColumnar = (items = [], cols = [], accessors = {}) => {
|
|
16
|
+
if (!items || items.length === 0) {
|
|
17
|
+
return { cols, rows: [] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rows = items.map((item) => {
|
|
21
|
+
return cols.map((col) => {
|
|
22
|
+
const accessor = accessors[col];
|
|
23
|
+
if (typeof accessor === 'function') {
|
|
24
|
+
return accessor(item);
|
|
25
|
+
}
|
|
26
|
+
return item[col];
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return { cols, rows };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reconstructs an array of objects from a columnar structure.
|
|
35
|
+
*
|
|
36
|
+
* @param {{ cols: readonly string[], rows: readonly any[][] }} columnar
|
|
37
|
+
* @returns {readonly Record<string, any>[]}
|
|
38
|
+
*/
|
|
39
|
+
export const fromColumnar = (columnar) => {
|
|
40
|
+
if (!columnar || !columnar.cols || !columnar.rows) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { cols, rows } = columnar;
|
|
45
|
+
return rows.map((row) => {
|
|
46
|
+
const obj = {};
|
|
47
|
+
cols.forEach((col, idx) => {
|
|
48
|
+
obj[col] = row[idx];
|
|
49
|
+
});
|
|
50
|
+
return obj;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { toColumnar, fromColumnar } from './columnar.js';
|
|
4
|
+
|
|
5
|
+
test('toColumnar: converts array of objects into cols and rows', () => {
|
|
6
|
+
const items = [
|
|
7
|
+
{ file: 'src/a.ts', tier: 'atom', lines: 40 },
|
|
8
|
+
{ file: 'src/b.ts', tier: 'molecule', lines: 75 }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const result = toColumnar(items, ['file', 'tier', 'lines']);
|
|
12
|
+
|
|
13
|
+
assert.deepStrictEqual(result.cols, ['file', 'tier', 'lines']);
|
|
14
|
+
assert.strictEqual(result.rows.length, 2);
|
|
15
|
+
assert.deepStrictEqual(result.rows[0], ['src/a.ts', 'atom', 40]);
|
|
16
|
+
assert.deepStrictEqual(result.rows[1], ['src/b.ts', 'molecule', 75]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('toColumnar: handles empty or missing arrays cleanly', () => {
|
|
20
|
+
assert.deepStrictEqual(toColumnar([], ['file', 'tier']), { cols: ['file', 'tier'], rows: [] });
|
|
21
|
+
assert.deepStrictEqual(toColumnar(null, ['file']), { cols: ['file'], rows: [] });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('toColumnar: supports custom accessor functions for nested fields', () => {
|
|
25
|
+
const items = [
|
|
26
|
+
{ file: 'src/card.vue', symbols: [{ name: 'UserCard' }, { name: 'CardBadge' }] }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const result = toColumnar(items, ['file', 'symbols'], {
|
|
30
|
+
symbols: (item) => item.symbols.map((s) => s.name)
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.deepStrictEqual(result.rows[0], ['src/card.vue', ['UserCard', 'CardBadge']]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('fromColumnar: reconstructs array of objects from columnar data', () => {
|
|
37
|
+
const columnar = {
|
|
38
|
+
cols: ['file', 'tier', 'lines'],
|
|
39
|
+
rows: [
|
|
40
|
+
['src/a.ts', 'atom', 40],
|
|
41
|
+
['src/b.ts', 'molecule', 75]
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const items = fromColumnar(columnar);
|
|
46
|
+
assert.strictEqual(items.length, 2);
|
|
47
|
+
assert.deepStrictEqual(items[0], { file: 'src/a.ts', tier: 'atom', lines: 40 });
|
|
48
|
+
assert.deepStrictEqual(items[1], { file: 'src/b.ts', tier: 'molecule', lines: 75 });
|
|
49
|
+
});
|