@chem-x/starter-kit 26.9.28-1234 → 26.10.4-1258
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/ai-slop-detector.js +29 -1
- package/cli/audit/slop-string-literal.spec.js +34 -0
- package/cli/languages.js +8 -0
- package/cli/languages.spec.js +21 -0
- package/cli/polyglot.spec.js +111 -0
- package/cli/reader-polyglot.spec.js +47 -0
- package/cli/reader.js +23 -0
- package/cli/search-ast.js +54 -0
- package/cli/search.js +2 -1
- package/cli/search.spec.js +15 -0
- package/package.json +1 -1
|
@@ -73,10 +73,38 @@ const normalizeWords = (text) => {
|
|
|
73
73
|
.filter((w) => w.length > 1 && !STOP_WORDS.has(w));
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
// A line-based scan cannot tell a comment from a string that quotes one. Walk the line
|
|
77
|
+
// up to the match and track quote state: if a quote is still open at that offset, the
|
|
78
|
+
// match sits inside a string literal and is describing the pattern, not committing it.
|
|
79
|
+
// Lines that are wholly a comment short-circuit, so apostrophes in prose cannot open a
|
|
80
|
+
// phantom string and suppress a real finding.
|
|
81
|
+
const COMMENT_LINE_START = /^\s*(?:\/\/|#|\*|--)/;
|
|
82
|
+
|
|
83
|
+
const isInsideStringLiteral = (lineText, matchIndex) => {
|
|
84
|
+
if (matchIndex <= 0) return false;
|
|
85
|
+
if (COMMENT_LINE_START.test(lineText)) return false;
|
|
86
|
+
|
|
87
|
+
let quote = null;
|
|
88
|
+
for (let i = 0; i < matchIndex; i += 1) {
|
|
89
|
+
const ch = lineText[i];
|
|
90
|
+
if (ch === '\\') {
|
|
91
|
+
i += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (quote) {
|
|
95
|
+
if (ch === quote) quote = null;
|
|
96
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
97
|
+
quote = ch;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return quote !== null;
|
|
101
|
+
};
|
|
102
|
+
|
|
76
103
|
export const checkSlopTextPatterns = (content, lines, relativePath, violations) => {
|
|
77
104
|
lines.forEach((lineText, idx) => {
|
|
78
105
|
for (const pat of CONVERSATIONAL_PATTERNS) {
|
|
79
|
-
|
|
106
|
+
const hit = pat.regex.exec(lineText);
|
|
107
|
+
if (hit && !isInsideStringLiteral(lineText, hit.index)) {
|
|
80
108
|
const meta = RULE_REGISTRY[pat.rule];
|
|
81
109
|
violations.push({
|
|
82
110
|
filePath: relativePath,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { auditCode } from './rules.js';
|
|
4
|
+
|
|
5
|
+
const fired = (code, file = 'src/thing.js') =>
|
|
6
|
+
auditCode(code, file, file).some((v) => v.rule === 'AI_SLOP_LAZY_PLACEHOLDER');
|
|
7
|
+
|
|
8
|
+
test('slop: real truncation placeholder in a comment still fires', () => {
|
|
9
|
+
assert.equal(fired('function a() {\n // ... rest of code\n}\n'), true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('slop: trailing truncation comment after code still fires', () => {
|
|
13
|
+
assert.equal(fired('doThing(); // ... remaining implementation\n'), true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('slop: placeholder text inside a double-quoted string does not fire', () => {
|
|
17
|
+
const code = 'const copy = "Blocks lazy \'// ...rest of code\' truncation placeholders";\n';
|
|
18
|
+
assert.equal(fired(code), false, 'marketing copy describing the pattern must not be flagged');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('slop: placeholder text inside a single-quoted string does not fire', () => {
|
|
22
|
+
const code = "const copy = 'we detect // ... existing code markers';\n";
|
|
23
|
+
assert.equal(fired(code), false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('slop: placeholder inside a template literal does not fire', () => {
|
|
27
|
+
const code = 'const copy = `catches // ... remaining logic in your diff`;\n';
|
|
28
|
+
assert.equal(fired(code), false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('slop: string-literal guard applies to non-JS languages too', () => {
|
|
32
|
+
const py = 'DESCRIPTION = "flags // ... rest of code placeholders"\n';
|
|
33
|
+
assert.equal(fired(py, 'services/detect.py'), false);
|
|
34
|
+
});
|
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/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
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { generateAstOutline } from './reader.js';
|
|
4
|
+
|
|
5
|
+
const CPP = `
|
|
6
|
+
#include <memory>
|
|
7
|
+
namespace audio {
|
|
8
|
+
struct FrameHeader { uint32_t size; };
|
|
9
|
+
class StreamDecoder : public IDecoder {
|
|
10
|
+
public:
|
|
11
|
+
bool DecodeFrame(const FrameHeader& header);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const PY = `
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
class ConsentService:
|
|
20
|
+
def revoke(self, user_id):
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
def helper(x):
|
|
24
|
+
return x
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
test('outline: renders C++ declarations instead of an empty header', () => {
|
|
28
|
+
const out = generateAstOutline(CPP, 'src/audio/StreamDecoder.cpp');
|
|
29
|
+
const body = out.split('\n').filter((l) => !l.startsWith('// Outline:'));
|
|
30
|
+
assert.ok(body.length > 0, `expected symbols, got only the header:\n${out}`);
|
|
31
|
+
assert.ok(out.includes('StreamDecoder'), `expected StreamDecoder in:\n${out}`);
|
|
32
|
+
assert.ok(out.includes('FrameHeader'), `expected FrameHeader in:\n${out}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('outline: renders Python declarations instead of an empty header', () => {
|
|
36
|
+
const out = generateAstOutline(PY, 'services/consent.py');
|
|
37
|
+
const body = out.split('\n').filter((l) => !l.startsWith('// Outline:'));
|
|
38
|
+
assert.ok(body.length > 0, `expected symbols, got only the header:\n${out}`);
|
|
39
|
+
assert.ok(out.includes('ConsentService'), `expected ConsentService in:\n${out}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('outline: still renders JavaScript exports unchanged', () => {
|
|
43
|
+
const js = `export const FOO = 1;\nexport function bar(a) { return a; }\n`;
|
|
44
|
+
const out = generateAstOutline(js, 'cli/thing.js');
|
|
45
|
+
assert.ok(out.includes('FOO'), out);
|
|
46
|
+
assert.ok(out.includes('bar'), out);
|
|
47
|
+
});
|
package/cli/reader.js
CHANGED
|
@@ -4,6 +4,8 @@ import { parse } from '@babel/parser';
|
|
|
4
4
|
import traverseModule from '@babel/traverse';
|
|
5
5
|
import { ANSI } from './theme.js';
|
|
6
6
|
import { resolveSafePath } from './path-scope.js';
|
|
7
|
+
import { isBabelParsable } from './languages.js';
|
|
8
|
+
import { extractAstMetadata } from './search-ast.js';
|
|
7
9
|
|
|
8
10
|
import {
|
|
9
11
|
stripCodeComments,
|
|
@@ -34,6 +36,12 @@ const traverse = traverseModule.default || traverseModule;
|
|
|
34
36
|
* @param {string} filePath File path for parser context.
|
|
35
37
|
* @returns {string} Compressed structural outline.
|
|
36
38
|
*/
|
|
39
|
+
const OUTLINE_KIND_LABELS = {
|
|
40
|
+
class: 'class',
|
|
41
|
+
function: 'function',
|
|
42
|
+
symbol: 'symbol'
|
|
43
|
+
};
|
|
44
|
+
|
|
37
45
|
export const generateAstOutline = (code, filePath) => {
|
|
38
46
|
const isVue = filePath.endsWith('.vue');
|
|
39
47
|
const isSvelte = filePath.endsWith('.svelte');
|
|
@@ -52,6 +60,21 @@ export const generateAstOutline = (code, filePath) => {
|
|
|
52
60
|
const lines = [];
|
|
53
61
|
lines.push(`// Outline: ${filePath}`);
|
|
54
62
|
|
|
63
|
+
// Babel cannot parse C/C++, Python, Go, Rust, Java, C# or Kotlin. It also does not
|
|
64
|
+
// throw on them, because errorRecovery swallows the failure and yields an empty AST,
|
|
65
|
+
// so the catch-block fallback below never fires for these files. Route them to the
|
|
66
|
+
// polyglot extractor instead.
|
|
67
|
+
if (!isBabelParsable(filePath)) {
|
|
68
|
+
const meta = extractAstMetadata(scriptContent, filePath);
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (const sym of meta.symbols) {
|
|
71
|
+
if (!sym?.name || seen.has(sym.name)) continue;
|
|
72
|
+
seen.add(sym.name);
|
|
73
|
+
lines.push(`${OUTLINE_KIND_LABELS[sym.kind] || 'symbol'} ${sym.name}`);
|
|
74
|
+
}
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
try {
|
|
56
79
|
const ast = parse(scriptContent, {
|
|
57
80
|
sourceType: 'module',
|
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.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,7 +208,7 @@ const formatTierBadge = (tier) => {
|
|
|
207
208
|
return map[tier] || `${ANSI.DIM}[${tier}]${ANSI.RESET}`;
|
|
208
209
|
};
|
|
209
210
|
|
|
210
|
-
export { resolveTargetDir }
|
|
211
|
+
export { resolveTargetDir };
|
|
211
212
|
|
|
212
213
|
export const printSearchHelp = () => {
|
|
213
214
|
const BOLD = '\x1b[1m';
|
package/cli/search.spec.js
CHANGED
|
@@ -277,3 +277,18 @@ test('hybrid search: natural language "toggle a task item" ranks useTaskListCont
|
|
|
277
277
|
} catch {}
|
|
278
278
|
});
|
|
279
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
|
+
});
|