@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.
Files changed (43) hide show
  1. package/cli/audit/autofix.js +176 -0
  2. package/cli/audit/autofix.spec.js +85 -0
  3. package/cli/audit/pattern-detector.js +145 -0
  4. package/cli/audit/pattern-detector.spec.js +61 -0
  5. package/cli/audit/prompts.js +52 -28
  6. package/cli/audit/prompts.spec.js +25 -0
  7. package/cli/audit/roadmap.js +5 -20
  8. package/cli/audit/rules.js +16 -1
  9. package/cli/audit-preflight-git.js +54 -0
  10. package/cli/audit-preflight-scope.js +87 -0
  11. package/cli/audit-preflight.d.ts +16 -0
  12. package/cli/audit-preflight.js +85 -0
  13. package/cli/audit-preflight.spec.js +104 -0
  14. package/cli/audit.js +47 -22
  15. package/cli/generator-templates.js +67 -29
  16. package/cli/generator.d.ts +14 -1
  17. package/cli/generator.js +143 -84
  18. package/cli/generator.spec.js +23 -1
  19. package/cli/help.js +41 -0
  20. package/cli/index.js +71 -7
  21. package/cli/installer-templates.js +39 -6
  22. package/cli/installer.js +24 -11
  23. package/cli/mcp/index.js +39 -0
  24. package/cli/mcp/installer.js +192 -0
  25. package/cli/mcp/installer.spec.js +97 -0
  26. package/cli/mcp/prompts.js +102 -0
  27. package/cli/mcp/resources.js +114 -0
  28. package/cli/mcp/server.js +259 -0
  29. package/cli/mcp/server.spec.js +371 -0
  30. package/cli/mcp/tools.js +352 -0
  31. package/cli/mcp/types.d.ts +88 -0
  32. package/cli/mcp.js +326 -0
  33. package/cli/patcher.js +110 -0
  34. package/cli/postinstall.js +27 -0
  35. package/cli/project-detector.js +27 -0
  36. package/cli/project-detector.spec.js +26 -1
  37. package/cli/reader.js +333 -0
  38. package/cli/search.spec.js +5 -5
  39. package/docs/CHANGELOG.md +38 -0
  40. package/package.json +13 -4
  41. package/scripts/postinstall.mjs +27 -0
  42. package/scripts/pre-commit.sh +115 -0
  43. 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
+ });
@@ -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
  };
@@ -283,15 +293,16 @@ export const buildHotspotsPrompt = (report, options = {}) => {
283
293
  }
284
294
 
285
295
  lines.push('### STRICT EXECUTION RULES:');
286
- lines.push('1. Branch First: Change to a new branch prefixed with `chem-x/NAMEOFIMPROVEMENT` before making changes, then commit the branch and create a PR to main.');
287
- lines.push('2. 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.');
288
- lines.push('3. Molecular Limits: Molecule capsules must strictly remain under 100 lines per file.');
289
- lines.push('4. Table-of-Contents Views: Top-level page views must be 10 to 20 line declarative Table of Contents assembling molecules via named slots.');
290
- lines.push('5. Composable Return Contracts: Custom hooks/composables must strictly limit returns to 3 to 5 properties (State + Status + Actions).');
291
- 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
- lines.push('7. Zero breaking changes to external component APIs, route exports, or existing props.');
293
- lines.push('');
294
- lines.push(buildAgentCommandsSection());
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. Branch First: Change to a new branch prefixed with `chem-x/NAMEOFIMPROVEMENT` before making changes.');
332
- lines.push('2. Pre-Split Pattern Discovery: Survey cross-file patterns before slicing; extract canonical shared capsules first.');
333
- lines.push('3. Molecular Capsule Limit: Maximum 100 lines per molecule capsule file.');
334
- 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
- lines.push('5. Zero breaking changes to external component APIs, route exports, or existing props.');
336
- lines.push('');
337
- lines.push(buildAgentCommandsSection());
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 + sections.join('\n\n---\n\n');
382
- return deduplicateRolePreambles(rawPrompt);
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
  });