@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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MSampleCardProps, SampleCardBadgeDescriptor } from './types';
|
|
3
|
+
|
|
4
|
+
export const resolveBadgeDescriptor = (
|
|
5
|
+
status: 'active' | 'archived',
|
|
6
|
+
isHighValue: boolean
|
|
7
|
+
): SampleCardBadgeDescriptor => {
|
|
8
|
+
if (status !== 'active') {
|
|
9
|
+
return {
|
|
10
|
+
text: status,
|
|
11
|
+
className: 'm-sample-card__badge m-sample-card__badge--archived'
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (isHighValue) {
|
|
16
|
+
return {
|
|
17
|
+
text: status,
|
|
18
|
+
className: 'm-sample-card__badge m-sample-card__badge--high-value'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
text: status,
|
|
24
|
+
className: 'm-sample-card__badge m-sample-card__badge--standard'
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const MSampleCard: React.FC<MSampleCardProps> = ({
|
|
29
|
+
title,
|
|
30
|
+
subtitle,
|
|
31
|
+
value,
|
|
32
|
+
status = 'active',
|
|
33
|
+
onAction
|
|
34
|
+
}) => {
|
|
35
|
+
const isHighValue = value > 1000;
|
|
36
|
+
const badge = resolveBadgeDescriptor(status, isHighValue);
|
|
37
|
+
|
|
38
|
+
const handleActionClick = () => {
|
|
39
|
+
if (!onAction) return;
|
|
40
|
+
onAction();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="m-sample-card">
|
|
45
|
+
<div className="m-sample-card__header">
|
|
46
|
+
<div className="m-sample-card__title-group">
|
|
47
|
+
<h3 className="m-sample-card__title">{title}</h3>
|
|
48
|
+
{subtitle && <p className="m-sample-card__subtitle">{subtitle}</p>}
|
|
49
|
+
</div>
|
|
50
|
+
<span className={badge.className}>
|
|
51
|
+
{badge.text}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="m-sample-card__body">
|
|
55
|
+
<div className="m-sample-card__value">
|
|
56
|
+
${value.toLocaleString()}
|
|
57
|
+
</div>
|
|
58
|
+
{onAction && (
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="m-sample-card__action"
|
|
62
|
+
onClick={handleActionClick}
|
|
63
|
+
>
|
|
64
|
+
Action
|
|
65
|
+
</button>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default MSampleCard;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.m-tab-button {
|
|
2
|
+
padding: 0.375rem 0.625rem;
|
|
3
|
+
border-radius: 0.5rem;
|
|
4
|
+
font-size: 0.75rem;
|
|
5
|
+
font-family: monospace;
|
|
6
|
+
font-weight: 700;
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
gap: 0.375rem;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
transition: all 0.2s ease-in-out;
|
|
13
|
+
|
|
14
|
+
&--flexible {
|
|
15
|
+
flex: 1 1 0%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
&--inactive {
|
|
19
|
+
color: var(--color-slate-400, #94a3b8);
|
|
20
|
+
|
|
21
|
+
&:hover {
|
|
22
|
+
color: var(--color-slate-200, #e2e8f0);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&--lime {
|
|
27
|
+
background-color: #84cc16;
|
|
28
|
+
color: #ffffff;
|
|
29
|
+
box-shadow: 0 4px 6px -1px rgba(132, 204, 22, 0.2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&--pink {
|
|
33
|
+
background-color: #ec4899;
|
|
34
|
+
color: #ffffff;
|
|
35
|
+
box-shadow: 0 4px 6px -1px rgba(236, 72, 153, 0.2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&--sky {
|
|
39
|
+
background-color: #0ea5e9;
|
|
40
|
+
color: #ffffff;
|
|
41
|
+
box-shadow: 0 4px 6px -1px rgba(14, 165, 233, 0.2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&--slate {
|
|
45
|
+
background-color: #334155;
|
|
46
|
+
color: #ffffff;
|
|
47
|
+
box-shadow: 0 4px 6px -1px rgba(51, 65, 85, 0.2);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -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,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;
|
package/cli/audit/autofix.js
CHANGED
|
@@ -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({
|