@chemx/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.
@@ -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
- if (pat.regex.test(lineText)) {
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
 
@@ -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
+ });
@@ -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 } from './path-scope.js';
211
+ export { resolveTargetDir };
211
212
 
212
213
  export const printSearchHelp = () => {
213
214
  const BOLD = '\x1b[1m';
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chemx/starter-kit",
3
- "version": "26.9.28-1234",
3
+ "version": "26.10.4-1258",
4
4
  "description": "Chemical X Protocol: Private drop-in architecture starter kit and capsule generator",
5
5
  "type": "module",
6
6
  "bin": {