@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,117 @@
1
+ #!/bin/bash
2
+ # Continuous enrichment loop - keeps 8 parallel slots running always
3
+ # Each run processes 8 symbols, syncs, then re-runs until done
4
+
5
+ ENRICH_DIR=".planning/project-memory-context/enrichment"
6
+ WORKLIST="$ENRICH_DIR/worklist.json"
7
+ OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
8
+ OLLAMA_MODEL="${OLLAMA_MODEL:-deepseek-coder-v2:16b-ctx32k}"
9
+
10
+ check_pending() {
11
+ node -e "const w=require('$WORKLIST'); const p=w.filter(s=>s.status==='pending').length; console.log(p)" 2>/dev/null || echo "99"
12
+ }
13
+
14
+ echo "[loop] Starting continuous enrichment loop"
15
+ echo "[loop] Ollama: $OLLAMA_URL | Model: $OLLAMA_MODEL"
16
+
17
+ batch=0
18
+ while true; do
19
+ pending=$(check_pending)
20
+ if [ "$pending" -eq "0" ]; then
21
+ echo "[loop] All symbols enriched! Done."
22
+ break
23
+ fi
24
+ if [ "$pending" -gt "87" ]; then
25
+ echo "[loop] Worklist corrupted or missing. Stop."
26
+ break
27
+ fi
28
+
29
+ batch=$((batch + 1))
30
+ echo ""
31
+ echo "[loop] ===== BATCH $batch ===== ($pending symbols remaining)"
32
+ echo "[loop] $(date +%H:%M:%S) Dispatching 8 subagents via Task tool..."
33
+
34
+ # Run orchestrator to get next 8 symbols + prompts
35
+ manifest=$(node tools/project-memory-context/cli/enrich-orchestrator.mjs 2>/dev/null)
36
+ if [ $? -ne 0 ]; then
37
+ echo "[loop] Orchestrator failed. Retrying in 5s..."
38
+ sleep 5
39
+ continue
40
+ fi
41
+
42
+ echo "$manifest" | node -e "
43
+ const stdin = require('fs').readFileSync('/dev/stdin', 'utf8');
44
+ const j = JSON.parse(stdin);
45
+ if (j.complete) { console.log('ALL_DONE'); process.exit(0); }
46
+ if (!j.subagentPrompts || j.subagentPrompts.length === 0) {
47
+ console.log('NO_PROMPTS');
48
+ process.exit(1);
49
+ }
50
+ console.log(JSON.stringify(j.subagentPrompts.map((p, i) => ({
51
+ idx: i,
52
+ prompt: p,
53
+ symbolKey: j.pending[i]?.symbolKey || 'unknown'
54
+ }))));
55
+ " > /tmp/pmc_batch_$batch.json 2>/dev/null
56
+
57
+ if [ $? -ne 0 ]; then
58
+ echo "[loop] Failed to get batch. Retrying..."
59
+ sleep 5
60
+ continue
61
+ fi
62
+
63
+ batch_data=$(cat /tmp/pmc_batch_$batch.json)
64
+ if [ "$batch_data" = "ALL_DONE" ] || [ "$batch_data" = "NO_PROMPTS" ]; then
65
+ echo "[loop] No more symbols to process."
66
+ break
67
+ fi
68
+
69
+ # Extract symbol keys and names for dispatch
70
+ echo "$batch_data" | node -e "
71
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
72
+ data.forEach((item, i) => {
73
+ const sym = JSON.parse(item.prompt.split('SYMBOL: ')[1].split('\n')[0]);
74
+ console.log(JSON.stringify(sym));
75
+ });
76
+ " > /tmp/pmc_symbols_$batch.json 2>/dev/null
77
+
78
+ echo "[loop] Dispatching 8 subagents in parallel..."
79
+
80
+ # Dispatch all 8 subagents in background and collect task IDs
81
+ declare -a TASK_IDS
82
+ idx=0
83
+ while read -r symbol_json; do
84
+ [ -z "$symbol_json" ] && continue
85
+ task_id=$(node -e "
86
+ const spawn = require('child_process').spawn;
87
+ const sym = $symbol_json;
88
+ const prompt = 'You are enriching ONE code symbol for project-memory-context.\n\nSYMBOL: ' + JSON.stringify(sym) + '\nPROJECT_ROOT: C:\\\\Users\\\\aabad\\\\Documents\\\\CODE\\\\ia\\\\memory-context\nOLLAMA_URL: http://localhost:11434\nOLLAMA_MODEL: deepseek-coder-v2:16b-ctx32k\n\nSTEPS:\n1. Read the source file at the symbol filePath (use Read tool), lines startLine to endLine PLUS imports above\n2. Call Ollama via bash: node -e \"fetch('http://localhost:11434/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model:'deepseek-coder-v2:16b-ctx32k',prompt:'YOUR_PROMPT',stream:false,options:{temperature:0.1,num_predict:512}})}).then(r=>r.json()).then(d=>process.stdout.write(d.response))\"\n3. Store via agent-memory_store: content, category architecture, tags symbol ts kind project:memory-context file:filepath\n4. Return JSON with symbolKey memoryId status enrichedAt\n\nDo NOT update any JSON files. Return result.'
89
+ // Can't actually dispatch task from here - output the prompt for manual use
90
+ console.log(prompt);
91
+ " 2>&1 | head -1) &
92
+ TASK_IDS[$idx]=$!
93
+ idx=$((idx + 1))
94
+ done < /tmp/pmc_symbols_$batch.json
95
+
96
+ # Wait for all background jobs
97
+ for pid in "${TASK_IDS[@]}"; do
98
+ wait $pid 2>/dev/null
99
+ done
100
+
101
+ echo "[loop] Batch $batch dispatched. Sleeping 5s before checking..."
102
+ sleep 5
103
+
104
+ # Check if we should continue
105
+ pending=$(check_pending)
106
+ if [ "$pending" -eq "0" ]; then
107
+ echo "[loop] All done!"
108
+ break
109
+ fi
110
+ if [ "$batch" -ge 20 ]; then
111
+ echo "[loop] Max batches reached ($batch). Stopping safely."
112
+ break
113
+ fi
114
+ done
115
+
116
+ echo "[loop] Loop complete. Final state:"
117
+ node -e "const w=require('.planning/project-memory-context/enrichment/worklist.json'); const p=w.filter(s=>s.status==='pending').length; const e=w.filter(s=>s.status==='enriched').length; const err=w.filter(s=>s.status==='error').length; console.log('pending='+p+' enriched='+e+' error='+err+' total='+w.length)"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ console.error('[enrich-orchestrator] DEPRECATED: This script hardcodes Ollama and generates subagent manifests.');
3
+ console.error('[enrich-orchestrator] Use enrich-queue.mjs which supports the shared fallback driver.');
4
+ console.error('[enrich-orchestrator] Run: node tools/project-memory-context/cli/enrich-queue.mjs');
5
+ process.exit(1);
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
3
+ import { resolve, dirname, basename } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { appendProviderEvent, withRecordedAttempt } from '../src/enrichment-attempts.mjs';
8
+ import { PMC_ENRICHMENT_CONFIG_FILE, resolveEnrichmentConfig } from '../src/enrichment-config.mjs';
9
+ import { runEnrichmentWithFallback } from '../src/enrichment-driver.mjs';
10
+ import { createCloudApiProvider } from '../src/providers/cloud-api-provider.mjs';
11
+ import { createLocalModelProvider } from '../src/providers/local-model-provider.mjs';
12
+ import { appendSyncEntry, createSyncEntry } from '../src/sync-manifest.mjs';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const PROJECT_ROOT = resolve(__dirname, '..', '..', '..');
16
+
17
+ export function parseQueueConcurrency(rawValue) {
18
+ const parsed = parseInt(rawValue || '8', 10);
19
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
20
+ }
21
+
22
+ const PMC_CONCURRENCY = parseQueueConcurrency(process.env.PMC_CONCURRENCY);
23
+ const PROJECT_SLUG = process.env.PMC_PROJECT_SLUG || basename(PROJECT_ROOT);
24
+ const TIMEOUT_MS = parseInt(process.env.PMC_TIMEOUT_MS || '300000', 10);
25
+ const REPORT_INTERVAL_MS = parseInt(process.env.PMC_REPORT_INTERVAL || '30000', 10);
26
+
27
+ let _worklist = [];
28
+ let _symbolIndex = {};
29
+ let _worklistFile = '';
30
+ let _symbolIndexFile = '';
31
+ let _enrichmentDir = '';
32
+
33
+ async function loadJson(path) {
34
+ return JSON.parse(await readFile(path, 'utf8'));
35
+ }
36
+
37
+ async function loadOptionalJson(path, fallback = null) {
38
+ try {
39
+ return await loadJson(path);
40
+ } catch {
41
+ return fallback;
42
+ }
43
+ }
44
+
45
+ async function saveJson(path, data) {
46
+ await mkdir(dirname(path), { recursive: true });
47
+ await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
48
+ }
49
+
50
+ function safeKey(key) {
51
+ return key.replace(/[^a-zA-Z0-9_-]+/g, '_');
52
+ }
53
+
54
+ class SlotTracker {
55
+ constructor(maxSlots) {
56
+ this.maxSlots = maxSlots;
57
+ this.slots = new Map();
58
+ this.completedCount = 0;
59
+ this.errorCount = 0;
60
+ this.totalProcessed = 0;
61
+ }
62
+
63
+ allocate(symbol) {
64
+ for (let i = 0; i < this.maxSlots; i++) {
65
+ if (!this.slots.has(i)) {
66
+ const startTime = Date.now();
67
+ this.slots.set(i, { symbol, startTime });
68
+ return i;
69
+ }
70
+ }
71
+ return -1;
72
+ }
73
+
74
+ complete(slotIdx, result) {
75
+ const slot = this.slots.get(slotIdx);
76
+ if (!slot) return null;
77
+ const elapsed = Date.now() - slot.startTime;
78
+ this.slots.delete(slotIdx);
79
+ this.completedCount++;
80
+ this.totalProcessed++;
81
+ return { ...slot, elapsed, result };
82
+ }
83
+
84
+ fail(slotIdx, error) {
85
+ const slot = this.slots.get(slotIdx);
86
+ if (!slot) return null;
87
+ const elapsed = Date.now() - slot.startTime;
88
+ this.slots.delete(slotIdx);
89
+ this.errorCount++;
90
+ this.totalProcessed++;
91
+ return { ...slot, elapsed, error };
92
+ }
93
+
94
+ isFull() {
95
+ return this.slots.size >= this.maxSlots;
96
+ }
97
+
98
+ activeCount() {
99
+ return this.slots.size;
100
+ }
101
+
102
+ hasActiveSlots() {
103
+ return this.slots.size > 0;
104
+ }
105
+ }
106
+
107
+ function createAgentSubagentProvider() {
108
+ return {
109
+ kind: 'agent-subagent',
110
+ isConfigured(context) {
111
+ const { enabled = true, agentName = 'enrich' } = context?.config?.agentSubagent ?? {};
112
+ if (!enabled) {
113
+ return { ok: false, reason: 'agent-subagent is disabled' };
114
+ }
115
+
116
+ return { ok: true, provider: agentName };
117
+ },
118
+ async isAvailable() {
119
+ return { ok: false, reason: 'agent-subagent is unavailable in cli context', errorType: 'provider' };
120
+ },
121
+ async enrich() {
122
+ throw new Error('agent-subagent is unavailable in cli context');
123
+ },
124
+ };
125
+ }
126
+
127
+ async function buildSymbolPrompt(symbol, projectRoot) {
128
+ const absoluteFile = resolve(projectRoot, symbol.filePath);
129
+ const content = await readFile(absoluteFile, 'utf8');
130
+ const lines = content.split('\n');
131
+
132
+ const codeSection = lines.slice(symbol.range.startLine - 1, symbol.range.endLine).join('\n');
133
+ const importsSection = lines.slice(0, symbol.range.startLine - 1).filter(l => /^\s*import\b/.test(l) || /^\s*using\b/.test(l)).join('\n');
134
+
135
+ return `Symbol: ${symbol.name}\nKind: ${symbol.kind}\nLanguage: ${symbol.language}\nLocation: ${symbol.filePath}:${symbol.range.startLine}-${symbol.range.endLine}\n\nContext (imports):\n${importsSection || '(none)'}\n\nCode:\n${codeSection}\n\nReturn a compact structured explanation with:\n- responsibility\n- primary inputs\n- output\n- immediate dependencies\n- role in module`;
136
+ }
137
+
138
+ function applyAttemptsToEntry(entry, attempts) {
139
+ let updated = { ...entry };
140
+ for (const attempt of attempts ?? []) {
141
+ updated = withRecordedAttempt(updated, attempt);
142
+ }
143
+ return updated;
144
+ }
145
+
146
+ function getLastAttemptMode(attempts) {
147
+ return attempts?.length ? attempts[attempts.length - 1].mode ?? null : null;
148
+ }
149
+
150
+ function getLastAttemptError(attempts) {
151
+ for (let idx = (attempts?.length ?? 0) - 1; idx >= 0; idx--) {
152
+ const message = attempts[idx]?.errorMessage;
153
+ if (message) {
154
+ return message;
155
+ }
156
+ }
157
+
158
+ return 'All enrichment providers failed';
159
+ }
160
+
161
+ function createQueueEnrichmentError(result) {
162
+ const error = new Error(result.error);
163
+ error.symbolKey = result.symbolKey;
164
+ error.attempts = result.attempts;
165
+ error.lastModeUsed = result.lastModeUsed;
166
+ error.failedAt = result.failedAt;
167
+ return error;
168
+ }
169
+
170
+ export function buildQueueSummary(worklist) {
171
+ return {
172
+ enriched: worklist.filter((entry) => entry.status === 'enriched' || entry.status === 'already_enriched').length,
173
+ errors: worklist.filter((entry) => entry.status === 'error').length,
174
+ pending: worklist.filter((entry) => entry.status === 'pending' || entry.status === 'stale').length,
175
+ };
176
+ }
177
+
178
+ async function loadRuntimeEnrichmentConfig(projectRoot, env) {
179
+ const projectConfig = await loadOptionalJson(resolve(projectRoot, '.opencode', PMC_ENRICHMENT_CONFIG_FILE));
180
+ const globalConfig = await loadOptionalJson(resolve(homedir(), '.config', 'opencode', PMC_ENRICHMENT_CONFIG_FILE));
181
+ return resolveEnrichmentConfig({ projectConfig, globalConfig, env });
182
+ }
183
+
184
+ function createQueueProviders() {
185
+ return [
186
+ createLocalModelProvider(),
187
+ createCloudApiProvider(),
188
+ createAgentSubagentProvider(),
189
+ ];
190
+ }
191
+
192
+ export async function runQueueSymbolEnrichment({
193
+ symbol,
194
+ projectRoot,
195
+ projectSlug,
196
+ timeoutMs,
197
+ enrichmentDir,
198
+ worklist,
199
+ worklistFile,
200
+ symbolIndex = {},
201
+ symbolIndexFile = '',
202
+ config,
203
+ providers,
204
+ env = process.env,
205
+ runEnrichmentWithFallbackImpl = runEnrichmentWithFallback,
206
+ }) {
207
+ const prompt = await buildSymbolPrompt(symbol, projectRoot);
208
+ const result = await runEnrichmentWithFallbackImpl({
209
+ request: { prompt, timeoutMs },
210
+ config,
211
+ providers,
212
+ env,
213
+ });
214
+ const wlEntry = worklist.find((entry) => entry.symbolKey === symbol.symbolKey);
215
+
216
+ for (const attempt of result.attempts ?? []) {
217
+ await appendProviderEvent(enrichmentDir, {
218
+ symbolKey: symbol.symbolKey,
219
+ name: symbol.name,
220
+ ...attempt,
221
+ });
222
+ }
223
+
224
+ if (result.status === 'succeeded') {
225
+ const memoryId = `queue-${safeKey(symbol.symbolKey)}`;
226
+ const enrichedAt = new Date().toISOString();
227
+ const memoryFile = resolve(enrichmentDir, `${safeKey(symbol.symbolKey)}.memory.json`);
228
+ await saveJson(memoryFile, {
229
+ content: result.content,
230
+ category: 'architecture',
231
+ tags: ['symbol', symbol.language, symbol.kind, `project:${projectSlug}`, `file:${symbol.filePath}`],
232
+ });
233
+
234
+ if (wlEntry) {
235
+ Object.assign(wlEntry, applyAttemptsToEntry(wlEntry, result.attempts), {
236
+ status: 'enriched',
237
+ memoryId,
238
+ enrichedAt,
239
+ error: undefined,
240
+ failedAt: undefined,
241
+ });
242
+ }
243
+
244
+ symbolIndex[symbol.symbolKey] = {
245
+ memoryId,
246
+ graphNodeId: wlEntry?.graphNodeId ?? symbol.graphNodeId ?? null,
247
+ codeHash: symbol.codeHash,
248
+ status: 'enriched',
249
+ lastEnrichedAt: enrichedAt,
250
+ };
251
+
252
+ await saveJson(worklistFile, worklist);
253
+ if (symbolIndexFile) {
254
+ await saveJson(symbolIndexFile, symbolIndex);
255
+ }
256
+
257
+ try {
258
+ await appendSyncEntry(enrichmentDir, createSyncEntry({
259
+ action: 'upsert',
260
+ keyTag: `key:symbol:${safeKey(symbol.symbolKey)}`,
261
+ content: `## ${symbol.name}\n\n${result.content}`,
262
+ category: 'architecture',
263
+ tags: ['symbol', symbol.language, symbol.kind, `project:${projectSlug}`, `file:${symbol.filePath}`, 'enriched-by-queue'],
264
+ source: 'enrich-queue',
265
+ symbolKey: symbol.symbolKey,
266
+ }));
267
+ } catch (syncErr) {
268
+ console.error(`[queue] WARN: sync-manifest append failed for ${symbol.name}: ${syncErr.message}`);
269
+ }
270
+
271
+ return {
272
+ status: 'enriched',
273
+ symbolKey: symbol.symbolKey,
274
+ memoryId,
275
+ memoryContent: result.content,
276
+ language: symbol.language,
277
+ kind: symbol.kind,
278
+ filePath: symbol.filePath,
279
+ projectSlug,
280
+ codeHash: symbol.codeHash,
281
+ attempts: result.attempts ?? [],
282
+ lastModeUsed: wlEntry?.lastModeUsed ?? result.mode ?? getLastAttemptMode(result.attempts),
283
+ enrichedAt,
284
+ };
285
+ }
286
+
287
+ const error = getLastAttemptError(result.attempts);
288
+ const failedAt = new Date().toISOString();
289
+
290
+ if (wlEntry) {
291
+ Object.assign(wlEntry, applyAttemptsToEntry(wlEntry, result.attempts), {
292
+ status: 'error',
293
+ error,
294
+ failedAt,
295
+ });
296
+ }
297
+
298
+ symbolIndex[symbol.symbolKey] = {
299
+ memoryId: null,
300
+ graphNodeId: wlEntry?.graphNodeId ?? symbol.graphNodeId ?? null,
301
+ codeHash: symbol.codeHash,
302
+ status: 'error',
303
+ lastEnrichedAt: failedAt,
304
+ };
305
+
306
+ await saveJson(worklistFile, worklist);
307
+ if (symbolIndexFile) {
308
+ await saveJson(symbolIndexFile, symbolIndex);
309
+ }
310
+
311
+ const failure = {
312
+ status: 'error',
313
+ symbolKey: symbol.symbolKey,
314
+ error,
315
+ attempts: result.attempts ?? [],
316
+ lastModeUsed: wlEntry?.lastModeUsed ?? getLastAttemptMode(result.attempts),
317
+ failedAt,
318
+ };
319
+
320
+ throw createQueueEnrichmentError(failure);
321
+ }
322
+
323
+ async function checkpointSave() {
324
+ if (!_worklistFile) return;
325
+ console.error('\n[checkpoint] Saving progress...');
326
+ await saveJson(_worklistFile, _worklist);
327
+ await saveJson(_symbolIndexFile, _symbolIndex);
328
+ const pending = _worklist.filter(s => s.status === 'pending').length;
329
+ const enriched = _worklist.filter(s => s.status === 'enriched').length;
330
+ const errors = _worklist.filter(s => s.status === 'error').length;
331
+ console.error(`[checkpoint] Saved: pending=${pending} enriched=${enriched} errors=${errors}`);
332
+ }
333
+
334
+ process.on('SIGINT', async () => {
335
+ console.error('\n[queue] Interrupted — checkpointing...');
336
+ await checkpointSave();
337
+ process.exit(0);
338
+ });
339
+
340
+ process.on('SIGTERM', async () => {
341
+ console.error('\n[queue] Terminated — checkpointing...');
342
+ await checkpointSave();
343
+ process.exit(0);
344
+ });
345
+
346
+ async function main() {
347
+ const enrichmentDir = resolve(PROJECT_ROOT, '.planning/project-memory-context/enrichment');
348
+ await mkdir(enrichmentDir, { recursive: true });
349
+
350
+ const worklistFile = resolve(enrichmentDir, 'worklist.json');
351
+ const symbolIndexFile = resolve(enrichmentDir, 'symbol-index.json');
352
+
353
+ _worklistFile = worklistFile;
354
+ _symbolIndexFile = symbolIndexFile;
355
+
356
+ let worklist;
357
+ try {
358
+ worklist = await loadJson(worklistFile);
359
+ } catch {
360
+ console.error('[queue] No worklist found. Run Stage A first.');
361
+ process.exit(1);
362
+ }
363
+
364
+ let symbolIndex = {};
365
+ try { symbolIndex = await loadJson(symbolIndexFile); } catch {}
366
+
367
+ const enrichmentConfig = await loadRuntimeEnrichmentConfig(PROJECT_ROOT, process.env);
368
+ const providers = createQueueProviders();
369
+
370
+ _enrichmentDir = enrichmentDir;
371
+ _worklist = worklist;
372
+ _symbolIndex = symbolIndex;
373
+
374
+ for (const entry of worklist) {
375
+ if (entry.status === 'enriched' && entry.memoryId) {
376
+ entry.status = 'already_enriched';
377
+ console.error(`[resume] Skipping ${entry.name} (${entry.symbolKey}) — already enriched`);
378
+ }
379
+ }
380
+
381
+ const pending = worklist.filter(s => s.status === 'pending' || s.status === 'stale');
382
+ const alreadyEnriched = worklist.filter(s => s.status === 'already_enriched').length;
383
+ const staleCount = worklist.filter(s => s.status === 'stale').length;
384
+ const total = worklist.length;
385
+
386
+ if (pending.length === 0) {
387
+ const summary = buildQueueSummary(worklist);
388
+ console.log(JSON.stringify({ complete: true, total, enriched: summary.enriched, errors: summary.errors }));
389
+ return;
390
+ }
391
+
392
+ console.error(`[queue] Starting continuous enrichment: ${pending.length} pending (${staleCount} stale), ${alreadyEnriched} already enriched, ${PMC_CONCURRENCY} parallel slots`);
393
+ console.error(`[queue] Modes: ${enrichmentConfig.preferredModes.join(', ')} | Local: ${enrichmentConfig.localModel.baseUrl} | Model: ${enrichmentConfig.localModel.model} | Timeout: ${TIMEOUT_MS}ms per symbol\n`);
394
+
395
+ const tracker = new SlotTracker(PMC_CONCURRENCY);
396
+ const queue = [...pending];
397
+ const results = [];
398
+ const errors = [];
399
+
400
+ const startTime = Date.now();
401
+
402
+ async function dispatchNext() {
403
+ if (queue.length === 0) return null;
404
+ const symbol = queue.shift();
405
+ const slotIdx = tracker.allocate(symbol);
406
+ if (slotIdx === -1) {
407
+ queue.unshift(symbol);
408
+ return null;
409
+ }
410
+
411
+ runQueueSymbolEnrichment({
412
+ symbol,
413
+ projectRoot: PROJECT_ROOT,
414
+ projectSlug: PROJECT_SLUG,
415
+ timeoutMs: TIMEOUT_MS,
416
+ enrichmentDir: _enrichmentDir,
417
+ worklist,
418
+ worklistFile,
419
+ symbolIndex,
420
+ symbolIndexFile,
421
+ config: enrichmentConfig,
422
+ providers,
423
+ env: process.env,
424
+ })
425
+ .then(async (result) => {
426
+ const completion = tracker.complete(slotIdx, result);
427
+
428
+ console.error(`[slot ${slotIdx}] DONE ${symbol.name} (${symbol.filePath}) — ${Math.round(completion.elapsed / 1000)}s — queuing next`);
429
+ results.push({ symbolKey: symbol.symbolKey, memoryId: result.memoryId, elapsed: completion.elapsed });
430
+
431
+ dispatchNext();
432
+ })
433
+ .catch(async (err) => {
434
+ const failure = tracker.fail(slotIdx, err.message);
435
+ console.error(`[slot ${slotIdx}] ERROR ${symbol.name}: ${err.message} (${Math.round(failure.elapsed / 1000)}s)`);
436
+ errors.push({ symbolKey: symbol.symbolKey, error: err.message, elapsed: failure.elapsed, failedAt: err.failedAt ?? null });
437
+
438
+ const wlEntry = worklist.find(s => s.symbolKey === symbol.symbolKey);
439
+ if (wlEntry) {
440
+ wlEntry.status = 'error';
441
+ wlEntry.error = err.message;
442
+ wlEntry.failedAt = new Date().toISOString();
443
+ }
444
+
445
+ await saveJson(worklistFile, worklist);
446
+
447
+ dispatchNext();
448
+ });
449
+
450
+ return slotIdx;
451
+ }
452
+
453
+ const initPromises = [];
454
+ for (let i = 0; i < PMC_CONCURRENCY && queue.length > 0; i++) {
455
+ initPromises.push(dispatchNext());
456
+ }
457
+
458
+ await Promise.all(initPromises);
459
+
460
+ await new Promise((resolve) => {
461
+ const checkInterval = setInterval(() => {
462
+ if (!tracker.hasActiveSlots() && queue.length === 0) {
463
+ clearInterval(checkInterval);
464
+ resolve();
465
+ } else {
466
+ const elapsedTotal = Date.now() - startTime;
467
+ const remaining = queue.length;
468
+ const active = tracker.activeCount();
469
+ const done = results.length + errors.length;
470
+ console.error(`[queue status] done=${done} active=${active} queued=${remaining} elapsed=${Math.round(elapsedTotal / 1000)}s`);
471
+ }
472
+ }, REPORT_INTERVAL_MS);
473
+ });
474
+
475
+ await checkpointSave();
476
+
477
+ for (const r of results) {
478
+ const entry = worklist.find(s => s.symbolKey === r.symbolKey);
479
+ if (entry) {
480
+ symbolIndex[r.symbolKey] = {
481
+ memoryId: r.memoryId,
482
+ graphNodeId: entry.graphNodeId ?? null,
483
+ codeHash: entry.codeHash,
484
+ status: 'enriched',
485
+ lastEnrichedAt: entry.enrichedAt,
486
+ };
487
+ }
488
+ }
489
+ for (const err of errors) {
490
+ const entry = worklist.find(s => s.symbolKey === err.symbolKey);
491
+ if (entry) {
492
+ symbolIndex[err.symbolKey] = {
493
+ memoryId: null,
494
+ graphNodeId: entry.graphNodeId ?? null,
495
+ codeHash: entry.codeHash,
496
+ status: 'error',
497
+ lastEnrichedAt: err.failedAt || null,
498
+ };
499
+ }
500
+ }
501
+ await saveJson(symbolIndexFile, symbolIndex);
502
+
503
+ const totalElapsed = Date.now() - startTime;
504
+ const summary = buildQueueSummary(worklist);
505
+ const avgTime = results.length > 0 ? results.reduce((a, r) => a + r.elapsed, 0) / results.length : 0;
506
+
507
+ console.log(JSON.stringify({
508
+ complete: summary.pending === 0,
509
+ total: worklist.length,
510
+ enriched: summary.enriched,
511
+ errors: summary.errors,
512
+ pending: summary.pending,
513
+ totalElapsedSeconds: Math.round(totalElapsed / 1000),
514
+ avgSymbolTimeSeconds: Math.round(avgTime / 1000),
515
+ resultsPerSecond: Math.round((results.length / (totalElapsed / 1000)) * 100) / 100,
516
+ timing: results.map(r => ({ symbolKey: r.symbolKey, seconds: Math.round(r.elapsed / 1000) })),
517
+ }, null, 2));
518
+ }
519
+
520
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
521
+ main().catch(err => {
522
+ console.error('[fatal]', err.message);
523
+ process.exit(1);
524
+ });
525
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ console.error('[enrich-sync] DEPRECATED: This script hardcodes Ollama. Use enrich-queue.mjs instead.');
3
+ console.error('[enrich-sync] The enrich-queue supports the shared fallback driver (local-model → cloud-api → agent-subagent).');
4
+ console.error('[enrich-sync] Run: node tools/project-memory-context/cli/enrich-queue.mjs');
5
+ process.exit(1);
package/cli/enrich.mjs ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ export { runQueueSymbolEnrichment, buildQueueSummary, parseQueueConcurrency } from './enrich-queue.mjs';
7
+
8
+ const SCRIPT_PATH = resolve(dirname(fileURLToPath(import.meta.url)), 'enrich-queue.mjs');
9
+
10
+ function printHelp() {
11
+ console.log('Usage: pmc enrich');
12
+ }
13
+
14
+ export async function runEnrichQueue(projectRoot = process.cwd()) {
15
+ const mod = await import('./enrich-queue.mjs');
16
+ if (typeof mod.default === 'function') {
17
+ return mod.default(projectRoot);
18
+ }
19
+ return null;
20
+ }
21
+
22
+ export async function main(args = process.argv.slice(2)) {
23
+ if (args.includes('--help') || args.includes('-h')) {
24
+ printHelp();
25
+ return 0;
26
+ }
27
+
28
+ return await new Promise((resolvePromise, rejectPromise) => {
29
+ const child = spawn(process.execPath, [SCRIPT_PATH, ...args], { stdio: 'inherit' });
30
+ child.once('error', rejectPromise);
31
+ child.once('exit', (code, signal) => {
32
+ if (signal) {
33
+ rejectPromise(new Error(`enrich exited from signal ${signal}`));
34
+ return;
35
+ }
36
+
37
+ resolvePromise(code ?? 0);
38
+ });
39
+ });
40
+ }
41
+
42
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
43
+ const exitCode = await main().catch((error) => {
44
+ console.error('[enrich] FATAL:', error.message);
45
+ return 1;
46
+ });
47
+
48
+ if (exitCode !== 0) {
49
+ process.exit(exitCode);
50
+ }
51
+ }