@chemx/starter-kit 26.9.14-1358 → 26.9.16-1249
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 +176 -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 +52 -28
- package/cli/audit/prompts.spec.js +25 -0
- package/cli/audit/roadmap.js +5 -20
- package/cli/audit/rules.js +16 -1
- 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 +104 -0
- package/cli/audit.js +47 -22
- 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 +41 -0
- package/cli/index.js +71 -7
- package/cli/installer-templates.js +39 -6
- package/cli/installer.js +24 -11
- package/cli/mcp/index.js +39 -0
- package/cli/mcp/installer.js +192 -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/postinstall.js +27 -0
- package/cli/project-detector.js +27 -0
- package/cli/project-detector.spec.js +26 -1
- package/cli/reader.js +333 -0
- package/cli/search.spec.js +5 -5
- package/docs/CHANGELOG.md +38 -0
- package/package.json +13 -4
- package/scripts/postinstall.mjs +27 -0
- package/scripts/pre-commit.sh +115 -0
- package/scripts/publish-both.mjs +108 -0
|
@@ -0,0 +1,176 @@
|
|
|
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'].join(' '),
|
|
19
|
+
['feel free', 'to tweak'].join(' '),
|
|
20
|
+
['let me know', 'if you need'].join(' '),
|
|
21
|
+
['as an ai', 'language model'].join(' ')
|
|
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
|
+
if (!shouldDropLine && shouldFix('TYPOGRAPHY_EM_DASH') && line.includes('\u2014')) {
|
|
76
|
+
line = line.replace(/\u2014/g, '-');
|
|
77
|
+
fixes.push({
|
|
78
|
+
line: lineNumber,
|
|
79
|
+
rule: 'TYPOGRAPHY_EM_DASH',
|
|
80
|
+
action: 'Replaced em dash with standard hyphen'
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!shouldDropLine) {
|
|
85
|
+
resultLines.push(line);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
fixedContent: resultLines.join('\n'),
|
|
91
|
+
fixes
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const collectFiles = (targetPath, baseDir) => {
|
|
96
|
+
const stat = fs.statSync(targetPath);
|
|
97
|
+
if (stat.isFile()) {
|
|
98
|
+
return [targetPath];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let files = [];
|
|
102
|
+
const entries = fs.readdirSync(targetPath, { withFileTypes: true });
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
105
|
+
const fullPath = path.join(targetPath, entry.name);
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
files = files.concat(collectFiles(fullPath, baseDir));
|
|
108
|
+
} else if (FIXABLE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
109
|
+
files.push(fullPath);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return files;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const runAutofix = (targetPath, options = {}) => {
|
|
116
|
+
const cwd = options.cwd || process.cwd();
|
|
117
|
+
const resolvedTarget = path.resolve(cwd, targetPath || 'src');
|
|
118
|
+
const dryRun = Boolean(options.dryRun);
|
|
119
|
+
|
|
120
|
+
if (!fs.existsSync(resolvedTarget)) {
|
|
121
|
+
return {
|
|
122
|
+
target: targetPath || 'src',
|
|
123
|
+
dryRun,
|
|
124
|
+
filesScanned: 0,
|
|
125
|
+
filesChanged: 0,
|
|
126
|
+
totalFixes: 0,
|
|
127
|
+
fixes: []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const files = collectFiles(resolvedTarget, cwd);
|
|
132
|
+
const allFixes = [];
|
|
133
|
+
let filesChanged = 0;
|
|
134
|
+
|
|
135
|
+
for (const filePath of files) {
|
|
136
|
+
const relPath = path.relative(cwd, filePath);
|
|
137
|
+
const originalContent = fs.readFileSync(filePath, 'utf-8');
|
|
138
|
+
const { fixedContent, fixes } = autofixContent(originalContent, options);
|
|
139
|
+
|
|
140
|
+
if (fixes.length > 0) {
|
|
141
|
+
filesChanged += 1;
|
|
142
|
+
for (const fix of fixes) {
|
|
143
|
+
allFixes.push({
|
|
144
|
+
file: relPath,
|
|
145
|
+
line: fix.line,
|
|
146
|
+
rule: fix.rule,
|
|
147
|
+
action: fix.action
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!dryRun) {
|
|
152
|
+
fs.writeFileSync(filePath, fixedContent, 'utf-8');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Token optimization: cap fixes list to top 15 samples if very large
|
|
158
|
+
const MAX_REPORTED_FIXES = 15;
|
|
159
|
+
const isCapped = allFixes.length > MAX_REPORTED_FIXES;
|
|
160
|
+
const reportedFixes = isCapped ? allFixes.slice(0, MAX_REPORTED_FIXES) : allFixes;
|
|
161
|
+
|
|
162
|
+
const result = {
|
|
163
|
+
target: targetPath || 'src',
|
|
164
|
+
dryRun,
|
|
165
|
+
filesScanned: files.length,
|
|
166
|
+
filesChanged,
|
|
167
|
+
totalFixes: allFixes.length,
|
|
168
|
+
fixes: reportedFixes
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (isCapped) {
|
|
172
|
+
result.omittedFixesCount = allFixes.length - MAX_REPORTED_FIXES;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
};
|
|
@@ -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
|
};
|
|
@@ -283,15 +293,16 @@ export const buildHotspotsPrompt = (report, options = {}) => {
|
|
|
283
293
|
}
|
|
284
294
|
|
|
285
295
|
lines.push('### STRICT EXECUTION RULES:');
|
|
286
|
-
lines.push('1.
|
|
287
|
-
lines.push('2.
|
|
288
|
-
lines.push('3.
|
|
289
|
-
lines.push('4.
|
|
290
|
-
lines.push('5.
|
|
291
|
-
lines.push('6.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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.');
|
|
302
|
+
if (!isSubSection) {
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push(buildAgentCommandsSection());
|
|
305
|
+
}
|
|
295
306
|
|
|
296
307
|
return lines.join('\n');
|
|
297
308
|
};
|
|
@@ -328,17 +339,29 @@ export const buildPillarPrompt = (report, pillarName, options = {}) => {
|
|
|
328
339
|
}
|
|
329
340
|
|
|
330
341
|
lines.push('### STRICT EXECUTION RULES:');
|
|
331
|
-
lines.push('1.
|
|
332
|
-
lines.push('2.
|
|
333
|
-
lines.push('3.
|
|
334
|
-
lines.push('4.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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.');
|
|
346
|
+
if (!isSubSection) {
|
|
347
|
+
lines.push('');
|
|
348
|
+
lines.push(buildAgentCommandsSection());
|
|
349
|
+
}
|
|
338
350
|
|
|
339
351
|
return lines.join('\n');
|
|
340
352
|
};
|
|
341
353
|
|
|
354
|
+
export const deduplicateRefactoringCommands = (promptText) => {
|
|
355
|
+
if (typeof promptText !== 'string' || !promptText) return '';
|
|
356
|
+
const headerMarker = '### AI AGENT DISCOVERY & REFACTORING COMMANDS:';
|
|
357
|
+
const parts = promptText.split(headerMarker);
|
|
358
|
+
if (parts.length <= 2) return promptText;
|
|
359
|
+
|
|
360
|
+
const prefix = parts.slice(0, -1).join('').replace(/---\s*\n\s*---\s*\n/g, '---\n').trimEnd();
|
|
361
|
+
const lastSection = parts[parts.length - 1];
|
|
362
|
+
return `${prefix}\n\n${headerMarker}${lastSection}`;
|
|
363
|
+
};
|
|
364
|
+
|
|
342
365
|
export const deduplicateRolePreambles = (promptText, options = {}) => {
|
|
343
366
|
if (typeof promptText !== 'string' || !promptText) return '';
|
|
344
367
|
const { onlyIdentical = false } = options;
|
|
@@ -378,8 +401,9 @@ export const buildMasterPrompt = (report) => {
|
|
|
378
401
|
if (hasNoSections) return '';
|
|
379
402
|
|
|
380
403
|
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
|
-
|
|
404
|
+
const rawPrompt = `${header}${sections.join('\n\n---\n\n')}\n\n---\n\n${buildAgentCommandsSection()}`;
|
|
405
|
+
const dedupedRoles = deduplicateRolePreambles(rawPrompt);
|
|
406
|
+
return deduplicateRefactoringCommands(dedupedRoles);
|
|
383
407
|
};
|
|
384
408
|
|
|
385
409
|
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
|
});
|