@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,88 @@
|
|
|
1
|
+
import { classifyEnrichmentError } from '../enrichment-errors.mjs';
|
|
2
|
+
|
|
3
|
+
function getCloudApiConfig(context) {
|
|
4
|
+
return context?.config?.cloudApi ?? {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function trimTrailingSlash(value) {
|
|
8
|
+
return value.replace(/\/+$/, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildSignal(request) {
|
|
12
|
+
return request?.timeoutMs ? AbortSignal.timeout(request.timeoutMs) : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createCloudApiProvider({ fetchImpl = fetch } = {}) {
|
|
16
|
+
return {
|
|
17
|
+
kind: 'cloud-api',
|
|
18
|
+
isConfigured(context) {
|
|
19
|
+
const { provider = 'openai-compatible', baseUrl, model, apiKeyEnv } = getCloudApiConfig(context);
|
|
20
|
+
const apiKey = context?.env?.[apiKeyEnv];
|
|
21
|
+
|
|
22
|
+
if (!baseUrl || !model || !apiKeyEnv) {
|
|
23
|
+
return { ok: false, reason: 'cloud-api requires baseUrl, model, and apiKeyEnv' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
return { ok: false, reason: `cloud-api requires api key in ${apiKeyEnv}` };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { ok: true, provider };
|
|
31
|
+
},
|
|
32
|
+
async isAvailable(context) {
|
|
33
|
+
const configured = this.isConfigured(context);
|
|
34
|
+
if (!configured.ok) {
|
|
35
|
+
return configured;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { provider = 'openai-compatible', baseUrl, apiKeyEnv } = getCloudApiConfig(context);
|
|
39
|
+
const apiKey = context?.env?.[apiKeyEnv];
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {
|
|
43
|
+
method: 'GET',
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${apiKey}`,
|
|
46
|
+
},
|
|
47
|
+
signal: buildSignal(context?.request),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const message = `${provider} ${response.status}: ${typeof response.text === 'function' ? await response.text() : 'provider unavailable'}`;
|
|
51
|
+
const classified = classifyEnrichmentError(new Error(message));
|
|
52
|
+
return { ok: false, reason: classified.message, errorType: classified.type };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const classified = classifyEnrichmentError(error);
|
|
57
|
+
return { ok: false, reason: classified.message, errorType: classified.type };
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async enrich(request, context) {
|
|
61
|
+
const { provider = 'openai-compatible', baseUrl, model, apiKeyEnv } = getCloudApiConfig(context);
|
|
62
|
+
const apiKey = context?.env?.[apiKeyEnv];
|
|
63
|
+
const response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/chat/completions`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
Authorization: `Bearer ${apiKey}`,
|
|
68
|
+
},
|
|
69
|
+
signal: buildSignal(request),
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
model,
|
|
72
|
+
messages: [{ role: 'user', content: request.prompt }],
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`${provider} ${response.status}: ${await response.text()}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
return {
|
|
82
|
+
content: data.choices?.[0]?.message?.content ?? '',
|
|
83
|
+
provider,
|
|
84
|
+
model,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { classifyEnrichmentError } from '../enrichment-errors.mjs';
|
|
2
|
+
|
|
3
|
+
function getLocalModelConfig(context) {
|
|
4
|
+
return context?.config?.localModel ?? {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function buildSignal(request) {
|
|
8
|
+
return request?.timeoutMs ? AbortSignal.timeout(request.timeoutMs) : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createLocalModelProvider({ fetchImpl = fetch } = {}) {
|
|
12
|
+
return {
|
|
13
|
+
kind: 'local-model',
|
|
14
|
+
isConfigured(context) {
|
|
15
|
+
const { baseUrl, model } = getLocalModelConfig(context);
|
|
16
|
+
if (!baseUrl || !model) {
|
|
17
|
+
return { ok: false, reason: 'local-model requires baseUrl and model' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { ok: true };
|
|
21
|
+
},
|
|
22
|
+
async isAvailable(context) {
|
|
23
|
+
const configured = this.isConfigured(context);
|
|
24
|
+
if (!configured.ok) {
|
|
25
|
+
return configured;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { baseUrl } = getLocalModelConfig(context);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetchImpl(`${baseUrl}/api/tags`, {
|
|
32
|
+
signal: buildSignal(context?.request),
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const message = `ollama ${response.status}: ${typeof response.text === 'function' ? await response.text() : 'provider unavailable'}`;
|
|
36
|
+
const classified = classifyEnrichmentError(new Error(message));
|
|
37
|
+
return { ok: false, reason: classified.message, errorType: classified.type };
|
|
38
|
+
}
|
|
39
|
+
return { ok: true };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const classified = classifyEnrichmentError(error);
|
|
42
|
+
return { ok: false, reason: classified.message, errorType: classified.type };
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async enrich(request, context) {
|
|
46
|
+
const { provider = 'ollama', baseUrl, model } = getLocalModelConfig(context);
|
|
47
|
+
const response = await fetchImpl(`${baseUrl}/api/generate`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
signal: buildSignal(request),
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
model,
|
|
53
|
+
prompt: request.prompt,
|
|
54
|
+
stream: false,
|
|
55
|
+
options: { temperature: 0.1, num_predict: 512 },
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(`Ollama ${response.status}: ${await response.text()}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
return { content: data.response, provider, model };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function createEmptyRefreshState() {
|
|
2
|
+
return {
|
|
3
|
+
trackedFiles: {},
|
|
4
|
+
memoryHashes: {},
|
|
5
|
+
updatedAt: null,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function updateRefreshStateEntry(state, filePath, hash) {
|
|
10
|
+
return {
|
|
11
|
+
...state,
|
|
12
|
+
trackedFiles: {
|
|
13
|
+
...state.trackedFiles,
|
|
14
|
+
[filePath]: hash,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function shouldRefreshProjectContext(state, filePath, nextHash) {
|
|
20
|
+
return state.trackedFiles[filePath] !== nextHash;
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export async function loadResultInput(rawInput) {
|
|
4
|
+
const input = String(rawInput ?? '').trim();
|
|
5
|
+
const jsonText = input.startsWith('@')
|
|
6
|
+
? await readFile(input.slice(1), 'utf8')
|
|
7
|
+
: input;
|
|
8
|
+
return JSON.parse(jsonText);
|
|
9
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createDepthConfig } from './query-engine.mjs';
|
|
2
|
+
|
|
3
|
+
export function renderContext({ target, neighbors, edges, depth, depthReached, projectBase, memoryContents, sourceCode }) {
|
|
4
|
+
const config = createDepthConfig(depth);
|
|
5
|
+
const maxChars = config.maxTokens * 4;
|
|
6
|
+
|
|
7
|
+
const sections = [];
|
|
8
|
+
|
|
9
|
+
if (projectBase) {
|
|
10
|
+
sections.push(`## Project Base\nStack: ${projectBase.stack} | Architecture: ${projectBase.architecture}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const enrichment = target.memoryId ? (memoryContents.get(target.memoryId) ?? '') : '';
|
|
14
|
+
const rangeStr = target.range ? ` (L${target.range.startLine}-${target.range.endLine})` : '';
|
|
15
|
+
let targetSection = `### Target: ${target.name} (${target.kind})\nFile: ${target.filePath}${rangeStr}`;
|
|
16
|
+
if (enrichment) {
|
|
17
|
+
targetSection += `\n${enrichment}`;
|
|
18
|
+
}
|
|
19
|
+
sections.push(targetSection);
|
|
20
|
+
|
|
21
|
+
if (neighbors.length > 0 && depth !== 'compact' || neighbors.length > 0 && depth === 'compact') {
|
|
22
|
+
const edgeMap = new Map();
|
|
23
|
+
for (const edge of edges) {
|
|
24
|
+
if (!edgeMap.has(edge.target)) edgeMap.set(edge.target, []);
|
|
25
|
+
edgeMap.get(edge.target).push(edge);
|
|
26
|
+
if (!edgeMap.has(edge.source)) edgeMap.set(edge.source, []);
|
|
27
|
+
edgeMap.get(edge.source).push(edge);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (const nb of neighbors) {
|
|
32
|
+
const nbEnrichment = nb.memoryId ? (memoryContents.get(nb.memoryId) ?? '') : '';
|
|
33
|
+
const firstLine = nbEnrichment ? nbEnrichment.split('\n')[0] : '';
|
|
34
|
+
|
|
35
|
+
const relEdge = edges.find(e => e.source === nb.graphNodeId || e.target === nb.graphNodeId);
|
|
36
|
+
const relation = relEdge ? relEdge.relation : '';
|
|
37
|
+
const dir = relEdge ? (relEdge.source === target.graphNodeId ? '→' : '←') : '';
|
|
38
|
+
|
|
39
|
+
let bullet = `- **${nb.name}** (${nb.kind}) ${dir} ${relation}`;
|
|
40
|
+
if (nb.filePath && nb.filePath !== target.filePath) {
|
|
41
|
+
bullet += ` — ${nb.filePath}`;
|
|
42
|
+
}
|
|
43
|
+
if (firstLine) {
|
|
44
|
+
bullet += `\n ${firstLine}`;
|
|
45
|
+
}
|
|
46
|
+
lines.push(bullet);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (lines.length > 0) {
|
|
50
|
+
sections.push(`### Structural Neighbors\n${lines.join('\n')}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (depth === 'extended' || depth === 'deep' || depth === 'disk') {
|
|
55
|
+
const communities = new Map();
|
|
56
|
+
for (const nb of neighbors) {
|
|
57
|
+
const comm = nb.community ?? 'default';
|
|
58
|
+
if (!communities.has(comm)) communities.set(comm, []);
|
|
59
|
+
communities.get(comm).push(nb);
|
|
60
|
+
}
|
|
61
|
+
if (communities.size > 0 && !(communities.size === 1 && communities.has('default'))) {
|
|
62
|
+
const commLines = [];
|
|
63
|
+
for (const [comm, members] of communities) {
|
|
64
|
+
if (comm === 'default') continue;
|
|
65
|
+
commLines.push(`- **${comm}**: ${members.map(m => m.name).join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
if (commLines.length > 0) {
|
|
68
|
+
sections.push(`### Module Communities\n${commLines.join('\n')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (depth === 'deep' || depth === 'disk') {
|
|
74
|
+
const inbound = edges.filter(e => e.target === target.graphNodeId);
|
|
75
|
+
if (inbound.length > 0) {
|
|
76
|
+
const impactLines = inbound.map(e => {
|
|
77
|
+
const srcNode = neighbors.find(n => n.graphNodeId === e.source);
|
|
78
|
+
const srcName = srcNode ? srcNode.name : e.source;
|
|
79
|
+
return `- ${srcName} — ${e.relation}`;
|
|
80
|
+
});
|
|
81
|
+
sections.push(`### Impact Scope\n${impactLines.join('\n')}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (depth === 'disk' && sourceCode) {
|
|
86
|
+
sections.push(`### Source Code\n\`\`\`\n${sourceCode}\n\`\`\``);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const header = `## Context: ${target.name}`;
|
|
90
|
+
let result = header + '\n\n' + sections.join('\n\n');
|
|
91
|
+
|
|
92
|
+
if (result.length > maxChars) {
|
|
93
|
+
result = result.slice(0, maxChars);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
const DEPTH_PRESETS = {
|
|
2
|
+
compact: { maxHops: 1, includeCommunity: false, maxTokens: 2000, readSourceFiles: false },
|
|
3
|
+
extended: { maxHops: 2, includeCommunity: true, maxTokens: 5000, readSourceFiles: false },
|
|
4
|
+
deep: { maxHops: 3, includeCommunity: true, maxTokens: 10000, readSourceFiles: false },
|
|
5
|
+
disk: { maxHops: 3, includeCommunity: true, maxTokens: 15000, readSourceFiles: true },
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createDepthConfig(depth) {
|
|
9
|
+
const preset = DEPTH_PRESETS[depth] ?? DEPTH_PRESETS.compact;
|
|
10
|
+
return { ...preset };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseSymbolKeyParts(key) {
|
|
14
|
+
return key.split('|');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function extractName(parts) {
|
|
18
|
+
return parts.length >= 5 ? parts[parts.length - 2] : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractFilePath(parts) {
|
|
22
|
+
return parts.length >= 2 ? parts[1] : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizePath(filePath) {
|
|
26
|
+
return String(filePath ?? '').replace(/\\/g, '/');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function focusToEdgeTypes(focus) {
|
|
30
|
+
const map = {
|
|
31
|
+
dependencies: ['imports', 'imports_from'],
|
|
32
|
+
callers: ['calls'],
|
|
33
|
+
containment: ['contains', 'method'],
|
|
34
|
+
};
|
|
35
|
+
return map[focus] ?? ['calls', 'imports', 'imports_from', 'contains', 'method'];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createQueryEngine({ graph, symbolIndex, worklist, enrichmentDir, projectSlug }) {
|
|
39
|
+
const nodeMap = new Map();
|
|
40
|
+
for (const node of graph.nodes ?? []) {
|
|
41
|
+
nodeMap.set(node.id, node);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const graphNodeIdToSymbolKeyMap = new Map();
|
|
45
|
+
const nameToSymbolKeys = new Map();
|
|
46
|
+
const filePathToSymbolKeys = new Map();
|
|
47
|
+
|
|
48
|
+
for (const key of Object.keys(symbolIndex)) {
|
|
49
|
+
const entry = symbolIndex[key];
|
|
50
|
+
if (entry.graphNodeId) {
|
|
51
|
+
graphNodeIdToSymbolKeyMap.set(entry.graphNodeId, key);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parts = parseSymbolKeyParts(key);
|
|
55
|
+
const name = extractName(parts);
|
|
56
|
+
if (name) {
|
|
57
|
+
const arr = nameToSymbolKeys.get(name);
|
|
58
|
+
if (arr) {
|
|
59
|
+
arr.push(key);
|
|
60
|
+
} else {
|
|
61
|
+
nameToSymbolKeys.set(name, [key]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const fp = extractFilePath(parts);
|
|
66
|
+
if (fp) {
|
|
67
|
+
const normalized = normalizePath(fp);
|
|
68
|
+
const arr = filePathToSymbolKeys.get(normalized);
|
|
69
|
+
if (arr) {
|
|
70
|
+
arr.push(key);
|
|
71
|
+
} else {
|
|
72
|
+
filePathToSymbolKeys.set(normalized, [key]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const links = graph.links ?? [];
|
|
78
|
+
|
|
79
|
+
function traverseGraph({ nodeIds, maxHops, edgeTypes, direction }) {
|
|
80
|
+
const types = edgeTypes ?? ['calls', 'imports', 'imports_from', 'contains', 'method'];
|
|
81
|
+
const dir = direction ?? 'outbound';
|
|
82
|
+
|
|
83
|
+
const visited = new Set();
|
|
84
|
+
const resultNodes = [];
|
|
85
|
+
const resultEdges = [];
|
|
86
|
+
let frontier = [];
|
|
87
|
+
|
|
88
|
+
for (const id of nodeIds) {
|
|
89
|
+
const node = nodeMap.get(id);
|
|
90
|
+
if (!node) continue;
|
|
91
|
+
visited.add(id);
|
|
92
|
+
resultNodes.push(node);
|
|
93
|
+
frontier.push(id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let depthReached = 0;
|
|
97
|
+
|
|
98
|
+
for (let hop = 0; hop < maxHops; hop++) {
|
|
99
|
+
const nextFrontier = [];
|
|
100
|
+
for (const nodeId of frontier) {
|
|
101
|
+
for (const link of links) {
|
|
102
|
+
if (!types.includes(link.relation)) continue;
|
|
103
|
+
let neighbor = null;
|
|
104
|
+
if (dir === 'outbound' && link.source === nodeId) {
|
|
105
|
+
neighbor = link.target;
|
|
106
|
+
} else if (dir === 'inbound' && link.target === nodeId) {
|
|
107
|
+
neighbor = link.source;
|
|
108
|
+
}
|
|
109
|
+
if (neighbor != null && !visited.has(neighbor)) {
|
|
110
|
+
visited.add(neighbor);
|
|
111
|
+
const neighborNode = nodeMap.get(neighbor);
|
|
112
|
+
if (neighborNode) {
|
|
113
|
+
resultNodes.push(neighborNode);
|
|
114
|
+
resultEdges.push(link);
|
|
115
|
+
nextFrontier.push(neighbor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (nextFrontier.length === 0) break;
|
|
121
|
+
frontier = nextFrontier;
|
|
122
|
+
depthReached = hop + 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { nodes: resultNodes, edges: resultEdges, depth_reached: depthReached };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveWorklistEntry(symbolKey) {
|
|
129
|
+
return (worklist ?? []).find((e) => e.symbolKey === symbolKey) ?? null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildSymbolInfo(symbolKey) {
|
|
133
|
+
const entry = symbolIndex[symbolKey];
|
|
134
|
+
const wl = resolveWorklistEntry(symbolKey);
|
|
135
|
+
const parts = parseSymbolKeyParts(symbolKey);
|
|
136
|
+
return {
|
|
137
|
+
symbolKey,
|
|
138
|
+
name: wl?.name ?? extractName(parts),
|
|
139
|
+
filePath: wl?.filePath ?? extractFilePath(parts),
|
|
140
|
+
kind: wl?.kind ?? null,
|
|
141
|
+
range: wl?.range ?? null,
|
|
142
|
+
graphNodeId: entry?.graphNodeId ?? null,
|
|
143
|
+
memoryId: entry?.memoryId ?? null,
|
|
144
|
+
status: entry?.status ?? null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function querySymbolContext({ symbolKey, depth }) {
|
|
149
|
+
const config = createDepthConfig(depth);
|
|
150
|
+
const target = buildSymbolInfo(symbolKey);
|
|
151
|
+
if (!target.graphNodeId) {
|
|
152
|
+
return { target, neighbors: [], edges: [], depth_reached: 0 };
|
|
153
|
+
}
|
|
154
|
+
const traversal = traverseGraph({ nodeIds: [target.graphNodeId], maxHops: config.maxHops });
|
|
155
|
+
const neighbors = traversal.nodes
|
|
156
|
+
.filter((n) => n.id !== target.graphNodeId)
|
|
157
|
+
.map((n) => {
|
|
158
|
+
const sk = graphNodeIdToSymbolKeyMap.get(n.id);
|
|
159
|
+
if (sk) return buildSymbolInfo(sk);
|
|
160
|
+
return { graphNodeId: n.id, label: n.label, sourceFile: n.source_file ?? null, symbolKey: null };
|
|
161
|
+
});
|
|
162
|
+
return { target, neighbors, edges: traversal.edges, depth_reached: traversal.depth_reached };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function queryFileContext({ filePath, depth }) {
|
|
166
|
+
const config = createDepthConfig(depth);
|
|
167
|
+
const normalized = normalizePath(filePath);
|
|
168
|
+
const symbolKeys = filePathToSymbolKeys.get(normalized) ?? [];
|
|
169
|
+
const symbols = symbolKeys.map(buildSymbolInfo);
|
|
170
|
+
const fileNodeIds = (graph.nodes ?? [])
|
|
171
|
+
.filter((n) => normalizePath(n.source_file ?? '') === normalized)
|
|
172
|
+
.map((n) => n.id);
|
|
173
|
+
const outTraversal = traverseGraph({ nodeIds: fileNodeIds, maxHops: config.maxHops });
|
|
174
|
+
const inTraversal = traverseGraph({ nodeIds: fileNodeIds, maxHops: config.maxHops, direction: 'inbound' });
|
|
175
|
+
const fileNodeIdSet = new Set(fileNodeIds);
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
const neighbors = [];
|
|
178
|
+
const edges = [];
|
|
179
|
+
for (const n of [...outTraversal.nodes, ...inTraversal.nodes]) {
|
|
180
|
+
if (fileNodeIdSet.has(n.id) || seen.has(n.id)) continue;
|
|
181
|
+
seen.add(n.id);
|
|
182
|
+
const sk = graphNodeIdToSymbolKeyMap.get(n.id);
|
|
183
|
+
neighbors.push(sk ? buildSymbolInfo(sk) : { graphNodeId: n.id, label: n.label, sourceFile: n.source_file ?? null, symbolKey: null });
|
|
184
|
+
}
|
|
185
|
+
const edgeSet = new Set();
|
|
186
|
+
for (const e of [...outTraversal.edges, ...inTraversal.edges]) {
|
|
187
|
+
const key = `${e.source}->${e.target}`;
|
|
188
|
+
if (!edgeSet.has(key)) { edgeSet.add(key); edges.push(e); }
|
|
189
|
+
}
|
|
190
|
+
const depth_reached = Math.max(outTraversal.depth_reached, inTraversal.depth_reached);
|
|
191
|
+
return { symbols, neighbors, edges, depth_reached };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function queryImpactScope({ symbolKeys, depth }) {
|
|
195
|
+
const config = createDepthConfig(depth);
|
|
196
|
+
const targets = symbolKeys.map(buildSymbolInfo);
|
|
197
|
+
const nodeIds = targets.map((t) => t.graphNodeId).filter(Boolean);
|
|
198
|
+
const traversal = traverseGraph({ nodeIds, maxHops: config.maxHops, direction: 'inbound' });
|
|
199
|
+
const targetIdSet = new Set(nodeIds);
|
|
200
|
+
const dependents = traversal.nodes
|
|
201
|
+
.filter((n) => !targetIdSet.has(n.id))
|
|
202
|
+
.map((n) => {
|
|
203
|
+
const sk = graphNodeIdToSymbolKeyMap.get(n.id);
|
|
204
|
+
if (sk) return buildSymbolInfo(sk);
|
|
205
|
+
return { graphNodeId: n.id, label: n.label, sourceFile: n.source_file ?? null, symbolKey: null };
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
target: targets.length === 1 ? targets[0] : targets,
|
|
209
|
+
dependents,
|
|
210
|
+
edges: traversal.edges,
|
|
211
|
+
depth_reached: traversal.depth_reached,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
graphNodeIdToSymbolKey(graphNodeId) {
|
|
217
|
+
return graphNodeIdToSymbolKeyMap.get(graphNodeId) ?? null;
|
|
218
|
+
},
|
|
219
|
+
findSymbolKeyByName(name) {
|
|
220
|
+
return nameToSymbolKeys.get(name) ?? [];
|
|
221
|
+
},
|
|
222
|
+
findSymbolKeysByFilePath(filePath) {
|
|
223
|
+
return filePathToSymbolKeys.get(normalizePath(filePath)) ?? [];
|
|
224
|
+
},
|
|
225
|
+
traverseGraph,
|
|
226
|
+
querySymbolContext,
|
|
227
|
+
queryFileContext,
|
|
228
|
+
queryImpactScope,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function parseList(value) {
|
|
2
|
+
return String(value ?? '')
|
|
3
|
+
.split(',')
|
|
4
|
+
.map((item) => item.trim())
|
|
5
|
+
.filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeSemanticReport(report) {
|
|
9
|
+
const fields = new Map();
|
|
10
|
+
|
|
11
|
+
for (const finding of report.findings ?? []) {
|
|
12
|
+
const [rawKey, ...rest] = String(finding).split(':');
|
|
13
|
+
if (!rawKey || rest.length === 0) continue;
|
|
14
|
+
fields.set(rawKey.trim().toLowerCase(), rest.join(':').trim());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const summary = String(report.summary ?? '').trim();
|
|
18
|
+
return {
|
|
19
|
+
responsibility: fields.get('responsibility') || summary,
|
|
20
|
+
inputs: parseList(fields.get('inputs')),
|
|
21
|
+
output: fields.get('output') || 'Not specified.',
|
|
22
|
+
dependencies: parseList(fields.get('dependencies')),
|
|
23
|
+
role: fields.get('role') || 'Not specified.',
|
|
24
|
+
summary,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function sliceLines(content, startLine, endLine) {
|
|
2
|
+
return content.split('\n').slice(startLine - 1, endLine).join('\n');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function collectTypeScriptContext(lines, symbolStartLine) {
|
|
6
|
+
const context = [];
|
|
7
|
+
for (let index = 0; index < symbolStartLine - 1; index += 1) {
|
|
8
|
+
const line = lines[index];
|
|
9
|
+
if (/^\s*import\b/.test(line)) {
|
|
10
|
+
context.push(line);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function collectCSharpContext(lines, symbolStartLine) {
|
|
17
|
+
const context = [];
|
|
18
|
+
for (let index = 0; index < symbolStartLine - 1; index += 1) {
|
|
19
|
+
const line = lines[index];
|
|
20
|
+
if (/^\s*using\s+/.test(line) || /^\s*namespace\s+/.test(line)) {
|
|
21
|
+
context.push(line);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return context;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildSemanticPrompt(unit) {
|
|
28
|
+
return [
|
|
29
|
+
`Symbol Key: ${unit.symbolKey}`,
|
|
30
|
+
`Language: ${unit.language}`,
|
|
31
|
+
`Kind: ${unit.kind}`,
|
|
32
|
+
`Name: ${unit.name}`,
|
|
33
|
+
`Location: ${unit.filePath}:${unit.range.startLine}-${unit.range.endLine}`,
|
|
34
|
+
'',
|
|
35
|
+
'Context:',
|
|
36
|
+
unit.context || 'None',
|
|
37
|
+
'',
|
|
38
|
+
'Code:',
|
|
39
|
+
unit.code,
|
|
40
|
+
'',
|
|
41
|
+
'Return a compact structured explanation with:',
|
|
42
|
+
'- responsibility',
|
|
43
|
+
'- primary inputs',
|
|
44
|
+
'- output',
|
|
45
|
+
'- immediate dependencies',
|
|
46
|
+
'- role in module',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildSemanticUnit({ symbol, content }) {
|
|
51
|
+
const lines = content.split('\n');
|
|
52
|
+
const { startLine, endLine } = symbol.range;
|
|
53
|
+
const contextLines = symbol.language === 'csharp'
|
|
54
|
+
? collectCSharpContext(lines, startLine)
|
|
55
|
+
: collectTypeScriptContext(lines, startLine);
|
|
56
|
+
|
|
57
|
+
const unit = {
|
|
58
|
+
symbolKey: symbol.symbolKey,
|
|
59
|
+
graphNodeId: symbol.graphNodeId ?? null,
|
|
60
|
+
language: symbol.language,
|
|
61
|
+
filePath: symbol.filePath,
|
|
62
|
+
kind: symbol.kind,
|
|
63
|
+
name: symbol.name,
|
|
64
|
+
range: symbol.range,
|
|
65
|
+
codeHash: symbol.codeHash,
|
|
66
|
+
context: contextLines.join('\n').trim(),
|
|
67
|
+
code: sliceLines(content, startLine, endLine),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...unit,
|
|
72
|
+
prompt: buildSemanticPrompt(unit),
|
|
73
|
+
};
|
|
74
|
+
}
|