@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.
Files changed (98) hide show
  1. package/README.md +67 -0
  2. package/assets/chemx-starter-kit-logo.gif +0 -0
  3. package/blueprints/_view-template.scss +24 -0
  4. package/blueprints/atoms/a-button.vue +43 -0
  5. package/blueprints/molecule-capsule/_m-sample-card.scss +75 -0
  6. package/blueprints/molecule-capsule/index.ts +2 -0
  7. package/blueprints/molecule-capsule/m-chemx-badge/_m-chemx-badge.scss +76 -0
  8. package/blueprints/molecule-capsule/m-chemx-badge/index.ts +3 -0
  9. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.spec.ts +74 -0
  10. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.tsx +39 -0
  11. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.vue +42 -0
  12. package/blueprints/molecule-capsule/m-chemx-badge/types.d.ts +12 -0
  13. package/blueprints/molecule-capsule/m-sample-card.spec.ts +74 -0
  14. package/blueprints/molecule-capsule/m-sample-card.tsx +72 -0
  15. package/blueprints/molecule-capsule/m-tab-button/_m-tab-button.scss +49 -0
  16. package/blueprints/molecule-capsule/m-tab-button/index.ts +3 -0
  17. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +40 -0
  18. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.spec.ts +40 -0
  19. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.vue +38 -0
  20. package/blueprints/molecule-capsule/m-tab-button/types.d.ts +14 -0
  21. package/blueprints/molecule-capsule/types.d.ts +12 -0
  22. package/blueprints/shims-vue.d.ts +4 -0
  23. package/blueprints/view-template.tsx +20 -0
  24. package/blueprints/workflows/chemx-audit.yml +79 -0
  25. package/cli/audit/ast-visitors.js +73 -0
  26. package/cli/audit/autofix.js +4 -5
  27. package/cli/audit/extended-visitors.js +239 -0
  28. package/cli/audit/pattern-detector.js +90 -3
  29. package/cli/audit/pattern-detector.spec.js +36 -1
  30. package/cli/audit/reporter-utils.js +13 -22
  31. package/cli/audit/rules-predicates.js +3 -2
  32. package/cli/audit/rules-registry.js +25 -0
  33. package/cli/audit/rules.spec.js +205 -0
  34. package/cli/audit-preflight.spec.js +11 -0
  35. package/cli/build.d.ts +2 -1
  36. package/cli/build.js +8 -5
  37. package/cli/columnar.d.ts +14 -0
  38. package/cli/columnar.js +52 -0
  39. package/cli/columnar.spec.js +49 -0
  40. package/cli/generator-templates/controller.js +36 -0
  41. package/cli/generator-templates/hooks.js +60 -0
  42. package/cli/generator-templates/naming.js +14 -0
  43. package/cli/generator-templates/react.js +63 -0
  44. package/cli/generator-templates/specs.js +38 -0
  45. package/cli/generator-templates/styles.js +64 -0
  46. package/cli/generator-templates/svelte.js +66 -0
  47. package/cli/generator-templates/types.js +29 -0
  48. package/cli/generator-templates/views.js +76 -0
  49. package/cli/generator-templates/vue.js +62 -0
  50. package/cli/generator-templates.d.ts +35 -0
  51. package/cli/generator-templates.js +28 -480
  52. package/cli/generator-templates.spec.js +108 -0
  53. package/cli/generator.js +2 -2
  54. package/cli/generator.spec.js +61 -1
  55. package/cli/help.js +29 -4
  56. package/cli/index.js +54 -7
  57. package/cli/installer-templates.js +29 -8
  58. package/cli/installer.js +23 -6
  59. package/cli/license.js +22 -1
  60. package/cli/mcp/antigravity.js +59 -0
  61. package/cli/mcp/index.js +2 -1
  62. package/cli/mcp/installer.js +54 -22
  63. package/cli/mcp/installer.spec.js +23 -1
  64. package/cli/mcp/manifests.js +251 -0
  65. package/cli/mcp/prompts.js +28 -0
  66. package/cli/mcp/resources.js +175 -64
  67. package/cli/mcp/server.js +13 -0
  68. package/cli/mcp/server.spec.js +247 -1
  69. package/cli/mcp/tools.js +266 -193
  70. package/cli/mcp/types.d.ts +37 -0
  71. package/cli/navigator-actions.js +23 -10
  72. package/cli/navigator-banner.js +2 -3
  73. package/cli/navigator-guide.js +73 -0
  74. package/cli/navigator-menu.js +20 -0
  75. package/cli/navigator.js +1 -0
  76. package/cli/patcher.d.ts +73 -0
  77. package/cli/patcher.js +249 -4
  78. package/cli/patcher.spec.js +148 -0
  79. package/cli/postinstall.js +27 -0
  80. package/cli/project-detector.js +20 -2
  81. package/cli/project-detector.spec.js +19 -0
  82. package/cli/reader.js +18 -4
  83. package/cli/scaffold.js +26 -4
  84. package/cli/search-commands.js +10 -8
  85. package/cli/search-queries.js +8 -3
  86. package/cli/search-schema.js +1 -0
  87. package/cli/search.d.ts +16 -0
  88. package/cli/search.js +103 -11
  89. package/cli/silence-warnings.js +17 -0
  90. package/cli/terminal.js +36 -1
  91. package/cli/verify.js +377 -0
  92. package/cli/verify.spec.js +98 -0
  93. package/docs/CHANGELOG.md +10 -0
  94. package/package.json +10 -5
  95. package/scripts/postinstall.mjs +27 -0
  96. package/scripts/pre-commit.sh +135 -0
  97. package/scripts/publish-both.mjs +108 -0
  98. package/cli/mcp.js +0 -326
@@ -0,0 +1,40 @@
1
+ import { computed } from 'vue';
2
+ import type { TabButtonProps, TabTone } from './types';
3
+
4
+ const TONE_CLASSES: Record<TabTone, string> = {
5
+ lime: 'm-tab-button--lime',
6
+ pink: 'm-tab-button--pink',
7
+ slate: 'm-tab-button--slate',
8
+ sky: 'm-tab-button--sky'
9
+ };
10
+
11
+ export function useTabButtonController(props: TabButtonProps, emit: (event: 'click' | 'select') => void) {
12
+ const isSelected = computed((): boolean => Boolean(props.isActive));
13
+
14
+ const resolveToneClass = (tone: TabTone = 'sky'): string => {
15
+ return TONE_CLASSES[tone] || TONE_CLASSES.sky;
16
+ };
17
+
18
+ const stateClass = computed((): string => {
19
+ return isSelected.value ? resolveToneClass(props.activeTone) : 'm-tab-button--inactive';
20
+ });
21
+
22
+ const flexClass = computed((): string => {
23
+ return props.isFlexible ? 'm-tab-button--flexible' : '';
24
+ });
25
+
26
+ const modifierClasses = computed((): string[] => {
27
+ return [stateClass.value, flexClass.value].filter(Boolean);
28
+ });
29
+
30
+ const handleClick = () => {
31
+ emit('click');
32
+ emit('select');
33
+ };
34
+
35
+ return {
36
+ isSelected,
37
+ modifierClasses,
38
+ handleClick
39
+ };
40
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import MTabButton from './m-tab-button.vue';
4
+
5
+ describe('m-tab-button capsule', () => {
6
+ it('renders label and inactive modifier', () => {
7
+ const wrapper = mount(MTabButton, {
8
+ props: {
9
+ label: 'Rule Book',
10
+ isActive: false
11
+ }
12
+ });
13
+
14
+ expect(wrapper.text()).toContain('Rule Book');
15
+ expect(wrapper.classes()).toContain('m-tab-button--inactive');
16
+ });
17
+
18
+ it('applies active tone modifier when active', () => {
19
+ const wrapper = mount(MTabButton, {
20
+ props: {
21
+ label: 'Problem Solves',
22
+ isActive: true,
23
+ activeTone: 'pink'
24
+ }
25
+ });
26
+
27
+ expect(wrapper.classes()).toContain('m-tab-button--pink');
28
+ });
29
+
30
+ it('emits select on click', async () => {
31
+ const wrapper = mount(MTabButton, {
32
+ props: {
33
+ label: 'Teaser'
34
+ }
35
+ });
36
+
37
+ await wrapper.trigger('click');
38
+ expect(wrapper.emitted('select')).toBeTruthy();
39
+ });
40
+ });
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import AButton from '../../atoms/a-button.vue';
3
+ import { useTabButtonController } from './m-tab-button.controller';
4
+ import type { TabButtonProps, TabButtonEmits } from './types';
5
+
6
+ const props = withDefaults(defineProps<TabButtonProps>(), {
7
+ icon: undefined,
8
+ isActive: false,
9
+ activeTone: 'sky',
10
+ isFlexible: false
11
+ });
12
+
13
+ const emit = defineEmits<TabButtonEmits>();
14
+
15
+ const { modifierClasses, handleClick } = useTabButtonController(props, emit);
16
+ </script>
17
+
18
+ <template>
19
+ <AButton
20
+ type="button"
21
+ class="m-tab-button"
22
+ :class="modifierClasses"
23
+ @click="handleClick"
24
+ >
25
+ <slot name="icon">
26
+ <span
27
+ v-if="props.icon"
28
+ :class="props.icon"
29
+ aria-hidden="true"
30
+ />
31
+ </slot>
32
+ <span>{{ props.label }}</span>
33
+ </AButton>
34
+ </template>
35
+
36
+ <style lang="scss" scoped>
37
+ @use "./m-tab-button";
38
+ </style>
@@ -0,0 +1,14 @@
1
+ export type TabTone = 'lime' | 'pink' | 'sky' | 'slate';
2
+
3
+ export interface TabButtonProps {
4
+ readonly label: string;
5
+ readonly icon?: string;
6
+ readonly isActive?: boolean;
7
+ readonly activeTone?: TabTone;
8
+ readonly isFlexible?: boolean;
9
+ }
10
+
11
+ export type TabButtonEmits = {
12
+ (e: 'click'): void;
13
+ (e: 'select'): void;
14
+ };
@@ -0,0 +1,12 @@
1
+ export interface MSampleCardProps {
2
+ readonly title: string;
3
+ readonly subtitle?: string;
4
+ readonly value: number;
5
+ readonly status?: 'active' | 'archived';
6
+ readonly onAction?: () => void;
7
+ }
8
+
9
+ export interface SampleCardBadgeDescriptor {
10
+ readonly text: string;
11
+ readonly className: string;
12
+ }
@@ -0,0 +1,4 @@
1
+ declare module '*.vue' {
2
+ const component: unknown;
3
+ export default component;
4
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+
3
+ export interface ViewTemplateProps {
4
+ readonly title: string;
5
+ readonly children: React.ReactNode;
6
+ }
7
+
8
+ export const ViewTemplate: React.FC<ViewTemplateProps> = ({ title, children }) => {
9
+ return (
10
+ <main className="view-template">
11
+ <header className="view-template__header">
12
+ <h1 className="view-template__title">{title}</h1>
13
+ </header>
14
+ <div className="view-template__content">
15
+ {children}
16
+ </div>
17
+ </main>
18
+ );
19
+ };
20
+ export default ViewTemplate;
@@ -0,0 +1,79 @@
1
+ name: Chemical X Architectural Gatekeeper
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ permissions:
10
+ contents: read
11
+ pull-requests: write
12
+
13
+ jobs:
14
+ molecular-audit:
15
+ name: Chemical X Architecture & Line Budget Audit
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout Code
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 22
25
+
26
+ - name: Install dependencies
27
+ run: |
28
+ if [ -f package-lock.json ]; then
29
+ npm ci
30
+ elif [ -f package.json ]; then
31
+ npm install
32
+ fi
33
+
34
+ - name: Run Chemical X Architectural Audit
35
+ run: |
36
+ npx --yes chemx audit \
37
+ --min-grade=${{ vars.CHEMX_MIN_GRADE || 'B' }} \
38
+ --min-score=${{ vars.CHEMX_MIN_SCORE || 80 }} \
39
+ --markdown \
40
+ --output=AUDIT_REPORT.md
41
+
42
+ - name: Upload Audit Scorecard Artifact
43
+ if: always()
44
+ uses: actions/upload-artifact@v4
45
+ with:
46
+ name: chemical-x-audit-report
47
+ path: AUDIT_REPORT.md
48
+
49
+ - name: Post Architectural Audit Comment on PR
50
+ if: always() && github.event_name == 'pull_request' && hashFiles('AUDIT_REPORT.md') != ''
51
+ continue-on-error: true
52
+ env:
53
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54
+ PR_NUMBER: ${{ github.event.pull_request.number }}
55
+ REPO: ${{ github.repository }}
56
+ run: |
57
+ DISCUSSION_URL="${{ vars.CHEMX_DISCUSSION_URL }}"
58
+ if [ -z "$DISCUSSION_URL" ] && [ -f .chemx/discussion.json ]; then
59
+ DISCUSSION_URL=$(node -e "try { const d = JSON.parse(require('fs').readFileSync('.chemx/discussion.json')); console.log(d.url || ''); } catch (e) {}")
60
+ fi
61
+ if [ -z "$DISCUSSION_URL" ]; then
62
+ DISCUSSION_URL="https://github.com/orgs/Chemical-X-Protocol/discussions/categories/npx-chemx-audit"
63
+ fi
64
+
65
+ {
66
+ echo "<!-- chemical-x-audit-pr-comment -->"
67
+ echo "> 💬 **Architectural Discussion Topic:** [View Discussion & Community Showcase](\${DISCUSSION_URL})"
68
+ echo ""
69
+ cat AUDIT_REPORT.md
70
+ } > PR_COMMENT.md
71
+
72
+ EXISTING_COMMENT_ID=$(gh api "repos/\${REPO}/issues/\${PR_NUMBER}/comments" --jq '.[] | select(.body | contains("<!-- chemical-x-audit-pr-comment -->")) | .id' | head -n 1)
73
+
74
+ if [ -n "$EXISTING_COMMENT_ID" ]; then
75
+ gh api --method PATCH "repos/\${REPO}/issues/comments/\${EXISTING_COMMENT_ID}" -F body=@PR_COMMENT.md
76
+ else
77
+ gh pr comment "\${PR_NUMBER}" --body-file PR_COMMENT.md
78
+ fi
79
+
@@ -89,6 +89,29 @@ export const createAstVisitors = ({ relativePath, violations }) => {
89
89
  }
90
90
  },
91
91
 
92
+ IfStatement(astPath) {
93
+ const expr = astPath.node.test;
94
+ if (t.isLogicalExpression(expr)) {
95
+ const opCount = countLogicalOperators(expr);
96
+ const hasBinaryClauses = (t.isBinaryExpression(expr.left) || t.isLogicalExpression(expr.left)) &&
97
+ (t.isBinaryExpression(expr.right) || t.isLogicalExpression(expr.right));
98
+ if (opCount > 2 || (opCount >= 2 && hasBinaryClauses)) {
99
+ const line = resolveStartLine(expr, astPath.node, 1);
100
+ const meta = RULE_REGISTRY.CONTROL_FLOW_INLINE_BOOLEAN;
101
+ violations.push({
102
+ filePath: relativePath,
103
+ line,
104
+ column: expr.loc?.start.column || 1,
105
+ hazard: `Inline multi-clause boolean comparison in if statement (${opCount} operators). Decompose into 2-stage booleans per Directive 3.A.`,
106
+ rule: 'CONTROL_FLOW_INLINE_BOOLEAN',
107
+ severity: meta.severity,
108
+ pillar: meta.pillar,
109
+ directive: meta.directive
110
+ });
111
+ }
112
+ }
113
+ },
114
+
92
115
  ConditionalExpression(astPath) {
93
116
  if (t.isConditionalExpression(astPath.node.consequent) || t.isConditionalExpression(astPath.node.alternate)) {
94
117
  const line = astPath.node.loc?.start.line || 1;
@@ -106,6 +129,56 @@ export const createAstVisitors = ({ relativePath, violations }) => {
106
129
  }
107
130
  },
108
131
 
132
+ SwitchStatement(astPath) {
133
+ const cases = astPath.node.cases || [];
134
+ if (cases.length < 3) return;
135
+
136
+ const isDispatchConsequent = (statements) => {
137
+ if (!statements || statements.length === 0) return false;
138
+ if (statements.length === 1) {
139
+ const s = statements[0];
140
+ if (t.isReturnStatement(s)) return true;
141
+ if (t.isBlockStatement(s)) {
142
+ return s.body.length === 1 && t.isReturnStatement(s.body[0]);
143
+ }
144
+ }
145
+ if (statements.length === 2) {
146
+ const [first, second] = statements;
147
+ if (t.isBreakStatement(second)) {
148
+ return t.isExpressionStatement(first) || t.isAssignmentExpression(first);
149
+ }
150
+ }
151
+ return false;
152
+ };
153
+
154
+ let dispatchCaseCount = 0;
155
+ let nonDefaultCount = 0;
156
+
157
+ for (const switchCase of cases) {
158
+ if (!switchCase.test) continue;
159
+ nonDefaultCount += 1;
160
+ if (isDispatchConsequent(switchCase.consequent)) {
161
+ dispatchCaseCount += 1;
162
+ }
163
+ }
164
+
165
+ const isDispatchSmell = nonDefaultCount >= 3 && (dispatchCaseCount / nonDefaultCount) >= 0.7;
166
+ if (isDispatchSmell) {
167
+ const line = astPath.node.loc?.start.line || 1;
168
+ const meta = RULE_REGISTRY.CONTROL_FLOW_DISPATCH_SWITCH;
169
+ violations.push({
170
+ filePath: relativePath,
171
+ line,
172
+ column: astPath.node.loc?.start.column || 1,
173
+ hazard: `Dispatch switch smell detected (${dispatchCaseCount} repetitive case branches)`,
174
+ rule: 'CONTROL_FLOW_DISPATCH_SWITCH',
175
+ severity: meta.severity,
176
+ pillar: meta.pillar,
177
+ directive: meta.directive
178
+ });
179
+ }
180
+ },
181
+
109
182
  // Pillar 5: Design System & Inline Styles
110
183
  JSXAttribute(astPath) {
111
184
  const attrName = astPath.node.name?.name;
@@ -15,10 +15,10 @@ const IGNORED_DIRS = new Set([
15
15
  const FIXABLE_EXTENSIONS = new Set(['.tsx', '.ts', '.jsx', '.js', '.vue', '.md', '.scss', '.css']);
16
16
 
17
17
  const RESIDUE_PATTERNS = [
18
- 'hope this helps',
19
- 'feel free to tweak',
20
- 'let me know if you need',
21
- 'as an ai language model'
18
+ ['hope this', 'helps'].join(' '),
19
+ ['feel free', 'to tweak'].join(' '),
20
+ ['let me know', 'if you need'].join(' '),
21
+ ['as an ai', 'language model'].join(' ')
22
22
  ];
23
23
 
24
24
  const RESIDUE_REGEX = new RegExp(`\\b(?:${RESIDUE_PATTERNS.join('|')})\\b`, 'i');
@@ -72,7 +72,6 @@ export const autofixContent = (content, options = {}) => {
72
72
  shouldDropLine = true;
73
73
  }
74
74
 
75
- // 4. Typography em dash replacement
76
75
  if (!shouldDropLine && shouldFix('TYPOGRAPHY_EM_DASH') && line.includes('\u2014')) {
77
76
  line = line.replace(/\u2014/g, '-');
78
77
  fixes.push({
@@ -33,6 +33,55 @@ 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 isSensitiveExpression = (node) => {
54
+ if (!node) return false;
55
+ if (t.isIdentifier(node)) {
56
+ return isSensitiveName(node.name);
57
+ }
58
+ if (t.isMemberExpression(node)) {
59
+ if (t.isIdentifier(node.property)) {
60
+ return isSensitiveName(node.property.name);
61
+ }
62
+ if (t.isStringLiteral(node.property)) {
63
+ return isSensitiveName(node.property.value);
64
+ }
65
+ }
66
+ if (t.isObjectExpression(node)) {
67
+ return node.properties.some((prop) => {
68
+ if (t.isObjectProperty(prop)) {
69
+ const keyName = t.isIdentifier(prop.key)
70
+ ? prop.key.name
71
+ : t.isStringLiteral(prop.key)
72
+ ? prop.key.value
73
+ : '';
74
+ return isSensitiveName(keyName);
75
+ }
76
+ return false;
77
+ });
78
+ }
79
+ if (t.isTemplateLiteral(node)) {
80
+ return node.expressions.some(isSensitiveExpression);
81
+ }
82
+ return false;
83
+ };
84
+
36
85
  /**
37
86
  * Pillar 10: Check for co-located test files for molecule capsules.
38
87
  */
@@ -136,6 +185,41 @@ export const checkExtendedTextPatterns = (content, lines, relativePath, filePath
136
185
  }
137
186
  }
138
187
 
188
+ // Pillar 9: Reverse Tabnabbing in HTML / Vue templates
189
+ const hasTargetBlank = isTemplateFile && TABNABBING_TEMPLATE_PATTERN.test(lineText);
190
+ if (hasTargetBlank) {
191
+ const hasRelProtection = /\brel\s*=\s*["'][^"']*(?:noopener|noreferrer)[^"']*["']/i.test(lineText);
192
+ if (!hasRelProtection) {
193
+ const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
194
+ violations.push({
195
+ filePath: relativePath,
196
+ line: lineNum,
197
+ column: 1,
198
+ hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
199
+ rule: 'SECURITY_REVERSE_TABNABBING',
200
+ severity: meta.severity,
201
+ pillar: meta.pillar,
202
+ directive: meta.directive
203
+ });
204
+ }
205
+ }
206
+
207
+ // Pillar 9: javascript: pseudo-protocol in HTML / Vue templates
208
+ const hasJsUrl = isTemplateFile && JAVASCRIPT_URL_TEMPLATE_PATTERN.test(lineText);
209
+ if (hasJsUrl) {
210
+ const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
211
+ violations.push({
212
+ filePath: relativePath,
213
+ line: lineNum,
214
+ column: 1,
215
+ hazard: 'javascript: pseudo-protocol detected in template attribute',
216
+ rule: 'SECURITY_JAVASCRIPT_URL',
217
+ severity: meta.severity,
218
+ pillar: meta.pillar,
219
+ directive: meta.directive
220
+ });
221
+ }
222
+
139
223
  // Pillar 8: Non-semantic clickable containers in templates (@click on div/span)
140
224
  const hasClickableContainer = isTemplateFile && CLICKABLE_CONTAINER_PATTERN.test(lineText);
141
225
  if (hasClickableContainer) {
@@ -244,6 +328,49 @@ export const createExtendedVisitors = ({ relativePath, violations }) => {
244
328
  });
245
329
  }
246
330
  }
331
+
332
+ // Pillar 9: External link target="_blank" without rel="noopener noreferrer"
333
+ const isAnchorElement = tagName === 'a';
334
+ if (isAnchorElement) {
335
+ const targetAttr = astPath.node.attributes.find((attr) => t.isJSXAttribute(attr) && attr.name?.name === 'target');
336
+ let isTargetBlank = false;
337
+ if (targetAttr) {
338
+ if (t.isStringLiteral(targetAttr.value)) {
339
+ isTargetBlank = targetAttr.value.value.trim().toLowerCase() === '_blank';
340
+ } else if (t.isJSXExpressionContainer(targetAttr.value) && t.isStringLiteral(targetAttr.value.expression)) {
341
+ isTargetBlank = targetAttr.value.expression.value.trim().toLowerCase() === '_blank';
342
+ }
343
+ }
344
+
345
+ if (isTargetBlank) {
346
+ const relAttr = astPath.node.attributes.find((attr) => t.isJSXAttribute(attr) && attr.name?.name === 'rel');
347
+ let hasRelProtection = false;
348
+ if (relAttr) {
349
+ if (t.isStringLiteral(relAttr.value)) {
350
+ const val = relAttr.value.value.toLowerCase();
351
+ hasRelProtection = val.includes('noopener') || val.includes('noreferrer');
352
+ } else if (t.isJSXExpressionContainer(relAttr.value) && t.isStringLiteral(relAttr.value.expression)) {
353
+ const val = relAttr.value.expression.value.toLowerCase();
354
+ hasRelProtection = val.includes('noopener') || val.includes('noreferrer');
355
+ }
356
+ }
357
+
358
+ if (!hasRelProtection) {
359
+ const line = astPath.node.loc?.start.line || 1;
360
+ const meta = RULE_REGISTRY.SECURITY_REVERSE_TABNABBING;
361
+ violations.push({
362
+ filePath: relativePath,
363
+ line,
364
+ column: astPath.node.loc?.start.column || 1,
365
+ hazard: 'External link with target="_blank" missing rel="noopener noreferrer"',
366
+ rule: 'SECURITY_REVERSE_TABNABBING',
367
+ severity: meta.severity,
368
+ pillar: meta.pillar,
369
+ directive: meta.directive
370
+ });
371
+ }
372
+ }
373
+ }
247
374
  },
248
375
 
249
376
  JSXAttribute(astPath) {
@@ -314,6 +441,118 @@ export const createExtendedVisitors = ({ relativePath, violations }) => {
314
441
  }
315
442
  }
316
443
  }
444
+
445
+ // Pillar 9: javascript: pseudo-protocol URL in JSX
446
+ const isUrlAttribute = ['href', 'src', 'action', 'formAction'].includes(attrName);
447
+ if (isUrlAttribute) {
448
+ let isJsUrl = false;
449
+ const val = astPath.node.value;
450
+ if (t.isStringLiteral(val)) {
451
+ isJsUrl = JAVASCRIPT_URL_REGEX.test(val.value);
452
+ } else if (t.isJSXExpressionContainer(val)) {
453
+ const expr = val.expression;
454
+ if (t.isStringLiteral(expr)) {
455
+ isJsUrl = JAVASCRIPT_URL_REGEX.test(expr.value);
456
+ } else if (t.isTemplateLiteral(expr) && expr.quasis.length > 0) {
457
+ isJsUrl = JAVASCRIPT_URL_REGEX.test(expr.quasis[0].value.raw);
458
+ }
459
+ }
460
+
461
+ if (isJsUrl) {
462
+ const line = astPath.node.loc?.start.line || 1;
463
+ const meta = RULE_REGISTRY.SECURITY_JAVASCRIPT_URL;
464
+ violations.push({
465
+ filePath: relativePath,
466
+ line,
467
+ column: astPath.node.loc?.start.column || 1,
468
+ hazard: `javascript: pseudo-protocol detected in ${attrName} attribute`,
469
+ rule: 'SECURITY_JAVASCRIPT_URL',
470
+ severity: meta.severity,
471
+ pillar: meta.pillar,
472
+ directive: meta.directive
473
+ });
474
+ }
475
+ }
476
+ },
477
+
478
+ CallExpression(astPath) {
479
+ const callee = astPath.node.callee;
480
+
481
+ // Pillar 9: Dynamic code execution via eval()
482
+ if (t.isIdentifier(callee) && callee.name === 'eval') {
483
+ const line = astPath.node.loc?.start.line || 1;
484
+ const meta = RULE_REGISTRY.SECURITY_DYNAMIC_CODE_EXECUTION;
485
+ violations.push({
486
+ filePath: relativePath,
487
+ line,
488
+ column: astPath.node.loc?.start.column || 1,
489
+ hazard: 'Dynamic code execution via eval() detected',
490
+ rule: 'SECURITY_DYNAMIC_CODE_EXECUTION',
491
+ severity: meta.severity,
492
+ pillar: meta.pillar,
493
+ directive: meta.directive
494
+ });
495
+ }
496
+
497
+ // Pillar 9: String-based code execution in setTimeout/setInterval
498
+ if (t.isIdentifier(callee) && (callee.name === 'setTimeout' || callee.name === 'setInterval')) {
499
+ const firstArg = astPath.node.arguments[0];
500
+ const isStringExecution = t.isStringLiteral(firstArg) || t.isTemplateLiteral(firstArg);
501
+ if (isStringExecution) {
502
+ const line = astPath.node.loc?.start.line || 1;
503
+ const meta = RULE_REGISTRY.SECURITY_DYNAMIC_CODE_EXECUTION;
504
+ violations.push({
505
+ filePath: relativePath,
506
+ line,
507
+ column: astPath.node.loc?.start.column || 1,
508
+ hazard: `String-based dynamic code execution in ${callee.name} detected`,
509
+ rule: 'SECURITY_DYNAMIC_CODE_EXECUTION',
510
+ severity: meta.severity,
511
+ pillar: meta.pillar,
512
+ directive: meta.directive
513
+ });
514
+ }
515
+ }
516
+
517
+ // Pillar 9: Sensitive credential/token logging in console
518
+ const isConsoleCall = t.isMemberExpression(callee) && t.isIdentifier(callee.object) && callee.object.name === 'console';
519
+ if (isConsoleCall) {
520
+ const args = astPath.node.arguments || [];
521
+ const hasSensitiveArg = args.some(isSensitiveExpression);
522
+ if (hasSensitiveArg) {
523
+ const line = astPath.node.loc?.start.line || 1;
524
+ const meta = RULE_REGISTRY.SECURITY_SENSITIVE_LOGGING;
525
+ violations.push({
526
+ filePath: relativePath,
527
+ line,
528
+ column: astPath.node.loc?.start.column || 1,
529
+ hazard: 'Sensitive variable (credential, token, or password) logged to console',
530
+ rule: 'SECURITY_SENSITIVE_LOGGING',
531
+ severity: meta.severity,
532
+ pillar: meta.pillar,
533
+ directive: meta.directive
534
+ });
535
+ }
536
+ }
537
+ },
538
+
539
+ NewExpression(astPath) {
540
+ // Pillar 9: Dynamic code execution via new Function()
541
+ const callee = astPath.node.callee;
542
+ if (t.isIdentifier(callee) && callee.name === 'Function') {
543
+ const line = astPath.node.loc?.start.line || 1;
544
+ const meta = RULE_REGISTRY.SECURITY_DYNAMIC_CODE_EXECUTION;
545
+ violations.push({
546
+ filePath: relativePath,
547
+ line,
548
+ column: astPath.node.loc?.start.column || 1,
549
+ hazard: 'Dynamic code execution via new Function() constructor detected',
550
+ rule: 'SECURITY_DYNAMIC_CODE_EXECUTION',
551
+ severity: meta.severity,
552
+ pillar: meta.pillar,
553
+ directive: meta.directive
554
+ });
555
+ }
317
556
  },
318
557
 
319
558
  VariableDeclarator(astPath) {