@chemx/starter-kit 26.9.14-1358 → 26.9.16-41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/audit/autofix.js +177 -0
- package/cli/audit/autofix.spec.js +85 -0
- package/cli/audit/pattern-detector.js +145 -0
- package/cli/audit/pattern-detector.spec.js +61 -0
- package/cli/audit/prompts.js +42 -16
- package/cli/audit/prompts.spec.js +25 -0
- package/cli/audit/rules.js +12 -1
- package/cli/generator-templates.js +67 -29
- package/cli/generator.d.ts +14 -1
- package/cli/generator.js +143 -84
- package/cli/generator.spec.js +23 -1
- package/cli/help.js +32 -0
- package/cli/index.js +31 -1
- package/cli/installer.js +24 -11
- package/cli/mcp/index.js +39 -0
- package/cli/mcp/installer.js +182 -0
- package/cli/mcp/installer.spec.js +97 -0
- package/cli/mcp/prompts.js +102 -0
- package/cli/mcp/resources.js +114 -0
- package/cli/mcp/server.js +259 -0
- package/cli/mcp/server.spec.js +371 -0
- package/cli/mcp/tools.js +352 -0
- package/cli/mcp/types.d.ts +88 -0
- package/cli/mcp.js +326 -0
- package/cli/patcher.js +110 -0
- package/cli/project-detector.js +27 -0
- package/cli/project-detector.spec.js +26 -1
- package/cli/reader.js +319 -0
- package/cli/search.spec.js +5 -5
- package/docs/CHANGELOG.md +31 -0
- package/package.json +12 -4
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const IGNORED_DIRS = new Set([
|
|
5
|
+
'node_modules',
|
|
6
|
+
'dist',
|
|
7
|
+
'build',
|
|
8
|
+
'vendor',
|
|
9
|
+
'.git',
|
|
10
|
+
'.next',
|
|
11
|
+
'.turbo',
|
|
12
|
+
'.cache'
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const FIXABLE_EXTENSIONS = new Set(['.tsx', '.ts', '.jsx', '.js', '.vue', '.md', '.scss', '.css']);
|
|
16
|
+
|
|
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'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const RESIDUE_REGEX = new RegExp(`\\b(?:${RESIDUE_PATTERNS.join('|')})\\b`, 'i');
|
|
25
|
+
const PREAMBLE_REGEX = /\b(?:here(?:'s| is) the (?:complete|updated|refactored|full) (?:code|implementation|file|component|version))\b/i;
|
|
26
|
+
const TRUNCATION_REGEX = /^\s*\/\/\s*\.\.\.\s*(?:existing|rest of|remaining)\s+(?:code|implementation|logic|imports)/i;
|
|
27
|
+
const MARKDOWN_FENCE_REGEX = /^\s*```(?:typescript|javascript|tsx|jsx|vue|js|ts|html|css|scss)?\s*$/;
|
|
28
|
+
|
|
29
|
+
export const autofixContent = (content, options = {}) => {
|
|
30
|
+
const allowedRules = options.rules ? new Set(options.rules) : null;
|
|
31
|
+
const shouldFix = (rule) => !allowedRules || allowedRules.has(rule);
|
|
32
|
+
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
const fixes = [];
|
|
35
|
+
const resultLines = [];
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
let line = lines[i];
|
|
39
|
+
const lineNumber = i + 1;
|
|
40
|
+
let shouldDropLine = false;
|
|
41
|
+
|
|
42
|
+
// 1. Leaked markdown fence removal
|
|
43
|
+
if (shouldFix('AI_SLOP_CONVERSATIONAL_ARTIFACT') && MARKDOWN_FENCE_REGEX.test(line)) {
|
|
44
|
+
fixes.push({
|
|
45
|
+
line: lineNumber,
|
|
46
|
+
rule: 'AI_SLOP_CONVERSATIONAL_ARTIFACT',
|
|
47
|
+
action: 'Removed leaked markdown code fence'
|
|
48
|
+
});
|
|
49
|
+
shouldDropLine = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Pure conversational preamble or residue comment removal
|
|
53
|
+
if (!shouldDropLine && shouldFix('AI_SLOP_CONVERSATIONAL_ARTIFACT')) {
|
|
54
|
+
const isComment = /^\s*(?:\/\/|\/\*|<!--)/.test(line);
|
|
55
|
+
if (isComment && (PREAMBLE_REGEX.test(line) || RESIDUE_REGEX.test(line))) {
|
|
56
|
+
fixes.push({
|
|
57
|
+
line: lineNumber,
|
|
58
|
+
rule: 'AI_SLOP_CONVERSATIONAL_ARTIFACT',
|
|
59
|
+
action: 'Removed AI conversational residue comment'
|
|
60
|
+
});
|
|
61
|
+
shouldDropLine = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. AI lazy placeholder comment removal
|
|
66
|
+
if (!shouldDropLine && shouldFix('AI_SLOP_LAZY_PLACEHOLDER') && TRUNCATION_REGEX.test(line)) {
|
|
67
|
+
fixes.push({
|
|
68
|
+
line: lineNumber,
|
|
69
|
+
rule: 'AI_SLOP_LAZY_PLACEHOLDER',
|
|
70
|
+
action: 'Removed lazy AI truncation placeholder comment'
|
|
71
|
+
});
|
|
72
|
+
shouldDropLine = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. Typography em dash replacement
|
|
76
|
+
if (!shouldDropLine && shouldFix('TYPOGRAPHY_EM_DASH') && line.includes('\u2014')) {
|
|
77
|
+
line = line.replace(/\u2014/g, '-');
|
|
78
|
+
fixes.push({
|
|
79
|
+
line: lineNumber,
|
|
80
|
+
rule: 'TYPOGRAPHY_EM_DASH',
|
|
81
|
+
action: 'Replaced em dash with standard hyphen'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!shouldDropLine) {
|
|
86
|
+
resultLines.push(line);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
fixedContent: resultLines.join('\n'),
|
|
92
|
+
fixes
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const collectFiles = (targetPath, baseDir) => {
|
|
97
|
+
const stat = fs.statSync(targetPath);
|
|
98
|
+
if (stat.isFile()) {
|
|
99
|
+
return [targetPath];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let files = [];
|
|
103
|
+
const entries = fs.readdirSync(targetPath, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
106
|
+
const fullPath = path.join(targetPath, entry.name);
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
files = files.concat(collectFiles(fullPath, baseDir));
|
|
109
|
+
} else if (FIXABLE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
110
|
+
files.push(fullPath);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return files;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const runAutofix = (targetPath, options = {}) => {
|
|
117
|
+
const cwd = options.cwd || process.cwd();
|
|
118
|
+
const resolvedTarget = path.resolve(cwd, targetPath || 'src');
|
|
119
|
+
const dryRun = Boolean(options.dryRun);
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(resolvedTarget)) {
|
|
122
|
+
return {
|
|
123
|
+
target: targetPath || 'src',
|
|
124
|
+
dryRun,
|
|
125
|
+
filesScanned: 0,
|
|
126
|
+
filesChanged: 0,
|
|
127
|
+
totalFixes: 0,
|
|
128
|
+
fixes: []
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const files = collectFiles(resolvedTarget, cwd);
|
|
133
|
+
const allFixes = [];
|
|
134
|
+
let filesChanged = 0;
|
|
135
|
+
|
|
136
|
+
for (const filePath of files) {
|
|
137
|
+
const relPath = path.relative(cwd, filePath);
|
|
138
|
+
const originalContent = fs.readFileSync(filePath, 'utf-8');
|
|
139
|
+
const { fixedContent, fixes } = autofixContent(originalContent, options);
|
|
140
|
+
|
|
141
|
+
if (fixes.length > 0) {
|
|
142
|
+
filesChanged += 1;
|
|
143
|
+
for (const fix of fixes) {
|
|
144
|
+
allFixes.push({
|
|
145
|
+
file: relPath,
|
|
146
|
+
line: fix.line,
|
|
147
|
+
rule: fix.rule,
|
|
148
|
+
action: fix.action
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!dryRun) {
|
|
153
|
+
fs.writeFileSync(filePath, fixedContent, 'utf-8');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Token optimization: cap fixes list to top 15 samples if very large
|
|
159
|
+
const MAX_REPORTED_FIXES = 15;
|
|
160
|
+
const isCapped = allFixes.length > MAX_REPORTED_FIXES;
|
|
161
|
+
const reportedFixes = isCapped ? allFixes.slice(0, MAX_REPORTED_FIXES) : allFixes;
|
|
162
|
+
|
|
163
|
+
const result = {
|
|
164
|
+
target: targetPath || 'src',
|
|
165
|
+
dryRun,
|
|
166
|
+
filesScanned: files.length,
|
|
167
|
+
filesChanged,
|
|
168
|
+
totalFixes: allFixes.length,
|
|
169
|
+
fixes: reportedFixes
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (isCapped) {
|
|
173
|
+
result.omittedFixesCount = allFixes.length - MAX_REPORTED_FIXES;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result;
|
|
177
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { autofixContent, runAutofix } from './autofix.js';
|
|
7
|
+
|
|
8
|
+
test('autofixContent: replaces em dash with standard hyphen', () => {
|
|
9
|
+
const input = 'const title = "Welcome — User Dashboard";\n// Notes — see section 3';
|
|
10
|
+
const { fixedContent, fixes } = autofixContent(input);
|
|
11
|
+
|
|
12
|
+
assert.equal(fixes.length, 2);
|
|
13
|
+
assert.equal(fixes[0].rule, 'TYPOGRAPHY_EM_DASH');
|
|
14
|
+
assert.equal(fixedContent, 'const title = "Welcome - User Dashboard";\n// Notes - see section 3');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('autofixContent: removes leaked markdown code fences', () => {
|
|
18
|
+
const input = '```typescript\nimport { ref } from "vue";\n```';
|
|
19
|
+
const { fixedContent, fixes } = autofixContent(input);
|
|
20
|
+
|
|
21
|
+
assert.equal(fixes.length, 2);
|
|
22
|
+
assert.equal(fixes[0].rule, 'AI_SLOP_CONVERSATIONAL_ARTIFACT');
|
|
23
|
+
assert.equal(fixes[1].rule, 'AI_SLOP_CONVERSATIONAL_ARTIFACT');
|
|
24
|
+
assert.equal(fixedContent, 'import { ref } from "vue";');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('autofixContent: removes conversational residue comments and preambles', () => {
|
|
28
|
+
const input = [
|
|
29
|
+
'// Here is the complete code:',
|
|
30
|
+
'export const add = (a, b) => a + b;',
|
|
31
|
+
'// Hope this helps! Feel free to tweak as needed.'
|
|
32
|
+
].join('\n');
|
|
33
|
+
|
|
34
|
+
const { fixedContent, fixes } = autofixContent(input);
|
|
35
|
+
assert.equal(fixes.length, 2);
|
|
36
|
+
assert.equal(fixes[0].rule, 'AI_SLOP_CONVERSATIONAL_ARTIFACT');
|
|
37
|
+
assert.equal(fixes[1].rule, 'AI_SLOP_CONVERSATIONAL_ARTIFACT');
|
|
38
|
+
assert.equal(fixedContent, 'export const add = (a, b) => a + b;');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('autofixContent: removes lazy truncation comments', () => {
|
|
42
|
+
const input = [
|
|
43
|
+
'export const config = {',
|
|
44
|
+
' // ... existing code',
|
|
45
|
+
' timeout: 5000',
|
|
46
|
+
'};'
|
|
47
|
+
].join('\n');
|
|
48
|
+
|
|
49
|
+
const { fixedContent, fixes } = autofixContent(input);
|
|
50
|
+
assert.equal(fixes.length, 1);
|
|
51
|
+
assert.equal(fixes[0].rule, 'AI_SLOP_LAZY_PLACEHOLDER');
|
|
52
|
+
assert.equal(fixedContent, 'export const config = {\n timeout: 5000\n};');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('autofixContent: respects rule filtering', () => {
|
|
56
|
+
const input = '```\nconst x = "foo — bar";';
|
|
57
|
+
const { fixedContent, fixes } = autofixContent(input, { rules: ['TYPOGRAPHY_EM_DASH'] });
|
|
58
|
+
|
|
59
|
+
assert.equal(fixes.length, 1);
|
|
60
|
+
assert.equal(fixes[0].rule, 'TYPOGRAPHY_EM_DASH');
|
|
61
|
+
assert.ok(fixedContent.includes('```'));
|
|
62
|
+
assert.ok(fixedContent.includes('foo - bar'));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('runAutofix: handles dryRun and file write correctly', () => {
|
|
66
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-autofix-test-'));
|
|
67
|
+
const testFile = path.join(tempDir, 'test.ts');
|
|
68
|
+
fs.writeFileSync(testFile, 'const msg = "test — em dash";\n// hope this helps', 'utf-8');
|
|
69
|
+
|
|
70
|
+
// Dry run: reports fixes without modifying file
|
|
71
|
+
const dryResult = runAutofix(testFile, { dryRun: true, cwd: tempDir });
|
|
72
|
+
assert.equal(dryResult.filesChanged, 1);
|
|
73
|
+
assert.equal(dryResult.totalFixes, 2);
|
|
74
|
+
assert.equal(dryResult.dryRun, true);
|
|
75
|
+
assert.equal(fs.readFileSync(testFile, 'utf-8'), 'const msg = "test — em dash";\n// hope this helps');
|
|
76
|
+
|
|
77
|
+
// Live run: writes changes
|
|
78
|
+
const liveResult = runAutofix(testFile, { dryRun: false, cwd: tempDir });
|
|
79
|
+
assert.equal(liveResult.filesChanged, 1);
|
|
80
|
+
assert.equal(liveResult.totalFixes, 2);
|
|
81
|
+
assert.equal(liveResult.dryRun, false);
|
|
82
|
+
assert.equal(fs.readFileSync(testFile, 'utf-8'), 'const msg = "test - em dash";');
|
|
83
|
+
|
|
84
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
85
|
+
});
|
|
@@ -73,6 +73,7 @@ export const createPatternRegistry = () => {
|
|
|
73
73
|
totalHits: bucket.occurrences.length,
|
|
74
74
|
hasHotspot,
|
|
75
75
|
impactScore,
|
|
76
|
+
uniqueFiles,
|
|
76
77
|
occurrences: bucket.occurrences
|
|
77
78
|
});
|
|
78
79
|
}
|
|
@@ -83,6 +84,150 @@ export const createPatternRegistry = () => {
|
|
|
83
84
|
return { record, resolveHarmonizationCandidates };
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
const VOID_ELEMENTS = new Set([
|
|
88
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
export const extractTemplateTokens = (html) => {
|
|
92
|
+
const tokens = [];
|
|
93
|
+
let i = 0;
|
|
94
|
+
let line = 1;
|
|
95
|
+
|
|
96
|
+
while (i < html.length) {
|
|
97
|
+
if (html[i] === '\n') {
|
|
98
|
+
line++;
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (html.startsWith('<!--', i)) {
|
|
104
|
+
const endComment = html.indexOf('-->', i + 4);
|
|
105
|
+
if (endComment === -1) break;
|
|
106
|
+
for (let c = i; c < endComment + 3; c++) {
|
|
107
|
+
if (html[c] === '\n') line++;
|
|
108
|
+
}
|
|
109
|
+
i = endComment + 3;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (html[i] === '<') {
|
|
114
|
+
const tagLine = line;
|
|
115
|
+
let j = i + 1;
|
|
116
|
+
const isClosing = html[j] === '/';
|
|
117
|
+
if (isClosing) j++;
|
|
118
|
+
|
|
119
|
+
const nameStart = j;
|
|
120
|
+
while (j < html.length && /[a-zA-Z0-9_-]/.test(html[j])) {
|
|
121
|
+
j++;
|
|
122
|
+
}
|
|
123
|
+
const tagName = html.slice(nameStart, j);
|
|
124
|
+
|
|
125
|
+
if (tagName) {
|
|
126
|
+
let isSelfClosing = false;
|
|
127
|
+
let inQuote = null;
|
|
128
|
+
|
|
129
|
+
while (j < html.length) {
|
|
130
|
+
const ch = html[j];
|
|
131
|
+
if (ch === '\n') line++;
|
|
132
|
+
|
|
133
|
+
if (inQuote) {
|
|
134
|
+
if (ch === inQuote && html[j - 1] !== '\\') {
|
|
135
|
+
inQuote = null;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
if (ch === '"' || ch === "'") {
|
|
139
|
+
inQuote = ch;
|
|
140
|
+
} else if (ch === '/' && html[j + 1] === '>') {
|
|
141
|
+
isSelfClosing = true;
|
|
142
|
+
j += 2;
|
|
143
|
+
break;
|
|
144
|
+
} else if (ch === '>') {
|
|
145
|
+
j++;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
j++;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const isVoid = VOID_ELEMENTS.has(tagName.toLowerCase());
|
|
153
|
+
tokens.push({
|
|
154
|
+
tag: tagName,
|
|
155
|
+
isClosing,
|
|
156
|
+
isSelfClosing: isSelfClosing || isVoid,
|
|
157
|
+
line: tagLine
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
i = j;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return tokens;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const buildTagTree = (tokens) => {
|
|
172
|
+
const root = { tag: 'ROOT', line: 1, children: [] };
|
|
173
|
+
const stack = [root];
|
|
174
|
+
|
|
175
|
+
for (const token of tokens) {
|
|
176
|
+
if (token.isClosing) {
|
|
177
|
+
for (let s = stack.length - 1; s > 0; s--) {
|
|
178
|
+
if (stack[s].tag === token.tag) {
|
|
179
|
+
stack.length = s;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
const node = { tag: token.tag, line: token.line, children: [] };
|
|
185
|
+
stack[stack.length - 1].children.push(node);
|
|
186
|
+
if (!token.isSelfClosing) {
|
|
187
|
+
stack.push(node);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return root.children;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const getHierarchy = (node, depth = 0) => {
|
|
196
|
+
if (depth > 2 || !node) return '';
|
|
197
|
+
const childHierarchy = (node.children || [])
|
|
198
|
+
.map((c) => getHierarchy(c, depth + 1))
|
|
199
|
+
.filter(Boolean);
|
|
200
|
+
if (childHierarchy.length === 0) return node.tag;
|
|
201
|
+
return `${node.tag}>(${childHierarchy.join('+')})`;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const recordTemplatePatterns = (registry, templateHtml, relativePath, startLineOffset = 0) => {
|
|
205
|
+
if (!registry || !templateHtml) return;
|
|
206
|
+
const tokens = extractTemplateTokens(templateHtml);
|
|
207
|
+
const nodes = buildTagTree(tokens);
|
|
208
|
+
|
|
209
|
+
const traverseNodes = (nodeList) => {
|
|
210
|
+
for (const node of nodeList) {
|
|
211
|
+
if (node.children && node.children.length >= MIN_CHILD_NODES) {
|
|
212
|
+
const hierarchy = getHierarchy(node);
|
|
213
|
+
if (hierarchy && hierarchy.includes('>')) {
|
|
214
|
+
registry.record('UI_STRUCTURE', hierarchy, {
|
|
215
|
+
filePath: relativePath,
|
|
216
|
+
line: node.line + startLineOffset,
|
|
217
|
+
detail: hierarchy
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (node.children && node.children.length > 0) {
|
|
222
|
+
traverseNodes(node.children);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
traverseNodes(nodes);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
|
|
86
231
|
export const createPatternVisitors = (registry, relativePath) => {
|
|
87
232
|
if (!registry) return {};
|
|
88
233
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
createPatternRegistry,
|
|
5
|
+
extractTemplateTokens,
|
|
6
|
+
buildTagTree,
|
|
7
|
+
recordTemplatePatterns
|
|
8
|
+
} from './pattern-detector.js';
|
|
9
|
+
|
|
10
|
+
test('extractTemplateTokens: extracts tags, ignoring comments and handling quotes', () => {
|
|
11
|
+
const html = `
|
|
12
|
+
<!-- comment -->
|
|
13
|
+
<v-card class="elevation-2" :title="count > 0 ? 'yes' : 'no'">
|
|
14
|
+
<v-card-title>Title</v-card-title>
|
|
15
|
+
<x-btn />
|
|
16
|
+
</v-card>
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const tokens = extractTemplateTokens(html);
|
|
20
|
+
assert.equal(tokens[0].tag, 'v-card');
|
|
21
|
+
assert.equal(tokens[0].isClosing, false);
|
|
22
|
+
assert.equal(tokens[1].tag, 'v-card-title');
|
|
23
|
+
assert.equal(tokens[2].tag, 'v-card-title');
|
|
24
|
+
assert.equal(tokens[2].isClosing, true);
|
|
25
|
+
assert.equal(tokens[3].tag, 'x-btn');
|
|
26
|
+
assert.equal(tokens[3].isSelfClosing, true);
|
|
27
|
+
assert.equal(tokens[4].tag, 'v-card');
|
|
28
|
+
assert.equal(tokens[4].isClosing, true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('buildTagTree and recordTemplatePatterns: captures UI_STRUCTURE from Vue template', () => {
|
|
32
|
+
const registry = createPatternRegistry();
|
|
33
|
+
const template1 = `
|
|
34
|
+
<template>
|
|
35
|
+
<v-sheet class="panel">
|
|
36
|
+
<x-btn color="primary">Save</x-btn>
|
|
37
|
+
<x-btn color="secondary">Cancel</x-btn>
|
|
38
|
+
</v-sheet>
|
|
39
|
+
</template>
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const template2 = `
|
|
43
|
+
<template>
|
|
44
|
+
<v-sheet class="controls">
|
|
45
|
+
<x-btn color="error">Delete</x-btn>
|
|
46
|
+
<x-btn color="warning">Reset</x-btn>
|
|
47
|
+
</v-sheet>
|
|
48
|
+
</template>
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
recordTemplatePatterns(registry, template1, 'src/components/A.vue');
|
|
52
|
+
recordTemplatePatterns(registry, template2, 'src/components/B.vue');
|
|
53
|
+
|
|
54
|
+
const candidates = registry.resolveHarmonizationCandidates();
|
|
55
|
+
const uiCandidate = candidates.find((c) => c.type === 'UI_STRUCTURE');
|
|
56
|
+
|
|
57
|
+
assert.ok(uiCandidate, 'Expected UI_STRUCTURE candidate to be found');
|
|
58
|
+
assert.equal(uiCandidate.detail, 'v-sheet>(x-btn+x-btn)');
|
|
59
|
+
assert.equal(uiCandidate.fileCount, 2);
|
|
60
|
+
assert.deepEqual(uiCandidate.uniqueFiles.sort(), ['src/components/A.vue', 'src/components/B.vue']);
|
|
61
|
+
});
|
package/cli/audit/prompts.js
CHANGED
|
@@ -107,8 +107,10 @@ export const buildGradeFPrompt = (report, options = {}) => {
|
|
|
107
107
|
lines.push('4. Zero synthetic or mock data: return live data or explicit empty states.');
|
|
108
108
|
lines.push('5. Zero render-hack setTimeout: replace with await nextTick() or flush: post.');
|
|
109
109
|
lines.push('6. Output surgical diffs or modular replacements. Preserve existing external exports.');
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
if (!isSubSection) {
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(buildAgentCommandsSection());
|
|
113
|
+
}
|
|
112
114
|
|
|
113
115
|
return lines.join('\n');
|
|
114
116
|
};
|
|
@@ -149,8 +151,10 @@ export const buildGradeDPrompt = (report, options = {}) => {
|
|
|
149
151
|
lines.push('1. Hook Saturation: Extract component hooks into dedicated domain composables adhering to the 3-5 property return limit (State + Status + Actions).');
|
|
150
152
|
lines.push('2. Complex Control Flow: Break multi-clause conditionals into 2-stage atomic boolean variables with early-return guard clauses.');
|
|
151
153
|
lines.push('3. Zero breaking changes to caller APIs or existing props.');
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
if (!isSubSection) {
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push(buildAgentCommandsSection());
|
|
157
|
+
}
|
|
154
158
|
|
|
155
159
|
return lines.join('\n');
|
|
156
160
|
};
|
|
@@ -191,8 +195,10 @@ export const buildGradeCPrompt = (report, options = {}) => {
|
|
|
191
195
|
lines.push('1. Zero raw inline styles: Replace style={{...}} with atom props, SCSS mixins (@include glass), or scoped BEM classes.');
|
|
192
196
|
lines.push('2. Extract anonymous inline callbacks into named functions before passing as props.');
|
|
193
197
|
lines.push('3. Co-locate granular types (*.d.ts) inside feature capsules (< 100 lines of code). Avoid type monoliths.');
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
if (!isSubSection) {
|
|
199
|
+
lines.push('');
|
|
200
|
+
lines.push(buildAgentCommandsSection());
|
|
201
|
+
}
|
|
196
202
|
|
|
197
203
|
return lines.join('\n');
|
|
198
204
|
};
|
|
@@ -220,8 +226,10 @@ export const buildGradeBPrompt = (report, options = {}) => {
|
|
|
220
226
|
lines.push('1. Typography Hygiene: Replace all em dashes with standard hyphens (-) or colons (:).');
|
|
221
227
|
lines.push('2. Logging Hygiene: Remove unguarded console.log calls or route through central debug proxy.');
|
|
222
228
|
lines.push('3. FontAwesome Compliance: Remove text-* utility classes from icons; use :color prop or inline CSS.');
|
|
223
|
-
|
|
224
|
-
|
|
229
|
+
if (!isSubSection) {
|
|
230
|
+
lines.push('');
|
|
231
|
+
lines.push(buildAgentCommandsSection());
|
|
232
|
+
}
|
|
225
233
|
|
|
226
234
|
return lines.join('\n');
|
|
227
235
|
};
|
|
@@ -252,8 +260,10 @@ export const buildAiSlopPrompt = (report, options = {}) => {
|
|
|
252
260
|
lines.push('4. Paranoia Catch Wrappers: Replace shallow or empty catch blocks with intentional error propagation or ResultTuple ([data, error]).');
|
|
253
261
|
lines.push('5. Inline Utility Reinventions: Replace reinvented inline helper utilities with shared project utilities or native methods.');
|
|
254
262
|
lines.push('6. Type Widening: Eliminate lazy "any" type casts on error parameters or variables; enforce strict typing.');
|
|
255
|
-
|
|
256
|
-
|
|
263
|
+
if (!isSubSection) {
|
|
264
|
+
lines.push('');
|
|
265
|
+
lines.push(buildAgentCommandsSection());
|
|
266
|
+
}
|
|
257
267
|
|
|
258
268
|
return lines.join('\n');
|
|
259
269
|
};
|
|
@@ -290,8 +300,10 @@ export const buildHotspotsPrompt = (report, options = {}) => {
|
|
|
290
300
|
lines.push('5. Composable Return Contracts: Custom hooks/composables must strictly limit returns to 3 to 5 properties (State + Status + Actions).');
|
|
291
301
|
lines.push('6. Type Co-location: Co-locate granular types/*.d.ts files inside each feature capsule (< 100 lines per type file) instead of creating type monoliths.');
|
|
292
302
|
lines.push('7. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
293
|
-
|
|
294
|
-
|
|
303
|
+
if (!isSubSection) {
|
|
304
|
+
lines.push('');
|
|
305
|
+
lines.push(buildAgentCommandsSection());
|
|
306
|
+
}
|
|
295
307
|
|
|
296
308
|
return lines.join('\n');
|
|
297
309
|
};
|
|
@@ -333,12 +345,25 @@ export const buildPillarPrompt = (report, pillarName, options = {}) => {
|
|
|
333
345
|
lines.push('3. Molecular Capsule Limit: Maximum 100 lines per molecule capsule file.');
|
|
334
346
|
lines.push('4. Table-of-Contents Views: Top-level page views must be 10 to 20 line declarative templates assembling components via named slots.');
|
|
335
347
|
lines.push('5. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
336
|
-
|
|
337
|
-
|
|
348
|
+
if (!isSubSection) {
|
|
349
|
+
lines.push('');
|
|
350
|
+
lines.push(buildAgentCommandsSection());
|
|
351
|
+
}
|
|
338
352
|
|
|
339
353
|
return lines.join('\n');
|
|
340
354
|
};
|
|
341
355
|
|
|
356
|
+
export const deduplicateRefactoringCommands = (promptText) => {
|
|
357
|
+
if (typeof promptText !== 'string' || !promptText) return '';
|
|
358
|
+
const headerMarker = '### AI AGENT DISCOVERY & REFACTORING COMMANDS:';
|
|
359
|
+
const parts = promptText.split(headerMarker);
|
|
360
|
+
if (parts.length <= 2) return promptText;
|
|
361
|
+
|
|
362
|
+
const prefix = parts.slice(0, -1).join('').replace(/---\s*\n\s*---\s*\n/g, '---\n').trimEnd();
|
|
363
|
+
const lastSection = parts[parts.length - 1];
|
|
364
|
+
return `${prefix}\n\n${headerMarker}${lastSection}`;
|
|
365
|
+
};
|
|
366
|
+
|
|
342
367
|
export const deduplicateRolePreambles = (promptText, options = {}) => {
|
|
343
368
|
if (typeof promptText !== 'string' || !promptText) return '';
|
|
344
369
|
const { onlyIdentical = false } = options;
|
|
@@ -378,8 +403,9 @@ export const buildMasterPrompt = (report) => {
|
|
|
378
403
|
if (hasNoSections) return '';
|
|
379
404
|
|
|
380
405
|
const header = 'Act as a Principal Systems Architect. Execute a phased architectural refactoring of our codebase according to Chemical X Molecular Architecture Standards.\n\n';
|
|
381
|
-
const rawPrompt = header
|
|
382
|
-
|
|
406
|
+
const rawPrompt = `${header}${sections.join('\n\n---\n\n')}\n\n---\n\n${buildAgentCommandsSection()}`;
|
|
407
|
+
const dedupedRoles = deduplicateRolePreambles(rawPrompt);
|
|
408
|
+
return deduplicateRefactoringCommands(dedupedRoles);
|
|
383
409
|
};
|
|
384
410
|
|
|
385
411
|
export const formatPromptBox = (title, promptText) => {
|
|
@@ -2,6 +2,7 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import {
|
|
4
4
|
deduplicateRolePreambles,
|
|
5
|
+
deduplicateRefactoringCommands,
|
|
5
6
|
buildGradeFPrompt,
|
|
6
7
|
buildGradeDPrompt,
|
|
7
8
|
buildGradeCPrompt,
|
|
@@ -288,4 +289,28 @@ test('buildMasterPrompt: has single top-level role preamble and no repeated Act
|
|
|
288
289
|
// Ensure "Act as" occurs exactly once in the entire composite prompt
|
|
289
290
|
const matches = prompt.match(/Act as\b/g) || [];
|
|
290
291
|
assert.strictEqual(matches.length, 1, `Expected exactly 1 "Act as", found ${matches.length}`);
|
|
292
|
+
|
|
293
|
+
// Ensure refactoring commands section occurs exactly once
|
|
294
|
+
const commandMatches = prompt.match(/### AI AGENT DISCOVERY & REFACTORING COMMANDS:/g) || [];
|
|
295
|
+
assert.strictEqual(commandMatches.length, 1, `Expected exactly 1 refactoring commands section, found ${commandMatches.length}`);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('deduplicateRefactoringCommands: collapses multiple occurrences to single block', () => {
|
|
299
|
+
const header = '### AI AGENT DISCOVERY & REFACTORING COMMANDS:';
|
|
300
|
+
const input = [
|
|
301
|
+
'Section 1 content',
|
|
302
|
+
header,
|
|
303
|
+
'- Command A',
|
|
304
|
+
'---',
|
|
305
|
+
'Section 2 content',
|
|
306
|
+
header,
|
|
307
|
+
'- Command B'
|
|
308
|
+
].join('\n');
|
|
309
|
+
|
|
310
|
+
const result = deduplicateRefactoringCommands(input);
|
|
311
|
+
const matches = result.match(/### AI AGENT DISCOVERY & REFACTORING COMMANDS:/g) || [];
|
|
312
|
+
assert.strictEqual(matches.length, 1);
|
|
313
|
+
assert.ok(result.includes('Section 1 content'));
|
|
314
|
+
assert.ok(result.includes('Section 2 content'));
|
|
315
|
+
assert.ok(result.includes('- Command B'));
|
|
291
316
|
});
|
package/cli/audit/rules.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
checkExtendedTextPatterns,
|
|
19
19
|
createExtendedVisitors
|
|
20
20
|
} from './extended-visitors.js';
|
|
21
|
-
import { createPatternVisitors } from './pattern-detector.js';
|
|
21
|
+
import { createPatternVisitors, recordTemplatePatterns } from './pattern-detector.js';
|
|
22
22
|
|
|
23
23
|
export { PILLARS, RULE_REGISTRY };
|
|
24
24
|
|
|
@@ -102,6 +102,17 @@ 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
|
+
// Vue template structural pattern discovery
|
|
106
|
+
if (ext === '.vue' && options.patternRegistry) {
|
|
107
|
+
const templateMatch = content.match(/<template\b[^>]*>([\s\S]*?)<\/template>/i);
|
|
108
|
+
if (templateMatch) {
|
|
109
|
+
const templateContent = templateMatch[1];
|
|
110
|
+
const templateStartIndex = templateMatch.index || 0;
|
|
111
|
+
const startLineOffset = content.slice(0, templateStartIndex).split('\n').length - 1;
|
|
112
|
+
recordTemplatePatterns(options.patternRegistry, templateContent, relativePath, startLineOffset);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
105
116
|
const codeToParse = extractParseableCode(content, ext);
|
|
106
117
|
if (!codeToParse.trim()) {
|
|
107
118
|
return violations;
|