@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,30 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ensureProjectMemoryContextDirs,
|
|
5
|
+
readJsonArtifact,
|
|
6
|
+
writeJsonArtifact,
|
|
7
|
+
} from './artifacts.mjs';
|
|
8
|
+
import { persistEnrichmentResult } from './persist-enrichment-result.mjs';
|
|
9
|
+
import { updateWorklistEntry } from './worklist-state.mjs';
|
|
10
|
+
|
|
11
|
+
export async function finalizeEnrichment({ projectRoot, result }) {
|
|
12
|
+
const persisted = await persistEnrichmentResult({ projectRoot, result });
|
|
13
|
+
const dirs = await ensureProjectMemoryContextDirs(projectRoot);
|
|
14
|
+
const worklistFile = join(dirs.enrichment, 'worklist.json');
|
|
15
|
+
const worklist = await readJsonArtifact(worklistFile, []);
|
|
16
|
+
const updatedWorklist = updateWorklistEntry(worklist, result.symbolKey, {
|
|
17
|
+
status: result.status,
|
|
18
|
+
memoryId: result.memoryId,
|
|
19
|
+
graphNodeId: result.graphNodeId ?? null,
|
|
20
|
+
enrichedAt: result.enrichedAt,
|
|
21
|
+
error: undefined,
|
|
22
|
+
});
|
|
23
|
+
await writeJsonArtifact(worklistFile, updatedWorklist);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...persisted,
|
|
27
|
+
worklistFile,
|
|
28
|
+
worklist: updatedWorklist,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function backfillGraphNode({
|
|
2
|
+
graph,
|
|
3
|
+
symbolKey,
|
|
4
|
+
graphNodeId,
|
|
5
|
+
memoryId,
|
|
6
|
+
semanticSummary,
|
|
7
|
+
codeHash,
|
|
8
|
+
enrichedAt,
|
|
9
|
+
status,
|
|
10
|
+
}) {
|
|
11
|
+
return {
|
|
12
|
+
...graph,
|
|
13
|
+
nodes: (graph.nodes ?? []).map((node) => {
|
|
14
|
+
const matchesGraphNodeId = graphNodeId && node?.id === graphNodeId;
|
|
15
|
+
const matchesSymbolKey = node?.metadata?.symbolKey === symbolKey;
|
|
16
|
+
if (!matchesGraphNodeId && !matchesSymbolKey) {
|
|
17
|
+
return node;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
...node,
|
|
22
|
+
metadata: {
|
|
23
|
+
...node.metadata,
|
|
24
|
+
symbolKey,
|
|
25
|
+
graphNodeId: graphNodeId ?? node.id,
|
|
26
|
+
memoryId,
|
|
27
|
+
semanticSummary,
|
|
28
|
+
codeHash,
|
|
29
|
+
lastEnrichedAt: enrichedAt,
|
|
30
|
+
enrichmentStatus: status,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function normalizePath(value) {
|
|
2
|
+
return String(value ?? '').replace(/\\/g, '/');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function getNodeMetadata(node) {
|
|
6
|
+
return node?.metadata ?? {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function extractLabelFromGraphify(raw) {
|
|
10
|
+
return raw.replace(/^\./, '').replace(/\(\)$/, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function inferKindFromLabel(raw) {
|
|
14
|
+
if (/^\./.test(raw)) return 'function';
|
|
15
|
+
if (/\.\w+\(\)$/.test(raw)) return 'method';
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findBySymbolKey(graph, symbolKey) {
|
|
20
|
+
return (graph.nodes ?? []).find((node) => {
|
|
21
|
+
const metadata = getNodeMetadata(node);
|
|
22
|
+
return metadata.symbolKey === symbolKey || node.symbolKey === symbolKey;
|
|
23
|
+
}) ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function findByFilePathAndName(graph, symbol) {
|
|
27
|
+
const filePath = normalizePath(symbol.filePath);
|
|
28
|
+
const candidates = (graph.nodes ?? []).filter((node) => {
|
|
29
|
+
const nodeFilePath = normalizePath(node.source_file ?? node.filePath ?? '');
|
|
30
|
+
return nodeFilePath === filePath;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const exact = candidates.find((node) => {
|
|
34
|
+
const raw = node.label ?? '';
|
|
35
|
+
const cleaned = extractLabelFromGraphify(raw);
|
|
36
|
+
return cleaned === symbol.name;
|
|
37
|
+
});
|
|
38
|
+
if (exact) return exact;
|
|
39
|
+
|
|
40
|
+
const inferable = candidates.find((node) => {
|
|
41
|
+
const raw = node.label ?? '';
|
|
42
|
+
const cleaned = extractLabelFromGraphify(raw);
|
|
43
|
+
const inferredKind = inferKindFromLabel(raw);
|
|
44
|
+
if (cleaned !== symbol.name) return false;
|
|
45
|
+
if (!inferredKind) return true;
|
|
46
|
+
if (symbol.kind === 'method' && inferredKind === 'function') return true;
|
|
47
|
+
if (symbol.kind === inferredKind) return true;
|
|
48
|
+
return false;
|
|
49
|
+
});
|
|
50
|
+
if (inferable) return inferable;
|
|
51
|
+
|
|
52
|
+
const normLabel = (symbol.name ?? '').toLowerCase();
|
|
53
|
+
return candidates.find((node) => (node.norm_label ?? '').toLowerCase() === normLabel) ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function attachGraphNodeIds({ symbols, graph }) {
|
|
57
|
+
return symbols.map((symbol) => {
|
|
58
|
+
const matchedNode = findBySymbolKey(graph, symbol.symbolKey) ?? findByFilePathAndName(graph, symbol);
|
|
59
|
+
return {
|
|
60
|
+
...symbol,
|
|
61
|
+
graphNodeId: matchedNode?.id ?? symbol.graphNodeId ?? null,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function normalizeString(value) {
|
|
2
|
+
return String(value ?? '').trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function normalizeList(values) {
|
|
6
|
+
return Array.from(new Set((values ?? []).map((value) => normalizeString(value)).filter(Boolean)));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildIntakeContext({ projectDescription, mappingGoals, focusAreas = [] }) {
|
|
10
|
+
return {
|
|
11
|
+
projectDescription: normalizeString(projectDescription),
|
|
12
|
+
mappingGoals: normalizeList(mappingGoals),
|
|
13
|
+
focusAreas: normalizeList(focusAreas),
|
|
14
|
+
createdAt: new Date().toISOString(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isAgentInstructionFile, normalizeProjectPath } from './platform.mjs';
|
|
2
|
+
|
|
3
|
+
const PACKAGE_FILES = new Set(['package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'tsconfig.json', 'global.json']);
|
|
4
|
+
|
|
5
|
+
export function detectInvalidatedProjectContextKinds(changedFiles) {
|
|
6
|
+
const invalidated = new Set();
|
|
7
|
+
for (const file of changedFiles) {
|
|
8
|
+
const normalizedFile = normalizeProjectPath(file);
|
|
9
|
+
|
|
10
|
+
if (PACKAGE_FILES.has(normalizedFile) || normalizedFile.endsWith('.csproj')) {
|
|
11
|
+
invalidated.add('stack-runtime');
|
|
12
|
+
invalidated.add('dependencies-summary');
|
|
13
|
+
invalidated.add('integrations-summary');
|
|
14
|
+
}
|
|
15
|
+
if (normalizedFile.startsWith('.planning/project-memory-context/project-context/declared/technical-rules')) {
|
|
16
|
+
invalidated.add('technical-rules');
|
|
17
|
+
}
|
|
18
|
+
if (normalizedFile.startsWith('.planning/project-memory-context/project-context/declared/project-requirements')) {
|
|
19
|
+
invalidated.add('project-requirements');
|
|
20
|
+
}
|
|
21
|
+
if (normalizedFile.startsWith('.planning/project-memory-context/project-context/declared/known-issues-and-fixes')) {
|
|
22
|
+
invalidated.add('known-issues-and-fixes');
|
|
23
|
+
}
|
|
24
|
+
if (normalizedFile.startsWith('.planning/project-memory-context/project-context/declared/architecture-target')) {
|
|
25
|
+
invalidated.add('architecture-target');
|
|
26
|
+
}
|
|
27
|
+
if (normalizedFile === 'README.md' || isAgentInstructionFile(normalizedFile)) {
|
|
28
|
+
invalidated.add('technical-rules');
|
|
29
|
+
invalidated.add('project-requirements');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [...invalidated];
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function renderProjectContextMarkdown(memory) {
|
|
2
|
+
const sourceFiles = (memory.source_files ?? []).map((file) => `- \`${file}\``).join('\n') || '- None';
|
|
3
|
+
const graphRefs = (memory.graph_refs ?? []).map((ref) => `- \`${ref}\``).join('\n') || '- None';
|
|
4
|
+
return [
|
|
5
|
+
`# ${memory.title}`,
|
|
6
|
+
'',
|
|
7
|
+
`**Kind:** ${memory.kind}`,
|
|
8
|
+
`**Updated:** ${memory.updated_at}`,
|
|
9
|
+
'',
|
|
10
|
+
'## Summary',
|
|
11
|
+
'',
|
|
12
|
+
memory.summary,
|
|
13
|
+
'',
|
|
14
|
+
'## Body',
|
|
15
|
+
'',
|
|
16
|
+
memory.body,
|
|
17
|
+
'',
|
|
18
|
+
'## Source Files',
|
|
19
|
+
'',
|
|
20
|
+
sourceFiles,
|
|
21
|
+
'',
|
|
22
|
+
'## Graph References',
|
|
23
|
+
'',
|
|
24
|
+
graphRefs,
|
|
25
|
+
'',
|
|
26
|
+
].join('\n');
|
|
27
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createMaterializedProjectContext } from './project-context-schema.mjs';
|
|
2
|
+
|
|
3
|
+
function formatList(label, values) {
|
|
4
|
+
return values?.length ? `- ${label}: ${values.join(', ')}` : `- ${label}: Not detected`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function materializeProjectContextMemories({ projectSlug, detected, declared, updatedAt }) {
|
|
8
|
+
const commonTags = ['project-context', `project:${projectSlug}`];
|
|
9
|
+
|
|
10
|
+
return [
|
|
11
|
+
createMaterializedProjectContext({
|
|
12
|
+
kind: 'stack-runtime',
|
|
13
|
+
title: 'Project stack and runtime',
|
|
14
|
+
summary: `Languages: ${detected.stack.languages.join(', ')}. Frameworks: ${detected.stack.frameworks.join(', ')}.`,
|
|
15
|
+
body: [
|
|
16
|
+
formatList('Languages', detected.stack.languages),
|
|
17
|
+
formatList('Runtimes', detected.stack.runtimes),
|
|
18
|
+
formatList('Frameworks', detected.stack.frameworks),
|
|
19
|
+
formatList('Package managers', detected.stack.packageManagers),
|
|
20
|
+
].join('\n'),
|
|
21
|
+
tags: [...commonTags, 'stack'],
|
|
22
|
+
sourceFiles: ['package.json', 'tsconfig.json'],
|
|
23
|
+
graphRefs: [],
|
|
24
|
+
sourceMode: 'detected',
|
|
25
|
+
confidence: 'high',
|
|
26
|
+
updatedAt,
|
|
27
|
+
}),
|
|
28
|
+
createMaterializedProjectContext({
|
|
29
|
+
kind: 'dependencies-summary',
|
|
30
|
+
title: 'Dependency summary',
|
|
31
|
+
summary: `Critical dependencies: ${(detected.stack.dependenciesSummary.critical ?? []).join(', ') || 'none'}.`,
|
|
32
|
+
body: [formatList('Critical', detected.stack.dependenciesSummary.critical), formatList('Testing', detected.stack.dependenciesSummary.testing)].join('\n'),
|
|
33
|
+
tags: [...commonTags, 'dependencies'],
|
|
34
|
+
sourceFiles: ['package.json'],
|
|
35
|
+
graphRefs: [],
|
|
36
|
+
sourceMode: 'detected',
|
|
37
|
+
confidence: 'high',
|
|
38
|
+
updatedAt,
|
|
39
|
+
}),
|
|
40
|
+
createMaterializedProjectContext({
|
|
41
|
+
kind: 'integrations-summary',
|
|
42
|
+
title: 'Integration summary',
|
|
43
|
+
summary: `Detected services: ${(detected.stack.integrations.detectedServices ?? []).join(', ') || 'none'}.`,
|
|
44
|
+
body: formatList('Services', detected.stack.integrations.detectedServices),
|
|
45
|
+
tags: [...commonTags, 'integrations'],
|
|
46
|
+
sourceFiles: ['src'],
|
|
47
|
+
graphRefs: [],
|
|
48
|
+
sourceMode: 'detected',
|
|
49
|
+
confidence: 'medium',
|
|
50
|
+
updatedAt,
|
|
51
|
+
}),
|
|
52
|
+
createMaterializedProjectContext({
|
|
53
|
+
kind: 'architecture-current',
|
|
54
|
+
title: 'Current architecture',
|
|
55
|
+
summary: `Pattern: ${detected.architecture.pattern}. Entry points: ${(detected.architecture.entryPoints ?? []).join(', ') || 'none'}.`,
|
|
56
|
+
body: [
|
|
57
|
+
`- Pattern: ${detected.architecture.pattern}`,
|
|
58
|
+
formatList('Entry points', detected.architecture.entryPoints),
|
|
59
|
+
].join('\n'),
|
|
60
|
+
tags: [...commonTags, 'architecture', 'current-state'],
|
|
61
|
+
sourceFiles: detected.architecture.entryPoints,
|
|
62
|
+
graphRefs: detected.architecture.graphRefs,
|
|
63
|
+
sourceMode: 'detected',
|
|
64
|
+
confidence: 'medium',
|
|
65
|
+
updatedAt,
|
|
66
|
+
}),
|
|
67
|
+
createMaterializedProjectContext({
|
|
68
|
+
kind: 'architecture-target',
|
|
69
|
+
title: 'Target architecture',
|
|
70
|
+
summary: declared.architectureTarget.architecture || 'No target architecture declared.',
|
|
71
|
+
body: declared.architectureTarget.architecture || 'No target architecture declared.',
|
|
72
|
+
tags: [...commonTags, 'architecture', 'target-state'],
|
|
73
|
+
sourceFiles: ['.planning/project-memory-context/project-context/declared/architecture-target.json'],
|
|
74
|
+
graphRefs: [],
|
|
75
|
+
sourceMode: 'declared',
|
|
76
|
+
confidence: 'high',
|
|
77
|
+
updatedAt,
|
|
78
|
+
}),
|
|
79
|
+
createMaterializedProjectContext({
|
|
80
|
+
kind: 'structure-summary',
|
|
81
|
+
title: 'Project structure summary',
|
|
82
|
+
summary: `Root directories: ${(detected.structure.rootDirectories ?? []).join(', ') || 'none'}.`,
|
|
83
|
+
body: [formatList('Root directories', detected.structure.rootDirectories), formatList('Key subtrees', detected.structure.keySubtrees), formatList('Entry points', detected.structure.entryPoints)].join('\n'),
|
|
84
|
+
tags: [...commonTags, 'structure'],
|
|
85
|
+
sourceFiles: detected.structure.entryPoints,
|
|
86
|
+
graphRefs: [],
|
|
87
|
+
sourceMode: 'detected',
|
|
88
|
+
confidence: 'high',
|
|
89
|
+
updatedAt,
|
|
90
|
+
}),
|
|
91
|
+
createMaterializedProjectContext({
|
|
92
|
+
kind: 'technical-rules',
|
|
93
|
+
title: 'Technical rules',
|
|
94
|
+
summary: [...(declared.technicalRules.rules ?? []), ...(detected.rules.rules ?? [])].slice(0, 2).join(' '),
|
|
95
|
+
body: [...(declared.technicalRules.rules ?? []), ...(detected.rules.rules ?? [])].map((line) => `- ${line}`).join('\n'),
|
|
96
|
+
tags: [...commonTags, 'rules'],
|
|
97
|
+
sourceFiles: ['README.md'],
|
|
98
|
+
graphRefs: [],
|
|
99
|
+
sourceMode: 'merged',
|
|
100
|
+
confidence: 'medium',
|
|
101
|
+
updatedAt,
|
|
102
|
+
}),
|
|
103
|
+
createMaterializedProjectContext({
|
|
104
|
+
kind: 'project-requirements',
|
|
105
|
+
title: 'Project requirements',
|
|
106
|
+
summary: (declared.projectRequirements.requirements ?? []).slice(0, 2).join(' '),
|
|
107
|
+
body: (declared.projectRequirements.requirements ?? []).map((line) => `- ${line}`).join('\n') || '- No declared requirements.',
|
|
108
|
+
tags: [...commonTags, 'requirements'],
|
|
109
|
+
sourceFiles: ['.planning/project-memory-context/project-context/declared/project-requirements.json'],
|
|
110
|
+
graphRefs: [],
|
|
111
|
+
sourceMode: 'declared',
|
|
112
|
+
confidence: 'high',
|
|
113
|
+
updatedAt,
|
|
114
|
+
}),
|
|
115
|
+
createMaterializedProjectContext({
|
|
116
|
+
kind: 'known-issues-and-fixes',
|
|
117
|
+
title: 'Known issues and fixes',
|
|
118
|
+
summary: `${(declared.knownIssuesAndFixes.items ?? []).length} known issues recorded.`,
|
|
119
|
+
body: (declared.knownIssuesAndFixes.items ?? []).map((item) => `- Symptom: ${item.symptom}; Workaround: ${item.workaround}`).join('\n') || '- No known issues recorded.',
|
|
120
|
+
tags: [...commonTags, 'issues'],
|
|
121
|
+
sourceFiles: ['.planning/project-memory-context/project-context/declared/known-issues-and-fixes.json'],
|
|
122
|
+
graphRefs: [],
|
|
123
|
+
sourceMode: 'declared',
|
|
124
|
+
confidence: 'high',
|
|
125
|
+
updatedAt,
|
|
126
|
+
}),
|
|
127
|
+
];
|
|
128
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function normalizeList(values) {
|
|
2
|
+
return (values ?? []).map((value) => String(value).trim()).filter(Boolean);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function buildMemoryPayload({ projectSlug, job, semantic }) {
|
|
6
|
+
const inputs = normalizeList(semantic.inputs);
|
|
7
|
+
const dependencies = normalizeList(semantic.dependencies);
|
|
8
|
+
const tags = [
|
|
9
|
+
'symbol',
|
|
10
|
+
job.language,
|
|
11
|
+
job.kind,
|
|
12
|
+
`project:${projectSlug}`,
|
|
13
|
+
`file:${job.filePath}`,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const content = [
|
|
17
|
+
`Symbol: ${job.name}`,
|
|
18
|
+
`Kind: ${job.kind}`,
|
|
19
|
+
`Language: ${job.language}`,
|
|
20
|
+
`Location: ${job.filePath}:${job.range.startLine}-${job.range.endLine}`,
|
|
21
|
+
'',
|
|
22
|
+
'Responsibility:',
|
|
23
|
+
semantic.responsibility,
|
|
24
|
+
'',
|
|
25
|
+
'Inputs:',
|
|
26
|
+
...(inputs.length > 0 ? inputs.map((input) => `- ${input}`) : ['- None detected']),
|
|
27
|
+
'',
|
|
28
|
+
'Output:',
|
|
29
|
+
semantic.output,
|
|
30
|
+
'',
|
|
31
|
+
'Immediate dependencies:',
|
|
32
|
+
...(dependencies.length > 0 ? dependencies.map((dependency) => `- ${dependency}`) : ['- None detected']),
|
|
33
|
+
'',
|
|
34
|
+
'Role in module:',
|
|
35
|
+
semantic.role,
|
|
36
|
+
].join('\n');
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
content,
|
|
40
|
+
category: 'architecture',
|
|
41
|
+
tags,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildEnrichmentResult({ job, memoryId, semanticSummary, status, enrichedAt }) {
|
|
46
|
+
return {
|
|
47
|
+
symbolKey: job.symbolKey,
|
|
48
|
+
graphNodeId: job.graphNodeId ?? null,
|
|
49
|
+
memoryId,
|
|
50
|
+
codeHash: job.codeHash,
|
|
51
|
+
semanticSummary,
|
|
52
|
+
status,
|
|
53
|
+
enrichedAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -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 { applyEnrichmentResult } from './enrichment-linker.mjs';
|
|
9
|
+
|
|
10
|
+
export async function persistEnrichmentResult({ projectRoot, result }) {
|
|
11
|
+
const dirs = await ensureProjectMemoryContextDirs(projectRoot);
|
|
12
|
+
const graphFile = join(dirs.graph, 'graph.json');
|
|
13
|
+
const indexFile = join(dirs.enrichment, 'symbol-index.json');
|
|
14
|
+
|
|
15
|
+
const graph = await readJsonArtifact(graphFile, { nodes: [], edges: [] });
|
|
16
|
+
const symbolIndex = await readJsonArtifact(indexFile, {});
|
|
17
|
+
|
|
18
|
+
const updated = applyEnrichmentResult({
|
|
19
|
+
graph,
|
|
20
|
+
symbolIndex,
|
|
21
|
+
result,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await writeJsonArtifact(graphFile, updated.graph);
|
|
25
|
+
await writeJsonArtifact(indexFile, updated.symbolIndex);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
graphFile,
|
|
29
|
+
indexFile,
|
|
30
|
+
graph: updated.graph,
|
|
31
|
+
symbolIndex: updated.symbolIndex,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/src/platform.mjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join, posix, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const PROJECT_DIR_NAMES = ['.pmc', '.opencode', '.claude', '.cursor'];
|
|
7
|
+
const INSTRUCTION_FILES = new Set([
|
|
8
|
+
'AGENTS.md',
|
|
9
|
+
'CLAUDE.md',
|
|
10
|
+
'GEMINI.md',
|
|
11
|
+
'.cursorrules',
|
|
12
|
+
'.windsurfrules',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function normalizeProjectPath(filePath) {
|
|
16
|
+
if (filePath === '') return '';
|
|
17
|
+
// Converts backslashes to forward slashes and resolves . / .. segments.
|
|
18
|
+
// posix.normalize preserves meaningful spaces.
|
|
19
|
+
return posix.normalize(String(filePath).replace(/\\/g, '/'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isAgentInstructionFile(filePath) {
|
|
23
|
+
const normalizedPath = normalizeProjectPath(filePath);
|
|
24
|
+
return INSTRUCTION_FILES.has(normalizedPath.split('/').at(-1));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function detectAgentType(projectRoot) {
|
|
28
|
+
if (existsSync(join(projectRoot, '.opencode'))) return 'opencode';
|
|
29
|
+
if (existsSync(join(projectRoot, 'CLAUDE.md'))) return 'claude-code';
|
|
30
|
+
if (existsSync(join(projectRoot, '.claude'))) return 'claude-code';
|
|
31
|
+
if (existsSync(join(projectRoot, '.cursorrules'))) return 'cursor';
|
|
32
|
+
if (existsSync(join(projectRoot, '.cursor'))) return 'cursor';
|
|
33
|
+
return 'generic';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveConfigDirs(projectRoot = process.cwd(), options = {}) {
|
|
37
|
+
const root = resolve(projectRoot);
|
|
38
|
+
const exists = options.exists ?? existsSync;
|
|
39
|
+
const homeDir = options.homeDir ?? homedir();
|
|
40
|
+
|
|
41
|
+
const projectConfig = PROJECT_DIR_NAMES
|
|
42
|
+
.map((name) => join(root, name))
|
|
43
|
+
.find((candidate) => exists(candidate)) ?? join(root, '.pmc');
|
|
44
|
+
|
|
45
|
+
const globalConfig = [
|
|
46
|
+
join(homeDir, '.config', 'pmc'),
|
|
47
|
+
join(homeDir, '.config', 'opencode'),
|
|
48
|
+
join(homeDir, '.claude'),
|
|
49
|
+
].find((candidate) => exists(candidate)) ?? join(homeDir, '.config', 'pmc');
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
projectRoot: root,
|
|
53
|
+
projectConfig,
|
|
54
|
+
globalConfig,
|
|
55
|
+
projectConfigDir: projectConfig,
|
|
56
|
+
globalConfigDir: globalConfig,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function spawnBackground(command, args = [], options = {}) {
|
|
61
|
+
const spawnImpl = options.spawnImpl ?? spawn;
|
|
62
|
+
// Always use array-form args (never shell: true with a concatenated string)
|
|
63
|
+
// so paths containing spaces are passed correctly on all platforms.
|
|
64
|
+
const child = spawnImpl(command, args, {
|
|
65
|
+
cwd: options.cwd,
|
|
66
|
+
detached: true,
|
|
67
|
+
stdio: 'ignore',
|
|
68
|
+
shell: false,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
child.unref();
|
|
72
|
+
return child.pid;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolvePythonBin(options = {}) {
|
|
76
|
+
const platform = options.platform ?? process.platform;
|
|
77
|
+
const execFileSyncImpl = options.execFileSyncImpl ?? execFileSync;
|
|
78
|
+
|
|
79
|
+
if (platform === 'win32') {
|
|
80
|
+
return 'python';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
execFileSyncImpl('python3', ['--version'], { stdio: 'ignore' });
|
|
85
|
+
return 'python3';
|
|
86
|
+
} catch {
|
|
87
|
+
return 'python';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveGraphify(options = {}) {
|
|
92
|
+
const env = options.env ?? process.env;
|
|
93
|
+
const platform = options.platform ?? process.platform;
|
|
94
|
+
const execFileSyncImpl = options.execFileSyncImpl ?? execFileSync;
|
|
95
|
+
|
|
96
|
+
if (env.PMC_GRAPHIFY_PATH) {
|
|
97
|
+
return env.PMC_GRAPHIFY_PATH;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (env.PMC_GRAPHIFY_BIN) {
|
|
101
|
+
return env.PMC_GRAPHIFY_BIN;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const finder = platform === 'win32' ? 'where' : 'which';
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
return execFileSyncImpl(finder, ['graphify'], { encoding: 'utf8' }).trim().split(/\r?\n/)[0];
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error('graphify not found. Install it with `pip install graphifyy` or set PMC_GRAPHIFY_PATH or PMC_GRAPHIFY_BIN.');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function buildInjectedPmcConfig({ installState }) {
|
|
2
|
+
return {
|
|
3
|
+
mcp: {
|
|
4
|
+
'pmc-agent-memory': {
|
|
5
|
+
type: 'local',
|
|
6
|
+
command: ['npx', '-y', '@aabadin/agent-memory-mcp'],
|
|
7
|
+
enabled: true,
|
|
8
|
+
environment: {
|
|
9
|
+
MEMORY_DB_PATH: installState.memoryDbPath,
|
|
10
|
+
EMBEDDING_MODEL: 'Xenova/bge-m3',
|
|
11
|
+
EMBEDDING_DIMENSIONS: '1024',
|
|
12
|
+
EMBEDDING_POOLING: 'cls',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ensureProjectMemoryContextDirs,
|
|
6
|
+
readJsonArtifact,
|
|
7
|
+
writeJsonArtifact,
|
|
8
|
+
} from './artifacts.mjs';
|
|
9
|
+
import { buildSemanticUnit } from './semantic-unit.mjs';
|
|
10
|
+
|
|
11
|
+
export async function prepareSemanticJobs({ projectRoot }) {
|
|
12
|
+
const resolvedRoot = resolve(projectRoot);
|
|
13
|
+
const dirs = await ensureProjectMemoryContextDirs(resolvedRoot);
|
|
14
|
+
const worklistFile = join(dirs.enrichment, 'worklist.json');
|
|
15
|
+
const outputFile = join(dirs.enrichment, 'semantic-jobs.json');
|
|
16
|
+
const worklist = await readJsonArtifact(worklistFile, []);
|
|
17
|
+
|
|
18
|
+
const pendingSymbols = worklist.filter((symbol) => symbol.status === 'pending');
|
|
19
|
+
const jobs = [];
|
|
20
|
+
|
|
21
|
+
for (const symbol of pendingSymbols) {
|
|
22
|
+
const absoluteFile = resolve(resolvedRoot, symbol.filePath);
|
|
23
|
+
const content = await readFile(absoluteFile, 'utf8');
|
|
24
|
+
jobs.push(buildSemanticUnit({ symbol, content }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await writeJsonArtifact(outputFile, jobs);
|
|
28
|
+
return {
|
|
29
|
+
file: outputFile,
|
|
30
|
+
count: jobs.length,
|
|
31
|
+
jobs,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const PROJECT_CONTEXT_KINDS = [
|
|
4
|
+
'stack-runtime',
|
|
5
|
+
'dependencies-summary',
|
|
6
|
+
'integrations-summary',
|
|
7
|
+
'architecture-current',
|
|
8
|
+
'architecture-target',
|
|
9
|
+
'structure-summary',
|
|
10
|
+
'technical-rules',
|
|
11
|
+
'project-requirements',
|
|
12
|
+
'known-issues-and-fixes',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function buildProjectContextMemoryKey(kind) {
|
|
16
|
+
if (!PROJECT_CONTEXT_KINDS.includes(kind)) {
|
|
17
|
+
throw new Error(`Unknown project context kind: ${kind}`);
|
|
18
|
+
}
|
|
19
|
+
return `project-context:${kind}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hashProjectContextContent(value) {
|
|
23
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createMaterializedProjectContext({
|
|
27
|
+
kind,
|
|
28
|
+
title,
|
|
29
|
+
summary,
|
|
30
|
+
body,
|
|
31
|
+
tags,
|
|
32
|
+
sourceFiles,
|
|
33
|
+
graphRefs,
|
|
34
|
+
sourceMode,
|
|
35
|
+
confidence,
|
|
36
|
+
detectedSources = [],
|
|
37
|
+
declaredSources = [],
|
|
38
|
+
updatedAt,
|
|
39
|
+
}) {
|
|
40
|
+
const contentShape = { title, summary, body, tags, sourceFiles, graphRefs, sourceMode, confidence, detectedSources, declaredSources };
|
|
41
|
+
return {
|
|
42
|
+
memory_key: buildProjectContextMemoryKey(kind),
|
|
43
|
+
title,
|
|
44
|
+
kind,
|
|
45
|
+
source_mode: sourceMode,
|
|
46
|
+
summary,
|
|
47
|
+
body,
|
|
48
|
+
tags,
|
|
49
|
+
source_files: sourceFiles,
|
|
50
|
+
graph_refs: graphRefs,
|
|
51
|
+
detected_sources: detectedSources,
|
|
52
|
+
declared_sources: declaredSources,
|
|
53
|
+
confidence,
|
|
54
|
+
content_hash: hashProjectContextContent(contentShape),
|
|
55
|
+
updated_at: updatedAt,
|
|
56
|
+
};
|
|
57
|
+
}
|