@chemx/starter-kit 26.9.14-205 → 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/LICENSE +21 -0
- package/README.md +15 -3
- 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 +134 -32
- package/cli/audit/prompts.spec.js +316 -0
- package/cli/audit/roadmap.js +10 -0
- package/cli/audit/rules.js +12 -1
- package/cli/audit/types.d.ts +9 -2
- package/cli/audit.js +4 -0
- package/cli/generator-templates.js +257 -41
- package/cli/generator.d.ts +14 -1
- package/cli/generator.js +236 -73
- package/cli/generator.spec.js +125 -0
- package/cli/help.js +40 -0
- package/cli/index.js +86 -11
- package/cli/installer-templates.js +1 -1
- package/cli/installer.js +93 -7
- package/cli/license.js +6 -1
- 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/mutators.js +287 -0
- package/cli/mutators.spec.js +176 -0
- package/cli/navigator-actions.js +19 -1
- package/cli/navigator-menu.js +11 -0
- package/cli/navigator.js +14 -0
- package/cli/patcher.js +110 -0
- package/cli/project-detector.js +127 -0
- package/cli/project-detector.spec.js +104 -0
- package/cli/reader.js +319 -0
- package/cli/search-ast.js +158 -0
- package/cli/search-commands.js +384 -0
- package/cli/search-db.js +262 -0
- package/cli/search-queries.js +236 -0
- package/cli/search-schema.js +147 -0
- package/cli/search.d.ts +171 -0
- package/cli/search.js +309 -0
- package/cli/search.spec.js +219 -0
- package/cli/theme.js +1 -0
- package/docs/CHANGELOG.md +46 -0
- package/package.json +27 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xopher Pollard / Chemical X Protocol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -30,9 +30,21 @@ starter-kit/
|
|
|
30
30
|
└── index.js # Interactive capsule generator
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Release Versioning: Minute-Precision CalVer
|
|
34
|
+
|
|
35
|
+
This package adheres to **Minute-Precision Calendar Versioning** (`YY.MM.DD-MMMM`):
|
|
36
|
+
- `YY.MM.DD`: Release date (e.g. `26.9.14` for Sept 14, 2026).
|
|
37
|
+
- `MMMM`: Minute of the day (0 to 1439).
|
|
38
|
+
|
|
39
|
+
Because autonomous coding agent models update frequently, this package is continuously integrated and deployed via automated CI whenever new agent directives, AST checks, or framework rules are tuned. Rapid version iterations reflect active, daily alignment rather than breaking SemVer shifts.
|
|
40
|
+
|
|
41
|
+
> [!NOTE]
|
|
42
|
+
> Download statistics on npm reflect continuous automated test matrix verification and test runner execution across automated environments.
|
|
43
|
+
|
|
33
44
|
---
|
|
34
45
|
|
|
35
|
-
##
|
|
46
|
+
## License
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
Core CLI tools and capsule generators are distributed under the **MIT License**.
|
|
49
|
+
Private production monorepos and extended starter suites are unlocked for verified GitHub Sponsors.
|
|
50
|
+
Explore sponsorship details at [https://chemicalx.xophz.com](https://chemicalx.xophz.com).
|
|
@@ -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
|
+
});
|