@tyroneross/navgator 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 (84) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +486 -0
  4. package/agents/architecture-advisor.md +109 -0
  5. package/commands/nav-check.md +64 -0
  6. package/commands/nav-connections.md +58 -0
  7. package/commands/nav-diagram.md +106 -0
  8. package/commands/nav-export.md +71 -0
  9. package/commands/nav-impact.md +58 -0
  10. package/commands/nav-scan.md +46 -0
  11. package/commands/nav-status.md +44 -0
  12. package/dist/cli/index.d.ts +7 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +627 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/config.d.ts +95 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +262 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/diagram.d.ts +36 -0
  21. package/dist/diagram.d.ts.map +1 -0
  22. package/dist/diagram.js +333 -0
  23. package/dist/diagram.js.map +1 -0
  24. package/dist/index.d.ts +16 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +18 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/scanner.d.ts +57 -0
  29. package/dist/scanner.d.ts.map +1 -0
  30. package/dist/scanner.js +282 -0
  31. package/dist/scanner.js.map +1 -0
  32. package/dist/scanners/connections/ast-scanner.d.ts +26 -0
  33. package/dist/scanners/connections/ast-scanner.d.ts.map +1 -0
  34. package/dist/scanners/connections/ast-scanner.js +430 -0
  35. package/dist/scanners/connections/ast-scanner.js.map +1 -0
  36. package/dist/scanners/connections/service-calls.d.ts +14 -0
  37. package/dist/scanners/connections/service-calls.d.ts.map +1 -0
  38. package/dist/scanners/connections/service-calls.js +719 -0
  39. package/dist/scanners/connections/service-calls.js.map +1 -0
  40. package/dist/scanners/infrastructure/index.d.ts +27 -0
  41. package/dist/scanners/infrastructure/index.d.ts.map +1 -0
  42. package/dist/scanners/infrastructure/index.js +233 -0
  43. package/dist/scanners/infrastructure/index.js.map +1 -0
  44. package/dist/scanners/packages/npm.d.ts +18 -0
  45. package/dist/scanners/packages/npm.d.ts.map +1 -0
  46. package/dist/scanners/packages/npm.js +256 -0
  47. package/dist/scanners/packages/npm.js.map +1 -0
  48. package/dist/scanners/packages/pip.d.ts +14 -0
  49. package/dist/scanners/packages/pip.d.ts.map +1 -0
  50. package/dist/scanners/packages/pip.js +228 -0
  51. package/dist/scanners/packages/pip.js.map +1 -0
  52. package/dist/scanners/prompts/detector.d.ts +119 -0
  53. package/dist/scanners/prompts/detector.d.ts.map +1 -0
  54. package/dist/scanners/prompts/detector.js +617 -0
  55. package/dist/scanners/prompts/detector.js.map +1 -0
  56. package/dist/scanners/prompts/index.d.ts +51 -0
  57. package/dist/scanners/prompts/index.d.ts.map +1 -0
  58. package/dist/scanners/prompts/index.js +340 -0
  59. package/dist/scanners/prompts/index.js.map +1 -0
  60. package/dist/scanners/prompts/types.d.ts +127 -0
  61. package/dist/scanners/prompts/types.d.ts.map +1 -0
  62. package/dist/scanners/prompts/types.js +37 -0
  63. package/dist/scanners/prompts/types.js.map +1 -0
  64. package/dist/setup.d.ts +65 -0
  65. package/dist/setup.d.ts.map +1 -0
  66. package/dist/setup.js +261 -0
  67. package/dist/setup.js.map +1 -0
  68. package/dist/storage.d.ts +147 -0
  69. package/dist/storage.d.ts.map +1 -0
  70. package/dist/storage.js +931 -0
  71. package/dist/storage.js.map +1 -0
  72. package/dist/types.d.ts +296 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/types.js +55 -0
  75. package/dist/types.js.map +1 -0
  76. package/dist/ui-server.d.ts +17 -0
  77. package/dist/ui-server.d.ts.map +1 -0
  78. package/dist/ui-server.js +815 -0
  79. package/dist/ui-server.js.map +1 -0
  80. package/hooks/hooks.json +57 -0
  81. package/package.json +80 -0
  82. package/scripts/ibr-ui-test.mjs +359 -0
  83. package/scripts/postinstall.cjs +35 -0
  84. package/skills/architecture-awareness/SKILL.md +141 -0
@@ -0,0 +1,931 @@
1
+ /**
2
+ * NavGator Storage System
3
+ * File-based persistence for components and connections
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as crypto from 'crypto';
8
+ import { getConfig, getComponentsPath, getConnectionsPath, getIndexPath, getGraphPath, getSnapshotsPath, getHashesPath, getSummaryPath, getSummaryFullPath, getFileMapPath, getPromptsPath, ensureStorageDirectories, isValidComponentId, isValidConnectionId, } from './config.js';
9
+ // =============================================================================
10
+ // COMPONENT STORAGE
11
+ // =============================================================================
12
+ /**
13
+ * Store a component to disk
14
+ */
15
+ export async function storeComponent(component, config, projectRoot) {
16
+ const cfg = config || getConfig();
17
+ ensureStorageDirectories(cfg, projectRoot);
18
+ const componentsPath = getComponentsPath(cfg, projectRoot);
19
+ const filePath = path.join(componentsPath, `${component.component_id}.json`);
20
+ await fs.promises.writeFile(filePath, JSON.stringify(component, null, 2), 'utf-8');
21
+ return {
22
+ component_id: component.component_id,
23
+ file_path: filePath,
24
+ };
25
+ }
26
+ /**
27
+ * Load a component by ID
28
+ */
29
+ export async function loadComponent(componentId, config, projectRoot) {
30
+ if (!isValidComponentId(componentId)) {
31
+ return null;
32
+ }
33
+ const cfg = config || getConfig();
34
+ const componentsPath = getComponentsPath(cfg, projectRoot);
35
+ const filePath = path.join(componentsPath, `${componentId}.json`);
36
+ if (!fs.existsSync(filePath)) {
37
+ return null;
38
+ }
39
+ try {
40
+ const content = await fs.promises.readFile(filePath, 'utf-8');
41
+ return JSON.parse(content);
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Load all components (parallelized for efficiency)
49
+ */
50
+ export async function loadAllComponents(config, projectRoot) {
51
+ const cfg = config || getConfig();
52
+ const componentsPath = getComponentsPath(cfg, projectRoot);
53
+ if (!fs.existsSync(componentsPath)) {
54
+ return [];
55
+ }
56
+ const files = await fs.promises.readdir(componentsPath);
57
+ const jsonFiles = files.filter((f) => f.endsWith('.json'));
58
+ // Parallelize reads
59
+ const results = await Promise.all(jsonFiles.map(async (file) => {
60
+ try {
61
+ const filePath = path.join(componentsPath, file);
62
+ const content = await fs.promises.readFile(filePath, 'utf-8');
63
+ return JSON.parse(content);
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }));
69
+ return results.filter((c) => c !== null);
70
+ }
71
+ /**
72
+ * Delete a component by ID
73
+ */
74
+ export async function deleteComponent(componentId, config, projectRoot) {
75
+ if (!isValidComponentId(componentId)) {
76
+ return false;
77
+ }
78
+ const cfg = config || getConfig();
79
+ const componentsPath = getComponentsPath(cfg, projectRoot);
80
+ const filePath = path.join(componentsPath, `${componentId}.json`);
81
+ if (!fs.existsSync(filePath)) {
82
+ return false;
83
+ }
84
+ await fs.promises.unlink(filePath);
85
+ return true;
86
+ }
87
+ // =============================================================================
88
+ // CONNECTION STORAGE
89
+ // =============================================================================
90
+ /**
91
+ * Store a connection to disk
92
+ */
93
+ export async function storeConnection(connection, config, projectRoot) {
94
+ const cfg = config || getConfig();
95
+ ensureStorageDirectories(cfg, projectRoot);
96
+ const connectionsPath = getConnectionsPath(cfg, projectRoot);
97
+ const filePath = path.join(connectionsPath, `${connection.connection_id}.json`);
98
+ await fs.promises.writeFile(filePath, JSON.stringify(connection, null, 2), 'utf-8');
99
+ return {
100
+ connection_id: connection.connection_id,
101
+ file_path: filePath,
102
+ };
103
+ }
104
+ /**
105
+ * Load a connection by ID
106
+ */
107
+ export async function loadConnection(connectionId, config, projectRoot) {
108
+ if (!isValidConnectionId(connectionId)) {
109
+ return null;
110
+ }
111
+ const cfg = config || getConfig();
112
+ const connectionsPath = getConnectionsPath(cfg, projectRoot);
113
+ const filePath = path.join(connectionsPath, `${connectionId}.json`);
114
+ if (!fs.existsSync(filePath)) {
115
+ return null;
116
+ }
117
+ try {
118
+ const content = await fs.promises.readFile(filePath, 'utf-8');
119
+ return JSON.parse(content);
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ /**
126
+ * Load all connections (parallelized for efficiency)
127
+ */
128
+ export async function loadAllConnections(config, projectRoot) {
129
+ const cfg = config || getConfig();
130
+ const connectionsPath = getConnectionsPath(cfg, projectRoot);
131
+ if (!fs.existsSync(connectionsPath)) {
132
+ return [];
133
+ }
134
+ const files = await fs.promises.readdir(connectionsPath);
135
+ const jsonFiles = files.filter((f) => f.endsWith('.json'));
136
+ // Parallelize reads
137
+ const results = await Promise.all(jsonFiles.map(async (file) => {
138
+ try {
139
+ const filePath = path.join(connectionsPath, file);
140
+ const content = await fs.promises.readFile(filePath, 'utf-8');
141
+ return JSON.parse(content);
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }));
147
+ return results.filter((c) => c !== null);
148
+ }
149
+ /**
150
+ * Delete a connection by ID
151
+ */
152
+ export async function deleteConnection(connectionId, config, projectRoot) {
153
+ if (!isValidConnectionId(connectionId)) {
154
+ return false;
155
+ }
156
+ const cfg = config || getConfig();
157
+ const connectionsPath = getConnectionsPath(cfg, projectRoot);
158
+ const filePath = path.join(connectionsPath, `${connectionId}.json`);
159
+ if (!fs.existsSync(filePath)) {
160
+ return false;
161
+ }
162
+ await fs.promises.unlink(filePath);
163
+ return true;
164
+ }
165
+ // =============================================================================
166
+ // INDEX MANAGEMENT
167
+ // =============================================================================
168
+ /**
169
+ * Build and save the index from current components and connections
170
+ */
171
+ export async function buildIndex(config, projectRoot) {
172
+ const cfg = config || getConfig();
173
+ const components = await loadAllComponents(cfg, projectRoot);
174
+ const connections = await loadAllConnections(cfg, projectRoot);
175
+ const index = {
176
+ version: '1.0.0',
177
+ last_scan: Date.now(),
178
+ project_path: projectRoot || process.cwd(),
179
+ components: {
180
+ by_name: {},
181
+ by_type: {},
182
+ by_layer: {},
183
+ by_status: {},
184
+ },
185
+ connections: {
186
+ by_type: {},
187
+ by_from: {},
188
+ by_to: {},
189
+ },
190
+ stats: {
191
+ total_components: components.length,
192
+ total_connections: connections.length,
193
+ components_by_type: {},
194
+ connections_by_type: {},
195
+ outdated_count: 0,
196
+ vulnerable_count: 0,
197
+ },
198
+ };
199
+ // Index components
200
+ for (const component of components) {
201
+ // By name
202
+ index.components.by_name[component.name] = component.component_id;
203
+ // By type
204
+ if (!index.components.by_type[component.type]) {
205
+ index.components.by_type[component.type] = [];
206
+ }
207
+ index.components.by_type[component.type].push(component.component_id);
208
+ // By layer
209
+ if (!index.components.by_layer[component.role.layer]) {
210
+ index.components.by_layer[component.role.layer] = [];
211
+ }
212
+ index.components.by_layer[component.role.layer].push(component.component_id);
213
+ // By status
214
+ if (!index.components.by_status[component.status]) {
215
+ index.components.by_status[component.status] = [];
216
+ }
217
+ index.components.by_status[component.status].push(component.component_id);
218
+ // Stats
219
+ index.stats.components_by_type[component.type] =
220
+ (index.stats.components_by_type[component.type] || 0) + 1;
221
+ if (component.status === 'outdated')
222
+ index.stats.outdated_count++;
223
+ if (component.status === 'vulnerable')
224
+ index.stats.vulnerable_count++;
225
+ }
226
+ // Index connections
227
+ for (const connection of connections) {
228
+ // By type
229
+ if (!index.connections.by_type[connection.connection_type]) {
230
+ index.connections.by_type[connection.connection_type] = [];
231
+ }
232
+ index.connections.by_type[connection.connection_type].push(connection.connection_id);
233
+ // By from
234
+ if (!index.connections.by_from[connection.from.component_id]) {
235
+ index.connections.by_from[connection.from.component_id] = [];
236
+ }
237
+ index.connections.by_from[connection.from.component_id].push(connection.connection_id);
238
+ // By to
239
+ if (!index.connections.by_to[connection.to.component_id]) {
240
+ index.connections.by_to[connection.to.component_id] = [];
241
+ }
242
+ index.connections.by_to[connection.to.component_id].push(connection.connection_id);
243
+ // Stats
244
+ index.stats.connections_by_type[connection.connection_type] =
245
+ (index.stats.connections_by_type[connection.connection_type] || 0) + 1;
246
+ }
247
+ // Save index
248
+ const indexPath = getIndexPath(cfg, projectRoot);
249
+ await fs.promises.writeFile(indexPath, JSON.stringify(index, null, 2), 'utf-8');
250
+ return index;
251
+ }
252
+ /**
253
+ * Load the index
254
+ */
255
+ export async function loadIndex(config, projectRoot) {
256
+ const cfg = config || getConfig();
257
+ const indexPath = getIndexPath(cfg, projectRoot);
258
+ if (!fs.existsSync(indexPath)) {
259
+ return null;
260
+ }
261
+ try {
262
+ const content = await fs.promises.readFile(indexPath, 'utf-8');
263
+ return JSON.parse(content);
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
269
+ // =============================================================================
270
+ // GRAPH BUILDING
271
+ // =============================================================================
272
+ /**
273
+ * Build the connection graph
274
+ */
275
+ export async function buildGraph(config, projectRoot) {
276
+ const cfg = config || getConfig();
277
+ const components = await loadAllComponents(cfg, projectRoot);
278
+ const connections = await loadAllConnections(cfg, projectRoot);
279
+ const nodes = components.map((c) => ({
280
+ id: c.component_id,
281
+ name: c.name,
282
+ type: c.type,
283
+ layer: c.role.layer,
284
+ }));
285
+ const edges = connections.map((c) => ({
286
+ id: c.connection_id,
287
+ source: c.from.component_id,
288
+ target: c.to.component_id,
289
+ type: c.connection_type,
290
+ label: c.description,
291
+ }));
292
+ const graph = {
293
+ nodes,
294
+ edges,
295
+ metadata: {
296
+ generated_at: Date.now(),
297
+ component_count: nodes.length,
298
+ connection_count: edges.length,
299
+ },
300
+ };
301
+ // Save graph
302
+ const graphPath = getGraphPath(cfg, projectRoot);
303
+ await fs.promises.writeFile(graphPath, JSON.stringify(graph, null, 2), 'utf-8');
304
+ return graph;
305
+ }
306
+ // =============================================================================
307
+ // FILE MAP (Tier 2 - O(1) file-to-component lookup)
308
+ // =============================================================================
309
+ /**
310
+ * Build a map of file paths → component IDs for fast lookup in hooks.
311
+ * Sources: component config_files + connection code_reference files + connection locations.
312
+ */
313
+ export async function buildFileMap(config, projectRoot) {
314
+ const cfg = config || getConfig();
315
+ const root = projectRoot || process.cwd();
316
+ const components = await loadAllComponents(cfg, root);
317
+ const connections = await loadAllConnections(cfg, root);
318
+ const fileMap = {};
319
+ // Index config files from components
320
+ for (const c of components) {
321
+ for (const f of c.source.config_files || []) {
322
+ fileMap[f] = c.component_id;
323
+ }
324
+ }
325
+ // Index source files from connections (code_reference, from.location, to.location)
326
+ for (const conn of connections) {
327
+ if (conn.code_reference?.file) {
328
+ // Map to the "from" component — this file uses/imports the dependency
329
+ fileMap[conn.code_reference.file] = conn.from.component_id;
330
+ }
331
+ if (conn.from.location?.file) {
332
+ fileMap[conn.from.location.file] = conn.from.component_id;
333
+ }
334
+ if (conn.to.location?.file) {
335
+ fileMap[conn.to.location.file] = conn.to.component_id;
336
+ }
337
+ }
338
+ const fileMapPath = getFileMapPath(cfg, root);
339
+ await fs.promises.writeFile(fileMapPath, JSON.stringify(fileMap, null, 2), 'utf-8');
340
+ return fileMap;
341
+ }
342
+ // =============================================================================
343
+ // PROMPT STORAGE (Tier 2 - Full prompt content for on-demand loading)
344
+ // =============================================================================
345
+ /**
346
+ * Save prompt scan results to prompts.json
347
+ */
348
+ export async function savePromptScan(promptData, config, projectRoot) {
349
+ const cfg = config || getConfig();
350
+ const root = projectRoot || process.cwd();
351
+ ensureStorageDirectories(cfg, root);
352
+ const promptsPath = getPromptsPath(cfg, root);
353
+ await fs.promises.writeFile(promptsPath, JSON.stringify(promptData, null, 2), 'utf-8');
354
+ }
355
+ // =============================================================================
356
+ // SUMMARY GENERATION (Tier 1 - Hot Context for LLMs)
357
+ // =============================================================================
358
+ const AI_PROVIDER_NAMES = new Set([
359
+ 'openai', '@anthropic-ai/sdk', '@langchain/core', '@langchain/openai',
360
+ '@langchain/anthropic', '@langchain/groq', 'groq-sdk', 'langsmith',
361
+ '@mistralai/mistralai', 'replicate', '@huggingface/inference',
362
+ '@google/generative-ai', '@vercel/ai', 'ai', 'cohere-ai',
363
+ ]);
364
+ /**
365
+ * Build a concise markdown summary with pointers to detail files.
366
+ * This is the "hot context" an LLM reads first on cold start.
367
+ */
368
+ export async function buildSummary(config, projectRoot, promptScan) {
369
+ const cfg = config || getConfig();
370
+ const root = projectRoot || process.cwd();
371
+ const components = await loadAllComponents(cfg, root);
372
+ const connections = await loadAllConnections(cfg, root);
373
+ const now = new Date().toISOString();
374
+ const aiComponents = components.filter((c) => AI_PROVIDER_NAMES.has(c.name) || c.type === 'llm' || c.type === 'service');
375
+ // Group components by layer
376
+ const byLayer = new Map();
377
+ for (const c of components) {
378
+ const layer = c.role.layer;
379
+ if (!byLayer.has(layer))
380
+ byLayer.set(layer, []);
381
+ byLayer.get(layer).push(c);
382
+ }
383
+ // Build markdown
384
+ const lines = [];
385
+ lines.push('# Architecture Summary');
386
+ lines.push(`> NavGator auto-generated | Scanned: ${now}`);
387
+ lines.push(`> ${components.length} components | ${connections.length} connections | ${aiComponents.length} AI providers`);
388
+ lines.push('');
389
+ // Components by layer
390
+ lines.push('## Components');
391
+ lines.push('');
392
+ const layerOrder = ['frontend', 'backend', 'database', 'queue', 'infra', 'external'];
393
+ for (const layer of layerOrder) {
394
+ const group = byLayer.get(layer);
395
+ if (!group || group.length === 0)
396
+ continue;
397
+ lines.push(`### ${layer.charAt(0).toUpperCase() + layer.slice(1)} (${group.length})`);
398
+ for (const c of group) {
399
+ const ver = c.version ? ` v${c.version}` : '';
400
+ lines.push(`- **${c.name}**${ver} — ${c.role.purpose} \`components/${c.component_id}.json\``);
401
+ }
402
+ lines.push('');
403
+ }
404
+ // AI/LLM routing table
405
+ if (aiComponents.length > 0 || connections.some((c) => c.connection_type === 'service-call')) {
406
+ lines.push('## AI/LLM Routing');
407
+ lines.push('| Provider | File | Line | Purpose | Detail |');
408
+ lines.push('|----------|------|------|---------|--------|');
409
+ const aiConnections = connections.filter((c) => {
410
+ const targetComp = components.find((comp) => comp.component_id === c.to.component_id);
411
+ return targetComp && (AI_PROVIDER_NAMES.has(targetComp.name) || targetComp.type === 'llm' || targetComp.type === 'service');
412
+ });
413
+ for (const conn of aiConnections) {
414
+ const target = components.find((comp) => comp.component_id === conn.to.component_id);
415
+ const file = conn.code_reference?.file || conn.from.location?.file || '—';
416
+ const line = conn.code_reference?.line_start || conn.from.location?.line || '—';
417
+ const purpose = conn.description || target?.role.purpose || '—';
418
+ lines.push(`| ${target?.name || '?'} | ${file} | ${line} | ${purpose} | \`connections/${conn.connection_id}.json\` |`);
419
+ }
420
+ // Also list AI components with no connections yet
421
+ for (const c of aiComponents) {
422
+ const hasConn = connections.some((conn) => conn.to.component_id === c.component_id);
423
+ if (!hasConn) {
424
+ const configFile = c.source.config_files?.[0] || '—';
425
+ lines.push(`| ${c.name} | ${configFile} | — | ${c.role.purpose} | \`components/${c.component_id}.json\` |`);
426
+ }
427
+ }
428
+ lines.push('');
429
+ }
430
+ // Top connections (cap at 20)
431
+ if (connections.length > 0) {
432
+ const maxConns = Math.min(connections.length, 20);
433
+ lines.push(`## Connections (${connections.length > 20 ? `top 20 of ${connections.length}` : connections.length})`);
434
+ for (let i = 0; i < maxConns; i++) {
435
+ const conn = connections[i];
436
+ const fromComp = components.find((c) => c.component_id === conn.from.component_id);
437
+ const toComp = components.find((c) => c.component_id === conn.to.component_id);
438
+ const file = conn.code_reference?.file || '';
439
+ const line = conn.code_reference?.line_start ? `:${conn.code_reference.line_start}` : '';
440
+ lines.push(`- ${fromComp?.name || '?'} → ${toComp?.name || '?'} (${conn.connection_type}) ${file}${line}`);
441
+ }
442
+ lines.push('');
443
+ }
444
+ // Delta — compare with previous summary
445
+ const summaryPath = getSummaryPath(cfg, root);
446
+ if (fs.existsSync(summaryPath)) {
447
+ try {
448
+ const prev = await fs.promises.readFile(summaryPath, 'utf-8');
449
+ const prevNames = new Set();
450
+ for (const match of prev.matchAll(/^- \*\*(.+?)\*\*/gm)) {
451
+ prevNames.add(match[1]);
452
+ }
453
+ const currentNames = new Set(components.map((c) => c.name));
454
+ const added = components.filter((c) => !prevNames.has(c.name));
455
+ const removed = [...prevNames].filter((n) => !currentNames.has(n));
456
+ if (added.length > 0 || removed.length > 0) {
457
+ lines.push('## Changes Since Last Scan');
458
+ for (const c of added) {
459
+ lines.push(`- Added: \`${c.name}\` (${c.role.layer})`);
460
+ }
461
+ for (const name of removed) {
462
+ lines.push(`- Removed: \`${name}\``);
463
+ }
464
+ lines.push('');
465
+ }
466
+ }
467
+ catch {
468
+ // First scan or parse error — skip delta
469
+ }
470
+ }
471
+ // Prompts section (pointers only — full content in prompts.json)
472
+ if (promptScan && promptScan.prompts.length > 0) {
473
+ lines.push(`## Prompts (${promptScan.prompts.length}) — full content: \`prompts.json\``);
474
+ lines.push('| Name | File | Line | Provider | Category |');
475
+ lines.push('|------|------|------|----------|----------|');
476
+ const maxPrompts = Math.min(promptScan.prompts.length, 20);
477
+ for (let i = 0; i < maxPrompts; i++) {
478
+ const p = promptScan.prompts[i];
479
+ const provider = p.provider?.provider || '—';
480
+ const model = p.provider?.model ? ` (${p.provider.model})` : '';
481
+ const cat = p.category || '—';
482
+ lines.push(`| ${p.name} | ${p.location.file} | ${p.location.lineStart} | ${provider}${model} | ${cat} |`);
483
+ }
484
+ if (promptScan.prompts.length > 20) {
485
+ lines.push(`| ... | | | ${promptScan.prompts.length - 20} more in prompts.json | |`);
486
+ }
487
+ lines.push('');
488
+ }
489
+ // Detail pointers
490
+ lines.push('## Detail Pointers');
491
+ lines.push(`- Full index: \`index.json\``);
492
+ lines.push(`- Connection graph: \`graph.json\``);
493
+ lines.push(`- File map: \`file_map.json\``);
494
+ if (promptScan && promptScan.prompts.length > 0) {
495
+ lines.push(`- Prompts: \`prompts.json\` (${promptScan.prompts.length} prompts, full content)`);
496
+ }
497
+ lines.push(`- All components: \`components/\` (${components.length} files)`);
498
+ lines.push(`- All connections: \`connections/\` (${connections.length} files)`);
499
+ lines.push('');
500
+ const fullContent = lines.join('\n');
501
+ const lineCount = lines.length;
502
+ const COMPRESSION_THRESHOLD = 150;
503
+ if (lineCount > COMPRESSION_THRESHOLD) {
504
+ // Write full version to SUMMARY_FULL.md
505
+ const fullPath = getSummaryFullPath(cfg, root);
506
+ await fs.promises.writeFile(fullPath, fullContent, 'utf-8');
507
+ // Build compressed version: top 10 per layer, AI routing, top 10 connections
508
+ const compressed = [];
509
+ compressed.push('# Architecture Summary (Compressed)');
510
+ compressed.push('');
511
+ compressed.push('> **This is a compressed summary.** Full version: `SUMMARY_FULL.md`');
512
+ compressed.push('');
513
+ compressed.push(`> NavGator auto-generated | Scanned: ${now}`);
514
+ compressed.push(`> ${components.length} components | ${connections.length} connections | ${aiComponents.length} AI providers`);
515
+ compressed.push('');
516
+ // Components (top 10 per layer)
517
+ const hasLayerContent = layerOrder.some((l) => (byLayer.get(l)?.length || 0) > 0);
518
+ if (hasLayerContent) {
519
+ compressed.push('## Components (top 10 per layer)');
520
+ compressed.push('');
521
+ for (const layer of layerOrder) {
522
+ const group = byLayer.get(layer);
523
+ if (!group || group.length === 0)
524
+ continue;
525
+ compressed.push(`### ${layer.charAt(0).toUpperCase() + layer.slice(1)} (${group.length})`);
526
+ const top = group.slice(0, 10);
527
+ for (const c of top) {
528
+ const ver = c.version ? ` v${c.version}` : '';
529
+ compressed.push(`- **${c.name}**${ver} — ${c.role.purpose} \`components/${c.component_id}.json\``);
530
+ }
531
+ if (group.length > 10) {
532
+ compressed.push(`- ... and ${group.length - 10} more (see SUMMARY_FULL.md)`);
533
+ }
534
+ compressed.push('');
535
+ }
536
+ }
537
+ // AI/LLM routing table (preserved in compressed version)
538
+ if (aiComponents.length > 0 || connections.some((c) => c.connection_type === 'service-call')) {
539
+ compressed.push('## AI/LLM Routing');
540
+ compressed.push('| Provider | File | Line | Purpose | Detail |');
541
+ compressed.push('|----------|------|------|---------|--------|');
542
+ const aiConnections = connections.filter((c) => {
543
+ const targetComp = components.find((comp) => comp.component_id === c.to.component_id);
544
+ return targetComp && (AI_PROVIDER_NAMES.has(targetComp.name) || targetComp.type === 'llm' || targetComp.type === 'service');
545
+ });
546
+ const maxAiConns = Math.min(aiConnections.length, 10);
547
+ for (let i = 0; i < maxAiConns; i++) {
548
+ const conn = aiConnections[i];
549
+ const target = components.find((comp) => comp.component_id === conn.to.component_id);
550
+ const file = conn.code_reference?.file || conn.from.location?.file || '—';
551
+ const line = conn.code_reference?.line_start || conn.from.location?.line || '—';
552
+ const purpose = conn.description || target?.role.purpose || '—';
553
+ compressed.push(`| ${target?.name || '?'} | ${file} | ${line} | ${purpose} | \`connections/${conn.connection_id}.json\` |`);
554
+ }
555
+ if (aiConnections.length > 10) {
556
+ compressed.push(`| ... | | | ${aiConnections.length - 10} more (see SUMMARY_FULL.md) | |`);
557
+ }
558
+ // AI components with no connections
559
+ for (const c of aiComponents) {
560
+ const hasConn = connections.some((conn) => conn.to.component_id === c.component_id);
561
+ if (!hasConn) {
562
+ const configFile = c.source.config_files?.[0] || '—';
563
+ compressed.push(`| ${c.name} | ${configFile} | — | ${c.role.purpose} | \`components/${c.component_id}.json\` |`);
564
+ }
565
+ }
566
+ compressed.push('');
567
+ }
568
+ // Connections (top 10)
569
+ if (connections.length > 0) {
570
+ const maxConns = Math.min(connections.length, 10);
571
+ compressed.push(`## Connections (top 10 of ${connections.length})`);
572
+ for (let i = 0; i < maxConns; i++) {
573
+ const conn = connections[i];
574
+ const fromComp = components.find((c) => c.component_id === conn.from.component_id);
575
+ const toComp = components.find((c) => c.component_id === conn.to.component_id);
576
+ const file = conn.code_reference?.file || '';
577
+ const line = conn.code_reference?.line_start ? `:${conn.code_reference.line_start}` : '';
578
+ compressed.push(`- ${fromComp?.name || '?'} → ${toComp?.name || '?'} (${conn.connection_type}) ${file}${line}`);
579
+ }
580
+ compressed.push('');
581
+ }
582
+ // Add prompts pointer if available
583
+ if (promptScan && promptScan.prompts.length > 0) {
584
+ compressed.push(`## Prompts (${promptScan.prompts.length}) — full content: \`prompts.json\``);
585
+ compressed.push('| Name | File | Provider |');
586
+ compressed.push('|------|------|----------|');
587
+ const maxP = Math.min(promptScan.prompts.length, 10);
588
+ for (let i = 0; i < maxP; i++) {
589
+ const p = promptScan.prompts[i];
590
+ const name = p.name || '?';
591
+ const file = p.location?.file ? `${p.location.file}:${p.location.lineStart}` : '?';
592
+ const provider = p.provider?.provider || 'unknown';
593
+ compressed.push(`| ${name} | ${file} | ${provider} |`);
594
+ }
595
+ if (promptScan.prompts.length > 10) {
596
+ compressed.push(`| ... | +${promptScan.prompts.length - 10} more in prompts.json | |`);
597
+ }
598
+ compressed.push('');
599
+ }
600
+ compressed.push('## Detail Pointers');
601
+ compressed.push('- **Full summary**: `SUMMARY_FULL.md`');
602
+ compressed.push(`- Full index: \`index.json\``);
603
+ compressed.push(`- Connection graph: \`graph.json\``);
604
+ compressed.push(`- File map: \`file_map.json\``);
605
+ compressed.push(`- Prompts: \`prompts.json\` (${promptScan?.prompts?.length || 0} prompts, full content)`);
606
+ compressed.push(`- All components: \`components/\` (${components.length} files)`);
607
+ compressed.push(`- All connections: \`connections/\` (${connections.length} files)`);
608
+ compressed.push('');
609
+ const compressedContent = compressed.join('\n');
610
+ await fs.promises.writeFile(summaryPath, compressedContent, 'utf-8');
611
+ return compressedContent;
612
+ }
613
+ const content = lines.join('\n');
614
+ await fs.promises.writeFile(summaryPath, content, 'utf-8');
615
+ return content;
616
+ }
617
+ /**
618
+ * Load the graph
619
+ */
620
+ export async function loadGraph(config, projectRoot) {
621
+ const cfg = config || getConfig();
622
+ const graphPath = getGraphPath(cfg, projectRoot);
623
+ if (!fs.existsSync(graphPath)) {
624
+ return null;
625
+ }
626
+ try {
627
+ const content = await fs.promises.readFile(graphPath, 'utf-8');
628
+ return JSON.parse(content);
629
+ }
630
+ catch {
631
+ return null;
632
+ }
633
+ }
634
+ // =============================================================================
635
+ // SNAPSHOTS
636
+ // =============================================================================
637
+ /**
638
+ * Create a snapshot of current architecture
639
+ */
640
+ export async function createSnapshot(reason, config, projectRoot) {
641
+ const cfg = config || getConfig();
642
+ ensureStorageDirectories(cfg, projectRoot);
643
+ const components = await loadAllComponents(cfg, projectRoot);
644
+ const connections = await loadAllConnections(cfg, projectRoot);
645
+ const timestamp = Date.now();
646
+ const snapshotId = `SNAP_${new Date(timestamp).toISOString().replace(/[-:T.Z]/g, '').slice(0, 14)}`;
647
+ const snapshot = {
648
+ snapshot_id: snapshotId,
649
+ timestamp,
650
+ reason,
651
+ components: components.map((c) => ({
652
+ component_id: c.component_id,
653
+ name: c.name,
654
+ type: c.type,
655
+ version: c.version,
656
+ status: c.status,
657
+ })),
658
+ connections: connections.map((c) => ({
659
+ connection_id: c.connection_id,
660
+ from: c.from.component_id,
661
+ to: c.to.component_id,
662
+ type: c.connection_type,
663
+ })),
664
+ stats: {
665
+ total_components: components.length,
666
+ total_connections: connections.length,
667
+ },
668
+ };
669
+ const snapshotsPath = getSnapshotsPath(cfg, projectRoot);
670
+ const filePath = path.join(snapshotsPath, `${snapshotId}.json`);
671
+ await fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), 'utf-8');
672
+ return {
673
+ snapshot_id: snapshotId,
674
+ file_path: filePath,
675
+ };
676
+ }
677
+ // =============================================================================
678
+ // BULK OPERATIONS
679
+ // =============================================================================
680
+ /**
681
+ * Store multiple components at once (parallelized for efficiency)
682
+ */
683
+ export async function storeComponents(components, config, projectRoot) {
684
+ const cfg = config || getConfig();
685
+ ensureStorageDirectories(cfg, projectRoot);
686
+ const componentsPath = getComponentsPath(cfg, projectRoot);
687
+ // Parallelize writes in batches to avoid overwhelming the filesystem
688
+ const batchSize = 50;
689
+ for (let i = 0; i < components.length; i += batchSize) {
690
+ const batch = components.slice(i, i + batchSize);
691
+ await Promise.all(batch.map(async (component) => {
692
+ const filePath = path.join(componentsPath, `${component.component_id}.json`);
693
+ await fs.promises.writeFile(filePath, JSON.stringify(component, null, 2), 'utf-8');
694
+ }));
695
+ }
696
+ }
697
+ /**
698
+ * Store multiple connections at once (parallelized for efficiency)
699
+ */
700
+ export async function storeConnections(connections, config, projectRoot) {
701
+ const cfg = config || getConfig();
702
+ ensureStorageDirectories(cfg, projectRoot);
703
+ const connectionsPath = getConnectionsPath(cfg, projectRoot);
704
+ // Parallelize writes in batches to avoid overwhelming the filesystem
705
+ const batchSize = 50;
706
+ for (let i = 0; i < connections.length; i += batchSize) {
707
+ const batch = connections.slice(i, i + batchSize);
708
+ await Promise.all(batch.map(async (connection) => {
709
+ const filePath = path.join(connectionsPath, `${connection.connection_id}.json`);
710
+ await fs.promises.writeFile(filePath, JSON.stringify(connection, null, 2), 'utf-8');
711
+ }));
712
+ }
713
+ }
714
+ /**
715
+ * Clear all stored data (parallelized for efficiency)
716
+ */
717
+ export async function clearStorage(config, projectRoot) {
718
+ const cfg = config || getConfig();
719
+ const componentsPath = getComponentsPath(cfg, projectRoot);
720
+ const connectionsPath = getConnectionsPath(cfg, projectRoot);
721
+ const indexPath = getIndexPath(cfg, projectRoot);
722
+ const graphPath = getGraphPath(cfg, projectRoot);
723
+ const deletePromises = [];
724
+ // Delete all component files
725
+ if (fs.existsSync(componentsPath)) {
726
+ const componentFiles = await fs.promises.readdir(componentsPath);
727
+ deletePromises.push(...componentFiles.map((file) => fs.promises.unlink(path.join(componentsPath, file)).catch(() => { })));
728
+ }
729
+ // Delete all connection files
730
+ if (fs.existsSync(connectionsPath)) {
731
+ const connectionFiles = await fs.promises.readdir(connectionsPath);
732
+ deletePromises.push(...connectionFiles.map((file) => fs.promises.unlink(path.join(connectionsPath, file)).catch(() => { })));
733
+ }
734
+ // Delete index and graph
735
+ if (fs.existsSync(indexPath)) {
736
+ deletePromises.push(fs.promises.unlink(indexPath).catch(() => { }));
737
+ }
738
+ if (fs.existsSync(graphPath)) {
739
+ deletePromises.push(fs.promises.unlink(graphPath).catch(() => { }));
740
+ }
741
+ await Promise.all(deletePromises);
742
+ }
743
+ // =============================================================================
744
+ // STATISTICS
745
+ // =============================================================================
746
+ /**
747
+ * Get storage statistics
748
+ */
749
+ export async function getStorageStats(config, projectRoot) {
750
+ const cfg = config || getConfig();
751
+ const components = await loadAllComponents(cfg, projectRoot);
752
+ const connections = await loadAllConnections(cfg, projectRoot);
753
+ // Calculate disk usage
754
+ const componentsPath = getComponentsPath(cfg, projectRoot);
755
+ const connectionsPath = getConnectionsPath(cfg, projectRoot);
756
+ let diskUsage = 0;
757
+ if (fs.existsSync(componentsPath)) {
758
+ const files = await fs.promises.readdir(componentsPath);
759
+ for (const file of files) {
760
+ const stats = await fs.promises.stat(path.join(componentsPath, file));
761
+ diskUsage += stats.size;
762
+ }
763
+ }
764
+ if (fs.existsSync(connectionsPath)) {
765
+ const files = await fs.promises.readdir(connectionsPath);
766
+ for (const file of files) {
767
+ const stats = await fs.promises.stat(path.join(connectionsPath, file));
768
+ diskUsage += stats.size;
769
+ }
770
+ }
771
+ // Find oldest and newest timestamps
772
+ const allTimestamps = [
773
+ ...components.map((c) => c.timestamp),
774
+ ...connections.map((c) => c.timestamp),
775
+ ];
776
+ return {
777
+ total_components: components.length,
778
+ total_connections: connections.length,
779
+ disk_usage_kb: Math.round(diskUsage / 1024),
780
+ oldest_timestamp: allTimestamps.length > 0 ? Math.min(...allTimestamps) : null,
781
+ newest_timestamp: allTimestamps.length > 0 ? Math.max(...allTimestamps) : null,
782
+ };
783
+ }
784
+ // =============================================================================
785
+ // FILE HASH TRACKING
786
+ // =============================================================================
787
+ /**
788
+ * Compute SHA-256 hash of a file
789
+ */
790
+ export async function computeFileHash(filePath) {
791
+ const content = await fs.promises.readFile(filePath);
792
+ return crypto.createHash('sha256').update(content).digest('hex');
793
+ }
794
+ /**
795
+ * Compute hashes for multiple files (parallelized in batches for efficiency)
796
+ */
797
+ export async function computeFileHashes(files, projectRoot) {
798
+ const hashes = {};
799
+ const timestamp = Date.now();
800
+ // Process in batches to avoid too many open file handles
801
+ const batchSize = 100;
802
+ for (let i = 0; i < files.length; i += batchSize) {
803
+ const batch = files.slice(i, i + batchSize);
804
+ const results = await Promise.all(batch.map(async (file) => {
805
+ const filePath = path.join(projectRoot, file);
806
+ try {
807
+ const stats = await fs.promises.stat(filePath);
808
+ if (!stats.isFile())
809
+ return null;
810
+ const hash = await computeFileHash(filePath);
811
+ return {
812
+ file,
813
+ record: {
814
+ hash,
815
+ lastScanned: timestamp,
816
+ size: stats.size,
817
+ },
818
+ };
819
+ }
820
+ catch {
821
+ return null;
822
+ }
823
+ }));
824
+ for (const result of results) {
825
+ if (result) {
826
+ hashes[result.file] = result.record;
827
+ }
828
+ }
829
+ }
830
+ return hashes;
831
+ }
832
+ /**
833
+ * Save file hashes to disk
834
+ */
835
+ export async function saveHashes(hashes, config, projectRoot) {
836
+ const cfg = config || getConfig();
837
+ const root = projectRoot || process.cwd();
838
+ ensureStorageDirectories(cfg, root);
839
+ const navHashes = {
840
+ version: '1.0',
841
+ generatedAt: Date.now(),
842
+ projectPath: root,
843
+ files: hashes,
844
+ };
845
+ const hashesPath = getHashesPath(cfg, root);
846
+ await fs.promises.writeFile(hashesPath, JSON.stringify(navHashes, null, 2), 'utf-8');
847
+ }
848
+ /**
849
+ * Load file hashes from disk
850
+ */
851
+ export async function loadHashes(config, projectRoot) {
852
+ const cfg = config || getConfig();
853
+ const hashesPath = getHashesPath(cfg, projectRoot);
854
+ if (!fs.existsSync(hashesPath)) {
855
+ return null;
856
+ }
857
+ try {
858
+ const content = await fs.promises.readFile(hashesPath, 'utf-8');
859
+ return JSON.parse(content);
860
+ }
861
+ catch {
862
+ return null;
863
+ }
864
+ }
865
+ /**
866
+ * Detect which files have changed since last scan
867
+ */
868
+ export async function detectFileChanges(currentFiles, projectRoot, config) {
869
+ const cfg = config || getConfig();
870
+ const previousHashes = await loadHashes(cfg, projectRoot);
871
+ const result = {
872
+ added: [],
873
+ modified: [],
874
+ removed: [],
875
+ unchanged: [],
876
+ };
877
+ // No previous scan - all files are new
878
+ if (!previousHashes) {
879
+ result.added = [...currentFiles];
880
+ return result;
881
+ }
882
+ const previousFiles = new Set(Object.keys(previousHashes.files));
883
+ // Check current files
884
+ for (const file of currentFiles) {
885
+ const filePath = path.join(projectRoot, file);
886
+ if (!previousFiles.has(file)) {
887
+ // New file
888
+ result.added.push(file);
889
+ }
890
+ else {
891
+ // File existed before - check if modified
892
+ try {
893
+ const currentHash = await computeFileHash(filePath);
894
+ if (currentHash !== previousHashes.files[file].hash) {
895
+ result.modified.push(file);
896
+ }
897
+ else {
898
+ result.unchanged.push(file);
899
+ }
900
+ }
901
+ catch {
902
+ // Can't read file, treat as modified
903
+ result.modified.push(file);
904
+ }
905
+ previousFiles.delete(file);
906
+ }
907
+ }
908
+ // Remaining files in previousFiles were removed
909
+ result.removed = Array.from(previousFiles);
910
+ return result;
911
+ }
912
+ /**
913
+ * Get a summary of file changes for display
914
+ */
915
+ export function formatFileChangeSummary(changes) {
916
+ const parts = [];
917
+ if (changes.added.length > 0) {
918
+ parts.push(`${changes.added.length} added`);
919
+ }
920
+ if (changes.modified.length > 0) {
921
+ parts.push(`${changes.modified.length} modified`);
922
+ }
923
+ if (changes.removed.length > 0) {
924
+ parts.push(`${changes.removed.length} removed`);
925
+ }
926
+ if (parts.length === 0) {
927
+ return 'No files changed';
928
+ }
929
+ return parts.join(', ');
930
+ }
931
+ //# sourceMappingURL=storage.js.map