@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.
- package/cli/audit/csharp-analyzer.js +360 -7
- package/cli/audit/csharp-analyzer.spec.js +216 -0
- package/cli/audit/metrics.js +5 -3
- package/cli/audit/reporter-markdown.js +1 -1
- package/cli/audit/reporter-sections.js +1 -1
- package/cli/audit/rules-registry.js +10 -0
- package/cli/audit/rules.js +1 -1
- package/cli/audit.js +16 -8
- package/cli/commands/cmd-audit.js +6 -4
- package/cli/installer-templates.js +37 -6
- package/cli/languages.js +8 -0
- package/cli/languages.spec.js +21 -0
- package/cli/path-scope.js +50 -0
- package/cli/polyglot.spec.js +111 -0
- package/cli/search-ast.js +54 -0
- package/cli/search.d.ts +2 -1
- package/cli/search.js +2 -21
- package/cli/search.spec.js +27 -0
- package/package.json +1 -1
- package/scripts/pre-commit.sh +28 -6
package/cli/audit/rules.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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" ]
|
|
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" ]
|
|
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 "%
|
|
85
|
-
printf "
|
|
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
|
|
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
|
|
package/cli/languages.spec.js
CHANGED
|
@@ -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
|
+
};
|
package/cli/polyglot.spec.js
CHANGED
|
@@ -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
|
|
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';
|
package/cli/search.spec.js
CHANGED
|
@@ -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
package/scripts/pre-commit.sh
CHANGED
|
@@ -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
|
|
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 "%
|
|
105
|
-
printf "
|
|
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
|
|
131
|
-
printf "
|
|
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
|