genesis-compiler 1.0.0 → 1.2.0

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.
Files changed (73) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +443 -0
  3. package/bin/genesis.js +15 -0
  4. package/docs/assurance-model.md +26 -0
  5. package/docs/prompt-integration.md +98 -0
  6. package/docs/stack-components.md +304 -0
  7. package/package.json +57 -7
  8. package/plugins/genesis/.codex-plugin/plugin.json +19 -0
  9. package/plugins/genesis/hooks.json +18 -0
  10. package/prompts/blueprint.txt +9 -0
  11. package/prompts/describe.txt +17 -0
  12. package/prompts/deslop.txt +14 -0
  13. package/prompts/program.txt +12 -0
  14. package/prompts/reconcile.txt +12 -0
  15. package/prompts/review.txt +12 -0
  16. package/prompts/start.txt +30 -0
  17. package/prompts/work.txt +28 -0
  18. package/skills/genesis-deslop/SKILL.md +36 -0
  19. package/skills/genesis-deslop/agents/openai.yaml +4 -0
  20. package/skills/genesis-program/SKILL.md +66 -0
  21. package/skills/genesis-program/agents/openai.yaml +4 -0
  22. package/skills/genesis-project/SKILL.md +53 -0
  23. package/skills/genesis-project/agents/openai.yaml +4 -0
  24. package/src/cli.js +276 -0
  25. package/src/index/agent-skills.js +425 -0
  26. package/src/index/assets.js +19 -0
  27. package/src/index/blueprint.js +38 -0
  28. package/src/index/check.js +102 -0
  29. package/src/index/code-index.js +283 -0
  30. package/src/index/code-indexers/ast-grep.js +414 -0
  31. package/src/index/codex-hooks.js +367 -0
  32. package/src/index/codex-plugin.js +73 -0
  33. package/src/index/context.js +137 -0
  34. package/src/index/environment-files.js +19 -0
  35. package/src/index/errors.js +26 -0
  36. package/src/index/git.js +26 -0
  37. package/src/index/init.js +48 -0
  38. package/src/index/launch.js +34 -0
  39. package/src/index/paths.js +10 -0
  40. package/src/index/process.js +89 -0
  41. package/src/index/program.js +181 -0
  42. package/src/index/project-files.js +24 -0
  43. package/src/index/project-state.js +87 -0
  44. package/src/index/prompt.js +347 -0
  45. package/src/index/stack-catalog.js +72 -0
  46. package/src/index/stack-command.js +65 -0
  47. package/src/index/stack-composition.js +38 -0
  48. package/src/index/stack-environment-files.js +83 -0
  49. package/src/index/stack-launch.js +428 -0
  50. package/src/index/stack-piece.js +283 -0
  51. package/src/index/stack-preflight.js +25 -0
  52. package/src/index/stack-process.js +25 -0
  53. package/src/index/stack-workspace-setup.js +129 -0
  54. package/src/index/stack.js +302 -0
  55. package/src/index/utils.js +85 -0
  56. package/src/index/verification.js +77 -0
  57. package/src/index/workspace-setup.js +55 -0
  58. package/src/index.js +102 -0
  59. package/stacks/pieces/cpp.md +22 -0
  60. package/stacks/pieces/csharp.md +22 -0
  61. package/stacks/pieces/go.md +22 -0
  62. package/stacks/pieces/java.md +22 -0
  63. package/stacks/pieces/jskit-mysql.md +37 -0
  64. package/stacks/pieces/jskit.md +70 -0
  65. package/stacks/pieces/kotlin.md +22 -0
  66. package/stacks/pieces/mysql.md +18 -0
  67. package/stacks/pieces/nodejs.md +25 -0
  68. package/stacks/pieces/php.md +23 -0
  69. package/stacks/pieces/python.md +23 -0
  70. package/stacks/pieces/ruby.md +22 -0
  71. package/stacks/pieces/rust.md +22 -0
  72. package/stacks/pieces/shell.md +23 -0
  73. package/stacks/pieces/vue.md +19 -0
@@ -0,0 +1,283 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { astGrepCodeIndexers } from './code-indexers/ast-grep.js';
5
+ import { asDiagnostic } from './errors.js';
6
+ import { gitContext } from './git.js';
7
+ import { isProjectContentPath } from './paths.js';
8
+ import { inspectProgram } from './program.js';
9
+ import { gitVisibleFileStates } from './project-files.js';
10
+ import { readStack } from './stack.js';
11
+ import { sha256, stableJson, writeFileAtomic } from './utils.js';
12
+
13
+ export const MACHINE_CITY_PATH = '.genesis/machine-city.json';
14
+ export const PROGRAM_CITY_PATH = '.genesis/program-city.json';
15
+
16
+ const INDEXERS = astGrepCodeIndexers;
17
+
18
+ function directoryId(value) {
19
+ return `directory:${value || '.'}`;
20
+ }
21
+
22
+ function fileId(value) {
23
+ return `file:${value}`;
24
+ }
25
+
26
+ function subsystemId(value) {
27
+ return `subsystem:${value}`;
28
+ }
29
+
30
+ function operationId(module) {
31
+ return `operation:${module.subsystem}/${module.name}`;
32
+ }
33
+
34
+ function parentDirectory(value) {
35
+ const parent = path.posix.dirname(value);
36
+ return parent === '.' ? '' : parent;
37
+ }
38
+
39
+ function directoryRecords(paths) {
40
+ const directories = new Set(['']);
41
+ for (const value of paths) {
42
+ let current = parentDirectory(value);
43
+ while (true) {
44
+ directories.add(current);
45
+ if (!current) break;
46
+ current = parentDirectory(current);
47
+ }
48
+ }
49
+ return [...directories].sort().map((directory) => ({
50
+ id: directoryId(directory),
51
+ path: directory,
52
+ title: directory ? path.posix.basename(directory) : 'Project',
53
+ parentId: directory ? directoryId(parentDirectory(directory)) : null,
54
+ }));
55
+ }
56
+
57
+ function subsystemRecords(subsystems) {
58
+ const paths = new Set();
59
+ for (const subsystem of subsystems) {
60
+ const segments = subsystem.split('/');
61
+ for (let count = 1; count <= segments.length; count += 1) {
62
+ paths.add(segments.slice(0, count).join('/'));
63
+ }
64
+ }
65
+ return [...paths].sort().map((subsystem) => ({
66
+ id: subsystemId(subsystem),
67
+ path: subsystem,
68
+ title: path.posix.basename(subsystem).replace(/-/gu, ' '),
69
+ parentId: subsystem.includes('/') ? subsystemId(parentDirectory(subsystem)) : null,
70
+ }));
71
+ }
72
+
73
+ function normalizedFunction(entry, file, extractor) {
74
+ const qualifiedName = entry.qualifiedName || entry.name;
75
+ return {
76
+ id: `function:${file.path}:${qualifiedName}:${entry.kind}:${entry.line}:${entry.column}`,
77
+ name: entry.name,
78
+ qualifiedName,
79
+ kind: entry.kind,
80
+ visibility: entry.visibility,
81
+ container: entry.container || null,
82
+ path: file.path,
83
+ fileId: fileId(file.path),
84
+ line: entry.line,
85
+ column: entry.column,
86
+ parameters: entry.parameters || [],
87
+ async: entry.async === true,
88
+ generator: entry.generator === true,
89
+ static: entry.static === true,
90
+ role: file.role,
91
+ language: file.language,
92
+ extractor,
93
+ };
94
+ }
95
+
96
+ function mergeIndexedFiles(contributions) {
97
+ const files = new Map();
98
+ const functions = [];
99
+ for (const contribution of contributions) {
100
+ for (const file of contribution.files || []) {
101
+ const existing = files.get(file.path);
102
+ const record = existing || {
103
+ id: fileId(file.path),
104
+ path: file.path,
105
+ language: file.language,
106
+ role: file.role,
107
+ mode: file.mode,
108
+ bytes: file.bytes,
109
+ lines: file.lines,
110
+ hash: file.hash,
111
+ extractors: [],
112
+ functionIds: [],
113
+ };
114
+ if (!record.extractors.includes(contribution.extractor)) record.extractors.push(contribution.extractor);
115
+ for (const entry of file.functions || []) {
116
+ const normalized = normalizedFunction(entry, file, contribution.extractor);
117
+ functions.push(normalized);
118
+ record.functionIds.push(normalized.id);
119
+ }
120
+ files.set(file.path, record);
121
+ }
122
+ }
123
+ return {
124
+ files: [...files.values()].sort((left, right) => left.path.localeCompare(right.path)),
125
+ functions: functions.sort((left, right) => (
126
+ left.path.localeCompare(right.path) || left.line - right.line || left.qualifiedName.localeCompare(right.qualifiedName)
127
+ )),
128
+ };
129
+ }
130
+
131
+ function machineCity({ components, contributions, diagnostics, indexers }) {
132
+ const merged = mergeIndexedFiles(contributions);
133
+ const visibility = new Map(merged.functions.map((entry) => [entry.id, entry.visibility]));
134
+ const codeHash = sha256(stableJson(merged.files.map(({ path: filePath, hash, mode }) => ({
135
+ path: filePath,
136
+ hash,
137
+ mode,
138
+ }))));
139
+ return {
140
+ schema: 'genesis.machine-city.v1',
141
+ schemaVersion: 1,
142
+ status: indexers.length === 0 ? 'unconfigured' : diagnostics.length > 0 ? 'completed-with-warning' : 'current',
143
+ codeHash,
144
+ stackComponents: components,
145
+ indexers,
146
+ diagnostics,
147
+ functions: merged.functions,
148
+ districts: directoryRecords(merged.files.map(({ path: filePath }) => filePath)),
149
+ buildings: merged.files.map((file) => ({
150
+ ...file,
151
+ districtId: directoryId(parentDirectory(file.path)),
152
+ title: path.posix.basename(file.path),
153
+ publicFunctionCount: file.functionIds.filter((id) => visibility.get(id) === 'public').length,
154
+ internalFunctionCount: file.functionIds.filter((id) => visibility.get(id) === 'internal').length,
155
+ })),
156
+ };
157
+ }
158
+
159
+ async function programCity(projectRoot) {
160
+ try {
161
+ const program = await inspectProgram(projectRoot);
162
+ const operations = program.modules.map((module) => ({
163
+ id: operationId(module),
164
+ name: module.name,
165
+ title: module.title,
166
+ description: module.description,
167
+ publicContract: module.publicContract,
168
+ implementationMap: module.implementationMap,
169
+ path: module.path,
170
+ subsystem: module.subsystem,
171
+ districtId: subsystemId(module.subsystem),
172
+ sources: module.sources,
173
+ sourceFileIds: module.sources.map(fileId),
174
+ }));
175
+ return {
176
+ schema: 'genesis.program-city.v1',
177
+ schemaVersion: 1,
178
+ status: program.status,
179
+ programHash: sha256(stableJson(operations)),
180
+ diagnostics: [],
181
+ districts: subsystemRecords(program.subsystems),
182
+ buildings: operations,
183
+ links: operations.flatMap((operation) => operation.sourceFileIds.map((targetId) => ({
184
+ kind: 'implemented-by',
185
+ fromId: operation.id,
186
+ toId: targetId,
187
+ }))),
188
+ };
189
+ } catch (error) {
190
+ return {
191
+ schema: 'genesis.program-city.v1',
192
+ schemaVersion: 1,
193
+ status: 'invalid',
194
+ programHash: sha256('invalid'),
195
+ diagnostics: [asDiagnostic(error)],
196
+ districts: [],
197
+ buildings: [],
198
+ links: [],
199
+ };
200
+ }
201
+ }
202
+
203
+ async function writeIfChanged(projectRoot, relativePath, value) {
204
+ const location = path.join(projectRoot, relativePath);
205
+ const source = stableJson(value);
206
+ try {
207
+ if (await readFile(location, 'utf8') === source) return false;
208
+ } catch (error) {
209
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
210
+ }
211
+ await writeFileAtomic(location, source);
212
+ return true;
213
+ }
214
+
215
+ function functionMatches(entry, queries) {
216
+ if (queries.length === 0) return false;
217
+ const haystack = [entry.name, entry.qualifiedName, entry.path, entry.kind, entry.visibility]
218
+ .join('\n')
219
+ .toLowerCase();
220
+ return queries.every((query) => haystack.includes(query.toLowerCase()));
221
+ }
222
+
223
+ function formatCount(value, singular) {
224
+ return `${value} ${singular}${value === 1 ? '' : 's'}`;
225
+ }
226
+
227
+ export async function buildProjectIndex({ projectRoot, queries = [], write = true } = {}) {
228
+ const root = (await gitContext(projectRoot)).repositoryRoot;
229
+ const stack = await readStack(root);
230
+ const states = await gitVisibleFileStates(root, { includePath: isProjectContentPath });
231
+ const files = [...states]
232
+ .filter(([, state]) => state.exists && !state.symlink && !state.special && state.hash)
233
+ .map(([filePath, state]) => ({ path: filePath, hash: state.hash, mode: state.mode }));
234
+ const indexers = [...new Set(stack.components.flatMap((component) => component.indexers || []))].sort();
235
+ const contributions = [];
236
+ const diagnostics = [];
237
+ for (const id of indexers) {
238
+ const indexer = INDEXERS.get(id);
239
+ if (!indexer) {
240
+ diagnostics.push({ code: 'CODE_INDEXER_UNAVAILABLE', message: `No installed code indexer exists for ${id}.` });
241
+ continue;
242
+ }
243
+ try {
244
+ const contribution = await indexer.extract({ files, projectRoot: root });
245
+ contributions.push({ ...contribution, extractor: id });
246
+ diagnostics.push(...(contribution.diagnostics || []).map((diagnostic) => ({ ...diagnostic, extractor: id })));
247
+ } catch (error) {
248
+ diagnostics.push({ ...asDiagnostic(error), code: 'CODE_INDEXER_FAILED', extractor: id });
249
+ }
250
+ }
251
+ const machine = machineCity({
252
+ components: stack.components.map(({ id }) => id),
253
+ contributions,
254
+ diagnostics,
255
+ indexers,
256
+ });
257
+ const program = await programCity(root);
258
+ const changedFiles = [];
259
+ if (write) {
260
+ if (await writeIfChanged(root, MACHINE_CITY_PATH, machine)) changedFiles.push(MACHINE_CITY_PATH);
261
+ if (await writeIfChanged(root, PROGRAM_CITY_PATH, program)) changedFiles.push(PROGRAM_CITY_PATH);
262
+ }
263
+ const allMatches = machine.functions.filter((entry) => functionMatches(entry, queries));
264
+ const allDiagnostics = [...diagnostics, ...program.diagnostics];
265
+ return {
266
+ status: allDiagnostics.length > 0 ? 'completed-with-warning' : 'ready',
267
+ summary: [
268
+ `Indexed ${formatCount(machine.functions.length, 'function')}`,
269
+ `in ${formatCount(machine.buildings.length, 'file')}`,
270
+ `and ${formatCount(program.buildings.length, 'Program operation')}.`,
271
+ ].join(' '),
272
+ paths: { machine: MACHINE_CITY_PATH, program: PROGRAM_CITY_PATH },
273
+ changedFiles,
274
+ diagnostics: allDiagnostics,
275
+ functionCount: machine.functions.length,
276
+ fileCount: machine.buildings.length,
277
+ operationCount: program.buildings.length,
278
+ totalMatches: allMatches.length,
279
+ matches: allMatches.slice(0, 200),
280
+ machine,
281
+ program,
282
+ };
283
+ }
@@ -0,0 +1,414 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import bashLanguage from '@ast-grep/lang-bash';
5
+ import cLanguage from '@ast-grep/lang-c';
6
+ import cppLanguage from '@ast-grep/lang-cpp';
7
+ import csharpLanguage from '@ast-grep/lang-csharp';
8
+ import goLanguage from '@ast-grep/lang-go';
9
+ import javaLanguage from '@ast-grep/lang-java';
10
+ import kotlinLanguage from '@ast-grep/lang-kotlin';
11
+ import phpLanguage from '@ast-grep/lang-php';
12
+ import pythonLanguage from '@ast-grep/lang-python';
13
+ import rubyLanguage from '@ast-grep/lang-ruby';
14
+ import rustLanguage from '@ast-grep/lang-rust';
15
+ import { parseAsync, registerDynamicLanguage } from '@ast-grep/napi';
16
+
17
+ const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
18
+ const OPENING_PARAMETER_DELIMITERS = '([{<';
19
+ const CLOSING_PARAMETER_DELIMITERS = ')]}>';
20
+ const EXCLUDED_DIRECTORIES = new Set([
21
+ '.next', '.nuxt', '.output', '.svelte-kit', 'build', 'coverage', 'dist',
22
+ 'node_modules', 'storage', 'target', 'vendor',
23
+ ]);
24
+
25
+ registerDynamicLanguage({
26
+ bash: bashLanguage,
27
+ c: cLanguage,
28
+ cpp: cppLanguage,
29
+ csharp: csharpLanguage,
30
+ go: goLanguage,
31
+ java: javaLanguage,
32
+ kotlin: kotlinLanguage,
33
+ php: phpLanguage,
34
+ python: pythonLanguage,
35
+ ruby: rubyLanguage,
36
+ rust: rustLanguage,
37
+ });
38
+
39
+ function javascriptParser(filePath) {
40
+ if (/\.tsx$/u.test(filePath)) return 'Tsx';
41
+ if (/\.ts$/u.test(filePath)) return 'TypeScript';
42
+ if (/\.jsx$/u.test(filePath)) return 'Tsx';
43
+ return 'JavaScript';
44
+ }
45
+
46
+ function cFamilyParser(filePath) {
47
+ return /\.(?:c|h)$/u.test(filePath) ? 'c' : 'cpp';
48
+ }
49
+
50
+ const DEFINITIONS = Object.freeze({
51
+ javascript: {
52
+ extensions: ['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
53
+ kinds: ['function_declaration', 'generator_function_declaration', 'method_definition', 'arrow_function', 'function_expression'],
54
+ parser: javascriptParser,
55
+ },
56
+ python: { extensions: ['.py', '.pyi'], kinds: ['function_definition'], parser: 'python' },
57
+ java: { extensions: ['.java'], kinds: ['method_declaration', 'constructor_declaration'], parser: 'java' },
58
+ csharp: { extensions: ['.cs'], kinds: ['method_declaration', 'constructor_declaration', 'local_function_statement'], parser: 'csharp' },
59
+ cpp: {
60
+ extensions: ['.c', '.cc', '.cpp', '.cxx', '.h', '.hh', '.hpp', '.hxx'],
61
+ kinds: ['function_definition'],
62
+ parser: cFamilyParser,
63
+ },
64
+ php: { extensions: ['.php'], kinds: ['function_definition', 'method_declaration'], parser: 'php' },
65
+ go: { extensions: ['.go'], kinds: ['function_declaration', 'method_declaration'], parser: 'go' },
66
+ rust: { extensions: ['.rs'], kinds: ['function_item'], parser: 'rust' },
67
+ ruby: { extensions: ['.rb', '.rake'], kinds: ['method', 'singleton_method'], parser: 'ruby' },
68
+ kotlin: { extensions: ['.kt', '.kts'], kinds: ['function_declaration', 'secondary_constructor'], parser: 'kotlin' },
69
+ shell: {
70
+ extensions: ['.bash', '.sh'],
71
+ kinds: ['function_definition'],
72
+ parser: 'bash',
73
+ shebang: /^#![^\n]*\b(?:bash|dash|ksh|sh|zsh)\b/u,
74
+ },
75
+ });
76
+ const CALLABLE_KINDS = new Set(Object.values(DEFINITIONS).flatMap(({ kinds }) => kinds));
77
+
78
+ function safeField(node, name) {
79
+ try { return node.field(name); } catch { return null; }
80
+ }
81
+
82
+ function firstDescendant(node, kinds) {
83
+ const wanted = new Set(kinds);
84
+ const pending = [...(node?.children() || [])];
85
+ while (pending.length > 0) {
86
+ const candidate = pending.shift();
87
+ if (wanted.has(candidate.kind())) return candidate;
88
+ pending.unshift(...candidate.children());
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function declaredName(node) {
94
+ const direct = safeField(node, 'name');
95
+ if (direct) return direct.text().trim();
96
+ const parent = node.parent();
97
+ if (['arrow_function', 'function_expression'].includes(node.kind())) {
98
+ if (parent?.kind() === 'variable_declarator') {
99
+ return safeField(parent, 'name')?.text().trim() || parent.child(0)?.text().trim() || '';
100
+ }
101
+ if (parent?.kind() === 'pair') {
102
+ return safeField(parent, 'key')?.text().trim() || parent.child(0)?.text().trim() || '';
103
+ }
104
+ if (parent?.kind() === 'assignment_expression') {
105
+ const target = safeField(parent, 'left')?.text().trim() || parent.child(0)?.text().trim() || '';
106
+ return target.match(/[A-Za-z_$][\w$]*$/u)?.[0] || '';
107
+ }
108
+ return '';
109
+ }
110
+ const declarator = safeField(node, 'declarator');
111
+ if (declarator) {
112
+ const identifier = firstDescendant(declarator, [
113
+ 'destructor_name', 'field_identifier', 'identifier', 'operator_name', 'qualified_identifier',
114
+ ]);
115
+ if (identifier) return identifier.text().trim();
116
+ }
117
+ const ownIdentifier = node.children().find((child) => [
118
+ 'field_identifier', 'identifier', 'name', 'property_identifier', 'simple_identifier',
119
+ ].includes(child.kind()));
120
+ if (ownIdentifier) return ownIdentifier.text().trim();
121
+ return '';
122
+ }
123
+
124
+ function parameterText(node) {
125
+ const direct = safeField(node, 'parameters');
126
+ if (direct) return direct.text().trim();
127
+ const declarator = safeField(node, 'declarator');
128
+ return firstDescendant(declarator, [
129
+ 'formal_parameters', 'parameter_list', 'parameters', 'function_value_parameters',
130
+ ])?.text().trim() || '';
131
+ }
132
+
133
+ function splitParameters(value) {
134
+ const source = value.replace(/^\(/u, '').replace(/\)$/u, '').trim();
135
+ if (!source) return [];
136
+ const result = [];
137
+ let start = 0;
138
+ let depth = 0;
139
+ for (let index = 0; index < source.length; index += 1) {
140
+ if (OPENING_PARAMETER_DELIMITERS.includes(source[index])) depth += 1;
141
+ else if (CLOSING_PARAMETER_DELIMITERS.includes(source[index])) depth = Math.max(0, depth - 1);
142
+ else if (source[index] === ',' && depth === 0) {
143
+ result.push(source.slice(start, index).trim());
144
+ start = index + 1;
145
+ }
146
+ }
147
+ result.push(source.slice(start).trim());
148
+ return result.filter(Boolean);
149
+ }
150
+
151
+ function ancestor(node, kinds) {
152
+ const wanted = new Set(kinds);
153
+ return node.ancestors().find((candidate) => wanted.has(candidate.kind())) || null;
154
+ }
155
+
156
+ const CONTAINER_KINDS = new Set([
157
+ 'class', 'class_declaration', 'class_definition', 'class_specifier', 'enum_declaration',
158
+ 'impl_item', 'interface_declaration', 'module', 'object_declaration', 'struct_item', 'trait_item',
159
+ ]);
160
+
161
+ function containerName(node, language) {
162
+ if (language === 'go' && node.kind() === 'method_declaration') {
163
+ const receiver = safeField(node, 'receiver')?.text().trim();
164
+ if (receiver) return receiver.replace(/^\(|\)$/gu, '').replace(/^\w+\s+\*?/u, '').trim();
165
+ }
166
+ const container = node.ancestors().find((candidate) => CONTAINER_KINDS.has(candidate.kind()));
167
+ if (!container) {
168
+ const parentFunction = node.ancestors().find((candidate) => DEFINITIONS.javascript.kinds.includes(candidate.kind())
169
+ || candidate.kind() === 'function_definition');
170
+ return parentFunction ? declaredName(parentFunction) : null;
171
+ }
172
+ if (container.kind() === 'impl_item') {
173
+ return safeField(container, 'type')?.text().trim()
174
+ || container.text().match(/^\s*impl(?:<[^>]+>)?\s+([^\s{]+)/u)?.[1]
175
+ || null;
176
+ }
177
+ return safeField(container, 'name')?.text().trim()
178
+ || container.children().find((child) => [
179
+ 'constant', 'identifier', 'name', 'simple_identifier', 'type_identifier',
180
+ ].includes(child.kind()))?.text().trim()
181
+ || null;
182
+ }
183
+
184
+ function directText(node) {
185
+ return node.children().slice(0, 4).map((child) => child.text()).join(' ');
186
+ }
187
+
188
+ function cPlusPlusVisibility(node) {
189
+ const classBody = ancestor(node, ['field_declaration_list']);
190
+ if (!classBody) return /^\s*static\b/u.test(node.text()) ? 'internal' : 'public';
191
+ const classNode = ancestor(node, ['class_specifier', 'struct_specifier', 'union_specifier']);
192
+ let current = /^\s*class\b/u.test(classNode?.text() || '') ? 'private' : 'public';
193
+ for (const sibling of classBody.children()) {
194
+ if (sibling.range().start.index >= node.range().start.index) break;
195
+ if (sibling.kind() === 'access_specifier') current = sibling.text().replace(':', '').trim();
196
+ }
197
+ return current === 'public' ? 'public' : 'internal';
198
+ }
199
+
200
+ function rubyVisibility(node) {
201
+ const body = ancestor(node, ['body_statement']);
202
+ if (!body) return declaredName(node).startsWith('_') ? 'internal' : 'public';
203
+ let current = 'public';
204
+ for (const sibling of body.children()) {
205
+ if (sibling.range().start.index >= node.range().start.index) break;
206
+ const text = sibling.text().trim();
207
+ if (['private', 'protected', 'public'].includes(text)) current = text;
208
+ }
209
+ return current === 'public' && !declaredName(node).startsWith('_') ? 'public' : 'internal';
210
+ }
211
+
212
+ function javascriptExportedNames(source) {
213
+ const names = new Set();
214
+ for (const match of source.matchAll(/\bexport\s*\{([^}]+)\}/gu)) {
215
+ for (const item of match[1].split(',')) {
216
+ const local = item.trim().split(/\s+as\s+/u)[0];
217
+ if (local) names.add(local);
218
+ }
219
+ }
220
+ for (const match of source.matchAll(/\b(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=/gu)) names.add(match[1]);
221
+ for (const match of source.matchAll(/\bmodule\.exports\s*=\s*\{([^}]+)\}/gu)) {
222
+ for (const item of match[1].split(',')) {
223
+ const value = item.trim().split(':').at(-1)?.trim();
224
+ if (/^[A-Za-z_$][\w$]*$/u.test(value || '')) names.add(value);
225
+ }
226
+ }
227
+ return names;
228
+ }
229
+
230
+ function javascriptVisibility(node, name, exportedNames) {
231
+ if (name.startsWith('#') || /\b(?:private|protected)\s+/u.test(directText(node))) return 'internal';
232
+ const classNode = ancestor(node, ['class_declaration', 'class_expression']);
233
+ if (classNode) {
234
+ const className = declaredName(classNode);
235
+ const exported = classNode.parent()?.kind() === 'export_statement' || exportedNames.has(className);
236
+ return exported ? 'public' : 'internal';
237
+ }
238
+ const declaration = ['arrow_function', 'function_expression'].includes(node.kind())
239
+ ? ancestor(node, ['lexical_declaration', 'variable_declaration'])
240
+ : node;
241
+ return declaration?.parent()?.kind() === 'export_statement' || exportedNames.has(name)
242
+ ? 'public'
243
+ : 'internal';
244
+ }
245
+
246
+ function visibility(node, name, language, exportedNames) {
247
+ const head = directText(node);
248
+ if (language === 'javascript') return javascriptVisibility(node, name, exportedNames);
249
+ if (language === 'python') {
250
+ const nestedFunction = node.ancestors().some((candidate) => candidate.kind() === 'function_definition');
251
+ return !nestedFunction && !name.startsWith('_') ? 'public' : 'internal';
252
+ }
253
+ if (['java', 'csharp'].includes(language)) return /\bpublic\b/u.test(head) ? 'public' : 'internal';
254
+ if (language === 'cpp') return cPlusPlusVisibility(node);
255
+ if (language === 'php') return /\b(?:private|protected)\b/u.test(head) ? 'internal' : 'public';
256
+ if (language === 'go') return /^\p{Lu}/u.test(name) ? 'public' : 'internal';
257
+ if (language === 'rust') return /^\s*pub(?:\([^)]*\))?\s+/u.test(node.text()) ? 'public' : 'internal';
258
+ if (language === 'ruby') return rubyVisibility(node);
259
+ if (language === 'kotlin') return /\b(?:private|protected|internal)\b/u.test(head) ? 'internal' : 'public';
260
+ if (language === 'shell') return name.startsWith('_') ? 'internal' : 'public';
261
+ return 'internal';
262
+ }
263
+
264
+ function callableKind(node, name) {
265
+ if (node.kind().includes('constructor') || ['__construct', '__init__', 'constructor'].includes(name)) return 'constructor';
266
+ if (node.kind().includes('method')) return 'method';
267
+ const owner = node.ancestors().find((candidate) => (
268
+ CONTAINER_KINDS.has(candidate.kind()) || CALLABLE_KINDS.has(candidate.kind())
269
+ ));
270
+ return owner && CONTAINER_KINDS.has(owner.kind()) ? 'method' : 'function';
271
+ }
272
+
273
+ function scriptUnits(source, filePath, definition) {
274
+ if (!filePath.endsWith('.vue')) {
275
+ const parser = typeof definition.parser === 'function' ? definition.parser(filePath) : definition.parser;
276
+ return [{ source, lineOffset: 0, parser }];
277
+ }
278
+ const units = [];
279
+ for (const match of source.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/giu)) {
280
+ const content = match[2] || '';
281
+ const contentIndex = (match.index || 0) + match[0].indexOf(content);
282
+ const typescript = /\blang\s*=\s*["']tsx?["']/iu.test(match[1] || '');
283
+ const jsx = /\blang\s*=\s*["'](?:jsx|tsx)["']/iu.test(match[1] || '');
284
+ units.push({
285
+ source: content,
286
+ lineOffset: source.slice(0, contentIndex).split('\n').length - 1,
287
+ parser: jsx ? 'Tsx' : typescript ? 'TypeScript' : 'JavaScript',
288
+ });
289
+ }
290
+ return units;
291
+ }
292
+
293
+ function functionsInTree(root, definition, { language, lineOffset, source }) {
294
+ const exportedNames = language === 'javascript' ? javascriptExportedNames(source) : new Set();
295
+ const nodes = new Map();
296
+ for (const kind of definition.kinds) {
297
+ for (const node of root.findAll({ rule: { kind } })) nodes.set(node.id(), node);
298
+ }
299
+ return [...nodes.values()].sort((left, right) => left.range().start.index - right.range().start.index)
300
+ .map((node) => {
301
+ const name = declaredName(node);
302
+ if (!name) return null;
303
+ const container = containerName(node, language);
304
+ const location = node.range().start;
305
+ const parameters = splitParameters(parameterText(node));
306
+ const resolvedVisibility = visibility(node, name, language, exportedNames);
307
+ return {
308
+ name,
309
+ qualifiedName: [container, name].filter(Boolean).join(language === 'php' ? '::' : '.'),
310
+ kind: callableKind(node, name),
311
+ visibility: resolvedVisibility,
312
+ container,
313
+ line: location.line + lineOffset + 1,
314
+ column: location.column + 1,
315
+ parameters,
316
+ async: /^\s*async\b/u.test(node.text()),
317
+ generator: /\bfunction\s*\*|\byield\b/u.test(node.text()),
318
+ static: /\bstatic\b/u.test(directText(node)),
319
+ };
320
+ }).filter(Boolean);
321
+ }
322
+
323
+ function hasSupportedExtension(filePath, definition) {
324
+ const normalized = filePath.toLowerCase();
325
+ return definition.extensions.some((extension) => normalized.endsWith(extension));
326
+ }
327
+
328
+ function isCandidate(file, definition) {
329
+ if (file.path.split('/').some((segment) => EXCLUDED_DIRECTORIES.has(segment))) return false;
330
+ return hasSupportedExtension(file.path, definition)
331
+ || (definition.shebang && file.mode === 0o100755 && path.posix.extname(file.path) === '');
332
+ }
333
+
334
+ function fileRole(filePath) {
335
+ return /(?:^|\/)(?:__tests__|tests?|spec)(?:\/|$)|\.(?:spec|test)\.[^.]+$|(?:Test|Spec)\.[^.]+$/u.test(filePath)
336
+ ? 'test'
337
+ : 'source';
338
+ }
339
+
340
+ function syntaxDiagnostics(root, filePath, lineOffset) {
341
+ return root.findAll({ rule: { kind: 'ERROR' } }).slice(0, 20).map((node) => ({
342
+ code: 'CODE_INDEX_PARSE_RECOVERED',
343
+ message: 'The parser recovered from invalid or unsupported syntax.',
344
+ path: filePath,
345
+ line: node.range().start.line + lineOffset + 1,
346
+ }));
347
+ }
348
+
349
+ function createIndexer(id) {
350
+ const definition = DEFINITIONS[id];
351
+ return {
352
+ id,
353
+ async extract({ files, projectRoot }) {
354
+ const indexedFiles = [];
355
+ const diagnostics = [];
356
+ for (const file of files.filter((candidate) => isCandidate(candidate, definition))) {
357
+ let source;
358
+ try {
359
+ source = await readFile(path.join(projectRoot, file.path), 'utf8');
360
+ } catch (error) {
361
+ diagnostics.push({
362
+ code: 'CODE_INDEX_FILE_READ_FAILED',
363
+ message: error.message,
364
+ path: file.path,
365
+ });
366
+ continue;
367
+ }
368
+ if (Buffer.byteLength(source) > MAX_SOURCE_BYTES) {
369
+ diagnostics.push({
370
+ code: 'CODE_INDEX_FILE_TOO_LARGE',
371
+ message: `Skipped a source file larger than ${MAX_SOURCE_BYTES} bytes.`,
372
+ path: file.path,
373
+ });
374
+ continue;
375
+ }
376
+ if (!hasSupportedExtension(file.path, definition) && !definition.shebang?.test(source)) continue;
377
+ const functions = [];
378
+ for (const unit of scriptUnits(source, file.path, definition)) {
379
+ try {
380
+ const parsed = await parseAsync(unit.parser, unit.source);
381
+ const root = parsed.root();
382
+ functions.push(...functionsInTree(root, definition, {
383
+ language: id,
384
+ lineOffset: unit.lineOffset,
385
+ source: unit.source,
386
+ }));
387
+ diagnostics.push(...syntaxDiagnostics(root, file.path, unit.lineOffset));
388
+ } catch (error) {
389
+ diagnostics.push({
390
+ code: 'CODE_INDEX_PARSE_FAILED',
391
+ message: error.message,
392
+ path: file.path,
393
+ });
394
+ }
395
+ }
396
+ indexedFiles.push({
397
+ path: file.path,
398
+ language: id,
399
+ role: fileRole(file.path),
400
+ mode: file.mode,
401
+ bytes: Buffer.byteLength(source),
402
+ lines: source.length === 0 ? 0 : source.split('\n').length - (source.endsWith('\n') ? 1 : 0),
403
+ hash: file.hash,
404
+ functions,
405
+ });
406
+ }
407
+ return { files: indexedFiles, diagnostics };
408
+ },
409
+ };
410
+ }
411
+
412
+ export const astGrepCodeIndexers = new Map(
413
+ Object.keys(DEFINITIONS).map((id) => [id, createIndexer(id)]),
414
+ );