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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56431
|
-
|
|
56432
|
-
|
|
56433
|
-
|
|
56434
|
-
|
|
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 (
|
|
57516
|
-
|
|
57517
|
-
|
|
57518
|
-
|
|
57519
|
-
|
|
57520
|
-
|
|
57521
|
-
|
|
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
|
-
}
|
|
57531
|
-
|
|
57532
|
-
|
|
57533
|
-
|
|
57534
|
-
|
|
57535
|
-
|
|
57536
|
-
|
|
57537
|
-
|
|
57538
|
-
|
|
57539
|
-
|
|
57540
|
-
|
|
57541
|
-
|
|
57542
|
-
|
|
57543
|
-
|
|
57544
|
-
|
|
57545
|
-
|
|
57546
|
-
|
|
57547
|
-
|
|
57548
|
-
|
|
57549
|
-
|
|
57550
|
-
|
|
57551
|
-
|
|
57552
|
-
|
|
57553
|
-
|
|
57554
|
-
|
|
57555
|
-
|
|
57556
|
-
|
|
57557
|
-
|
|
57558
|
-
|
|
57559
|
-
|
|
57560
|
-
|
|
57561
|
-
|
|
57562
|
-
|
|
57563
|
-
|
|
57564
|
-
|
|
57565
|
-
|
|
57566
|
-
|
|
57567
|
-
|
|
57568
|
-
|
|
57569
|
-
|
|
57570
|
-
|
|
57571
|
-
|
|
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 (
|
|
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 (
|
|
57628
|
+
if (exists(indexPath)) {
|
|
57603
57629
|
return indexPath;
|
|
57604
57630
|
}
|
|
57605
57631
|
}
|
|
57606
|
-
if (
|
|
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 (
|
|
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 (
|
|
57658
|
+
if (exists(indexPath)) {
|
|
57633
57659
|
return indexPath;
|
|
57634
57660
|
}
|
|
57635
57661
|
}
|
|
57636
|
-
if (
|
|
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 (
|
|
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 (
|
|
57684
|
+
if (exists(indexPath)) {
|
|
57659
57685
|
return indexPath;
|
|
57660
57686
|
}
|
|
57661
57687
|
}
|
|
57662
|
-
if (
|
|
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 (
|
|
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 (
|
|
57707
|
+
if (exists(indexPath)) {
|
|
57682
57708
|
return indexPath;
|
|
57683
57709
|
}
|
|
57684
57710
|
}
|
|
57685
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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,
|
|
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,
|
|
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
|
-
|
|
58424
|
-
|
|
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.
|
|
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/
|
|
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
|
+
}
|