@chemx/starter-kit 26.9.16-41 → 26.9.17-1100
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 +181 -2
- 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 +243 -0
- package/cli/audit/pattern-detector.js +90 -3
- package/cli/audit/pattern-detector.spec.js +36 -1
- package/cli/audit/prompts.js +10 -12
- package/cli/audit/reporter-utils.js +13 -22
- package/cli/audit/roadmap.js +5 -20
- package/cli/audit/rules-predicates.js +3 -2
- package/cli/audit/rules-registry.js +25 -0
- package/cli/audit/rules.js +4 -0
- package/cli/audit/rules.spec.js +205 -0
- package/cli/audit-preflight-git.js +54 -0
- package/cli/audit-preflight-scope.js +87 -0
- package/cli/audit-preflight.d.ts +16 -0
- package/cli/audit-preflight.js +85 -0
- package/cli/audit-preflight.spec.js +115 -0
- package/cli/audit.js +48 -22
- package/cli/build/detector.js +40 -23
- package/cli/build/detector.spec.js +81 -0
- package/cli/build.d.ts +2 -1
- package/cli/build.js +16 -12
- 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 +38 -4
- package/cli/index.js +112 -14
- package/cli/installer-templates.js +67 -13
- package/cli/installer.d.ts +2 -0
- package/cli/installer.js +48 -21
- package/cli/installer.spec.js +72 -0
- 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 +24 -1
- package/cli/mcp/manifests.js +332 -0
- package/cli/mcp/prompts.js +28 -0
- package/cli/mcp/resources.js +175 -64
- package/cli/mcp/server.js +51 -5
- package/cli/mcp/server.spec.js +247 -1
- package/cli/mcp/tools.js +386 -194
- package/cli/mcp/types.d.ts +37 -0
- package/cli/navigator-actions.js +23 -10
- package/cli/navigator-banner-helpers.js +33 -0
- package/cli/navigator-banner.js +19 -38
- package/cli/navigator-banner.spec.js +68 -0
- 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 +5 -0
- package/cli/search.d.ts +16 -0
- package/cli/search.js +103 -11
- package/cli/silence-warnings.js +17 -0
- package/cli/team/index.js +30 -0
- package/cli/team/team-adversarial-locks.spec.js +105 -0
- package/cli/team/team-adversarial-telemetry.spec.js +93 -0
- package/cli/team/team-commands.js +199 -0
- package/cli/team/team-db-agents.js +79 -0
- package/cli/team/team-db-feed.js +84 -0
- package/cli/team/team-db-lock-promotion.js +91 -0
- package/cli/team/team-db-locks.js +94 -0
- package/cli/team/team-db-task-helpers.js +94 -0
- package/cli/team/team-db-tasks.js +90 -0
- package/cli/team/team-db.js +93 -0
- package/cli/team/team-format.js +80 -0
- package/cli/team/team-lock-concurrency.spec.js +182 -0
- package/cli/team/team-schema.js +74 -0
- package/cli/team/team-telemetry.js +104 -0
- package/cli/team/team-triage.js +88 -0
- package/cli/team/team.spec.js +344 -0
- package/cli/terminal.js +36 -1
- package/cli/ui-actions-helpers.js +66 -0
- package/cli/ui-actions.js +85 -0
- package/cli/ui-attention.js +74 -0
- package/cli/ui-client-api.js +49 -0
- package/cli/ui-client-cert.js +27 -0
- package/cli/ui-client-script.js +83 -0
- package/cli/ui-db-studio.js +79 -0
- package/cli/ui-handlers.js +95 -0
- package/cli/ui-html.js +35 -0
- package/cli/ui-server-routes.js +63 -0
- package/cli/ui-server.js +75 -0
- package/cli/ui-styles.js +34 -0
- package/cli/ui-template-views.js +63 -0
- package/cli/ui-template.js +91 -0
- package/cli/ui.spec.js +305 -0
- package/cli/verify.js +367 -0
- package/cli/verify.spec.js +98 -0
- package/docs/CHANGELOG.md +10 -0
- package/package.json +13 -15
- 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
|
@@ -33,6 +33,59 @@ const FAKE_GREEN_PATTERNS = [
|
|
|
33
33
|
const IMG_WITHOUT_ALT_PATTERN = new RegExp(['<', 'img\\b', '(?![^>]*\\balt\\s*=)', '[^>]*>'].join(''), 'i');
|
|
34
34
|
const CLICKABLE_CONTAINER_PATTERN = new RegExp(['<', '(div|span)\\b', '(?![^>]*\\brole\\s*=\\s*[\'"](?:button|link|tab)[\'"])', '[^>]*\\b(?:@click|v-on:click)\\s*='].join(''), 'i');
|
|
35
35
|
|
|
36
|
+
const SENSITIVE_KEYWORDS = new Set([
|
|
37
|
+
'password', 'passwd', 'secret', 'token', 'apikey', 'api_key',
|
|
38
|
+
'authtoken', 'auth_token', 'accesstoken', 'access_token',
|
|
39
|
+
'refreshtoken', 'refresh_token', 'privatekey', 'private_key',
|
|
40
|
+
'creditcard', 'credit_card', 'ssn'
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const isSensitiveName = (name = '') => {
|
|
44
|
+
const normalized = String(name).toLowerCase();
|
|
45
|
+
if (SENSITIVE_KEYWORDS.has(normalized)) return true;
|
|
46
|
+
return /^(pass(word)?|token|secret|api[_-]?key)$/i.test(normalized);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const JAVASCRIPT_URL_REGEX = /^\s*javascript:/i;
|
|
50
|
+
const TABNABBING_TEMPLATE_PATTERN = /<a\b[^>]*\btarget\s*=\s*["']_blank["'][^>]*>/i;
|
|
51
|
+
const JAVASCRIPT_URL_TEMPLATE_PATTERN = /\b(?:href|src|action|formaction)\s*=\s*["']\s*javascript:/i;
|
|
52
|
+
|
|
53
|
+
const resolvePropertyKeyName = (key) => {
|
|
54
|
+
const isIdentifier = t.isIdentifier(key);
|
|
55
|
+
if (isIdentifier) return key.name;
|
|
56
|
+
const isStringLiteral = t.isStringLiteral(key);
|
|
57
|
+
if (isStringLiteral) return key.value;
|
|
58
|
+
return '';
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const isSensitiveExpression = (node) => {
|
|
62
|
+
if (!node) return false;
|
|
63
|
+
if (t.isIdentifier(node)) {
|
|
64
|
+
return isSensitiveName(node.name);
|
|
65
|
+
}
|
|
66
|
+
if (t.isMemberExpression(node)) {
|
|
67
|
+
if (t.isIdentifier(node.property)) {
|
|
68
|
+
return isSensitiveName(node.property.name);
|
|
69
|
+
}
|
|
70
|
+
if (t.isStringLiteral(node.property)) {
|
|
71
|
+
return isSensitiveName(node.property.value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (t.isObjectExpression(node)) {
|
|
75
|
+
return node.properties.some((prop) => {
|
|
76
|
+
if (t.isObjectProperty(prop)) {
|
|
77
|
+
const keyName = resolvePropertyKeyName(prop.key);
|
|
78
|
+
return isSensitiveName(keyName);
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (t.isTemplateLiteral(node)) {
|
|
84
|
+
return node.expressions.some(isSensitiveExpression);
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
};
|
|
88
|
+
|
|
36
89
|
/**
|
|
37
90
|
* Pillar 10: Check for co-located test files for molecule capsules.
|
|
38
91
|
*/
|
|
@@ -136,6 +189,41 @@ export const checkExtendedTextPatterns = (content, lines, relativePath, filePath
|
|
|
136
189
|
}
|
|
137
190
|
}
|
|
138
191
|
|
|
192
|
+
// Pillar 9: Reverse Tabnabbing in HTML / Vue templates
|
|
193
|
+
const hasTargetBlank = isTemplateFile && TABNABBING_TEMPLATE_PATTERN.test(lineText);
|
|
194
|
+
if (hasTargetBlank) {
|
|
195
|
+
const hasRelProtection = /\brel\s*=\s*["'][^"']*(?:noopener|noreferrer)[^"']*["']/i.test(lineText);
|
|
196
|
+
if (!hasRelProtection) {
|
|
197
|
+
const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
|
|
198
|
+
violations.push({
|
|
199
|
+
filePath: relativePath,
|
|
200
|
+
line: lineNum,
|
|
201
|
+
column: 1,
|
|
202
|
+
hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
|
|
203
|
+
rule: 'SECURITY_REVERSE_TABNABBING',
|
|
204
|
+
severity: meta.severity,
|
|
205
|
+
pillar: meta.pillar,
|
|
206
|
+
directive: meta.directive
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Pillar 9: javascript: pseudo-protocol in HTML / Vue templates
|
|
212
|
+
const hasJsUrl = isTemplateFile && JAVASCRIPT_URL_TEMPLATE_PATTERN.test(lineText);
|
|
213
|
+
if (hasJsUrl) {
|
|
214
|
+
const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
|
|
215
|
+
violations.push({
|
|
216
|
+
filePath: relativePath,
|
|
217
|
+
line: lineNum,
|
|
218
|
+
column: 1,
|
|
219
|
+
hazard: 'javascript: pseudo-protocol detected in template attribute',
|
|
220
|
+
rule: 'SECURITY_JAVASCRIPT_URL',
|
|
221
|
+
severity: meta.severity,
|
|
222
|
+
pillar: meta.pillar,
|
|
223
|
+
directive: meta.directive
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
139
227
|
// Pillar 8: Non-semantic clickable containers in templates (@click on div/span)
|
|
140
228
|
const hasClickableContainer = isTemplateFile && CLICKABLE_CONTAINER_PATTERN.test(lineText);
|
|
141
229
|
if (hasClickableContainer) {
|
|
@@ -244,6 +332,49 @@ export const createExtendedVisitors = ({ relativePath, violations }) => {
|
|
|
244
332
|
});
|
|
245
333
|
}
|
|
246
334
|
}
|
|
335
|
+
|
|
336
|
+
// Pillar 9: External link target="_blank" without rel="noopener noreferrer"
|
|
337
|
+
const isAnchorElement = tagName === 'a';
|
|
338
|
+
if (isAnchorElement) {
|
|
339
|
+
const targetAttr = astPath.node.attributes.find((attr) => t.isJSXAttribute(attr) && attr.name?.name === 'target');
|
|
340
|
+
let isTargetBlank = false;
|
|
341
|
+
if (targetAttr) {
|
|
342
|
+
if (t.isStringLiteral(targetAttr.value)) {
|
|
343
|
+
isTargetBlank = targetAttr.value.value.trim().toLowerCase() === '_blank';
|
|
344
|
+
} else if (t.isJSXExpressionContainer(targetAttr.value) && t.isStringLiteral(targetAttr.value.expression)) {
|
|
345
|
+
isTargetBlank = targetAttr.value.expression.value.trim().toLowerCase() === '_blank';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (isTargetBlank) {
|
|
350
|
+
const relAttr = astPath.node.attributes.find((attr) => t.isJSXAttribute(attr) && attr.name?.name === 'rel');
|
|
351
|
+
let hasRelProtection = false;
|
|
352
|
+
if (relAttr) {
|
|
353
|
+
if (t.isStringLiteral(relAttr.value)) {
|
|
354
|
+
const val = relAttr.value.value.toLowerCase();
|
|
355
|
+
hasRelProtection = val.includes('noopener') || val.includes('noreferrer');
|
|
356
|
+
} else if (t.isJSXExpressionContainer(relAttr.value) && t.isStringLiteral(relAttr.value.expression)) {
|
|
357
|
+
const val = relAttr.value.expression.value.toLowerCase();
|
|
358
|
+
hasRelProtection = val.includes('noopener') || val.includes('noreferrer');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!hasRelProtection) {
|
|
363
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
364
|
+
const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
|
|
365
|
+
violations.push({
|
|
366
|
+
filePath: relativePath,
|
|
367
|
+
line,
|
|
368
|
+
column: astPath.node.loc?.start.column || 1,
|
|
369
|
+
hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
|
|
370
|
+
rule: 'SECURITY_REVERSE_TABNABBING',
|
|
371
|
+
severity: meta.severity,
|
|
372
|
+
pillar: meta.pillar,
|
|
373
|
+
directive: meta.directive
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
247
378
|
},
|
|
248
379
|
|
|
249
380
|
JSXAttribute(astPath) {
|
|
@@ -314,6 +445,118 @@ export const createExtendedVisitors = ({ relativePath, violations }) => {
|
|
|
314
445
|
}
|
|
315
446
|
}
|
|
316
447
|
}
|
|
448
|
+
|
|
449
|
+
// Pillar 9: javascript: pseudo-protocol URL in JSX
|
|
450
|
+
const isUrlAttribute = ['href', 'src', 'action', 'formAction'].includes(attrName);
|
|
451
|
+
if (isUrlAttribute) {
|
|
452
|
+
let isJsUrl = false;
|
|
453
|
+
const val = astPath.node.value;
|
|
454
|
+
if (t.isStringLiteral(val)) {
|
|
455
|
+
isJsUrl = JAVASCRIPT_URL_REGEX.test(val.value);
|
|
456
|
+
} else if (t.isJSXExpressionContainer(val)) {
|
|
457
|
+
const expr = val.expression;
|
|
458
|
+
if (t.isStringLiteral(expr)) {
|
|
459
|
+
isJsUrl = JAVASCRIPT_URL_REGEX.test(expr.value);
|
|
460
|
+
} else if (t.isTemplateLiteral(expr) && expr.quasis.length > 0) {
|
|
461
|
+
isJsUrl = JAVASCRIPT_URL_REGEX.test(expr.quasis[0].value.raw);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (isJsUrl) {
|
|
466
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
467
|
+
const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
|
|
468
|
+
violations.push({
|
|
469
|
+
filePath: relativePath,
|
|
470
|
+
line,
|
|
471
|
+
column: astPath.node.loc?.start.column || 1,
|
|
472
|
+
hazard: `javascript: pseudo-protocol detected in ${attrName} attribute`,
|
|
473
|
+
rule: 'SECURITY_JAVASCRIPT_URL',
|
|
474
|
+
severity: meta.severity,
|
|
475
|
+
pillar: meta.pillar,
|
|
476
|
+
directive: meta.directive
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
CallExpression(astPath) {
|
|
483
|
+
const callee = astPath.node.callee;
|
|
484
|
+
|
|
485
|
+
// Pillar 9: Dynamic code execution via eval()
|
|
486
|
+
if (t.isIdentifier(callee) && callee.name === 'eval') {
|
|
487
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
488
|
+
const meta = RULE_REGISTRY.SECURITY_DYNAMIC_CODE_EXECUTION;
|
|
489
|
+
violations.push({
|
|
490
|
+
filePath: relativePath,
|
|
491
|
+
line,
|
|
492
|
+
column: astPath.node.loc?.start.column || 1,
|
|
493
|
+
hazard: 'Dynamic code execution via eval() detected',
|
|
494
|
+
rule: 'SECURITY_DYNAMIC_CODE_EXECUTION',
|
|
495
|
+
severity: meta.severity,
|
|
496
|
+
pillar: meta.pillar,
|
|
497
|
+
directive: meta.directive
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Pillar 9: String-based code execution in setTimeout/setInterval
|
|
502
|
+
if (t.isIdentifier(callee) && (callee.name === 'setTimeout' || callee.name === 'setInterval')) {
|
|
503
|
+
const firstArg = astPath.node.arguments[0];
|
|
504
|
+
const isStringExecution = t.isStringLiteral(firstArg) || t.isTemplateLiteral(firstArg);
|
|
505
|
+
if (isStringExecution) {
|
|
506
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
507
|
+
const meta = RULE_REGISTRY.SECURITY_DYNAMIC_CODE_EXECUTION;
|
|
508
|
+
violations.push({
|
|
509
|
+
filePath: relativePath,
|
|
510
|
+
line,
|
|
511
|
+
column: astPath.node.loc?.start.column || 1,
|
|
512
|
+
hazard: `String-based dynamic code execution in ${callee.name} detected`,
|
|
513
|
+
rule: 'SECURITY_DYNAMIC_CODE_EXECUTION',
|
|
514
|
+
severity: meta.severity,
|
|
515
|
+
pillar: meta.pillar,
|
|
516
|
+
directive: meta.directive
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Pillar 9: Sensitive credential/token logging in console
|
|
522
|
+
const isConsoleCall = t.isMemberExpression(callee) && t.isIdentifier(callee.object) && callee.object.name === 'console';
|
|
523
|
+
if (isConsoleCall) {
|
|
524
|
+
const args = astPath.node.arguments || [];
|
|
525
|
+
const hasSensitiveArg = args.some(isSensitiveExpression);
|
|
526
|
+
if (hasSensitiveArg) {
|
|
527
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
528
|
+
const meta = RULE_REGISTRY.SECURITY_SENSITIVE_LOGGING;
|
|
529
|
+
violations.push({
|
|
530
|
+
filePath: relativePath,
|
|
531
|
+
line,
|
|
532
|
+
column: astPath.node.loc?.start.column || 1,
|
|
533
|
+
hazard: 'Sensitive variable (credential, token, or password) logged to console',
|
|
534
|
+
rule: 'SECURITY_SENSITIVE_LOGGING',
|
|
535
|
+
severity: meta.severity,
|
|
536
|
+
pillar: meta.pillar,
|
|
537
|
+
directive: meta.directive
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
NewExpression(astPath) {
|
|
544
|
+
// Pillar 9: Dynamic code execution via new Function()
|
|
545
|
+
const callee = astPath.node.callee;
|
|
546
|
+
if (t.isIdentifier(callee) && callee.name === 'Function') {
|
|
547
|
+
const line = astPath.node.loc?.start.line || 1;
|
|
548
|
+
const meta = RULE_REGISTRY.SECURITY_DYNAMIC_CODE_EXECUTION;
|
|
549
|
+
violations.push({
|
|
550
|
+
filePath: relativePath,
|
|
551
|
+
line,
|
|
552
|
+
column: astPath.node.loc?.start.column || 1,
|
|
553
|
+
hazard: 'Dynamic code execution via new Function() constructor detected',
|
|
554
|
+
rule: 'SECURITY_DYNAMIC_CODE_EXECUTION',
|
|
555
|
+
severity: meta.severity,
|
|
556
|
+
pillar: meta.pillar,
|
|
557
|
+
directive: meta.directive
|
|
558
|
+
});
|
|
559
|
+
}
|
|
317
560
|
},
|
|
318
561
|
|
|
319
562
|
VariableDeclarator(astPath) {
|
|
@@ -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
|
+
|
package/cli/audit/prompts.js
CHANGED
|
@@ -293,13 +293,12 @@ export const buildHotspotsPrompt = (report, options = {}) => {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
lines.push('### STRICT EXECUTION RULES:');
|
|
296
|
-
lines.push('1.
|
|
297
|
-
lines.push('2.
|
|
298
|
-
lines.push('3.
|
|
299
|
-
lines.push('4.
|
|
300
|
-
lines.push('5.
|
|
301
|
-
lines.push('6.
|
|
302
|
-
lines.push('7. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
296
|
+
lines.push('1. Pre-Split Pattern Discovery: Audit recurring UI layouts, state machines, and predicates across monoliths first; extract canonical shared capsules before slicing to prevent bespoke pattern duplication.');
|
|
297
|
+
lines.push('2. Molecular Limits: Molecule capsules must strictly remain under 100 lines per file.');
|
|
298
|
+
lines.push('3. Table-of-Contents Views: Top-level page views must be 10 to 20 line declarative Table of Contents assembling molecules via named slots.');
|
|
299
|
+
lines.push('4. Composable Return Contracts: Custom hooks/composables must strictly limit returns to 3 to 5 properties (State + Status + Actions).');
|
|
300
|
+
lines.push('5. Type Co-location: Co-locate granular types/*.d.ts files inside each feature capsule (< 100 lines per type file) instead of creating type monoliths.');
|
|
301
|
+
lines.push('6. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
303
302
|
if (!isSubSection) {
|
|
304
303
|
lines.push('');
|
|
305
304
|
lines.push(buildAgentCommandsSection());
|
|
@@ -340,11 +339,10 @@ export const buildPillarPrompt = (report, pillarName, options = {}) => {
|
|
|
340
339
|
}
|
|
341
340
|
|
|
342
341
|
lines.push('### STRICT EXECUTION RULES:');
|
|
343
|
-
lines.push('1.
|
|
344
|
-
lines.push('2.
|
|
345
|
-
lines.push('3.
|
|
346
|
-
lines.push('4.
|
|
347
|
-
lines.push('5. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
342
|
+
lines.push('1. Pre-Split Pattern Discovery: Survey cross-file patterns before slicing; extract canonical shared capsules first.');
|
|
343
|
+
lines.push('2. Molecular Capsule Limit: Maximum 100 lines per molecule capsule file.');
|
|
344
|
+
lines.push('3. Table-of-Contents Views: Top-level page views must be 10 to 20 line declarative templates assembling components via named slots.');
|
|
345
|
+
lines.push('4. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
348
346
|
if (!isSubSection) {
|
|
349
347
|
lines.push('');
|
|
350
348
|
lines.push(buildAgentCommandsSection());
|
|
@@ -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) => {
|
package/cli/audit/roadmap.js
CHANGED
|
@@ -128,12 +128,6 @@ export const buildRemediationRoadmap = (report) => {
|
|
|
128
128
|
target: '.git/hooks/pre-commit & .github/workflows/',
|
|
129
129
|
action: 'Run npx chemx install to lock in line budgets and prevent future monolith regressions.',
|
|
130
130
|
locations: []
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
title: 'Branch, Commit & Pull Request Workflow',
|
|
134
|
-
target: 'git checkout -b chem-x/NAMEOFIMPROVEMENT',
|
|
135
|
-
action: 'Change to a branch prefixed with chem-x/NAMEOFIMPROVEMENT to make changes, commit the branch, and create a PR to main.',
|
|
136
|
-
locations: []
|
|
137
131
|
}
|
|
138
132
|
]
|
|
139
133
|
});
|
|
@@ -202,13 +196,6 @@ export const buildSelfHealingRoadmapPrompt = (report) => {
|
|
|
202
196
|
|
|
203
197
|
lines.push('Act as a Principal Systems Architect. Execute a self-healing architectural remediation of our codebase by following this strict multi-phase sequence in order:\n');
|
|
204
198
|
|
|
205
|
-
lines.push('### MANDATORY GIT BRANCHING & PR WORKFLOW:');
|
|
206
|
-
lines.push('1. Before modifying any files, change to a dedicated feature branch prefixed with `chem-x/NAMEOFIMPROVEMENT`:');
|
|
207
|
-
lines.push(' `git checkout -b chem-x/NAMEOFIMPROVEMENT`');
|
|
208
|
-
lines.push('2. Execute each remediation phase sequentially on this branch.');
|
|
209
|
-
lines.push('3. Commit the branch with atomic, intention-revealing commit messages.');
|
|
210
|
-
lines.push('4. Create a Pull Request (PR) to `main` upon completing the remediation phases.\n');
|
|
211
|
-
|
|
212
199
|
lines.push('### AI AGENT DISCOVERY & REFACTORING COMMANDS:');
|
|
213
200
|
lines.push('- Discovery & Inspect: Run `pnpm q "<target>" --inspect` (or `npx chemx search "<target>" --inspect`) to inspect component props and hooks before editing.');
|
|
214
201
|
lines.push('- Tier Filter: Run `pnpm q "<query>" --tier=molecule` (or `atom`, `organism`, `hook`) to find related capsules.');
|
|
@@ -234,13 +221,11 @@ export const buildSelfHealingRoadmapPrompt = (report) => {
|
|
|
234
221
|
});
|
|
235
222
|
|
|
236
223
|
lines.push('### EXECUTION DISCIPLINE:');
|
|
237
|
-
lines.push('1.
|
|
238
|
-
lines.push('2.
|
|
239
|
-
lines.push('3.
|
|
240
|
-
lines.push('4.
|
|
241
|
-
lines.push('5.
|
|
242
|
-
lines.push('6. Zero synthetic or mock data; zero em dashes (use hyphens or colons).');
|
|
243
|
-
lines.push('7. PR to Main: Commit the branch and create a Pull Request to main.');
|
|
224
|
+
lines.push('1. Complete Step 1 (Pattern Harvesting) completely before touching any monolithic file in Step 3.');
|
|
225
|
+
lines.push('2. Spliced views in Step 3 must directly import and bind to the canonical capsules created in Step 1.');
|
|
226
|
+
lines.push('3. Every new molecule component must stay under 100 lines.');
|
|
227
|
+
lines.push('4. Top-level page views must be 10-20 line declarative Table-of-Contents templates assembling molecules via named slots.');
|
|
228
|
+
lines.push('5. Zero synthetic or mock data; zero em dashes (use hyphens or colons).');
|
|
244
229
|
|
|
245
230
|
return lines.join('\n');
|
|
246
231
|
};
|
|
@@ -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',
|
package/cli/audit/rules.js
CHANGED
|
@@ -102,6 +102,10 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
102
102
|
// Pillars 8-11: Extended Fast Checks (A11y, Security secrets, Fake tests, Co-located specs)
|
|
103
103
|
checkExtendedTextPatterns(content, lines, relativePath, filePath, violations);
|
|
104
104
|
|
|
105
|
+
if (options.fast) {
|
|
106
|
+
return violations;
|
|
107
|
+
}
|
|
108
|
+
|
|
105
109
|
// Vue template structural pattern discovery
|
|
106
110
|
if (ext === '.vue' && options.patternRegistry) {
|
|
107
111
|
const templateMatch = content.match(/<template\b[^>]*>([\s\S]*?)<\/template>/i);
|