@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.
Files changed (88) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +123 -0
  3. package/bin/pmc.mjs +11 -0
  4. package/cli/apply-enrichment-result.mjs +31 -0
  5. package/cli/batch-enrich.mjs +5 -0
  6. package/cli/bootstrap.mjs +357 -0
  7. package/cli/build-worklist.mjs +35 -0
  8. package/cli/context.mjs +49 -0
  9. package/cli/doctor.mjs +29 -0
  10. package/cli/enrich-batch.mjs +11 -0
  11. package/cli/enrich-loop.sh +117 -0
  12. package/cli/enrich-orchestrator.mjs +5 -0
  13. package/cli/enrich-queue.mjs +525 -0
  14. package/cli/enrich-sync.mjs +5 -0
  15. package/cli/enrich.mjs +51 -0
  16. package/cli/fail-enrichment.mjs +28 -0
  17. package/cli/finalize-enrichment.mjs +25 -0
  18. package/cli/init.mjs +66 -0
  19. package/cli/install-pmc.mjs +153 -0
  20. package/cli/materialize-enrichment-artifacts.mjs +38 -0
  21. package/cli/new-project.mjs +41 -0
  22. package/cli/prepare-semantic-jobs.mjs +8 -0
  23. package/cli/project-context.mjs +224 -0
  24. package/cli/sanitize.mjs +235 -0
  25. package/cli/save-intake-context.mjs +22 -0
  26. package/cli/setup.mjs +80 -0
  27. package/cli/status.mjs +81 -0
  28. package/mcp/local-model-server.mjs +74 -0
  29. package/package.json +60 -0
  30. package/plugin/index.mjs +27 -0
  31. package/src/artifacts.mjs +39 -0
  32. package/src/change-detector.mjs +10 -0
  33. package/src/command-dispatch.mjs +84 -0
  34. package/src/declared-intake.mjs +25 -0
  35. package/src/doctor.mjs +114 -0
  36. package/src/enrichment-artifacts.mjs +67 -0
  37. package/src/enrichment-attempts.mjs +17 -0
  38. package/src/enrichment-config.mjs +121 -0
  39. package/src/enrichment-driver.mjs +167 -0
  40. package/src/enrichment-errors.mjs +46 -0
  41. package/src/enrichment-linker.mjs +29 -0
  42. package/src/extractors/architecture-extractor.mjs +8 -0
  43. package/src/extractors/js-ts-extractor.mjs +118 -0
  44. package/src/extractors/regex-extractor.mjs +439 -0
  45. package/src/extractors/rules-extractor.mjs +9 -0
  46. package/src/extractors/stack-extractor.mjs +48 -0
  47. package/src/extractors/structure-extractor.mjs +31 -0
  48. package/src/fail-enrichment.mjs +33 -0
  49. package/src/finalize-enrichment.mjs +30 -0
  50. package/src/graph-backfill.mjs +35 -0
  51. package/src/graph-node-resolver.mjs +64 -0
  52. package/src/index.mjs +2 -0
  53. package/src/intake-context.mjs +16 -0
  54. package/src/invalidation-matrix.mjs +33 -0
  55. package/src/markdown-renderer.mjs +27 -0
  56. package/src/materializer.mjs +128 -0
  57. package/src/memory-payload.mjs +55 -0
  58. package/src/persist-enrichment-result.mjs +33 -0
  59. package/src/platform.mjs +111 -0
  60. package/src/plugin-config.mjs +17 -0
  61. package/src/prepare-semantic-jobs.mjs +33 -0
  62. package/src/project-context-schema.mjs +57 -0
  63. package/src/providers/cloud-api-provider.mjs +88 -0
  64. package/src/providers/local-model-provider.mjs +67 -0
  65. package/src/refresh-state.mjs +21 -0
  66. package/src/result-input.mjs +9 -0
  67. package/src/retrieval/context-renderer.mjs +97 -0
  68. package/src/retrieval/query-engine.mjs +230 -0
  69. package/src/semantic-report.mjs +26 -0
  70. package/src/semantic-unit.mjs +74 -0
  71. package/src/setup-bootstrap.mjs +131 -0
  72. package/src/symbol-extractor.mjs +29 -0
  73. package/src/symbol-index.mjs +30 -0
  74. package/src/symbol-keys.mjs +28 -0
  75. package/src/sync-manifest.mjs +119 -0
  76. package/src/template-installer.mjs +181 -0
  77. package/src/worklist-state.mjs +12 -0
  78. package/templates/claude-code/CLAUDE.md.snippet +36 -0
  79. package/templates/cursor/.cursorrules.snippet +36 -0
  80. package/templates/generic/README-SETUP.md +53 -0
  81. package/templates/opencode/agent/enrich.md +28 -0
  82. package/templates/opencode/autostart-snippet.md +13 -0
  83. package/templates/opencode/commands/get-context.md +22 -0
  84. package/templates/opencode/commands/new-project.md +32 -0
  85. package/templates/opencode/commands/sanitize.md +21 -0
  86. package/templates/opencode/commands/sync-context.md +22 -0
  87. package/templates/project-memory-context workflow.md +129 -0
  88. package/templates/project-memory-context.md +42 -0
@@ -0,0 +1,84 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
6
+
7
+ const COMMANDS = new Map([
8
+ ['bootstrap', 'cli/bootstrap.mjs'],
9
+ ['context', 'cli/context.mjs'],
10
+ ['doctor', 'cli/doctor.mjs'],
11
+ ['enrich', 'cli/enrich.mjs'],
12
+ ['help', null],
13
+ ['init', 'cli/init.mjs'],
14
+ ['install-pmc', 'cli/install-pmc.mjs'],
15
+ ['new-project', 'cli/new-project.mjs'],
16
+ ['project-context', 'cli/project-context.mjs'],
17
+ ['sanitize', 'cli/sanitize.mjs'],
18
+ ['setup', 'cli/setup.mjs'],
19
+ ['status', 'cli/status.mjs'],
20
+ ]);
21
+
22
+ function usageText() {
23
+ return `Usage: pmc <${[...COMMANDS.keys()].join('|')}>`;
24
+ }
25
+
26
+ function writeLine(stream, message) {
27
+ stream.write(`${message}\n`);
28
+ }
29
+
30
+ export function resolveCommand(argv = []) {
31
+ const name = argv[0] ?? 'help';
32
+ const relativeModule = COMMANDS.get(name);
33
+
34
+ if (relativeModule === undefined) {
35
+ return { name, modulePath: null, args: argv.slice(1), valid: false };
36
+ }
37
+
38
+ return {
39
+ name,
40
+ modulePath: relativeModule ? resolve(PACKAGE_ROOT, relativeModule) : null,
41
+ args: argv.slice(1),
42
+ valid: true,
43
+ };
44
+ }
45
+
46
+ export async function runCommand(argv = [], options = {}) {
47
+ const { stdio = 'inherit', stdout = process.stdout, stderr = process.stderr } = options;
48
+ const command = resolveCommand(argv);
49
+
50
+ if (!command.valid) {
51
+ writeLine(stderr, `Invalid command: ${command.name}`);
52
+ writeLine(stdout, usageText());
53
+ return 1;
54
+ }
55
+
56
+ if (!command.modulePath) {
57
+ writeLine(stdout, usageText());
58
+ return 0;
59
+ }
60
+
61
+ return await new Promise((resolvePromise, rejectPromise) => {
62
+ const child = spawn(process.execPath, [command.modulePath, ...command.args], { stdio });
63
+
64
+ if (stdio === 'pipe') {
65
+ child.stdout?.on('data', (chunk) => {
66
+ stdout.write(chunk);
67
+ });
68
+
69
+ child.stderr?.on('data', (chunk) => {
70
+ stderr.write(chunk);
71
+ });
72
+ }
73
+
74
+ child.once('error', rejectPromise);
75
+ child.once('exit', (code, signal) => {
76
+ if (signal) {
77
+ rejectPromise(new Error(`Command exited from signal ${signal}`));
78
+ return;
79
+ }
80
+
81
+ resolvePromise(code ?? 0);
82
+ });
83
+ });
84
+ }
@@ -0,0 +1,25 @@
1
+ export function createDeclaredProjectContextTemplates({
2
+ architectureTarget = '',
3
+ technicalRules = [],
4
+ projectRequirements = [],
5
+ knownIssuesAndFixes = [],
6
+ } = {}) {
7
+ return {
8
+ 'architecture-target.json': {
9
+ title: 'Target project architecture',
10
+ architecture: architectureTarget,
11
+ },
12
+ 'technical-rules.json': {
13
+ title: 'Technical rules',
14
+ rules: technicalRules,
15
+ },
16
+ 'project-requirements.json': {
17
+ title: 'Project requirements',
18
+ requirements: projectRequirements,
19
+ },
20
+ 'known-issues-and-fixes.json': {
21
+ title: 'Known issues and fixes',
22
+ items: knownIssuesAndFixes,
23
+ },
24
+ };
25
+ }
package/src/doctor.mjs ADDED
@@ -0,0 +1,114 @@
1
+ import { access, constants } from 'node:fs/promises';
2
+
3
+ const DEFAULT_OLLAMA_URL = 'http://localhost:11434';
4
+
5
+ export async function runDoctor({
6
+ env = process.env,
7
+ fetchImpl = globalThis.fetch,
8
+ resolvePythonBin = () => null,
9
+ resolveGraphify = () => null,
10
+ spawnCheck = null,
11
+ } = {}) {
12
+ const checks = await Promise.all([
13
+ checkNodeVersion(),
14
+ checkPython(resolvePythonBin, spawnCheck),
15
+ checkGraphify(resolvePythonBin, spawnCheck),
16
+ checkOllama(env, fetchImpl),
17
+ checkMemoryDbPath(env),
18
+ checkEmbeddingCachePath(env),
19
+ ]);
20
+ return { checks };
21
+ }
22
+
23
+ async function checkNodeVersion() {
24
+ const major = parseInt(process.version.slice(1).split('.')[0], 10);
25
+ return {
26
+ name: 'node-version',
27
+ status: major >= 18 ? 'ok' : 'fail',
28
+ message: major >= 18
29
+ ? `Node.js ${process.version} ✓`
30
+ : `Node.js ${process.version} requires ≥ 18 — upgrade Node.js`,
31
+ };
32
+ }
33
+
34
+ async function checkPython(resolvePythonBin, spawnCheck) {
35
+ const bin = resolvePythonBin?.() ?? null;
36
+ if (!bin) {
37
+ return { name: 'python', status: 'fail', message: 'Python 3 not found in PATH — install python3 or set PATH' };
38
+ }
39
+ if (!spawnCheck) {
40
+ return { name: 'python', status: 'ok', message: `${bin} found (not verified — no spawn check provided)` };
41
+ }
42
+ const result = await spawnCheck(bin, ['--version']);
43
+ return {
44
+ name: 'python',
45
+ status: result.exitCode === 0 ? 'ok' : 'fail',
46
+ message: result.exitCode === 0
47
+ ? `${bin} ${result.stdout?.trim() ?? ''} ✓`
48
+ : `${bin} failed to run — check Python installation`,
49
+ };
50
+ }
51
+
52
+ async function checkGraphify(resolvePythonBin, spawnCheck) {
53
+ const bin = resolvePythonBin?.() ?? 'python3';
54
+ if (!spawnCheck) {
55
+ return { name: 'graphifyy', status: 'warn', message: 'graphifyy check skipped (no spawn check provided)' };
56
+ }
57
+ const result = await spawnCheck(bin, ['-c', 'import graphifyy; print("ok")']);
58
+ return {
59
+ name: 'graphifyy',
60
+ status: result.exitCode === 0 ? 'ok' : 'fail',
61
+ message: result.exitCode === 0
62
+ ? 'graphifyy importable ✓'
63
+ : 'graphifyy not installed — run: pip install graphifyy',
64
+ };
65
+ }
66
+
67
+ async function checkOllama(env, fetchImpl) {
68
+ const baseUrl = env.PMC_LOCAL_MODEL_BASE_URL ?? DEFAULT_OLLAMA_URL;
69
+ try {
70
+ const res = await Promise.race([
71
+ fetchImpl(`${baseUrl}/api/tags`),
72
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
73
+ ]);
74
+ return {
75
+ name: 'ollama',
76
+ status: res.ok ? 'ok' : 'warn',
77
+ message: res.ok
78
+ ? `Ollama reachable at ${baseUrl} ✓`
79
+ : `Ollama responded ${res.status} at ${baseUrl}`,
80
+ };
81
+ } catch {
82
+ return {
83
+ name: 'ollama',
84
+ status: 'warn',
85
+ message: `Ollama not reachable at ${baseUrl} — enrichment will use cloud-api fallback`,
86
+ };
87
+ }
88
+ }
89
+
90
+ async function checkMemoryDbPath(env) {
91
+ const p = env.MEMORY_DB_PATH;
92
+ if (!p) {
93
+ return { name: 'memory-db-path', status: 'fail', message: 'MEMORY_DB_PATH not set — required for agent-memory-mcp' };
94
+ }
95
+ try {
96
+ await access(p, constants.W_OK);
97
+ return { name: 'memory-db-path', status: 'ok', message: `${p} writable ✓` };
98
+ } catch {
99
+ return { name: 'memory-db-path', status: 'warn', message: `${p} does not exist yet (will be created on first run)` };
100
+ }
101
+ }
102
+
103
+ async function checkEmbeddingCachePath(env) {
104
+ const p = env.EMBEDDING_CACHE_PATH;
105
+ if (!p) {
106
+ return { name: 'embedding-cache', status: 'ok', message: 'EMBEDDING_CACHE_PATH not set (cache disabled — optional)' };
107
+ }
108
+ try {
109
+ await access(p, constants.W_OK);
110
+ return { name: 'embedding-cache', status: 'ok', message: `${p} writable ✓` };
111
+ } catch {
112
+ return { name: 'embedding-cache', status: 'warn', message: `${p} does not exist yet (will be created on first run)` };
113
+ }
114
+ }
@@ -0,0 +1,67 @@
1
+ import { join, resolve } from 'node:path';
2
+
3
+ import {
4
+ ensureProjectMemoryContextDirs,
5
+ writeJsonArtifact,
6
+ } from './artifacts.mjs';
7
+ import { buildEnrichmentResult, buildMemoryPayload } from './memory-payload.mjs';
8
+ import { normalizeSemanticReport } from './semantic-report.mjs';
9
+
10
+ function safeSymbolKey(symbolKey) {
11
+ return symbolKey.replace(/[^a-zA-Z0-9_-]+/g, '_');
12
+ }
13
+
14
+ export function buildEnrichmentArtifacts({ projectSlug, job, report, memoryId, enrichedAt }) {
15
+ const semantic = normalizeSemanticReport(report);
16
+ const memoryPayload = buildMemoryPayload({ projectSlug, job, semantic });
17
+ const enrichmentResult = memoryId
18
+ ? buildEnrichmentResult({
19
+ job,
20
+ memoryId,
21
+ semanticSummary: semantic.summary || semantic.responsibility,
22
+ status: 'enriched',
23
+ enrichedAt,
24
+ })
25
+ : null;
26
+
27
+ return {
28
+ semantic,
29
+ memoryPayload,
30
+ enrichmentResult,
31
+ };
32
+ }
33
+
34
+ export async function persistEnrichmentArtifacts({
35
+ projectRoot,
36
+ projectSlug,
37
+ job,
38
+ report,
39
+ memoryId,
40
+ enrichedAt,
41
+ }) {
42
+ const resolvedRoot = resolve(projectRoot);
43
+ const dirs = await ensureProjectMemoryContextDirs(resolvedRoot);
44
+ const artifacts = buildEnrichmentArtifacts({
45
+ projectSlug,
46
+ job,
47
+ report,
48
+ memoryId,
49
+ enrichedAt,
50
+ });
51
+ const fileStem = safeSymbolKey(job.symbolKey);
52
+ const memoryPayloadFile = join(dirs.enrichment, `${fileStem}.memory.json`);
53
+ const enrichmentResultFile = memoryId
54
+ ? join(dirs.enrichment, `${fileStem}.result.json`)
55
+ : null;
56
+
57
+ await writeJsonArtifact(memoryPayloadFile, artifacts.memoryPayload);
58
+ if (artifacts.enrichmentResult && enrichmentResultFile) {
59
+ await writeJsonArtifact(enrichmentResultFile, artifacts.enrichmentResult);
60
+ }
61
+
62
+ return {
63
+ ...artifacts,
64
+ memoryPayloadFile,
65
+ enrichmentResultFile,
66
+ };
67
+ }
@@ -0,0 +1,17 @@
1
+ import { appendFile, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ export async function appendProviderEvent(enrichmentDir, event) {
5
+ await mkdir(enrichmentDir, { recursive: true });
6
+ await appendFile(join(enrichmentDir, 'provider-events.jsonl'), `${JSON.stringify(event)}\n`, 'utf8');
7
+ }
8
+
9
+ export function withRecordedAttempt(entry, attempt) {
10
+ const attempts = [...(entry.attempts ?? []), attempt];
11
+
12
+ return {
13
+ ...entry,
14
+ attempts,
15
+ lastModeUsed: attempt.mode ?? entry.lastModeUsed ?? null,
16
+ };
17
+ }
@@ -0,0 +1,121 @@
1
+ import { join } from 'node:path';
2
+
3
+ import { resolveConfigDirs } from './platform.mjs';
4
+
5
+ const DEFAULTS = {
6
+ preferredModes: ['local-model', 'cloud-api', 'agent-subagent'],
7
+ localModel: {
8
+ provider: 'ollama',
9
+ baseUrl: 'http://localhost:11434',
10
+ model: 'deepseek-coder-v2:16b-ctx32k',
11
+ },
12
+ cloudApi: {
13
+ provider: 'openai-compatible',
14
+ baseUrl: '',
15
+ model: '',
16
+ apiKeyEnv: 'PMC_CLOUD_API_KEY',
17
+ },
18
+ agentSubagent: {
19
+ enabled: true,
20
+ agentName: 'enrich',
21
+ },
22
+ };
23
+
24
+ export const PMC_ENRICHMENT_CONFIG_FILE = 'project-memory-context.json';
25
+
26
+ const VALID_MODES = new Set(DEFAULTS.preferredModes);
27
+
28
+ function normalizePreferredModes(modes) {
29
+ const filtered = [...new Set((modes ?? []).filter((mode) => VALID_MODES.has(mode)))];
30
+ return filtered.length > 0 ? filtered : [...DEFAULTS.preferredModes];
31
+ }
32
+
33
+ function mergeConfig(base, override) {
34
+ return {
35
+ ...base,
36
+ ...override,
37
+ localModel: {
38
+ ...base.localModel,
39
+ ...override?.localModel,
40
+ },
41
+ cloudApi: {
42
+ ...base.cloudApi,
43
+ ...override?.cloudApi,
44
+ },
45
+ agentSubagent: {
46
+ ...base.agentSubagent,
47
+ ...override?.agentSubagent,
48
+ },
49
+ };
50
+ }
51
+
52
+ function unwrapEnrichmentConfig(config) {
53
+ if (!config) {
54
+ return {};
55
+ }
56
+
57
+ return config.enrichment ?? config;
58
+ }
59
+
60
+ export function readEnvPreferredModes(env) {
61
+ return env.PMC_ENRICHMENT_PREFERRED_MODES
62
+ ? env.PMC_ENRICHMENT_PREFERRED_MODES
63
+ .split(',')
64
+ .map((mode) => mode.trim())
65
+ .filter(Boolean)
66
+ : null;
67
+ }
68
+
69
+ export function resolveEnrichmentConfigPaths({
70
+ projectRoot = process.cwd(),
71
+ env = process.env,
72
+ platformOptions,
73
+ } = {}) {
74
+ const { projectConfigDir, globalConfigDir } = resolveConfigDirs(projectRoot, platformOptions);
75
+
76
+ return {
77
+ fileName: PMC_ENRICHMENT_CONFIG_FILE,
78
+ projectConfigPath: env.PMC_PROJECT_CONFIG ?? join(projectConfigDir, PMC_ENRICHMENT_CONFIG_FILE),
79
+ globalConfigPath: env.PMC_GLOBAL_CONFIG ?? join(globalConfigDir, PMC_ENRICHMENT_CONFIG_FILE),
80
+ };
81
+ }
82
+
83
+ export function resolveEnrichmentConfig({ projectConfig, globalConfig, env }) {
84
+ let resolved = mergeConfig(DEFAULTS, unwrapEnrichmentConfig(globalConfig));
85
+ resolved = mergeConfig(resolved, unwrapEnrichmentConfig(projectConfig));
86
+
87
+ const preferredModes = readEnvPreferredModes(env);
88
+ if (preferredModes) {
89
+ resolved.preferredModes = normalizePreferredModes(preferredModes);
90
+ }
91
+
92
+ if (env.PMC_LOCAL_MODEL_BASE_URL) {
93
+ resolved.localModel.baseUrl = env.PMC_LOCAL_MODEL_BASE_URL;
94
+ }
95
+
96
+ if (env.PMC_LOCAL_MODEL_NAME) {
97
+ resolved.localModel.model = env.PMC_LOCAL_MODEL_NAME;
98
+ }
99
+
100
+ if (env.PMC_CLOUD_API_BASE_URL) {
101
+ resolved.cloudApi.baseUrl = env.PMC_CLOUD_API_BASE_URL;
102
+ }
103
+
104
+ if (env.PMC_CLOUD_API_MODEL) {
105
+ resolved.cloudApi.model = env.PMC_CLOUD_API_MODEL;
106
+ }
107
+
108
+ if (env.PMC_CLOUD_API_KEY_ENV) {
109
+ resolved.cloudApi.apiKeyEnv = env.PMC_CLOUD_API_KEY_ENV;
110
+ }
111
+
112
+ if (env.PMC_AGENT_SUBAGENT_NAME) {
113
+ resolved.agentSubagent.agentName = env.PMC_AGENT_SUBAGENT_NAME;
114
+ }
115
+
116
+ resolved.preferredModes = normalizePreferredModes(resolved.preferredModes);
117
+
118
+ return resolved;
119
+ }
120
+
121
+ export { DEFAULTS };
@@ -0,0 +1,167 @@
1
+ import { classifyEnrichmentError } from './enrichment-errors.mjs';
2
+
3
+ function isFallbackEligible(type) {
4
+ return type === 'auth'
5
+ || type === 'timeout'
6
+ || type === 'network'
7
+ || type === 'rate-limit'
8
+ || type === 'provider';
9
+ }
10
+
11
+ function providerConfigForMode(config, mode) {
12
+ if (mode === 'local-model') {
13
+ return config?.localModel ?? {};
14
+ }
15
+
16
+ if (mode === 'cloud-api') {
17
+ return config?.cloudApi ?? {};
18
+ }
19
+
20
+ if (mode === 'agent-subagent') {
21
+ return config?.agentSubagent ?? {};
22
+ }
23
+
24
+ return {};
25
+ }
26
+
27
+ function providerNameForMode(config, mode, provider) {
28
+ return providerConfigForMode(config, mode).provider ?? provider?.kind ?? mode;
29
+ }
30
+
31
+ export async function runEnrichmentWithFallback({ request, config, providers, env = process.env }) {
32
+ const attempts = [];
33
+
34
+ for (const mode of config.preferredModes ?? []) {
35
+ const provider = providers.find((candidate) => candidate.kind === mode);
36
+ if (!provider) {
37
+ continue;
38
+ }
39
+
40
+ const providerName = providerNameForMode(config, mode, provider);
41
+ const startedAt = new Date().toISOString();
42
+ const configured = provider.isConfigured({ request, config, env });
43
+
44
+ if (!configured.ok) {
45
+ attempts.push({
46
+ mode,
47
+ provider: providerName,
48
+ status: 'skipped',
49
+ errorType: 'config',
50
+ errorMessage: configured.reason,
51
+ startedAt,
52
+ endedAt: new Date().toISOString(),
53
+ });
54
+ continue;
55
+ }
56
+
57
+ let available;
58
+ try {
59
+ available = await provider.isAvailable({ request, config, env });
60
+ } catch (error) {
61
+ const classified = classifyEnrichmentError(error);
62
+ attempts.push({
63
+ mode,
64
+ provider: providerName,
65
+ status: 'failed',
66
+ errorType: classified.type,
67
+ errorMessage: classified.message,
68
+ startedAt,
69
+ endedAt: new Date().toISOString(),
70
+ });
71
+
72
+ if (!isFallbackEligible(classified.type)) {
73
+ return {
74
+ status: 'error',
75
+ content: null,
76
+ mode: null,
77
+ provider: null,
78
+ model: null,
79
+ attempts,
80
+ };
81
+ }
82
+ continue;
83
+ }
84
+
85
+ if (!available.ok) {
86
+ const classified = available.errorType
87
+ ? { type: available.errorType, message: available.reason }
88
+ : classifyEnrichmentError(new Error(available.reason ?? 'provider unavailable'));
89
+ const attempt = {
90
+ mode,
91
+ provider: providerName,
92
+ status: 'skipped',
93
+ errorType: classified.type,
94
+ errorMessage: classified.message,
95
+ startedAt,
96
+ endedAt: new Date().toISOString(),
97
+ };
98
+ attempts.push(attempt);
99
+
100
+ if (!isFallbackEligible(classified.type)) {
101
+ return {
102
+ status: 'error',
103
+ content: null,
104
+ mode: null,
105
+ provider: null,
106
+ model: null,
107
+ attempts,
108
+ };
109
+ }
110
+ continue;
111
+ }
112
+
113
+ try {
114
+ const result = await provider.enrich(request, { request, config, env });
115
+ const attempt = {
116
+ mode,
117
+ provider: result.provider ?? providerName,
118
+ model: result.model ?? null,
119
+ status: 'succeeded',
120
+ startedAt,
121
+ endedAt: new Date().toISOString(),
122
+ };
123
+ attempts.push(attempt);
124
+
125
+ return {
126
+ status: 'succeeded',
127
+ content: result.content,
128
+ mode,
129
+ provider: result.provider ?? providerName,
130
+ model: result.model ?? null,
131
+ attempts,
132
+ };
133
+ } catch (error) {
134
+ const classified = classifyEnrichmentError(error);
135
+ const attempt = {
136
+ mode,
137
+ provider: providerName,
138
+ status: 'failed',
139
+ errorType: classified.type,
140
+ errorMessage: classified.message,
141
+ startedAt,
142
+ endedAt: new Date().toISOString(),
143
+ };
144
+ attempts.push(attempt);
145
+
146
+ if (!isFallbackEligible(classified.type)) {
147
+ return {
148
+ status: 'error',
149
+ content: null,
150
+ mode: null,
151
+ provider: null,
152
+ model: null,
153
+ attempts,
154
+ };
155
+ }
156
+ }
157
+ }
158
+
159
+ return {
160
+ status: 'error',
161
+ content: null,
162
+ mode: null,
163
+ provider: null,
164
+ model: null,
165
+ attempts,
166
+ };
167
+ }
@@ -0,0 +1,46 @@
1
+ function messageOf(error) {
2
+ if (error instanceof Error) {
3
+ return error.message;
4
+ }
5
+
6
+ return String(error ?? 'Unknown enrichment error');
7
+ }
8
+
9
+ export function classifyEnrichmentError(error) {
10
+ const message = messageOf(error);
11
+ const normalized = message.toLowerCase();
12
+
13
+ let type = 'runtime';
14
+
15
+ if (normalized.includes('401') || normalized.includes('unauthorized') || normalized.includes('forbidden')) {
16
+ type = 'auth';
17
+ } else if (normalized.includes('429') || normalized.includes('rate limit')) {
18
+ type = 'rate-limit';
19
+ } else if (
20
+ normalized.includes('500')
21
+ || normalized.includes('502')
22
+ || normalized.includes('503')
23
+ || normalized.includes('504')
24
+ || normalized.includes('404')
25
+ || normalized.includes('provider unavailable')
26
+ || normalized.includes('model missing')
27
+ ) {
28
+ type = 'provider';
29
+ } else if (
30
+ normalized.includes('econnrefused')
31
+ || normalized.includes('enotfound')
32
+ || normalized.includes('econnreset')
33
+ || normalized.includes('socket')
34
+ || normalized.includes('network')
35
+ ) {
36
+ type = 'network';
37
+ } else if (normalized.includes('timeout')) {
38
+ type = 'timeout';
39
+ } else if (normalized.includes('config')) {
40
+ type = 'config';
41
+ } else if (normalized.includes('provider')) {
42
+ type = 'provider';
43
+ }
44
+
45
+ return { type, message };
46
+ }
@@ -0,0 +1,29 @@
1
+ import { backfillGraphNode } from './graph-backfill.mjs';
2
+ import { upsertSymbolIndexEntry } from './symbol-index.mjs';
3
+
4
+ export function applyEnrichmentResult({ graph, symbolIndex, result }) {
5
+ const updatedGraph = backfillGraphNode({
6
+ graph,
7
+ symbolKey: result.symbolKey,
8
+ graphNodeId: result.graphNodeId,
9
+ memoryId: result.memoryId,
10
+ semanticSummary: result.semanticSummary,
11
+ codeHash: result.codeHash,
12
+ enrichedAt: result.enrichedAt,
13
+ status: result.status,
14
+ });
15
+
16
+ const updatedIndex = upsertSymbolIndexEntry(symbolIndex, {
17
+ symbolKey: result.symbolKey,
18
+ graphNodeId: result.graphNodeId,
19
+ memoryId: result.memoryId,
20
+ codeHash: result.codeHash,
21
+ status: result.status,
22
+ lastEnrichedAt: result.enrichedAt,
23
+ });
24
+
25
+ return {
26
+ graph: updatedGraph,
27
+ symbolIndex: updatedIndex,
28
+ };
29
+ }
@@ -0,0 +1,8 @@
1
+ export async function detectArchitectureContext({ graph }) {
2
+ const labels = (graph?.nodes ?? []).map((node) => String(node.label ?? node.id ?? ''));
3
+ return {
4
+ pattern: 'detected-structure',
5
+ entryPoints: labels.filter((label) => /(?:^|\/)main\.[cm]?[jt]sx?$|(?:^|\/)app\.[cm]?[jt]sx?$/.test(label)),
6
+ graphRefs: labels.map((label) => `node:${label}`),
7
+ };
8
+ }