@aabadin/project-memory-context 0.1.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/LICENSE +674 -0
- package/README.md +123 -0
- package/bin/pmc.mjs +11 -0
- package/cli/apply-enrichment-result.mjs +31 -0
- package/cli/batch-enrich.mjs +5 -0
- package/cli/bootstrap.mjs +357 -0
- package/cli/build-worklist.mjs +35 -0
- package/cli/context.mjs +49 -0
- package/cli/doctor.mjs +29 -0
- package/cli/enrich-batch.mjs +11 -0
- package/cli/enrich-loop.sh +117 -0
- package/cli/enrich-orchestrator.mjs +5 -0
- package/cli/enrich-queue.mjs +525 -0
- package/cli/enrich-sync.mjs +5 -0
- package/cli/enrich.mjs +51 -0
- package/cli/fail-enrichment.mjs +28 -0
- package/cli/finalize-enrichment.mjs +25 -0
- package/cli/init.mjs +66 -0
- package/cli/install-pmc.mjs +153 -0
- package/cli/materialize-enrichment-artifacts.mjs +38 -0
- package/cli/new-project.mjs +41 -0
- package/cli/prepare-semantic-jobs.mjs +8 -0
- package/cli/project-context.mjs +224 -0
- package/cli/sanitize.mjs +235 -0
- package/cli/save-intake-context.mjs +22 -0
- package/cli/setup.mjs +80 -0
- package/cli/status.mjs +81 -0
- package/mcp/local-model-server.mjs +74 -0
- package/package.json +60 -0
- package/plugin/index.mjs +27 -0
- package/src/artifacts.mjs +39 -0
- package/src/change-detector.mjs +10 -0
- package/src/command-dispatch.mjs +84 -0
- package/src/declared-intake.mjs +25 -0
- package/src/doctor.mjs +114 -0
- package/src/enrichment-artifacts.mjs +67 -0
- package/src/enrichment-attempts.mjs +17 -0
- package/src/enrichment-config.mjs +121 -0
- package/src/enrichment-driver.mjs +167 -0
- package/src/enrichment-errors.mjs +46 -0
- package/src/enrichment-linker.mjs +29 -0
- package/src/extractors/architecture-extractor.mjs +8 -0
- package/src/extractors/js-ts-extractor.mjs +118 -0
- package/src/extractors/regex-extractor.mjs +439 -0
- package/src/extractors/rules-extractor.mjs +9 -0
- package/src/extractors/stack-extractor.mjs +48 -0
- package/src/extractors/structure-extractor.mjs +31 -0
- package/src/fail-enrichment.mjs +33 -0
- package/src/finalize-enrichment.mjs +30 -0
- package/src/graph-backfill.mjs +35 -0
- package/src/graph-node-resolver.mjs +64 -0
- package/src/index.mjs +2 -0
- package/src/intake-context.mjs +16 -0
- package/src/invalidation-matrix.mjs +33 -0
- package/src/markdown-renderer.mjs +27 -0
- package/src/materializer.mjs +128 -0
- package/src/memory-payload.mjs +55 -0
- package/src/persist-enrichment-result.mjs +33 -0
- package/src/platform.mjs +111 -0
- package/src/plugin-config.mjs +17 -0
- package/src/prepare-semantic-jobs.mjs +33 -0
- package/src/project-context-schema.mjs +57 -0
- package/src/providers/cloud-api-provider.mjs +88 -0
- package/src/providers/local-model-provider.mjs +67 -0
- package/src/refresh-state.mjs +21 -0
- package/src/result-input.mjs +9 -0
- package/src/retrieval/context-renderer.mjs +97 -0
- package/src/retrieval/query-engine.mjs +230 -0
- package/src/semantic-report.mjs +26 -0
- package/src/semantic-unit.mjs +74 -0
- package/src/setup-bootstrap.mjs +131 -0
- package/src/symbol-extractor.mjs +29 -0
- package/src/symbol-index.mjs +30 -0
- package/src/symbol-keys.mjs +28 -0
- package/src/sync-manifest.mjs +119 -0
- package/src/template-installer.mjs +181 -0
- package/src/worklist-state.mjs +12 -0
- package/templates/claude-code/CLAUDE.md.snippet +36 -0
- package/templates/cursor/.cursorrules.snippet +36 -0
- package/templates/generic/README-SETUP.md +53 -0
- package/templates/opencode/agent/enrich.md +28 -0
- package/templates/opencode/autostart-snippet.md +13 -0
- package/templates/opencode/commands/get-context.md +22 -0
- package/templates/opencode/commands/new-project.md +32 -0
- package/templates/opencode/commands/sanitize.md +21 -0
- package/templates/opencode/commands/sync-context.md +22 -0
- package/templates/project-memory-context workflow.md +129 -0
- package/templates/project-memory-context.md +42 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { buildSymbolKey } from '../symbol-keys.mjs';
|
|
4
|
+
|
|
5
|
+
const TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
|
|
6
|
+
const JSX_EXTENSIONS = new Set(['.jsx', '.tsx']);
|
|
7
|
+
|
|
8
|
+
function makePlugins(ext) {
|
|
9
|
+
const plugins = ['decorators'];
|
|
10
|
+
if (TS_EXTENSIONS.has(ext)) plugins.push('typescript');
|
|
11
|
+
if (JSX_EXTENSIONS.has(ext)) plugins.push('jsx');
|
|
12
|
+
return plugins;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildCodeHash(lines, startLine, endLine) {
|
|
16
|
+
const fragment = lines.slice(startLine - 1, endLine).join('\n');
|
|
17
|
+
return createHash('sha1').update(fragment).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isExported(node) {
|
|
21
|
+
// Babel wraps exported declarations in ExportNamedDeclaration / ExportDefaultDeclaration
|
|
22
|
+
return node._exported === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function symbolFromNode(node, lines, filePath, language, kind, name, arity) {
|
|
26
|
+
const startLine = node.loc?.start?.line ?? 1;
|
|
27
|
+
const endLine = node.loc?.end?.line ?? startLine;
|
|
28
|
+
const codeHash = buildCodeHash(lines, startLine, endLine);
|
|
29
|
+
const exportScope = node._exported ? 'exported' : 'local';
|
|
30
|
+
const symbol = {
|
|
31
|
+
language,
|
|
32
|
+
filePath: filePath.replace(/\\/g, '/'),
|
|
33
|
+
kind,
|
|
34
|
+
name,
|
|
35
|
+
exportScope,
|
|
36
|
+
arity,
|
|
37
|
+
range: { startLine, endLine },
|
|
38
|
+
codeHash,
|
|
39
|
+
};
|
|
40
|
+
symbol.symbolKey = buildSymbolKey(symbol);
|
|
41
|
+
return symbol;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function extractJsTsSymbols({ filePath, content }) {
|
|
45
|
+
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
|
|
46
|
+
const language = TS_EXTENSIONS.has(ext) ? 'ts' : 'js';
|
|
47
|
+
|
|
48
|
+
let ast;
|
|
49
|
+
try {
|
|
50
|
+
ast = parse(content, {
|
|
51
|
+
sourceType: 'module',
|
|
52
|
+
allowImportExportEverywhere: true,
|
|
53
|
+
allowReturnOutsideFunction: true,
|
|
54
|
+
errorRecovery: true,
|
|
55
|
+
plugins: makePlugins(ext),
|
|
56
|
+
});
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lines = content.split('\n');
|
|
62
|
+
const symbols = [];
|
|
63
|
+
|
|
64
|
+
for (const node of ast.program.body) {
|
|
65
|
+
const exported = node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration';
|
|
66
|
+
const decl = exported ? (node.declaration ?? node) : node;
|
|
67
|
+
if (decl) decl._exported = exported;
|
|
68
|
+
|
|
69
|
+
switch (decl?.type) {
|
|
70
|
+
case 'ClassDeclaration':
|
|
71
|
+
case 'ClassExpression':
|
|
72
|
+
if (decl.id?.name) {
|
|
73
|
+
symbols.push(symbolFromNode(decl, lines, filePath, language, 'class', decl.id.name, undefined));
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case 'FunctionDeclaration':
|
|
78
|
+
if (decl.id?.name) {
|
|
79
|
+
const arity = decl.params?.length ?? 0;
|
|
80
|
+
symbols.push(symbolFromNode(decl, lines, filePath, language, 'function', decl.id.name, arity));
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'VariableDeclaration':
|
|
85
|
+
for (const declarator of decl.declarations ?? []) {
|
|
86
|
+
const init = declarator.init;
|
|
87
|
+
const isArrow = init?.type === 'ArrowFunctionExpression' || init?.type === 'FunctionExpression';
|
|
88
|
+
if (isArrow && declarator.id?.name) {
|
|
89
|
+
declarator._exported = decl._exported;
|
|
90
|
+
const arity = init.params?.length ?? 0;
|
|
91
|
+
symbols.push(symbolFromNode(declarator, lines, filePath, language, 'function', declarator.id.name, arity));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case 'TSInterfaceDeclaration':
|
|
97
|
+
if (decl.id?.name) {
|
|
98
|
+
symbols.push(symbolFromNode(decl, lines, filePath, language, 'interface', decl.id.name, undefined));
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case 'TSTypeAliasDeclaration':
|
|
103
|
+
if (decl.id?.name) {
|
|
104
|
+
symbols.push(symbolFromNode(decl, lines, filePath, language, 'type', decl.id.name, undefined));
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case 'TSEnumDeclaration':
|
|
109
|
+
if (decl.id?.name) {
|
|
110
|
+
symbols.push(symbolFromNode(decl, lines, filePath, language, 'class', decl.id.name, undefined));
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
symbols.sort((a, b) => a.range.startLine - b.range.startLine);
|
|
117
|
+
return symbols;
|
|
118
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { buildSymbolKey } from '../symbol-keys.mjs';
|
|
3
|
+
|
|
4
|
+
// ── Shared utilities ───────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function codeHash(lines, startLine, endLine) {
|
|
7
|
+
return createHash('sha1').update(lines.slice(startLine - 1, endLine).join('\n')).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function findBlockEnd(lines, startIdx) {
|
|
11
|
+
let depth = 0;
|
|
12
|
+
let opened = false;
|
|
13
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
14
|
+
for (const ch of lines[i]) {
|
|
15
|
+
if (ch === '{') { depth++; opened = true; }
|
|
16
|
+
if (ch === '}') { depth--; if (opened && depth === 0) return i + 1; }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return startIdx + 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeSymbol(filePath, language, kind, name, exportScope, lines, startLine, endLine, arity) {
|
|
23
|
+
const symbol = {
|
|
24
|
+
language,
|
|
25
|
+
filePath: filePath.replace(/\\/g, '/'),
|
|
26
|
+
kind,
|
|
27
|
+
name,
|
|
28
|
+
exportScope,
|
|
29
|
+
arity,
|
|
30
|
+
range: { startLine, endLine },
|
|
31
|
+
codeHash: codeHash(lines, startLine, endLine),
|
|
32
|
+
};
|
|
33
|
+
symbol.symbolKey = buildSymbolKey(symbol);
|
|
34
|
+
return symbol;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Language extractors ────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function extractPython(lines, filePath) {
|
|
40
|
+
const symbols = [];
|
|
41
|
+
const classRe = /^(class)\s+(\w+)/;
|
|
42
|
+
const funcRe = /^(async\s+def|def)\s+(\w+)\s*\(([^)]*)\)/;
|
|
43
|
+
const decoratedFuncRe = /^(async\s+def|def)\s+(\w+)/;
|
|
44
|
+
|
|
45
|
+
let pendingDecorator = false;
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const line = lines[i].trimStart();
|
|
48
|
+
if (line.startsWith('#')) continue;
|
|
49
|
+
if (line.startsWith('@')) { pendingDecorator = true; continue; }
|
|
50
|
+
|
|
51
|
+
const classMatch = classRe.exec(line);
|
|
52
|
+
if (classMatch) {
|
|
53
|
+
const name = classMatch[2];
|
|
54
|
+
const endLine = findBlockEnd(lines, i);
|
|
55
|
+
symbols.push(makeSymbol(filePath, 'python', 'class', name, name.startsWith('_') ? 'local' : 'exported', lines, i + 1, endLine, undefined));
|
|
56
|
+
pendingDecorator = false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fnMatch = funcRe.exec(line) ?? (pendingDecorator ? decoratedFuncRe.exec(line) : null);
|
|
61
|
+
if (fnMatch) {
|
|
62
|
+
const name = fnMatch[2];
|
|
63
|
+
const params = fnMatch[3] ?? '';
|
|
64
|
+
const arity = params.trim() ? params.split(',').filter(p => p.trim() && p.trim() !== 'self' && p.trim() !== 'cls').length : 0;
|
|
65
|
+
// Only top-level (not indented)
|
|
66
|
+
if (!lines[i].startsWith(' ') && !lines[i].startsWith('\t')) {
|
|
67
|
+
const endLine = findBlockEnd(lines, i);
|
|
68
|
+
symbols.push(makeSymbol(filePath, 'python', 'function', name, name.startsWith('_') ? 'local' : 'exported', lines, i + 1, endLine, arity));
|
|
69
|
+
}
|
|
70
|
+
pendingDecorator = false;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
pendingDecorator = false;
|
|
74
|
+
}
|
|
75
|
+
return symbols;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extractJava(lines, filePath) {
|
|
79
|
+
const symbols = [];
|
|
80
|
+
const classRe = /^(?:public\s+)?(?:abstract\s+)?(?:final\s+)?(class|interface|enum|@interface|record)\s+(\w+)/;
|
|
81
|
+
const methodRe = /^\s+(?:@\w+\s+)*(?:public|protected|private|static|final|abstract|synchronized|native|default)[\w\s<>,\[\]]*?\s+(\w+)\s*\(([^)]*)\)/;
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < lines.length; i++) {
|
|
84
|
+
const line = lines[i].trimStart();
|
|
85
|
+
const classMatch = classRe.exec(line);
|
|
86
|
+
if (classMatch) {
|
|
87
|
+
const kind = classMatch[1] === 'interface' ? 'interface' : 'class';
|
|
88
|
+
const name = classMatch[2];
|
|
89
|
+
const exported = lines[i].includes('public');
|
|
90
|
+
const endLine = findBlockEnd(lines, i);
|
|
91
|
+
symbols.push(makeSymbol(filePath, 'java', kind, name, exported ? 'exported' : 'local', lines, i + 1, endLine, undefined));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const methodMatch = methodRe.exec(lines[i]);
|
|
96
|
+
if (methodMatch) {
|
|
97
|
+
const name = methodMatch[1];
|
|
98
|
+
if (name === 'if' || name === 'for' || name === 'while' || name === 'switch') continue;
|
|
99
|
+
const params = methodMatch[2] ?? '';
|
|
100
|
+
const arity = params.trim() ? params.split(',').length : 0;
|
|
101
|
+
const exported = lines[i].includes('public');
|
|
102
|
+
const endLine = findBlockEnd(lines, i);
|
|
103
|
+
symbols.push(makeSymbol(filePath, 'java', 'function', name, exported ? 'exported' : 'local', lines, i + 1, endLine, arity));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return symbols;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function extractGo(lines, filePath) {
|
|
110
|
+
const symbols = [];
|
|
111
|
+
const funcRe = /^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(([^)]*)\)/;
|
|
112
|
+
const typeRe = /^type\s+(\w+)\s+(struct|interface|func)/;
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
115
|
+
const funcMatch = funcRe.exec(lines[i]);
|
|
116
|
+
if (funcMatch) {
|
|
117
|
+
const name = funcMatch[1];
|
|
118
|
+
const params = funcMatch[2] ?? '';
|
|
119
|
+
const arity = params.trim() ? params.split(',').length : 0;
|
|
120
|
+
const exported = name[0] === name[0].toUpperCase() && /[A-Z]/.test(name[0]);
|
|
121
|
+
const endLine = findBlockEnd(lines, i);
|
|
122
|
+
symbols.push(makeSymbol(filePath, 'go', 'function', name, exported ? 'exported' : 'local', lines, i + 1, endLine, arity));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const typeMatch = typeRe.exec(lines[i]);
|
|
127
|
+
if (typeMatch) {
|
|
128
|
+
const name = typeMatch[1];
|
|
129
|
+
const kind = typeMatch[2] === 'interface' ? 'interface' : 'class';
|
|
130
|
+
const exported = name[0] === name[0].toUpperCase() && /[A-Z]/.test(name[0]);
|
|
131
|
+
const endLine = findBlockEnd(lines, i);
|
|
132
|
+
symbols.push(makeSymbol(filePath, 'go', kind, name, exported ? 'exported' : 'local', lines, i + 1, endLine, undefined));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return symbols;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractRust(lines, filePath) {
|
|
139
|
+
const symbols = [];
|
|
140
|
+
const patterns = [
|
|
141
|
+
{ kind: 'class', re: /^(?:pub(?:\([^)]+\))?\s+)?struct\s+(\w+)/ },
|
|
142
|
+
{ kind: 'class', re: /^(?:pub(?:\([^)]+\))?\s+)?enum\s+(\w+)/ },
|
|
143
|
+
{ kind: 'interface', re: /^(?:pub(?:\([^)]+\))?\s+)?trait\s+(\w+)/ },
|
|
144
|
+
{ kind: 'function', re: /^(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?fn\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)/ },
|
|
145
|
+
{ kind: 'class', re: /^impl(?:<[^>]+>)?\s+(?:\w+\s+for\s+)?(\w+)/ },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < lines.length; i++) {
|
|
149
|
+
const line = lines[i];
|
|
150
|
+
if (line.trimStart().startsWith('//')) continue;
|
|
151
|
+
for (const { kind, re } of patterns) {
|
|
152
|
+
const m = re.exec(line);
|
|
153
|
+
if (m) {
|
|
154
|
+
const name = m[1];
|
|
155
|
+
if (!name || name === 'fn' || name === 'struct') continue;
|
|
156
|
+
const params = m[2] ?? '';
|
|
157
|
+
const arity = kind === 'function' && params.trim() ? params.split(',').length : undefined;
|
|
158
|
+
const exported = line.trimStart().startsWith('pub');
|
|
159
|
+
const endLine = findBlockEnd(lines, i);
|
|
160
|
+
symbols.push(makeSymbol(filePath, 'rust', kind, name, exported ? 'exported' : 'local', lines, i + 1, endLine, arity));
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return symbols;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractRuby(lines, filePath) {
|
|
169
|
+
const symbols = [];
|
|
170
|
+
const classRe = /^(?:class|module)\s+([\w:]+)/;
|
|
171
|
+
const methodRe = /^ def\s+(?:self\.)?(\w+)(?:\(([^)]*)\))?/;
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < lines.length; i++) {
|
|
174
|
+
const classMatch = classRe.exec(lines[i]);
|
|
175
|
+
if (classMatch) {
|
|
176
|
+
const name = classMatch[1].split('::').at(-1);
|
|
177
|
+
// Find matching 'end'
|
|
178
|
+
let depth = 1, end = i + 1;
|
|
179
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
180
|
+
const t = lines[j].trimStart();
|
|
181
|
+
if (/^(class|module|def|do|if|unless|while|until|for|begin|case)\b/.test(t)) depth++;
|
|
182
|
+
if (/^end\b/.test(t)) { depth--; if (depth === 0) { end = j + 1; break; } }
|
|
183
|
+
}
|
|
184
|
+
symbols.push(makeSymbol(filePath, 'ruby', 'class', name, 'exported', lines, i + 1, end, undefined));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const methodMatch = methodRe.exec(lines[i]);
|
|
188
|
+
if (methodMatch) {
|
|
189
|
+
const name = methodMatch[1];
|
|
190
|
+
const params = methodMatch[2] ?? '';
|
|
191
|
+
const arity = params.trim() ? params.split(',').length : 0;
|
|
192
|
+
symbols.push(makeSymbol(filePath, 'ruby', 'function', name, 'exported', lines, i + 1, i + 1, arity));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return symbols;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractPhp(lines, filePath) {
|
|
199
|
+
const symbols = [];
|
|
200
|
+
const classRe = /^(?:abstract\s+|final\s+)?(?:class|interface|trait|enum)\s+(\w+)/;
|
|
201
|
+
const funcRe = /^(?:function|(?:public|protected|private|static|abstract|final)[\w\s]*function)\s+(?:&\s*)?(\w+)\s*\(([^)]*)\)/;
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < lines.length; i++) {
|
|
204
|
+
const line = lines[i].trimStart();
|
|
205
|
+
const classMatch = classRe.exec(line);
|
|
206
|
+
if (classMatch) {
|
|
207
|
+
const name = classMatch[1];
|
|
208
|
+
const keyword = line.match(/^(?:\w+\s+)*(class|interface|trait|enum)/)?.[1] ?? 'class';
|
|
209
|
+
const kind = keyword === 'interface' ? 'interface' : 'class';
|
|
210
|
+
const endLine = findBlockEnd(lines, i);
|
|
211
|
+
symbols.push(makeSymbol(filePath, 'php', kind, name, 'exported', lines, i + 1, endLine, undefined));
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const funcMatch = funcRe.exec(line);
|
|
215
|
+
if (funcMatch) {
|
|
216
|
+
const name = funcMatch[1];
|
|
217
|
+
const params = funcMatch[2] ?? '';
|
|
218
|
+
const arity = params.trim() ? params.split(',').length : 0;
|
|
219
|
+
const exported = /\bpublic\b/.test(line) || !lines[i].startsWith(' ');
|
|
220
|
+
const endLine = findBlockEnd(lines, i);
|
|
221
|
+
symbols.push(makeSymbol(filePath, 'php', 'function', name, exported ? 'exported' : 'local', lines, i + 1, endLine, arity));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return symbols;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function extractKotlin(lines, filePath) {
|
|
228
|
+
const symbols = [];
|
|
229
|
+
const classRe = /^(?:(?:public|private|protected|internal|abstract|open|sealed|data|inline|value|enum)\s+)*(?:class|object|interface)\s+(\w+)/;
|
|
230
|
+
const funcRe = /^(?:(?:public|private|protected|internal|override|suspend|inline|operator|infix|tailrec|external)\s+)*fun\s+(?:<[^>]+>\s+)?(?:\w+\s*\.\s*)?(\w+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)/;
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < lines.length; i++) {
|
|
233
|
+
const line = lines[i];
|
|
234
|
+
const classMatch = classRe.exec(line.trimStart());
|
|
235
|
+
if (classMatch) {
|
|
236
|
+
const name = classMatch[1];
|
|
237
|
+
const keyword = line.match(/(class|object|interface)\s/)?.[1] ?? 'class';
|
|
238
|
+
const kind = keyword === 'interface' ? 'interface' : 'class';
|
|
239
|
+
const exported = !line.includes('private') && !line.includes('internal');
|
|
240
|
+
const endLine = findBlockEnd(lines, i);
|
|
241
|
+
symbols.push(makeSymbol(filePath, 'kotlin', kind, name, exported ? 'exported' : 'local', lines, i + 1, endLine, undefined));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const funcMatch = funcRe.exec(line.trimStart());
|
|
245
|
+
if (funcMatch) {
|
|
246
|
+
const name = funcMatch[1];
|
|
247
|
+
const params = funcMatch[2] ?? '';
|
|
248
|
+
const arity = params.trim() ? params.split(',').length : 0;
|
|
249
|
+
const exported = !line.includes('private') && !line.includes('internal');
|
|
250
|
+
const endLine = findBlockEnd(lines, i);
|
|
251
|
+
symbols.push(makeSymbol(filePath, 'kotlin', 'function', name, exported ? 'exported' : 'local', lines, i + 1, endLine, arity));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return symbols;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function extractSwift(lines, filePath) {
|
|
258
|
+
const symbols = [];
|
|
259
|
+
const patterns = [
|
|
260
|
+
{ kind: 'class', re: /^(?:(?:public|private|internal|open|final|fileprivate)\s+)*(?:class|struct|actor|enum)\s+(\w+)/ },
|
|
261
|
+
{ kind: 'interface', re: /^(?:(?:public|private|internal|open|fileprivate)\s+)*protocol\s+(\w+)/ },
|
|
262
|
+
{ kind: 'class', re: /^(?:(?:public|private|internal|open|fileprivate)\s+)*extension\s+(\w+)/ },
|
|
263
|
+
{ kind: 'function', re: /^(?:(?:public|private|internal|open|override|static|class|fileprivate|mutating|nonmutating|dynamic|final|required|convenience|async|throws|rethrows)\s+)*func\s+(\w+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)/ },
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
for (let i = 0; i < lines.length; i++) {
|
|
267
|
+
const line = lines[i];
|
|
268
|
+
if (line.trimStart().startsWith('//')) continue;
|
|
269
|
+
for (const { kind, re } of patterns) {
|
|
270
|
+
const m = re.exec(line.trimStart());
|
|
271
|
+
if (m) {
|
|
272
|
+
const name = m[1];
|
|
273
|
+
const params = m[2] ?? '';
|
|
274
|
+
const arity = kind === 'function' && params.trim() ? params.split(',').length : undefined;
|
|
275
|
+
const exported = /\bpublic\b|\bopen\b/.test(line);
|
|
276
|
+
const endLine = findBlockEnd(lines, i);
|
|
277
|
+
symbols.push(makeSymbol(filePath, 'swift', kind, name, exported ? 'exported' : 'local', lines, i + 1, endLine, arity));
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return symbols;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function extractCpp(lines, filePath) {
|
|
286
|
+
const symbols = [];
|
|
287
|
+
const classRe = /^(?:template\s*<[^>]*>\s*)?(?:class|struct)\s+(\w+)(?:\s*:\s*[\w\s,:<>]*)?(?:\s*\{|$)/;
|
|
288
|
+
const nsRe = /^namespace\s+(\w+)\s*\{/;
|
|
289
|
+
const funcRe = /^(?:(?:inline|static|virtual|explicit|friend|constexpr|consteval|auto|template\s*<[^>]*>)\s+)*(?:[\w:*&<>\s]+\s+)?(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:noexcept\s*)?(?:override\s*)?(?:\{|;)$/;
|
|
290
|
+
|
|
291
|
+
for (let i = 0; i < lines.length; i++) {
|
|
292
|
+
const line = lines[i];
|
|
293
|
+
if (line.trimStart().startsWith('//') || line.trimStart().startsWith('#')) continue;
|
|
294
|
+
|
|
295
|
+
const nsMatch = nsRe.exec(line.trimStart());
|
|
296
|
+
if (nsMatch) {
|
|
297
|
+
symbols.push(makeSymbol(filePath, 'cpp', 'class', nsMatch[1], 'exported', lines, i + 1, i + 1, undefined));
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const classMatch = classRe.exec(line.trimStart());
|
|
302
|
+
if (classMatch) {
|
|
303
|
+
const name = classMatch[1];
|
|
304
|
+
if (name === 'override' || name === 'final') continue;
|
|
305
|
+
const endLine = findBlockEnd(lines, i);
|
|
306
|
+
symbols.push(makeSymbol(filePath, 'cpp', 'class', name, 'exported', lines, i + 1, endLine, undefined));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const funcMatch = funcRe.exec(line.trimStart());
|
|
311
|
+
if (funcMatch) {
|
|
312
|
+
const name = funcMatch[1];
|
|
313
|
+
const reserved = new Set(['if','for','while','switch','catch','return','delete','new']);
|
|
314
|
+
if (reserved.has(name) || name.includes('<') || name.includes(':')) continue;
|
|
315
|
+
const params = funcMatch[2] ?? '';
|
|
316
|
+
const arity = params.trim() && params.trim() !== 'void' ? params.split(',').length : 0;
|
|
317
|
+
const endLine = findBlockEnd(lines, i);
|
|
318
|
+
symbols.push(makeSymbol(filePath, 'cpp', 'function', name, 'exported', lines, i + 1, endLine, arity));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return symbols;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function extractCSharp(lines, filePath) {
|
|
325
|
+
const symbols = [];
|
|
326
|
+
let currentNamespace = 'global';
|
|
327
|
+
let currentContainer = null;
|
|
328
|
+
let containerDepth = null;
|
|
329
|
+
let braceDepth = 0;
|
|
330
|
+
|
|
331
|
+
for (let i = 0; i < lines.length; i++) {
|
|
332
|
+
const line = lines[i];
|
|
333
|
+
const nsMatch = line.match(/^\s*namespace\s+([\w.]+)/);
|
|
334
|
+
if (nsMatch) currentNamespace = nsMatch[1];
|
|
335
|
+
|
|
336
|
+
const typeMatch = line.match(/^\s*(?:public\s+)?(?:partial\s+)?(?:abstract\s+)?(?:sealed\s+)?(?:static\s+)?(record|class|interface|enum|struct)\s+(\w+)/);
|
|
337
|
+
if (typeMatch) {
|
|
338
|
+
const rawKind = typeMatch[1];
|
|
339
|
+
const kind = rawKind === 'interface' ? 'interface' : rawKind === 'record' ? 'record' : 'class';
|
|
340
|
+
const name = typeMatch[2];
|
|
341
|
+
const exported = line.includes('public');
|
|
342
|
+
const startLine = i + 1;
|
|
343
|
+
const endLine = findBlockEnd(lines, i);
|
|
344
|
+
const symbol = {
|
|
345
|
+
language: 'csharp',
|
|
346
|
+
filePath: filePath.replace(/\\/g, '/'),
|
|
347
|
+
kind,
|
|
348
|
+
name,
|
|
349
|
+
exportScope: exported ? 'exported' : 'local',
|
|
350
|
+
namespace: currentNamespace,
|
|
351
|
+
containerName: currentContainer?.name ?? 'none',
|
|
352
|
+
signature: '()',
|
|
353
|
+
range: { startLine, endLine },
|
|
354
|
+
codeHash: codeHash(lines, startLine, endLine),
|
|
355
|
+
};
|
|
356
|
+
symbol.symbolKey = buildSymbolKey(symbol);
|
|
357
|
+
symbols.push(symbol);
|
|
358
|
+
|
|
359
|
+
if (['class','interface','record','struct'].includes(rawKind)) {
|
|
360
|
+
currentContainer = { name, kind: rawKind };
|
|
361
|
+
containerDepth = braceDepth + (line.includes('{') ? 1 : 0);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (currentContainer) {
|
|
366
|
+
const methodMatch = line.match(/^\s+(?:public|protected|private|internal|static|virtual|override|abstract|async|sealed|new|extern)[\w\s<>,?.\[\]]*\s+(\w+)\s*\(([^)]*)\)/);
|
|
367
|
+
if (methodMatch && methodMatch[1] !== currentContainer.name) {
|
|
368
|
+
const name = methodMatch[1];
|
|
369
|
+
const params = methodMatch[2] ?? '';
|
|
370
|
+
const arity = params.trim() ? params.split(',').length : 0;
|
|
371
|
+
const exported = line.includes('public');
|
|
372
|
+
const startLine = i + 1;
|
|
373
|
+
const endLine = findBlockEnd(lines, i);
|
|
374
|
+
const symbol = {
|
|
375
|
+
language: 'csharp',
|
|
376
|
+
filePath: filePath.replace(/\\/g, '/'),
|
|
377
|
+
kind: 'method',
|
|
378
|
+
name,
|
|
379
|
+
exportScope: exported ? 'exported' : 'local',
|
|
380
|
+
namespace: currentNamespace,
|
|
381
|
+
containerName: currentContainer.name,
|
|
382
|
+
signature: `(${params.split(',').map(p => p.trim().split(' ')[0]).filter(Boolean).join(',')})`,
|
|
383
|
+
arity,
|
|
384
|
+
range: { startLine, endLine },
|
|
385
|
+
codeHash: codeHash(lines, startLine, endLine),
|
|
386
|
+
};
|
|
387
|
+
symbol.symbolKey = buildSymbolKey(symbol);
|
|
388
|
+
symbols.push(symbol);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (const ch of line) {
|
|
393
|
+
if (ch === '{') braceDepth++;
|
|
394
|
+
if (ch === '}') braceDepth--;
|
|
395
|
+
}
|
|
396
|
+
if (currentContainer && containerDepth !== null && braceDepth < containerDepth) {
|
|
397
|
+
currentContainer = null;
|
|
398
|
+
containerDepth = null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return symbols;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── Public dispatcher ──────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
const LANGUAGE_EXTRACTORS = {
|
|
407
|
+
python: extractPython,
|
|
408
|
+
java: extractJava,
|
|
409
|
+
go: extractGo,
|
|
410
|
+
rust: extractRust,
|
|
411
|
+
ruby: extractRuby,
|
|
412
|
+
php: extractPhp,
|
|
413
|
+
kotlin: extractKotlin,
|
|
414
|
+
swift: extractSwift,
|
|
415
|
+
cpp: extractCpp,
|
|
416
|
+
csharp: extractCSharp,
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
export const EXTENSION_TO_LANGUAGE = new Map([
|
|
420
|
+
['.py', 'python'],
|
|
421
|
+
['.java', 'java'],
|
|
422
|
+
['.go', 'go'],
|
|
423
|
+
['.rs', 'rust'],
|
|
424
|
+
['.rb', 'ruby'],
|
|
425
|
+
['.php', 'php'],
|
|
426
|
+
['.kt', 'kotlin'], ['.kts', 'kotlin'],
|
|
427
|
+
['.swift','swift'],
|
|
428
|
+
['.cpp', 'cpp'], ['.cc', 'cpp'], ['.cxx', 'cpp'],
|
|
429
|
+
['.hpp', 'cpp'], ['.h', 'cpp'],
|
|
430
|
+
['.cs', 'csharp'],
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
export function extractRegexSymbols({ filePath, content }) {
|
|
434
|
+
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
|
|
435
|
+
const language = EXTENSION_TO_LANGUAGE.get(ext);
|
|
436
|
+
if (!language) return [];
|
|
437
|
+
const lines = content.split('\n');
|
|
438
|
+
return LANGUAGE_EXTRACTORS[language](lines, filePath);
|
|
439
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function detectRulesContext({ readmeText = '' }) {
|
|
2
|
+
const rules = [];
|
|
3
|
+
for (const sentence of readmeText.split(/(?<=[.!?])\s+/)) {
|
|
4
|
+
if (/^use\s/i.test(sentence) || /^avoid\s/i.test(sentence) || /^keep\s/i.test(sentence)) {
|
|
5
|
+
rules.push(sentence.trim());
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
return { rules };
|
|
9
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
async function readJson(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function detectServicesFromSrc(projectRoot) {
|
|
13
|
+
const detected = new Set();
|
|
14
|
+
try {
|
|
15
|
+
const entries = await readdir(join(projectRoot, 'src'), { recursive: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (typeof entry !== 'string') continue;
|
|
18
|
+
if (!entry.endsWith('.js') && !entry.endsWith('.mjs') && !entry.endsWith('.ts') && !entry.endsWith('.tsx')) continue;
|
|
19
|
+
const content = await readFile(join(projectRoot, 'src', entry), 'utf8');
|
|
20
|
+
if (content.includes('@supabase/supabase-js')) detected.add('supabase');
|
|
21
|
+
if (content.includes('stripe')) detected.add('stripe');
|
|
22
|
+
if (content.includes('aws-sdk') || content.includes('@aws-sdk/')) detected.add('aws');
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
return [...detected].sort();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function detectStackContext(projectRoot) {
|
|
29
|
+
const packageJson = await readJson(join(projectRoot, 'package.json'));
|
|
30
|
+
const tsconfig = await readJson(join(projectRoot, 'tsconfig.json'));
|
|
31
|
+
const deps = packageJson?.dependencies ?? {};
|
|
32
|
+
const devDeps = packageJson?.devDependencies ?? {};
|
|
33
|
+
const allDeps = { ...deps, ...devDeps };
|
|
34
|
+
return {
|
|
35
|
+
languages: tsconfig ? ['typescript'] : [],
|
|
36
|
+
runtimes: ['node'],
|
|
37
|
+
frameworks: ['next', 'react'].filter((name) => name in allDeps),
|
|
38
|
+
packageManagers: packageJson?.packageManager ? [packageJson.packageManager] : [],
|
|
39
|
+
buildTools: ['typescript'].filter((name) => name in allDeps),
|
|
40
|
+
dependenciesSummary: {
|
|
41
|
+
critical: Object.keys(deps).filter((name) => ['react', 'next', 'zod'].includes(name)),
|
|
42
|
+
testing: Object.keys(devDeps).filter((name) => ['vitest', 'jest'].includes(name)),
|
|
43
|
+
},
|
|
44
|
+
integrations: {
|
|
45
|
+
detectedServices: await detectServicesFromSrc(projectRoot),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
async function listDirectories(dir, root, depth = 0, maxDepth = 2, acc = []) {
|
|
5
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
if (!entry.isDirectory()) continue;
|
|
8
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.planning') continue;
|
|
9
|
+
const full = join(dir, entry.name);
|
|
10
|
+
const rel = relative(root, full).replace(/\\/g, '/');
|
|
11
|
+
acc.push({ rel, depth });
|
|
12
|
+
if (depth < maxDepth - 1) {
|
|
13
|
+
await listDirectories(full, root, depth + 1, maxDepth, acc);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return acc;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function detectStructureContext(projectRoot) {
|
|
20
|
+
const dirs = await listDirectories(projectRoot, projectRoot);
|
|
21
|
+
const rootDirectories = dirs.filter((item) => item.depth === 0).map((item) => item.rel);
|
|
22
|
+
const keySubtrees = dirs.filter((item) => item.depth > 0).map((item) => item.rel);
|
|
23
|
+
const entryPoints = [];
|
|
24
|
+
for (const rel of ['src/main.ts', 'src/index.ts', 'src/app.ts', 'src/server.ts']) {
|
|
25
|
+
try {
|
|
26
|
+
const s = await stat(join(projectRoot, rel));
|
|
27
|
+
if (s.isFile()) entryPoints.push(rel);
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
return { rootDirectories, keySubtrees, entryPoints };
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ensureProjectMemoryContextDirs,
|
|
5
|
+
readJsonArtifact,
|
|
6
|
+
writeJsonArtifact,
|
|
7
|
+
} from './artifacts.mjs';
|
|
8
|
+
import { updateWorklistEntry } from './worklist-state.mjs';
|
|
9
|
+
|
|
10
|
+
export async function recordEnrichmentFailure({ projectRoot, symbolKey, error, failedAt }) {
|
|
11
|
+
const dirs = await ensureProjectMemoryContextDirs(projectRoot);
|
|
12
|
+
const worklistFile = join(dirs.enrichment, 'worklist.json');
|
|
13
|
+
const failuresFile = join(dirs.enrichment, 'failures.json');
|
|
14
|
+
const worklist = await readJsonArtifact(worklistFile, []);
|
|
15
|
+
const failures = await readJsonArtifact(failuresFile, []);
|
|
16
|
+
|
|
17
|
+
const updatedWorklist = updateWorklistEntry(worklist, symbolKey, {
|
|
18
|
+
status: 'error',
|
|
19
|
+
error,
|
|
20
|
+
failedAt,
|
|
21
|
+
});
|
|
22
|
+
const updatedFailures = [...failures, { symbolKey, error, failedAt }];
|
|
23
|
+
|
|
24
|
+
await writeJsonArtifact(worklistFile, updatedWorklist);
|
|
25
|
+
await writeJsonArtifact(failuresFile, updatedFailures);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
worklistFile,
|
|
29
|
+
failuresFile,
|
|
30
|
+
worklist: updatedWorklist,
|
|
31
|
+
failures: updatedFailures,
|
|
32
|
+
};
|
|
33
|
+
}
|