@chemx/starter-kit 26.9.11-481 → 26.9.12-107
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 +262 -0
- package/cli/audit/extended-visitors.js +359 -0
- package/cli/audit/history.js +19 -2
- package/cli/audit/metrics.js +69 -0
- package/cli/audit/prompts.js +175 -48
- package/cli/audit/reporter-banner.d.ts +3 -0
- package/cli/audit/reporter-banner.js +74 -0
- package/cli/audit/reporter-grades.js +11 -26
- package/cli/audit/reporter-grouping-markdown.js +83 -0
- package/cli/audit/reporter-grouping.d.ts +42 -0
- package/cli/audit/reporter-grouping.js +243 -0
- package/cli/audit/reporter-markdown.js +34 -22
- package/cli/audit/reporter-sections.d.ts +10 -0
- package/cli/audit/reporter-sections.js +259 -0
- package/cli/audit/reporter-summary.d.ts +4 -0
- package/cli/audit/reporter-summary.js +142 -0
- package/cli/audit/reporter-utils.js +13 -1
- package/cli/audit/reporter.d.ts +16 -0
- package/cli/audit/reporter.js +47 -420
- package/cli/audit/rules-helpers.js +16 -0
- package/cli/audit/rules-registry.js +88 -1
- package/cli/audit/rules.js +32 -1
- package/cli/audit/social-git.js +133 -131
- package/cli/audit/social.js +15 -2
- package/cli/audit/types.d.ts +28 -27
- package/cli/audit.js +56 -3
- package/cli/badge.js +262 -0
- package/cli/generator-templates.js +264 -0
- package/cli/generator.d.ts +10 -0
- package/cli/generator.js +151 -0
- package/cli/help.js +17 -7
- package/cli/index.js +26 -16
- package/cli/installer.d.ts +2 -0
- package/cli/installer.js +52 -38
- package/cli/license.js +17 -4
- package/cli/navigator-actions.js +63 -1
- package/cli/navigator-banner.js +17 -6
- package/cli/navigator-grades.js +9 -0
- package/cli/navigator-menu.js +36 -0
- package/cli/navigator-share.js +7 -4
- package/cli/navigator.d.ts +5 -1
- package/cli/navigator.js +49 -12
- package/cli/scaffold.js +3 -54
- package/cli/terminal.js +4 -1
- package/docs/CHANGELOG.md +50 -0
- package/package.json +1 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { RULE_REGISTRY } from './rules-registry.js';
|
|
3
|
+
|
|
4
|
+
const RESIDUE_PATTERNS = [
|
|
5
|
+
['hope this', 'helps'].join(' '),
|
|
6
|
+
['feel free', 'to tweak'].join(' '),
|
|
7
|
+
['let me know', 'if you need'].join(' '),
|
|
8
|
+
['as', 'requested'].join(' '),
|
|
9
|
+
['as an ai', 'language model'].join(' ')
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const PLACEHOLDER_PATTERNS = [
|
|
13
|
+
['replace with', 'your own'].join(' '),
|
|
14
|
+
['replace this with', 'actual'].join(' '),
|
|
15
|
+
['insert your', 'logic here'].join(' ')
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const CONVERSATIONAL_PATTERNS = [
|
|
19
|
+
{
|
|
20
|
+
regex: /\b(?:here(?:'s| is) the (?:complete|updated|refactored|full) (?:code|implementation|file|component|version))\b/i,
|
|
21
|
+
rule: 'AI_SLOP_CONVERSATIONAL_ARTIFACT',
|
|
22
|
+
hazard: 'LLM conversational preamble detected in source'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
regex: new RegExp(`\\b(?:${RESIDUE_PATTERNS.join('|')})\\b`, 'i'),
|
|
26
|
+
rule: 'AI_SLOP_CONVERSATIONAL_ARTIFACT',
|
|
27
|
+
hazard: 'LLM assistant conversational residue detected in source'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
regex: new RegExp(`\\b(?:${PLACEHOLDER_PATTERNS.join('|')})\\b`, 'i'),
|
|
31
|
+
rule: 'AI_SLOP_CONVERSATIONAL_ARTIFACT',
|
|
32
|
+
hazard: 'Generic AI boilerplate instruction placeholder detected'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
regex: /\/\/\s*\.\.\.\s*(?:existing|rest of|remaining)\s+(?:code|implementation|logic|imports)/i,
|
|
36
|
+
rule: 'AI_SLOP_LAZY_PLACEHOLDER',
|
|
37
|
+
hazard: 'AI truncation placeholder detected (lazy incomplete implementation)'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
regex: /```(?:typescript|javascript|tsx|jsx|vue|js|ts|html|css)?\s*$/m,
|
|
41
|
+
rule: 'AI_SLOP_CONVERSATIONAL_ARTIFACT',
|
|
42
|
+
hazard: 'Leaked markdown code block fence detected in source'
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const REINVENTED_UTILS = new Set([
|
|
47
|
+
'clamp',
|
|
48
|
+
'slugify',
|
|
49
|
+
'debounce',
|
|
50
|
+
'throttle',
|
|
51
|
+
'deepclone',
|
|
52
|
+
'deepmerge',
|
|
53
|
+
'capitalize'
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const STOP_WORDS = new Set([
|
|
57
|
+
'the', 'a', 'an', 'to', 'if', 'and', 'of', 'for', 'in', 'is', 'it',
|
|
58
|
+
'this', 'that', 'with', 'from', 'as', 'on', 'function', 'const', 'let', 'var'
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const normalizeWords = (text) => {
|
|
62
|
+
return text
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/[^a-z0-9]/g, ' ')
|
|
65
|
+
.split(/\s+/)
|
|
66
|
+
.filter((w) => w.length > 1 && !STOP_WORDS.has(w));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const checkSlopTextPatterns = (content, lines, relativePath, violations) => {
|
|
70
|
+
lines.forEach((lineText, idx) => {
|
|
71
|
+
for (const pat of CONVERSATIONAL_PATTERNS) {
|
|
72
|
+
if (pat.regex.test(lineText)) {
|
|
73
|
+
const meta = RULE_REGISTRY[pat.rule];
|
|
74
|
+
violations.push({
|
|
75
|
+
filePath: relativePath,
|
|
76
|
+
line: idx + 1,
|
|
77
|
+
column: 1,
|
|
78
|
+
hazard: pat.hazard,
|
|
79
|
+
rule: pat.rule,
|
|
80
|
+
severity: meta.severity,
|
|
81
|
+
pillar: meta.pillar,
|
|
82
|
+
directive: meta.directive,
|
|
83
|
+
isAiSlop: true
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const trimmed = lineText.trim();
|
|
90
|
+
if (trimmed.startsWith('//') && !trimmed.startsWith('///')) {
|
|
91
|
+
const commentBody = trimmed.replace(/^\/\/\s*/, '');
|
|
92
|
+
const commentWords = normalizeWords(commentBody);
|
|
93
|
+
|
|
94
|
+
if (commentWords.length >= 2 && commentWords.length <= 6) {
|
|
95
|
+
let nextIdx = idx + 1;
|
|
96
|
+
while (nextIdx < lines.length) {
|
|
97
|
+
const nextTrim = lines[nextIdx].trim();
|
|
98
|
+
if (nextTrim && !nextTrim.startsWith('//') && !nextTrim.startsWith('/*')) {
|
|
99
|
+
const nextWords = new Set(normalizeWords(nextTrim));
|
|
100
|
+
let matchCount = 0;
|
|
101
|
+
for (const cw of commentWords) {
|
|
102
|
+
if (nextWords.has(cw)) matchCount += 1;
|
|
103
|
+
}
|
|
104
|
+
const matchRatio = matchCount / commentWords.length;
|
|
105
|
+
if (matchRatio >= 0.75) {
|
|
106
|
+
const meta = RULE_REGISTRY.AI_SLOP_ECHO_COMMENT;
|
|
107
|
+
violations.push({
|
|
108
|
+
filePath: relativePath,
|
|
109
|
+
line: idx + 1,
|
|
110
|
+
column: 1,
|
|
111
|
+
hazard: `Trivial echo comment detected: "${commentBody.slice(0, 40)}" repeats adjacent code`,
|
|
112
|
+
rule: 'AI_SLOP_ECHO_COMMENT',
|
|
113
|
+
severity: meta.severity,
|
|
114
|
+
pillar: meta.pillar,
|
|
115
|
+
directive: meta.directive,
|
|
116
|
+
isAiSlop: true
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
nextIdx += 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
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
|
+
|
|
137
|
+
return {
|
|
138
|
+
CatchClause(astPath) {
|
|
139
|
+
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
|
+
);
|
|
148
|
+
|
|
149
|
+
if (isShallowCatch) {
|
|
150
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
151
|
+
const meta = RULE_REGISTRY.AI_SLOP_SHALLOW_CATCH;
|
|
152
|
+
violations.push({
|
|
153
|
+
filePath: relativePath,
|
|
154
|
+
line,
|
|
155
|
+
column: astPath.node.loc?.start.column || 1,
|
|
156
|
+
hazard: 'Shallow catch paranoia wrapper (silent suppression without handling)',
|
|
157
|
+
rule: 'AI_SLOP_SHALLOW_CATCH',
|
|
158
|
+
severity: meta.severity,
|
|
159
|
+
pillar: meta.pillar,
|
|
160
|
+
directive: meta.directive,
|
|
161
|
+
isAiSlop: true
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
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
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
BlockStatement(astPath) {
|
|
186
|
+
const stmts = astPath.node.body;
|
|
187
|
+
if (stmts.length >= 2) {
|
|
188
|
+
for (let i = 0; i < stmts.length - 1; i++) {
|
|
189
|
+
const curr = stmts[i];
|
|
190
|
+
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
|
+
) {
|
|
199
|
+
const varName = curr.declarations[0].id.name;
|
|
200
|
+
const line = curr.loc?.start.line || 1;
|
|
201
|
+
const meta = RULE_REGISTRY.AI_SLOP_REDUNDANT_PASSTHROUGH;
|
|
202
|
+
violations.push({
|
|
203
|
+
filePath: relativePath,
|
|
204
|
+
line,
|
|
205
|
+
column: curr.loc?.start.column || 1,
|
|
206
|
+
hazard: `Redundant single-use passthrough assignment "${varName}" before return`,
|
|
207
|
+
rule: 'AI_SLOP_REDUNDANT_PASSTHROUGH',
|
|
208
|
+
severity: meta.severity,
|
|
209
|
+
pillar: meta.pillar,
|
|
210
|
+
directive: meta.directive,
|
|
211
|
+
isAiSlop: true
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
FunctionDeclaration(astPath) {
|
|
219
|
+
if (!isComponentFile) return;
|
|
220
|
+
const fnName = astPath.node.id?.name?.toLowerCase();
|
|
221
|
+
if (fnName && REINVENTED_UTILS.has(fnName)) {
|
|
222
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
223
|
+
const meta = RULE_REGISTRY.AI_SLOP_UTILITY_REINVENTION;
|
|
224
|
+
violations.push({
|
|
225
|
+
filePath: relativePath,
|
|
226
|
+
line,
|
|
227
|
+
column: astPath.node.loc?.start.column || 1,
|
|
228
|
+
hazard: `Inline utility reinvention "${astPath.node.id.name}" in component capsule`,
|
|
229
|
+
rule: 'AI_SLOP_UTILITY_REINVENTION',
|
|
230
|
+
severity: meta.severity,
|
|
231
|
+
pillar: meta.pillar,
|
|
232
|
+
directive: meta.directive,
|
|
233
|
+
isAiSlop: true
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
VariableDeclarator(astPath) {
|
|
239
|
+
if (!isComponentFile) return;
|
|
240
|
+
const idName = astPath.node.id?.name?.toLowerCase();
|
|
241
|
+
if (
|
|
242
|
+
idName &&
|
|
243
|
+
REINVENTED_UTILS.has(idName) &&
|
|
244
|
+
(t.isArrowFunctionExpression(astPath.node.init) || t.isFunctionExpression(astPath.node.init))
|
|
245
|
+
) {
|
|
246
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
247
|
+
const meta = RULE_REGISTRY.AI_SLOP_UTILITY_REINVENTION;
|
|
248
|
+
violations.push({
|
|
249
|
+
filePath: relativePath,
|
|
250
|
+
line,
|
|
251
|
+
column: astPath.node.loc?.start.column || 1,
|
|
252
|
+
hazard: `Inline utility reinvention "${astPath.node.id.name}" in component capsule`,
|
|
253
|
+
rule: 'AI_SLOP_UTILITY_REINVENTION',
|
|
254
|
+
severity: meta.severity,
|
|
255
|
+
pillar: meta.pillar,
|
|
256
|
+
directive: meta.directive,
|
|
257
|
+
isAiSlop: true
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
};
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as t from '@babel/types';
|
|
4
|
+
import { RULE_REGISTRY } from './rules-registry.js';
|
|
5
|
+
import { toResultSync } from './rules-helpers.js';
|
|
6
|
+
|
|
7
|
+
const BARE_BOOLEANS = new Set([
|
|
8
|
+
'loading', 'valid', 'active', 'visible', 'open', 'disabled',
|
|
9
|
+
'checked', 'ready', 'editing', 'submitting', 'pending', 'busy',
|
|
10
|
+
'selected', 'expanded'
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const BARE_HANDLERS = new Set([
|
|
14
|
+
'checkout', 'submit', 'save', 'delete', 'close', 'open',
|
|
15
|
+
'reset', 'confirm', 'cancel', 'refresh', 'sync', 'send', 'pay'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const SECRET_PATTERNS = [
|
|
19
|
+
/(['"])(?:sk-[a-zA-Z0-9_-]{24,}|ghp_[a-zA-Z0-9]{30,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z-_]{35})\1/
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const FAKE_GREEN_PATTERNS = [
|
|
23
|
+
/expect\s*\(\s*(true|false|1|0)\s*\)\s*\.\s*to(?:Be|Equal|StrictEqual)\s*\(\s*\1\s*\)/,
|
|
24
|
+
/assert\s*\.\s*(?:strictEqual|equal|deepEqual)\s*\(\s*(true|false|1|0)\s*,\s*\1\s*\)/,
|
|
25
|
+
/assert\s*\.\s*ok\s*\(\s*(?:true|1)\s*\)/
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const IMG_WITHOUT_ALT_PATTERN = new RegExp(['<', 'img\\b', '(?![^>]*\\balt\\s*=)', '[^>]*>'].join(''), 'i');
|
|
29
|
+
const CLICKABLE_CONTAINER_PATTERN = new RegExp(['<', '(div|span)\\b', '(?![^>]*\\brole\\s*=\\s*[\'"](?:button|link|tab)[\'"])', '[^>]*\\b(?:@click|v-on:click)\\s*='].join(''), 'i');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pillar 10: Check for co-located test files for molecule capsules.
|
|
33
|
+
*/
|
|
34
|
+
const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
|
|
35
|
+
const baseName = path.basename(filePath);
|
|
36
|
+
const ext = path.extname(filePath);
|
|
37
|
+
|
|
38
|
+
// Stage 1: Atomic Concept Declarations
|
|
39
|
+
const isMoleculePath = relativePath.includes('molecules') || baseName.startsWith('m-');
|
|
40
|
+
const isComponentExt = ext === '.vue' || ext === '.tsx' || ext === '.jsx';
|
|
41
|
+
const isTestOrSpecFile = baseName.includes('.spec.') || baseName.includes('.test.');
|
|
42
|
+
|
|
43
|
+
// Stage 2: Unified Decision Variable & Early Guard Clause
|
|
44
|
+
const isMoleculeComponent = isMoleculePath && isComponentExt && !isTestOrSpecFile;
|
|
45
|
+
if (!isMoleculeComponent) return;
|
|
46
|
+
|
|
47
|
+
const dir = path.dirname(filePath);
|
|
48
|
+
const [siblings, dirError] = toResultSync(() => (fs.existsSync(dir) ? fs.readdirSync(dir) : []));
|
|
49
|
+
|
|
50
|
+
// Stage 1: Atomic Guard Check
|
|
51
|
+
const hasDirError = Boolean(dirError);
|
|
52
|
+
const hasSiblings = Boolean(siblings);
|
|
53
|
+
const canInspectSiblings = !hasDirError && hasSiblings;
|
|
54
|
+
if (!canInspectSiblings) return;
|
|
55
|
+
|
|
56
|
+
const isTestSibling = (fileName) => /\.(test|spec)\.[jt]sx?$/.test(fileName);
|
|
57
|
+
const hasCoLocatedTest = siblings.some(isTestSibling);
|
|
58
|
+
if (hasCoLocatedTest) return;
|
|
59
|
+
|
|
60
|
+
const meta = RULE_REGISTRY.TEST_MISSING_COLOCATED;
|
|
61
|
+
violations.push({
|
|
62
|
+
filePath: relativePath,
|
|
63
|
+
line: 1,
|
|
64
|
+
column: 1,
|
|
65
|
+
hazard: 'Molecule capsule missing co-located test file (*.spec.ts or *.test.ts)',
|
|
66
|
+
rule: 'TEST_MISSING_COLOCATED',
|
|
67
|
+
severity: meta.severity,
|
|
68
|
+
pillar: meta.pillar,
|
|
69
|
+
directive: meta.directive
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
|
|
74
|
+
checkMoleculeCoLocatedTest(filePath, relativePath, violations);
|
|
75
|
+
|
|
76
|
+
const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath);
|
|
77
|
+
const ext = path.extname(filePath);
|
|
78
|
+
const isTemplateFile = ext === '.vue' || ext === '.html' || ext === '.svelte';
|
|
79
|
+
|
|
80
|
+
lines.forEach((lineText, idx) => {
|
|
81
|
+
const lineNum = idx + 1;
|
|
82
|
+
const trimmed = lineText.trim();
|
|
83
|
+
const isComment = trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*');
|
|
84
|
+
|
|
85
|
+
// Pillar 9: Hardcoded Secret Detection
|
|
86
|
+
const hasEnvReference = trimmed.includes('process.env') || trimmed.includes('import.meta.env');
|
|
87
|
+
const canScanSecrets = !isComment && !hasEnvReference;
|
|
88
|
+
|
|
89
|
+
if (canScanSecrets) {
|
|
90
|
+
for (const pat of SECRET_PATTERNS) {
|
|
91
|
+
const matchesSecretPattern = pat.test(lineText);
|
|
92
|
+
const hasPlaceholder = lineText.includes('dummy') || lineText.includes('placeholder');
|
|
93
|
+
const isHardcodedSecret = matchesSecretPattern && !hasPlaceholder;
|
|
94
|
+
|
|
95
|
+
if (isHardcodedSecret) {
|
|
96
|
+
const meta = RULE_REGISTRY.SECURITY_HARDCODED_SECRET;
|
|
97
|
+
violations.push({
|
|
98
|
+
filePath: relativePath,
|
|
99
|
+
line: lineNum,
|
|
100
|
+
column: 1,
|
|
101
|
+
hazard: 'High-entropy API key or secret token detected in source code',
|
|
102
|
+
rule: 'SECURITY_HARDCODED_SECRET',
|
|
103
|
+
severity: meta.severity,
|
|
104
|
+
pillar: meta.pillar,
|
|
105
|
+
directive: meta.directive
|
|
106
|
+
});
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Pillar 9: Unsanitized v-html injection (Vue / HTML templates)
|
|
113
|
+
const hasVHtml = isTemplateFile && /\bv-html\s*=\s*["']/.test(lineText);
|
|
114
|
+
if (hasVHtml) {
|
|
115
|
+
const hasSanitizer = /(?:sanitize|DOMPurify|escapeHtml|filterXSS)/i.test(lineText);
|
|
116
|
+
const isUnsanitizedVHtml = !hasSanitizer;
|
|
117
|
+
|
|
118
|
+
if (isUnsanitizedVHtml) {
|
|
119
|
+
const meta = RULE_REGISTRY.SECURITY_RAW_HTML_INJECTION;
|
|
120
|
+
violations.push({
|
|
121
|
+
filePath: relativePath,
|
|
122
|
+
line: lineNum,
|
|
123
|
+
column: 1,
|
|
124
|
+
hazard: 'Unsanitized HTML injection via v-html detected without DOMPurify',
|
|
125
|
+
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
126
|
+
severity: meta.severity,
|
|
127
|
+
pillar: meta.pillar,
|
|
128
|
+
directive: meta.directive
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Pillar 8: Non-semantic clickable containers in templates (@click on div/span)
|
|
134
|
+
const hasClickableContainer = isTemplateFile && CLICKABLE_CONTAINER_PATTERN.test(lineText);
|
|
135
|
+
if (hasClickableContainer) {
|
|
136
|
+
const meta = RULE_REGISTRY.A11Y_CLICKABLE_NON_SEMANTIC;
|
|
137
|
+
violations.push({
|
|
138
|
+
filePath: relativePath,
|
|
139
|
+
line: lineNum,
|
|
140
|
+
column: 1,
|
|
141
|
+
hazard: 'Clickable generic container detected without native button/link semantics',
|
|
142
|
+
rule: 'A11Y_CLICKABLE_NON_SEMANTIC',
|
|
143
|
+
severity: meta.severity,
|
|
144
|
+
pillar: meta.pillar,
|
|
145
|
+
directive: meta.directive
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Pillar 8: Missing alt attribute on img tags
|
|
150
|
+
const hasMissingAltImg = isTemplateFile && IMG_WITHOUT_ALT_PATTERN.test(lineText);
|
|
151
|
+
if (hasMissingAltImg) {
|
|
152
|
+
const meta = RULE_REGISTRY.A11Y_IMAGE_MISSING_ALT;
|
|
153
|
+
violations.push({
|
|
154
|
+
filePath: relativePath,
|
|
155
|
+
line: lineNum,
|
|
156
|
+
column: 1,
|
|
157
|
+
hazard: 'Image element missing alt text attribute',
|
|
158
|
+
rule: 'A11Y_IMAGE_MISSING_ALT',
|
|
159
|
+
severity: meta.severity,
|
|
160
|
+
pillar: meta.pillar,
|
|
161
|
+
directive: meta.directive
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Pillar 10: Fake green tests (only in spec/test files)
|
|
166
|
+
const canCheckFakeGreen = isTestFile && !isComment;
|
|
167
|
+
if (canCheckFakeGreen) {
|
|
168
|
+
for (const pat of FAKE_GREEN_PATTERNS) {
|
|
169
|
+
const isFakeGreenMatch = pat.test(lineText);
|
|
170
|
+
if (isFakeGreenMatch) {
|
|
171
|
+
const meta = RULE_REGISTRY.TEST_FAKE_GREEN;
|
|
172
|
+
violations.push({
|
|
173
|
+
filePath: relativePath,
|
|
174
|
+
line: lineNum,
|
|
175
|
+
column: 1,
|
|
176
|
+
hazard: 'Fake green test assertion detected (asserting literal against itself)',
|
|
177
|
+
rule: 'TEST_FAKE_GREEN',
|
|
178
|
+
severity: meta.severity,
|
|
179
|
+
pillar: meta.pillar,
|
|
180
|
+
directive: meta.directive
|
|
181
|
+
});
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const createExtendedVisitors = ({ relativePath, violations }) => {
|
|
190
|
+
return {
|
|
191
|
+
JSXOpeningElement(astPath) {
|
|
192
|
+
const tagName = astPath.node.name?.name;
|
|
193
|
+
|
|
194
|
+
// Pillar 8: Clickable non-semantic JSX elements (div, span)
|
|
195
|
+
const isGenericContainer = tagName === 'div' || tagName === 'span';
|
|
196
|
+
if (isGenericContainer) {
|
|
197
|
+
const isClickAttr = (attr) => t.isJSXAttribute(attr) && attr.name?.name === 'onClick';
|
|
198
|
+
const isRoleAttr = (attr) => t.isJSXAttribute(attr) && attr.name?.name === 'role';
|
|
199
|
+
const hasClick = astPath.node.attributes.some(isClickAttr);
|
|
200
|
+
const hasRole = astPath.node.attributes.some(isRoleAttr);
|
|
201
|
+
const isClickableWithoutRole = hasClick && !hasRole;
|
|
202
|
+
|
|
203
|
+
if (isClickableWithoutRole) {
|
|
204
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
205
|
+
const meta = RULE_REGISTRY.A11Y_CLICKABLE_NON_SEMANTIC;
|
|
206
|
+
violations.push({
|
|
207
|
+
filePath: relativePath,
|
|
208
|
+
line,
|
|
209
|
+
column: astPath.node.loc?.start.column || 1,
|
|
210
|
+
hazard: `Clickable non-semantic <${tagName}> detected without native button/link element`,
|
|
211
|
+
rule: 'A11Y_CLICKABLE_NON_SEMANTIC',
|
|
212
|
+
severity: meta.severity,
|
|
213
|
+
pillar: meta.pillar,
|
|
214
|
+
directive: meta.directive
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Pillar 8: Missing alt attribute on img JSX
|
|
220
|
+
const isImgElement = tagName === 'img';
|
|
221
|
+
if (isImgElement) {
|
|
222
|
+
const isAltAttr = (attr) => t.isJSXAttribute(attr) && attr.name?.name === 'alt';
|
|
223
|
+
const hasAlt = astPath.node.attributes.some(isAltAttr);
|
|
224
|
+
const isMissingAlt = !hasAlt;
|
|
225
|
+
|
|
226
|
+
if (isMissingAlt) {
|
|
227
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
228
|
+
const meta = RULE_REGISTRY.A11Y_IMAGE_MISSING_ALT;
|
|
229
|
+
violations.push({
|
|
230
|
+
filePath: relativePath,
|
|
231
|
+
line,
|
|
232
|
+
column: astPath.node.loc?.start.column || 1,
|
|
233
|
+
hazard: 'Image element missing alt text attribute in JSX',
|
|
234
|
+
rule: 'A11Y_IMAGE_MISSING_ALT',
|
|
235
|
+
severity: meta.severity,
|
|
236
|
+
pillar: meta.pillar,
|
|
237
|
+
directive: meta.directive
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
JSXAttribute(astPath) {
|
|
244
|
+
const attrName = astPath.node.name?.name;
|
|
245
|
+
|
|
246
|
+
// Pillar 9: dangerouslySetInnerHTML without sanitizer
|
|
247
|
+
const isDangerouslySetHtml = attrName === 'dangerouslySetInnerHTML';
|
|
248
|
+
if (isDangerouslySetHtml) {
|
|
249
|
+
const value = astPath.node.value;
|
|
250
|
+
let isSanitized = false;
|
|
251
|
+
|
|
252
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
253
|
+
const expr = value.expression;
|
|
254
|
+
if (t.isObjectExpression(expr)) {
|
|
255
|
+
for (const prop of expr.properties) {
|
|
256
|
+
const isHtmlProp = t.isObjectProperty(prop) && prop.key?.name === '__html';
|
|
257
|
+
if (isHtmlProp && t.isCallExpression(prop.value)) {
|
|
258
|
+
const calleeName = prop.value.callee?.name || prop.value.callee?.property?.name || '';
|
|
259
|
+
const hasSanitizeCallee = /sanitize/i.test(calleeName);
|
|
260
|
+
if (hasSanitizeCallee) isSanitized = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const isUnsanitized = !isSanitized;
|
|
267
|
+
if (isUnsanitized) {
|
|
268
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
269
|
+
const meta = RULE_REGISTRY.SECURITY_RAW_HTML_INJECTION;
|
|
270
|
+
violations.push({
|
|
271
|
+
filePath: relativePath,
|
|
272
|
+
line,
|
|
273
|
+
column: astPath.node.loc?.start.column || 1,
|
|
274
|
+
hazard: 'Unsanitized HTML injection via dangerouslySetInnerHTML without DOMPurify',
|
|
275
|
+
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
276
|
+
severity: meta.severity,
|
|
277
|
+
pillar: meta.pillar,
|
|
278
|
+
directive: meta.directive
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Pillar 11: Action handler naming (e.g. onClick={checkout} instead of onClick={handleCheckout})
|
|
284
|
+
const isActionHandler = attrName === 'onClick' || attrName === 'onSubmit';
|
|
285
|
+
if (isActionHandler) {
|
|
286
|
+
const value = astPath.node.value;
|
|
287
|
+
const isContainerExpr = t.isJSXExpressionContainer(value);
|
|
288
|
+
const isIdentifierExpr = isContainerExpr && t.isIdentifier(value.expression);
|
|
289
|
+
|
|
290
|
+
if (isIdentifierExpr) {
|
|
291
|
+
const fnName = value.expression.name;
|
|
292
|
+
const isBareHandler = BARE_HANDLERS.has(fnName.toLowerCase());
|
|
293
|
+
|
|
294
|
+
if (isBareHandler) {
|
|
295
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
296
|
+
const meta = RULE_REGISTRY.NAMING_HANDLER_PREFIX;
|
|
297
|
+
const suggested = `handle${fnName.charAt(0).toUpperCase()}${fnName.slice(1)}`;
|
|
298
|
+
violations.push({
|
|
299
|
+
filePath: relativePath,
|
|
300
|
+
line,
|
|
301
|
+
column: astPath.node.loc?.start.column || 1,
|
|
302
|
+
hazard: `Action handler "${fnName}" missing handle prefix (e.g. ${suggested})`,
|
|
303
|
+
rule: 'NAMING_HANDLER_PREFIX',
|
|
304
|
+
severity: meta.severity,
|
|
305
|
+
pillar: meta.pillar,
|
|
306
|
+
directive: meta.directive
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
VariableDeclarator(astPath) {
|
|
314
|
+
// Pillar 11: Bare boolean variables missing is/has/can/should prefix
|
|
315
|
+
const isIdentifierNode = t.isIdentifier(astPath.node.id);
|
|
316
|
+
const hasInitNode = Boolean(astPath.node.init);
|
|
317
|
+
const canInspectDeclarator = isIdentifierNode && hasInitNode;
|
|
318
|
+
|
|
319
|
+
if (canInspectDeclarator) {
|
|
320
|
+
const varName = astPath.node.id.name;
|
|
321
|
+
const isBareBoolean = BARE_BOOLEANS.has(varName.toLowerCase());
|
|
322
|
+
|
|
323
|
+
if (isBareBoolean) {
|
|
324
|
+
let isBooleanInit = false;
|
|
325
|
+
const init = astPath.node.init;
|
|
326
|
+
|
|
327
|
+
if (t.isBooleanLiteral(init)) {
|
|
328
|
+
isBooleanInit = true;
|
|
329
|
+
} else if (t.isCallExpression(init)) {
|
|
330
|
+
const callee = init.callee;
|
|
331
|
+
const calleeName = t.isIdentifier(callee) ? callee.name : '';
|
|
332
|
+
const isRefOrStateHook = calleeName === 'ref' || calleeName === 'useState';
|
|
333
|
+
|
|
334
|
+
if (isRefOrStateHook) {
|
|
335
|
+
const arg = init.arguments[0];
|
|
336
|
+
const isBoolLiteral = t.isBooleanLiteral(arg);
|
|
337
|
+
if (isBoolLiteral) isBooleanInit = true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (isBooleanInit) {
|
|
342
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
343
|
+
const meta = RULE_REGISTRY.NAMING_BARE_BOOLEAN;
|
|
344
|
+
violations.push({
|
|
345
|
+
filePath: relativePath,
|
|
346
|
+
line,
|
|
347
|
+
column: astPath.node.loc?.start.column || 1,
|
|
348
|
+
hazard: `Bare boolean variable "${varName}" missing is/has/can/should prefix`,
|
|
349
|
+
rule: 'NAMING_BARE_BOOLEAN',
|
|
350
|
+
severity: meta.severity,
|
|
351
|
+
pillar: meta.pillar,
|
|
352
|
+
directive: meta.directive
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
};
|
package/cli/audit/history.js
CHANGED
|
@@ -26,7 +26,10 @@ export const ensureChemxDir = (cwd = process.cwd()) => {
|
|
|
26
26
|
const trailingNewline = content.endsWith('\n') || content.length === 0 ? '' : '\n';
|
|
27
27
|
fs.appendFileSync(gitignorePath, `${trailingNewline}# Chemical X local telemetry & audit history\n.chemx/\n`, 'utf-8');
|
|
28
28
|
}
|
|
29
|
-
} catch {
|
|
29
|
+
} catch (err) {
|
|
30
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
return dir;
|
|
@@ -62,6 +65,17 @@ export const createSnapshotFromReport = (report) => {
|
|
|
62
65
|
grade: health.grade,
|
|
63
66
|
label: health.label
|
|
64
67
|
},
|
|
68
|
+
aiSlop: report.aiSlop ? {
|
|
69
|
+
score: report.aiSlop.score,
|
|
70
|
+
grade: report.aiSlop.grade,
|
|
71
|
+
label: report.aiSlop.label,
|
|
72
|
+
violationsCount: report.aiSlop.violationsCount || 0
|
|
73
|
+
} : {
|
|
74
|
+
score: 100,
|
|
75
|
+
grade: 'A+',
|
|
76
|
+
label: 'Pure Artisanal',
|
|
77
|
+
violationsCount: 0
|
|
78
|
+
},
|
|
65
79
|
metrics: {
|
|
66
80
|
scannedFiles: metrics.scannedFiles,
|
|
67
81
|
totalLoc: metrics.totalLoc,
|
|
@@ -117,7 +131,10 @@ export const getAuditBaseline = (cwd = process.cwd()) => {
|
|
|
117
131
|
try {
|
|
118
132
|
const raw = fs.readFileSync(baselinePath, 'utf-8');
|
|
119
133
|
return JSON.parse(raw);
|
|
120
|
-
} catch {
|
|
134
|
+
} catch (err) {
|
|
135
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
121
138
|
}
|
|
122
139
|
const history = getAuditHistory(cwd);
|
|
123
140
|
return history.length > 0 ? history[0] : null;
|