@chemx/starter-kit 26.9.28-276 → 26.10.4-235

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.
@@ -111,7 +111,7 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
111
111
 
112
112
  const lang = getLanguageForFile(filePath);
113
113
  if (lang?.id === 'csharp') {
114
- analyzeCSharpCode(content, relativePath, violations);
114
+ analyzeCSharpCode(content, relativePath, violations, config);
115
115
  }
116
116
 
117
117
  if (options.fast || !isBabelParsable(filePath)) {
package/cli/audit.js CHANGED
@@ -84,12 +84,12 @@ const IGNORED_DIRS = new Set([
84
84
  'out'
85
85
  ]);
86
86
 
87
- const isSourceFile = (name) => {
88
- return isPolyglotSourceFile(name, { includeTests: false });
87
+ const isSourceFile = (name, options = {}) => {
88
+ return isPolyglotSourceFile(name, { includeTests: false, ...options });
89
89
  };
90
90
 
91
91
  export const auditFile = (filePath, relativePath) => {
92
- if (!isSourceFile(path.basename(filePath))) return [];
92
+ if (!isSourceFile(path.basename(filePath), { includeTests: true })) return [];
93
93
  const content = fs.readFileSync(filePath, 'utf-8');
94
94
  return auditCode(content, filePath, relativePath);
95
95
  };
@@ -125,12 +125,18 @@ export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
125
125
  let fileStats = [];
126
126
  let totalHooks = 0;
127
127
 
128
+ const targetRel = path.relative(baseDir, targetDir);
129
+ const isTargetingTests = /(?:^|[\\/])(?:tests?|specs?)(?:[\\/]|$)/i.test(targetRel) ||
130
+ /(?:^|[\\/])(?:tests?|specs?)(?:[\\/]|$)/i.test(targetDir);
131
+ const includeTests = Boolean(scanOptions.includeTests || isTargetingTests);
132
+ const effectiveScanOptions = { ...scanOptions, includeTests };
133
+
128
134
  if (scanOptions.fileList && scanOptions.fileList.length > 0) {
129
135
  for (const item of scanOptions.fileList) {
130
136
  const fullPath = path.isAbsolute(item) ? item : path.resolve(baseDir, item);
131
137
  const relPath = path.relative(baseDir, fullPath);
132
- if (fs.existsSync(fullPath) && isSourceFile(path.basename(fullPath))) {
133
- const result = auditFileEntry(fullPath, relPath, scanOptions);
138
+ if (fs.existsSync(fullPath) && isSourceFile(path.basename(fullPath), { includeTests: true })) {
139
+ const result = auditFileEntry(fullPath, relPath, effectiveScanOptions);
134
140
  violations = violations.concat(result.fileViolations);
135
141
  fileStats.push(result.fileStat);
136
142
  totalHooks += result.hookCount;
@@ -150,13 +156,13 @@ export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
150
156
 
151
157
  if (entry.isDirectory()) {
152
158
  if (!IGNORED_DIRS.has(entry.name)) {
153
- const sub = scanTree(fullPath, baseDir, scanOptions);
159
+ const sub = scanTree(fullPath, baseDir, effectiveScanOptions);
154
160
  violations = violations.concat(sub.violations);
155
161
  fileStats = fileStats.concat(sub.fileStats);
156
162
  totalHooks += sub.totalHooks;
157
163
  }
158
- } else if (isSourceFile(entry.name)) {
159
- const result = auditFileEntry(fullPath, relPath, scanOptions);
164
+ } else if (isSourceFile(entry.name, { includeTests })) {
165
+ const result = auditFileEntry(fullPath, relPath, effectiveScanOptions);
160
166
  violations = violations.concat(result.fileViolations);
161
167
  fileStats.push(result.fileStat);
162
168
  totalHooks += result.hookCount;
@@ -177,11 +183,13 @@ export const runAudit = (targetDir = 'src', options = {}) => {
177
183
  const patternRegistry = createPatternRegistry();
178
184
  const hookRegistry = createHookShapeRegistry();
179
185
  const config = options.config || loadProjectConfig(cwd);
186
+ const includeTests = Boolean(options.includeTests);
180
187
  const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, {
181
188
  patternRegistry,
182
189
  hookRegistry,
183
190
  fast: Boolean(options.fast),
184
191
  fileList: options.fileList || null,
192
+ includeTests,
185
193
  config
186
194
  });
187
195
 
@@ -101,7 +101,8 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
101
101
  fileList = preflight.fileList;
102
102
  }
103
103
 
104
- const auditOptions = { outputFile, model, costPerMillion, fast: isFast, fileList, stage, config: projectConfig };
104
+ const includeTests = rawArgs.includes('--include-tests') || rawArgs.includes('--tests');
105
+ const auditOptions = { outputFile, model, costPerMillion, fast: isFast, fileList, stage, config: projectConfig, includeTests };
105
106
  const report = executeAstAudit(targetDir, auditOptions);
106
107
  saveAuditSnapshot(report);
107
108
  try {
@@ -112,7 +113,8 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
112
113
  const shouldTriage = !rawArgs.includes('--no-triage');
113
114
  if (shouldTriage) {
114
115
  const createdTasks = autoGenerateTasksFromAudit(syncRes.db, { cwd: process.cwd(), targetDir });
115
- if (isCli && !isJson && createdTasks.length > 0) {
116
+ const shouldLogTriage = isCli && !isJson && createdTasks.length > 0;
117
+ if (shouldLogTriage) {
116
118
  process.stdout.write(`\x1b[32m✔\x1b[0m Auto-triage synchronized ${createdTasks.length} team task(s) in SQLite backlog.\n`);
117
119
  }
118
120
  }
@@ -141,8 +143,8 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
141
143
  }
142
144
  }
143
145
  }
144
- } catch {
145
- // Gracefully bypass indexing when database is readonly in sandboxed environment
146
+ } catch (err) {
147
+ if (process.env.DEBUG) process.stderr.write(`[debug] Indexing bypassed: ${err?.message}\n`);
146
148
  }
147
149
 
148
150
  // Stage 1: Atomic failure predicates
@@ -62,27 +62,47 @@ MIN_SCORE="\${CHEMX_MIN_SCORE:-\${CONF_MIN_SCORE:-${minScore}}}"
62
62
  MAX_LINES="\${CHEMX_MAX_LINES:-\${CONF_MAX_LINES:-500}}"
63
63
  MAX_MOLECULE_LINES="\${CHEMX_MAX_MOLECULE_LINES:-\${CONF_MAX_MOL:-100}}"
64
64
 
65
- STAGED_FILES=\$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(jsx?|tsx?|vue|svelte)\$' | grep -vE '(\\.(d\\.ts|min\\.|test\\.|spec\\.))')
65
+ STAGED_FILES=\$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(jsx?|tsx?|vue|svelte|cs|py|go)\$' | grep -vE '(\\.(d\\.ts|min\\.|test\\.|spec\\.))')
66
66
  [ -z "\$STAGED_FILES" ] && exit 0
67
67
 
68
68
  FAILED=0
69
69
  ERRORS=""
70
+ EXCEEDED_FILES=""
70
71
  for F in \$STAGED_FILES; do
71
72
  [ ! -f "\$F" ] && continue
72
73
  L=\$(wc -l < "\$F" | tr -d ' ')
73
74
  case "\$F" in
74
75
  *molecules*|*/m-*|m-*)
75
- [ "\$L" -gt "\$MAX_MOLECULE_LINES" ] && FAILED=1 && ERRORS="\${ERRORS}\\n \${C_RED}✕\${C_RESET} \$F (\$L LOC > \$MAX_MOLECULE_LINES molecule limit)" ;;
76
+ if [ "\$L" -gt "\$MAX_MOLECULE_LINES" ]; then
77
+ FAILED=1
78
+ ERRORS="\${ERRORS}\\n \${C_RED}✕\${C_RESET} \$F (\$L LOC > \$MAX_MOLECULE_LINES molecule limit)"
79
+ EXCEEDED_FILES="\${EXCEEDED_FILES}\\n- \$F (\$L LOC > \$MAX_MOLECULE_LINES limit)"
80
+ fi
81
+ ;;
76
82
  *)
77
- [ "\$L" -gt "\$MAX_LINES" ] && FAILED=1 && ERRORS="\${ERRORS}\\n \${C_RED}✕\${C_RESET} \$F (\$L LOC > \$MAX_LINES file budget)" ;;
83
+ if [ "\$L" -gt "\$MAX_LINES" ]; then
84
+ FAILED=1
85
+ ERRORS="\${ERRORS}\\n \${C_RED}✕\${C_RESET} \$F (\$L LOC > \$MAX_LINES file budget)"
86
+ EXCEEDED_FILES="\${EXCEEDED_FILES}\\n- \$F (\$L LOC > \$MAX_LINES limit)"
87
+ fi
88
+ ;;
78
89
  esac
79
90
  done
80
91
 
81
92
  if [ "\$FAILED" -eq 1 ]; then
82
93
  printf "\\n%s%s[Chemical X] Commit Blocked: Staged files exceed architectural line budgets%s\\n" "\$C_BOLD" "\$C_RED" "\$C_RESET"
83
94
  printf "%b\\n\\n" "\$ERRORS"
84
- printf "%sMonolithic files degrade AI context windows and cause hallucination loops.%s\\n" "\$C_YELLOW" "\$C_RESET"
85
- printf "Decompose large files into single-purpose crystalline capsules or configure thresholds in .chemx/config.json before committing.\\n\\n"
95
+ printf "%s╭──────────────────────────────────────────────────────────────────────────╮%s\\n" "\$C_CYAN" "\$C_RESET"
96
+ printf "%s│ 🤖 AI REFACTOR PROMPT (Copy & paste into your AI assistant): │%s\\n" "\$C_CYAN" "\$C_RESET"
97
+ printf "%s╰──────────────────────────────────────────────────────────────────────────╯%s\\n" "\$C_CYAN" "\$C_RESET"
98
+ printf "Please refactor the following files that exceed Chemical X line budgets:%b\\n\\n" "\$EXCEEDED_FILES"
99
+ printf "Refactor Directives:\\n"
100
+ printf "1. Decompose monolithic logic into crystalline single-purpose modules (< %s lines for files, < %s lines for molecules).\\n" "\$MAX_LINES" "\$MAX_MOLECULE_LINES"
101
+ printf "2. Extract presentation into Table-of-Contents views and business state into composables/services.\\n"
102
+ printf "3. Preserve all existing symbols, exports, and public API contracts.\\n"
103
+ printf "4. Decompose complex inline booleans and flatten nested control flow.\\n"
104
+ printf "%s────────────────────────────────────────────────────────────────────────────%s\\n\\n" "\$C_CYAN" "\$C_RESET"
105
+ printf "%s💡 Tip: To bypass line budgets temporarily: CHEMX_SKIP_PRECOMMIT=1 git commit%s\\n\\n" "\$C_YELLOW" "\$C_RESET"
86
106
  exit 1
87
107
  fi
88
108
 
@@ -104,7 +124,18 @@ if [ -n "\$AUDIT_BIN" ]; then
104
124
  if ! AUDIT_OUT=\$(eval "\$AUDIT_BIN audit --git --min-grade=\$MIN_GRADE --min-score=\$MIN_SCORE --non-interactive" < /dev/null 2>&1); then
105
125
  printf "\\n%s%s[Chemical X] Commit Blocked: Architectural health verification failed%s\\n" "\$C_BOLD" "\$C_RED" "\$C_RESET"
106
126
  printf "%s\\n\\n" "\$AUDIT_OUT"
107
- printf "%s💡 Tip: Want crystalline drop-in templates? Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol%s\\n\\n" "\$C_CYAN" "\$C_RESET"
127
+ printf "%s╭──────────────────────────────────────────────────────────────────────────╮%s\\n" "\$C_CYAN" "\$C_RESET"
128
+ printf "%s│ 🤖 AI REFACTOR PROMPT (Copy & paste into your AI assistant): │%s\\n" "\$C_CYAN" "\$C_RESET"
129
+ printf "%s╰──────────────────────────────────────────────────────────────────────────╯%s\\n" "\$C_CYAN" "\$C_RESET"
130
+ printf "Please fix the Chemical X architectural hazards reported above in staged files.\\n\\n"
131
+ printf "Refactor Directives:\\n"
132
+ printf "1. Surgically resolve each flagged Critical and High severity hazard.\\n"
133
+ printf "2. Decompose monoliths into single-purpose crystalline capsules.\\n"
134
+ printf "3. Preserve all existing symbols, exports, and test contracts.\\n"
135
+ printf "4. Verify with 'chemx audit' after making changes.\\n"
136
+ printf "%s────────────────────────────────────────────────────────────────────────────%s\\n\\n" "\$C_CYAN" "\$C_RESET"
137
+ printf "%s💡 Tip: Run 'chemx audit' locally to inspect details or run autofixes.%s\\n" "\$C_CYAN" "\$C_RESET"
138
+ printf " To bypass this check temporarily: CHEMX_SKIP_PRECOMMIT=1 git commit\\n\\n"
108
139
  exit 1
109
140
  fi
110
141
  fi
package/cli/languages.js CHANGED
@@ -76,6 +76,14 @@ export const LANGUAGE_DEFINITIONS = {
76
76
  parser: 'jvm',
77
77
  commentPrefix: '//',
78
78
  blockComment: { start: '/*', end: '*/' }
79
+ },
80
+ cpp: {
81
+ id: 'cpp',
82
+ name: 'C / C++',
83
+ extensions: new Set(['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh', '.inl']),
84
+ parser: 'cpp',
85
+ commentPrefix: '//',
86
+ blockComment: { start: '/*', end: '*/' }
79
87
  }
80
88
  };
81
89
 
@@ -42,3 +42,24 @@ test('languages: resolves language metadata', () => {
42
42
  assert.equal(py.id, 'python');
43
43
  assert.equal(py.commentPrefix, '#');
44
44
  });
45
+
46
+ test('languages: recognizes C and C++ source extensions', () => {
47
+ assert.equal(isSourceFile('StreamDecoder.cpp'), true);
48
+ assert.equal(isSourceFile('StreamDecoder.cc'), true);
49
+ assert.equal(isSourceFile('StreamDecoder.cxx'), true);
50
+ assert.equal(isSourceFile('StreamDecoder.hpp'), true);
51
+ assert.equal(isSourceFile('StreamDecoder.h'), true);
52
+ assert.equal(isSourceFile('legacy_shim.c'), true);
53
+ });
54
+
55
+ test('languages: maps C and C++ extensions to the cpp language', () => {
56
+ assert.equal(getLanguageForFile('StreamDecoder.cpp').id, 'cpp');
57
+ assert.equal(getLanguageForFile('StreamDecoder.h').id, 'cpp');
58
+ assert.equal(getLanguageForFile('legacy_shim.c').id, 'cpp');
59
+ assert.equal(LANGUAGE_DEFINITIONS.cpp.name, 'C / C++');
60
+ });
61
+
62
+ test('languages: C and C++ are not babel parsable', () => {
63
+ assert.equal(isBabelParsable('StreamDecoder.cpp'), false);
64
+ assert.equal(isBabelParsable('StreamDecoder.h'), false);
65
+ });
package/cli/path-scope.js CHANGED
@@ -61,3 +61,53 @@ export const isPathTraversal = (targetPath, baseDir = process.cwd()) => {
61
61
  return true;
62
62
  }
63
63
  };
64
+
65
+ const hasDotNetProject = (cwd) => {
66
+ try {
67
+ const rootEntries = fs.readdirSync(cwd, { withFileTypes: true });
68
+ const hasSln = rootEntries.some((e) => e.isFile() && e.name.endsWith('.sln'));
69
+ const hasRootCsproj = rootEntries.some((e) => e.isFile() && e.name.endsWith('.csproj'));
70
+ if (hasSln || hasRootCsproj) return true;
71
+
72
+ return rootEntries.some((e) => {
73
+ const isCandidateDir = e.isDirectory() && e.name !== 'src' && e.name !== 'node_modules' && !e.name.startsWith('.');
74
+ if (!isCandidateDir) return false;
75
+ const subPath = path.join(cwd, e.name);
76
+ try {
77
+ return fs.readdirSync(subPath).some((f) => f.endsWith('.csproj'));
78
+ } catch (err) {
79
+ if (process.env.DEBUG) process.stderr.write(`[debug] Read failed: ${err?.message}\n`);
80
+ return false;
81
+ }
82
+ });
83
+ } catch (err) {
84
+ if (process.env.DEBUG) process.stderr.write(`[debug] Scan failed: ${err?.message}\n`);
85
+ return false;
86
+ }
87
+ };
88
+
89
+ export const resolveTargetDir = (customOrFlag = null, dirFlag = null, cwd = process.cwd()) => {
90
+ const isCustomPath = Boolean(customOrFlag && !customOrFlag.startsWith('--dir='));
91
+ if (isCustomPath) {
92
+ return customOrFlag;
93
+ }
94
+
95
+ const effectiveFlag = dirFlag || (customOrFlag?.startsWith('--dir=') ? customOrFlag : null);
96
+ if (effectiveFlag) {
97
+ const [, flagValue] = effectiveFlag.split('=');
98
+ if (flagValue !== undefined) {
99
+ return flagValue;
100
+ }
101
+ }
102
+
103
+ if (hasDotNetProject(cwd)) {
104
+ return '.';
105
+ }
106
+
107
+ const hasSrcDirectory = fs.existsSync(path.resolve(cwd, 'src'));
108
+ if (hasSrcDirectory) {
109
+ return 'src';
110
+ }
111
+
112
+ return '.';
113
+ };
@@ -109,3 +109,114 @@ func TestDummy(t *testing.T) {
109
109
  assert.equal(goViolations.some((v) => v.rule === 'SYNTAX_PARSE_ERROR'), false);
110
110
  assert.equal(goViolations.some((v) => v.rule === 'SYNTHETIC_MOCK_DATA'), true);
111
111
  });
112
+
113
+ test('polyglot: extracts C++ classes, structs, functions and includes', () => {
114
+ const cppCode = `
115
+ #include <memory>
116
+ #include "vendor/codec.h"
117
+
118
+ namespace audio {
119
+
120
+ struct FrameHeader {
121
+ uint32_t size;
122
+ };
123
+
124
+ enum class Codec { Opus, Flac };
125
+
126
+ class StreamDecoder : public IDecoder {
127
+ public:
128
+ bool DecodeFrame(const FrameHeader& header);
129
+ };
130
+
131
+ bool StreamDecoder::DecodeFrame(const FrameHeader& header) {
132
+ return header.size > 0;
133
+ }
134
+
135
+ } // namespace audio
136
+ `;
137
+
138
+ const meta = extractAstMetadata(cppCode, 'src/audio/StreamDecoder.cpp');
139
+ const names = meta.symbols.map((s) => s.name);
140
+ assert.ok(names.includes('StreamDecoder'), 'must extract class StreamDecoder');
141
+ assert.ok(names.includes('FrameHeader'), 'must extract struct FrameHeader');
142
+ assert.ok(names.includes('Codec'), 'must extract enum class Codec');
143
+ assert.ok(names.includes('DecodeFrame'), 'must extract method DecodeFrame');
144
+ assert.ok(meta.imports.some((i) => i.sourceModule === 'vendor/codec.h'), 'must map #include to imports');
145
+ assert.ok(meta.imports.some((i) => i.sourceModule === 'memory'), 'must map angle-bracket includes');
146
+ });
147
+
148
+ test('polyglot: audits C++ without Babel syntax errors', () => {
149
+ const cppCode = `
150
+ #include <string>
151
+ void Connect() {
152
+ std::string email = "user@example.com";
153
+ }
154
+ `;
155
+ const violations = auditCode(cppCode, 'src/net/Client.cpp', 'src/net/Client.cpp');
156
+ assert.equal(violations.some((v) => v.rule === 'SYNTAX_PARSE_ERROR'), false, 'C++ must not trigger Babel SYNTAX_PARSE_ERROR');
157
+ assert.equal(violations.some((v) => v.rule === 'SYNTHETIC_MOCK_DATA'), true, 'must detect placeholder email in C++');
158
+ });
159
+
160
+ test('polyglot: extracts Rust items and use imports', () => {
161
+ const rustCode = `
162
+ use std::sync::Arc;
163
+
164
+ pub struct Decoder {
165
+ buffer: Vec<u8>,
166
+ }
167
+
168
+ pub enum State { Idle, Running }
169
+
170
+ pub trait Sink {
171
+ fn write(&self, data: &[u8]);
172
+ }
173
+
174
+ impl Decoder {
175
+ pub fn new() -> Self { Decoder { buffer: Vec::new() } }
176
+ }
177
+ `;
178
+ const meta = extractAstMetadata(rustCode, 'src/decoder.rs');
179
+ const names = meta.symbols.map((s) => s.name);
180
+ assert.ok(names.includes('Decoder'), 'must extract pub struct');
181
+ assert.ok(names.includes('State'), 'must extract pub enum');
182
+ assert.ok(names.includes('Sink'), 'must extract pub trait');
183
+ assert.ok(names.includes('new'), 'must extract fn');
184
+ assert.ok(meta.imports.some((i) => i.sourceModule === 'std::sync::Arc'), 'must map use statements');
185
+ });
186
+
187
+ test('polyglot: extracts Kotlin declarations and imports', () => {
188
+ const kotlinCode = `
189
+ import kotlinx.coroutines.flow.Flow
190
+
191
+ class ConsentRepository(private val api: Api) {
192
+ fun observe(): Flow<List<Consent>> = api.stream()
193
+ }
194
+
195
+ data class Consent(val id: String)
196
+
197
+ object Registry {
198
+ fun lookup(id: String) = id
199
+ }
200
+ `;
201
+ const meta = extractAstMetadata(kotlinCode, 'src/main/kotlin/ConsentRepository.kt');
202
+ const names = meta.symbols.map((s) => s.name);
203
+ assert.ok(names.includes('ConsentRepository'), 'must extract class');
204
+ assert.ok(names.includes('Consent'), 'must extract data class');
205
+ assert.ok(names.includes('Registry'), 'must extract object');
206
+ assert.ok(names.includes('observe'), 'must extract fun');
207
+ assert.ok(meta.imports.some((i) => i.sourceModule === 'kotlinx.coroutines.flow.Flow'), 'must map kotlin imports');
208
+ });
209
+
210
+ test('polyglot: declaration keywords never leak in as symbol names', () => {
211
+ const cppCode = `
212
+ enum class Codec { Opus, Flac };
213
+ struct Frame { int size; };
214
+ namespace audio { }
215
+ `;
216
+ const meta = extractAstMetadata(cppCode, 'src/audio/Codec.h');
217
+ const names = meta.symbols.map((s) => s.name);
218
+ assert.equal(names.includes('class'), false, 'bare "class" must not be extracted as a symbol');
219
+ assert.equal(names.includes('struct'), false, 'bare "struct" must not be extracted as a symbol');
220
+ assert.ok(names.includes('Codec'));
221
+ assert.ok(names.includes('Frame'));
222
+ });
package/cli/search-ast.js CHANGED
@@ -21,6 +21,17 @@ export const resolveArchitectureTier = (relativePath) => {
21
21
  return 'utility';
22
22
  };
23
23
 
24
+ const CPP_NON_DECLARATION_KEYWORDS = new Set([
25
+ 'if', 'for', 'while', 'switch', 'catch', 'return', 'sizeof', 'else', 'do', 'throw', 'new', 'delete'
26
+ ]);
27
+
28
+ // Declaration keywords are never symbol names. Guards cross-language regex overlap,
29
+ // e.g. the Rust `enum` pattern matching C++ `enum class Codec` and capturing `class`.
30
+ const RESERVED_SYMBOL_NAMES = new Set([
31
+ 'class', 'struct', 'enum', 'union', 'interface', 'object', 'fun', 'trait', 'impl', 'mod',
32
+ 'namespace', 'typename', 'template', 'public', 'private', 'protected', 'static', 'const'
33
+ ]);
34
+
24
35
  const extractRegexFallback = (content, filePath = '') => {
25
36
  const symbols = [];
26
37
  const imports = [];
@@ -47,6 +58,49 @@ const extractRegexFallback = (content, filePath = '') => {
47
58
  symbols.push({ name: m[1], kind: 'function', isExport: true, startLine: 1, endLine: 1, signature: '' });
48
59
  }
49
60
 
61
+ const cppTypeMatches = content.matchAll(/\b(?:class|struct|union|enum(?:\s+class)?)\s+([A-Za-z_][A-Za-z0-9_]*)/g);
62
+ for (const m of cppTypeMatches) {
63
+ if (!RESERVED_SYMBOL_NAMES.has(m[1])) {
64
+ symbols.push({ name: m[1], kind: 'class', isExport: true, startLine: 1, endLine: 1, signature: '' });
65
+ }
66
+ }
67
+
68
+ const cppQualifiedMatches = content.matchAll(/\b[A-Za-z_][A-Za-z0-9_]*\s*::\s*([A-Za-z_~][A-Za-z0-9_]*)\s*\(/g);
69
+ for (const m of cppQualifiedMatches) {
70
+ symbols.push({ name: m[1], kind: 'function', isExport: true, startLine: 1, endLine: 1, signature: '' });
71
+ }
72
+
73
+ const cppDeclMatches = content.matchAll(/^[ \t]*(?:[A-Za-z_][A-Za-z0-9_:<>,*& \t]*?)\s+\*?([A-Za-z_][A-Za-z0-9_]*)\s*\([^)]*\)\s*(?:const\s*)?[;{]/gm);
74
+ for (const m of cppDeclMatches) {
75
+ if (!CPP_NON_DECLARATION_KEYWORDS.has(m[1])) {
76
+ symbols.push({ name: m[1], kind: 'function', isExport: true, startLine: 1, endLine: 1, signature: '' });
77
+ }
78
+ }
79
+
80
+ const rustMatches = content.matchAll(/\b(?:pub\s+)?(?:struct|enum|trait|impl|mod|fn)\s+([A-Za-z_][A-Za-z0-9_]*)/g);
81
+ for (const m of rustMatches) {
82
+ if (!RESERVED_SYMBOL_NAMES.has(m[1])) {
83
+ symbols.push({ name: m[1], kind: 'symbol', isExport: true, startLine: 1, endLine: 1, signature: '' });
84
+ }
85
+ }
86
+
87
+ const kotlinMatches = content.matchAll(/\b(?:data\s+|sealed\s+|open\s+|abstract\s+|inner\s+)?(?:class|object|interface|fun)\s+([A-Za-z_][A-Za-z0-9_]*)/g);
88
+ for (const m of kotlinMatches) {
89
+ if (!RESERVED_SYMBOL_NAMES.has(m[1])) {
90
+ symbols.push({ name: m[1], kind: 'symbol', isExport: true, startLine: 1, endLine: 1, signature: '' });
91
+ }
92
+ }
93
+
94
+ const cppIncludes = content.matchAll(/#include\s*[<"]([^>"]+)[>"]/g);
95
+ for (const m of cppIncludes) {
96
+ imports.push({ importedSymbol: '*', sourceModule: m[1], line: 1 });
97
+ }
98
+
99
+ const rustUses = content.matchAll(/\buse\s+([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)+)/g);
100
+ for (const m of rustUses) {
101
+ imports.push({ importedSymbol: '*', sourceModule: m[1], line: 1 });
102
+ }
103
+
50
104
  const csImports = content.matchAll(/using\s+([A-Za-z0-9_.]+);/g);
51
105
  for (const m of csImports) {
52
106
  imports.push({ importedSymbol: '*', sourceModule: m[1], line: 1 });
package/cli/search.d.ts CHANGED
@@ -101,7 +101,8 @@ export declare function runSearch(
101
101
 
102
102
  export declare function resolveTargetDir(
103
103
  customOrFlag?: string | null,
104
- dirFlag?: string | null
104
+ dirFlag?: string | null,
105
+ cwd?: string
105
106
  ): string;
106
107
 
107
108
  export declare function findSymbolDefinition(
package/cli/search.js CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  import { runGenerateWizard } from './generator.js';
37
37
  import { runMutatorCli } from './mutators.js';
38
38
  import { toColumnar } from './columnar.js';
39
+ import { resolveTargetDir } from './path-scope.js';
39
40
  import { ANSI } from './theme.js';
40
41
 
41
42
  export { toColumnar, fromColumnar } from './columnar.js';
@@ -207,27 +208,7 @@ const formatTierBadge = (tier) => {
207
208
  return map[tier] || `${ANSI.DIM}[${tier}]${ANSI.RESET}`;
208
209
  };
209
210
 
210
- export const resolveTargetDir = (customOrFlag = null, dirFlag = null) => {
211
- const isCustomPath = Boolean(customOrFlag && !customOrFlag.startsWith('--dir='));
212
- if (isCustomPath) {
213
- return customOrFlag;
214
- }
215
-
216
- const effectiveFlag = dirFlag || (customOrFlag?.startsWith('--dir=') ? customOrFlag : null);
217
- if (effectiveFlag) {
218
- const [, flagValue] = effectiveFlag.split('=');
219
- if (flagValue !== undefined) {
220
- return flagValue;
221
- }
222
- }
223
-
224
- const hasSrcDirectory = fs.existsSync('src');
225
- if (hasSrcDirectory) {
226
- return 'src';
227
- }
228
-
229
- return '.';
230
- };
211
+ export { resolveTargetDir };
231
212
 
232
213
  export const printSearchHelp = () => {
233
214
  const BOLD = '\x1b[1m';
@@ -57,6 +57,18 @@ test('resolveTargetDir: defaults to src if present, otherwise . when no flags pr
57
57
  assert.strictEqual(resultNoArgs, expectedDefault);
58
58
  });
59
59
 
60
+ test('resolveTargetDir: defaults to . when .sln or csproj exists at repo root', () => {
61
+ const tmpDir = path.resolve('scratch/test-dotnet-repo');
62
+ fs.mkdirSync(tmpDir, { recursive: true });
63
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
64
+ fs.writeFileSync(path.join(tmpDir, 'Solution.sln'), '', 'utf8');
65
+
66
+ const resolved = resolveTargetDir(null, null, tmpDir);
67
+ assert.strictEqual(resolved, '.');
68
+
69
+ fs.rmSync(tmpDir, { recursive: true, force: true });
70
+ });
71
+
60
72
  test('search-db: indexes symbols with line ranges and finds definition', () => {
61
73
  const db = openIndexDb();
62
74
  if (!db) return;
@@ -265,3 +277,18 @@ test('hybrid search: natural language "toggle a task item" ranks useTaskListCont
265
277
  } catch {}
266
278
  });
267
279
 
280
+
281
+ test('runSearch: resolveTargetDir is bound in module scope, not only re-exported', async () => {
282
+ const { runSearch } = await import('./search.js');
283
+ let caught = null;
284
+ try {
285
+ await runSearch(['__chemx_nonexistent_symbol__'], false);
286
+ } catch (err) {
287
+ caught = err;
288
+ }
289
+ assert.equal(
290
+ caught instanceof ReferenceError,
291
+ false,
292
+ `runSearch threw a ReferenceError: ${caught && caught.message}`
293
+ );
294
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chemx/starter-kit",
3
- "version": "26.9.28-276",
3
+ "version": "26.10.4-235",
4
4
  "description": "Chemical X Protocol: Private drop-in architecture starter kit and capsule generator",
5
5
  "type": "module",
6
6
  "bin": {
@@ -66,8 +66,8 @@ MIN_SCORE="${CHEMX_MIN_SCORE:-${CONF_MIN_SCORE:-80}}"
66
66
  MAX_LINES="${CHEMX_MAX_LINES:-${CONF_MAX_LINES:-500}}"
67
67
  MAX_MOLECULE_LINES="${CHEMX_MAX_MOLECULE_LINES:-${CONF_MAX_MOL:-100}}"
68
68
 
69
- # Detect staged source files
70
- STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(jsx?|tsx?|vue|svelte)$' | grep -vE '(\.d\.ts|\.min\.|\.test\.|\.spec\.)')
69
+ # Detect staged source files (including polyglot C#, Python, Go)
70
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(jsx?|tsx?|vue|svelte|cs|py|go)$' | grep -vE '(\.(d\.ts|min\.|test\.|spec\.))')
71
71
 
72
72
  if [ -z "$STAGED_FILES" ]; then
73
73
  exit 0
@@ -75,6 +75,7 @@ fi
75
75
 
76
76
  LINE_BUDGET_FAILED=0
77
77
  LINE_BUDGET_ERRORS=""
78
+ EXCEEDED_FILES=""
78
79
 
79
80
  for FILE in $STAGED_FILES; do
80
81
  if [ -f "$FILE" ]; then
@@ -86,12 +87,14 @@ for FILE in $STAGED_FILES; do
86
87
  if [ "$LINES" -gt "$MAX_MOLECULE_LINES" ]; then
87
88
  LINE_BUDGET_FAILED=1
88
89
  LINE_BUDGET_ERRORS="${LINE_BUDGET_ERRORS}\n ${C_RED}✕${C_RESET} $FILE ($LINES LOC > $MAX_MOLECULE_LINES LOC molecule capsule limit)"
90
+ EXCEEDED_FILES="${EXCEEDED_FILES}\n- $FILE ($LINES LOC > $MAX_MOLECULE_LINES LOC molecule limit)"
89
91
  fi
90
92
  ;;
91
93
  *)
92
94
  if [ "$LINES" -gt "$MAX_LINES" ]; then
93
95
  LINE_BUDGET_FAILED=1
94
96
  LINE_BUDGET_ERRORS="${LINE_BUDGET_ERRORS}\n ${C_RED}✕${C_RESET} $FILE ($LINES LOC > $MAX_LINES LOC file budget)"
97
+ EXCEEDED_FILES="${EXCEEDED_FILES}\n- $FILE ($LINES LOC > $MAX_LINES LOC file budget)"
95
98
  fi
96
99
  ;;
97
100
  esac
@@ -101,8 +104,17 @@ done
101
104
  if [ "$LINE_BUDGET_FAILED" -eq 1 ]; then
102
105
  printf "\n%s%s[Chemical X] Commit Blocked: Staged files exceed architectural line budgets%s\n" "$C_BOLD" "$C_RED" "$C_RESET"
103
106
  printf "%b\n\n" "$LINE_BUDGET_ERRORS"
104
- printf "%sMonolithic files degrade AI context windows and cause hallucination loops.%s\n" "$C_YELLOW" "$C_RESET"
105
- printf "Decompose large files into single-purpose crystalline capsules before committing.\n\n"
107
+ printf "%s╭──────────────────────────────────────────────────────────────────────────╮%s\n" "$C_CYAN" "$C_RESET"
108
+ printf "%s│ 🤖 AI REFACTOR PROMPT (Copy & paste into your AI assistant): │%s\n" "$C_CYAN" "$C_RESET"
109
+ printf "%s╰──────────────────────────────────────────────────────────────────────────╯%s\n" "$C_CYAN" "$C_RESET"
110
+ printf "Please refactor the following files that exceed Chemical X line budgets:%b\n\n" "$EXCEEDED_FILES"
111
+ printf "Refactor Directives:\n"
112
+ printf "1. Decompose monolithic logic into single-purpose crystalline capsules or helper modules (< %s lines for files, < %s lines for molecules).\n" "$MAX_LINES" "$MAX_MOLECULE_LINES"
113
+ printf "2. Extract presentation into Table-of-Contents views and business state into composables/services.\n"
114
+ printf "3. Preserve all existing symbols, exports, and public API contracts.\n"
115
+ printf "4. Decompose complex inline booleans and flatten nested control flow.\n"
116
+ printf "%s────────────────────────────────────────────────────────────────────────────%s\n\n" "$C_CYAN" "$C_RESET"
117
+ printf "%s💡 Tip: To bypass line budgets temporarily: CHEMX_SKIP_PRECOMMIT=1 git commit%s\n\n" "$C_YELLOW" "$C_RESET"
106
118
  exit 1
107
119
  fi
108
120
 
@@ -127,8 +139,18 @@ if [ -n "$AUDIT_BIN" ]; then
127
139
  if ! AUDIT_OUT=$(eval "$AUDIT_CMD < /dev/null" 2>&1); then
128
140
  printf "\n%s%s[Chemical X] Commit Blocked: Architectural health verification failed%s\n" "$C_BOLD" "$C_RED" "$C_RESET"
129
141
  printf "%s\n\n" "$AUDIT_OUT"
130
- printf "%s💡 Tip: Want crystalline drop-in templates to refactor in minutes?%s\n" "$C_CYAN" "$C_RESET"
131
- printf " Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol\n\n"
142
+ printf "%s╭──────────────────────────────────────────────────────────────────────────╮%s\n" "$C_CYAN" "$C_RESET"
143
+ printf "%s│ 🤖 AI REFACTOR PROMPT (Copy & paste into your AI assistant): │%s\n" "$C_CYAN" "$C_RESET"
144
+ printf "%s╰──────────────────────────────────────────────────────────────────────────╯%s\n" "$C_CYAN" "$C_RESET"
145
+ printf "Please fix the Chemical X architectural hazards reported above in staged files.\n\n"
146
+ printf "Refactor Directives:\n"
147
+ printf "1. Surgically resolve each flagged Critical and High severity hazard.\n"
148
+ printf "2. Decompose monoliths into single-purpose crystalline capsules.\n"
149
+ printf "3. Preserve all existing symbols, exports, and test contracts.\n"
150
+ printf "4. Verify with 'chemx audit' after making changes.\n"
151
+ printf "%s────────────────────────────────────────────────────────────────────────────%s\n\n" "$C_CYAN" "$C_RESET"
152
+ printf "%s💡 Tip: Run 'chemx audit' locally to inspect details or run autofixes.%s\n" "$C_CYAN" "$C_RESET"
153
+ printf " To bypass this check temporarily: CHEMX_SKIP_PRECOMMIT=1 git commit\n\n"
132
154
  exit 1
133
155
  fi
134
156
  fi