@chem-x/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.
@@ -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
+ });
@@ -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
- lines.push('');
111
- lines.push(buildAgentCommandsSection());
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
- lines.push('');
153
- lines.push(buildAgentCommandsSection());
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
- lines.push('');
195
- lines.push(buildAgentCommandsSection());
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
- lines.push('');
224
- lines.push(buildAgentCommandsSection());
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
- lines.push('');
256
- lines.push(buildAgentCommandsSection());
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
- lines.push('');
294
- lines.push(buildAgentCommandsSection());
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
- lines.push('');
337
- lines.push(buildAgentCommandsSection());
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 + sections.join('\n\n---\n\n');
382
- return deduplicateRolePreambles(rawPrompt);
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
  });
@@ -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;