arcvision 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ArcVision
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -4587,7 +4587,10 @@ var require_tsconfig_utils = __commonJS({
4587
4587
  "src/core/tsconfig-utils.js"(exports2, module2) {
4588
4588
  var fs2 = require("fs");
4589
4589
  var path2 = require("path");
4590
+ var configCache = /* @__PURE__ */ new Map();
4590
4591
  function loadTSConfig(startDir) {
4592
+ if (configCache.has(startDir))
4593
+ return configCache.get(startDir);
4591
4594
  let currentDir = startDir;
4592
4595
  const root = path2.parse(currentDir).root;
4593
4596
  while (currentDir) {
@@ -4605,17 +4608,18 @@ var require_tsconfig_utils = __commonJS({
4605
4608
  try {
4606
4609
  parsed = JSON.parse(content);
4607
4610
  } catch (e) {
4608
- const stripped = content.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1").replace(/,\s*([\]}])/g, "$1");
4611
+ let stripped = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/([^\n\r\"\']|^[^\"\']*)\/\/.*$/gm, "$1").replace(/,\s*(\}|\])/g, "$1").replace(/^[\uFEFF\u200B]+|[\uFEFF\u200B]+$/g, "").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, "");
4609
4612
  parsed = JSON.parse(stripped);
4610
4613
  }
4611
4614
  if (parsed) {
4612
- return {
4615
+ const result = {
4613
4616
  options: parsed.compilerOptions || {},
4614
4617
  configDir: currentDir
4615
4618
  };
4619
+ configCache.set(startDir, result);
4620
+ return result;
4616
4621
  }
4617
4622
  } catch (error) {
4618
- console.warn(`Warning: Could not parse ${tsconfigPath}:`, error.message);
4619
4623
  }
4620
4624
  }
4621
4625
  }
@@ -4623,7 +4627,9 @@ var require_tsconfig_utils = __commonJS({
4623
4627
  break;
4624
4628
  currentDir = path2.dirname(currentDir);
4625
4629
  }
4626
- return { options: null, configDir: startDir };
4630
+ const fallback = { options: null, configDir: startDir };
4631
+ configCache.set(startDir, fallback);
4632
+ return fallback;
4627
4633
  }
4628
4634
  module2.exports = { loadTSConfig };
4629
4635
  }
@@ -56393,6 +56399,8 @@ var require_parser = __commonJS({
56393
56399
  allowImportExportEverywhere: true,
56394
56400
  allowReturnOutsideFunction: true,
56395
56401
  allowSuperOutsideMethod: true,
56402
+ errorRecovery: true,
56403
+ // Allow parser to continue after errors
56396
56404
  plugins: [
56397
56405
  "jsx",
56398
56406
  "typescript",
@@ -56427,14 +56435,24 @@ var require_parser = __commonJS({
56427
56435
  try {
56428
56436
  ast = parser.parse(content, parserOptions);
56429
56437
  } catch (error) {
56430
- console.warn(`\u26A0\uFE0F Failed to parse ${filePath}: ${error.message}`);
56431
- return {
56432
- id: filePath,
56433
- imports: [],
56434
- exports: [],
56435
- functions: [],
56436
- apiCalls: []
56438
+ const fallbackParserOptions = {
56439
+ ...parserOptions,
56440
+ // Allow more flexible parsing
56441
+ errorRecovery: true
56442
+ // Allow parser to continue after errors
56437
56443
  };
56444
+ try {
56445
+ ast = parser.parse(content, fallbackParserOptions);
56446
+ } catch (fallbackError) {
56447
+ console.warn(`\u26A0\uFE0F Failed to parse ${filePath}: ${error.message}`);
56448
+ return {
56449
+ id: filePath,
56450
+ imports: [],
56451
+ exports: [],
56452
+ functions: [],
56453
+ apiCalls: []
56454
+ };
56455
+ }
56438
56456
  }
56439
56457
  const metadata = {
56440
56458
  id: filePath,
@@ -57510,65 +57528,68 @@ var require_pass1_facts = __commonJS({
57510
57528
  ...scanOptions,
57511
57529
  ignore: [...scanOptions.ignore, "**/*.d.ts", "**/.next/**", "**/coverage/**", "**/arcvision.context.json"]
57512
57530
  });
57531
+ const CONCURRENCY = 100;
57513
57532
  const rawNodes = [];
57514
57533
  let totalFacts = 0;
57515
- for (const file of files) {
57516
- try {
57517
- const relativePath = path2.relative(directory, file);
57518
- const normalizedRelativePath = normalize(relativePath);
57519
- let metadata = {};
57520
- if (file.endsWith(".json")) {
57521
- try {
57534
+ for (let i = 0; i < files.length; i += CONCURRENCY) {
57535
+ const batch = files.slice(i, i + CONCURRENCY);
57536
+ const promises = batch.map(async (file) => {
57537
+ try {
57538
+ const relativePath = path2.relative(directory, file);
57539
+ const normalizedRelativePath = normalize(relativePath);
57540
+ let metadata = {};
57541
+ if (file.endsWith(".json")) {
57522
57542
  const content = fs2.readFileSync(file, "utf-8");
57523
- JSON.parse(content);
57524
57543
  metadata = {
57525
57544
  id: file,
57526
57545
  isJson: true,
57527
- // JSONs might have "imports" in semantic sense (like package.json), but raw syntax is just data
57528
57546
  raw: content
57529
57547
  };
57530
- } catch (e) {
57531
- console.warn(`\u26A0\uFE0F Invalid JSON in ${normalizedRelativePath}: ${e.message}`);
57532
- continue;
57533
- }
57534
- } else {
57535
- metadata = parser.parseFile(file);
57536
- }
57537
- metadata = await pluginManager.processFile(file, metadata);
57538
- const node = {
57539
- id: normalizedRelativePath,
57540
- // Unique ID for this pass
57541
- filePath: file,
57542
- type: "file",
57543
- // The "facts" are the metadata properties
57544
- facts: {
57545
- imports: metadata.imports || [],
57546
- exports: metadata.exports || [],
57547
- functions: metadata.functions || [],
57548
- classes: metadata.classes || [],
57549
- types: metadata.types || [],
57550
- calls: {
57551
- functions: metadata.functionCalls || [],
57552
- methods: metadata.methodCalls || [],
57553
- constructors: metadata.constructorCalls || []
57554
- },
57555
- typeAnalysis: {
57556
- typeImports: metadata.typeImports || [],
57557
- interfaceDeps: metadata.interfaceDependencies || [],
57558
- genericDeps: metadata.genericDependencies || []
57559
- },
57560
- di: {
57561
- injections: metadata.constructorInjections || [],
57562
- hooks: metadata.hookDependencies || [],
57563
- context: metadata.contextUsages || []
57564
- },
57565
- react: metadata.componentUsage || []
57566
- }
57567
- };
57568
- totalFacts += (metadata.imports?.length || 0) + (metadata.functionCalls?.length || 0);
57569
- rawNodes.push(node);
57570
- } catch (e) {
57571
- console.warn(`\u26A0\uFE0F Pass 1 failed for ${file}: ${e.message}`);
57548
+ } else {
57549
+ metadata = parser.parseFile(file);
57550
+ }
57551
+ metadata = await pluginManager.processFile(file, metadata);
57552
+ const node = {
57553
+ id: normalizedRelativePath,
57554
+ filePath: file,
57555
+ type: "file",
57556
+ facts: {
57557
+ imports: metadata.imports || [],
57558
+ exports: metadata.exports || [],
57559
+ functions: metadata.functions || [],
57560
+ classes: metadata.classes || [],
57561
+ types: metadata.types || [],
57562
+ calls: {
57563
+ functions: metadata.functionCalls || [],
57564
+ methods: metadata.methodCalls || [],
57565
+ constructors: metadata.constructorCalls || []
57566
+ },
57567
+ typeAnalysis: {
57568
+ typeImports: metadata.typeImports || [],
57569
+ interfaceDeps: metadata.interfaceDependencies || [],
57570
+ genericDeps: metadata.genericDependencies || []
57571
+ },
57572
+ di: {
57573
+ injections: metadata.constructorInjections || [],
57574
+ hooks: metadata.hookDependencies || [],
57575
+ context: metadata.contextUsages || []
57576
+ },
57577
+ react: metadata.componentUsage || []
57578
+ }
57579
+ };
57580
+ return { node, factCount: (metadata.imports?.length || 0) + (metadata.functionCalls?.length || 0) };
57581
+ } catch (e) {
57582
+ console.warn(`\u26A0\uFE0F Pass 1 failed for ${file}: ${e.message}`);
57583
+ return null;
57584
+ }
57585
+ });
57586
+ const results = await Promise.all(promises);
57587
+ results.filter(Boolean).forEach((res) => {
57588
+ rawNodes.push(res.node);
57589
+ totalFacts += res.factCount;
57590
+ });
57591
+ if (i % (CONCURRENCY * 4) === 0 && i > 0) {
57592
+ console.log(` ... processed ${Math.min(i + CONCURRENCY, files.length)} / ${files.length} files`);
57572
57593
  }
57573
57594
  }
57574
57595
  console.log(` \u2713 Scanned ${rawNodes.length} files`);
@@ -57584,26 +57605,31 @@ var require_path_resolver = __commonJS({
57584
57605
  "src/core/path-resolver.js"(exports2, module2) {
57585
57606
  var fs2 = require("fs");
57586
57607
  var path2 = require("path");
57587
- function resolveImport(importPath, importerPath, projectRoot, tsconfig, workspaceMap) {
57608
+ function resolveImport(importPath, importerPath, projectRoot, tsconfig, workspaceMap, fileSet) {
57588
57609
  if (importPath.startsWith("http://") || importPath.startsWith("https://") || importPath.startsWith("data:") || importPath.startsWith("file:")) {
57589
57610
  return null;
57590
57611
  }
57612
+ const exists = (p) => {
57613
+ if (fileSet)
57614
+ return fileSet.has(p);
57615
+ return fs2.existsSync(p);
57616
+ };
57591
57617
  if (importPath.startsWith(".")) {
57592
57618
  let resolvedPath = path2.resolve(path2.dirname(importerPath), importPath);
57593
57619
  const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
57594
57620
  for (const ext of extensions) {
57595
- if (fs2.existsSync(resolvedPath + ext)) {
57621
+ if (exists(resolvedPath + ext)) {
57596
57622
  return resolvedPath + ext;
57597
57623
  }
57598
57624
  }
57599
57625
  const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx", "index.json"];
57600
57626
  for (const idxFile of indexFiles) {
57601
57627
  const indexPath = path2.join(resolvedPath, idxFile);
57602
- if (fs2.existsSync(indexPath)) {
57628
+ if (exists(indexPath)) {
57603
57629
  return indexPath;
57604
57630
  }
57605
57631
  }
57606
- if (fs2.existsSync(resolvedPath)) {
57632
+ if (exists(resolvedPath)) {
57607
57633
  return resolvedPath;
57608
57634
  }
57609
57635
  return null;
@@ -57622,18 +57648,18 @@ var require_path_resolver = __commonJS({
57622
57648
  const targetPath = path2.resolve(projectRoot, path2.join(targetBase, suffix));
57623
57649
  const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
57624
57650
  for (const ext of extensions) {
57625
- if (fs2.existsSync(targetPath + ext)) {
57651
+ if (exists(targetPath + ext)) {
57626
57652
  return targetPath + ext;
57627
57653
  }
57628
57654
  }
57629
57655
  const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx", "index.json"];
57630
57656
  for (const idxFile of indexFiles) {
57631
57657
  const indexPath = path2.join(targetPath, idxFile);
57632
- if (fs2.existsSync(indexPath)) {
57658
+ if (exists(indexPath)) {
57633
57659
  return indexPath;
57634
57660
  }
57635
57661
  }
57636
- if (fs2.existsSync(targetPath)) {
57662
+ if (exists(targetPath)) {
57637
57663
  return targetPath;
57638
57664
  }
57639
57665
  }
@@ -57648,18 +57674,18 @@ var require_path_resolver = __commonJS({
57648
57674
  }
57649
57675
  const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
57650
57676
  for (const ext of extensions) {
57651
- if (fs2.existsSync(targetPath + ext)) {
57677
+ if (exists(targetPath + ext)) {
57652
57678
  return targetPath + ext;
57653
57679
  }
57654
57680
  }
57655
57681
  const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx", "index.json"];
57656
57682
  for (const idxFile of indexFiles) {
57657
57683
  const indexPath = path2.join(targetPath, idxFile);
57658
- if (fs2.existsSync(indexPath)) {
57684
+ if (exists(indexPath)) {
57659
57685
  return indexPath;
57660
57686
  }
57661
57687
  }
57662
- if (fs2.existsSync(targetPath)) {
57688
+ if (exists(targetPath)) {
57663
57689
  return targetPath;
57664
57690
  }
57665
57691
  }
@@ -57671,18 +57697,18 @@ var require_path_resolver = __commonJS({
57671
57697
  let resolvedPath = path2.resolve(projectRoot, tsconfig.baseUrl, importPath);
57672
57698
  const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
57673
57699
  for (const ext of extensions) {
57674
- if (fs2.existsSync(resolvedPath + ext)) {
57700
+ if (exists(resolvedPath + ext)) {
57675
57701
  return resolvedPath + ext;
57676
57702
  }
57677
57703
  }
57678
57704
  const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx", "index.json"];
57679
57705
  for (const idxFile of indexFiles) {
57680
57706
  const indexPath = path2.join(resolvedPath, idxFile);
57681
- if (fs2.existsSync(indexPath)) {
57707
+ if (exists(indexPath)) {
57682
57708
  return indexPath;
57683
57709
  }
57684
57710
  }
57685
- if (fs2.existsSync(resolvedPath)) {
57711
+ if (exists(resolvedPath)) {
57686
57712
  return resolvedPath;
57687
57713
  }
57688
57714
  }
@@ -57690,21 +57716,21 @@ var require_path_resolver = __commonJS({
57690
57716
  const packageRoot = workspaceMap.get(importPath);
57691
57717
  try {
57692
57718
  const pkgPath = path2.join(packageRoot, "package.json");
57693
- if (fs2.existsSync(pkgPath)) {
57719
+ if (exists(pkgPath)) {
57694
57720
  const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
57695
57721
  const mainFile = pkg.main || pkg.module || "index.js";
57696
57722
  const searchPath = path2.join(packageRoot, mainFile);
57697
57723
  const extensions = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
57698
57724
  for (const ext of extensions) {
57699
57725
  const p = searchPath + ext;
57700
- if (fs2.existsSync(p))
57726
+ if (exists(p))
57701
57727
  return p;
57702
57728
  }
57703
57729
  }
57704
57730
  const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx"];
57705
57731
  for (const idx of indexFiles) {
57706
57732
  const p = path2.join(packageRoot, idx);
57707
- if (fs2.existsSync(p))
57733
+ if (exists(p))
57708
57734
  return p;
57709
57735
  }
57710
57736
  } catch (e) {
@@ -57718,14 +57744,14 @@ var require_path_resolver = __commonJS({
57718
57744
  const targetPath = path2.join(pkgRoot, suffix);
57719
57745
  const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ""];
57720
57746
  for (const ext of extensions) {
57721
- if (fs2.existsSync(targetPath + ext)) {
57747
+ if (exists(targetPath + ext)) {
57722
57748
  return targetPath + ext;
57723
57749
  }
57724
57750
  }
57725
57751
  const indexFiles = ["index.ts", "index.tsx", "index.js", "index.jsx"];
57726
57752
  for (const idx of indexFiles) {
57727
57753
  const p = path2.join(targetPath, idx);
57728
- if (fs2.existsSync(p))
57754
+ if (exists(p))
57729
57755
  return p;
57730
57756
  }
57731
57757
  }
@@ -58036,6 +58062,7 @@ var require_pass2_semantics = __commonJS({
58036
58062
  const normalize = (p) => p.replace(/\\/g, "/");
58037
58063
  const workspaceMap = workspaceContext ? workspaceContext.workspaceMap : null;
58038
58064
  const fileSet = new Set(rawNodes.map((n) => n.id));
58065
+ const absoluteFileSet = new Set(rawNodes.map((n) => n.filePath));
58039
58066
  const edges = [];
58040
58067
  const addEdge = (source, target, type, confidence = 1, metadata = {}) => {
58041
58068
  if (!source || !target)
@@ -58054,11 +58081,14 @@ var require_pass2_semantics = __commonJS({
58054
58081
  for (const node of rawNodes) {
58055
58082
  if (!node.facts.imports)
58056
58083
  continue;
58084
+ const nodeConfig = loadTSConfig(path2.dirname(node.filePath));
58085
+ const nodeTsconfig = nodeConfig.options;
58086
+ const nodeConfigRoot = nodeConfig.configDir;
58057
58087
  for (const imp of node.facts.imports) {
58058
58088
  const importString = imp.source;
58059
58089
  let targetId = null;
58060
58090
  let resolvedPath = null;
58061
- const resolvedAbs = resolveImport(importString, node.filePath, configRoot, tsconfig, workspaceMap);
58091
+ const resolvedAbs = resolveImport(importString, node.filePath, nodeConfigRoot, nodeTsconfig, workspaceMap, absoluteFileSet);
58062
58092
  if (resolvedAbs) {
58063
58093
  targetId = normalize(path2.relative(rootDir, resolvedAbs));
58064
58094
  resolvedPath = targetId;
@@ -58139,9 +58169,12 @@ var require_pass2_semantics = __commonJS({
58139
58169
  for (const node of rawNodes) {
58140
58170
  if (!node.facts.typeAnalysis)
58141
58171
  continue;
58172
+ const nodeConfig = loadTSConfig(path2.dirname(node.filePath));
58173
+ const nodeTsconfig = nodeConfig.options;
58174
+ const nodeConfigRoot = nodeConfig.configDir;
58142
58175
  const { typeImports, interfaceDeps, genericDeps } = node.facts.typeAnalysis;
58143
58176
  for (const imp of typeImports) {
58144
- const resolvedAbs = resolveImport(imp.source, node.filePath, rootDir, tsconfig, workspaceMap);
58177
+ const resolvedAbs = resolveImport(imp.source, node.filePath, nodeConfigRoot, nodeTsconfig, workspaceMap, absoluteFileSet);
58145
58178
  if (resolvedAbs) {
58146
58179
  const targetId = normalize(path2.relative(rootDir, resolvedAbs));
58147
58180
  if (fileSet.has(targetId)) {
@@ -58415,15 +58448,19 @@ var require_pass4_signals = __commonJS({
58415
58448
  });
58416
58449
  const criticalityList = analyzeCriticality(blastRadiusMap, reverseGraph, nodes);
58417
58450
  const criticalityMap = new Map(criticalityList.map((c) => [c.file, c]));
58451
+ const outDegreeMap = /* @__PURE__ */ new Map();
58452
+ const inDegreeMap = /* @__PURE__ */ new Map();
58453
+ edges.forEach((e) => {
58454
+ outDegreeMap.set(e.source, (outDegreeMap.get(e.source) || 0) + 1);
58455
+ inDegreeMap.set(e.target, (inDegreeMap.get(e.target) || 0) + 1);
58456
+ });
58418
58457
  const finalNodes = nodes.map((node) => {
58419
58458
  const signals = {
58420
58459
  blast_radius: blastRadiusMap[node.id] || 0,
58421
58460
  criticality: criticalityMap.get(node.id)?.criticalityScore || 0,
58422
58461
  is_hub: criticalityMap.get(node.id)?.isHub || false,
58423
- // Calculate instability: closer to 1 means "depends on many things" (outgoing edges)
58424
- // (coupling)
58425
- outgoing_deps: edges.filter((e) => e.source === node.id).length,
58426
- incoming_deps: edges.filter((e) => e.target === node.id).length
58462
+ outgoing_deps: outDegreeMap.get(node.id) || 0,
58463
+ incoming_deps: inDegreeMap.get(node.id) || 0
58427
58464
  };
58428
58465
  return {
58429
58466
  id: node.id,
@@ -58493,8 +58530,8 @@ var require_context_builder = __commonJS({
58493
58530
  language = "javascript"
58494
58531
  } = options;
58495
58532
  const nodes = fileNodes.map((file) => {
58496
- const role = file.role || "Structure";
58497
- const layer = file.layer || "generic";
58533
+ const role = file.role || file.structure && file.structure.role || "Structure";
58534
+ const layer = file.layer || file.structure && file.structure.layer || "generic";
58498
58535
  const dependencies = [];
58499
58536
  const uniqueDeps = /* @__PURE__ */ new Set();
58500
58537
  const addDep = (target) => {
@@ -58505,8 +58542,6 @@ var require_context_builder = __commonJS({
58505
58542
  };
58506
58543
  if (file.intelligence && file.intelligence.connections) {
58507
58544
  file.intelligence.connections.forEach((c) => addDep(c.target));
58508
- } else {
58509
- edges.filter((e) => e.source === file.id).forEach((e) => addDep(e.target));
58510
58545
  }
58511
58546
  const nodeObj = {
58512
58547
  id: stableId(file.id),
@@ -58515,7 +58550,6 @@ var require_context_builder = __commonJS({
58515
58550
  role,
58516
58551
  dependencies,
58517
58552
  blast_radius: file.signals && file.signals.blast_radius || 0,
58518
- // New Schema Extensions (if supported by backend, otherwise ignored)
58519
58553
  layer,
58520
58554
  criticality: file.signals ? file.signals.criticality : 0
58521
58555
  };
@@ -65235,6 +65269,8 @@ function analyzeBlastRadius(architectureMap) {
65235
65269
  let blastRadius = 0;
65236
65270
  if (node.metadata && node.metadata.blast_radius !== void 0) {
65237
65271
  blastRadius = node.metadata.blast_radius;
65272
+ } else if (node.signals && node.signals.blast_radius !== void 0) {
65273
+ blastRadius = node.signals.blast_radius;
65238
65274
  } else if (architectureMap.contextSurface && architectureMap.contextSurface.topBlastRadiusFiles) {
65239
65275
  const foundFile = architectureMap.contextSurface.topBlastRadiusFiles.find((f) => f.file === node.path || f.file === node.id);
65240
65276
  if (foundFile) {
@@ -0,0 +1,84 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Arcvision System-Level Structural Context",
4
+ "type": "object",
5
+ "required": [
6
+ "schema_version",
7
+ "generated_at",
8
+ "system",
9
+ "nodes",
10
+ "edges",
11
+ "metrics"
12
+ ],
13
+ "properties": {
14
+ "schema_version": {
15
+ "type": "string",
16
+ "description": "Semantic version of the Arcvision context schema (e.g. 1.0.0)"
17
+ },
18
+ "generated_at": {
19
+ "type": "string",
20
+ "format": "date-time",
21
+ "description": "ISO-8601 timestamp when context was generated"
22
+ },
23
+ "system": {
24
+ "type": "object",
25
+ "required": ["name", "root_path", "language"],
26
+ "properties": {
27
+ "name": { "type": "string" },
28
+ "root_path": { "type": "string" },
29
+ "language": { "type": "string" }
30
+ }
31
+ },
32
+ "nodes": {
33
+ "type": "array",
34
+ "items": {
35
+ "type": "object",
36
+ "required": ["id", "type", "path", "role"],
37
+ "properties": {
38
+ "id": {
39
+ "type": "string",
40
+ "description": "Deterministic, stable ID (never random)"
41
+ },
42
+ "type": {
43
+ "type": "string",
44
+ "enum": ["file", "module", "class", "function", "service"]
45
+ },
46
+ "path": {
47
+ "type": "string",
48
+ "description": "Normalized relative path"
49
+ },
50
+ "role": {
51
+ "type": "string",
52
+ "description": "Why this node exists in the system"
53
+ },
54
+ "dependencies": {
55
+ "type": "array",
56
+ "items": { "type": "string" }
57
+ }
58
+ }
59
+ }
60
+ },
61
+ "edges": {
62
+ "type": "array",
63
+ "items": {
64
+ "type": "object",
65
+ "required": ["from", "to", "relation"],
66
+ "properties": {
67
+ "from": { "type": "string" },
68
+ "to": { "type": "string" },
69
+ "relation": {
70
+ "type": "string",
71
+ "enum": ["imports", "calls", "owns", "depends_on"]
72
+ }
73
+ }
74
+ }
75
+ },
76
+ "metrics": {
77
+ "type": "object",
78
+ "description": "System-wide aggregate metrics",
79
+ "additionalProperties": {
80
+ "type": ["number", "string"]
81
+ }
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,76 @@
1
+ # Blast Radius Implementation
2
+
3
+ ## Architecture
4
+
5
+ ### Files Added/Modified
6
+ 1. `src/core/blastRadius.js` - Core blast radius calculation logic
7
+ 2. `src/core/scanner.js` - Modified to include blast radius in output
8
+ 3. `src/index.js` - Modified to display blast radius insight
9
+
10
+ ### Core Functions
11
+
12
+ #### `buildReverseDependencyGraph(architectureMap)`
13
+ - Creates a reverse dependency map where keys are imported files and values are arrays of files that import them
14
+ - Takes the architecture map (with nodes and edges) as input
15
+ - Returns a map: `{ [importedFile]: [importingFile1, importingFile2, ...] }`
16
+
17
+ #### `computeBlastRadius(reverseGraph)`
18
+ - Calculates the blast radius for each file as the count of files that import it
19
+ - Takes the reverse dependency graph as input
20
+ - Returns a map: `{ [filePath]: blastRadiusNumber }`
21
+
22
+ #### `findHighestBlastRadius(blastRadiusMap)`
23
+ - Finds the file with the highest blast radius value
24
+ - Takes the blast radius map as input
25
+ - Returns an object: `{ file: filePath, blast_radius: radius }` or null
26
+
27
+ ## Data Flow
28
+
29
+ 1. **Scanning Phase**: The scanner builds the architecture map with nodes and edges as before
30
+ 2. **Blast Radius Calculation Phase**: After the architecture map is built:
31
+ - Reverse dependency graph is built from the edges
32
+ - Blast radius is computed for each file
33
+ - Blast radius values are added to each node's metadata
34
+ 3. **Output Phase**: The CLI prints the architecture map as JSON and then shows the blast radius insight
35
+
36
+ ## Data Structure Changes
37
+
38
+ ### Node Metadata
39
+ Each node in the architecture map now includes:
40
+ ```javascript
41
+ {
42
+ id: "relative/file/path.js",
43
+ type: "file",
44
+ metadata: {
45
+ imports: [...],
46
+ exports: [...],
47
+ functions: [...],
48
+ apiCalls: [...],
49
+ blast_radius: 5 // NEW FIELD
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### CLI Output
55
+ The CLI now prints an additional message after the scan:
56
+ ```
57
+ ⚠️ src/core/utils.js has the highest blast radius (5). Changes here may affect many parts of the system.
58
+ ```
59
+
60
+ ## Algorithm Complexity
61
+ - **Time Complexity**: O(V + E) where V is the number of files and E is the number of import relationships
62
+ - **Space Complexity**: O(V + E) for storing the reverse dependency graph
63
+
64
+ ## Error Handling
65
+ - Handles empty repositories gracefully
66
+ - Handles files with no imports or no dependents
67
+ - Maintains backward compatibility with existing functionality
68
+ - Preserves all existing fields in the architecture map
69
+
70
+ ## Testing Considerations
71
+ The implementation should be tested with:
72
+ - Empty repositories
73
+ - Repositories with no dependencies
74
+ - Repositories with complex dependency chains
75
+ - Repositories with circular dependencies
76
+ - Large repositories with many files
@@ -0,0 +1,44 @@
1
+ # Blast Radius Feature
2
+
3
+ ## Overview
4
+ The Blast Radius feature analyzes your codebase to identify files that are most critical to your application. It calculates how many other files depend on each file, helping you identify high-structure areas where changes could have wide-ranging roles.
5
+
6
+ ## How It Works
7
+ - **Blast Radius Score**: For each file, the blast radius is calculated as the number of files that import it (direct dependencies only).
8
+ - **Reverse Dependency Graph**: The system builds a reverse dependency graph to track which files import each file.
9
+ - **Structure Assessment**: Files with higher blast radius scores are considered higher structure because changes to them could affect many other parts of the system.
10
+
11
+ ## Output
12
+ The blast radius score is added to each file's metadata in the architecture map:
13
+
14
+ ```json
15
+ {
16
+ "nodes": [
17
+ {
18
+ "id": "src/auth/session.ts",
19
+ "type": "file",
20
+ "metadata": {
21
+ "imports": ["src/db/client.ts", "src/utils/logger.ts"],
22
+ "exports": [],
23
+ "functions": [],
24
+ "apiCalls": [],
25
+ "blast_radius": 2
26
+ }
27
+ }
28
+ ],
29
+ "edges": [...]
30
+ }
31
+ ```
32
+
33
+ ## CLI Output
34
+ After scanning, the CLI will display a warning for the file with the highest blast radius:
35
+
36
+ ```
37
+ ⚠️ src/auth/session.ts has the highest blast radius (2). Changes here may affect many parts of the system.
38
+ ```
39
+
40
+ ## Use Cases
41
+ - Identify critical files that require extra care during refactoring
42
+ - Understand the potential role of code changes
43
+ - Prioritize code review efforts for high-structure files
44
+ - Analyze architectural dependencies in your codebase
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcvision",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Architecture scanner for modern codebases",
5
5
  "bin": {
6
6
  "arcvision": "./dist/index.js"
@@ -38,11 +38,15 @@
38
38
  "author": "ArcVision",
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "https://github.com/your-org/arcvision.git"
41
+ "url": "https://github.com/arcvision/arcvision.git"
42
42
  },
43
43
  "license": "MIT",
44
44
  "files": [
45
- "dist"
45
+ "dist",
46
+ "README.md",
47
+ "LICENSE",
48
+ "schema",
49
+ "docs"
46
50
  ],
47
51
  "publishConfig": {
48
52
  "access": "public"
@@ -0,0 +1,84 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Arcvision System-Level Structural Context",
4
+ "type": "object",
5
+ "required": [
6
+ "schema_version",
7
+ "generated_at",
8
+ "system",
9
+ "nodes",
10
+ "edges",
11
+ "metrics"
12
+ ],
13
+ "properties": {
14
+ "schema_version": {
15
+ "type": "string",
16
+ "description": "Semantic version of the Arcvision context schema (e.g. 1.0.0)"
17
+ },
18
+ "generated_at": {
19
+ "type": "string",
20
+ "format": "date-time",
21
+ "description": "ISO-8601 timestamp when context was generated"
22
+ },
23
+ "system": {
24
+ "type": "object",
25
+ "required": ["name", "root_path", "language"],
26
+ "properties": {
27
+ "name": { "type": "string" },
28
+ "root_path": { "type": "string" },
29
+ "language": { "type": "string" }
30
+ }
31
+ },
32
+ "nodes": {
33
+ "type": "array",
34
+ "items": {
35
+ "type": "object",
36
+ "required": ["id", "type", "path", "role"],
37
+ "properties": {
38
+ "id": {
39
+ "type": "string",
40
+ "description": "Deterministic, stable ID (never random)"
41
+ },
42
+ "type": {
43
+ "type": "string",
44
+ "enum": ["file", "module", "class", "function", "service"]
45
+ },
46
+ "path": {
47
+ "type": "string",
48
+ "description": "Normalized relative path"
49
+ },
50
+ "role": {
51
+ "type": "string",
52
+ "description": "Why this node exists in the system"
53
+ },
54
+ "dependencies": {
55
+ "type": "array",
56
+ "items": { "type": "string" }
57
+ }
58
+ }
59
+ }
60
+ },
61
+ "edges": {
62
+ "type": "array",
63
+ "items": {
64
+ "type": "object",
65
+ "required": ["from", "to", "relation"],
66
+ "properties": {
67
+ "from": { "type": "string" },
68
+ "to": { "type": "string" },
69
+ "relation": {
70
+ "type": "string",
71
+ "enum": ["imports", "calls", "owns", "depends_on"]
72
+ }
73
+ }
74
+ }
75
+ },
76
+ "metrics": {
77
+ "type": "object",
78
+ "description": "System-wide aggregate metrics",
79
+ "additionalProperties": {
80
+ "type": ["number", "string"]
81
+ }
82
+ }
83
+ }
84
+ }