@chemx/starter-kit 26.9.16-41 → 26.9.17-309
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 +4 -0
- package/assets/chemx-starter-kit-logo.gif +0 -0
- package/blueprints/_view-template.scss +24 -0
- package/blueprints/atoms/a-button.vue +43 -0
- package/blueprints/molecule-capsule/_m-sample-card.scss +75 -0
- package/blueprints/molecule-capsule/index.ts +2 -0
- package/blueprints/molecule-capsule/m-chemx-badge/_m-chemx-badge.scss +76 -0
- package/blueprints/molecule-capsule/m-chemx-badge/index.ts +3 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.tsx +39 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.vue +42 -0
- package/blueprints/molecule-capsule/m-chemx-badge/types.d.ts +12 -0
- package/blueprints/molecule-capsule/m-sample-card.spec.ts +74 -0
- package/blueprints/molecule-capsule/m-sample-card.tsx +72 -0
- package/blueprints/molecule-capsule/m-tab-button/_m-tab-button.scss +49 -0
- package/blueprints/molecule-capsule/m-tab-button/index.ts +3 -0
- package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +43 -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/autofix.js +4 -5
- 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/roadmap.js +5 -20
- package/cli/audit/rules-predicates.js +3 -2
- package/cli/audit/rules.js +4 -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 +47 -22
- 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 +9 -0
- package/cli/index.js +44 -7
- package/cli/installer-templates.js +67 -13
- package/cli/license.js +22 -1
- package/cli/mcp/installer.js +31 -21
- package/cli/mcp/resources.js +114 -7
- package/cli/mcp/server.spec.js +34 -0
- package/cli/mcp/tools.js +14 -4
- 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 +19 -3
- package/cli/terminal.js +36 -1
- package/docs/CHANGELOG.md +10 -0
- package/package.json +8 -5
- package/scripts/postinstall.mjs +27 -0
- package/scripts/pre-commit.sh +135 -0
- package/scripts/publish-both.mjs +108 -0
|
@@ -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
|
+
|
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({
|
|
@@ -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());
|
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
|
};
|
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);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
const SOURCE_FILE_PATTERN = /\.(tsx|ts|jsx|js|vue|svelte)$/;
|
|
5
|
+
const EXCLUDED_PATTERNS = ['.test.', '.spec.', '.min.', '.d.ts'];
|
|
6
|
+
|
|
7
|
+
export const isSourceFilePath = (filePath) => {
|
|
8
|
+
const isExtensionValid = SOURCE_FILE_PATTERN.test(filePath);
|
|
9
|
+
if (!isExtensionValid) return false;
|
|
10
|
+
const isExcluded = EXCLUDED_PATTERNS.some((pat) => filePath.includes(pat));
|
|
11
|
+
return !isExcluded;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const parseGitStatusOutput = (output) => {
|
|
15
|
+
const lines = output.split('\n');
|
|
16
|
+
const files = [];
|
|
17
|
+
|
|
18
|
+
for (const rawLine of lines) {
|
|
19
|
+
const trimmed = rawLine.trim();
|
|
20
|
+
if (!trimmed) continue;
|
|
21
|
+
const cleanPath = trimmed.replace(/^[MADRCU?! ]+\s+/, '').trim().replace(/^"|"$/g, '');
|
|
22
|
+
if (cleanPath) {
|
|
23
|
+
files.push(cleanPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return files;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getGitChangedFiles = (cwd = process.cwd()) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = spawnSync('git', ['status', '--porcelain', '-u'], { cwd, encoding: 'utf-8' });
|
|
33
|
+
const isSuccess = result.status === 0;
|
|
34
|
+
if (!isSuccess || !result.stdout) return [];
|
|
35
|
+
return parseGitStatusOutput(result.stdout);
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const resolveGitAuditScope = (cwd = process.cwd()) => {
|
|
42
|
+
const rawFiles = getGitChangedFiles(cwd);
|
|
43
|
+
const existingSourceFiles = rawFiles.filter((relPath) => {
|
|
44
|
+
const isSource = isSourceFilePath(relPath);
|
|
45
|
+
const exists = fs.existsSync(relPath);
|
|
46
|
+
return isSource && exists;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const hasChangedFiles = existingSourceFiles.length > 0;
|
|
50
|
+
return {
|
|
51
|
+
ok: hasChangedFiles,
|
|
52
|
+
files: existingSourceFiles
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { hasGum, gumChoose, gumInput, promptQuestion } from './terminal.js';
|
|
4
|
+
import { resolveGitAuditScope } from './audit-preflight-git.js';
|
|
5
|
+
|
|
6
|
+
const CANDIDATE_NAMES = ['src', 'app', 'cli', 'components', 'lib', 'packages', 'modules'];
|
|
7
|
+
|
|
8
|
+
export const detectCandidateDirectories = (cwd = process.cwd()) => {
|
|
9
|
+
return CANDIDATE_NAMES.filter((name) => {
|
|
10
|
+
const fullPath = path.resolve(cwd, name);
|
|
11
|
+
return fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory();
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const promptSpecificDirectory = async (cwd) => {
|
|
16
|
+
const candidates = detectCandidateDirectories(cwd);
|
|
17
|
+
const options = [...candidates.map((dir) => `📂 ${dir}`), '✏️ Custom directory path...'];
|
|
18
|
+
|
|
19
|
+
let choice = '';
|
|
20
|
+
if (hasGum()) {
|
|
21
|
+
choice = gumChoose(options, 'Select Target Directory:');
|
|
22
|
+
} else {
|
|
23
|
+
process.stdout.write('\n' + options.map((opt, i) => ` ${i + 1}. ${opt}`).join('\n') + '\n\n');
|
|
24
|
+
const ans = await promptQuestion(`Select directory [1-${options.length}]: `);
|
|
25
|
+
const index = parseInt(ans, 10) - 1;
|
|
26
|
+
choice = options[index] || '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isCustomPath = !choice || choice.includes('Custom');
|
|
30
|
+
if (isCustomPath) {
|
|
31
|
+
if (hasGum()) {
|
|
32
|
+
return gumInput('Target directory path:', 'src') || 'src';
|
|
33
|
+
}
|
|
34
|
+
const entered = await promptQuestion('Target directory path [src]: ');
|
|
35
|
+
return entered || 'src';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const matched = candidates.find((dir) => choice.includes(dir));
|
|
39
|
+
return matched || 'src';
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const promptAuditScope = async (defaultDir, rawArgs, cwd = process.cwd()) => {
|
|
43
|
+
const hasGitFlag = rawArgs.includes('--git') || rawArgs.includes('--changed');
|
|
44
|
+
if (hasGitFlag) {
|
|
45
|
+
const gitScope = resolveGitAuditScope(cwd);
|
|
46
|
+
return { targetDir: defaultDir, fileList: gitScope.ok ? gitScope.files : null };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const scopeOptions = [
|
|
50
|
+
'📁 Whole Project (Scan entire workspace)',
|
|
51
|
+
'📂 Specific Directory (Select target folder)',
|
|
52
|
+
'🌿 Git Changed Files (Scan modified or staged files only)'
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
let scopeChoice = '';
|
|
56
|
+
if (hasGum()) {
|
|
57
|
+
scopeChoice = gumChoose(scopeOptions, 'Select Audit Scope:');
|
|
58
|
+
} else {
|
|
59
|
+
process.stdout.write('\n' + scopeOptions.map((opt, i) => ` ${i + 1}. ${opt}`).join('\n') + '\n\n');
|
|
60
|
+
const ans = await promptQuestion(`Select audit scope [1-${scopeOptions.length}]: `);
|
|
61
|
+
const index = parseInt(ans, 10) - 1;
|
|
62
|
+
scopeChoice = scopeOptions[index] || '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const isCancelled = !scopeChoice;
|
|
66
|
+
if (isCancelled) return null;
|
|
67
|
+
|
|
68
|
+
const isGit = scopeChoice.includes('Git');
|
|
69
|
+
if (isGit) {
|
|
70
|
+
const gitScope = resolveGitAuditScope(cwd);
|
|
71
|
+
const hasFiles = gitScope.ok;
|
|
72
|
+
if (!hasFiles) {
|
|
73
|
+
process.stdout.write('\n\x1b[33m⚠ No git modified or staged source files found. Auditing whole project.\x1b[0m\n');
|
|
74
|
+
return { targetDir: defaultDir, fileList: null };
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write(`\n\x1b[32m✔ Focused on ${gitScope.files.length} git modified/staged file(s).\x1b[0m\n`);
|
|
77
|
+
return { targetDir: defaultDir, fileList: gitScope.files };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isSpecific = scopeChoice.includes('Specific');
|
|
81
|
+
if (isSpecific) {
|
|
82
|
+
const chosenDir = await promptSpecificDirectory(cwd);
|
|
83
|
+
return { targetDir: chosenDir, fileList: null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { targetDir: defaultDir, fileList: null };
|
|
87
|
+
};
|