@chemx/create-chemx 26.9.16-41 → 26.9.16-668

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.
@@ -293,13 +293,12 @@ export const buildHotspotsPrompt = (report, options = {}) => {
293
293
  }
294
294
 
295
295
  lines.push('### STRICT EXECUTION RULES:');
296
- 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.');
297
- 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.');
298
- lines.push('3. Molecular Limits: Molecule capsules must strictly remain under 100 lines per file.');
299
- 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.');
300
- lines.push('5. Composable Return Contracts: Custom hooks/composables must strictly limit returns to 3 to 5 properties (State + Status + Actions).');
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.');
302
- lines.push('7. Zero breaking changes to external component APIs, route exports, or existing props.');
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.');
303
302
  if (!isSubSection) {
304
303
  lines.push('');
305
304
  lines.push(buildAgentCommandsSection());
@@ -340,11 +339,10 @@ export const buildPillarPrompt = (report, pillarName, options = {}) => {
340
339
  }
341
340
 
342
341
  lines.push('### STRICT EXECUTION RULES:');
343
- lines.push('1. Branch First: Change to a new branch prefixed with `chem-x/NAMEOFIMPROVEMENT` before making changes.');
344
- lines.push('2. Pre-Split Pattern Discovery: Survey cross-file patterns before slicing; extract canonical shared capsules first.');
345
- lines.push('3. Molecular Capsule Limit: Maximum 100 lines per molecule capsule file.');
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.');
347
- lines.push('5. Zero breaking changes to external component APIs, route exports, or existing props.');
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.');
348
346
  if (!isSubSection) {
349
347
  lines.push('');
350
348
  lines.push(buildAgentCommandsSection());
@@ -128,12 +128,6 @@ export const buildRemediationRoadmap = (report) => {
128
128
  target: '.git/hooks/pre-commit & .github/workflows/',
129
129
  action: 'Run npx chemx install to lock in line budgets and prevent future monolith regressions.',
130
130
  locations: []
131
- },
132
- {
133
- title: 'Branch, Commit & Pull Request Workflow',
134
- target: 'git checkout -b chem-x/NAMEOFIMPROVEMENT',
135
- action: 'Change to a branch prefixed with chem-x/NAMEOFIMPROVEMENT to make changes, commit the branch, and create a PR to main.',
136
- locations: []
137
131
  }
138
132
  ]
139
133
  });
@@ -202,13 +196,6 @@ export const buildSelfHealingRoadmapPrompt = (report) => {
202
196
 
203
197
  lines.push('Act as a Principal Systems Architect. Execute a self-healing architectural remediation of our codebase by following this strict multi-phase sequence in order:\n');
204
198
 
205
- lines.push('### MANDATORY GIT BRANCHING & PR WORKFLOW:');
206
- lines.push('1. Before modifying any files, change to a dedicated feature branch prefixed with `chem-x/NAMEOFIMPROVEMENT`:');
207
- lines.push(' `git checkout -b chem-x/NAMEOFIMPROVEMENT`');
208
- lines.push('2. Execute each remediation phase sequentially on this branch.');
209
- lines.push('3. Commit the branch with atomic, intention-revealing commit messages.');
210
- lines.push('4. Create a Pull Request (PR) to `main` upon completing the remediation phases.\n');
211
-
212
199
  lines.push('### AI AGENT DISCOVERY & REFACTORING COMMANDS:');
213
200
  lines.push('- Discovery & Inspect: Run `pnpm q "<target>" --inspect` (or `npx chemx search "<target>" --inspect`) to inspect component props and hooks before editing.');
214
201
  lines.push('- Tier Filter: Run `pnpm q "<query>" --tier=molecule` (or `atom`, `organism`, `hook`) to find related capsules.');
@@ -234,13 +221,11 @@ export const buildSelfHealingRoadmapPrompt = (report) => {
234
221
  });
235
222
 
236
223
  lines.push('### EXECUTION DISCIPLINE:');
237
- lines.push('1. Branch First: Change to a new branch prefixed with `chem-x/NAMEOFIMPROVEMENT` (e.g. `git checkout -b chem-x/<name-of-improvement>`) before making any changes.');
238
- lines.push('2. Complete Step 1 (Pattern Harvesting) completely before touching any monolithic file in Step 3.');
239
- lines.push('3. Spliced views in Step 3 must directly import and bind to the canonical capsules created in Step 1.');
240
- lines.push('4. Every new molecule component must stay under 100 lines.');
241
- lines.push('5. Top-level page views must be 10-20 line declarative Table-of-Contents templates assembling molecules via named slots.');
242
- lines.push('6. Zero synthetic or mock data; zero em dashes (use hyphens or colons).');
243
- lines.push('7. PR to Main: Commit the branch and create a Pull Request to main.');
224
+ lines.push('1. Complete Step 1 (Pattern Harvesting) completely before touching any monolithic file in Step 3.');
225
+ lines.push('2. Spliced views in Step 3 must directly import and bind to the canonical capsules created in Step 1.');
226
+ lines.push('3. Every new molecule component must stay under 100 lines.');
227
+ lines.push('4. Top-level page views must be 10-20 line declarative Table-of-Contents templates assembling molecules via named slots.');
228
+ lines.push('5. Zero synthetic or mock data; zero em dashes (use hyphens or colons).');
244
229
 
245
230
  return lines.join('\n');
246
231
  };
@@ -102,6 +102,10 @@ 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
+ if (options.fast) {
106
+ return violations;
107
+ }
108
+
105
109
  // Vue template structural pattern discovery
106
110
  if (ext === '.vue' && options.patternRegistry) {
107
111
  const templateMatch = content.match(/<template\b[^>]*>([\s\S]*?)<\/template>/i);
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ const SOURCE_FILE_PATTERN = /\.(tsx|ts|jsx|js|vue|svelte)$/;
5
+ const EXCLUDED_PATTERNS = ['.test.', '.spec.', '.min.', '.d.ts'];
6
+
7
+ export const isSourceFilePath = (filePath) => {
8
+ const isExtensionValid = SOURCE_FILE_PATTERN.test(filePath);
9
+ if (!isExtensionValid) return false;
10
+ const isExcluded = EXCLUDED_PATTERNS.some((pat) => filePath.includes(pat));
11
+ return !isExcluded;
12
+ };
13
+
14
+ export const parseGitStatusOutput = (output) => {
15
+ const lines = output.split('\n');
16
+ const files = [];
17
+
18
+ for (const rawLine of lines) {
19
+ const trimmed = rawLine.trim();
20
+ if (!trimmed) continue;
21
+ const cleanPath = trimmed.replace(/^[MADRCU?! ]+\s+/, '').trim().replace(/^"|"$/g, '');
22
+ if (cleanPath) {
23
+ files.push(cleanPath);
24
+ }
25
+ }
26
+
27
+ return files;
28
+ };
29
+
30
+ export const getGitChangedFiles = (cwd = process.cwd()) => {
31
+ try {
32
+ const result = spawnSync('git', ['status', '--porcelain', '-u'], { cwd, encoding: 'utf-8' });
33
+ const isSuccess = result.status === 0;
34
+ if (!isSuccess || !result.stdout) return [];
35
+ return parseGitStatusOutput(result.stdout);
36
+ } catch {
37
+ return [];
38
+ }
39
+ };
40
+
41
+ export const resolveGitAuditScope = (cwd = process.cwd()) => {
42
+ const rawFiles = getGitChangedFiles(cwd);
43
+ const existingSourceFiles = rawFiles.filter((relPath) => {
44
+ const isSource = isSourceFilePath(relPath);
45
+ const exists = fs.existsSync(relPath);
46
+ return isSource && exists;
47
+ });
48
+
49
+ const hasChangedFiles = existingSourceFiles.length > 0;
50
+ return {
51
+ ok: hasChangedFiles,
52
+ files: existingSourceFiles
53
+ };
54
+ };
@@ -0,0 +1,87 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { hasGum, gumChoose, gumInput, promptQuestion } from './terminal.js';
4
+ import { resolveGitAuditScope } from './audit-preflight-git.js';
5
+
6
+ const CANDIDATE_NAMES = ['src', 'app', 'cli', 'components', 'lib', 'packages', 'modules'];
7
+
8
+ export const detectCandidateDirectories = (cwd = process.cwd()) => {
9
+ return CANDIDATE_NAMES.filter((name) => {
10
+ const fullPath = path.resolve(cwd, name);
11
+ return fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory();
12
+ });
13
+ };
14
+
15
+ const promptSpecificDirectory = async (cwd) => {
16
+ const candidates = detectCandidateDirectories(cwd);
17
+ const options = [...candidates.map((dir) => `📂 ${dir}`), '✏️ Custom directory path...'];
18
+
19
+ let choice = '';
20
+ if (hasGum()) {
21
+ choice = gumChoose(options, 'Select Target Directory:');
22
+ } else {
23
+ process.stdout.write('\n' + options.map((opt, i) => ` ${i + 1}. ${opt}`).join('\n') + '\n\n');
24
+ const ans = await promptQuestion(`Select directory [1-${options.length}]: `);
25
+ const index = parseInt(ans, 10) - 1;
26
+ choice = options[index] || '';
27
+ }
28
+
29
+ const isCustomPath = !choice || choice.includes('Custom');
30
+ if (isCustomPath) {
31
+ if (hasGum()) {
32
+ return gumInput('Target directory path:', 'src') || 'src';
33
+ }
34
+ const entered = await promptQuestion('Target directory path [src]: ');
35
+ return entered || 'src';
36
+ }
37
+
38
+ const matched = candidates.find((dir) => choice.includes(dir));
39
+ return matched || 'src';
40
+ };
41
+
42
+ export const promptAuditScope = async (defaultDir, rawArgs, cwd = process.cwd()) => {
43
+ const hasGitFlag = rawArgs.includes('--git') || rawArgs.includes('--changed');
44
+ if (hasGitFlag) {
45
+ const gitScope = resolveGitAuditScope(cwd);
46
+ return { targetDir: defaultDir, fileList: gitScope.ok ? gitScope.files : null };
47
+ }
48
+
49
+ const scopeOptions = [
50
+ '📁 Whole Project (Scan entire workspace)',
51
+ '📂 Specific Directory (Select target folder)',
52
+ '🌿 Git Changed Files (Scan modified or staged files only)'
53
+ ];
54
+
55
+ let scopeChoice = '';
56
+ if (hasGum()) {
57
+ scopeChoice = gumChoose(scopeOptions, 'Select Audit Scope:');
58
+ } else {
59
+ process.stdout.write('\n' + scopeOptions.map((opt, i) => ` ${i + 1}. ${opt}`).join('\n') + '\n\n');
60
+ const ans = await promptQuestion(`Select audit scope [1-${scopeOptions.length}]: `);
61
+ const index = parseInt(ans, 10) - 1;
62
+ scopeChoice = scopeOptions[index] || '';
63
+ }
64
+
65
+ const isCancelled = !scopeChoice;
66
+ if (isCancelled) return null;
67
+
68
+ const isGit = scopeChoice.includes('Git');
69
+ if (isGit) {
70
+ const gitScope = resolveGitAuditScope(cwd);
71
+ const hasFiles = gitScope.ok;
72
+ if (!hasFiles) {
73
+ process.stdout.write('\n\x1b[33m⚠ No git modified or staged source files found. Auditing whole project.\x1b[0m\n');
74
+ return { targetDir: defaultDir, fileList: null };
75
+ }
76
+ process.stdout.write(`\n\x1b[32m✔ Focused on ${gitScope.files.length} git modified/staged file(s).\x1b[0m\n`);
77
+ return { targetDir: defaultDir, fileList: gitScope.files };
78
+ }
79
+
80
+ const isSpecific = scopeChoice.includes('Specific');
81
+ if (isSpecific) {
82
+ const chosenDir = await promptSpecificDirectory(cwd);
83
+ return { targetDir: chosenDir, fileList: null };
84
+ }
85
+
86
+ return { targetDir: defaultDir, fileList: null };
87
+ };
@@ -0,0 +1,16 @@
1
+ export interface PreflightResult {
2
+ readonly targetDir: string;
3
+ readonly fast: boolean;
4
+ readonly fileList: readonly string[] | null;
5
+ }
6
+
7
+ export interface PreflightOptions {
8
+ readonly customDir?: string | null;
9
+ readonly defaultDir?: string;
10
+ readonly cwd?: string;
11
+ }
12
+
13
+ export declare function detectCandidateDirectories(cwd?: string): string[];
14
+ export declare function resolveGitAuditScope(cwd?: string): { ok: boolean; files: string[] };
15
+ export declare function getGitChangedFiles(cwd?: string): string[];
16
+ export declare function runAuditPreflight(rawArgs: readonly string[], options?: PreflightOptions): Promise<PreflightResult>;
@@ -0,0 +1,85 @@
1
+ import { hasGum, gumChoose, promptQuestion } from './terminal.js';
2
+ import { promptAuditScope } from './audit-preflight-scope.js';
3
+ import { isNonInteractiveSession } from './audit/rules-predicates.js';
4
+ import { resolveGitAuditScope } from './audit-preflight-git.js';
5
+
6
+ export { detectCandidateDirectories } from './audit-preflight-scope.js';
7
+ export { resolveGitAuditScope, getGitChangedFiles } from './audit-preflight-git.js';
8
+
9
+ const DEPTH_OPTIONS = [
10
+ '🔬 Full AST Audit (Deep molecular analysis, AST visitors, slop, patterns)',
11
+ '⚡ Fast Scan (Instant line-budget, token burn, and file metrics)'
12
+ ];
13
+
14
+ const promptAuditDepth = async (rawArgs) => {
15
+ const hasFastFlag = rawArgs.includes('--fast') || rawArgs.includes('--quick');
16
+ const hasFullFlag = rawArgs.includes('--full') || rawArgs.includes('--deep');
17
+
18
+ if (hasFastFlag) return true;
19
+ if (hasFullFlag) return false;
20
+
21
+ let depthChoice = '';
22
+ if (hasGum()) {
23
+ depthChoice = gumChoose(DEPTH_OPTIONS, 'Select Audit Depth:');
24
+ } else {
25
+ process.stdout.write('\n' + DEPTH_OPTIONS.map((opt, i) => ` ${i + 1}. ${opt}`).join('\n') + '\n\n');
26
+ const ans = await promptQuestion(`Select audit depth [1-${DEPTH_OPTIONS.length}]: `);
27
+ const index = parseInt(ans, 10) - 1;
28
+ depthChoice = DEPTH_OPTIONS[index] || '';
29
+ }
30
+
31
+ const isCancelled = !depthChoice;
32
+ if (isCancelled) return null;
33
+
34
+ return depthChoice.includes('Fast');
35
+ };
36
+
37
+ export const runAuditPreflight = async (rawArgs, options = {}) => {
38
+ const { customDir = null, defaultDir = 'src', cwd = process.cwd() } = options;
39
+
40
+ const isNonInteractive = isNonInteractiveSession(rawArgs);
41
+ const hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
42
+ const hasYesFlag = rawArgs.includes('-y') || rawArgs.includes('--yes');
43
+ const isInteractive = !isNonInteractive && hasTty && !hasYesFlag;
44
+
45
+ const hasExplicitDir = Boolean(customDir) || rawArgs.some((arg) => arg.startsWith('--dir='));
46
+ const hasExplicitGit = rawArgs.includes('--git') || rawArgs.includes('--changed');
47
+ const hasExplicitFast = rawArgs.includes('--fast') || rawArgs.includes('--quick');
48
+ const hasExplicitFull = rawArgs.includes('--full') || rawArgs.includes('--deep');
49
+ const isFullySpecified = (hasExplicitDir || hasExplicitGit) && (hasExplicitFast || hasExplicitFull);
50
+
51
+ if (!isInteractive || isFullySpecified) {
52
+ const fast = hasExplicitFast;
53
+ const fileList = hasExplicitGit ? resolveGitAuditScope(cwd).files : null;
54
+ return { targetDir: customDir || defaultDir, fast, fileList };
55
+ }
56
+
57
+ process.stdout.write('\n\x1b[1m\x1b[38;5;45m⚡ Chemical X Protocol\x1b[0m \x1b[38;5;220mInteractive Pre-Flight\x1b[0m\n\n');
58
+
59
+ let targetDir = customDir || defaultDir;
60
+ let fileList = null;
61
+
62
+ const needsScopePrompt = !hasExplicitDir && !hasExplicitGit;
63
+ if (needsScopePrompt) {
64
+ const scopeResult = await promptAuditScope(defaultDir, rawArgs, cwd);
65
+ if (!scopeResult) {
66
+ process.exit(0);
67
+ }
68
+ targetDir = scopeResult.targetDir;
69
+ fileList = scopeResult.fileList;
70
+ } else if (hasExplicitGit) {
71
+ fileList = resolveGitAuditScope(cwd).files;
72
+ }
73
+
74
+ const depthResult = await promptAuditDepth(rawArgs);
75
+ if (depthResult === null) {
76
+ process.exit(0);
77
+ }
78
+ const fast = depthResult;
79
+
80
+ const modeLabel = fast ? 'Fast line-budget scan' : 'Full molecular AST audit';
81
+ const targetLabel = fileList ? `${fileList.length} git modified file(s)` : targetDir;
82
+ process.stdout.write(`\n\x1b[36m⚡ Starting ${modeLabel} for ${targetLabel}...\x1b[0m\n\n`);
83
+
84
+ return { targetDir, fast, fileList };
85
+ };
@@ -0,0 +1,104 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { isSourceFilePath, parseGitStatusOutput } from './audit-preflight-git.js';
4
+ import { detectCandidateDirectories } from './audit-preflight-scope.js';
5
+ import { runAuditPreflight } from './audit-preflight.js';
6
+ import { scanTree, runAudit } from './audit.js';
7
+ import { auditCode } from './audit/rules.js';
8
+
9
+ test('isSourceFilePath: correctly validates source files and excludes tests', () => {
10
+ assert.strictEqual(isSourceFilePath('src/app.tsx'), true);
11
+ assert.strictEqual(isSourceFilePath('cli/index.js'), true);
12
+ assert.strictEqual(isSourceFilePath('components/card.vue'), true);
13
+ assert.strictEqual(isSourceFilePath('ui/button.svelte'), true);
14
+ assert.strictEqual(isSourceFilePath('src/app.test.tsx'), false);
15
+ assert.strictEqual(isSourceFilePath('src/app.spec.js'), false);
16
+ assert.strictEqual(isSourceFilePath('dist/bundle.min.js'), false);
17
+ assert.strictEqual(isSourceFilePath('types/index.d.ts'), false);
18
+ assert.strictEqual(isSourceFilePath('README.md'), false);
19
+ });
20
+
21
+ test('parseGitStatusOutput: extracts clean file paths from git porcelain output', () => {
22
+ const output = [
23
+ ' M src/components/card.tsx',
24
+ 'A cli/new-command.js',
25
+ '?? scripts/scratch.ts',
26
+ ' D old-file.js',
27
+ '"quoted/path/with space.ts"'
28
+ ].join('\n');
29
+
30
+ const parsed = parseGitStatusOutput(output);
31
+ assert.strictEqual(parsed.length, 5);
32
+ assert.strictEqual(parsed[0], 'src/components/card.tsx');
33
+ assert.strictEqual(parsed[1], 'cli/new-command.js');
34
+ assert.strictEqual(parsed[2], 'scripts/scratch.ts');
35
+ assert.strictEqual(parsed[3], 'old-file.js');
36
+ assert.strictEqual(parsed[4], 'quoted/path/with space.ts');
37
+ });
38
+
39
+ test('detectCandidateDirectories: identifies existing directory targets', () => {
40
+ const detected = detectCandidateDirectories(process.cwd());
41
+ assert.ok(Array.isArray(detected));
42
+ assert.ok(detected.includes('cli'));
43
+ });
44
+
45
+ test('runAuditPreflight: non-interactive mode resolves immediately without questions', async () => {
46
+ const result = await runAuditPreflight(['--ci'], {
47
+ defaultDir: 'cli',
48
+ cwd: process.cwd()
49
+ });
50
+
51
+ assert.strictEqual(result.targetDir, 'cli');
52
+ assert.strictEqual(result.fast, false);
53
+ assert.strictEqual(result.fileList, null);
54
+ });
55
+
56
+ test('runAuditPreflight: explicit fast and customDir flags bypass questions', async () => {
57
+ const result = await runAuditPreflight(['--fast', '--dir=cli'], {
58
+ customDir: 'cli',
59
+ defaultDir: 'src',
60
+ cwd: process.cwd()
61
+ });
62
+
63
+ assert.strictEqual(result.targetDir, 'cli');
64
+ assert.strictEqual(result.fast, true);
65
+ assert.strictEqual(result.fileList, null);
66
+ });
67
+
68
+ test('runAuditPreflight: yes flag accepts defaults immediately', async () => {
69
+ const result = await runAuditPreflight(['-y'], {
70
+ defaultDir: 'cli',
71
+ cwd: process.cwd()
72
+ });
73
+
74
+ assert.strictEqual(result.targetDir, 'cli');
75
+ assert.strictEqual(result.fast, false);
76
+ assert.strictEqual(result.fileList, null);
77
+ });
78
+
79
+ test('auditCode: fast mode executes line checks and bypasses AST parser', () => {
80
+ const longFile = 'const a = 1;\n'.repeat(600);
81
+ const violations = auditCode(longFile, 'src/monolith.ts', 'src/monolith.ts', { fast: true });
82
+
83
+ assert.ok(violations.length > 0);
84
+ const hasLineBudget = violations.some((v) => v.rule === 'LINE_BUDGET_FILE');
85
+ assert.strictEqual(hasLineBudget, true);
86
+ });
87
+
88
+ test('scanTree: fileList restricts scanning strictly to specified files', () => {
89
+ const result = scanTree('cli', process.cwd(), {
90
+ fileList: ['cli/installer-templates.js']
91
+ });
92
+
93
+ assert.strictEqual(result.fileStats.length, 1);
94
+ assert.strictEqual(result.fileStats[0].relativePath, 'cli/installer-templates.js');
95
+ });
96
+
97
+ test('runAudit: fast mode returns valid health metrics with zero AST traversal', () => {
98
+ const report = runAudit('cli', { fast: true });
99
+
100
+ assert.ok(report.scannedFiles > 0);
101
+ assert.ok(report.health);
102
+ assert.ok(typeof report.health.score === 'number');
103
+ assert.ok(typeof report.health.grade === 'string');
104
+ });
package/cli/audit.js CHANGED
@@ -97,11 +97,48 @@ export const auditFile = (filePath, relativePath) => {
97
97
  return auditCode(content, filePath, relativePath);
98
98
  };
99
99
 
100
+ const auditFileEntry = (fullPath, relPath, scanOptions) => {
101
+ const content = fs.readFileSync(fullPath, 'utf-8');
102
+ const lines = content.split('\n');
103
+ const baseName = path.basename(fullPath);
104
+ const isMolecule = relPath.includes('molecules') || relPath.includes('/m-') || baseName.startsWith('m-');
105
+
106
+ const fileStat = {
107
+ fullPath,
108
+ relativePath: relPath,
109
+ lineCount: lines.length,
110
+ charCount: content.length,
111
+ isMolecule
112
+ };
113
+
114
+ const hookMatches = content.match(/\buse[A-Z0-9]\w*\b/g);
115
+ const hookCount = hookMatches ? hookMatches.length : 0;
116
+ const fileViolations = auditCode(content, fullPath, relPath, {
117
+ patternRegistry: scanOptions.patternRegistry,
118
+ fast: scanOptions.fast
119
+ });
120
+
121
+ return { fileStat, hookCount, fileViolations };
122
+ };
123
+
100
124
  export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
101
125
  let violations = [];
102
126
  let fileStats = [];
103
127
  let totalHooks = 0;
104
- const { patternRegistry } = scanOptions;
128
+
129
+ if (scanOptions.fileList && scanOptions.fileList.length > 0) {
130
+ for (const item of scanOptions.fileList) {
131
+ const fullPath = path.isAbsolute(item) ? item : path.resolve(baseDir, item);
132
+ const relPath = path.relative(baseDir, fullPath);
133
+ if (fs.existsSync(fullPath) && isSourceFile(path.basename(fullPath))) {
134
+ const result = auditFileEntry(fullPath, relPath, scanOptions);
135
+ violations = violations.concat(result.fileViolations);
136
+ fileStats.push(result.fileStat);
137
+ totalHooks += result.hookCount;
138
+ }
139
+ }
140
+ return { violations, fileStats, totalHooks };
141
+ }
105
142
 
106
143
  if (!fs.existsSync(targetDir)) {
107
144
  return { violations, fileStats, totalHooks };
@@ -120,26 +157,10 @@ export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
120
157
  totalHooks += sub.totalHooks;
121
158
  }
122
159
  } else if (isSourceFile(entry.name)) {
123
- const content = fs.readFileSync(fullPath, 'utf-8');
124
- const lines = content.split('\n');
125
- const isMolecule = relPath.includes('molecules') || relPath.includes('/m-') || entry.name.startsWith('m-');
126
-
127
- fileStats.push({
128
- fullPath,
129
- relativePath: relPath,
130
- lineCount: lines.length,
131
- charCount: content.length,
132
- isMolecule
133
- });
134
-
135
- // Count hook declarations / invocations for metric
136
- const hookMatches = content.match(/\buse[A-Z0-9]\w*\b/g);
137
- if (hookMatches) {
138
- totalHooks += hookMatches.length;
139
- }
140
-
141
- const fileViolations = auditCode(content, fullPath, relPath, { patternRegistry });
142
- violations = violations.concat(fileViolations);
160
+ const result = auditFileEntry(fullPath, relPath, scanOptions);
161
+ violations = violations.concat(result.fileViolations);
162
+ fileStats.push(result.fileStat);
163
+ totalHooks += result.hookCount;
143
164
  }
144
165
  }
145
166
 
@@ -155,7 +176,11 @@ export const runAudit = (targetDir = 'src', options = {}) => {
155
176
  const cwd = process.cwd();
156
177
  const absoluteTarget = path.resolve(cwd, targetDir);
157
178
  const patternRegistry = createPatternRegistry();
158
- const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, { patternRegistry });
179
+ const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, {
180
+ patternRegistry,
181
+ fast: Boolean(options.fast),
182
+ fileList: options.fileList || null
183
+ });
159
184
 
160
185
  const scannedFiles = fileStats.length;
161
186
  const totalLoc = fileStats.reduce((acc, f) => acc + f.lineCount, 0);
package/cli/help.js CHANGED
@@ -111,6 +111,15 @@ export const printHelp = () => {
111
111
  ` Display this comprehensive CLI documentation and options guide.`,
112
112
  '',
113
113
  `${BOLD}AUDIT OPTIONS & FLAGS${RESET}`,
114
+ ` ${CYAN}--fast${RESET}, ${CYAN}--quick${RESET}`,
115
+ ` Fast scan: verify line budgets, token burn, and file metrics, bypassing deep AST traversal.`,
116
+ '',
117
+ ` ${CYAN}--git${RESET}, ${CYAN}--changed${RESET}`,
118
+ ` Scope audit strictly to modified, staged, or untracked source files in git.`,
119
+ '',
120
+ ` ${CYAN}-y${RESET}, ${CYAN}--yes${RESET}`,
121
+ ` Accept defaults and bypass interactive pre-flight wizard questions.`,
122
+ '',
114
123
  ` ${CYAN}--dir=<path>${RESET}`,
115
124
  ` Directory to analyze. Defaults to ${CYAN}src${RESET} if present, else current working directory.`,
116
125
  ` Example: ${CYAN}npx chemx audit --dir=apps/web/src${RESET}`,
package/cli/index.js CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  evaluateAuditFailure,
31
31
  isNonInteractiveSession
32
32
  } from './audit/rules-predicates.js';
33
+ import { runAuditPreflight, resolveGitAuditScope } from './audit-preflight.js';
33
34
  import { runInstallWizard } from './installer.js';
34
35
  import { runBadgeCommand } from './badge.js';
35
36
  import { runBuildAudit } from './build.js';
@@ -84,8 +85,42 @@ export const runAudit = async (customDir = null, isCli = false) => {
84
85
  const costFlag = rawArgs.find((arg) => arg.startsWith('--cost-per-million='));
85
86
  const costPerMillion = costFlag ? parseFloat(costFlag.split('=')[1]) : null;
86
87
 
87
- const targetDir = resolveTargetDir(customDir, dirFlag);
88
- const report = executeAstAudit(targetDir, { outputFile, model, costPerMillion });
88
+ const isNonInteractive = isNonInteractiveSession(rawArgs);
89
+ const isInteractive = !isNonInteractive && Boolean(process.stdin.isTTY && process.stdout.isTTY);
90
+
91
+ let targetDir = resolveTargetDir(customDir, dirFlag);
92
+ let isFast = rawArgs.includes('--fast') || rawArgs.includes('--quick');
93
+ let fileList = null;
94
+
95
+ const hasGitFlag = rawArgs.includes('--git') || rawArgs.includes('--changed');
96
+ if (hasGitFlag) {
97
+ const gitScope = resolveGitAuditScope(process.cwd());
98
+ if (gitScope.ok) {
99
+ fileList = gitScope.files;
100
+ }
101
+ }
102
+
103
+ const shouldRunPreflight = isCli && isInteractive && !isJson && !isMarkdown && !isShare;
104
+ if (shouldRunPreflight) {
105
+ const preflight = await runAuditPreflight(rawArgs, {
106
+ customDir,
107
+ defaultDir: targetDir,
108
+ cwd: process.cwd()
109
+ });
110
+ targetDir = preflight.targetDir;
111
+ isFast = preflight.fast;
112
+ fileList = preflight.fileList;
113
+ }
114
+
115
+ const auditOptions = {
116
+ outputFile,
117
+ model,
118
+ costPerMillion,
119
+ fast: isFast,
120
+ fileList
121
+ };
122
+
123
+ const report = executeAstAudit(targetDir, auditOptions);
89
124
  saveAuditSnapshot(report);
90
125
  const syncRes = syncSearchIndex(targetDir, process.cwd());
91
126
  if (syncRes?.db) {
@@ -120,12 +155,9 @@ export const runAudit = async (customDir = null, isCli = false) => {
120
155
  process.exit(0);
121
156
  }
122
157
 
123
- const isNonInteractive = isNonInteractiveSession(rawArgs);
124
- const isInteractive = !isNonInteractive && Boolean(process.stdin.isTTY && process.stdout.isTTY);
125
-
126
158
  if (isInteractive && !isUnroll) {
127
159
  const handleReAudit = () => {
128
- const refreshed = executeAstAudit(targetDir, { outputFile, model, costPerMillion });
160
+ const refreshed = executeAstAudit(targetDir, auditOptions);
129
161
  saveAuditSnapshot(refreshed);
130
162
  return refreshed;
131
163
  };
@@ -198,7 +230,9 @@ export {
198
230
  runBuildAudit,
199
231
  runSearch,
200
232
  syncSearchIndex,
201
- startMcpServer,
233
+ runMcpServer,
234
+ runMcpServer as startMcpServer,
235
+ runMcpInstaller,
202
236
  runReaderCli,
203
237
  readTokenOptimized,
204
238
  runPatcherCli,
@@ -2,15 +2,46 @@ export const buildPreCommitHookScript = (minGrade = 'B', minScore = 80) => `#!/b
2
2
  # Chemical X Protocol: Pre-Commit Line Budget & Architecture Gatekeeper
3
3
  # Free architectural guardrail preventing context bloat and monolith sprawl.
4
4
 
5
- MIN_GRADE="\${CHEMX_MIN_GRADE:-${minGrade}}"
6
- MIN_SCORE="\${CHEMX_MIN_SCORE:-${minScore}}"
7
- MAX_LINES="\${CHEMX_MAX_LINES:-500}"
8
- MAX_MOLECULE_LINES="\${CHEMX_MAX_MOLECULE_LINES:-100}"
9
-
10
5
  REPO_ROOT="\$(git rev-parse --show-toplevel 2>/dev/null)"
11
6
  [ -z "\$REPO_ROOT" ] && exit 0
12
7
  cd "\$REPO_ROOT" || exit 1
13
8
 
9
+ # Delegate to version-controlled script if present
10
+ if [ -f "\$REPO_ROOT/scripts/pre-commit.sh" ]; then
11
+ exec "\$REPO_ROOT/scripts/pre-commit.sh" "\$@"
12
+ fi
13
+
14
+ # Load thresholds from .chemx/config.json if available
15
+ CONF_MAX_LINES=""
16
+ CONF_MAX_MOL=""
17
+ CONF_MIN_GRADE=""
18
+ CONF_MIN_SCORE=""
19
+ CONFIG_FILE="\$REPO_ROOT/.chemx/config.json"
20
+
21
+ if [ -f "\$CONFIG_FILE" ]; then
22
+ if command -v node >/dev/null 2>&1; then
23
+ eval \$(node -e "
24
+ try {
25
+ const c = JSON.parse(require('fs').readFileSync('\$CONFIG_FILE', 'utf8'));
26
+ if (c.maxLineCount || c.maxLines) console.log('CONF_MAX_LINES=' + (c.maxLineCount || c.maxLines));
27
+ if (c.maxMoleculeLineCount || c.maxMoleculeLines) console.log('CONF_MAX_MOL=' + (c.maxMoleculeLineCount || c.maxMoleculeLines));
28
+ if (c.minGrade) console.log('CONF_MIN_GRADE=' + c.minGrade);
29
+ if (c.minScore) console.log('CONF_MIN_SCORE=' + c.minScore);
30
+ } catch (e) {}
31
+ ")
32
+ else
33
+ CONF_MAX_LINES=\$(grep -o '"maxLineCount"[[:space:]]*:[[:space:]]*[0-9]*' "\$CONFIG_FILE" 2>/dev/null | grep -o '[0-9]*\$')
34
+ CONF_MAX_MOL=\$(grep -o '"maxMoleculeLineCount"[[:space:]]*:[[:space:]]*[0-9]*' "\$CONFIG_FILE" 2>/dev/null | grep -o '[0-9]*\$')
35
+ CONF_MIN_GRADE=\$(grep -o '"minGrade"[[:space:]]*:[[:space:]]*"[^"]*"' "\$CONFIG_FILE" 2>/dev/null | sed 's/.*"minGrade"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\\1/')
36
+ CONF_MIN_SCORE=\$(grep -o '"minScore"[[:space:]]*:[[:space:]]*[0-9]*' "\$CONFIG_FILE" 2>/dev/null | grep -o '[0-9]*\$')
37
+ fi
38
+ fi
39
+
40
+ MIN_GRADE="\${CHEMX_MIN_GRADE:-\${CONF_MIN_GRADE:-${minGrade}}}"
41
+ MIN_SCORE="\${CHEMX_MIN_SCORE:-\${CONF_MIN_SCORE:-${minScore}}}"
42
+ MAX_LINES="\${CHEMX_MAX_LINES:-\${CONF_MAX_LINES:-500}}"
43
+ MAX_MOLECULE_LINES="\${CHEMX_MAX_MOLECULE_LINES:-\${CONF_MAX_MOL:-100}}"
44
+
14
45
  STAGED_FILES=\$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(jsx?|tsx?|vue|svelte)\$' | grep -vE '(\\.(d\\.ts|min\\.|test\\.|spec\\.))')
15
46
  [ -z "\$STAGED_FILES" ] && exit 0
16
47
 
@@ -29,6 +60,9 @@ done
29
60
 
30
61
  if [ "\$FAILED" -eq 1 ]; then
31
62
  printf "\\n\\033[1m\\033[31m[Chemical X] Commit Blocked: Staged files exceed architectural line budgets\\033[0m\\n\$ERRORS\\n\\n"
63
+ printf "\\033[33mMonolithic files degrade AI context windows and cause hallucination loops.\\033[0m\\n"
64
+ printf "Decompose large files into single-purpose crystalline capsules or configure thresholds in .chemx/config.json before committing.\\n\\n"
65
+ exit 1
32
66
  fi
33
67
 
34
68
  AUDIT_BIN=""
@@ -51,7 +85,6 @@ if [ -n "\$AUDIT_BIN" ]; then
51
85
  fi
52
86
  fi
53
87
 
54
- [ "\$FAILED" -eq 1 ] && exit 1
55
88
  printf "\\033[32m✔ [Chemical X] Pre-commit architectural guardrails passed.\\033[0m\\n"
56
89
  exit 0
57
90
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chemx/create-chemx",
3
- "version": "26.9.16-41",
3
+ "version": "26.9.16-668",
4
4
  "description": "Chemical X Protocol: Private drop-in architecture starter kit and capsule generator",
5
5
  "type": "module",
6
6
  "bin": {