genesis-compiler 1.5.2 → 1.7.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.
- package/README.md +66 -1
- package/docs/parsers.md +46 -0
- package/package.json +6 -13
- package/prompts/adopt.txt +15 -0
- package/prompts/describe.txt +16 -1
- package/prompts/program.txt +16 -1
- package/prompts/review.txt +2 -0
- package/prompts/work.txt +15 -0
- package/skills/genesis-program/SKILL.md +57 -1
- package/skills/genesis-project/SKILL.md +17 -0
- package/src/cli.js +51 -14
- package/src/index/code-index.js +10 -35
- package/src/index/code-indexers/ast-grep.js +40 -28
- package/src/index/code-indexers/parsers.js +231 -0
- package/src/index/code-indexers/worker.js +20 -0
- package/src/index/context.js +6 -0
- package/src/index/contracts.js +1 -0
- package/src/index/init.js +2 -0
- package/src/index/process.js +7 -1
- package/src/index/program.js +15 -9
- package/src/index/project-inspection.js +1 -0
- package/src/index/prompt.js +2 -0
- package/src/index/session-context.js +2 -0
- package/src/index/subsystems.js +109 -0
- package/src/index.js +3 -0
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
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';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { asDiagnostic } from '../errors.js';
|
|
6
|
+
import { runProcess } from '../process.js';
|
|
7
|
+
import { parserAbortSignal, prepareCodeParsers } from './parsers.js';
|
|
16
8
|
|
|
17
9
|
const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
18
10
|
const OPENING_PARAMETER_DELIMITERS = '([{<';
|
|
@@ -22,20 +14,6 @@ const EXCLUDED_DIRECTORIES = new Set([
|
|
|
22
14
|
'node_modules', 'storage', 'target', 'vendor',
|
|
23
15
|
]);
|
|
24
16
|
|
|
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
17
|
function javascriptParser(filePath) {
|
|
40
18
|
if (/\.tsx$/u.test(filePath)) return 'Tsx';
|
|
41
19
|
if (/\.ts$/u.test(filePath)) return 'TypeScript';
|
|
@@ -350,7 +328,7 @@ function createIndexer(id) {
|
|
|
350
328
|
const definition = DEFINITIONS[id];
|
|
351
329
|
return {
|
|
352
330
|
id,
|
|
353
|
-
async extract({ files, projectRoot }) {
|
|
331
|
+
async extract({ files, projectRoot, parseAsync, parserErrors = {} }) {
|
|
354
332
|
const indexedFiles = [];
|
|
355
333
|
const diagnostics = [];
|
|
356
334
|
for (const file of files.filter((candidate) => isCandidate(candidate, definition))) {
|
|
@@ -377,6 +355,10 @@ function createIndexer(id) {
|
|
|
377
355
|
const functions = [];
|
|
378
356
|
for (const unit of scriptUnits(source, file.path, definition)) {
|
|
379
357
|
try {
|
|
358
|
+
if (parserErrors[unit.parser]) {
|
|
359
|
+
diagnostics.push({ ...parserErrors[unit.parser], path: file.path });
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
380
362
|
const parsed = await parseAsync(unit.parser, unit.source);
|
|
381
363
|
const root = parsed.root();
|
|
382
364
|
functions.push(...functionsInTree(root, definition, {
|
|
@@ -386,8 +368,9 @@ function createIndexer(id) {
|
|
|
386
368
|
}));
|
|
387
369
|
diagnostics.push(...syntaxDiagnostics(root, file.path, unit.lineOffset));
|
|
388
370
|
} catch (error) {
|
|
371
|
+
if (error.code === 'PROCESS_ABORTED') throw error;
|
|
389
372
|
diagnostics.push({
|
|
390
|
-
code: 'CODE_INDEX_PARSE_FAILED',
|
|
373
|
+
code: error.code?.startsWith('PARSER_') ? error.code : 'CODE_INDEX_PARSE_FAILED',
|
|
391
374
|
message: error.message,
|
|
392
375
|
path: file.path,
|
|
393
376
|
});
|
|
@@ -412,3 +395,32 @@ function createIndexer(id) {
|
|
|
412
395
|
export const astGrepCodeIndexers = new Map(
|
|
413
396
|
Object.keys(DEFINITIONS).map((id) => [id, createIndexer(id)]),
|
|
414
397
|
);
|
|
398
|
+
|
|
399
|
+
// ast-grep permits one registration per process. A finite indexing child loads
|
|
400
|
+
// this request's languages once and releases every native parser when it exits.
|
|
401
|
+
export async function extractCodeIndexes({ indexers, files, projectRoot }) {
|
|
402
|
+
const selected = indexers.filter(id => astGrepCodeIndexers.has(id));
|
|
403
|
+
const diagnostics = indexers.filter(id => !astGrepCodeIndexers.has(id)).map(id => ({
|
|
404
|
+
code: 'CODE_INDEXER_UNAVAILABLE', message: `No installed code indexer exists for ${id}.`,
|
|
405
|
+
}));
|
|
406
|
+
const languages = selected.flatMap(id => files.filter(file => isCandidate(file, DEFINITIONS[id]))
|
|
407
|
+
.map(file => typeof DEFINITIONS[id].parser === 'function'
|
|
408
|
+
? DEFINITIONS[id].parser(file.path) : DEFINITIONS[id].parser));
|
|
409
|
+
if (languages.length === 0) return { contributions: [], diagnostics };
|
|
410
|
+
const { registrations, errors } = await prepareCodeParsers(languages);
|
|
411
|
+
let contributions;
|
|
412
|
+
try {
|
|
413
|
+
const result = await runProcess(process.execPath, [fileURLToPath(new URL('./worker.js', import.meta.url))], {
|
|
414
|
+
input: JSON.stringify({ indexers: selected, files, projectRoot, registrations, errors }),
|
|
415
|
+
signal: parserAbortSignal(), timeoutMs: 180_000, code: 'CODE_INDEXER_FAILED',
|
|
416
|
+
});
|
|
417
|
+
contributions = JSON.parse(result.stdout.toString('utf8'));
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (error.code === 'PROCESS_ABORTED') throw error;
|
|
420
|
+
return { contributions: [], diagnostics: [...diagnostics, {
|
|
421
|
+
...asDiagnostic(error), code: 'CODE_INDEXER_FAILED',
|
|
422
|
+
}] };
|
|
423
|
+
}
|
|
424
|
+
return { contributions, diagnostics: [...diagnostics, ...contributions.flatMap(value =>
|
|
425
|
+
value.diagnostics.map(diagnostic => ({ ...diagnostic, extractor: value.extractor }))) ] };
|
|
426
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { access, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { GenesisError, fail } from '../errors.js';
|
|
8
|
+
import { runProcess } from '../process.js';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const PACKAGES = Object.freeze({
|
|
12
|
+
bash: ['0.0.8', 'function example() { echo hello; }'],
|
|
13
|
+
c: ['0.0.6', 'int example(void) { return 1; }'],
|
|
14
|
+
cpp: ['0.0.6', 'int example() { return 1; }'],
|
|
15
|
+
csharp: ['0.0.6', 'class Example { public int Run() { return 1; } }'],
|
|
16
|
+
go: ['0.0.6', 'package example\nfunc Example() int { return 1 }'],
|
|
17
|
+
java: ['0.0.7', 'class Example { public int run() { return 1; } }'],
|
|
18
|
+
kotlin: ['0.0.7', 'fun example(): Int { return 1 }'],
|
|
19
|
+
php: ['0.0.7', '<?php function example() { return 1; }'],
|
|
20
|
+
python: ['0.0.6', 'def example():\n return 1\n'],
|
|
21
|
+
ruby: ['0.0.7', 'def example\n 1\nend'],
|
|
22
|
+
rust: ['0.0.7', 'fn example() -> i32 { 1 }'],
|
|
23
|
+
});
|
|
24
|
+
const installations = new Map();
|
|
25
|
+
const cancellation = new AsyncLocalStorage();
|
|
26
|
+
|
|
27
|
+
export function parserAbortSignal() {
|
|
28
|
+
return cancellation.getStore();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function withParserCancellation(signal, operation) {
|
|
32
|
+
return cancellation.run(signal, operation);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parserDefinition(language) {
|
|
36
|
+
if (language === 'javascript') return { language, package: '@ast-grep/napi', version: require('@ast-grep/napi/package.json').version, sample: 'function example() { return 1; }' };
|
|
37
|
+
if (!Object.hasOwn(PACKAGES, language)) {
|
|
38
|
+
fail('PARSER_UNKNOWN', `Unknown parser: ${language}. Run genesis parsers list.`);
|
|
39
|
+
}
|
|
40
|
+
const [version, sample] = PACKAGES[language];
|
|
41
|
+
return { language, package: `@ast-grep/lang-${language}`, version, sample };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parserRoot(directory, environment = process.env) {
|
|
45
|
+
const root = directory || environment.GENESIS_PARSER_ROOT
|
|
46
|
+
|| path.join(environment.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'genesis', 'parsers');
|
|
47
|
+
if (!path.isAbsolute(root)) fail('PARSER_ROOT_INVALID', 'The parser directory must be an absolute path.');
|
|
48
|
+
return root;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parserEnvironment({ directory, autoInstall, environment = process.env } = {}) {
|
|
52
|
+
const automatic = autoInstall === undefined ? environment.GENESIS_PARSER_AUTO_INSTALL ?? '1' : autoInstall ? '1' : '0';
|
|
53
|
+
if (!['0', '1'].includes(automatic)) fail('PARSER_POLICY_INVALID', 'GENESIS_PARSER_AUTO_INSTALL must be 0 or 1.');
|
|
54
|
+
return { GENESIS_PARSER_ROOT: parserRoot(directory, environment), GENESIS_PARSER_AUTO_INSTALL: automatic };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parserLocation(definition, root) {
|
|
58
|
+
return path.join(root, definition.language, definition.version, `${process.platform}-${process.arch}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function installedParser(definition, directory) {
|
|
62
|
+
if (definition.language === 'javascript') return { entry: '', packageRoot: null };
|
|
63
|
+
try {
|
|
64
|
+
await access(directory);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.code === 'ENOENT') return null;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
const packageRoot = path.join(directory, 'node_modules', definition.package);
|
|
70
|
+
try {
|
|
71
|
+
const manifest = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
72
|
+
if (manifest.name !== definition.package || manifest.version !== definition.version) {
|
|
73
|
+
throw new Error(`Expected ${definition.package}@${definition.version}.`);
|
|
74
|
+
}
|
|
75
|
+
const entry = path.join(packageRoot, 'index.js');
|
|
76
|
+
await access(entry);
|
|
77
|
+
return { entry, packageRoot };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new GenesisError('PARSER_INSTALL_INVALID', `Parser installation is invalid at ${directory}: ${error.message}`, { directory });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Validate in a child so its native library is closed before moving the install,
|
|
84
|
+
// and preparing all languages never loads them into the hosting process.
|
|
85
|
+
async function verifyParser(definition, installation, signal) {
|
|
86
|
+
const source = `
|
|
87
|
+
const { createRequire } = await import('node:module');
|
|
88
|
+
const require = createRequire(import.meta.url);
|
|
89
|
+
const [core, entry, language, sample] = process.argv.slice(1);
|
|
90
|
+
const { registerDynamicLanguage, parseAsync } = require(core);
|
|
91
|
+
const definition = entry ? require(entry) : null;
|
|
92
|
+
if (definition) registerDynamicLanguage({ [language]: definition });
|
|
93
|
+
const root = (await parseAsync(definition ? language : 'JavaScript', sample)).root();
|
|
94
|
+
if (root.findAll({ rule: { kind: 'ERROR' } }).length) throw new Error('Parser smoke check failed.');
|
|
95
|
+
process.stdout.write(definition?.libraryPath || '');
|
|
96
|
+
`;
|
|
97
|
+
const result = await runProcess(process.execPath, [
|
|
98
|
+
'--input-type=module', '-e', source,
|
|
99
|
+
require.resolve('@ast-grep/napi'), installation.entry, definition.language, definition.sample,
|
|
100
|
+
], { signal, timeoutMs: 30_000, code: 'PARSER_VERIFY_FAILED' });
|
|
101
|
+
return result.stdout.toString('utf8');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function installParser(definition, root, { signal, onProgress, environment = process.env }) {
|
|
105
|
+
const destination = parserLocation(definition, root);
|
|
106
|
+
const existing = await installedParser(definition, destination);
|
|
107
|
+
if (existing) return existing;
|
|
108
|
+
onProgress?.(`Preparing ${definition.language} code indexing (${definition.package}@${definition.version})…`);
|
|
109
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
110
|
+
const staging = await mkdtemp(`${destination}.install-`);
|
|
111
|
+
try {
|
|
112
|
+
await writeFile(path.join(staging, 'package.json'), JSON.stringify({ private: true }));
|
|
113
|
+
// Windows cannot spawn npm.cmd without a shell; invoke npm's JavaScript
|
|
114
|
+
// entry with Node so cache paths are never interpreted as shell code.
|
|
115
|
+
const npmCli = process.platform === 'win32'
|
|
116
|
+
? environment.npm_execpath || path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js')
|
|
117
|
+
: null;
|
|
118
|
+
await runProcess(npmCli ? process.execPath : 'npm', [
|
|
119
|
+
...(npmCli ? [npmCli] : []),
|
|
120
|
+
'install', '--prefix', staging, '--ignore-scripts', '--no-audit', '--no-fund',
|
|
121
|
+
'--omit=dev', '--omit=optional', '--package-lock=false', '--save-exact',
|
|
122
|
+
'--registry=https://registry.npmjs.org', `${definition.package}@${definition.version}`,
|
|
123
|
+
], { cwd: staging, env: environment, signal, timeoutMs: 180_000, code: 'PARSER_INSTALL_FAILED' });
|
|
124
|
+
const installation = await installedParser(definition, staging);
|
|
125
|
+
const libraryPath = await verifyParser(definition, installation, signal);
|
|
126
|
+
const prebuilds = path.join(installation.packageRoot, 'prebuilds');
|
|
127
|
+
const selected = path.dirname(libraryPath);
|
|
128
|
+
if (path.dirname(selected) !== prebuilds) {
|
|
129
|
+
fail('PARSER_PLATFORM_UNSUPPORTED', `No packaged parser binary is available for ${process.platform}-${process.arch}.`);
|
|
130
|
+
}
|
|
131
|
+
// The prepared cache is for this host. Keep upstream metadata and loader,
|
|
132
|
+
// but omit build sources and binaries for other operating systems.
|
|
133
|
+
await rm(path.join(installation.packageRoot, 'src'), { recursive: true, force: true });
|
|
134
|
+
for (const entry of await readdir(prebuilds)) {
|
|
135
|
+
if (path.join(prebuilds, entry) !== selected) {
|
|
136
|
+
await rm(path.join(prebuilds, entry), { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await verifyParser(definition, installation, signal);
|
|
140
|
+
try {
|
|
141
|
+
await rename(staging, destination);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Concurrent processes may prepare the same immutable version. Only a
|
|
144
|
+
// complete, independently verified directory can win the atomic rename.
|
|
145
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(error.code)) throw error;
|
|
146
|
+
await installedParser(definition, destination);
|
|
147
|
+
}
|
|
148
|
+
return await installedParser(definition, destination);
|
|
149
|
+
} finally {
|
|
150
|
+
await rm(staging, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ensureInstalled(definition, root, options) {
|
|
155
|
+
const key = parserLocation(definition, root);
|
|
156
|
+
if (!installations.has(key)) {
|
|
157
|
+
const pending = installParser(definition, root, options).finally(() => installations.delete(key));
|
|
158
|
+
installations.set(key, pending);
|
|
159
|
+
}
|
|
160
|
+
return installations.get(key);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function selectedParsers(languages, all) {
|
|
164
|
+
if (all && languages.length) fail('PARSER_SELECTION_INVALID', 'Choose parser names or --all, not both.');
|
|
165
|
+
if (!all && !languages.length) fail('PARSER_SELECTION_REQUIRED', 'Name a parser or use --all.');
|
|
166
|
+
return [...new Set(all ? ['javascript', ...Object.keys(PACKAGES)] : languages)].map(parserDefinition);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function listParsers({ directory, environment = process.env } = {}) {
|
|
170
|
+
const root = parserRoot(directory, environment);
|
|
171
|
+
const parsers = [{ language: 'javascript', status: 'built-in', package: '@ast-grep/napi' }];
|
|
172
|
+
for (const language of Object.keys(PACKAGES)) {
|
|
173
|
+
const definition = parserDefinition(language);
|
|
174
|
+
try {
|
|
175
|
+
const installed = await installedParser(definition, parserLocation(definition, root));
|
|
176
|
+
parsers.push({ language, package: definition.package, version: definition.version, status: installed ? 'installed' : 'missing' });
|
|
177
|
+
} catch (error) {
|
|
178
|
+
parsers.push({ language, package: definition.package, version: definition.version, status: 'invalid', message: error.message });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { status: 'ready', directory: root, parsers };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function installParsers({ languages = [], all = false, directory, environment = process.env, signal, onProgress } = {}) {
|
|
185
|
+
const definitions = selectedParsers(languages, all);
|
|
186
|
+
const root = parserRoot(directory, environment);
|
|
187
|
+
for (const definition of definitions) {
|
|
188
|
+
const installation = await ensureInstalled(definition, root, { signal, onProgress, environment });
|
|
189
|
+
await verifyParser(definition, installation, signal);
|
|
190
|
+
}
|
|
191
|
+
return { status: 'ready', directory: root, languages: definitions.map(({ language }) => language) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function verifyParsers({ languages = [], all = false, directory, environment = process.env, signal } = {}) {
|
|
195
|
+
const definitions = selectedParsers(languages, all);
|
|
196
|
+
const root = parserRoot(directory, environment);
|
|
197
|
+
for (const definition of definitions) {
|
|
198
|
+
const installation = await installedParser(definition, parserLocation(definition, root));
|
|
199
|
+
if (!installation) fail('PARSER_NOT_INSTALLED', `${definition.language} is not installed in ${root}. Run genesis parsers install ${definition.language}.`);
|
|
200
|
+
await verifyParser(definition, installation, signal);
|
|
201
|
+
}
|
|
202
|
+
return { status: 'ready', directory: root, languages: definitions.map(({ language }) => language) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function prepareCodeParsers(languages, { environment = process.env, signal = cancellation.getStore() } = {}) {
|
|
206
|
+
const policy = parserEnvironment({ environment });
|
|
207
|
+
const root = policy.GENESIS_PARSER_ROOT;
|
|
208
|
+
const registrations = {};
|
|
209
|
+
const errors = {};
|
|
210
|
+
for (const language of new Set(languages)) {
|
|
211
|
+
if (['JavaScript', 'TypeScript', 'Tsx'].includes(language)) continue;
|
|
212
|
+
try {
|
|
213
|
+
const definition = parserDefinition(language);
|
|
214
|
+
let installation = await installedParser(definition, parserLocation(definition, root));
|
|
215
|
+
if (!installation) {
|
|
216
|
+
if (policy.GENESIS_PARSER_AUTO_INSTALL === '0') {
|
|
217
|
+
fail('PARSER_NOT_INSTALLED', `${language} is not installed in ${root}; automatic parser installation is disabled.`);
|
|
218
|
+
}
|
|
219
|
+
installation = await ensureInstalled(definition, root, {
|
|
220
|
+
signal, environment, onProgress: (message) => process.stderr.write(`[genesis] ${message}\n`),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// Read the upstream loader's data without loading the native library.
|
|
224
|
+
registrations[language] = { ...require(installation.entry) };
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error.code === 'PROCESS_ABORTED') throw error;
|
|
227
|
+
errors[language] = { code: error.code || 'PARSER_LOAD_FAILED', message: error.message };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { registrations, errors };
|
|
231
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { parseAsync, registerDynamicLanguage } from '@ast-grep/napi';
|
|
2
|
+
import { astGrepCodeIndexers } from './ast-grep.js';
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
const chunks = [];
|
|
6
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7
|
+
const input = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
8
|
+
if (Object.keys(input.registrations).length) registerDynamicLanguage(input.registrations);
|
|
9
|
+
const contributions = [];
|
|
10
|
+
for (const id of input.indexers) {
|
|
11
|
+
const result = await astGrepCodeIndexers.get(id).extract({
|
|
12
|
+
files: input.files, projectRoot: input.projectRoot, parseAsync, parserErrors: input.errors,
|
|
13
|
+
});
|
|
14
|
+
contributions.push({ ...result, extractor: id });
|
|
15
|
+
}
|
|
16
|
+
process.stdout.write(JSON.stringify(contributions));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
process.stderr.write(`${error.stack || error}\n`);
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
}
|
package/src/index/context.js
CHANGED
|
@@ -111,6 +111,11 @@ export async function contextForProjectPaths({ paths, projectRoot, stackPackages
|
|
|
111
111
|
'',
|
|
112
112
|
'Use Program as a concise, fallible explanation. Code, tests, and runtime behavior remain the evidence.',
|
|
113
113
|
'',
|
|
114
|
+
'## Relevant subsystems',
|
|
115
|
+
'',
|
|
116
|
+
...(['missing', 'empty'].includes(program.subsystemMap?.status) ? ['The subsystem map needs authoring. Before implementation, create genesis/subsystems.md from source, schema, and existing Program; preserve existing explanations and do not infer ownership from folders.'] : []),
|
|
117
|
+
...((program.subsystemMap?.subsystems || []).filter((entry) => targets.includes('') || modules.some((module) => module.subsystem === entry.id)).map((entry) => JSON.stringify(entry, null, 2))),
|
|
118
|
+
'',
|
|
114
119
|
'## Relevant Program',
|
|
115
120
|
'',
|
|
116
121
|
...programDetails,
|
|
@@ -147,6 +152,7 @@ export async function contextForProjectPaths({ paths, projectRoot, stackPackages
|
|
|
147
152
|
status: 'ready',
|
|
148
153
|
paths: targets,
|
|
149
154
|
modules: moduleSources.map(({ source: _source, ...module }) => module),
|
|
155
|
+
subsystems: (program.subsystemMap?.subsystems || []).filter((entry) => targets.includes('') || modules.some((module) => module.subsystem === entry.id)),
|
|
150
156
|
components: stack.components.map(({ id }) => id),
|
|
151
157
|
engineeringProfile: engineering.profile.id,
|
|
152
158
|
verificationCommands: stack.verificationCommands.map(({ label, argv }) => ({ label, argv })),
|
package/src/index/contracts.js
CHANGED
package/src/index/init.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SUBSYSTEMS_PATH, EMPTY_SUBSYSTEMS_SOURCE } from './subsystems.js';
|
|
1
2
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
@@ -40,6 +41,7 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
|
|
|
40
41
|
const projectFormat = await inspectProjectFormatAtRoot(root);
|
|
41
42
|
requireCurrentProjectFormat(projectFormat, { allowUninitialized: true });
|
|
42
43
|
const created = (await Promise.all([
|
|
44
|
+
createIfMissing(root, SUBSYSTEMS_PATH, EMPTY_SUBSYSTEMS_SOURCE),
|
|
43
45
|
createIfMissing(root, BLUEPRINT_PATH, BLUEPRINT_SKELETON_SOURCE),
|
|
44
46
|
createIfMissing(root, COLLABORATION_PATH, COLLABORATION_SKELETON_SOURCE),
|
|
45
47
|
createIfMissing(root, ENGINEERING_PATH, ENGINEERING_SKELETON_SOURCE),
|
package/src/index/process.js
CHANGED
|
@@ -38,6 +38,7 @@ function signalProcessTree(child, signal) {
|
|
|
38
38
|
|
|
39
39
|
function executeFile(command, args, {
|
|
40
40
|
maxBuffer,
|
|
41
|
+
input,
|
|
41
42
|
signal,
|
|
42
43
|
terminationGraceMs = DEFAULT_TERMINATION_GRACE_MS,
|
|
43
44
|
timeoutMs,
|
|
@@ -130,7 +131,10 @@ function executeFile(command, args, {
|
|
|
130
131
|
)), timeoutMs);
|
|
131
132
|
timeoutTimer.unref?.();
|
|
132
133
|
}
|
|
133
|
-
child.stdin?.
|
|
134
|
+
child.stdin?.on('error', (error) => {
|
|
135
|
+
if (error.code !== 'EPIPE') spawnError = error;
|
|
136
|
+
});
|
|
137
|
+
child.stdin?.end(input);
|
|
134
138
|
});
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -156,6 +160,7 @@ export async function runProcess(command, args, {
|
|
|
156
160
|
env = process.env,
|
|
157
161
|
maxBytes = 32 * 1024 * 1024,
|
|
158
162
|
code = 'PROCESS_EXEC_FAILED',
|
|
163
|
+
input,
|
|
159
164
|
signal,
|
|
160
165
|
terminationGraceMs,
|
|
161
166
|
timeoutMs,
|
|
@@ -168,6 +173,7 @@ export async function runProcess(command, args, {
|
|
|
168
173
|
encoding: 'buffer',
|
|
169
174
|
maxBuffer: maxBytes,
|
|
170
175
|
shell: false,
|
|
176
|
+
input,
|
|
171
177
|
signal,
|
|
172
178
|
terminationGraceMs,
|
|
173
179
|
timeoutMs,
|
package/src/index/program.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { inspectSubsystems } from './subsystems.js';
|
|
4
5
|
import { GenesisError } from './errors.js';
|
|
5
6
|
import { isProjectContentPath, PROGRAM_ROOT } from './paths.js';
|
|
6
7
|
import { normalizeRelative } from './utils.js';
|
|
@@ -55,16 +56,15 @@ function moduleIdentity(programPath) {
|
|
|
55
56
|
const relative = programPath.slice(`${PROGRAM_ROOT}/`.length, -3);
|
|
56
57
|
const normalized = normalizeRelative(relative);
|
|
57
58
|
const segments = normalized.split('/');
|
|
58
|
-
if (segments.
|
|
59
|
+
if (segments.some((segment) => !CONCEPT_NAME.test(segment))) {
|
|
59
60
|
throw new GenesisError(
|
|
60
61
|
'PROGRAM_INVALID',
|
|
61
|
-
`Program module needs lowercase
|
|
62
|
+
`Program module needs lowercase path names: ${programPath}.`,
|
|
62
63
|
{ path: programPath },
|
|
63
64
|
);
|
|
64
65
|
}
|
|
65
66
|
return {
|
|
66
67
|
name: segments.at(-1),
|
|
67
|
-
subsystem: segments.slice(0, -1).join('/'),
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -93,13 +93,12 @@ function moduleContents(source, programPath) {
|
|
|
93
93
|
const sources = sourceLines.map((line) => line.match(SOURCE_LINE)?.[1]);
|
|
94
94
|
const nextHeading = headings.find(({ index }) => index > contractHeading[0].index);
|
|
95
95
|
const contractLines = lines
|
|
96
|
-
.slice(contractHeading[0].index + 1, nextHeading?.index ?? lines.length)
|
|
97
|
-
.filter((line) => line.trim());
|
|
96
|
+
.slice(contractHeading[0].index + 1, nextHeading?.index ?? lines.length);
|
|
98
97
|
if (
|
|
99
98
|
sources.length === 0
|
|
100
99
|
|| sources.some((sourcePath) => sourcePath === undefined)
|
|
101
100
|
|| new Set(sources).size !== sources.length
|
|
102
|
-
|| contractLines.
|
|
101
|
+
|| !contractLines.some((line) => line.trim())
|
|
103
102
|
) {
|
|
104
103
|
throw new GenesisError(
|
|
105
104
|
'PROGRAM_INVALID',
|
|
@@ -136,8 +135,13 @@ function moduleContents(source, programPath) {
|
|
|
136
135
|
|
|
137
136
|
export async function inspectProgram(projectRoot) {
|
|
138
137
|
const { files, found } = await markdownFiles(projectRoot);
|
|
138
|
+
const subsystemMap = await inspectSubsystems({ projectRoot });
|
|
139
|
+
const assignments = new Map(subsystemMap.subsystems.flatMap((entry) => entry.program.map((file) => [file, entry.id])));
|
|
140
|
+
for (const file of assignments.keys()) {
|
|
141
|
+
if (!files.includes(file)) throw new GenesisError('SUBSYSTEMS_PROGRAM_MISSING', `Subsystem cites missing Program module: ${file}.`);
|
|
142
|
+
}
|
|
139
143
|
if (!found || files.length === 0) {
|
|
140
|
-
return { status: 'missing', files: [], modules: [], subsystems:
|
|
144
|
+
return { status: subsystemMap.status === 'valid' ? 'valid' : 'missing', files: [], modules: [], subsystems: subsystemMap.subsystems.map(({ id }) => id), subsystemMap };
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
const modules = [];
|
|
@@ -150,6 +154,7 @@ export async function inspectProgram(projectRoot) {
|
|
|
150
154
|
}
|
|
151
155
|
const identity = moduleIdentity(programPath);
|
|
152
156
|
const contents = moduleContents(source, programPath);
|
|
157
|
+
if (subsystemMap.status === 'valid' && !assignments.has(programPath)) throw new GenesisError('SUBSYSTEMS_PROGRAM_UNASSIGNED', `Program module has no declared subsystem: ${programPath}.`);
|
|
153
158
|
const { sources } = contents;
|
|
154
159
|
for (const sourcePath of sources) {
|
|
155
160
|
if (!sourcePath || !isProjectContentPath(sourcePath) || !await sourceExists(projectRoot, sourcePath)) {
|
|
@@ -164,7 +169,7 @@ export async function inspectProgram(projectRoot) {
|
|
|
164
169
|
path: programPath,
|
|
165
170
|
name: identity.name,
|
|
166
171
|
sources,
|
|
167
|
-
subsystem:
|
|
172
|
+
subsystem: assignments.get(programPath) ?? null,
|
|
168
173
|
title: contents.title,
|
|
169
174
|
description: contents.description,
|
|
170
175
|
publicContract: contents.publicContract,
|
|
@@ -176,6 +181,7 @@ export async function inspectProgram(projectRoot) {
|
|
|
176
181
|
status: 'valid',
|
|
177
182
|
files,
|
|
178
183
|
modules,
|
|
179
|
-
subsystems:
|
|
184
|
+
subsystems: subsystemMap.subsystems.map(({ id }) => id),
|
|
185
|
+
subsystemMap,
|
|
180
186
|
};
|
|
181
187
|
}
|
package/src/index/prompt.js
CHANGED
|
@@ -74,6 +74,7 @@ function programContext(program) {
|
|
|
74
74
|
return {
|
|
75
75
|
status: program.status,
|
|
76
76
|
files: program.files,
|
|
77
|
+
subsystemMap: program.subsystemMap,
|
|
77
78
|
subsystems: program.subsystems,
|
|
78
79
|
modules: program.modules.map(({ path, name, sources, subsystem }) => ({
|
|
79
80
|
path,
|
|
@@ -90,6 +91,7 @@ function startProgramContext(program) {
|
|
|
90
91
|
status: program.status,
|
|
91
92
|
subsystems: program.subsystems,
|
|
92
93
|
moduleCount: program.modules.length,
|
|
94
|
+
subsystemMapStatus: program.subsystemMap?.status,
|
|
93
95
|
...(program.diagnostic ? { diagnostic: program.diagnostic } : {}),
|
|
94
96
|
};
|
|
95
97
|
}
|
|
@@ -149,6 +149,8 @@ export async function projectSessionContext({
|
|
|
149
149
|
...skills.diagnostics.map(({ message }) => `- Agent Skill maintenance: ${message}`),
|
|
150
150
|
...(skills.diagnostics.length > 0 ? ['- Skill inspection is read-only. Synchronization is an explicit source change; follow the current task authorization and host write boundary before running it.'] : []),
|
|
151
151
|
'- After locating source, run `genesis context <path...>`; before adding a helper or public operation, run `genesis index <name-or-path...>` and reuse an existing owner.',
|
|
152
|
+
'- If genesis/subsystems.md is missing or empty in an existing explained application, author it from source, schema and existing Program before implementation. This is ordinary explanatory adoption, not a startup blocker; do not run tests or application commands merely to create the map.',
|
|
153
|
+
'- Read genesis/subsystems.md for declared subsystem responsibilities, Program membership, and data ownership/use. Maintain it in the same implementation turn when those facts change; reuse existing boundaries, verify source/schema, and never infer membership from folders.',
|
|
152
154
|
'- Program is fallible explanation; code, tests, and runtime behavior remain evidence.',
|
|
153
155
|
'- Keep Blueprint and affected Program explanations aligned with intentional observable product behavior in the same implementation turn. Private restructuring may need only source citations or no explanatory change.',
|
|
154
156
|
'- Before reporting completion, compare the requested observable behavior, required inputs and resources, declared project operations, and focused evidence with what actually exists. State anything not proven.',
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { gitContext } from './git.js';
|
|
4
|
+
import { readStack } from './stack.js';
|
|
5
|
+
import { GenesisError } from './errors.js';
|
|
6
|
+
import { normalizeRelative, sha256, stableJson } from './utils.js';
|
|
7
|
+
|
|
8
|
+
export const SUBSYSTEMS_PATH = 'genesis/subsystems.md';
|
|
9
|
+
export const EMPTY_SUBSYSTEMS_SOURCE = '# Subsystems\n\n- Nothing.\n';
|
|
10
|
+
|
|
11
|
+
// Table identities are declarations, never database queries or SQL identifiers.
|
|
12
|
+
export async function inspectSubsystems({ projectRoot = process.cwd() } = {}) {
|
|
13
|
+
projectRoot = (await gitContext(projectRoot)).repositoryRoot;
|
|
14
|
+
let source;
|
|
15
|
+
try { source = await readFile(path.join(projectRoot, SUBSYSTEMS_PATH), 'utf8'); }
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code !== 'ENOENT') throw error;
|
|
18
|
+
return { contract: 'genesis.subsystems.v0', status: 'missing', path: SUBSYSTEMS_PATH, subsystems: [] };
|
|
19
|
+
}
|
|
20
|
+
const invalid = (message) => { throw new GenesisError('SUBSYSTEMS_INVALID', `${SUBSYSTEMS_PATH}: ${message}`); };
|
|
21
|
+
const lines = source.replace(/\r\n?/gu, '\n').trim().split('\n');
|
|
22
|
+
if (lines.shift() !== '# Subsystems') invalid('Expected # Subsystems.');
|
|
23
|
+
const subsystems = [];
|
|
24
|
+
let current;
|
|
25
|
+
let section;
|
|
26
|
+
const sections = new Set();
|
|
27
|
+
const finish = () => {
|
|
28
|
+
if (!current) return;
|
|
29
|
+
if (!current.description.trim() || sections.size !== 3) invalid(`Subsystem ${current.id} needs a responsibility and Program, Data owned, Data used sections.`);
|
|
30
|
+
current.description = current.description.trim();
|
|
31
|
+
subsystems.push(current);
|
|
32
|
+
};
|
|
33
|
+
const empty = lines.join('\n').trim() === '- Nothing.';
|
|
34
|
+
if (!empty && !lines.some((line) => line.trim())) invalid('An empty map must say - Nothing.');
|
|
35
|
+
if (!empty) for (const line of lines) {
|
|
36
|
+
if (!line.trim()) {
|
|
37
|
+
if (current && !section) current.description += '\n';
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const heading = line.match(/^## `([a-z0-9]+(?:-[a-z0-9]+)*)` (\S.*)$/u);
|
|
41
|
+
if (heading) {
|
|
42
|
+
finish();
|
|
43
|
+
current = { id: heading[1], title: heading[2], description: '', program: [], dataOwned: [], dataUsed: [] };
|
|
44
|
+
section = null;
|
|
45
|
+
sections.clear();
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!current) invalid('Expected a subsystem heading or - Nothing.');
|
|
49
|
+
const sectionHeading = line.match(/^### (Program|Data owned|Data used)$/u);
|
|
50
|
+
if (sectionHeading) {
|
|
51
|
+
section = { Program: 'program', 'Data owned': 'dataOwned', 'Data used': 'dataUsed' }[sectionHeading[1]];
|
|
52
|
+
if (sections.has(section)) invalid(`Duplicate ${sectionHeading[1]} section.`);
|
|
53
|
+
sections.add(section);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (line.startsWith('#')) invalid(`Unexpected heading: ${line}`);
|
|
57
|
+
if (!section) { current.description += `${line}\n`; continue; }
|
|
58
|
+
if (line === '- Nothing.') {
|
|
59
|
+
if (current[section].length || current[`${section}Empty`]) invalid('Nothing must be the only section entry.');
|
|
60
|
+
current[`${section}Empty`] = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (current[`${section}Empty`]) invalid('Nothing must be the only section entry.');
|
|
64
|
+
if (section === 'program') {
|
|
65
|
+
const match = line.match(/^- `([^`]+)`$/u);
|
|
66
|
+
if (!match || !/^genesis\/program\/[a-z0-9-]+(?:\/[a-z0-9-]+)*\.md$/u.test(match[1]) || normalizeRelative(match[1]) !== match[1]) invalid(`Invalid Program reference: ${line}`);
|
|
67
|
+
current.program.push(match[1]);
|
|
68
|
+
} else {
|
|
69
|
+
const match = line.match(/^- Table `([^`]+)` `([^`]+)` `([^`]+)`$/u);
|
|
70
|
+
if (!match) invalid(`Expected Table resource, schema, and table: ${line}`);
|
|
71
|
+
current[section].push({ resource: match[1], schema: match[2], table: match[3] });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finish();
|
|
75
|
+
const ids = new Set();
|
|
76
|
+
const programs = new Set();
|
|
77
|
+
const owners = new Map();
|
|
78
|
+
for (const subsystem of subsystems) {
|
|
79
|
+
if (ids.has(subsystem.id)) invalid(`Duplicate subsystem ${subsystem.id}.`);
|
|
80
|
+
ids.add(subsystem.id);
|
|
81
|
+
for (const key of ['program', 'dataOwned', 'dataUsed']) {
|
|
82
|
+
if (!subsystem[key].length && !subsystem[`${key}Empty`]) invalid(`${subsystem.id}: empty sections must say - Nothing.`);
|
|
83
|
+
delete subsystem[`${key}Empty`];
|
|
84
|
+
const entries = subsystem[key].map((entry) => JSON.stringify(entry));
|
|
85
|
+
if (new Set(entries).size !== entries.length) invalid(`${subsystem.id}: duplicate ${key} entry.`);
|
|
86
|
+
}
|
|
87
|
+
for (const file of subsystem.program) {
|
|
88
|
+
if (programs.has(file)) invalid(`Program operation has multiple owners: ${file}.`);
|
|
89
|
+
programs.add(file);
|
|
90
|
+
}
|
|
91
|
+
for (const table of subsystem.dataOwned) {
|
|
92
|
+
const key = JSON.stringify(table);
|
|
93
|
+
if (owners.has(key)) invalid(`Table has multiple owners: ${key}.`);
|
|
94
|
+
owners.set(key, subsystem.id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const tables = subsystems.flatMap((entry) => [...entry.dataOwned, ...entry.dataUsed]);
|
|
98
|
+
if (tables.length) {
|
|
99
|
+
const stack = await readStack(projectRoot);
|
|
100
|
+
const resources = new Set(stack.resources.map(({ resource }) => resource.id));
|
|
101
|
+
for (const table of tables) if (!resources.has(table.resource)) invalid(`Undeclared Stack resource: ${table.resource}.`);
|
|
102
|
+
}
|
|
103
|
+
for (const subsystem of subsystems) for (const table of subsystem.dataUsed) {
|
|
104
|
+
const owner = owners.get(JSON.stringify(table));
|
|
105
|
+
if (!owner || owner === subsystem.id) invalid(`${subsystem.id}: Data used must reference a table owned by another declared subsystem.`);
|
|
106
|
+
}
|
|
107
|
+
return { contract: 'genesis.subsystems.v0', status: subsystems.length ? 'valid' : 'empty', path: SUBSYSTEMS_PATH,
|
|
108
|
+
identity: sha256(stableJson(subsystems)), subsystems };
|
|
109
|
+
}
|