arcvision 0.2.28 → 0.2.29

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.
@@ -71,19 +71,21 @@ class CallResolver {
71
71
  // Better: SymbolIndexer ensure 'default' is the name for default exports.
72
72
  }
73
73
 
74
- // Additional attempts for common patterns
75
- // Attempt 3: Try with common function prefixes
76
- const commonPatterns = [
77
- `${targetFileId}::use${targetSymbolName.charAt(0).toUpperCase() + targetSymbolName.slice(1)}`,
78
- `${targetFileId}::get${targetSymbolName.charAt(0).toUpperCase() + targetSymbolName.slice(1)}`,
79
- `${targetFileId}::create${targetSymbolName.charAt(0).toUpperCase() + targetSymbolName.slice(1)}`,
80
- `${targetFileId}::_${targetSymbolName}`, // underscore prefix
81
- `${targetFileId}::${targetSymbolName}Impl`, // implementation suffix
82
- `${targetFileId}::${targetSymbolName}Handler` // handler suffix
83
- ];
84
-
85
- for (const patternId of commonPatterns) {
86
- if (this.symbolIndex.has(patternId)) return patternId;
74
+ // Additional attempts for common function-prefix patterns
75
+ // Guard: skip if targetSymbolName is falsy (e.g. undefined from a star import)
76
+ if (targetSymbolName && typeof targetSymbolName === 'string' && targetSymbolName.length > 0) {
77
+ const cap = targetSymbolName.charAt(0).toUpperCase() + targetSymbolName.slice(1);
78
+ const commonPatterns = [
79
+ `${targetFileId}::use${cap}`,
80
+ `${targetFileId}::get${cap}`,
81
+ `${targetFileId}::create${cap}`,
82
+ `${targetFileId}::_${targetSymbolName}`,
83
+ `${targetFileId}::${targetSymbolName}Impl`,
84
+ `${targetFileId}::${targetSymbolName}Handler`
85
+ ];
86
+ for (const patternId of commonPatterns) {
87
+ if (this.symbolIndex.has(patternId)) return patternId;
88
+ }
87
89
  }
88
90
  }
89
91
  }
@@ -97,17 +99,20 @@ class CallResolver {
97
99
  }
98
100
 
99
101
  // Additional local patterns
100
- const localPatterns = [
101
- `${fileId}::use${symbolName.charAt(0).toUpperCase() + symbolName.slice(1)}`,
102
- `${fileId}::get${symbolName.charAt(0).toUpperCase() + symbolName.slice(1)}`,
103
- `${fileId}::create${symbolName.charAt(0).toUpperCase() + symbolName.slice(1)}`,
104
- `${fileId}::_${symbolName}`, // underscore prefix
105
- `${fileId}::${symbolName}Impl`, // implementation suffix
106
- `${fileId}::${symbolName}Handler` // handler suffix
107
- ];
108
-
109
- for (const patternId of localPatterns) {
110
- if (this.symbolIndex.has(patternId)) return patternId;
102
+ // Guard: skip if symbolName is falsy
103
+ if (symbolName && typeof symbolName === 'string' && symbolName.length > 0) {
104
+ const cap = symbolName.charAt(0).toUpperCase() + symbolName.slice(1);
105
+ const localPatterns = [
106
+ `${fileId}::use${cap}`,
107
+ `${fileId}::get${cap}`,
108
+ `${fileId}::create${cap}`,
109
+ `${fileId}::_${symbolName}`,
110
+ `${fileId}::${symbolName}Impl`,
111
+ `${fileId}::${symbolName}Handler`
112
+ ];
113
+ for (const patternId of localPatterns) {
114
+ if (this.symbolIndex.has(patternId)) return patternId;
115
+ }
111
116
  }
112
117
 
113
118
  return null;
@@ -2,6 +2,7 @@ const path = require('path');
2
2
  const { spawnSync } = require('child_process');
3
3
  const crypto = require('crypto');
4
4
  const { stableId } = require('../engine/id-generator');
5
+ const { emptyIntelligenceBlock } = require('../engine/pass5_intelligence');
5
6
 
6
7
  /**
7
8
  * Build the context object that conforms to the Arcvision schema
@@ -40,7 +41,7 @@ function buildContext(fileNodes, edges, symbols, options = {}) {
40
41
  if (file.intelligence && file.intelligence.connections) {
41
42
  file.intelligence.connections.forEach(c => addDep(c.target));
42
43
  }
43
-
44
+
44
45
  // Add dependencies from metadata if intelligence is not available
45
46
  if (!file.intelligence && file.metadata && file.metadata.imports) {
46
47
  file.metadata.imports.forEach(imp => {
@@ -191,14 +192,17 @@ function buildContext(fileNodes, edges, symbols, options = {}) {
191
192
  architectural_boundaries: options.architecturalBoundaries || {},
192
193
  structural_invariants: options.structuralInvariants || [],
193
194
  // Include detected invariants from the current scan - these should conform to the schema specification
194
- invariants: Array.isArray(options.autoDetectedInvariants) && options.autoDetectedInvariants.length > 0 ? options.autoDetectedInvariants :
195
- Array.isArray(options.detectedInvariants) && options.detectedInvariants.length > 0 ? options.detectedInvariants :
196
- [],
195
+ invariants: Array.isArray(options.autoDetectedInvariants) && options.autoDetectedInvariants.length > 0 ? options.autoDetectedInvariants :
196
+ Array.isArray(options.detectedInvariants) && options.detectedInvariants.length > 0 ? options.detectedInvariants :
197
+ [],
197
198
  // Include invariant analysis results
198
199
  invariant_analysis: options.invariantAnalysis || null,
199
200
  // Include architectural health assessment
200
201
  architectural_health: options.architecturalHealth || null,
201
- authoritative_context: options.authoritativeContext || null
202
+ authoritative_context: options.authoritativeContext || null,
203
+ // Intelligence block — always populated, never null
204
+ // This is the canonical architecture intelligence layer from Pass 5.
205
+ intelligence: options.intelligence || emptyIntelligenceBlock()
202
206
  };
203
207
 
204
208
  // Phase 2 requirement: Artifact Hash (Trust Anchor)
@@ -206,7 +210,7 @@ function buildContext(fileNodes, edges, symbols, options = {}) {
206
210
  const contextWithoutIntegrity = { ...context };
207
211
  delete contextWithoutIntegrity.integrity;
208
212
  const hash = crypto.createHash('sha256').update(JSON.stringify(contextWithoutIntegrity)).digest('hex');
209
-
213
+
210
214
  // Now add the integrity field
211
215
  context.integrity = {
212
216
  sha256: hash
@@ -226,7 +230,7 @@ function getGitInfo() {
226
230
  const originResult = spawnSync('git', ['remote', 'get-url', 'origin'], {
227
231
  stdio: ['pipe', 'pipe', 'pipe']
228
232
  });
229
-
233
+
230
234
  let repoUrl = 'unknown';
231
235
  if (originResult.status === 0) {
232
236
  let url = originResult.stdout.toString().trim();
@@ -239,17 +243,17 @@ function getGitInfo() {
239
243
  }
240
244
  repoUrl = url;
241
245
  }
242
-
246
+
243
247
  // Get the current commit hash
244
248
  const commitResult = spawnSync('git', ['rev-parse', 'HEAD'], {
245
249
  stdio: ['pipe', 'pipe', 'pipe']
246
250
  });
247
-
251
+
248
252
  let commitHash = 'unknown';
249
253
  if (commitResult.status === 0) {
250
254
  commitHash = commitResult.stdout.toString().trim();
251
255
  }
252
-
256
+
253
257
  return {
254
258
  repo: repoUrl,
255
259
  commit: commitHash
@@ -274,7 +278,7 @@ function calculateIntegrityHash(obj) {
274
278
  if (objCopy.integrity) {
275
279
  delete objCopy.integrity;
276
280
  }
277
-
281
+
278
282
  const jsonString = JSON.stringify(objCopy);
279
283
  return crypto.createHash('sha256').update(jsonString).digest('hex');
280
284
  } catch (error) {
@@ -323,35 +327,35 @@ function generateStructuralLayers() {
323
327
  function generateAdaptiveStructuralLayers(directory) {
324
328
  const fs = require('fs');
325
329
  const path = require('path');
326
-
330
+
327
331
  // Analyze project characteristics to determine adaptive layers
328
332
  const hasPackageJson = fs.existsSync(path.join(directory, 'package.json'));
329
- const hasConfigFiles = fs.existsSync(path.join(directory, 'tsconfig.json')) ||
330
- fs.existsSync(path.join(directory, 'config')) ||
331
- fs.existsSync(path.join(directory, 'webpack.config.js'));
333
+ const hasConfigFiles = fs.existsSync(path.join(directory, 'tsconfig.json')) ||
334
+ fs.existsSync(path.join(directory, 'config')) ||
335
+ fs.existsSync(path.join(directory, 'webpack.config.js'));
332
336
  const hasSrcDir = fs.existsSync(path.join(directory, 'src'));
333
337
  const hasTestDir = fs.existsSync(path.join(directory, 'test')) || fs.existsSync(path.join(directory, 'tests'));
334
338
  const hasDocsDir = fs.existsSync(path.join(directory, 'docs'));
335
-
339
+
336
340
  // Count different file types to determine project size and type
337
341
  let totalFiles = 0;
338
342
  let jsFiles = 0;
339
343
  let tsFiles = 0;
340
344
  let luaFiles = 0;
341
345
  let configFiles = 0;
342
-
346
+
343
347
  function walk(dir, depth = 0) {
344
348
  if (depth > 3) return; // Limit depth to avoid performance issues
345
-
349
+
346
350
  if (!fs.existsSync(dir)) return;
347
-
351
+
348
352
  const items = fs.readdirSync(dir, { withFileTypes: true });
349
-
353
+
350
354
  for (const item of items) {
351
355
  const fullPath = path.join(dir, item.name);
352
-
356
+
353
357
  if (item.isDirectory()) {
354
- if (!item.name.startsWith('.') && item.name !== 'node_modules' &&
358
+ if (!item.name.startsWith('.') && item.name !== 'node_modules' &&
355
359
  item.name !== 'dist' && item.name !== 'build' && item.name !== '.git') {
356
360
  walk(fullPath, depth + 1);
357
361
  }
@@ -360,20 +364,20 @@ function generateAdaptiveStructuralLayers(directory) {
360
364
  if (item.name.endsWith('.js')) jsFiles++;
361
365
  if (item.name.endsWith('.ts')) tsFiles++;
362
366
  if (item.name.endsWith('.lua')) luaFiles++;
363
- if (['.json', '.yaml', '.yml', '.toml', '.config.js', '.config.ts', '.rc'].some(ext =>
367
+ if (['.json', '.yaml', '.yml', '.toml', '.config.js', '.config.ts', '.rc'].some(ext =>
364
368
  item.name.endsWith(ext))) configFiles++;
365
369
  }
366
370
  }
367
371
  }
368
-
372
+
369
373
  walk(directory);
370
-
374
+
371
375
  // Determine if this is a Lua-heavy project (like BullMQ)
372
376
  const isLuaProject = luaFiles > 0 && (luaFiles / totalFiles) > 0.1; // More than 10% Lua files
373
-
377
+
374
378
  // Determine project size
375
379
  const isLargeProject = totalFiles > 1000;
376
-
380
+
377
381
  // Adjust structural layers based on project characteristics
378
382
  const layers = {
379
383
  "runtime_code": {
@@ -413,7 +417,7 @@ function generateAdaptiveStructuralLayers(directory) {
413
417
  "description": "Static assets with no effect on execution semantics"
414
418
  }
415
419
  };
416
-
420
+
417
421
  // Add project-specific metadata
418
422
  layers.project_metadata = {
419
423
  "size_category": isLargeProject ? "large" : (totalFiles > 100 ? "medium" : "small"),
@@ -429,7 +433,7 @@ function generateAdaptiveStructuralLayers(directory) {
429
433
  "has_tests": hasTestDir,
430
434
  "is_lua_intensive": isLuaProject
431
435
  };
432
-
436
+
433
437
  return layers;
434
438
  }
435
439
 
@@ -441,13 +445,13 @@ function generateAdaptiveStructuralLayers(directory) {
441
445
  function generateProjectEnvelope(directory) {
442
446
  const fs = require('fs');
443
447
  const path = require('path');
444
-
448
+
445
449
  const envelope = {
446
450
  configuration_files: [],
447
451
  documentation: [],
448
452
  build_tools: []
449
453
  };
450
-
454
+
451
455
  // Common configuration files
452
456
  const configFiles = [
453
457
  'package.json',
@@ -497,7 +501,7 @@ function generateProjectEnvelope(directory) {
497
501
  'remix.config.js',
498
502
  'remix.config.ts'
499
503
  ];
500
-
504
+
501
505
  // Common documentation files
502
506
  const docFiles = [
503
507
  'README.md',
@@ -521,7 +525,7 @@ function generateProjectEnvelope(directory) {
521
525
  'CITATION.cff',
522
526
  'CITATION.bib'
523
527
  ];
524
-
528
+
525
529
  // Common build tool files
526
530
  const buildToolFiles = [
527
531
  'typedoc.config.cjs',
@@ -537,18 +541,18 @@ function generateProjectEnvelope(directory) {
537
541
  'compodoc.json',
538
542
  'documentation.yml'
539
543
  ];
540
-
544
+
541
545
  // Function to find files recursively
542
546
  function findFilesRecursively(dir, fileNames, results, maxDepth = 3, currentDepth = 0) {
543
547
  if (currentDepth > maxDepth) return;
544
-
548
+
545
549
  if (!fs.existsSync(dir)) return;
546
-
550
+
547
551
  const items = fs.readdirSync(dir, { withFileTypes: true });
548
-
552
+
549
553
  for (const item of items) {
550
554
  const fullPath = path.join(dir, item.name);
551
-
555
+
552
556
  if (item.isDirectory()) {
553
557
  if (!item.name.startsWith('.') && item.name !== 'node_modules' && item.name !== 'dist' && item.name !== 'build') {
554
558
  findFilesRecursively(fullPath, fileNames, results, maxDepth, currentDepth + 1);
@@ -564,53 +568,53 @@ function generateProjectEnvelope(directory) {
564
568
  }
565
569
  }
566
570
  }
567
-
571
+
568
572
  // Find all configuration files
569
573
  const foundConfigFiles = [];
570
574
  findFilesRecursively(directory, configFiles, foundConfigFiles);
571
-
575
+
572
576
  foundConfigFiles.forEach(fileObj => {
573
577
  envelope.configuration_files.push({
574
578
  path: fileObj.path,
575
579
  role: getRoleForConfigFile(fileObj.name)
576
580
  });
577
581
  });
578
-
582
+
579
583
  // Find all documentation files
580
584
  const foundDocFiles = [];
581
585
  findFilesRecursively(directory, docFiles, foundDocFiles);
582
-
586
+
583
587
  foundDocFiles.forEach(fileObj => {
584
588
  envelope.documentation.push({
585
589
  path: fileObj.path,
586
590
  role: getRoleForDocFile(fileObj.name)
587
591
  });
588
592
  });
589
-
593
+
590
594
  // Find all build tool files
591
595
  const foundBuildToolFiles = [];
592
596
  findFilesRecursively(directory, buildToolFiles, foundBuildToolFiles);
593
-
597
+
594
598
  foundBuildToolFiles.forEach(fileObj => {
595
599
  envelope.build_tools.push({
596
600
  path: fileObj.path,
597
601
  role: getRoleForBuildToolFile(fileObj.name)
598
602
  });
599
603
  });
600
-
604
+
601
605
  // Remove duplicates based on path
602
606
  envelope.configuration_files = envelope.configuration_files.filter((file, index, self) =>
603
607
  index === self.findIndex(f => f.path === file.path)
604
608
  );
605
-
609
+
606
610
  envelope.documentation = envelope.documentation.filter((file, index, self) =>
607
611
  index === self.findIndex(f => f.path === file.path)
608
612
  );
609
-
613
+
610
614
  envelope.build_tools = envelope.build_tools.filter((file, index, self) =>
611
615
  index === self.findIndex(f => f.path === file.path)
612
616
  );
613
-
617
+
614
618
  return envelope;
615
619
  }
616
620
 
@@ -31,7 +31,7 @@ class InvariantDetector {
31
31
 
32
32
  console.log('\n🔍 Starting Invariant Detection Analysis');
33
33
  console.log(` Analyzing ${scanResult.nodes.length} nodes and ${scanResult.edges.length} relationships`);
34
-
34
+
35
35
  // Analyze the structural context for potential invariants
36
36
  await this.analyzeNodesForInvariants(scanResult.nodes, directory);
37
37
  await this.analyzeEdgesForInvariants(scanResult.edges);
@@ -49,7 +49,7 @@ class InvariantDetector {
49
49
 
50
50
  console.log(`\n🛡️ Invariant Detection Complete`);
51
51
  console.log(` Found ${this.detectedInvariants.length} potential system invariants`);
52
-
52
+
53
53
  // Log summary of invariant types
54
54
  const typeCounts = {};
55
55
  this.detectedInvariants.forEach(inv => {
@@ -344,7 +344,7 @@ class InvariantDetector {
344
344
  { pattern: /range.*check/gi, description: 'Range check', critical: true },
345
345
  { pattern: /within.*bounds/gi, description: 'Bounds check', critical: true },
346
346
  { pattern: /validat(e|ion)/gi, description: 'Validation pattern', critical: false },
347
- { pattern: /guard/i, description: 'Guard pattern', critical: true },
347
+ { pattern: /guard/gi, description: 'Guard pattern', critical: true },
348
348
 
349
349
  // Access control and security patterns
350
350
  { pattern: /access.*control/gi, description: 'Access control', critical: true },
@@ -429,15 +429,27 @@ class InvariantDetector {
429
429
  */
430
430
  async findInvariantRelatedFiles(directory) {
431
431
  const invariantFiles = [];
432
+ const MAX_FILES = 200; // Cap to avoid OOM on giant monorepos
433
+
434
+ // Directories to skip — mirrors the Pass 1 exclusion list
435
+ const SKIP_DIRS = new Set([
436
+ 'node_modules', '.git', 'dist', 'build', '.next', '.turbo',
437
+ 'coverage', '.cache', '__pycache__', 'vendor', 'out',
438
+ 'target', '.yarn', '.pnp', 'arcvision_context'
439
+ ]);
432
440
 
433
441
  const walk = async (dir) => {
442
+ if (invariantFiles.length >= MAX_FILES) return; // Early exit if cap reached
434
443
  try {
435
444
  const entries = await fs.readdir(dir, { withFileTypes: true });
436
445
 
437
446
  for (const entry of entries) {
447
+ if (invariantFiles.length >= MAX_FILES) return;
438
448
  const fullPath = path.join(dir, entry.name);
439
449
 
440
450
  if (entry.isDirectory()) {
451
+ // Skip heavy or irrelevant directories
452
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
441
453
  await walk(fullPath);
442
454
  } else if (entry.isFile()) {
443
455
  const fileName = entry.name.toLowerCase();
@@ -11,7 +11,7 @@ function getCommitHash() {
11
11
  const result = spawnSync('git', ['rev-parse', 'HEAD'], {
12
12
  stdio: ['pipe', 'pipe', 'pipe']
13
13
  });
14
-
14
+
15
15
  if (result.status === 0) {
16
16
  return result.stdout.toString().trim();
17
17
  }
@@ -27,9 +27,10 @@ function getCommitHash() {
27
27
  * @param {string} timestamp - Generation timestamp
28
28
  * @param {string} toolVersion - ArcVision tool version
29
29
  * @param {Object} blastRadiusData - Blast radius analysis data
30
+ * @param {Object} intelligenceData - Architecture intelligence block from Pass 5
30
31
  * @returns {string} Formatted README content
31
32
  */
32
- function generateReadmeContent(commitHash, timestamp, toolVersion, blastRadiusData = null) {
33
+ function generateReadmeContent(commitHash, timestamp, toolVersion, blastRadiusData = null, intelligenceData = null) {
33
34
  let content = `# ArcVision System Context Artifact
34
35
 
35
36
 
@@ -89,7 +90,7 @@ If this artifact conflicts with human memory, **trust the artifact**.`;
89
90
  The following files have the highest blast radius and represent critical structural hubs in the system:
90
91
 
91
92
  `;
92
-
93
+
93
94
  blastRadiusData.topFiles.forEach((item, index) => {
94
95
  let warningMessage = '';
95
96
  if (index === 0) {
@@ -99,7 +100,7 @@ The following files have the highest blast radius and represent critical structu
99
100
  } else {
100
101
  warningMessage = 'Modifications can cause widespread inconsistencies.';
101
102
  }
102
-
103
+
103
104
  content += `- **${item.file}**
104
105
  - Blast Radius: ${item.blastRadius} files (${item.percentOfGraph}% of codebase)
105
106
  - Risk: ${warningMessage}
@@ -113,7 +114,7 @@ The following files have the highest blast radius and represent critical structu
113
114
 
114
115
  No high-structure files detected based on import dependencies.`;
115
116
  }
116
-
117
+
117
118
  content += `
118
119
 
119
120
  ## How to Use With AI
@@ -141,7 +142,58 @@ All explanations, decisions, and AI reasoning should reference it.
141
142
 
142
143
  Some execution script invocations are dynamically assembled at runtime and may not be statically traceable; such scripts are included
143
144
  as execution boundaries without guaranteed call-site resolution`;
144
-
145
+
146
+ // Add architecture intelligence summary if available
147
+ if (intelligenceData) {
148
+ const intel = intelligenceData;
149
+ const centrality = intel.centrality || {};
150
+ const hotspots = intel.hotspots || {};
151
+ const risk = intel.risk_assessment || {};
152
+ const recs = intel.recommendations || [];
153
+ const fragility = intel.fragility || {};
154
+ const path = require('path');
155
+
156
+ content += `
157
+
158
+ ## Architecture Intelligence Summary
159
+
160
+ _Generated by ArcVision — derived from structural graph analysis._
161
+
162
+ `;
163
+ if (centrality.most_central) {
164
+ const top = centrality.top_files && centrality.top_files[0];
165
+ const score = top ? ` (centrality score: ${top.score})` : '';
166
+ content += `- **Most Central Module**: \`${centrality.most_central}\`${score}\n`;
167
+ }
168
+
169
+ const clusters = hotspots.clusters || [];
170
+ const topCluster = clusters.find(c => c.cycle_count > 0) || clusters[0];
171
+ if (topCluster && topCluster.files && topCluster.files.length > 0) {
172
+ const names = topCluster.files.slice(0, 3).map(f => path.basename(f)).join(' + ');
173
+ const cycleInfo = topCluster.cycle_count > 0 ? ` (${topCluster.cycle_count} cycle(s))` : '';
174
+ content += `- **Coupling Hotspot**: ${names}${cycleInfo}\n`;
175
+ } else {
176
+ content += `- **Coupling Hotspot**: None detected\n`;
177
+ }
178
+
179
+ if (risk.risk_score !== undefined) {
180
+ content += `- **Risk Score**: ${risk.risk_score.toFixed(1)} / 10 [${risk.risk_level}]\n`;
181
+ }
182
+
183
+ if (intel.entropy) {
184
+ content += `- **Architectural Entropy**: ${intel.entropy.score} [${intel.entropy.level.toUpperCase()}]\n`;
185
+ }
186
+
187
+ if (fragility.most_fragile) {
188
+ const frag = fragility.most_fragile;
189
+ content += `- **Most Fragile Module**: \`${path.basename(frag.file)}\` — fragility score: ${frag.score}\n`;
190
+ }
191
+
192
+ if (recs.length > 0) {
193
+ content += `- **Top Recommendation**: ${recs[0]}\n`;
194
+ }
195
+ }
196
+
145
197
  return content;
146
198
  }
147
199
 
@@ -150,16 +202,17 @@ as execution boundaries without guaranteed call-site resolution`;
150
202
  * @param {string} outputDir - Directory to save the README
151
203
  * @param {string} toolVersion - ArcVision tool version
152
204
  * @param {Object} blastRadiusData - Blast radius analysis data
205
+ * @param {Object} intelligenceData - Architecture intelligence block from Pass 5
153
206
  */
154
- function generateReadme(outputDir, toolVersion, blastRadiusData = null) {
207
+ function generateReadme(outputDir, toolVersion, blastRadiusData = null, intelligenceData = null) {
155
208
  const commitHash = getCommitHash();
156
209
  const timestamp = new Date().toISOString();
157
- const readmeContent = generateReadmeContent(commitHash, timestamp, toolVersion, blastRadiusData);
158
-
210
+ const readmeContent = generateReadmeContent(commitHash, timestamp, toolVersion, blastRadiusData, intelligenceData);
211
+
159
212
  const readmePath = path.join(outputDir, 'README.md');
160
213
  fs.writeFileSync(readmePath, readmeContent);
161
-
162
- console.log(`✅ System context README generated at ${readmePath}`);
214
+
215
+ console.log(`\u2705 README generated at ${readmePath}`);
163
216
  }
164
217
 
165
218
  module.exports = { generateReadme, generateReadmeContent, getCommitHash };
@@ -7,6 +7,7 @@ const { executePass1 } = require('../engine/pass1_facts');
7
7
  const { executePass2 } = require('../engine/pass2_semantics');
8
8
  const { executePass3 } = require('../engine/pass3_lifter');
9
9
  const { executePass4 } = require('../engine/pass4_signals');
10
+ const { executePass5 } = require('../engine/pass5_intelligence');
10
11
 
11
12
  // Import authority core detection
12
13
  const { detectAuthorityCores, detectHiddenCoupling, detectArchitecturalArchetype } = require('./authority-core-detector');
@@ -64,6 +65,11 @@ async function scan(directory) {
64
65
 
65
66
  const { nodes, edges, contextSurface, symbols } = structuralContext;
66
67
 
68
+ // --- PASS 5: INTELLIGENCE SYNTHESIS ---
69
+ // Interprets the graph already computed — no new file scanning.
70
+ // Computes centrality, hotspots, fragility, boundary violations, risk, and recommendations.
71
+ const { intelligence } = executePass5({ nodes, edges });
72
+
67
73
  console.log(`\n🔍 Analysis Complete (${Date.now() - start}ms)`);
68
74
  console.log(` Nodes: ${nodes.length}`);
69
75
  console.log(` Edges: ${edges.length}`);
@@ -181,7 +187,7 @@ async function scan(directory) {
181
187
  existingIds.add(existingInv.id);
182
188
  }
183
189
  }
184
-
190
+
185
191
  // Ensure all invariants have statement fields as fallback
186
192
  console.log(`🔍 Ensuring all ${allInvariants.length} invariants have statement fields...`);
187
193
  for (let i = 0; i < allInvariants.length; i++) {
@@ -221,7 +227,7 @@ async function scan(directory) {
221
227
  // Synthesize failure modes
222
228
  let failure_modes = [];
223
229
  try {
224
- console.log('🔍 Synthesizing failure modes for ${allInvariants.length} invariants...');
230
+ console.log(`🔍 Synthesizing failure modes for ${allInvariants.length} invariants...`);
225
231
  failure_modes = failureModeSynthesizer.synthesizeFailureModes(
226
232
  { nodes, edges },
227
233
  allInvariants
@@ -359,7 +365,9 @@ async function scan(directory) {
359
365
  invariants: allInvariants,
360
366
  ownership,
361
367
  failure_modes,
362
- decision_guidance
368
+ decision_guidance,
369
+ // Intelligence block from Pass 5
370
+ intelligence
363
371
  };
364
372
 
365
373
  // Explicitly set archetype to ensure it's a proper object
@@ -423,7 +431,7 @@ async function scan(directory) {
423
431
  try {
424
432
  fs.writeFileSync(contextFilePath, JSON.stringify(sortedContext, null, 2));
425
433
  console.log('✅ Context file written successfully');
426
-
434
+
427
435
  // Verify file was written
428
436
  if (fs.existsSync(contextFilePath)) {
429
437
  const stats = fs.statSync(contextFilePath);