@titan-design/code-graph 0.10.0 → 0.13.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.
- package/README.md +135 -5
- package/dist/analysis/browser.d.ts +2 -0
- package/dist/analysis/browser.js +84 -0
- package/dist/analysis/browser.js.map +1 -0
- package/dist/browser-BBzShneN.d.ts +936 -0
- package/dist/{change-coupling-CyqHgRsm.d.ts → change-coupling-CT3bcZMD.d.ts} +5 -1
- package/dist/{chunk-PFI5XMUG.js → chunk-GX563F6R.js} +79 -89
- package/dist/chunk-GX563F6R.js.map +1 -0
- package/dist/chunk-HIVVDGWE.js +83 -0
- package/dist/chunk-HIVVDGWE.js.map +1 -0
- package/dist/chunk-L5KU543A.js +1395 -0
- package/dist/chunk-L5KU543A.js.map +1 -0
- package/dist/history/index.d.ts +25 -4
- package/dist/history/index.js +11 -3
- package/dist/index.d.ts +406 -353
- package/dist/index.js +1753 -889
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
- package/dist/chunk-PFI5XMUG.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,78 @@
|
|
|
1
1
|
import {
|
|
2
2
|
aggregateChurnWindows,
|
|
3
3
|
authorLinesByPath,
|
|
4
|
-
computeChangeCoupling,
|
|
5
4
|
computeOwnership,
|
|
6
|
-
couplingFor,
|
|
7
5
|
detectGitToplevel,
|
|
8
6
|
entriesWithin,
|
|
7
|
+
listTreeBlobs,
|
|
9
8
|
loadChurnEntries,
|
|
10
9
|
loadFileFirstSeen,
|
|
10
|
+
readBlobs,
|
|
11
|
+
resolveCommit,
|
|
11
12
|
runGit,
|
|
12
13
|
summarizeOwnership
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-GX563F6R.js";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_HEALTH_WEIGHTS,
|
|
17
|
+
DEFAULT_MAX_PACKAGE_SIZE,
|
|
18
|
+
EXTERNAL_BUCKET,
|
|
19
|
+
SYMBOL_ID_SEP,
|
|
20
|
+
UNIMPORTED_ROLES,
|
|
21
|
+
aggregateEdges,
|
|
22
|
+
bucketFilesByPackage,
|
|
23
|
+
bucketViolations,
|
|
24
|
+
buildBlastRadius,
|
|
25
|
+
buildCentralFiles,
|
|
26
|
+
buildHotExports,
|
|
27
|
+
buildNodeMetrics,
|
|
28
|
+
buildReportContext,
|
|
29
|
+
busFactorOf,
|
|
30
|
+
classifyCoupling,
|
|
31
|
+
collectNodeMetrics,
|
|
32
|
+
collectSymbolUtil,
|
|
33
|
+
compilePatterns,
|
|
34
|
+
computeArch,
|
|
35
|
+
computeHealth,
|
|
36
|
+
computePageRank,
|
|
37
|
+
computePartitionQuality,
|
|
38
|
+
computeRecencyWindows,
|
|
39
|
+
computeReportDrift,
|
|
40
|
+
computeSymbolConsumers,
|
|
41
|
+
computeSymbolCoupling,
|
|
42
|
+
filteredFileIds,
|
|
43
|
+
getEdgeWeight,
|
|
44
|
+
groupTestsBySource,
|
|
45
|
+
hotspotScoreOf,
|
|
46
|
+
invertBuckets,
|
|
47
|
+
keepNode,
|
|
48
|
+
linkTestsToSources,
|
|
49
|
+
lookupMetric,
|
|
50
|
+
matchesAny,
|
|
51
|
+
packagesReferencedByEdges,
|
|
52
|
+
pairKey,
|
|
53
|
+
parseSymbolId,
|
|
54
|
+
patternToRegex,
|
|
55
|
+
publicApiFiles,
|
|
56
|
+
rebasedViolationKey,
|
|
57
|
+
referencedNodes,
|
|
58
|
+
round3,
|
|
59
|
+
symbolId,
|
|
60
|
+
testCoverageCountMetrics,
|
|
61
|
+
toSortedEdges,
|
|
62
|
+
topBusFactorRisks,
|
|
63
|
+
topCentralFiles,
|
|
64
|
+
topDeadModules,
|
|
65
|
+
topGrowthRisks,
|
|
66
|
+
topHotspots,
|
|
67
|
+
topTestCoverageRisks,
|
|
68
|
+
topUntestedRisks,
|
|
69
|
+
topUnusedExports,
|
|
70
|
+
violationKey,
|
|
71
|
+
windowSuffix
|
|
72
|
+
} from "./chunk-L5KU543A.js";
|
|
73
|
+
import {
|
|
74
|
+
computeChangeCoupling
|
|
75
|
+
} from "./chunk-HIVVDGWE.js";
|
|
14
76
|
|
|
15
77
|
// src/store.ts
|
|
16
78
|
import { openDatabase, quoteIdent, runMigrations } from "@titan-design/store-sqlite";
|
|
@@ -231,8 +293,8 @@ function readMetricAggregates(stmts, snapshotId, name) {
|
|
|
231
293
|
|
|
232
294
|
// src/store.ts
|
|
233
295
|
var NODE_COLS = "id, kind, name, parent_id, language, role, attrs";
|
|
234
|
-
function isStructuralLayer(
|
|
235
|
-
return
|
|
296
|
+
function isStructuralLayer(edge2) {
|
|
297
|
+
return edge2.kind !== "references" && edge2.kind !== "calls";
|
|
236
298
|
}
|
|
237
299
|
var CodeGraphStore = class {
|
|
238
300
|
constructor(db) {
|
|
@@ -252,6 +314,15 @@ var CodeGraphStore = class {
|
|
|
252
314
|
});
|
|
253
315
|
return Number(result.lastInsertRowid);
|
|
254
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Run `write` in one transaction so a reader on another connection sees a new
|
|
319
|
+
* snapshot only once every row written for it is committed, never a snapshot
|
|
320
|
+
* row whose nodes or edges are still being inserted. The per-table inserts
|
|
321
|
+
* nest as savepoints inside it.
|
|
322
|
+
*/
|
|
323
|
+
atomically(write) {
|
|
324
|
+
return this.db.transaction(write)();
|
|
325
|
+
}
|
|
255
326
|
insertNodes(snapshotId, nodes) {
|
|
256
327
|
const stmt = this.statements.insertNode;
|
|
257
328
|
this.db.transaction(() => {
|
|
@@ -491,30 +562,80 @@ function runPrune(store, options = {}) {
|
|
|
491
562
|
}
|
|
492
563
|
|
|
493
564
|
// src/indexer.ts
|
|
494
|
-
import { realpathSync } from "fs";
|
|
495
|
-
import * as
|
|
565
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
566
|
+
import * as path10 from "path";
|
|
496
567
|
import { parseFile } from "@titan-design/code-parser";
|
|
497
568
|
|
|
498
569
|
// src/extractors/ts-morph-extractor.ts
|
|
499
570
|
import {
|
|
500
|
-
Project,
|
|
571
|
+
Project as Project2,
|
|
501
572
|
ScriptTarget,
|
|
502
573
|
ModuleKind,
|
|
503
574
|
ModuleResolutionKind
|
|
504
575
|
} from "ts-morph";
|
|
505
576
|
|
|
577
|
+
// src/index-source.ts
|
|
578
|
+
import { existsSync, readFileSync } from "fs";
|
|
579
|
+
import { Project } from "ts-morph";
|
|
580
|
+
|
|
581
|
+
// src/file-walk.ts
|
|
582
|
+
import * as fs from "fs/promises";
|
|
583
|
+
import * as path from "path";
|
|
584
|
+
import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
|
|
585
|
+
async function walkSourceFiles(rootDirs, languages) {
|
|
586
|
+
const seen = /* @__PURE__ */ new Set();
|
|
587
|
+
const out = [];
|
|
588
|
+
for (const rootDir of rootDirs) {
|
|
589
|
+
await collectFiles(rootDir, rootDir, languages, seen, out);
|
|
590
|
+
}
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
async function collectFiles(dir, rootDir, languages, seen, out) {
|
|
594
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
595
|
+
for (const entry of entries) {
|
|
596
|
+
const full = path.join(dir, entry.name);
|
|
597
|
+
if (entry.isDirectory()) {
|
|
598
|
+
if (isExcludedDir(entry.name)) continue;
|
|
599
|
+
await collectFiles(full, rootDir, languages, seen, out);
|
|
600
|
+
} else if (entry.isFile() && !seen.has(full)) {
|
|
601
|
+
const relative5 = path.relative(rootDir, full);
|
|
602
|
+
if (shouldIncludeFile(relative5, [...languages])) {
|
|
603
|
+
seen.add(full);
|
|
604
|
+
out.push(full);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/index-source.ts
|
|
611
|
+
var realFileSystem;
|
|
612
|
+
function realFileSystemHost() {
|
|
613
|
+
realFileSystem ??= new Project().getFileSystem();
|
|
614
|
+
return realFileSystem;
|
|
615
|
+
}
|
|
616
|
+
function workingTreeSource() {
|
|
617
|
+
return {
|
|
618
|
+
listFiles: async (rootDirs, languages) => (await walkSourceFiles(rootDirs, languages)).sort(),
|
|
619
|
+
readFile: (abs) => readFileSync(abs, "utf-8"),
|
|
620
|
+
fileExists: existsSync,
|
|
621
|
+
get fileSystem() {
|
|
622
|
+
return realFileSystemHost();
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
506
627
|
// src/extractors/file-nodes.ts
|
|
507
|
-
import * as
|
|
628
|
+
import * as path3 from "path";
|
|
508
629
|
import { getLanguageFromPath } from "@titan-design/code-parser";
|
|
509
630
|
|
|
510
631
|
// src/extractors/ids.ts
|
|
511
|
-
import * as
|
|
632
|
+
import * as path2 from "path";
|
|
512
633
|
var MODULE_EXT_RE = /\.(?:tsx|ts|jsx|js|mts|cts|mjs|cjs|py)$/;
|
|
513
634
|
function toPosix(p) {
|
|
514
|
-
return p.split(
|
|
635
|
+
return p.split(path2.sep).join("/");
|
|
515
636
|
}
|
|
516
637
|
function repoRelative(repoRoot, absPath) {
|
|
517
|
-
const rel =
|
|
638
|
+
const rel = path2.relative(repoRoot, absPath);
|
|
518
639
|
return toPosix(rel);
|
|
519
640
|
}
|
|
520
641
|
function fileId(repoRoot, absPath) {
|
|
@@ -532,15 +653,6 @@ function parentModuleId(id) {
|
|
|
532
653
|
function packageId(name) {
|
|
533
654
|
return name;
|
|
534
655
|
}
|
|
535
|
-
var SYMBOL_ID_SEP = "#";
|
|
536
|
-
function symbolId(fileId2, qualifiedName) {
|
|
537
|
-
return `${fileId2}${SYMBOL_ID_SEP}${qualifiedName}`;
|
|
538
|
-
}
|
|
539
|
-
function parseSymbolId(id) {
|
|
540
|
-
const idx = id.indexOf(SYMBOL_ID_SEP);
|
|
541
|
-
if (idx < 0) return null;
|
|
542
|
-
return { fileId: id.slice(0, idx), name: id.slice(idx + 1) };
|
|
543
|
-
}
|
|
544
656
|
function externalId(specifier) {
|
|
545
657
|
if (specifier.startsWith("node:")) {
|
|
546
658
|
return specifier;
|
|
@@ -563,8 +675,8 @@ function buildFileModuleNodes(repoRoot, absPath) {
|
|
|
563
675
|
const parentId = parentModuleId(mId) ?? void 0;
|
|
564
676
|
const language = getLanguageFromPath(absPath) ?? "typescript";
|
|
565
677
|
return [
|
|
566
|
-
{ id: fId, kind: "file", name:
|
|
567
|
-
{ id: mId, kind: "module", name:
|
|
678
|
+
{ id: fId, kind: "file", name: path3.basename(fId), parentId: mId, language },
|
|
679
|
+
{ id: mId, kind: "module", name: path3.basename(mId), parentId, language }
|
|
568
680
|
];
|
|
569
681
|
}
|
|
570
682
|
|
|
@@ -641,20 +753,29 @@ function isPropertyNamePosition(id) {
|
|
|
641
753
|
}
|
|
642
754
|
|
|
643
755
|
// src/scope-path.ts
|
|
644
|
-
var
|
|
756
|
+
var TS_CLASS_TYPES = /* @__PURE__ */ new Set([
|
|
645
757
|
"class_declaration",
|
|
646
758
|
"abstract_class_declaration",
|
|
647
|
-
"class"
|
|
759
|
+
"class"
|
|
760
|
+
]);
|
|
761
|
+
var TS_FUNCTION_DECL_TYPES = /* @__PURE__ */ new Set([
|
|
648
762
|
"function_declaration",
|
|
649
763
|
"generator_function_declaration",
|
|
650
|
-
"method_definition"
|
|
764
|
+
"method_definition"
|
|
765
|
+
]);
|
|
766
|
+
var TS_BOUND_FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
767
|
+
"arrow_function",
|
|
768
|
+
"function_expression",
|
|
769
|
+
"generator_function"
|
|
770
|
+
]);
|
|
771
|
+
var TS_NAMED_SCOPES = /* @__PURE__ */ new Set([
|
|
772
|
+
...TS_CLASS_TYPES,
|
|
773
|
+
...TS_FUNCTION_DECL_TYPES,
|
|
651
774
|
"internal_module"
|
|
652
775
|
]);
|
|
653
776
|
var TS_BOUND_SCOPES = /* @__PURE__ */ new Set([
|
|
654
777
|
"class",
|
|
655
|
-
|
|
656
|
-
"function_expression",
|
|
657
|
-
"generator_function",
|
|
778
|
+
...TS_BOUND_FUNCTION_TYPES,
|
|
658
779
|
"object"
|
|
659
780
|
]);
|
|
660
781
|
var PY_NAMED_SCOPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
|
|
@@ -701,9 +822,9 @@ function walkScopes(root, python, visit) {
|
|
|
701
822
|
|
|
702
823
|
// src/declared-names.ts
|
|
703
824
|
var TS_DECL_TYPES = /* @__PURE__ */ new Set([
|
|
704
|
-
|
|
705
|
-
"
|
|
706
|
-
"
|
|
825
|
+
...TS_FUNCTION_DECL_TYPES,
|
|
826
|
+
"class_declaration",
|
|
827
|
+
"abstract_class_declaration"
|
|
707
828
|
]);
|
|
708
829
|
var PY_DECL_TYPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
|
|
709
830
|
function collectDeclarations(file) {
|
|
@@ -738,7 +859,7 @@ function declaredNodeAt(node, declTypes) {
|
|
|
738
859
|
const name = node.childForFieldName("name")?.text;
|
|
739
860
|
return name ? { name, node } : null;
|
|
740
861
|
}
|
|
741
|
-
if ((node.type
|
|
862
|
+
if (TS_BOUND_FUNCTION_TYPES.has(node.type) && node.parent?.type === "variable_declarator") {
|
|
742
863
|
const name = node.parent.childForFieldName("name")?.text;
|
|
743
864
|
return name ? { name, node } : null;
|
|
744
865
|
}
|
|
@@ -821,8 +942,59 @@ function hasReceiver(fn) {
|
|
|
821
942
|
|
|
822
943
|
// src/extractors/symbol-signature.ts
|
|
823
944
|
import {
|
|
824
|
-
Node as
|
|
945
|
+
Node as Node3
|
|
825
946
|
} from "ts-morph";
|
|
947
|
+
|
|
948
|
+
// src/extractors/ts-scope.ts
|
|
949
|
+
import { Node as Node2 } from "ts-morph";
|
|
950
|
+
function bindingName2(node) {
|
|
951
|
+
const parent = node.getParent();
|
|
952
|
+
if (Node2.isVariableDeclaration(parent) || Node2.isPropertyDeclaration(parent)) return parent.getName();
|
|
953
|
+
if (Node2.isPropertyAssignment(parent)) return parent.getNameNode().getText();
|
|
954
|
+
if (Node2.isExportAssignment(parent)) return "default";
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
function declaredName(node) {
|
|
958
|
+
const name = node.getName();
|
|
959
|
+
if (name) return name;
|
|
960
|
+
return Node2.isExportable(node) && node.isDefaultExport() ? "default" : null;
|
|
961
|
+
}
|
|
962
|
+
function scopeSegment2(node) {
|
|
963
|
+
if (Node2.isClassDeclaration(node) || Node2.isFunctionDeclaration(node)) return declaredName(node);
|
|
964
|
+
if (Node2.isMethodDeclaration(node) || Node2.isGetAccessorDeclaration(node) || Node2.isSetAccessorDeclaration(node)) {
|
|
965
|
+
return node.getName();
|
|
966
|
+
}
|
|
967
|
+
if (Node2.isConstructorDeclaration(node)) return "constructor";
|
|
968
|
+
if (Node2.isModuleDeclaration(node)) return node.hasNamespaceKeyword() ? node.getName() : null;
|
|
969
|
+
if (Node2.isClassExpression(node) && node.getName()) return node.getName() ?? null;
|
|
970
|
+
const bound = Node2.isArrowFunction(node) || Node2.isFunctionExpression(node) || Node2.isClassExpression(node) || Node2.isObjectLiteralExpression(node);
|
|
971
|
+
return bound ? bindingName2(node) ?? ANONYMOUS_SCOPE : null;
|
|
972
|
+
}
|
|
973
|
+
function scopeOf(node) {
|
|
974
|
+
const segments = node.getAncestors().map(scopeSegment2).filter((s) => s !== null).reverse();
|
|
975
|
+
let scope = "";
|
|
976
|
+
for (const segment of segments) {
|
|
977
|
+
const repeatsAnonymous = segment === ANONYMOUS_SCOPE && (scope === ANONYMOUS_SCOPE || scope.endsWith(`.${ANONYMOUS_SCOPE}`));
|
|
978
|
+
if (!repeatsAnonymous) scope = qualify(scope, segment);
|
|
979
|
+
}
|
|
980
|
+
return scope;
|
|
981
|
+
}
|
|
982
|
+
function declarationQualifiedName(decl) {
|
|
983
|
+
if (Node2.isVariableDeclaration(decl)) {
|
|
984
|
+
const init = decl.getInitializer();
|
|
985
|
+
const callable = Node2.isArrowFunction(init) || Node2.isFunctionExpression(init);
|
|
986
|
+
return callable ? qualify(scopeOf(decl), decl.getName()) : null;
|
|
987
|
+
}
|
|
988
|
+
if (Node2.isArrowFunction(decl) || Node2.isFunctionExpression(decl)) {
|
|
989
|
+
const parent = decl.getParent();
|
|
990
|
+
return Node2.isVariableDeclaration(parent) ? qualify(scopeOf(parent), parent.getName()) : null;
|
|
991
|
+
}
|
|
992
|
+
const isDeclaration = Node2.isFunctionDeclaration(decl) || Node2.isClassDeclaration(decl) || Node2.isMethodDeclaration(decl) || Node2.isGetAccessorDeclaration(decl) || Node2.isSetAccessorDeclaration(decl) || Node2.isConstructorDeclaration(decl);
|
|
993
|
+
const own = isDeclaration ? scopeSegment2(decl) : null;
|
|
994
|
+
return own ? qualify(scopeOf(decl), own) : null;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/extractors/symbol-signature.ts
|
|
826
998
|
var MAX_SIGNATURE = 200;
|
|
827
999
|
var MAX_PURPOSE = 240;
|
|
828
1000
|
function declarationText(decl) {
|
|
@@ -833,31 +1005,72 @@ function declarationText(decl) {
|
|
|
833
1005
|
if (purpose) out.purpose = purpose;
|
|
834
1006
|
return out;
|
|
835
1007
|
}
|
|
1008
|
+
function createDeclarationLookup(sf) {
|
|
1009
|
+
let qualified;
|
|
1010
|
+
return (name) => {
|
|
1011
|
+
const fileLevel = fileLevelDeclaration(sf, name);
|
|
1012
|
+
if (fileLevel) return fileLevel;
|
|
1013
|
+
qualified ??= qualifiedDeclarations(sf);
|
|
1014
|
+
return qualified.get(name);
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
836
1017
|
function lookupDeclaration(sf, name) {
|
|
1018
|
+
return createDeclarationLookup(sf)(name);
|
|
1019
|
+
}
|
|
1020
|
+
function fileLevelDeclaration(sf, name) {
|
|
837
1021
|
return sf.getFunction(name) ?? sf.getClass(name) ?? sf.getInterface(name) ?? sf.getTypeAlias(name) ?? sf.getEnum(name) ?? sf.getVariableDeclaration(name);
|
|
838
1022
|
}
|
|
1023
|
+
function qualifiedDeclarations(sf) {
|
|
1024
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1025
|
+
sf.forEachDescendant((node) => {
|
|
1026
|
+
const name = declarationQualifiedName(node);
|
|
1027
|
+
if (name === null) return;
|
|
1028
|
+
const held = byName.get(name);
|
|
1029
|
+
if (!held || outranks(node, held)) byName.set(name, node);
|
|
1030
|
+
});
|
|
1031
|
+
return byName;
|
|
1032
|
+
}
|
|
1033
|
+
function outranks(candidate, held) {
|
|
1034
|
+
if (Node3.isOverloadable(held) && held.isOverload()) {
|
|
1035
|
+
return !(Node3.isOverloadable(candidate) && candidate.isOverload());
|
|
1036
|
+
}
|
|
1037
|
+
return Node3.isGetAccessorDeclaration(candidate) && Node3.isSetAccessorDeclaration(held);
|
|
1038
|
+
}
|
|
839
1039
|
function signatureOf(decl) {
|
|
840
|
-
if (
|
|
1040
|
+
if (Node3.isFunctionDeclaration(decl) || Node3.isMethodDeclaration(decl)) {
|
|
841
1041
|
return callableSignature(decl, decl.getName() ?? "");
|
|
842
1042
|
}
|
|
843
|
-
if (
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if (
|
|
1043
|
+
if (Node3.isGetAccessorDeclaration(decl) || Node3.isSetAccessorDeclaration(decl)) {
|
|
1044
|
+
return accessorSignature(decl);
|
|
1045
|
+
}
|
|
1046
|
+
if (Node3.isVariableDeclaration(decl)) return variableSignature(decl);
|
|
1047
|
+
if (Node3.isClassDeclaration(decl)) return clamp(`class ${decl.getName() ?? ""}`.trim());
|
|
1048
|
+
if (Node3.isInterfaceDeclaration(decl)) return clamp(`interface ${decl.getName()}`);
|
|
1049
|
+
if (Node3.isTypeAliasDeclaration(decl)) {
|
|
847
1050
|
return clamp(`type ${decl.getName()} = ${oneLine(decl.getTypeNode()?.getText() ?? "")}`);
|
|
848
1051
|
}
|
|
849
|
-
if (
|
|
1052
|
+
if (Node3.isEnumDeclaration(decl)) return clamp(`enum ${decl.getName()}`);
|
|
850
1053
|
return void 0;
|
|
851
1054
|
}
|
|
852
1055
|
function variableSignature(decl) {
|
|
853
1056
|
const init = decl.getInitializer();
|
|
854
|
-
if (init && (
|
|
1057
|
+
if (init && (Node3.isArrowFunction(init) || Node3.isFunctionExpression(init))) {
|
|
855
1058
|
return callableSignature(init, decl.getName());
|
|
856
1059
|
}
|
|
857
1060
|
const typeNode = decl.getTypeNode();
|
|
858
1061
|
const type = typeNode ? oneLine(typeNode.getText()) : cleanType(() => decl.getType().getText());
|
|
859
1062
|
return type ? clamp(`${decl.getName()}: ${type}`) : void 0;
|
|
860
1063
|
}
|
|
1064
|
+
function accessorSignature(decl) {
|
|
1065
|
+
const type = Node3.isGetAccessorDeclaration(decl) ? returnType(decl) : setterType(decl);
|
|
1066
|
+
return clamp(type ? `${decl.getName()}: ${type}` : decl.getName());
|
|
1067
|
+
}
|
|
1068
|
+
function setterType(decl) {
|
|
1069
|
+
const param = decl.getParameters()[0];
|
|
1070
|
+
if (!param) return void 0;
|
|
1071
|
+
const node = param.getTypeNode();
|
|
1072
|
+
return node ? oneLine(node.getText()) : cleanType(() => param.getType().getText());
|
|
1073
|
+
}
|
|
861
1074
|
function callableSignature(decl, name) {
|
|
862
1075
|
const params = decl.getParameters().map((p) => oneLine(p.getText())).join(", ");
|
|
863
1076
|
const ret = returnType(decl);
|
|
@@ -885,8 +1098,8 @@ function purposeOf(decl) {
|
|
|
885
1098
|
return desc ? clamp(oneLine(desc), MAX_PURPOSE) : void 0;
|
|
886
1099
|
}
|
|
887
1100
|
function jsDocsFor(decl) {
|
|
888
|
-
if (
|
|
889
|
-
if (
|
|
1101
|
+
if (Node3.isJSDocable(decl)) return decl.getJsDocs();
|
|
1102
|
+
if (Node3.isVariableDeclaration(decl)) return decl.getVariableStatement()?.getJsDocs();
|
|
890
1103
|
return void 0;
|
|
891
1104
|
}
|
|
892
1105
|
function oneLine(s) {
|
|
@@ -926,9 +1139,10 @@ function buildSymbolNodes(repoRoot, sourceFile, file) {
|
|
|
926
1139
|
exported.add(name);
|
|
927
1140
|
out.push(makeSymbolNode(fId, name, true, spans.get(name), declarationText(own), shapes.get(name)));
|
|
928
1141
|
}
|
|
1142
|
+
const lookup = createDeclarationLookup(sourceFile);
|
|
929
1143
|
for (const [name, span] of spans) {
|
|
930
1144
|
if (exported.has(name)) continue;
|
|
931
|
-
const decl =
|
|
1145
|
+
const decl = lookup(name);
|
|
932
1146
|
out.push(makeSymbolNode(fId, name, false, span, decl ? declarationText(decl) : {}, shapes.get(name)));
|
|
933
1147
|
}
|
|
934
1148
|
return out;
|
|
@@ -936,7 +1150,7 @@ function buildSymbolNodes(repoRoot, sourceFile, file) {
|
|
|
936
1150
|
|
|
937
1151
|
// src/extractors/dynamic-imports.ts
|
|
938
1152
|
import {
|
|
939
|
-
Node as
|
|
1153
|
+
Node as Node4,
|
|
940
1154
|
SyntaxKind as SyntaxKind2
|
|
941
1155
|
} from "ts-morph";
|
|
942
1156
|
function dynamicImportSpecifiers(sourceFile) {
|
|
@@ -968,12 +1182,12 @@ function dynamicImportBindings(sourceFile) {
|
|
|
968
1182
|
function destructuredTargetOf(call) {
|
|
969
1183
|
const parent = call.getParent();
|
|
970
1184
|
if (!parent) return void 0;
|
|
971
|
-
const init =
|
|
1185
|
+
const init = Node4.isAwaitExpression(parent) ? parent : call;
|
|
972
1186
|
const decl = init.getParent();
|
|
973
|
-
if (!decl || !
|
|
1187
|
+
if (!decl || !Node4.isVariableDeclaration(decl)) return void 0;
|
|
974
1188
|
if (decl.getInitializer() !== init) return void 0;
|
|
975
1189
|
const nameNode = decl.getNameNode();
|
|
976
|
-
return
|
|
1190
|
+
return Node4.isObjectBindingPattern(nameNode) ? nameNode : void 0;
|
|
977
1191
|
}
|
|
978
1192
|
function destructuredNames(pattern) {
|
|
979
1193
|
const names = [];
|
|
@@ -1000,8 +1214,7 @@ function recordDynamicSymbolRefEdges(sourceFile, srcFileId, agg, resolveSpecifie
|
|
|
1000
1214
|
}
|
|
1001
1215
|
|
|
1002
1216
|
// src/extractors/module-resolution.ts
|
|
1003
|
-
import * as
|
|
1004
|
-
import { existsSync } from "fs";
|
|
1217
|
+
import * as path4 from "path";
|
|
1005
1218
|
var RESOLVABLE_EXTS = [
|
|
1006
1219
|
".ts",
|
|
1007
1220
|
".tsx",
|
|
@@ -1023,45 +1236,45 @@ function* relativeResolutionCandidates(base) {
|
|
|
1023
1236
|
const stem = base.slice(0, base.length - jsExt[0].length);
|
|
1024
1237
|
for (const ext of RESOLVABLE_EXTS) yield stem + ext;
|
|
1025
1238
|
}
|
|
1026
|
-
for (const ext of RESOLVABLE_EXTS) yield
|
|
1239
|
+
for (const ext of RESOLVABLE_EXTS) yield path4.join(base, "index" + ext);
|
|
1027
1240
|
}
|
|
1028
|
-
function resolveRelativeAbs(
|
|
1241
|
+
function resolveRelativeAbs(fs2, fromAbs, specifier) {
|
|
1029
1242
|
if (!isRelativeSpecifier(specifier)) return null;
|
|
1030
|
-
const base =
|
|
1243
|
+
const base = path4.resolve(path4.dirname(fromAbs), specifier);
|
|
1031
1244
|
for (const candidate of relativeResolutionCandidates(base)) {
|
|
1032
|
-
if (
|
|
1245
|
+
if (fs2.fileExistsSync(candidate)) return candidate;
|
|
1033
1246
|
}
|
|
1034
1247
|
return null;
|
|
1035
1248
|
}
|
|
1036
1249
|
function inRepoFileId(repoRoot, abs) {
|
|
1037
|
-
const
|
|
1038
|
-
if (
|
|
1039
|
-
if (
|
|
1250
|
+
const relative5 = path4.relative(repoRoot, abs);
|
|
1251
|
+
if (relative5.startsWith("..") || path4.isAbsolute(relative5)) return null;
|
|
1252
|
+
if (relative5.split(path4.sep).includes("node_modules")) return null;
|
|
1040
1253
|
return fileId(repoRoot, abs);
|
|
1041
1254
|
}
|
|
1042
|
-
function remapDistToSrc(abs) {
|
|
1255
|
+
function remapDistToSrc(abs, fileExists) {
|
|
1043
1256
|
const m = /^(.*)[\\/]dist[\\/](.+)\.d\.ts$/.exec(abs);
|
|
1044
1257
|
if (!m) return abs;
|
|
1045
1258
|
const base = m[1];
|
|
1046
1259
|
const sub = m[2];
|
|
1047
1260
|
for (const ext of [".ts", ".tsx"]) {
|
|
1048
|
-
const candidate =
|
|
1049
|
-
if (
|
|
1261
|
+
const candidate = path4.join(base, "src", sub + ext);
|
|
1262
|
+
if (fileExists(candidate)) return candidate;
|
|
1050
1263
|
}
|
|
1051
1264
|
return abs;
|
|
1052
1265
|
}
|
|
1053
1266
|
function repoTypeRoots(repoRoot) {
|
|
1054
1267
|
const roots = [];
|
|
1055
|
-
for (let dir =
|
|
1056
|
-
roots.push(
|
|
1057
|
-
if (
|
|
1268
|
+
for (let dir = path4.resolve(repoRoot); ; dir = path4.dirname(dir)) {
|
|
1269
|
+
roots.push(path4.join(dir, "node_modules", "@types"));
|
|
1270
|
+
if (path4.dirname(dir) === dir) return roots;
|
|
1058
1271
|
}
|
|
1059
1272
|
}
|
|
1060
1273
|
|
|
1061
1274
|
// src/extractors/reexport-resolve.ts
|
|
1062
1275
|
function resolveReExportOrigin(ctx, importerAbs, specifier, name) {
|
|
1063
|
-
const
|
|
1064
|
-
const targetAbs = resolveRelativeAbs(
|
|
1276
|
+
const fs2 = ctx.project.getFileSystem();
|
|
1277
|
+
const targetAbs = resolveRelativeAbs(fs2, importerAbs, specifier);
|
|
1065
1278
|
return targetAbs ? walkReExports(ctx, targetAbs, name, /* @__PURE__ */ new Set()) : null;
|
|
1066
1279
|
}
|
|
1067
1280
|
function walkReExports(ctx, fileAbs, name, visited) {
|
|
@@ -1073,13 +1286,13 @@ function walkReExports(ctx, fileAbs, name, visited) {
|
|
|
1073
1286
|
const id = inRepoFileId(ctx.repoRoot, fileAbs);
|
|
1074
1287
|
return id ? symbolId(id, name) : null;
|
|
1075
1288
|
}
|
|
1076
|
-
const
|
|
1289
|
+
const fs2 = ctx.project.getFileSystem();
|
|
1077
1290
|
for (const ed of sf.getExportDeclarations()) {
|
|
1078
1291
|
const spec = ed.getModuleSpecifierValue();
|
|
1079
1292
|
if (!spec) continue;
|
|
1080
1293
|
const inward = reExportInwardName(ed, name);
|
|
1081
1294
|
if (inward === null) continue;
|
|
1082
|
-
const targetAbs = resolveRelativeAbs(
|
|
1295
|
+
const targetAbs = resolveRelativeAbs(fs2, fileAbs, spec);
|
|
1083
1296
|
if (!targetAbs) continue;
|
|
1084
1297
|
const origin = walkReExports(ctx, targetAbs, inward, visited);
|
|
1085
1298
|
if (origin) return origin;
|
|
@@ -1105,57 +1318,6 @@ function reExportInwardName(ed, name) {
|
|
|
1105
1318
|
|
|
1106
1319
|
// src/extractors/ts-calls.ts
|
|
1107
1320
|
import { Node as Node5, SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
1108
|
-
|
|
1109
|
-
// src/extractors/ts-scope.ts
|
|
1110
|
-
import { Node as Node4 } from "ts-morph";
|
|
1111
|
-
function bindingName2(node) {
|
|
1112
|
-
const parent = node.getParent();
|
|
1113
|
-
if (Node4.isVariableDeclaration(parent) || Node4.isPropertyDeclaration(parent)) return parent.getName();
|
|
1114
|
-
if (Node4.isPropertyAssignment(parent)) return parent.getNameNode().getText();
|
|
1115
|
-
if (Node4.isExportAssignment(parent)) return "default";
|
|
1116
|
-
return null;
|
|
1117
|
-
}
|
|
1118
|
-
function declaredName(node) {
|
|
1119
|
-
const name = node.getName();
|
|
1120
|
-
if (name) return name;
|
|
1121
|
-
return Node4.isExportable(node) && node.isDefaultExport() ? "default" : null;
|
|
1122
|
-
}
|
|
1123
|
-
function scopeSegment2(node) {
|
|
1124
|
-
if (Node4.isClassDeclaration(node) || Node4.isFunctionDeclaration(node)) return declaredName(node);
|
|
1125
|
-
if (Node4.isMethodDeclaration(node) || Node4.isGetAccessorDeclaration(node) || Node4.isSetAccessorDeclaration(node)) {
|
|
1126
|
-
return node.getName();
|
|
1127
|
-
}
|
|
1128
|
-
if (Node4.isConstructorDeclaration(node)) return "constructor";
|
|
1129
|
-
if (Node4.isModuleDeclaration(node)) return node.hasNamespaceKeyword() ? node.getName() : null;
|
|
1130
|
-
if (Node4.isClassExpression(node) && node.getName()) return node.getName() ?? null;
|
|
1131
|
-
const bound = Node4.isArrowFunction(node) || Node4.isFunctionExpression(node) || Node4.isClassExpression(node) || Node4.isObjectLiteralExpression(node);
|
|
1132
|
-
return bound ? bindingName2(node) ?? ANONYMOUS_SCOPE : null;
|
|
1133
|
-
}
|
|
1134
|
-
function scopeOf(node) {
|
|
1135
|
-
const segments = node.getAncestors().map(scopeSegment2).filter((s) => s !== null).reverse();
|
|
1136
|
-
let scope = "";
|
|
1137
|
-
for (const segment of segments) {
|
|
1138
|
-
const repeatsAnonymous = segment === ANONYMOUS_SCOPE && (scope === ANONYMOUS_SCOPE || scope.endsWith(`.${ANONYMOUS_SCOPE}`));
|
|
1139
|
-
if (!repeatsAnonymous) scope = qualify(scope, segment);
|
|
1140
|
-
}
|
|
1141
|
-
return scope;
|
|
1142
|
-
}
|
|
1143
|
-
function declarationQualifiedName(decl) {
|
|
1144
|
-
if (Node4.isVariableDeclaration(decl)) {
|
|
1145
|
-
const init = decl.getInitializer();
|
|
1146
|
-
const callable = Node4.isArrowFunction(init) || Node4.isFunctionExpression(init);
|
|
1147
|
-
return callable ? qualify(scopeOf(decl), decl.getName()) : null;
|
|
1148
|
-
}
|
|
1149
|
-
if (Node4.isArrowFunction(decl) || Node4.isFunctionExpression(decl)) {
|
|
1150
|
-
const parent = decl.getParent();
|
|
1151
|
-
return Node4.isVariableDeclaration(parent) ? qualify(scopeOf(parent), parent.getName()) : null;
|
|
1152
|
-
}
|
|
1153
|
-
const isDeclaration = Node4.isFunctionDeclaration(decl) || Node4.isClassDeclaration(decl) || Node4.isMethodDeclaration(decl) || Node4.isGetAccessorDeclaration(decl) || Node4.isSetAccessorDeclaration(decl) || Node4.isConstructorDeclaration(decl);
|
|
1154
|
-
const own = isDeclaration ? scopeSegment2(decl) : null;
|
|
1155
|
-
return own ? qualify(scopeOf(decl), own) : null;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// src/extractors/ts-calls.ts
|
|
1159
1321
|
function collectTsCallEdges(sourceFile, ctx) {
|
|
1160
1322
|
const agg = /* @__PURE__ */ new Map();
|
|
1161
1323
|
sourceFile.forEachDescendant((node) => {
|
|
@@ -1181,7 +1343,7 @@ function resolveCallee(call, ctx) {
|
|
|
1181
1343
|
}
|
|
1182
1344
|
function declarationId(decl, ctx) {
|
|
1183
1345
|
const sf = decl.getSourceFile();
|
|
1184
|
-
const abs = remapDistToSrc(sf.getFilePath());
|
|
1346
|
+
const abs = remapDistToSrc(sf.getFilePath(), ctx.fileExists);
|
|
1185
1347
|
if (sf.isDeclarationFile() && abs === sf.getFilePath()) return null;
|
|
1186
1348
|
const fileId2 = inRepoFileId(ctx.repoRoot, abs);
|
|
1187
1349
|
const name = fileId2 ? declarationQualifiedName(decl) : null;
|
|
@@ -1223,33 +1385,55 @@ function literalText(arg) {
|
|
|
1223
1385
|
}
|
|
1224
1386
|
|
|
1225
1387
|
// src/extractors/ts-morph-extractor.ts
|
|
1388
|
+
var RETIRE_BATCH = 100;
|
|
1226
1389
|
var TsMorphGraphExtractor = class {
|
|
1227
1390
|
name = "ts-morph-graph";
|
|
1228
1391
|
repoRoot;
|
|
1229
1392
|
tsConfigPath;
|
|
1230
1393
|
project;
|
|
1394
|
+
ownsProject;
|
|
1395
|
+
source;
|
|
1396
|
+
retired = [];
|
|
1397
|
+
fileExists = (abs) => this.source.fileExists(abs);
|
|
1231
1398
|
constructor(options) {
|
|
1232
1399
|
this.repoRoot = options.repoRoot;
|
|
1233
1400
|
this.tsConfigPath = options.tsConfigPath;
|
|
1234
1401
|
this.project = options.project;
|
|
1402
|
+
this.ownsProject = options.project === void 0;
|
|
1403
|
+
this.source = options.source ?? workingTreeSource();
|
|
1235
1404
|
}
|
|
1236
1405
|
extract(file) {
|
|
1237
1406
|
if (!isTypeScriptFile(file)) return [];
|
|
1238
1407
|
const project = this.ensureProject();
|
|
1239
1408
|
const sourceFile = this.loadSourceFile(project, file);
|
|
1409
|
+
try {
|
|
1410
|
+
return this.extractFrom(sourceFile, file);
|
|
1411
|
+
} finally {
|
|
1412
|
+
if (this.ownsProject) this.retire(project, sourceFile);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
retire(project, sourceFile) {
|
|
1416
|
+
this.retired.push(sourceFile);
|
|
1417
|
+
if (this.retired.length < RETIRE_BATCH) return;
|
|
1418
|
+
for (const sf of this.retired.splice(0)) project.removeSourceFile(sf);
|
|
1419
|
+
}
|
|
1420
|
+
extractFrom(sourceFile, file) {
|
|
1240
1421
|
const nodes = this.buildFileAndModuleNodes(sourceFile);
|
|
1241
1422
|
const symbolNodes2 = buildSymbolNodes(this.repoRoot, sourceFile, file);
|
|
1242
1423
|
const { edges, externalNodes } = this.collectEdges(sourceFile);
|
|
1243
1424
|
const calls = collectTsCallEdges(sourceFile, {
|
|
1244
1425
|
repoRoot: this.repoRoot,
|
|
1245
1426
|
srcFileId: fileId(this.repoRoot, sourceFile.getFilePath()),
|
|
1246
|
-
localSymbols: new Set(symbolNodes2.map((n) => n.name))
|
|
1427
|
+
localSymbols: new Set(symbolNodes2.map((n) => n.name)),
|
|
1428
|
+
fileExists: this.fileExists
|
|
1247
1429
|
});
|
|
1248
1430
|
return [{ nodes: [...nodes, ...symbolNodes2, ...externalNodes], edges: [...edges, ...calls] }];
|
|
1249
1431
|
}
|
|
1250
1432
|
ensureProject() {
|
|
1251
1433
|
if (this.project) return this.project;
|
|
1252
|
-
|
|
1434
|
+
const fileSystem = this.source.fileSystem;
|
|
1435
|
+
this.project = this.tsConfigPath ? new Project2({ tsConfigFilePath: this.tsConfigPath, fileSystem }) : new Project2({
|
|
1436
|
+
fileSystem,
|
|
1253
1437
|
compilerOptions: {
|
|
1254
1438
|
allowJs: true,
|
|
1255
1439
|
target: ScriptTarget.ESNext,
|
|
@@ -1350,7 +1534,7 @@ var TsMorphGraphExtractor = class {
|
|
|
1350
1534
|
if (targetSf) {
|
|
1351
1535
|
const decls = targetSf.getExportedDeclarations().get(importedName);
|
|
1352
1536
|
const originSf = decls && decls.length > 0 ? decls[0].getSourceFile() : targetSf;
|
|
1353
|
-
const originId = this.inRepoFileId(remapDistToSrc(originSf.getFilePath()));
|
|
1537
|
+
const originId = this.inRepoFileId(remapDistToSrc(originSf.getFilePath(), this.fileExists));
|
|
1354
1538
|
return originId ? symbolId(originId, importedName) : null;
|
|
1355
1539
|
}
|
|
1356
1540
|
return resolveReExportOrigin(
|
|
@@ -1389,7 +1573,7 @@ var TsMorphGraphExtractor = class {
|
|
|
1389
1573
|
}
|
|
1390
1574
|
resolveInternal(target) {
|
|
1391
1575
|
if (!target) return null;
|
|
1392
|
-
return this.inRepoFileId(remapDistToSrc(target.getFilePath()));
|
|
1576
|
+
return this.inRepoFileId(remapDistToSrc(target.getFilePath(), this.fileExists));
|
|
1393
1577
|
}
|
|
1394
1578
|
/**
|
|
1395
1579
|
* Resolve a relative specifier ts-morph failed to link by walking the
|
|
@@ -1417,14 +1601,13 @@ function isTypeScriptFile(file) {
|
|
|
1417
1601
|
}
|
|
1418
1602
|
|
|
1419
1603
|
// src/extractors/python-extractor.ts
|
|
1420
|
-
import * as
|
|
1421
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1604
|
+
import * as path5 from "path";
|
|
1422
1605
|
|
|
1423
1606
|
// src/extractors/python-calls.ts
|
|
1424
|
-
function collectPythonCallEdges(file, fileId2,
|
|
1607
|
+
function collectPythonCallEdges(file, fileId2, resolve5) {
|
|
1425
1608
|
const declarations = /* @__PURE__ */ new Map();
|
|
1426
1609
|
forEachDeclaration(file, (_name, qualifiedName, node) => declarations.set(qualifiedName, node));
|
|
1427
|
-
const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode,
|
|
1610
|
+
const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode, resolve5) };
|
|
1428
1611
|
const agg = /* @__PURE__ */ new Map();
|
|
1429
1612
|
walkScopes(file.tree.rootNode, true, (node, scope) => {
|
|
1430
1613
|
if (node.type !== "call") return;
|
|
@@ -1488,11 +1671,11 @@ function parameterNames(def) {
|
|
|
1488
1671
|
}
|
|
1489
1672
|
return out;
|
|
1490
1673
|
}
|
|
1491
|
-
function fromImports(root,
|
|
1674
|
+
function fromImports(root, resolve5) {
|
|
1492
1675
|
const out = /* @__PURE__ */ new Map();
|
|
1493
1676
|
for (const stmt of root.descendantsOfType("import_from_statement")) {
|
|
1494
1677
|
const moduleName = stmt?.childForFieldName("module_name")?.text;
|
|
1495
|
-
const moduleFile = moduleName ?
|
|
1678
|
+
const moduleFile = moduleName ? resolve5(moduleName) : null;
|
|
1496
1679
|
if (!stmt || !moduleFile) continue;
|
|
1497
1680
|
for (const imported of stmt.childrenForFieldName("name")) {
|
|
1498
1681
|
const alias = imported?.type === "aliased_import" ? imported.childForFieldName("alias")?.text : void 0;
|
|
@@ -1534,13 +1717,15 @@ function addKeyword(site, arg) {
|
|
|
1534
1717
|
// src/extractors/python-extractor.ts
|
|
1535
1718
|
function* candidates(base) {
|
|
1536
1719
|
yield `${base}.py`;
|
|
1537
|
-
yield
|
|
1720
|
+
yield path5.join(base, "__init__.py");
|
|
1538
1721
|
}
|
|
1539
1722
|
var PythonGraphExtractor = class {
|
|
1540
|
-
constructor(repoRoot) {
|
|
1723
|
+
constructor(repoRoot, source = workingTreeSource()) {
|
|
1541
1724
|
this.repoRoot = repoRoot;
|
|
1725
|
+
this.source = source;
|
|
1542
1726
|
}
|
|
1543
1727
|
repoRoot;
|
|
1728
|
+
source;
|
|
1544
1729
|
name = "python-graph";
|
|
1545
1730
|
extract(file) {
|
|
1546
1731
|
if (file.language !== "python") return [];
|
|
@@ -1572,12 +1757,12 @@ var PythonGraphExtractor = class {
|
|
|
1572
1757
|
resolveDotted(fromAbs, specifier) {
|
|
1573
1758
|
const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
|
|
1574
1759
|
const parts = specifier.slice(leadingDots).split(".").filter(Boolean);
|
|
1575
|
-
const relativeBase =
|
|
1576
|
-
const roots = leadingDots > 0 ? [relativeBase] : [
|
|
1760
|
+
const relativeBase = path5.resolve(path5.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
|
|
1761
|
+
const roots = leadingDots > 0 ? [relativeBase] : [path5.dirname(fromAbs), this.repoRoot];
|
|
1577
1762
|
for (const root of roots) {
|
|
1578
|
-
for (const candidate of candidates(
|
|
1763
|
+
for (const candidate of candidates(path5.join(root, ...parts))) {
|
|
1579
1764
|
const id = inRepoFileId(this.repoRoot, candidate);
|
|
1580
|
-
if (id !== null &&
|
|
1765
|
+
if (id !== null && this.source.fileExists(candidate)) return id;
|
|
1581
1766
|
}
|
|
1582
1767
|
}
|
|
1583
1768
|
return null;
|
|
@@ -1621,9 +1806,10 @@ var LanguageExtractor = class {
|
|
|
1621
1806
|
name = "language-graph";
|
|
1622
1807
|
delegates;
|
|
1623
1808
|
constructor(options) {
|
|
1809
|
+
const { repoRoot, tsConfigPath, source } = options;
|
|
1624
1810
|
this.delegates = [
|
|
1625
|
-
new TsMorphGraphExtractor({ repoRoot
|
|
1626
|
-
new PythonGraphExtractor(
|
|
1811
|
+
new TsMorphGraphExtractor({ repoRoot, tsConfigPath, source }),
|
|
1812
|
+
new PythonGraphExtractor(repoRoot, source)
|
|
1627
1813
|
];
|
|
1628
1814
|
}
|
|
1629
1815
|
extract(file) {
|
|
@@ -1632,7 +1818,7 @@ var LanguageExtractor = class {
|
|
|
1632
1818
|
};
|
|
1633
1819
|
|
|
1634
1820
|
// src/git-renames.ts
|
|
1635
|
-
import * as
|
|
1821
|
+
import * as path6 from "path";
|
|
1636
1822
|
function detectGitHead(repoRoot) {
|
|
1637
1823
|
return runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
1638
1824
|
}
|
|
@@ -1676,12 +1862,12 @@ function parseRenameOutput(text) {
|
|
|
1676
1862
|
return out;
|
|
1677
1863
|
}
|
|
1678
1864
|
function classifyRename(oldPath, newPath) {
|
|
1679
|
-
return
|
|
1865
|
+
return path6.posix.dirname(oldPath) === path6.posix.dirname(newPath) ? "rename" : "move";
|
|
1680
1866
|
}
|
|
1681
1867
|
function renameToAliases(repoRoot, pair) {
|
|
1682
1868
|
const reason = classifyRename(pair.oldPath, pair.newPath);
|
|
1683
|
-
const oldAbs =
|
|
1684
|
-
const newAbs =
|
|
1869
|
+
const oldAbs = path6.join(repoRoot, pair.oldPath);
|
|
1870
|
+
const newAbs = path6.join(repoRoot, pair.newPath);
|
|
1685
1871
|
return [
|
|
1686
1872
|
{ oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
|
|
1687
1873
|
{ oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
|
|
@@ -1774,8 +1960,8 @@ function walk(requestedId, maps) {
|
|
|
1774
1960
|
}
|
|
1775
1961
|
function chainSteps(input) {
|
|
1776
1962
|
if (input.from === null) return { steps: rootPath(input.lineage, input.to, input.maxHops), connected: true };
|
|
1777
|
-
const
|
|
1778
|
-
if (
|
|
1963
|
+
const path14 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
|
|
1964
|
+
if (path14) return { steps: path14, connected: true };
|
|
1779
1965
|
return { steps: [{ snapshotId: input.to, direction: "forward" }], connected: false };
|
|
1780
1966
|
}
|
|
1781
1967
|
function createAliasChain(input) {
|
|
@@ -1860,9 +2046,11 @@ function computeAliasBridge(store, input) {
|
|
|
1860
2046
|
return { baseSnapshotId: base.id, aliases: [...fileAliases, ...symbols] };
|
|
1861
2047
|
}
|
|
1862
2048
|
|
|
2049
|
+
// src/roles.ts
|
|
2050
|
+
import * as path8 from "path";
|
|
2051
|
+
|
|
1863
2052
|
// src/generated.ts
|
|
1864
|
-
import
|
|
1865
|
-
import * as path6 from "path";
|
|
2053
|
+
import * as path7 from "path";
|
|
1866
2054
|
var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
|
|
1867
2055
|
var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
|
|
1868
2056
|
function isGeneratedByHeuristic(id) {
|
|
@@ -1872,59 +2060,112 @@ function isGeneratedFile(id, patterns) {
|
|
|
1872
2060
|
for (const rx of patterns) if (rx.test(id)) return true;
|
|
1873
2061
|
return isGeneratedByHeuristic(id);
|
|
1874
2062
|
}
|
|
1875
|
-
function loadGeneratedPatterns(rootDir) {
|
|
2063
|
+
function loadGeneratedPatterns(rootDir, source = workingTreeSource()) {
|
|
1876
2064
|
try {
|
|
1877
|
-
const content =
|
|
2065
|
+
const content = source.readFile(path7.join(rootDir, ".gitattributes"));
|
|
1878
2066
|
return parseGeneratedPatterns(content);
|
|
1879
2067
|
} catch {
|
|
1880
2068
|
return [];
|
|
1881
2069
|
}
|
|
1882
2070
|
}
|
|
1883
2071
|
function parseGeneratedPatterns(content) {
|
|
2072
|
+
const rules = parseGeneratedRules(content);
|
|
1884
2073
|
const out = [];
|
|
2074
|
+
rules.forEach((rule, i) => {
|
|
2075
|
+
if (!rule.generated) return;
|
|
2076
|
+
const optOuts = rules.slice(i + 1).filter((r) => !r.generated);
|
|
2077
|
+
out.push(excluding(rule.rx, optOuts.map((r) => r.rx)));
|
|
2078
|
+
});
|
|
2079
|
+
return out;
|
|
2080
|
+
}
|
|
2081
|
+
function parseGeneratedRules(content) {
|
|
2082
|
+
const rules = [];
|
|
1885
2083
|
for (const raw of content.split("\n")) {
|
|
1886
2084
|
const line = raw.trim();
|
|
1887
2085
|
if (!line || line.startsWith("#")) continue;
|
|
1888
2086
|
const parsed = /^("(?:[^"\\]|\\.)*"|\S+)\s*(.*)$/.exec(line);
|
|
1889
2087
|
if (!parsed) continue;
|
|
1890
|
-
const
|
|
1891
|
-
if (
|
|
1892
|
-
|
|
1893
|
-
}
|
|
2088
|
+
const generated = generatedAttr(parsed[2].split(/\s+/).filter(Boolean));
|
|
2089
|
+
if (generated === void 0) continue;
|
|
2090
|
+
rules.push({ rx: gitattributesPatternToRegex(unquote(parsed[1])), generated });
|
|
1894
2091
|
}
|
|
1895
|
-
return
|
|
2092
|
+
return rules;
|
|
2093
|
+
}
|
|
2094
|
+
function excluding(rx, optOuts) {
|
|
2095
|
+
if (optOuts.length === 0) return rx;
|
|
2096
|
+
const rejected = optOuts.map((o) => `(?:${o.source})`).join("|");
|
|
2097
|
+
return new RegExp(`(?!${rejected})(?:${rx.source})`);
|
|
1896
2098
|
}
|
|
1897
2099
|
function generatedAttr(attrs) {
|
|
1898
2100
|
let value;
|
|
1899
2101
|
for (const a of attrs) {
|
|
1900
2102
|
if (a === "linguist-generated" || a === "linguist-generated=true") value = true;
|
|
1901
|
-
else if (a
|
|
2103
|
+
else if (OPT_OUT_TOKENS.has(a)) value = false;
|
|
1902
2104
|
}
|
|
1903
2105
|
return value;
|
|
1904
2106
|
}
|
|
2107
|
+
var OPT_OUT_TOKENS = /* @__PURE__ */ new Set([
|
|
2108
|
+
"-linguist-generated",
|
|
2109
|
+
"linguist-generated=false",
|
|
2110
|
+
"!linguist-generated"
|
|
2111
|
+
]);
|
|
1905
2112
|
function unquote(token) {
|
|
1906
2113
|
return token.startsWith('"') && token.endsWith('"') ? token.slice(1, -1).replace(/\\(.)/g, "$1") : token;
|
|
1907
2114
|
}
|
|
2115
|
+
var NEVER_MATCHES = /(?!)/;
|
|
1908
2116
|
function gitattributesPatternToRegex(pattern) {
|
|
1909
2117
|
let p = pattern;
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2118
|
+
if (p.endsWith("/")) return NEVER_MATCHES;
|
|
2119
|
+
const anchored = p.includes("/");
|
|
2120
|
+
if (p.startsWith("/")) p = p.slice(1);
|
|
2121
|
+
const prefix = anchored ? "^" : "^(?:.*/)?";
|
|
2122
|
+
return new RegExp(prefix + globBody(p) + "$");
|
|
2123
|
+
}
|
|
2124
|
+
function globBody(p) {
|
|
2125
|
+
const segments = p.split("/");
|
|
2126
|
+
return segments.map((seg, i) => {
|
|
2127
|
+
const last = i === segments.length - 1;
|
|
2128
|
+
if (seg === "**") return last ? ".+" : "(?:.*/)?";
|
|
2129
|
+
return globSegment(seg) + (last ? "" : "/");
|
|
2130
|
+
}).join("");
|
|
2131
|
+
}
|
|
2132
|
+
function globSegment(seg) {
|
|
2133
|
+
let out = "";
|
|
2134
|
+
for (const c of seg) {
|
|
2135
|
+
if (c === "*") out += "[^/]*";
|
|
2136
|
+
else if (c === "?") out += "[^/]";
|
|
2137
|
+
else out += c.replace(/[.+(){}|^$\\[\]]/, "\\$&");
|
|
2138
|
+
}
|
|
2139
|
+
return out;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// src/role-globs.ts
|
|
2143
|
+
function parseRoleGlobs(value, label, knownRoles) {
|
|
2144
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2145
|
+
throw new Error(`${label}: expected an object mapping roles to glob arrays`);
|
|
2146
|
+
}
|
|
2147
|
+
const out = {};
|
|
2148
|
+
for (const [role, globs] of Object.entries(value)) {
|
|
2149
|
+
if (!knownRoles.includes(role)) {
|
|
2150
|
+
throw new Error(`${label}: unknown role "${role}"`);
|
|
2151
|
+
}
|
|
2152
|
+
if (!Array.isArray(globs) || !globs.every((g) => typeof g === "string")) {
|
|
2153
|
+
throw new Error(`${label}: "${role}" must be an array of glob strings`);
|
|
2154
|
+
}
|
|
2155
|
+
out[role] = globs;
|
|
2156
|
+
}
|
|
2157
|
+
return out;
|
|
2158
|
+
}
|
|
2159
|
+
function roleGlobMatcher(globs, order) {
|
|
2160
|
+
const compiled = order.flatMap(
|
|
2161
|
+
(role) => (globs?.[role] ?? []).map((g) => ({ role, rx: gitattributesPatternToRegex(g) }))
|
|
2162
|
+
);
|
|
2163
|
+
return (id) => compiled.find(({ rx }) => rx.test(id))?.role;
|
|
1924
2164
|
}
|
|
1925
2165
|
|
|
1926
2166
|
// src/roles.ts
|
|
1927
2167
|
var TEST_RE = /(?:^|\/)(?:__tests__\/|tests?\/)|\.(?:test|spec)(?:\.[a-z]+)?$/;
|
|
2168
|
+
var STORY_RE = /\.stories(?:\.[cm]?[jt]sx?)?$|\.mdx$/;
|
|
1928
2169
|
var FIXTURE_RE = /(?:^|\/)fixtures(?:\/|$)/;
|
|
1929
2170
|
var SCRIPT_RE = /(?:^|\/)(?:scripts|archive)(?:\/|$)/;
|
|
1930
2171
|
var BARREL_RE = /(?:^|\/)index(?:\.[a-z]+)?$/;
|
|
@@ -1933,6 +2174,8 @@ var CONFIG_RE = /\.config(?:\.[a-z]+)?$/;
|
|
|
1933
2174
|
var ALL_ROLES = [
|
|
1934
2175
|
"test",
|
|
1935
2176
|
"fixture",
|
|
2177
|
+
"story",
|
|
2178
|
+
"lab",
|
|
1936
2179
|
"barrel",
|
|
1937
2180
|
"types",
|
|
1938
2181
|
"config",
|
|
@@ -1943,7 +2186,9 @@ var ALL_ROLES = [
|
|
|
1943
2186
|
];
|
|
1944
2187
|
function classifyRole(id, hints) {
|
|
1945
2188
|
if (hints?.isGenerated) return "generated";
|
|
2189
|
+
if (hints?.configuredRole) return hints.configuredRole;
|
|
1946
2190
|
if (TEST_RE.test(id)) return "test";
|
|
2191
|
+
if (STORY_RE.test(id)) return "story";
|
|
1947
2192
|
if (FIXTURE_RE.test(id)) return "fixture";
|
|
1948
2193
|
if (SCRIPT_RE.test(id)) return "script";
|
|
1949
2194
|
if (hints?.hasShebang) return "entry";
|
|
@@ -1952,8 +2197,14 @@ function classifyRole(id, hints) {
|
|
|
1952
2197
|
if (CONFIG_RE.test(id)) return "config";
|
|
1953
2198
|
return "source";
|
|
1954
2199
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
2200
|
+
var ROLE_GLOBS_PATH = ".codewatch/roles.json";
|
|
2201
|
+
function loadRoleGlobs(rootDir, source = workingTreeSource()) {
|
|
2202
|
+
const file = path8.join(rootDir, ROLE_GLOBS_PATH);
|
|
2203
|
+
if (!source.fileExists(file)) return {};
|
|
2204
|
+
return parseRoleGlobs(JSON.parse(source.readFile(file)), ROLE_GLOBS_PATH, ALL_ROLES);
|
|
2205
|
+
}
|
|
2206
|
+
function computeRoleHints(readFiles, idRoot, toId, source) {
|
|
2207
|
+
const patterns = loadGeneratedPatterns(idRoot, source);
|
|
1957
2208
|
const shebangIds = /* @__PURE__ */ new Set();
|
|
1958
2209
|
const generatedIds = /* @__PURE__ */ new Set();
|
|
1959
2210
|
for (const rf of readFiles) {
|
|
@@ -1961,47 +2212,320 @@ function computeRoleHints(readFiles, idRoot, toId) {
|
|
|
1961
2212
|
if (rf.content.startsWith("#!")) shebangIds.add(id);
|
|
1962
2213
|
if (isGeneratedFile(id, patterns)) generatedIds.add(id);
|
|
1963
2214
|
}
|
|
1964
|
-
return { shebangIds, generatedIds };
|
|
2215
|
+
return { shebangIds, generatedIds, roleGlobs: loadRoleGlobs(idRoot, source) };
|
|
1965
2216
|
}
|
|
1966
2217
|
function annotateRoles(nodes, options) {
|
|
2218
|
+
const configuredRole = roleGlobMatcher(options?.roleGlobs, ALL_ROLES);
|
|
1967
2219
|
return nodes.map(
|
|
1968
2220
|
(n) => n.kind === "file" || n.kind === "module" ? {
|
|
1969
2221
|
...n,
|
|
1970
2222
|
role: n.role ?? classifyRole(n.id, {
|
|
1971
2223
|
hasShebang: options?.shebangIds?.has(n.id) ?? false,
|
|
1972
|
-
isGenerated: options?.generatedIds?.has(n.id) ?? false
|
|
2224
|
+
isGenerated: options?.generatedIds?.has(n.id) ?? false,
|
|
2225
|
+
configuredRole: configuredRole(n.id)
|
|
1973
2226
|
})
|
|
1974
2227
|
} : n
|
|
1975
2228
|
);
|
|
1976
2229
|
}
|
|
1977
2230
|
|
|
1978
|
-
// src/
|
|
1979
|
-
import
|
|
1980
|
-
import * as
|
|
1981
|
-
import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
|
|
1982
|
-
|
|
2231
|
+
// src/git-tree-source.ts
|
|
2232
|
+
import { readlinkSync, realpathSync, statSync } from "fs";
|
|
2233
|
+
import * as path9 from "path";
|
|
2234
|
+
import { isExcludedDir as isExcludedDir2, shouldIncludeFile as shouldIncludeFile2 } from "@titan-design/code-parser";
|
|
2235
|
+
import { Project as Project3 } from "ts-morph";
|
|
2236
|
+
|
|
2237
|
+
// src/git-tree-host.ts
|
|
2238
|
+
function readOnly() {
|
|
2239
|
+
throw new Error("a git tree source is read-only");
|
|
2240
|
+
}
|
|
2241
|
+
function overlayHost(answers, real) {
|
|
2242
|
+
return {
|
|
2243
|
+
isCaseSensitive: () => real.isCaseSensitive(),
|
|
2244
|
+
readDirSync: answers.readDir,
|
|
2245
|
+
readFileSync: (p) => answers.readFile(p),
|
|
2246
|
+
readFile: async (p) => answers.readFile(p),
|
|
2247
|
+
fileExistsSync: answers.fileExists,
|
|
2248
|
+
fileExists: async (p) => answers.fileExists(p),
|
|
2249
|
+
directoryExistsSync: answers.directoryExists,
|
|
2250
|
+
directoryExists: async (p) => answers.directoryExists(p),
|
|
2251
|
+
realpathSync: answers.realpath,
|
|
2252
|
+
getCurrentDirectory: () => real.getCurrentDirectory(),
|
|
2253
|
+
glob: () => Promise.reject(new Error("glob is not supported on a git tree source")),
|
|
2254
|
+
globSync: () => readOnly(),
|
|
2255
|
+
delete: async () => readOnly(),
|
|
2256
|
+
deleteSync: readOnly,
|
|
2257
|
+
writeFile: async () => readOnly(),
|
|
2258
|
+
writeFileSync: readOnly,
|
|
2259
|
+
mkdir: async () => readOnly(),
|
|
2260
|
+
mkdirSync: readOnly,
|
|
2261
|
+
move: async () => readOnly(),
|
|
2262
|
+
moveSync: readOnly,
|
|
2263
|
+
copy: async () => readOnly(),
|
|
2264
|
+
copySync: readOnly
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// src/git-tree-source.ts
|
|
2269
|
+
var READ_BY_INDEX = /\.(?:[cm]?[jt]sx?|json|pyi?)$|(?:^|\/)\.gitattributes$/;
|
|
2270
|
+
var MAX_LINK_HOPS = 40;
|
|
2271
|
+
var emptyHost;
|
|
2272
|
+
function missing(abs, kind) {
|
|
2273
|
+
emptyHost ??= new Project3({ useInMemoryFileSystem: true }).getFileSystem();
|
|
2274
|
+
if (kind === "file") emptyHost.readFileSync(abs);
|
|
2275
|
+
else emptyHost.readDirSync(abs);
|
|
2276
|
+
throw new Error(`${abs} was expected to be missing`);
|
|
2277
|
+
}
|
|
2278
|
+
function linkTarget(abs) {
|
|
2279
|
+
try {
|
|
2280
|
+
return path9.resolve(path9.dirname(abs), readlinkSync(abs));
|
|
2281
|
+
} catch {
|
|
2282
|
+
return null;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
function relativeInside(root, abs) {
|
|
2286
|
+
const rel = path9.relative(root, abs);
|
|
2287
|
+
return rel === ".." || rel.startsWith(`..${path9.sep}`) || path9.isAbsolute(rel) ? null : rel;
|
|
2288
|
+
}
|
|
2289
|
+
function canonicalize(abs, hops = 0) {
|
|
2290
|
+
const rest = [];
|
|
2291
|
+
for (let at = path9.resolve(abs); ; at = path9.dirname(at)) {
|
|
2292
|
+
try {
|
|
2293
|
+
return path9.join(realpathSync(at), ...rest.reverse());
|
|
2294
|
+
} catch {
|
|
2295
|
+
const target = hops < MAX_LINK_HOPS ? linkTarget(at) : null;
|
|
2296
|
+
if (target !== null) return canonicalize(path9.join(target, ...rest.reverse()), hops + 1);
|
|
2297
|
+
if (path9.dirname(at) === at) return path9.resolve(abs);
|
|
2298
|
+
rest.push(path9.basename(at));
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
function firstExcluded(rel, includeSelf) {
|
|
2303
|
+
const segments = rel.split(path9.sep);
|
|
2304
|
+
return (includeSelf ? segments : segments.slice(0, -1)).findIndex(isExcludedDir2);
|
|
2305
|
+
}
|
|
2306
|
+
var CommitTree = class {
|
|
2307
|
+
constructor(root, commit) {
|
|
2308
|
+
this.root = root;
|
|
2309
|
+
for (const blob of listTreeBlobs(root, commit)) {
|
|
2310
|
+
const rel = blob.path.split("/").join(path9.sep);
|
|
2311
|
+
(blob.link ? this.links : this.oids).set(rel, blob.oid);
|
|
2312
|
+
this.addEntry(rel, blob.link ? "link" : "file");
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
root;
|
|
2316
|
+
oids = /* @__PURE__ */ new Map();
|
|
2317
|
+
links = /* @__PURE__ */ new Map();
|
|
2318
|
+
dirs = /* @__PURE__ */ new Map();
|
|
2319
|
+
canonical = /* @__PURE__ */ new Map();
|
|
2320
|
+
contents;
|
|
2321
|
+
/** The one place an answer's origin is decided; every file, dir and realpath answer goes through it. */
|
|
2322
|
+
locate(abs, kind) {
|
|
2323
|
+
return this.classify(this.resolve(path9.resolve(abs), 0), kind);
|
|
2324
|
+
}
|
|
2325
|
+
classify(canonical, kind) {
|
|
2326
|
+
const rel = relativeInside(this.root, canonical);
|
|
2327
|
+
if (rel === null) return { origin: "disk", canonical, rel: null };
|
|
2328
|
+
const tracked = kind === "file" ? this.oids.has(rel) : this.dirs.has(rel);
|
|
2329
|
+
if (tracked) return { origin: "tree", canonical, rel };
|
|
2330
|
+
return { origin: this.ownsBuildOutput(rel, kind === "dir") ? "disk" : "missing", canonical, rel };
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Untracked build output a checkout would see on disk: under an excluded dir
|
|
2334
|
+
* whose parent the commit tracks, so `untracked/dist` is as absent as `untracked`.
|
|
2335
|
+
* A tracked symlink only lands here when its chain loops or dangles, and is then absent.
|
|
2336
|
+
*/
|
|
2337
|
+
ownsBuildOutput(rel, includeSelf) {
|
|
2338
|
+
const at = firstExcluded(rel, includeSelf);
|
|
2339
|
+
if (at < 0 || this.links.has(rel)) return false;
|
|
2340
|
+
return this.dirs.has(rel.split(path9.sep).slice(0, at).join(path9.sep));
|
|
2341
|
+
}
|
|
2342
|
+
*files() {
|
|
2343
|
+
for (const rel of this.oids.keys()) yield path9.join(this.root, rel);
|
|
2344
|
+
}
|
|
2345
|
+
/** Children of a tracked dir by name, or undefined when the commit has no such dir. */
|
|
2346
|
+
children(rel) {
|
|
2347
|
+
return this.dirs.get(rel);
|
|
2348
|
+
}
|
|
2349
|
+
read(rel) {
|
|
2350
|
+
return this.blob(this.oids.get(rel));
|
|
2351
|
+
}
|
|
2352
|
+
/** Every link target joins the first batch: a checkout resolves links before it reads anything. */
|
|
2353
|
+
blob(oid) {
|
|
2354
|
+
this.contents ??= readBlobs(this.root, [
|
|
2355
|
+
...[...this.oids].filter(([r]) => READ_BY_INDEX.test(r)).map(([, o]) => o),
|
|
2356
|
+
...this.links.values()
|
|
2357
|
+
]);
|
|
2358
|
+
if (!this.contents.has(oid)) this.contents.set(oid, readBlobs(this.root, [oid]).get(oid));
|
|
2359
|
+
return this.contents.get(oid);
|
|
2360
|
+
}
|
|
2361
|
+
tracks(rel) {
|
|
2362
|
+
return rel !== null && (this.oids.has(rel) || this.dirs.has(rel));
|
|
2363
|
+
}
|
|
2364
|
+
resolve(abs, hops) {
|
|
2365
|
+
const parent = path9.dirname(abs);
|
|
2366
|
+
return parent === abs ? abs : this.step(this.resolve(parent, hops), path9.basename(abs), hops);
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Every component of every path and link target passes through here, memoized
|
|
2370
|
+
* per path and hop budget: a link named many times in nested targets is followed
|
|
2371
|
+
* once per budget, and a chain cut short by the budget never answers for a shorter one.
|
|
2372
|
+
*/
|
|
2373
|
+
step(dir, name, hops) {
|
|
2374
|
+
const key = `${hops}:${path9.join(dir, name)}`;
|
|
2375
|
+
let resolved = this.canonical.get(key);
|
|
2376
|
+
if (resolved === void 0) {
|
|
2377
|
+
resolved = this.stepOnce(dir, name, hops);
|
|
2378
|
+
this.canonical.set(key, resolved);
|
|
2379
|
+
}
|
|
2380
|
+
return resolved;
|
|
2381
|
+
}
|
|
2382
|
+
/** Disk is never consulted for a component the commit tracks, a symlink included. */
|
|
2383
|
+
stepOnce(dir, name, hops) {
|
|
2384
|
+
const next = path9.join(dir, name);
|
|
2385
|
+
const rel = relativeInside(this.root, next);
|
|
2386
|
+
if (this.tracks(rel)) return next;
|
|
2387
|
+
if (hops >= MAX_LINK_HOPS) return next;
|
|
2388
|
+
const linkOid = rel === null ? void 0 : this.links.get(rel);
|
|
2389
|
+
if (linkOid !== void 0) return this.follow(dir, this.blob(linkOid), hops + 1) ?? next;
|
|
2390
|
+
const target = linkTarget(next);
|
|
2391
|
+
return target === null ? next : this.resolve(target, hops + 1);
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* A tracked link's target walked one component at a time from `dir`, as a
|
|
2395
|
+
* checkout walks it: `..` leaves the dir actually reached, so `gone/..` dangles
|
|
2396
|
+
* (null) when `gone` is absent, where lexical normalisation would drop it.
|
|
2397
|
+
*/
|
|
2398
|
+
follow(dir, target, hops) {
|
|
2399
|
+
let at = path9.isAbsolute(target) ? path9.parse(path9.resolve(target)).root : dir;
|
|
2400
|
+
for (const name of target.split("/")) {
|
|
2401
|
+
if (name === "" || name === ".") continue;
|
|
2402
|
+
if (name !== "..") at = this.step(at, name, hops);
|
|
2403
|
+
else if (this.isDir(at)) at = path9.dirname(at);
|
|
2404
|
+
else return null;
|
|
2405
|
+
}
|
|
2406
|
+
return at;
|
|
2407
|
+
}
|
|
2408
|
+
isDir(canonical) {
|
|
2409
|
+
const { origin } = this.classify(canonical, "dir");
|
|
2410
|
+
if (origin !== "disk") return origin === "tree";
|
|
2411
|
+
return statSync(canonical, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
2412
|
+
}
|
|
2413
|
+
addEntry(rel, kind) {
|
|
2414
|
+
let child = rel;
|
|
2415
|
+
let childKind = kind;
|
|
2416
|
+
for (let dir = path9.dirname(rel); child !== "."; child = dir, dir = path9.dirname(dir)) {
|
|
2417
|
+
const key = dir === "." ? "" : dir;
|
|
2418
|
+
const known = this.dirs.has(key);
|
|
2419
|
+
const children = this.dirs.get(key) ?? /* @__PURE__ */ new Map();
|
|
2420
|
+
children.set(path9.basename(child), childKind);
|
|
2421
|
+
this.dirs.set(key, children);
|
|
2422
|
+
if (known) return;
|
|
2423
|
+
childKind = "dir";
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
function* ingestedUnder(tree, rootDir, languages) {
|
|
2428
|
+
for (const abs of tree.files()) {
|
|
2429
|
+
const rel = relativeInside(rootDir, abs);
|
|
2430
|
+
if (rel === null) continue;
|
|
2431
|
+
if (path9.dirname(rel).split(path9.sep).some(isExcludedDir2)) continue;
|
|
2432
|
+
if (shouldIncludeFile2(rel, [...languages])) yield abs;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
function listTreeFiles(tree, rootDirs, languages) {
|
|
1983
2436
|
const seen = /* @__PURE__ */ new Set();
|
|
1984
|
-
const out = [];
|
|
1985
2437
|
for (const rootDir of rootDirs) {
|
|
1986
|
-
|
|
2438
|
+
for (const abs of ingestedUnder(tree, rootDir, languages)) seen.add(abs);
|
|
1987
2439
|
}
|
|
1988
|
-
return
|
|
2440
|
+
return [...seen].sort();
|
|
1989
2441
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2442
|
+
function diskBuildDirs(real, canonical) {
|
|
2443
|
+
if (!real.directoryExistsSync(canonical)) return [];
|
|
2444
|
+
return real.readDirSync(canonical).filter((e) => e.isDirectory && isExcludedDir2(path9.basename(e.name)));
|
|
2445
|
+
}
|
|
2446
|
+
function treeEntry(at, kind, exists) {
|
|
2447
|
+
if (kind !== "link") return { name: at, isFile: kind === "file", isDirectory: kind === "dir", isSymlink: false };
|
|
2448
|
+
return { name: at, isFile: exists.file(at), isDirectory: exists.dir(at), isSymlink: true };
|
|
2449
|
+
}
|
|
2450
|
+
function readDirAt(tree, real, abs, exists) {
|
|
2451
|
+
const at = tree.locate(abs, "dir");
|
|
2452
|
+
const asAsked = (entry) => ({ ...entry, name: path9.join(path9.resolve(abs), path9.basename(entry.name)) });
|
|
2453
|
+
if (at.origin === "missing") return missing(abs, "dir");
|
|
2454
|
+
if (at.origin === "disk") return real.readDirSync(at.canonical).map(asAsked);
|
|
2455
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2456
|
+
for (const [name, kind] of tree.children(at.rel)) {
|
|
2457
|
+
entries.set(name, treeEntry(path9.join(path9.resolve(abs), name), kind, exists));
|
|
2458
|
+
}
|
|
2459
|
+
for (const entry of diskBuildDirs(real, at.canonical)) {
|
|
2460
|
+
const name = path9.basename(entry.name);
|
|
2461
|
+
if (!entries.has(name)) entries.set(name, asAsked(entry));
|
|
2004
2462
|
}
|
|
2463
|
+
return [...entries.values()];
|
|
2464
|
+
}
|
|
2465
|
+
function treeAnswers(tree, real) {
|
|
2466
|
+
const fileExists = (abs) => {
|
|
2467
|
+
const at = tree.locate(abs, "file");
|
|
2468
|
+
return at.origin === "tree" || at.origin === "disk" && real.fileExistsSync(at.canonical);
|
|
2469
|
+
};
|
|
2470
|
+
const directoryExists = (abs) => {
|
|
2471
|
+
const at = tree.locate(abs, "dir");
|
|
2472
|
+
return at.origin === "tree" || at.origin === "disk" && real.directoryExistsSync(at.canonical);
|
|
2473
|
+
};
|
|
2474
|
+
return {
|
|
2475
|
+
fileExists,
|
|
2476
|
+
readFile: (abs) => {
|
|
2477
|
+
const at = tree.locate(abs, "file");
|
|
2478
|
+
if (at.origin === "tree") return tree.read(at.rel);
|
|
2479
|
+
return at.origin === "disk" ? real.readFileSync(at.canonical, "utf-8") : missing(abs, "file");
|
|
2480
|
+
},
|
|
2481
|
+
directoryExists,
|
|
2482
|
+
readDir: (abs) => readDirAt(tree, real, abs, { file: fileExists, dir: directoryExists }),
|
|
2483
|
+
// Like the real host, realpath of an absent path throws.
|
|
2484
|
+
realpath: (abs) => fileExists(abs) || directoryExists(abs) ? tree.locate(abs, "file").canonical : missing(abs, "file")
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
function canonicalToplevel(repoRoot) {
|
|
2488
|
+
const toplevel = detectGitToplevel(path9.resolve(repoRoot));
|
|
2489
|
+
if (toplevel === null) throw new Error(`${repoRoot} is not inside a git repository`);
|
|
2490
|
+
return realpathSync(toplevel);
|
|
2491
|
+
}
|
|
2492
|
+
function gitTreeSource(repoRoot, rev) {
|
|
2493
|
+
const canonicalRoot = canonicalToplevel(repoRoot);
|
|
2494
|
+
const { commit, commitEpoch } = resolveCommit(canonicalRoot, rev);
|
|
2495
|
+
const tree = new CommitTree(canonicalRoot, commit);
|
|
2496
|
+
const real = workingTreeSource().fileSystem;
|
|
2497
|
+
const answers = treeAnswers(tree, real);
|
|
2498
|
+
let fileSystem;
|
|
2499
|
+
return {
|
|
2500
|
+
listFiles: async (rootDirs, languages) => listTreeFiles(tree, rootDirs, languages),
|
|
2501
|
+
readFile: answers.readFile,
|
|
2502
|
+
// Like `existsSync` in the working-tree source, true for a dir as well as a file.
|
|
2503
|
+
fileExists: (abs) => answers.fileExists(abs) || answers.directoryExists(abs),
|
|
2504
|
+
get fileSystem() {
|
|
2505
|
+
fileSystem ??= overlayHost(answers, real);
|
|
2506
|
+
return fileSystem;
|
|
2507
|
+
},
|
|
2508
|
+
revision: { repoRoot: canonicalRoot, commit, commitEpoch }
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
function rebaseOntoRoot(abs, repoRoot) {
|
|
2512
|
+
const rest = [];
|
|
2513
|
+
for (let at = path9.resolve(abs); path9.dirname(at) !== at; at = path9.dirname(at)) {
|
|
2514
|
+
if (realpathOrNull(at) === repoRoot) return path9.join(repoRoot, ...rest.reverse());
|
|
2515
|
+
rest.push(path9.basename(at));
|
|
2516
|
+
}
|
|
2517
|
+
return canonicalize(abs);
|
|
2518
|
+
}
|
|
2519
|
+
function realpathOrNull(abs) {
|
|
2520
|
+
try {
|
|
2521
|
+
return realpathSync(abs);
|
|
2522
|
+
} catch {
|
|
2523
|
+
return null;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function rootsAtRevision(paths, revision) {
|
|
2527
|
+
const idRoot = realpathSync(revision.repoRoot);
|
|
2528
|
+
return { rootDirs: paths.map((p) => rebaseOntoRoot(p, idRoot)), idRoot };
|
|
2005
2529
|
}
|
|
2006
2530
|
|
|
2007
2531
|
// src/barrel-resolve.ts
|
|
@@ -2010,9 +2534,9 @@ function edgeWeight(e) {
|
|
|
2010
2534
|
return typeof w === "number" && w > 0 ? w : 1;
|
|
2011
2535
|
}
|
|
2012
2536
|
function pruneDanglingReferences(nodes, edges) {
|
|
2013
|
-
for (const [key,
|
|
2014
|
-
const symbolLayer =
|
|
2015
|
-
if (symbolLayer && (!nodes.has(
|
|
2537
|
+
for (const [key, edge2] of edges) {
|
|
2538
|
+
const symbolLayer = edge2.kind === "references" || edge2.kind === "calls";
|
|
2539
|
+
if (symbolLayer && (!nodes.has(edge2.dstId) || !nodes.has(edge2.srcId))) edges.delete(key);
|
|
2016
2540
|
}
|
|
2017
2541
|
}
|
|
2018
2542
|
function resolveBarrelEdges(nodes, edges) {
|
|
@@ -2027,7 +2551,7 @@ function resolveBarrelEdges(nodes, edges) {
|
|
|
2027
2551
|
forwards.set(e.srcId, arr);
|
|
2028
2552
|
}
|
|
2029
2553
|
const cache = /* @__PURE__ */ new Map();
|
|
2030
|
-
function
|
|
2554
|
+
function resolve5(id, seen) {
|
|
2031
2555
|
if (!barrels.has(id)) return /* @__PURE__ */ new Map([[id, 1]]);
|
|
2032
2556
|
const cached = cache.get(id);
|
|
2033
2557
|
if (cached) return cached;
|
|
@@ -2038,7 +2562,7 @@ function resolveBarrelEdges(nodes, edges) {
|
|
|
2038
2562
|
const nextSeen = new Set(seen).add(id);
|
|
2039
2563
|
for (const t of targets) {
|
|
2040
2564
|
const share = t.weight / total;
|
|
2041
|
-
for (const [real, frac] of
|
|
2565
|
+
for (const [real, frac] of resolve5(t.dst, nextSeen)) {
|
|
2042
2566
|
out.set(real, (out.get(real) ?? 0) + share * frac);
|
|
2043
2567
|
}
|
|
2044
2568
|
}
|
|
@@ -2053,7 +2577,7 @@ function resolveBarrelEdges(nodes, edges) {
|
|
|
2053
2577
|
continue;
|
|
2054
2578
|
}
|
|
2055
2579
|
const w = edgeWeight(e);
|
|
2056
|
-
for (const [real, frac] of
|
|
2580
|
+
for (const [real, frac] of resolve5(e.dstId, /* @__PURE__ */ new Set())) {
|
|
2057
2581
|
if (real === e.srcId) continue;
|
|
2058
2582
|
resolved.push({
|
|
2059
2583
|
...e,
|
|
@@ -2067,7 +2591,6 @@ function resolveBarrelEdges(nodes, edges) {
|
|
|
2067
2591
|
|
|
2068
2592
|
// src/incremental.ts
|
|
2069
2593
|
import { createHash } from "crypto";
|
|
2070
|
-
import * as fs2 from "fs/promises";
|
|
2071
2594
|
import { getLanguageFromPath as getLanguageFromPath2 } from "@titan-design/code-parser";
|
|
2072
2595
|
|
|
2073
2596
|
// src/analysis/comment-lines.ts
|
|
@@ -2298,6 +2821,25 @@ function isLoggingCall(node) {
|
|
|
2298
2821
|
return false;
|
|
2299
2822
|
}
|
|
2300
2823
|
|
|
2824
|
+
// src/analysis/jsx-metrics.ts
|
|
2825
|
+
function rendersElement(node) {
|
|
2826
|
+
if (node.type === "jsx_self_closing_element") return true;
|
|
2827
|
+
if (node.type !== "jsx_element") return false;
|
|
2828
|
+
return node.childForFieldName("open_tag")?.childForFieldName("name") != null;
|
|
2829
|
+
}
|
|
2830
|
+
function jsxDepthOf(root, opensFunction) {
|
|
2831
|
+
let max = 0;
|
|
2832
|
+
const visit = (node, depth) => {
|
|
2833
|
+
const own = rendersElement(node) ? depth + 1 : depth;
|
|
2834
|
+
if (own > max) max = own;
|
|
2835
|
+
for (const child of node.namedChildren) {
|
|
2836
|
+
if (child && !opensFunction(child)) visit(child, own);
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
visit(root, 0);
|
|
2840
|
+
return max;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2301
2843
|
// src/cognitive-complexity.ts
|
|
2302
2844
|
function cognitiveComplexityOf(body, language) {
|
|
2303
2845
|
const handler = language === "python" ? PY_HANDLER : TS_HANDLER;
|
|
@@ -2331,9 +2873,8 @@ var TS_NESTING = /* @__PURE__ */ new Set([
|
|
|
2331
2873
|
"do_statement",
|
|
2332
2874
|
"switch_statement",
|
|
2333
2875
|
"catch_clause",
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
"arrow_function"
|
|
2876
|
+
...TS_FUNCTION_DECL_TYPES,
|
|
2877
|
+
...TS_BOUND_FUNCTION_TYPES
|
|
2337
2878
|
]);
|
|
2338
2879
|
var PY_NESTING = /* @__PURE__ */ new Set([
|
|
2339
2880
|
"if_statement",
|
|
@@ -2425,7 +2966,7 @@ function computeLcomMetrics(file, fileId2) {
|
|
|
2425
2966
|
return out;
|
|
2426
2967
|
}
|
|
2427
2968
|
function collectClasses(file) {
|
|
2428
|
-
const isClass = file.language === "python" ? (n) => n.type === "class_definition" : (n) =>
|
|
2969
|
+
const isClass = file.language === "python" ? (n) => n.type === "class_definition" : (n) => TS_CLASS_TYPES.has(n.type);
|
|
2429
2970
|
const out = [];
|
|
2430
2971
|
const visit = (node) => {
|
|
2431
2972
|
if (isClass(node)) out.push(node);
|
|
@@ -2463,14 +3004,21 @@ function collectPythonMethods(classNode) {
|
|
|
2463
3004
|
if (!body) return [];
|
|
2464
3005
|
const out = [];
|
|
2465
3006
|
for (const child of body.namedChildren) {
|
|
2466
|
-
|
|
2467
|
-
|
|
3007
|
+
const fn = child ? pythonMethodNode(child) : null;
|
|
3008
|
+
if (!fn) continue;
|
|
3009
|
+
const name = fn.childForFieldName("name")?.text;
|
|
2468
3010
|
if (!name || name === "__init__") continue;
|
|
2469
|
-
|
|
2470
|
-
out.push({ name, usedNames: thisRefs(child, "self") });
|
|
3011
|
+
out.push({ name, usedNames: thisRefs(fn, "self") });
|
|
2471
3012
|
}
|
|
2472
3013
|
return out;
|
|
2473
3014
|
}
|
|
3015
|
+
function pythonMethodNode(stmt) {
|
|
3016
|
+
if (stmt.type === "function_definition") return stmt;
|
|
3017
|
+
if (stmt.type !== "decorated_definition") return null;
|
|
3018
|
+
if (isPythonStaticOrClassMethod(stmt)) return null;
|
|
3019
|
+
const def = stmt.childForFieldName("definition");
|
|
3020
|
+
return def?.type === "function_definition" ? def : null;
|
|
3021
|
+
}
|
|
2474
3022
|
function methodName(methodNode) {
|
|
2475
3023
|
const nameNode = methodNode.childForFieldName("name");
|
|
2476
3024
|
return nameNode?.text ?? null;
|
|
@@ -2481,16 +3029,10 @@ function hasStaticModifier(methodNode) {
|
|
|
2481
3029
|
}
|
|
2482
3030
|
return false;
|
|
2483
3031
|
}
|
|
2484
|
-
function isPythonStaticOrClassMethod(
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
if (text.includes("@staticmethod") || text.includes("@classmethod")) {
|
|
2489
|
-
return true;
|
|
2490
|
-
}
|
|
2491
|
-
prev = prev.previousNamedSibling;
|
|
2492
|
-
}
|
|
2493
|
-
return false;
|
|
3032
|
+
function isPythonStaticOrClassMethod(decorated) {
|
|
3033
|
+
return decorated.namedChildren.some(
|
|
3034
|
+
(c) => c?.type === "decorator" && (c.text === "@staticmethod" || c.text === "@classmethod")
|
|
3035
|
+
);
|
|
2494
3036
|
}
|
|
2495
3037
|
function thisRefs(methodNode, selfKeyword) {
|
|
2496
3038
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -2635,6 +3177,7 @@ var SYMBOL_METRICS = [
|
|
|
2635
3177
|
{ name: "symbol_cyclomatic", stat: "cyclomatic", unit: "count" },
|
|
2636
3178
|
{ name: "symbol_loc", stat: "loc", unit: "lines" },
|
|
2637
3179
|
{ name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" },
|
|
3180
|
+
{ name: "symbol_jsx_depth", stat: "jsxDepth", unit: "count", omitZero: true },
|
|
2638
3181
|
{ name: "symbol_comment_lines", stat: "commentLines", unit: "lines" },
|
|
2639
3182
|
{ name: "symbol_docstring_lines", stat: "docstringLines", unit: "lines" },
|
|
2640
3183
|
{ name: "symbol_body_lines", stat: "bodyLines", unit: "lines" },
|
|
@@ -2659,6 +3202,7 @@ function symbolMetrics(fileId2, stats, symbolNames) {
|
|
|
2659
3202
|
for (const [name, values] of byName) {
|
|
2660
3203
|
const nodeId = symbolId(fileId2, name);
|
|
2661
3204
|
for (const m of SYMBOL_METRICS) {
|
|
3205
|
+
if (m.omitZero && values[m.stat] === 0) continue;
|
|
2662
3206
|
out.push({ nodeId, name: m.name, value: values[m.stat], unit: m.unit });
|
|
2663
3207
|
}
|
|
2664
3208
|
}
|
|
@@ -2673,10 +3217,6 @@ function functionShapeStats(fn, body, lines) {
|
|
|
2673
3217
|
}
|
|
2674
3218
|
|
|
2675
3219
|
// src/source-metrics.ts
|
|
2676
|
-
var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
2677
|
-
"function_declaration",
|
|
2678
|
-
"method_definition"
|
|
2679
|
-
]);
|
|
2680
3220
|
var PY_FUNCTION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
|
|
2681
3221
|
var TS_NESTING_TYPES = /* @__PURE__ */ new Set([
|
|
2682
3222
|
"if_statement",
|
|
@@ -2719,6 +3259,7 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
|
2719
3259
|
"cognitive_max",
|
|
2720
3260
|
"cognitive_sum",
|
|
2721
3261
|
"max_nesting_depth",
|
|
3262
|
+
"jsx_depth_max",
|
|
2722
3263
|
"class_count",
|
|
2723
3264
|
"lcom4_max",
|
|
2724
3265
|
...EXCEPTION_METRIC_NAMES,
|
|
@@ -2729,10 +3270,10 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
|
2729
3270
|
...SYMBOL_METRIC_NAMES
|
|
2730
3271
|
]);
|
|
2731
3272
|
var EMPTY_NAMES = /* @__PURE__ */ new Set();
|
|
2732
|
-
function computeSourceMetrics(files,
|
|
3273
|
+
function computeSourceMetrics(files, fileIdOf2, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
|
|
2733
3274
|
const out = [];
|
|
2734
3275
|
for (const file of files) {
|
|
2735
|
-
const id =
|
|
3276
|
+
const id = fileIdOf2(file.filePath);
|
|
2736
3277
|
out.push(...metricsForFile(id, file, symbolNamesByFile2.get(id) ?? EMPTY_NAMES));
|
|
2737
3278
|
}
|
|
2738
3279
|
return out;
|
|
@@ -2780,6 +3321,8 @@ function metricsForFile(nodeId, file, symbolNames) {
|
|
|
2780
3321
|
unit: "count"
|
|
2781
3322
|
});
|
|
2782
3323
|
}
|
|
3324
|
+
const jsxDepthMax = Math.max(jsxDepthIn(file, file.tree.rootNode), ...stats.map((s) => s.jsxDepth));
|
|
3325
|
+
if (jsxDepthMax > 0) out.push({ nodeId, name: "jsx_depth_max", value: jsxDepthMax, unit: "count" });
|
|
2783
3326
|
out.push(...symbolMetrics(nodeId, stats, symbolNames));
|
|
2784
3327
|
out.push(...computeLcomMetrics(file, nodeId));
|
|
2785
3328
|
out.push(...exceptionMetrics(nodeId, file.tree.rootNode, loc));
|
|
@@ -2790,7 +3333,7 @@ function countLoc(content) {
|
|
|
2790
3333
|
}
|
|
2791
3334
|
function analyzeFunctions(file) {
|
|
2792
3335
|
const stats = [];
|
|
2793
|
-
const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES :
|
|
3336
|
+
const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_DECL_TYPES;
|
|
2794
3337
|
const lines = file.content.split("\n");
|
|
2795
3338
|
walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
|
|
2796
3339
|
const fn = functionAt(node, fnTypes);
|
|
@@ -2800,6 +3343,7 @@ function analyzeFunctions(file) {
|
|
|
2800
3343
|
cyclomatic: cyclomaticOf(fn.body, file.language),
|
|
2801
3344
|
cognitive: cognitiveComplexityOf(fn.body, file.language),
|
|
2802
3345
|
nestingDepth: nestingDepthOf(fn.body, file.language, 0),
|
|
3346
|
+
jsxDepth: jsxDepthIn(file, fn.body),
|
|
2803
3347
|
loc: fn.node.endPosition.row - fn.node.startPosition.row + 1,
|
|
2804
3348
|
...functionShapeStats(fn.node, fn.body, lines)
|
|
2805
3349
|
});
|
|
@@ -2811,13 +3355,17 @@ function functionAt(node, fnTypes) {
|
|
|
2811
3355
|
const body = node.childForFieldName("body");
|
|
2812
3356
|
return body ? { name: node.childForFieldName("name")?.text ?? null, body, node } : null;
|
|
2813
3357
|
}
|
|
2814
|
-
if ((node.type
|
|
3358
|
+
if (TS_BOUND_FUNCTION_TYPES.has(node.type) && node.parent?.type === "variable_declarator") {
|
|
2815
3359
|
const body = node.childForFieldName("body");
|
|
2816
3360
|
if (!body) return null;
|
|
2817
3361
|
return { name: node.parent.childForFieldName("name")?.text ?? null, body, node };
|
|
2818
3362
|
}
|
|
2819
3363
|
return null;
|
|
2820
3364
|
}
|
|
3365
|
+
function jsxDepthIn(file, root) {
|
|
3366
|
+
if (file.language === "python") return 0;
|
|
3367
|
+
return jsxDepthOf(root, (node) => functionAt(node, TS_FUNCTION_DECL_TYPES) !== null);
|
|
3368
|
+
}
|
|
2821
3369
|
function nestingDepthOf(node, language, depth) {
|
|
2822
3370
|
const nestingTypes = language === "python" ? PY_NESTING_TYPES : TS_NESTING_TYPES;
|
|
2823
3371
|
let maxDepth = depth;
|
|
@@ -3010,12 +3558,12 @@ var TERMINALS2 = /* @__PURE__ */ new Set([
|
|
|
3010
3558
|
"break_statement",
|
|
3011
3559
|
"continue_statement"
|
|
3012
3560
|
]);
|
|
3013
|
-
function computeDeadCodeMetrics(files,
|
|
3561
|
+
function computeDeadCodeMetrics(files, fileIdOf2) {
|
|
3014
3562
|
const out = [];
|
|
3015
3563
|
for (const file of files) {
|
|
3016
3564
|
const counts = deadCodeOf(file);
|
|
3017
3565
|
if (!counts) continue;
|
|
3018
|
-
const id =
|
|
3566
|
+
const id = fileIdOf2(file.filePath);
|
|
3019
3567
|
const { unreachable, locals, params } = counts;
|
|
3020
3568
|
if (unreachable > 0) {
|
|
3021
3569
|
out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
|
|
@@ -3178,12 +3726,12 @@ var SEARCH_METHODS = /* @__PURE__ */ new Set([
|
|
|
3178
3726
|
"every",
|
|
3179
3727
|
"filter"
|
|
3180
3728
|
]);
|
|
3181
|
-
function computeGrowthRiskMetrics(files,
|
|
3729
|
+
function computeGrowthRiskMetrics(files, fileIdOf2) {
|
|
3182
3730
|
const out = [];
|
|
3183
3731
|
for (const file of files) {
|
|
3184
3732
|
const loopTypes = file.language === "python" ? PY_LOOP_TYPES : TS_LOOP_TYPES;
|
|
3185
3733
|
const root = file.tree.rootNode;
|
|
3186
|
-
const id =
|
|
3734
|
+
const id = fileIdOf2(file.filePath);
|
|
3187
3735
|
const depth = maxLoopDepth(root, loopTypes, 0);
|
|
3188
3736
|
if (depth >= 2) {
|
|
3189
3737
|
out.push({ nodeId: id, name: "loop_depth", value: depth, unit: "count" });
|
|
@@ -3254,8 +3802,8 @@ function reconstructFragment(repoRoot, absPath, fileId2, basis) {
|
|
|
3254
3802
|
nodes.push(...basis.symbolsByFile.get(fileId2) ?? []);
|
|
3255
3803
|
const edges = basis.edgesBySrc.get(fileId2) ?? [];
|
|
3256
3804
|
const seenExternals = /* @__PURE__ */ new Set();
|
|
3257
|
-
for (const
|
|
3258
|
-
const dst = basis.nodesById.get(
|
|
3805
|
+
for (const edge2 of edges) {
|
|
3806
|
+
const dst = basis.nodesById.get(edge2.dstId);
|
|
3259
3807
|
if (dst?.kind === "external" && !seenExternals.has(dst.id)) {
|
|
3260
3808
|
seenExternals.add(dst.id);
|
|
3261
3809
|
nodes.push({ id: dst.id, kind: "external", name: dst.name });
|
|
@@ -3301,12 +3849,12 @@ function sourceLanguage(filePath) {
|
|
|
3301
3849
|
const language = getLanguageFromPath2(filePath);
|
|
3302
3850
|
return language === "typescript" || language === "python" ? language : null;
|
|
3303
3851
|
}
|
|
3304
|
-
async function readSourceFiles(filePaths) {
|
|
3852
|
+
async function readSourceFiles(filePaths, source = workingTreeSource()) {
|
|
3305
3853
|
const out = [];
|
|
3306
3854
|
for (const filePath of filePaths) {
|
|
3307
3855
|
const language = sourceLanguage(filePath);
|
|
3308
3856
|
if (language === null) continue;
|
|
3309
|
-
const content =
|
|
3857
|
+
const content = source.readFile(filePath);
|
|
3310
3858
|
out.push({ filePath, language, content, hash: hashContent(content) });
|
|
3311
3859
|
}
|
|
3312
3860
|
return out;
|
|
@@ -3456,21 +4004,21 @@ function resolveRelativeId(srcFileId, specifier, fileIds) {
|
|
|
3456
4004
|
}
|
|
3457
4005
|
return null;
|
|
3458
4006
|
}
|
|
3459
|
-
function targetFileId(
|
|
3460
|
-
return parseSymbolId(
|
|
4007
|
+
function targetFileId(edge2) {
|
|
4008
|
+
return parseSymbolId(edge2.dstId)?.fileId ?? edge2.dstId;
|
|
3461
4009
|
}
|
|
3462
4010
|
function computeDeltaAffected(basis, currentFileIds) {
|
|
3463
4011
|
const affected = /* @__PURE__ */ new Set();
|
|
3464
|
-
const
|
|
4012
|
+
const removed2 = /* @__PURE__ */ new Set();
|
|
3465
4013
|
for (const id of basis.fingerprints.keys()) {
|
|
3466
|
-
if (!currentFileIds.has(id))
|
|
4014
|
+
if (!currentFileIds.has(id)) removed2.add(id);
|
|
3467
4015
|
}
|
|
3468
|
-
const added = currentFileIds.size !== basis.fingerprints.size ||
|
|
4016
|
+
const added = currentFileIds.size !== basis.fingerprints.size || removed2.size > 0;
|
|
3469
4017
|
if (!added) return affected;
|
|
3470
4018
|
for (const [srcId, edges] of basis.edgesBySrc) {
|
|
3471
4019
|
if (!currentFileIds.has(srcId)) continue;
|
|
3472
|
-
for (const
|
|
3473
|
-
if (isEdgeAffected(
|
|
4020
|
+
for (const edge2 of edges) {
|
|
4021
|
+
if (isEdgeAffected(edge2, srcId, removed2, currentFileIds)) {
|
|
3474
4022
|
affected.add(srcId);
|
|
3475
4023
|
break;
|
|
3476
4024
|
}
|
|
@@ -3478,11 +4026,11 @@ function computeDeltaAffected(basis, currentFileIds) {
|
|
|
3478
4026
|
}
|
|
3479
4027
|
return affected;
|
|
3480
4028
|
}
|
|
3481
|
-
function isEdgeAffected(
|
|
3482
|
-
if (
|
|
3483
|
-
const specifier =
|
|
4029
|
+
function isEdgeAffected(edge2, srcId, removed2, currentFileIds) {
|
|
4030
|
+
if (removed2.has(targetFileId(edge2))) return true;
|
|
4031
|
+
const specifier = edge2.attrs?.specifier;
|
|
3484
4032
|
if (!specifier || !isRelative(specifier)) return false;
|
|
3485
|
-
return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(
|
|
4033
|
+
return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(edge2);
|
|
3486
4034
|
}
|
|
3487
4035
|
|
|
3488
4036
|
// src/metrics.ts
|
|
@@ -3562,12 +4110,12 @@ function computeCallMetrics(nodes, edges) {
|
|
|
3562
4110
|
}
|
|
3563
4111
|
function collectInbound(edges) {
|
|
3564
4112
|
const inbound = /* @__PURE__ */ new Map();
|
|
3565
|
-
for (const
|
|
3566
|
-
if (
|
|
3567
|
-
let entry = inbound.get(
|
|
3568
|
-
if (!entry) inbound.set(
|
|
3569
|
-
if (
|
|
3570
|
-
entry.sites.push(...
|
|
4113
|
+
for (const edge2 of edges) {
|
|
4114
|
+
if (edge2.kind !== "calls") continue;
|
|
4115
|
+
let entry = inbound.get(edge2.dstId);
|
|
4116
|
+
if (!entry) inbound.set(edge2.dstId, entry = { callers: /* @__PURE__ */ new Set(), sites: [] });
|
|
4117
|
+
if (edge2.srcId !== edge2.dstId) entry.callers.add(edge2.srcId);
|
|
4118
|
+
entry.sites.push(...edge2.attrs?.sites ?? []);
|
|
3571
4119
|
}
|
|
3572
4120
|
return inbound;
|
|
3573
4121
|
}
|
|
@@ -3594,103 +4142,6 @@ function argumentAt(site, name, position) {
|
|
|
3594
4142
|
return ABSENT;
|
|
3595
4143
|
}
|
|
3596
4144
|
|
|
3597
|
-
// src/analysis/test-linker.ts
|
|
3598
|
-
var DEFAULT_MIN_CO_EDIT_COUNT = 2;
|
|
3599
|
-
var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
|
|
3600
|
-
var TESTS_DIR_RE = /(^|\/)(?:__tests__|tests?)\//;
|
|
3601
|
-
function pathCandidates(testId) {
|
|
3602
|
-
const out = /* @__PURE__ */ new Set();
|
|
3603
|
-
const deInfixed = testId.replace(TEST_INFIX_RE, "$1");
|
|
3604
|
-
if (deInfixed !== testId) out.add(deInfixed);
|
|
3605
|
-
const deDir = deInfixed.replace(TESTS_DIR_RE, "$1");
|
|
3606
|
-
if (deDir !== testId) out.add(deDir);
|
|
3607
|
-
return [...out];
|
|
3608
|
-
}
|
|
3609
|
-
function splitFileIds(nodes) {
|
|
3610
|
-
const testIds = [];
|
|
3611
|
-
const nonTestIds = /* @__PURE__ */ new Set();
|
|
3612
|
-
for (const n of nodes) {
|
|
3613
|
-
if (n.kind !== "file") continue;
|
|
3614
|
-
if (n.role === "test") testIds.push(n.id);
|
|
3615
|
-
else nonTestIds.add(n.id);
|
|
3616
|
-
}
|
|
3617
|
-
return { testIds, nonTestIds };
|
|
3618
|
-
}
|
|
3619
|
-
function linkTestsToSources(nodes, coEditPairs, options = {}) {
|
|
3620
|
-
const minCount = options.minCoEditCount ?? DEFAULT_MIN_CO_EDIT_COUNT;
|
|
3621
|
-
const { testIds, nonTestIds } = splitFileIds(nodes);
|
|
3622
|
-
const links = [];
|
|
3623
|
-
for (const testId of testIds) {
|
|
3624
|
-
const matched = pathCandidates(testId).filter((c) => nonTestIds.has(c));
|
|
3625
|
-
if (matched.length > 0) {
|
|
3626
|
-
for (const sourceId of matched) {
|
|
3627
|
-
links.push({ testId, sourceId, method: "path" });
|
|
3628
|
-
}
|
|
3629
|
-
continue;
|
|
3630
|
-
}
|
|
3631
|
-
const partner = couplingFor(coEditPairs, testId).find((p) => p.count >= minCount && nonTestIds.has(p.partner));
|
|
3632
|
-
if (partner) {
|
|
3633
|
-
links.push({ testId, sourceId: partner.partner, method: "coedit" });
|
|
3634
|
-
}
|
|
3635
|
-
}
|
|
3636
|
-
return links;
|
|
3637
|
-
}
|
|
3638
|
-
function testCoverageCountMetrics(links) {
|
|
3639
|
-
const testsBySource = groupTestsBySource(links);
|
|
3640
|
-
const out = [];
|
|
3641
|
-
for (const [sourceId, tests] of testsBySource) {
|
|
3642
|
-
out.push({
|
|
3643
|
-
nodeId: sourceId,
|
|
3644
|
-
name: "linked_test_count",
|
|
3645
|
-
value: tests.size,
|
|
3646
|
-
unit: "count"
|
|
3647
|
-
});
|
|
3648
|
-
}
|
|
3649
|
-
return out;
|
|
3650
|
-
}
|
|
3651
|
-
function groupTestsBySource(links) {
|
|
3652
|
-
const out = /* @__PURE__ */ new Map();
|
|
3653
|
-
for (const l of links) {
|
|
3654
|
-
let set = out.get(l.sourceId);
|
|
3655
|
-
if (!set) {
|
|
3656
|
-
set = /* @__PURE__ */ new Set();
|
|
3657
|
-
out.set(l.sourceId, set);
|
|
3658
|
-
}
|
|
3659
|
-
set.add(l.testId);
|
|
3660
|
-
}
|
|
3661
|
-
return out;
|
|
3662
|
-
}
|
|
3663
|
-
|
|
3664
|
-
// src/history-recency.ts
|
|
3665
|
-
var DAY_SECONDS = 86400;
|
|
3666
|
-
function windowSuffix(window) {
|
|
3667
|
-
return window === "lifetime" ? "lifetime" : `${window}d`;
|
|
3668
|
-
}
|
|
3669
|
-
function computeRecencyWindows(firstSeen, churnedIdsByWindow, nowEpoch) {
|
|
3670
|
-
const out = [];
|
|
3671
|
-
const ageEmitted = /* @__PURE__ */ new Set();
|
|
3672
|
-
for (const [windowDays, ids] of churnedIdsByWindow) {
|
|
3673
|
-
const suffix = windowSuffix(windowDays);
|
|
3674
|
-
for (const id of ids) {
|
|
3675
|
-
const seen = firstSeen.get(id);
|
|
3676
|
-
let recency = 1;
|
|
3677
|
-
if (seen !== void 0) {
|
|
3678
|
-
const ageDays = Math.max(0, (nowEpoch - seen) / DAY_SECONDS);
|
|
3679
|
-
if (windowDays !== "lifetime") recency = Math.min(1, ageDays / windowDays);
|
|
3680
|
-
if (!ageEmitted.has(id)) {
|
|
3681
|
-
out.push({ nodeId: id, name: "file_age_days", value: Math.round(ageDays), unit: "days" });
|
|
3682
|
-
ageEmitted.add(id);
|
|
3683
|
-
}
|
|
3684
|
-
}
|
|
3685
|
-
out.push({ nodeId: id, name: `recency_${suffix}`, value: round3(recency), unit: "ratio" });
|
|
3686
|
-
}
|
|
3687
|
-
}
|
|
3688
|
-
return out;
|
|
3689
|
-
}
|
|
3690
|
-
function round3(n) {
|
|
3691
|
-
return Math.round(n * 1e3) / 1e3;
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
4145
|
// src/history-metrics.ts
|
|
3695
4146
|
var DEFAULT_CHURN_WINDOWS = [30, 90, 180];
|
|
3696
4147
|
function resolveChurnWindows(requested, primaryWindow, includeLifetime) {
|
|
@@ -3702,7 +4153,12 @@ function loadHistoryMetrics(nodes, idRoot, options = {}) {
|
|
|
3702
4153
|
const knownPaths = collectFileIds(nodes);
|
|
3703
4154
|
const primaryWindow = options.churnWindowDays ?? 30;
|
|
3704
4155
|
const windows = resolveChurnWindows(options.churnWindows, primaryWindow, options.includeLifetime === true);
|
|
3705
|
-
const wide = loadChurnEntries({
|
|
4156
|
+
const wide = loadChurnEntries({
|
|
4157
|
+
repoRoot: idRoot,
|
|
4158
|
+
windowDays: windows[windows.length - 1],
|
|
4159
|
+
rev: options.rev,
|
|
4160
|
+
untilEpoch: options.nowEpoch
|
|
4161
|
+
});
|
|
3706
4162
|
if (wide === null) return null;
|
|
3707
4163
|
const nowEpoch = options.nowEpoch ?? Math.floor(Date.now() / 1e3);
|
|
3708
4164
|
const churnByWindow = aggregateChurnWindows(wide, windows, nowEpoch, knownPaths);
|
|
@@ -3710,7 +4166,7 @@ function loadHistoryMetrics(nodes, idRoot, options = {}) {
|
|
|
3710
4166
|
const metrics = [
|
|
3711
4167
|
...churnMetrics(churnByWindow),
|
|
3712
4168
|
...ownershipMetrics(primaryEntries, primaryWindow, knownPaths),
|
|
3713
|
-
...recencyMetrics(idRoot,
|
|
4169
|
+
...recencyMetrics({ idRoot, rev: options.rev, knownPaths, nowEpoch }, churnByWindow)
|
|
3714
4170
|
];
|
|
3715
4171
|
if (options.includeLifetime === true) metrics.push(...ownershipMetrics(wide, "lifetime", knownPaths));
|
|
3716
4172
|
return { metrics, primaryEntries };
|
|
@@ -3777,14 +4233,14 @@ function mergeAuthorChurn(tests, linesByTest) {
|
|
|
3777
4233
|
}
|
|
3778
4234
|
return byAuthor;
|
|
3779
4235
|
}
|
|
3780
|
-
function recencyMetrics(
|
|
4236
|
+
function recencyMetrics(at, churnByWindow) {
|
|
3781
4237
|
const churnedByWindow = /* @__PURE__ */ new Map();
|
|
3782
4238
|
for (const [window, byPath] of churnByWindow) {
|
|
3783
4239
|
if (byPath.size > 0) churnedByWindow.set(window, new Set(byPath.keys()));
|
|
3784
4240
|
}
|
|
3785
4241
|
if (churnedByWindow.size === 0) return [];
|
|
3786
|
-
const firstSeen = loadFileFirstSeen({ repoRoot: idRoot, knownPaths }) ?? /* @__PURE__ */ new Map();
|
|
3787
|
-
return computeRecencyWindows(firstSeen, churnedByWindow, nowEpoch);
|
|
4242
|
+
const firstSeen = loadFileFirstSeen({ repoRoot: at.idRoot, knownPaths: at.knownPaths, rev: at.rev }) ?? /* @__PURE__ */ new Map();
|
|
4243
|
+
return computeRecencyWindows(firstSeen, churnedByWindow, at.nowEpoch);
|
|
3788
4244
|
}
|
|
3789
4245
|
|
|
3790
4246
|
// src/index-metrics.ts
|
|
@@ -3828,17 +4284,17 @@ function computeTestCoverage(nodes, entries, windowDays) {
|
|
|
3828
4284
|
}
|
|
3829
4285
|
|
|
3830
4286
|
// src/merge.ts
|
|
3831
|
-
function edgeKey(
|
|
3832
|
-
return JSON.stringify([
|
|
4287
|
+
function edgeKey(edge2) {
|
|
4288
|
+
return JSON.stringify([edge2.srcId, edge2.dstId, edge2.kind]);
|
|
3833
4289
|
}
|
|
3834
4290
|
function mergeFragments(fragments) {
|
|
3835
4291
|
const nodes = /* @__PURE__ */ new Map();
|
|
3836
4292
|
const edges = /* @__PURE__ */ new Map();
|
|
3837
4293
|
for (const fragment of fragments) {
|
|
3838
4294
|
for (const node of fragment.nodes) if (!nodes.has(node.id)) nodes.set(node.id, node);
|
|
3839
|
-
for (const
|
|
3840
|
-
const key = edgeKey(
|
|
3841
|
-
if (!edges.has(key)) edges.set(key,
|
|
4295
|
+
for (const edge2 of fragment.edges) {
|
|
4296
|
+
const key = edgeKey(edge2);
|
|
4297
|
+
if (!edges.has(key)) edges.set(key, edge2);
|
|
3842
4298
|
}
|
|
3843
4299
|
}
|
|
3844
4300
|
return { nodes, edges };
|
|
@@ -3882,18 +4338,18 @@ function qualifiedByName(file) {
|
|
|
3882
4338
|
}
|
|
3883
4339
|
|
|
3884
4340
|
// src/indexer.ts
|
|
3885
|
-
var INDEX_VERSION = "0.
|
|
4341
|
+
var INDEX_VERSION = "0.22.0";
|
|
3886
4342
|
var LANGUAGES = ["typescript", "python"];
|
|
3887
4343
|
function canonicalizePath(p) {
|
|
3888
4344
|
try {
|
|
3889
|
-
return
|
|
4345
|
+
return realpathSync2(p);
|
|
3890
4346
|
} catch {
|
|
3891
4347
|
return p;
|
|
3892
4348
|
}
|
|
3893
4349
|
}
|
|
3894
4350
|
function normalizePaths(paths) {
|
|
3895
4351
|
if (paths.length === 0) throw new Error("indexPaths requires at least one path");
|
|
3896
|
-
return [...new Set(paths.map((p) =>
|
|
4352
|
+
return [...new Set(paths.map((p) => path10.resolve(p)))];
|
|
3897
4353
|
}
|
|
3898
4354
|
function countByKind(items) {
|
|
3899
4355
|
const out = {};
|
|
@@ -3916,8 +4372,9 @@ function resolveSymbolAliases(store, idRoot, parsedByPath, nodeIds) {
|
|
|
3916
4372
|
const files = [...parsedByPath.values()].map((file) => ({ fileId: fileId(idRoot, file.filePath), file }));
|
|
3917
4373
|
return qualifiedSymbolAliases(files, nodeIds);
|
|
3918
4374
|
}
|
|
3919
|
-
function resolveRoots(paths) {
|
|
4375
|
+
function resolveRoots(paths, source) {
|
|
3920
4376
|
const raw = normalizePaths(paths);
|
|
4377
|
+
if (source.revision) return rootsAtRevision(raw, source.revision);
|
|
3921
4378
|
const gitToplevel = detectGitToplevel(raw[0]);
|
|
3922
4379
|
const rootDirs = gitToplevel !== null ? raw.map(canonicalizePath) : raw;
|
|
3923
4380
|
return { rootDirs, idRoot: gitToplevel ?? rootDirs[0] };
|
|
@@ -3929,16 +4386,19 @@ async function parseAll(toParse) {
|
|
|
3929
4386
|
}
|
|
3930
4387
|
return parsed;
|
|
3931
4388
|
}
|
|
3932
|
-
function historyOptions(options) {
|
|
4389
|
+
function historyOptions(options, source) {
|
|
3933
4390
|
return {
|
|
3934
4391
|
churnWindowDays: options.churnWindowDays,
|
|
3935
4392
|
churnWindows: options.churnWindows,
|
|
3936
|
-
includeLifetime: options.lifetime === true
|
|
4393
|
+
includeLifetime: options.lifetime === true,
|
|
4394
|
+
rev: source.revision?.commit,
|
|
4395
|
+
nowEpoch: source.revision?.commitEpoch
|
|
3937
4396
|
};
|
|
3938
4397
|
}
|
|
3939
4398
|
async function indexPaths(store, options) {
|
|
3940
|
-
const
|
|
3941
|
-
const
|
|
4399
|
+
const source = options.source ?? workingTreeSource();
|
|
4400
|
+
const { rootDirs, idRoot } = resolveRoots(options.paths, source);
|
|
4401
|
+
const readFiles = await readSourceFiles(await source.listFiles(rootDirs, LANGUAGES), source);
|
|
3942
4402
|
const reuse = options.incremental !== false ? loadReuseBasis(store, INDEX_VERSION) : null;
|
|
3943
4403
|
const currentFileIds = new Set(readFiles.map((rf) => fileId(idRoot, rf.filePath)));
|
|
3944
4404
|
const affected = reuse ? computeDeltaAffected(reuse, currentFileIds) : /* @__PURE__ */ new Set();
|
|
@@ -3952,10 +4412,11 @@ async function indexPaths(store, options) {
|
|
|
3952
4412
|
parsedByPath,
|
|
3953
4413
|
reuse,
|
|
3954
4414
|
cosmeticFileIds: classified.cosmeticFileIds,
|
|
3955
|
-
extractor: new LanguageExtractor({ repoRoot: idRoot, tsConfigPath: options.tsConfig })
|
|
4415
|
+
extractor: new LanguageExtractor({ repoRoot: idRoot, tsConfigPath: options.tsConfig, source })
|
|
3956
4416
|
})
|
|
3957
4417
|
);
|
|
3958
|
-
const
|
|
4418
|
+
const roleHints = computeRoleHints(readFiles, idRoot, fileId, source);
|
|
4419
|
+
const annotated = annotateRoles([...accumulator.nodes.values()], roleHints);
|
|
3959
4420
|
accumulator.nodes = new Map(annotated.map((n) => [n.id, n]));
|
|
3960
4421
|
pruneDanglingReferences(accumulator.nodes, accumulator.edges);
|
|
3961
4422
|
const metrics = options.computeMetrics === false ? [] : buildIndexerMetrics({
|
|
@@ -3964,19 +4425,23 @@ async function indexPaths(store, options) {
|
|
|
3964
4425
|
parsedFiles: [...parsedByPath.values()],
|
|
3965
4426
|
reusedSourceMetrics: reuse ? reusedFileIds.flatMap((id) => reuse.sourceMetricsByFile.get(id) ?? []) : [],
|
|
3966
4427
|
idRoot,
|
|
3967
|
-
history: options.computeChurn === false ? void 0 : historyOptions(options)
|
|
4428
|
+
history: options.computeChurn === false ? void 0 : historyOptions(options, source)
|
|
3968
4429
|
});
|
|
3969
|
-
const rootDir = rootDirs[0];
|
|
4430
|
+
const rootDir = source.revision ? idRoot : rootDirs[0];
|
|
3970
4431
|
const ref = options.ref ?? "wd";
|
|
3971
|
-
const
|
|
4432
|
+
const commitHash = options.commitHash ?? source.revision?.commit ?? detectGitHead(rootDir) ?? void 0;
|
|
4433
|
+
const bridge = computeAliasBridge(store, { ...options, rootDir, idRoot, ref, commitHash, nodes: accumulator.nodes });
|
|
3972
4434
|
const aliases = [
|
|
3973
4435
|
...bridge.aliases,
|
|
3974
4436
|
...resolveSymbolAliases(store, idRoot, parsedByPath, new Set(accumulator.nodes.keys()))
|
|
3975
4437
|
];
|
|
3976
|
-
const commitHash = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
|
|
3977
4438
|
const snapshot = { ref, commitHash, aliasBase: bridge.baseSnapshotId };
|
|
3978
|
-
const
|
|
3979
|
-
store.
|
|
4439
|
+
const fingerprints = buildFingerprints(readFiles, idRoot, classified.structuralByFileId);
|
|
4440
|
+
const snapshotId = store.atomically(() => {
|
|
4441
|
+
const id = persist(store, snapshot, accumulator, aliases, metrics);
|
|
4442
|
+
store.insertFingerprints(id, fingerprints);
|
|
4443
|
+
return id;
|
|
4444
|
+
});
|
|
3980
4445
|
return {
|
|
3981
4446
|
snapshotId,
|
|
3982
4447
|
files: readFiles.length,
|
|
@@ -3993,14 +4458,14 @@ async function indexPaths(store, options) {
|
|
|
3993
4458
|
}
|
|
3994
4459
|
|
|
3995
4460
|
// src/index.ts
|
|
3996
|
-
import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as
|
|
4461
|
+
import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as parseFile3, shouldIncludeFile as shouldIncludeFile3 } from "@titan-design/code-parser";
|
|
3997
4462
|
|
|
3998
4463
|
// src/extractors/deep-ast.ts
|
|
3999
4464
|
import { readFileSync as readFileSync2 } from "fs";
|
|
4000
|
-
import * as
|
|
4465
|
+
import * as path11 from "path";
|
|
4001
4466
|
import {
|
|
4002
4467
|
Node as Node6,
|
|
4003
|
-
Project as
|
|
4468
|
+
Project as Project4,
|
|
4004
4469
|
ScriptTarget as ScriptTarget2
|
|
4005
4470
|
} from "ts-morph";
|
|
4006
4471
|
function computeDeepAst(input) {
|
|
@@ -4012,11 +4477,11 @@ function computeDeepAst(input) {
|
|
|
4012
4477
|
function loadSourceFile(absPath) {
|
|
4013
4478
|
try {
|
|
4014
4479
|
const text = readFileSync2(absPath, "utf8");
|
|
4015
|
-
const project = new
|
|
4480
|
+
const project = new Project4({
|
|
4016
4481
|
useInMemoryFileSystem: true,
|
|
4017
4482
|
compilerOptions: { target: ScriptTarget2.Latest, allowJs: true }
|
|
4018
4483
|
});
|
|
4019
|
-
return project.createSourceFile(`f${
|
|
4484
|
+
return project.createSourceFile(`f${path11.extname(absPath) || ".ts"}`, text);
|
|
4020
4485
|
} catch {
|
|
4021
4486
|
return null;
|
|
4022
4487
|
}
|
|
@@ -4194,11 +4659,11 @@ function innermostContaining(symbols, line) {
|
|
|
4194
4659
|
}
|
|
4195
4660
|
return best;
|
|
4196
4661
|
}
|
|
4197
|
-
function attributeCoverage(coverage,
|
|
4662
|
+
function attributeCoverage(coverage, fileIdOf2, symbolsByFile) {
|
|
4198
4663
|
const out = [];
|
|
4199
4664
|
for (const [absPath, fc] of Object.entries(coverage)) {
|
|
4200
4665
|
if (!isFileCoverage(fc)) continue;
|
|
4201
|
-
const fileId2 =
|
|
4666
|
+
const fileId2 = fileIdOf2(absPath);
|
|
4202
4667
|
if (!fileId2) continue;
|
|
4203
4668
|
const fnIds = Object.keys(fc.fnMap);
|
|
4204
4669
|
if (fnIds.length === 0) continue;
|
|
@@ -4229,142 +4694,196 @@ function pctMetric(nodeId, covered, total) {
|
|
|
4229
4694
|
};
|
|
4230
4695
|
}
|
|
4231
4696
|
|
|
4232
|
-
// src/analysis/
|
|
4233
|
-
|
|
4234
|
-
var
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
"re-exports": 0.5,
|
|
4239
|
-
calls: 1.5,
|
|
4240
|
-
extends: 1,
|
|
4241
|
-
implements: 1,
|
|
4242
|
-
references: 1,
|
|
4243
|
-
"depends-on": 1
|
|
4697
|
+
// src/analysis/context/minify.ts
|
|
4698
|
+
import { parseFile as parseFile2 } from "@titan-design/code-parser";
|
|
4699
|
+
var IMPORT_TYPES = {
|
|
4700
|
+
typescript: /* @__PURE__ */ new Set(["import_statement"]),
|
|
4701
|
+
tsx: /* @__PURE__ */ new Set(["import_statement"]),
|
|
4702
|
+
python: /* @__PURE__ */ new Set(["import_statement", "import_from_statement", "future_import_statement"])
|
|
4244
4703
|
};
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
const
|
|
4253
|
-
|
|
4254
|
-
const weights = { ...DEFAULT_EDGE_WEIGHTS, ...options.edgeWeights ?? {} };
|
|
4255
|
-
const idx = buildIdIndex(nodes);
|
|
4256
|
-
const adj = buildAdjacency(idx, edges, weights, n);
|
|
4257
|
-
const pers = buildPersonalization(idx, n, options.personalization);
|
|
4258
|
-
const { rank, iterations, converged } = powerIterate(
|
|
4259
|
-
pers,
|
|
4260
|
-
adj,
|
|
4261
|
-
damping,
|
|
4262
|
-
tolerance,
|
|
4263
|
-
maxIters
|
|
4264
|
-
);
|
|
4265
|
-
return { rows: rankToRows(nodes, rank), iterations, converged };
|
|
4704
|
+
var DELIMITERS = /* @__PURE__ */ new Set(["(", ")", "[", "]", "{", "}", ",", ";"]);
|
|
4705
|
+
var STRING_TYPES = {
|
|
4706
|
+
typescript: ["string", "template_string"],
|
|
4707
|
+
tsx: ["string", "template_string"],
|
|
4708
|
+
python: ["string"]
|
|
4709
|
+
};
|
|
4710
|
+
function importMarker(count, language) {
|
|
4711
|
+
const prefix = language === "python" ? "#" : "//";
|
|
4712
|
+
return `${prefix} \u2026 ${count} import${count === 1 ? "" : "s"}`;
|
|
4266
4713
|
}
|
|
4267
|
-
function
|
|
4268
|
-
|
|
4269
|
-
for (let i = 0; i < nodes.length; i++) idx.set(nodes[i].id, i);
|
|
4270
|
-
return idx;
|
|
4714
|
+
function originalLine(minified, outputLine) {
|
|
4715
|
+
return minified.lineMap[outputLine - 1];
|
|
4271
4716
|
}
|
|
4272
|
-
function
|
|
4273
|
-
|
|
4274
|
-
{ length: n },
|
|
4275
|
-
() => []
|
|
4276
|
-
);
|
|
4277
|
-
const outWeightSum = new Array(n).fill(0);
|
|
4278
|
-
for (const e of edges) {
|
|
4279
|
-
const si = idx.get(e.srcId);
|
|
4280
|
-
const di = idx.get(e.dstId);
|
|
4281
|
-
if (si === void 0 || di === void 0) continue;
|
|
4282
|
-
const w = weights[e.kind] ?? 1;
|
|
4283
|
-
if (w <= 0) continue;
|
|
4284
|
-
outNeighbors[si].push({ to: di, w });
|
|
4285
|
-
outWeightSum[si] += w;
|
|
4286
|
-
}
|
|
4287
|
-
return { outNeighbors, outWeightSum };
|
|
4288
|
-
}
|
|
4289
|
-
function powerIterate(pers, adj, damping, tolerance, maxIters) {
|
|
4290
|
-
const n = pers.length;
|
|
4291
|
-
let rank = pers.slice();
|
|
4292
|
-
let next = new Array(n).fill(0);
|
|
4293
|
-
let iterations = 0;
|
|
4294
|
-
let converged = false;
|
|
4295
|
-
for (let iter = 0; iter < maxIters; iter++) {
|
|
4296
|
-
iterations = iter + 1;
|
|
4297
|
-
const dangling = sumDangling(rank, adj.outWeightSum);
|
|
4298
|
-
const teleport = 1 - damping + damping * dangling;
|
|
4299
|
-
seedTeleport(next, pers, teleport);
|
|
4300
|
-
distributeRank(next, rank, adj, damping);
|
|
4301
|
-
const diff = l1Diff(rank, next);
|
|
4302
|
-
[rank, next] = [next, rank];
|
|
4303
|
-
next.fill(0);
|
|
4304
|
-
if (diff < tolerance) {
|
|
4305
|
-
converged = true;
|
|
4306
|
-
break;
|
|
4307
|
-
}
|
|
4308
|
-
}
|
|
4309
|
-
return { rank, iterations, converged };
|
|
4717
|
+
function isMinifyLanguage(language) {
|
|
4718
|
+
return language === "typescript" || language === "tsx" || language === "python";
|
|
4310
4719
|
}
|
|
4311
|
-
function
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4720
|
+
async function minifySource(text, language) {
|
|
4721
|
+
if (!isMinifyLanguage(language)) return unchanged(text);
|
|
4722
|
+
const { tree } = await parseFile2(text, "source", language);
|
|
4723
|
+
try {
|
|
4724
|
+
return minifyTree(tree.rootNode, text, language);
|
|
4725
|
+
} finally {
|
|
4726
|
+
tree.delete();
|
|
4315
4727
|
}
|
|
4316
|
-
return total;
|
|
4317
|
-
}
|
|
4318
|
-
function seedTeleport(next, pers, teleport) {
|
|
4319
|
-
for (let i = 0; i < next.length; i++) next[i] = teleport * pers[i];
|
|
4320
4728
|
}
|
|
4321
|
-
function
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4729
|
+
function minifyTree(root, text, language) {
|
|
4730
|
+
const rows = text.split("\n");
|
|
4731
|
+
const cuts = cutsByRow(removedSpans(root, language));
|
|
4732
|
+
const verbatim = verbatimRows(root, language);
|
|
4733
|
+
const block = leadingImportBlock(root, language);
|
|
4734
|
+
const out = [];
|
|
4735
|
+
for (let row = 0; row < rows.length; row++) {
|
|
4736
|
+
if (block && row >= block.startRow && row <= block.endRow) {
|
|
4737
|
+
if (row === block.startRow) out.push({ text: importMarker(block.count, language), line: row + 1, verbatim: false });
|
|
4738
|
+
continue;
|
|
4329
4739
|
}
|
|
4740
|
+
const kept = keepRow(rows[row] ?? "", cuts.get(row), verbatim.has(row));
|
|
4741
|
+
if (kept !== null) out.push({ text: kept, line: row + 1, verbatim: verbatim.has(row) });
|
|
4742
|
+
}
|
|
4743
|
+
return finish(collapseBlanks(out), text.endsWith("\n"));
|
|
4744
|
+
}
|
|
4745
|
+
function unchanged(text) {
|
|
4746
|
+
const rows = text.split("\n");
|
|
4747
|
+
const count = text === "" ? 0 : text.endsWith("\n") ? rows.length - 1 : rows.length;
|
|
4748
|
+
return { text, lineMap: Array.from({ length: count }, (_, i) => i + 1) };
|
|
4749
|
+
}
|
|
4750
|
+
function removedSpans(root, language) {
|
|
4751
|
+
const comments = descendantsOfType(root, "comment");
|
|
4752
|
+
if (language !== "python") {
|
|
4753
|
+
return [
|
|
4754
|
+
...[...comments, ...descendantsOfType(root, "hash_bang_line")].map(spanOf),
|
|
4755
|
+
...jsxCommentExpressions(root).map((n) => ({ ...spanOf(n), jsx: true }))
|
|
4756
|
+
];
|
|
4330
4757
|
}
|
|
4758
|
+
return [...comments.map(spanOf), ...pythonDocstrings(root).map(docstringSpan)];
|
|
4759
|
+
}
|
|
4760
|
+
function spanOf(node) {
|
|
4761
|
+
return { start: node.startPosition, end: node.endPosition };
|
|
4331
4762
|
}
|
|
4332
|
-
function
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
return
|
|
4763
|
+
function docstringSpan(docstring) {
|
|
4764
|
+
const semicolon = docstring.nextSibling;
|
|
4765
|
+
const sameRow = semicolon?.type === ";" && semicolon.startPosition.row === docstring.endPosition.row;
|
|
4766
|
+
return { start: docstring.startPosition, end: sameRow ? semicolon.endPosition : docstring.endPosition };
|
|
4336
4767
|
}
|
|
4337
|
-
function
|
|
4338
|
-
return
|
|
4768
|
+
function jsxCommentExpressions(root) {
|
|
4769
|
+
return descendantsOfType(root, "jsx_expression").filter(
|
|
4770
|
+
(n) => n.namedChildCount > 0 && n.namedChildren.every((c) => c?.type === "comment")
|
|
4771
|
+
);
|
|
4339
4772
|
}
|
|
4340
|
-
function
|
|
4341
|
-
|
|
4342
|
-
|
|
4773
|
+
function pythonDocstrings(root) {
|
|
4774
|
+
const bodies = [...descendantsOfType(root, "class_definition"), ...descendantsOfType(root, "function_definition")].map((def) => def.childForFieldName("body")).filter((body) => body !== null);
|
|
4775
|
+
const docstrings = bodies.map((body) => droppableDocstring(body));
|
|
4776
|
+
const module = moduleDocstring(root);
|
|
4777
|
+
return [module && !sharesEndRow(module) ? module : null, ...docstrings].filter((d) => d !== null);
|
|
4343
4778
|
}
|
|
4344
|
-
function
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4779
|
+
function droppableDocstring(body) {
|
|
4780
|
+
const docstring = pythonDocstring(body);
|
|
4781
|
+
if (!docstring || bodyStatements(body, docstring).length === 0) return null;
|
|
4782
|
+
return sharesEndRow(docstring) ? null : docstring;
|
|
4783
|
+
}
|
|
4784
|
+
function sharesEndRow(docstring) {
|
|
4785
|
+
let next = docstring.nextNamedSibling;
|
|
4786
|
+
while (next?.type === "comment") next = next.nextNamedSibling;
|
|
4787
|
+
return next?.startPosition.row === docstring.endPosition.row;
|
|
4788
|
+
}
|
|
4789
|
+
function moduleDocstring(root) {
|
|
4790
|
+
const first = root.namedChildren.find((c) => c !== null && c.type !== "comment");
|
|
4791
|
+
if (first?.type !== "expression_statement" || first.namedChildCount !== 1) return null;
|
|
4792
|
+
return first.namedChildren[0]?.type === "string" ? first : null;
|
|
4793
|
+
}
|
|
4794
|
+
function cutsByRow(spans) {
|
|
4795
|
+
const cuts = /* @__PURE__ */ new Map();
|
|
4796
|
+
for (const { start, end, jsx } of spans) {
|
|
4797
|
+
for (let row = start.row; row <= end.row; row++) {
|
|
4798
|
+
const from = row === start.row ? start.column : 0;
|
|
4799
|
+
const to = row === end.row ? end.column : Number.MAX_SAFE_INTEGER;
|
|
4800
|
+
const list = cuts.get(row) ?? [];
|
|
4801
|
+
list.push({ from, to, pad: row !== start.row, jsx: jsx === true });
|
|
4802
|
+
cuts.set(row, list);
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
return cuts;
|
|
4351
4806
|
}
|
|
4352
|
-
function
|
|
4353
|
-
|
|
4807
|
+
function verbatimRows(root, language) {
|
|
4808
|
+
const rows = /* @__PURE__ */ new Set();
|
|
4809
|
+
for (const type of STRING_TYPES[language]) {
|
|
4810
|
+
for (const s of descendantsOfType(root, type)) {
|
|
4811
|
+
for (let row = s.startPosition.row; row < s.endPosition.row; row++) rows.add(row);
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
return rows;
|
|
4354
4815
|
}
|
|
4355
|
-
function
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4816
|
+
function keepRow(row, cuts, verbatim) {
|
|
4817
|
+
if (!cuts) return verbatim ? row : row.trimEnd();
|
|
4818
|
+
let kept = "";
|
|
4819
|
+
let cutSinceKept = false;
|
|
4820
|
+
let onlyJsxCut = true;
|
|
4821
|
+
for (let col = 0; col < row.length; col++) {
|
|
4822
|
+
const hits = cuts.filter((c) => col >= c.from && col < c.to);
|
|
4823
|
+
const char = row[col] ?? "";
|
|
4824
|
+
if (hits.length === 0) {
|
|
4825
|
+
const before = kept.at(-1) ?? " ";
|
|
4826
|
+
if (cutSinceKept && wouldFuse(before, char) && !(onlyJsxCut && isTagEdge(before, char))) kept += " ";
|
|
4827
|
+
kept += char;
|
|
4828
|
+
cutSinceKept = false;
|
|
4829
|
+
onlyJsxCut = true;
|
|
4830
|
+
} else if (hits.every((c) => c.pad)) kept += " ";
|
|
4831
|
+
else {
|
|
4832
|
+
cutSinceKept = true;
|
|
4833
|
+
onlyJsxCut &&= hits.some((c) => c.jsx);
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
const trimmed = kept.trimEnd();
|
|
4837
|
+
if (trimmed === "") return null;
|
|
4838
|
+
return verbatim ? kept : trimmed;
|
|
4839
|
+
}
|
|
4840
|
+
function wouldFuse(before, after) {
|
|
4841
|
+
return /\S/.test(before) && /\S/.test(after) && !DELIMITERS.has(before) && !DELIMITERS.has(after);
|
|
4842
|
+
}
|
|
4843
|
+
function isTagEdge(before, after) {
|
|
4844
|
+
return before === ">" || after === "<";
|
|
4845
|
+
}
|
|
4846
|
+
function leadingImportBlock(root, language) {
|
|
4847
|
+
const children = root.namedChildren.filter((c) => c !== null);
|
|
4848
|
+
const docstring = language === "python" ? moduleDocstring(root) : null;
|
|
4849
|
+
let i = 0;
|
|
4850
|
+
while (i < children.length && isPreamble(children[i], docstring)) i++;
|
|
4851
|
+
const imports = [];
|
|
4852
|
+
for (; i < children.length; i++) {
|
|
4853
|
+
const child = children[i];
|
|
4854
|
+
if (child.type === "comment") continue;
|
|
4855
|
+
if (!IMPORT_TYPES[language].has(child.type)) break;
|
|
4856
|
+
imports.push(child);
|
|
4857
|
+
}
|
|
4858
|
+
while (docstring && imports[0]?.startPosition.row === docstring.endPosition.row) imports.shift();
|
|
4859
|
+
const next = children[i];
|
|
4860
|
+
while (next && imports.length > 0 && imports[imports.length - 1]?.endPosition.row === next.startPosition.row) imports.pop();
|
|
4861
|
+
const first = imports[0];
|
|
4862
|
+
const last = imports[imports.length - 1];
|
|
4863
|
+
if (!first || !last) return null;
|
|
4864
|
+
return { startRow: first.startPosition.row, endRow: last.endPosition.row, count: imports.length };
|
|
4865
|
+
}
|
|
4866
|
+
function isPreamble(node, docstring) {
|
|
4867
|
+
return node.type === "comment" || node.type === "hash_bang_line" || node.id === docstring?.id;
|
|
4868
|
+
}
|
|
4869
|
+
function collapseBlanks(lines) {
|
|
4870
|
+
const out = [];
|
|
4871
|
+
for (const line of lines) {
|
|
4872
|
+
const blank = line.text === "" && !line.verbatim;
|
|
4873
|
+
const prev = out[out.length - 1];
|
|
4874
|
+
if (blank && (!prev || prev.text === "" && !prev.verbatim)) continue;
|
|
4875
|
+
out.push(line);
|
|
4363
4876
|
}
|
|
4364
|
-
|
|
4877
|
+
while (out.length > 0 && out[out.length - 1]?.text === "" && !out[out.length - 1]?.verbatim) out.pop();
|
|
4878
|
+
return out;
|
|
4365
4879
|
}
|
|
4366
|
-
function
|
|
4367
|
-
|
|
4880
|
+
function finish(lines, trailingNewline) {
|
|
4881
|
+
const body = lines.map((l) => l.text).join("\n");
|
|
4882
|
+
return {
|
|
4883
|
+
text: trailingNewline && lines.length > 0 ? `${body}
|
|
4884
|
+
` : body,
|
|
4885
|
+
lineMap: lines.map((l) => l.line)
|
|
4886
|
+
};
|
|
4368
4887
|
}
|
|
4369
4888
|
|
|
4370
4889
|
// src/analysis/relevance.ts
|
|
@@ -4390,290 +4909,428 @@ function symmetrize(edges) {
|
|
|
4390
4909
|
return out;
|
|
4391
4910
|
}
|
|
4392
4911
|
|
|
4393
|
-
// src/analysis/
|
|
4394
|
-
var
|
|
4395
|
-
var
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
set.add(e.srcId);
|
|
4405
|
-
}
|
|
4406
|
-
const out = [];
|
|
4407
|
-
for (const [id, consumers] of bySymbol) {
|
|
4408
|
-
const parsed = parseSymbolId(id);
|
|
4409
|
-
if (!parsed) continue;
|
|
4410
|
-
out.push({
|
|
4411
|
-
symbolId: id,
|
|
4412
|
-
fileId: parsed.fileId,
|
|
4413
|
-
name: parsed.name,
|
|
4414
|
-
consumers: [...consumers].sort()
|
|
4415
|
-
});
|
|
4912
|
+
// src/analysis/context/signature-tree.ts
|
|
4913
|
+
var ELISION_MARKER = "...";
|
|
4914
|
+
var TRUNCATION_SUFFIX = "\u2026";
|
|
4915
|
+
var DEFAULT_MAX_LINE_CHARS = 120;
|
|
4916
|
+
var INDENT = " ";
|
|
4917
|
+
function renderSignatureTree(nodes, options = {}) {
|
|
4918
|
+
const max = options.maxLineChars ?? DEFAULT_MAX_LINE_CHARS;
|
|
4919
|
+
const lines = [];
|
|
4920
|
+
for (const [file, entries] of groupByFile(nodes)) {
|
|
4921
|
+
lines.push(truncate(file, max));
|
|
4922
|
+
for (const line of fileLines(entries)) lines.push(truncate(line, max));
|
|
4416
4923
|
}
|
|
4417
|
-
return
|
|
4924
|
+
return lines.join("\n");
|
|
4418
4925
|
}
|
|
4419
|
-
function
|
|
4420
|
-
|
|
4421
|
-
|
|
4926
|
+
function groupByFile(nodes) {
|
|
4927
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
4928
|
+
for (const node of nodes) {
|
|
4929
|
+
if (node.kind !== "symbol") continue;
|
|
4930
|
+
const parsed = parseSymbolId(node.id);
|
|
4931
|
+
const file = node.parentId ?? parsed?.fileId ?? node.id;
|
|
4932
|
+
const list = byFile.get(file) ?? [];
|
|
4933
|
+
list.push(toEntry(node, parsed?.name ?? node.name));
|
|
4934
|
+
byFile.set(file, list);
|
|
4422
4935
|
}
|
|
4423
|
-
return a
|
|
4936
|
+
return [...byFile].sort(([a], [b]) => compare2(a, b));
|
|
4424
4937
|
}
|
|
4425
|
-
function
|
|
4426
|
-
const
|
|
4427
|
-
const
|
|
4428
|
-
|
|
4429
|
-
const counts = /* @__PURE__ */ new Map();
|
|
4430
|
-
for (const symbols of bySource.values()) {
|
|
4431
|
-
if (symbols.length < 2 || symbols.length > largeThreshold) continue;
|
|
4432
|
-
accumulateSymbolPairs(symbols, counts);
|
|
4433
|
-
}
|
|
4434
|
-
return finalizePairs(counts, minCoImports);
|
|
4938
|
+
function toEntry(node, qualifiedName) {
|
|
4939
|
+
const start = numberAttr(node, "startLine");
|
|
4940
|
+
const end = numberAttr(node, "endLine") ?? start;
|
|
4941
|
+
return { node, start, end, depth: qualifiedName.split(".").length - 1 };
|
|
4435
4942
|
}
|
|
4436
|
-
function
|
|
4437
|
-
const
|
|
4438
|
-
|
|
4439
|
-
if (!parseSymbolId(e.dstId)) continue;
|
|
4440
|
-
let set = perSource.get(e.srcId);
|
|
4441
|
-
if (!set) {
|
|
4442
|
-
set = /* @__PURE__ */ new Set();
|
|
4443
|
-
perSource.set(e.srcId, set);
|
|
4444
|
-
}
|
|
4445
|
-
set.add(e.dstId);
|
|
4446
|
-
}
|
|
4447
|
-
const out = /* @__PURE__ */ new Map();
|
|
4448
|
-
for (const [src, set] of perSource) out.set(src, [...set].sort());
|
|
4449
|
-
return out;
|
|
4943
|
+
function numberAttr(node, key) {
|
|
4944
|
+
const value = node.attrs?.[key];
|
|
4945
|
+
return typeof value === "number" ? value : void 0;
|
|
4450
4946
|
}
|
|
4451
|
-
function
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4947
|
+
function fileLines(entries) {
|
|
4948
|
+
const ordered = entries.sort(compareEntries);
|
|
4949
|
+
const lines = [];
|
|
4950
|
+
let reach;
|
|
4951
|
+
for (const entry of ordered) {
|
|
4952
|
+
if (reach !== void 0 && entry.start !== void 0 && !adjacent(reach, entry.start)) {
|
|
4953
|
+
lines.push(ELISION_MARKER);
|
|
4456
4954
|
}
|
|
4955
|
+
lines.push(INDENT.repeat(entry.depth) + symbolText(entry.node));
|
|
4956
|
+
if (entry.end !== void 0) reach = Math.max(reach ?? entry.end, entry.end);
|
|
4457
4957
|
}
|
|
4958
|
+
return lines;
|
|
4458
4959
|
}
|
|
4459
|
-
function
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
bName: b.name,
|
|
4473
|
-
coImports,
|
|
4474
|
-
crossFile: a.fileId !== b.fileId
|
|
4475
|
-
});
|
|
4960
|
+
function adjacent(reach, nextStart) {
|
|
4961
|
+
return nextStart <= reach + 1;
|
|
4962
|
+
}
|
|
4963
|
+
function symbolText(node) {
|
|
4964
|
+
const signature = node.attrs?.signature;
|
|
4965
|
+
const text = typeof signature === "string" && signature !== "" ? signature : `${node.name} (${node.kind})`;
|
|
4966
|
+
return text.replace(/\s*\n\s*/g, " ");
|
|
4967
|
+
}
|
|
4968
|
+
function compareEntries(a, b) {
|
|
4969
|
+
if (a.start !== b.start) {
|
|
4970
|
+
if (a.start === void 0) return 1;
|
|
4971
|
+
if (b.start === void 0) return -1;
|
|
4972
|
+
return a.start - b.start;
|
|
4476
4973
|
}
|
|
4477
|
-
return
|
|
4974
|
+
return compare2(a.node.name, b.node.name) || compare2(a.node.id, b.node.id);
|
|
4975
|
+
}
|
|
4976
|
+
function compare2(a, b) {
|
|
4977
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
4478
4978
|
}
|
|
4479
|
-
function
|
|
4480
|
-
if (
|
|
4481
|
-
if (
|
|
4482
|
-
|
|
4483
|
-
return a.bId < b.bId ? -1 : a.bId > b.bId ? 1 : 0;
|
|
4979
|
+
function truncate(line, max) {
|
|
4980
|
+
if (line.length <= max) return line;
|
|
4981
|
+
if (max <= TRUNCATION_SUFFIX.length) return TRUNCATION_SUFFIX.slice(0, Math.max(max, 0));
|
|
4982
|
+
return line.slice(0, max - TRUNCATION_SUFFIX.length) + TRUNCATION_SUFFIX;
|
|
4484
4983
|
}
|
|
4485
4984
|
|
|
4486
|
-
// src/analysis/
|
|
4487
|
-
var
|
|
4488
|
-
var
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
barrelTargets,
|
|
4500
|
-
resolveBarrels
|
|
4501
|
-
);
|
|
4502
|
-
const counts = countByPackage(resolvedPairs);
|
|
4503
|
-
const totalEdges = resolvedPairs.length;
|
|
4504
|
-
const filesByPkg = filesPerPackage(input.fileByPackage);
|
|
4505
|
-
const abstractByPkg = countAbstractFiles(input.nodes, pkgByFile);
|
|
4506
|
-
const perPackage = input.packages.map(
|
|
4507
|
-
(p) => buildPackageStats(p.id, counts, filesByPkg, abstractByPkg)
|
|
4508
|
-
);
|
|
4509
|
-
const pairCoupling = buildPairCoupling(resolvedPairs, filesByPkg);
|
|
4510
|
-
const modularityQ = computeModularity(perPackage, totalEdges);
|
|
4511
|
-
const flagsCount = perPackage.reduce((acc, p) => acc + p.flags.length, 0) + pairCoupling.filter((c) => c.flag === "tight").length;
|
|
4512
|
-
return { modularityQ, totalEdges, perPackage, pairCoupling, flagsCount };
|
|
4985
|
+
// src/analysis/context/dossier.ts
|
|
4986
|
+
var SCHEMA_VERSION2 = "2";
|
|
4987
|
+
var CONSUMER_NOTE = "cross-file import resolution only; intra-file and dynamic import() callers are excluded";
|
|
4988
|
+
function splitConsumers(files, roleByFile) {
|
|
4989
|
+
const source = [];
|
|
4990
|
+
const test = [];
|
|
4991
|
+
for (const f of files) (roleByFile.get(f) === "test" ? test : source).push(f);
|
|
4992
|
+
return {
|
|
4993
|
+
source,
|
|
4994
|
+
test,
|
|
4995
|
+
counts: { source: source.length, test: test.length, total: files.length },
|
|
4996
|
+
note: CONSUMER_NOTE
|
|
4997
|
+
};
|
|
4513
4998
|
}
|
|
4514
|
-
function
|
|
4999
|
+
function consumerCounts(edges) {
|
|
4515
5000
|
const out = /* @__PURE__ */ new Map();
|
|
4516
|
-
for (const
|
|
4517
|
-
if (pkgId === "") continue;
|
|
4518
|
-
for (const f of files) out.set(f, pkgId);
|
|
4519
|
-
}
|
|
5001
|
+
for (const c of computeSymbolConsumers(edges)) out.set(c.symbolId, c.consumers);
|
|
4520
5002
|
return out;
|
|
4521
5003
|
}
|
|
4522
|
-
function
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
5004
|
+
function fileIdOf(symbolId2) {
|
|
5005
|
+
return parseSymbolId(symbolId2)?.fileId ?? null;
|
|
5006
|
+
}
|
|
5007
|
+
function buildSymbolDossier(input) {
|
|
5008
|
+
const id = input.target.id;
|
|
5009
|
+
const parsed = parseSymbolId(id);
|
|
5010
|
+
const fileId2 = parsed?.fileId ?? "";
|
|
5011
|
+
const m = input.metrics.get(id);
|
|
5012
|
+
const consumers = consumerCounts(input.refEdges).get(id) ?? [];
|
|
5013
|
+
const complexity = m?.cognitiveMax ?? input.metrics.get(fileId2)?.cognitiveMax ?? 0;
|
|
5014
|
+
const churn = input.churnByFile.get(fileId2) ?? 0;
|
|
5015
|
+
const coupledWith = computeSymbolCoupling(input.refEdges).filter((p) => p.aId === id || p.bId === id).map((p) => p.aId === id ? partner(p.bId, p.bName, p.bFile, p.coImports) : partner(p.aId, p.aName, p.aFile, p.coImports)).slice(0, 15);
|
|
5016
|
+
return {
|
|
5017
|
+
exported: input.target.attrs?.exported !== false,
|
|
5018
|
+
signature: attrString(input.target.attrs?.signature),
|
|
5019
|
+
purpose: attrString(input.target.attrs?.purpose),
|
|
5020
|
+
complexity: { cognitive: m?.cognitiveMax, cyclomatic: m?.cyclomaticMax },
|
|
5021
|
+
utilization: m?.utilization ?? 0,
|
|
5022
|
+
consumers: splitConsumers(consumers, input.roleByFile),
|
|
5023
|
+
blastRadius: (m?.utilization ?? 0) * complexity * churn,
|
|
5024
|
+
coupledWith
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
5027
|
+
function partner(symbolId2, name, fileId2, coImports) {
|
|
5028
|
+
return { symbolId: symbolId2, name, fileId: fileId2, coImports };
|
|
5029
|
+
}
|
|
5030
|
+
function attrString(value) {
|
|
5031
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
5032
|
+
}
|
|
5033
|
+
function fileSymbols(fileId2, nodes, metrics, consumers) {
|
|
5034
|
+
const lines = [];
|
|
5035
|
+
for (const n of nodes) {
|
|
5036
|
+
if (n.kind !== "symbol" || n.parentId !== fileId2) continue;
|
|
5037
|
+
const m = metrics.get(n.id);
|
|
5038
|
+
const exported = n.attrs?.exported !== false;
|
|
5039
|
+
lines.push({
|
|
5040
|
+
name: n.name,
|
|
5041
|
+
exported,
|
|
5042
|
+
signature: attrString(n.attrs?.signature) ?? void 0,
|
|
5043
|
+
cognitive: m?.cognitiveMax,
|
|
5044
|
+
cyclomatic: m?.cyclomaticMax,
|
|
5045
|
+
utilization: m?.utilization ?? 0,
|
|
5046
|
+
consumers: (consumers.get(n.id) ?? []).length,
|
|
5047
|
+
rank: exported ? m?.utilization ?? 0 : -(m?.cognitiveMax ?? 0)
|
|
5048
|
+
});
|
|
4526
5049
|
}
|
|
4527
|
-
return
|
|
5050
|
+
return sortSymbols(lines);
|
|
4528
5051
|
}
|
|
4529
|
-
function
|
|
4530
|
-
const
|
|
4531
|
-
|
|
4532
|
-
);
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
const
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
5052
|
+
function sortSymbols(lines) {
|
|
5053
|
+
const exported = lines.filter((l) => l.exported).sort((a, b) => b.utilization - a.utilization || (b.cognitive ?? 0) - (a.cognitive ?? 0));
|
|
5054
|
+
const internal = lines.filter((l) => !l.exported).sort((a, b) => (b.cognitive ?? 0) - (a.cognitive ?? 0) || a.name.localeCompare(b.name));
|
|
5055
|
+
return [...exported, ...internal].map(({ rank: _rank, ...line }) => line);
|
|
5056
|
+
}
|
|
5057
|
+
function redistributeImportance(symbols, fileCentrality) {
|
|
5058
|
+
const totalUtil = symbols.reduce((sum, s) => sum + s.utilization, 0);
|
|
5059
|
+
if (totalUtil <= 0 || fileCentrality <= 0) return symbols;
|
|
5060
|
+
return symbols.map((s) => ({
|
|
5061
|
+
...s,
|
|
5062
|
+
importance: Math.round(fileCentrality * (s.utilization / totalUtil) * 1e6) / 1e6
|
|
5063
|
+
}));
|
|
5064
|
+
}
|
|
5065
|
+
function fileConsumers(fileId2, refEdges) {
|
|
5066
|
+
const set = /* @__PURE__ */ new Set();
|
|
5067
|
+
for (const e of refEdges) if (fileIdOf(e.dstId) === fileId2) set.add(e.srcId);
|
|
5068
|
+
return [...set].sort();
|
|
5069
|
+
}
|
|
5070
|
+
function fileDependsOn(fileId2, refEdges, importEdges) {
|
|
5071
|
+
const set = /* @__PURE__ */ new Set();
|
|
5072
|
+
for (const e of refEdges) if (e.srcId === fileId2) {
|
|
5073
|
+
const f = fileIdOf(e.dstId);
|
|
5074
|
+
if (f) set.add(f);
|
|
5075
|
+
}
|
|
5076
|
+
for (const e of importEdges) if (e.srcId === fileId2) set.add(e.dstId);
|
|
5077
|
+
set.delete(fileId2);
|
|
5078
|
+
return [...set].sort();
|
|
5079
|
+
}
|
|
5080
|
+
function buildFileDossier(input) {
|
|
5081
|
+
const fileId2 = input.target.id;
|
|
5082
|
+
const consumers = consumerCounts(input.refEdges);
|
|
5083
|
+
const symbolUtil = collectSymbolUtil(input.nodes, input.metrics).filter((s) => s.fileId === fileId2);
|
|
5084
|
+
const churnValue = input.churnByFile.get(fileId2);
|
|
5085
|
+
const centrality = input.centrality.get(fileId2) ?? 0;
|
|
5086
|
+
return {
|
|
5087
|
+
metrics: input.metrics.get(fileId2) ?? {},
|
|
5088
|
+
churn: churnValue === void 0 ? null : { windowDays: input.churnWindowDays, value: churnValue },
|
|
5089
|
+
centrality,
|
|
5090
|
+
ownership: input.ownership?.get(fileId2) ?? null,
|
|
5091
|
+
symbols: redistributeImportance(
|
|
5092
|
+
fileSymbols(fileId2, input.nodes, input.metrics, consumers),
|
|
5093
|
+
centrality
|
|
5094
|
+
),
|
|
5095
|
+
dependsOn: fileDependsOn(fileId2, input.refEdges, input.importEdges),
|
|
5096
|
+
consumers: splitConsumers(fileConsumers(fileId2, input.refEdges), input.roleByFile),
|
|
5097
|
+
blastRadius: buildBlastRadius(symbolUtil, input.metrics, input.churnByFile, 15)
|
|
4561
5098
|
};
|
|
4562
|
-
for (const id of barrelIds) resolve4(id, /* @__PURE__ */ new Set());
|
|
4563
|
-
return resolved;
|
|
4564
5099
|
}
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
5100
|
+
var SIGNATURE_NOTE = "Type signatures and docstrings are indexed for resolvable TypeScript declarations (C-79); a null signature means it was unannotated or unresolvable \u2014 open target.path at target.span.";
|
|
5101
|
+
function buildContextDossier(input) {
|
|
5102
|
+
const { target, kind } = input;
|
|
5103
|
+
const span = readSpan(target);
|
|
5104
|
+
const path14 = kind === "symbol" ? parseSymbolId(target.id)?.fileId ?? target.id : target.id;
|
|
5105
|
+
const notes = [SIGNATURE_NOTE];
|
|
5106
|
+
if (kind === "file" && input.ownership === null) {
|
|
5107
|
+
notes.push("Ownership omitted (git-derived; run without --no-ownership on a repo checkout).");
|
|
5108
|
+
}
|
|
5109
|
+
return {
|
|
5110
|
+
schemaVersion: SCHEMA_VERSION2,
|
|
5111
|
+
target: { id: target.id, kind, name: target.name, path: path14, span },
|
|
5112
|
+
provenance: input.provenance,
|
|
5113
|
+
symbol: kind === "symbol" ? buildSymbolDossier(input) : void 0,
|
|
5114
|
+
file: kind === "file" ? buildFileDossier(input) : void 0,
|
|
5115
|
+
notes
|
|
5116
|
+
};
|
|
5117
|
+
}
|
|
5118
|
+
function readSpan(node) {
|
|
5119
|
+
const start = node.attrs?.startLine;
|
|
5120
|
+
const end = node.attrs?.endLine;
|
|
5121
|
+
if (typeof start === "number" && typeof end === "number") return { startLine: start, endLine: end };
|
|
5122
|
+
return void 0;
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5125
|
+
// src/analysis/context/format.ts
|
|
5126
|
+
function renderContextMarkdown(d) {
|
|
5127
|
+
const span = d.target.span ? `:${d.target.span.startLine}-${d.target.span.endLine}` : "";
|
|
5128
|
+
const lines = [
|
|
5129
|
+
`# ${d.target.kind === "symbol" ? d.target.name : d.target.path}`,
|
|
5130
|
+
"",
|
|
5131
|
+
`- **kind**: ${d.target.kind}`,
|
|
5132
|
+
`- **path**: \`${d.target.path}${span}\``,
|
|
5133
|
+
`- **snapshot**: ${d.provenance.ref} (${d.provenance.commitHash ?? "\u2014"}) @ ${d.provenance.takenAt}`,
|
|
5134
|
+
""
|
|
5135
|
+
];
|
|
5136
|
+
if (d.symbol) lines.push(...renderSymbol(d.symbol));
|
|
5137
|
+
if (d.file) lines.push(...renderFile(d.file));
|
|
5138
|
+
if (d.notes.length) lines.push("## Notes", ...d.notes.map((n) => `- ${n}`), "");
|
|
5139
|
+
return lines.join("\n");
|
|
5140
|
+
}
|
|
5141
|
+
function renderSymbol(s) {
|
|
5142
|
+
const out = [
|
|
5143
|
+
"## Symbol",
|
|
5144
|
+
`- **exported**: ${s.exported}`,
|
|
5145
|
+
`- **signature**: ${s.signature ? `\`${s.signature}\`` : "\u2014 (not indexed)"}`,
|
|
5146
|
+
`- **purpose**: ${s.purpose ?? "\u2014"}`,
|
|
5147
|
+
`- **complexity**: cognitive ${s.complexity.cognitive ?? "\u2014"}, cyclomatic ${s.complexity.cyclomatic ?? "\u2014"}`,
|
|
5148
|
+
`- **utilization**: ${s.utilization}`,
|
|
5149
|
+
`- **blast radius**: ${round(s.blastRadius)}`,
|
|
5150
|
+
...renderConsumers(s.consumers)
|
|
5151
|
+
];
|
|
5152
|
+
if (s.coupledWith.length) {
|
|
5153
|
+
out.push("### Co-imported with");
|
|
5154
|
+
for (const c of s.coupledWith) out.push(`- ${c.name} (\`${c.fileId}\`) \xD7${c.coImports}`);
|
|
5155
|
+
out.push("");
|
|
4576
5156
|
}
|
|
4577
5157
|
return out;
|
|
4578
5158
|
}
|
|
4579
|
-
function
|
|
4580
|
-
const
|
|
4581
|
-
const
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
}
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
5159
|
+
function renderFile(f) {
|
|
5160
|
+
const m = f.metrics;
|
|
5161
|
+
const own = f.ownership ? `${f.ownership.primaryOwner} (bus factor ${f.ownership.busFactor}, ${f.ownership.authorCount} authors)` : "\u2014";
|
|
5162
|
+
const out = [
|
|
5163
|
+
"## File",
|
|
5164
|
+
`- **loc**: ${m.loc ?? "\u2014"} \xB7 **cognitive** ${m.cognitiveMax ?? "\u2014"} \xB7 **cyclomatic** ${m.cyclomaticMax ?? "\u2014"}`,
|
|
5165
|
+
`- **fan-in/out**: ${m.fanIn ?? "\u2014"} / ${m.fanOut ?? "\u2014"} \xB7 **centrality**: ${round(f.centrality)}`,
|
|
5166
|
+
`- **churn**: ${f.churn ? `${f.churn.value} (${f.churn.windowDays}d)` : "\u2014"} \xB7 **role**: ${m.role ?? "\u2014"}`,
|
|
5167
|
+
`- **ownership**: ${own}`,
|
|
5168
|
+
"",
|
|
5169
|
+
...section(`Depends on (${f.dependsOn.length})`, f.dependsOn),
|
|
5170
|
+
...renderConsumers(f.consumers)
|
|
5171
|
+
];
|
|
5172
|
+
if (f.symbols.length) {
|
|
5173
|
+
out.push(`### Symbols (${f.symbols.length})`);
|
|
5174
|
+
for (const s of f.symbols) {
|
|
5175
|
+
const tag = s.exported ? "export" : "internal";
|
|
5176
|
+
const sig = s.signature ? ` \`${s.signature}\`` : "";
|
|
5177
|
+
const imp = s.importance !== void 0 ? `, importance ${s.importance}` : "";
|
|
5178
|
+
out.push(`- \`${s.name}\` (${tag})${sig} \u2014 util ${s.utilization}, cog ${s.cognitive ?? "\u2014"}, consumers ${s.consumers}${imp}`);
|
|
4595
5179
|
}
|
|
5180
|
+
out.push("");
|
|
5181
|
+
}
|
|
5182
|
+
if (f.blastRadius.length) {
|
|
5183
|
+
out.push("### Blast radius (look here first)");
|
|
5184
|
+
for (const b of f.blastRadius) out.push(`- \`${b.name}\` \u2014 score ${round(b.score)} (util ${b.utilization} \xD7 cog ${b.complexity} \xD7 churn ${b.churn})`);
|
|
5185
|
+
out.push("");
|
|
4596
5186
|
}
|
|
4597
5187
|
return out;
|
|
4598
5188
|
}
|
|
4599
|
-
function
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
const
|
|
4605
|
-
const
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
5189
|
+
function section(title, items) {
|
|
5190
|
+
if (!items.length) return [];
|
|
5191
|
+
return [`### ${title}`, ...items.map((i) => `- \`${i}\``), ""];
|
|
5192
|
+
}
|
|
5193
|
+
function renderConsumers(c) {
|
|
5194
|
+
const header = `### Consumers \u2014 ${c.counts.source} source, ${c.counts.test} test (${c.counts.total} total)`;
|
|
5195
|
+
const rows = c.source.map((i) => `- \`${i}\``);
|
|
5196
|
+
return [header, ...rows.length ? rows : ["- (no non-test consumers)"], `> ${c.note}`, ""];
|
|
5197
|
+
}
|
|
5198
|
+
function round(n) {
|
|
5199
|
+
return Math.round(n * 1e3) / 1e3;
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
// src/analysis/context/bundle.ts
|
|
5203
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5204
|
+
import * as path12 from "path";
|
|
5205
|
+
var BUNDLE_SCHEMA_VERSION = "2";
|
|
5206
|
+
var EDGE_NOTE = "references resolve cross-file import usage (src = importing file \u2192 dst = symbol); a symbol's dependencies are its declaring file's outbound edges (the graph resolves the source side at file granularity).";
|
|
5207
|
+
var COVERAGE_NOTE = "coverage_pct overlay not ingested; run `graph coverage <istanbul-report>` to populate.";
|
|
5208
|
+
var SOURCE_READ_NOTE = "source file could not be read from the working tree at this path.";
|
|
5209
|
+
function weightOf(e) {
|
|
5210
|
+
const w = e.attrs?.weight;
|
|
5211
|
+
return typeof w === "number" ? w : 1;
|
|
5212
|
+
}
|
|
5213
|
+
function relevanceCtx(input) {
|
|
5214
|
+
const { relevanceByFile, targetFileId: targetFileId2 } = input;
|
|
5215
|
+
if (!relevanceByFile || relevanceByFile.size === 0 || targetFileId2 === void 0) return null;
|
|
5216
|
+
return { relevanceByFile, targetFileId: targetFileId2 };
|
|
5217
|
+
}
|
|
5218
|
+
function fileOf(id) {
|
|
5219
|
+
return parseSymbolId(id)?.fileId ?? id;
|
|
5220
|
+
}
|
|
5221
|
+
function rankEdges(edges, ctx) {
|
|
5222
|
+
if (!ctx) return sortByWeight(edges);
|
|
5223
|
+
const annotated = edges.map((e) => {
|
|
5224
|
+
const neighbor = fileOf(e.from) === ctx.targetFileId ? fileOf(e.to) : fileOf(e.from);
|
|
5225
|
+
return { ...e, relevance: round4(ctx.relevanceByFile.get(neighbor) ?? 0) };
|
|
5226
|
+
});
|
|
5227
|
+
return annotated.sort(
|
|
5228
|
+
(a, b) => (b.relevance ?? 0) - (a.relevance ?? 0) || b.weight - a.weight || a.from.localeCompare(b.from) || a.to.localeCompare(b.to)
|
|
5229
|
+
);
|
|
5230
|
+
}
|
|
5231
|
+
function sortByWeight(edges) {
|
|
5232
|
+
return edges.sort(
|
|
5233
|
+
(a, b) => b.weight - a.weight || a.from.localeCompare(b.from) || a.to.localeCompare(b.to)
|
|
5234
|
+
);
|
|
5235
|
+
}
|
|
5236
|
+
function round4(n) {
|
|
5237
|
+
return Math.round(n * 1e6) / 1e6;
|
|
5238
|
+
}
|
|
5239
|
+
function symbolEdges(input) {
|
|
5240
|
+
const id = input.target.id;
|
|
5241
|
+
const fileId2 = parseSymbolId(id)?.fileId ?? "";
|
|
5242
|
+
const ctx = relevanceCtx(input);
|
|
5243
|
+
const callers = input.refEdges.filter((e) => e.dstId === id).map((e) => ({ from: e.srcId, to: id, kind: "references", weight: weightOf(e) }));
|
|
5244
|
+
const dependencies = [
|
|
5245
|
+
...input.refEdges.filter((e) => e.srcId === fileId2).map((e) => edge(e, "references")),
|
|
5246
|
+
...input.importEdges.filter((e) => e.srcId === fileId2).map((e) => edge(e, "imports"))
|
|
5247
|
+
];
|
|
5248
|
+
const coupledWith = (input.dossier.symbol?.coupledWith ?? []).map((c) => ({
|
|
5249
|
+
from: id,
|
|
5250
|
+
to: c.symbolId,
|
|
5251
|
+
kind: "coupled-with",
|
|
5252
|
+
weight: c.coImports
|
|
5253
|
+
}));
|
|
4610
5254
|
return {
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
incomingEdges: c.incoming,
|
|
4616
|
-
cohesion,
|
|
4617
|
-
instability,
|
|
4618
|
-
abstractness,
|
|
4619
|
-
layer,
|
|
4620
|
-
flags
|
|
5255
|
+
callers: rankEdges(callers, ctx),
|
|
5256
|
+
dependencies: rankEdges(dependencies, ctx),
|
|
5257
|
+
coupledWith: rankEdges(coupledWith, ctx),
|
|
5258
|
+
note: EDGE_NOTE
|
|
4621
5259
|
};
|
|
4622
5260
|
}
|
|
4623
|
-
function
|
|
4624
|
-
const
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
5261
|
+
function fileEdges(input) {
|
|
5262
|
+
const id = input.target.id;
|
|
5263
|
+
const ctx = relevanceCtx(input);
|
|
5264
|
+
const inbound = (e) => parseSymbolId(e.dstId)?.fileId === id;
|
|
5265
|
+
const callers = [
|
|
5266
|
+
...input.refEdges.filter(inbound).map((e) => edge(e, "references")),
|
|
5267
|
+
...input.importEdges.filter((e) => e.dstId === id).map((e) => edge(e, "imports"))
|
|
5268
|
+
];
|
|
5269
|
+
const dependencies = [
|
|
5270
|
+
...input.refEdges.filter((e) => e.srcId === id).map((e) => edge(e, "references")),
|
|
5271
|
+
...input.importEdges.filter((e) => e.srcId === id).map((e) => edge(e, "imports"))
|
|
5272
|
+
];
|
|
5273
|
+
return {
|
|
5274
|
+
callers: rankEdges(callers, ctx),
|
|
5275
|
+
dependencies: rankEdges(dependencies, ctx),
|
|
5276
|
+
coupledWith: [],
|
|
5277
|
+
note: EDGE_NOTE
|
|
5278
|
+
};
|
|
5279
|
+
}
|
|
5280
|
+
function edge(e, kind) {
|
|
5281
|
+
return { from: e.srcId, to: e.dstId, kind, weight: weightOf(e) };
|
|
5282
|
+
}
|
|
5283
|
+
function readSource(input) {
|
|
5284
|
+
const filePath = input.kind === "symbol" ? parseSymbolId(input.target.id)?.fileId ?? input.target.id : input.target.id;
|
|
5285
|
+
const span = input.dossier.target.span ?? null;
|
|
5286
|
+
const abs = input.repoRoot ? path12.join(input.repoRoot, filePath) : filePath;
|
|
5287
|
+
try {
|
|
5288
|
+
const content = readFileSync3(abs, "utf8");
|
|
5289
|
+
const text = span ? sliceSpan(content, span) : content;
|
|
5290
|
+
return { path: filePath, span, text };
|
|
5291
|
+
} catch {
|
|
5292
|
+
return { path: filePath, span, text: null, note: SOURCE_READ_NOTE };
|
|
4629
5293
|
}
|
|
4630
|
-
return out;
|
|
4631
5294
|
}
|
|
4632
|
-
function
|
|
4633
|
-
return
|
|
5295
|
+
function sliceSpan(content, span) {
|
|
5296
|
+
return content.split("\n").slice(span.startLine - 1, span.endLine).join("\n");
|
|
4634
5297
|
}
|
|
4635
|
-
function
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
5298
|
+
function buildContextBundle(input) {
|
|
5299
|
+
return {
|
|
5300
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
5301
|
+
dossier: input.dossier,
|
|
5302
|
+
source: readSource(input),
|
|
5303
|
+
edges: input.kind === "symbol" ? symbolEdges(input) : fileEdges(input),
|
|
5304
|
+
coverage: {
|
|
5305
|
+
pct: input.coveragePct,
|
|
5306
|
+
note: input.coveragePct === null ? COVERAGE_NOTE : void 0
|
|
5307
|
+
}
|
|
5308
|
+
};
|
|
4639
5309
|
}
|
|
4640
|
-
function
|
|
4641
|
-
const
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
}
|
|
4663
|
-
|
|
4664
|
-
if (a.from !== b.from) return a.from < b.from ? -1 : 1;
|
|
4665
|
-
return a.to < b.to ? -1 : 1;
|
|
4666
|
-
}
|
|
4667
|
-
function computeModularity(perPackage, totalEdges) {
|
|
4668
|
-
if (totalEdges === 0) return 0;
|
|
4669
|
-
let Q = 0;
|
|
4670
|
-
for (const p of perPackage) {
|
|
4671
|
-
const e_cc = p.internalEdges / totalEdges;
|
|
4672
|
-
const touching = p.internalEdges * 2 + p.outgoingEdges + p.incomingEdges;
|
|
4673
|
-
const a_c = touching / (2 * totalEdges);
|
|
4674
|
-
Q += e_cc - a_c * a_c;
|
|
4675
|
-
}
|
|
4676
|
-
return Q;
|
|
5310
|
+
function renderBundleText(bundle) {
|
|
5311
|
+
const cov = bundle.coverage.pct === null ? "\u2014" : `${bundle.coverage.pct}%`;
|
|
5312
|
+
return [
|
|
5313
|
+
renderContextMarkdown(bundle.dossier),
|
|
5314
|
+
"## Coverage",
|
|
5315
|
+
`- **coverage**: ${cov}`,
|
|
5316
|
+
"",
|
|
5317
|
+
...renderEdgeSection("Callers (inbound)", bundle.edges.callers),
|
|
5318
|
+
...renderEdgeSection("Dependencies (outbound)", bundle.edges.dependencies),
|
|
5319
|
+
...renderEdgeSection("Coupled with", bundle.edges.coupledWith),
|
|
5320
|
+
"## Source",
|
|
5321
|
+
"```typescript",
|
|
5322
|
+
bundle.source.text ?? `// ${bundle.source.note ?? "unavailable"}`,
|
|
5323
|
+
"```",
|
|
5324
|
+
""
|
|
5325
|
+
].join("\n");
|
|
5326
|
+
}
|
|
5327
|
+
function renderEdgeSection(title, edges) {
|
|
5328
|
+
if (!edges.length) return [];
|
|
5329
|
+
return [`### ${title}`, ...edges.map(renderEdgeLine), ""];
|
|
5330
|
+
}
|
|
5331
|
+
function renderEdgeLine(e) {
|
|
5332
|
+
const rel = e.relevance !== void 0 ? `, rel ${e.relevance}` : "";
|
|
5333
|
+
return `- \`${e.from}\` \u2192 \`${e.to}\` (${e.kind} \xD7${e.weight}${rel})`;
|
|
4677
5334
|
}
|
|
4678
5335
|
|
|
4679
5336
|
// src/analysis/snapshot.ts
|
|
@@ -4693,6 +5350,57 @@ function snapshotSymbolCoupling(store, snapshotId, options = {}) {
|
|
|
4693
5350
|
return computeSymbolCoupling(snapshotReferenceEdges(store, snapshotId), options);
|
|
4694
5351
|
}
|
|
4695
5352
|
|
|
5353
|
+
// src/analysis/dashboard-symbol-coupling.ts
|
|
5354
|
+
var MIN_SHARED_CONSUMERS = 2;
|
|
5355
|
+
var MAX_PAIRS = 40;
|
|
5356
|
+
var MAX_GROUPS = 15;
|
|
5357
|
+
var MAX_SYMBOLS_PER_GROUP = 10;
|
|
5358
|
+
var CONSUMER_SAMPLE = 12;
|
|
5359
|
+
function buildSymbolCouplingPayload(edges) {
|
|
5360
|
+
return {
|
|
5361
|
+
symbolCoupling: buildCouplingRows(edges),
|
|
5362
|
+
symbolConsumers: buildConsumerGroups(edges)
|
|
5363
|
+
};
|
|
5364
|
+
}
|
|
5365
|
+
function buildCouplingRows(edges) {
|
|
5366
|
+
return computeSymbolCoupling(edges).slice(0, MAX_PAIRS).map((p) => ({
|
|
5367
|
+
aName: p.aName,
|
|
5368
|
+
aFile: p.aFile,
|
|
5369
|
+
bName: p.bName,
|
|
5370
|
+
bFile: p.bFile,
|
|
5371
|
+
coImports: p.coImports,
|
|
5372
|
+
crossFile: p.crossFile
|
|
5373
|
+
}));
|
|
5374
|
+
}
|
|
5375
|
+
function buildConsumerGroups(edges) {
|
|
5376
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
5377
|
+
for (const s of computeSymbolConsumers(edges)) {
|
|
5378
|
+
if (s.consumers.length < MIN_SHARED_CONSUMERS) continue;
|
|
5379
|
+
const bucket = byFile.get(s.fileId) ?? [];
|
|
5380
|
+
bucket.push({
|
|
5381
|
+
name: s.name,
|
|
5382
|
+
consumers: s.consumers.slice(0, CONSUMER_SAMPLE),
|
|
5383
|
+
consumerCount: s.consumers.length
|
|
5384
|
+
});
|
|
5385
|
+
byFile.set(s.fileId, bucket);
|
|
5386
|
+
}
|
|
5387
|
+
return foldGroups(byFile);
|
|
5388
|
+
}
|
|
5389
|
+
function foldGroups(byFile) {
|
|
5390
|
+
const groups = [];
|
|
5391
|
+
for (const [fileId2, symbols] of byFile) {
|
|
5392
|
+
const kept = symbols.slice(0, MAX_SYMBOLS_PER_GROUP);
|
|
5393
|
+
groups.push({
|
|
5394
|
+
fileId: fileId2,
|
|
5395
|
+
symbols: kept,
|
|
5396
|
+
totalConsumers: kept.reduce((n, s) => n + s.consumerCount, 0)
|
|
5397
|
+
});
|
|
5398
|
+
}
|
|
5399
|
+
return groups.sort(
|
|
5400
|
+
(a, b) => b.symbols.length - a.symbols.length || b.totalConsumers - a.totalConsumers || (a.fileId < b.fileId ? -1 : 1)
|
|
5401
|
+
).slice(0, MAX_GROUPS);
|
|
5402
|
+
}
|
|
5403
|
+
|
|
4696
5404
|
// src/read.ts
|
|
4697
5405
|
function listNodes(store, snapshotId, opts) {
|
|
4698
5406
|
return store.listNodes(snapshotId, opts);
|
|
@@ -4788,7 +5496,7 @@ var SOURCE = [
|
|
|
4788
5496
|
direction: "higher-worse",
|
|
4789
5497
|
absent: "exclude",
|
|
4790
5498
|
source: "source-metrics",
|
|
4791
|
-
description: "Highest cyclomatic complexity of any function in the file."
|
|
5499
|
+
description: "Highest cyclomatic complexity of any function in the file; points attention at branchy code."
|
|
4792
5500
|
},
|
|
4793
5501
|
{
|
|
4794
5502
|
name: "cyclomatic_sum",
|
|
@@ -4798,7 +5506,7 @@ var SOURCE = [
|
|
|
4798
5506
|
direction: "higher-worse",
|
|
4799
5507
|
absent: "zero",
|
|
4800
5508
|
source: "source-metrics",
|
|
4801
|
-
description: "Cyclomatic complexity summed over the file's functions."
|
|
5509
|
+
description: "Cyclomatic complexity summed over the file's functions; points attention at branchy code."
|
|
4802
5510
|
},
|
|
4803
5511
|
{
|
|
4804
5512
|
name: "cognitive_max",
|
|
@@ -4808,7 +5516,7 @@ var SOURCE = [
|
|
|
4808
5516
|
direction: "higher-worse",
|
|
4809
5517
|
absent: "exclude",
|
|
4810
5518
|
source: "source-metrics",
|
|
4811
|
-
description: "Highest cognitive complexity of any function in the file."
|
|
5519
|
+
description: "Highest cognitive complexity of any function in the file, an estimate of comprehension friction."
|
|
4812
5520
|
},
|
|
4813
5521
|
{
|
|
4814
5522
|
name: "cognitive_sum",
|
|
@@ -4818,7 +5526,7 @@ var SOURCE = [
|
|
|
4818
5526
|
direction: "higher-worse",
|
|
4819
5527
|
absent: "zero",
|
|
4820
5528
|
source: "source-metrics",
|
|
4821
|
-
description: "Cognitive complexity summed over the file's functions."
|
|
5529
|
+
description: "Cognitive complexity summed over the file's functions, an estimate of comprehension friction."
|
|
4822
5530
|
},
|
|
4823
5531
|
{
|
|
4824
5532
|
name: "max_nesting_depth",
|
|
@@ -4830,6 +5538,16 @@ var SOURCE = [
|
|
|
4830
5538
|
source: "source-metrics",
|
|
4831
5539
|
description: "Deepest block nesting inside any function in the file."
|
|
4832
5540
|
},
|
|
5541
|
+
{
|
|
5542
|
+
name: "jsx_depth_max",
|
|
5543
|
+
unit: "count",
|
|
5544
|
+
appliesTo: FILE,
|
|
5545
|
+
rollup: "max",
|
|
5546
|
+
direction: "higher-worse",
|
|
5547
|
+
absent: "exclude",
|
|
5548
|
+
source: "source-metrics",
|
|
5549
|
+
description: "Deepest JSX element tree in the file: the max of symbol_jsx_depth over its functions and module-scope JSX. Written only when > 0."
|
|
5550
|
+
},
|
|
4833
5551
|
{
|
|
4834
5552
|
name: "symbol_cognitive",
|
|
4835
5553
|
unit: "count",
|
|
@@ -4838,7 +5556,7 @@ var SOURCE = [
|
|
|
4838
5556
|
direction: "higher-worse",
|
|
4839
5557
|
absent: "exclude",
|
|
4840
5558
|
source: "source-metrics",
|
|
4841
|
-
description: "Cognitive complexity of the function a symbol names; the max when several functions share the name."
|
|
5559
|
+
description: "Cognitive complexity (comprehension friction) of the function a symbol names; the max when several functions share the name."
|
|
4842
5560
|
},
|
|
4843
5561
|
{
|
|
4844
5562
|
name: "symbol_cyclomatic",
|
|
@@ -4848,7 +5566,7 @@ var SOURCE = [
|
|
|
4848
5566
|
direction: "higher-worse",
|
|
4849
5567
|
absent: "exclude",
|
|
4850
5568
|
source: "source-metrics",
|
|
4851
|
-
description: "Cyclomatic complexity of the function a symbol names; the max when several functions share the name."
|
|
5569
|
+
description: "Cyclomatic complexity (an attention pointer) of the function a symbol names; the max when several functions share the name."
|
|
4852
5570
|
},
|
|
4853
5571
|
{
|
|
4854
5572
|
name: "symbol_loc",
|
|
@@ -4870,6 +5588,16 @@ var SOURCE = [
|
|
|
4870
5588
|
source: "source-metrics",
|
|
4871
5589
|
description: "Deepest block nesting inside the function a symbol names; the max when several share the name."
|
|
4872
5590
|
},
|
|
5591
|
+
{
|
|
5592
|
+
name: "symbol_jsx_depth",
|
|
5593
|
+
unit: "count",
|
|
5594
|
+
appliesTo: SYMBOL,
|
|
5595
|
+
rollup: "max",
|
|
5596
|
+
direction: "higher-worse",
|
|
5597
|
+
absent: "exclude",
|
|
5598
|
+
source: "source-metrics",
|
|
5599
|
+
description: "Most JSX elements on one ancestor chain in the function, fragments not counted. Walks into {\u2026} and inline callbacks, stops at a nested named function; attribute JSX sits one below its owner. Written only when > 0."
|
|
5600
|
+
},
|
|
4873
5601
|
{
|
|
4874
5602
|
name: "symbol_comment_lines",
|
|
4875
5603
|
unit: "lines",
|
|
@@ -5116,7 +5844,7 @@ var HISTORY = [
|
|
|
5116
5844
|
absent: "exclude",
|
|
5117
5845
|
source: "history",
|
|
5118
5846
|
windowed: true,
|
|
5119
|
-
description: "min(1, file age / window) for a file that churned in the {w} window; 1 for lifetime.
|
|
5847
|
+
description: "min(1, file age / window) for a file that churned in the {w} window; 1 for lifetime. Discounts the hotspot score, churn \xD7 complexity after Adam Tornhill and CodeScene."
|
|
5120
5848
|
},
|
|
5121
5849
|
{
|
|
5122
5850
|
name: "file_age_days",
|
|
@@ -5259,39 +5987,6 @@ function indexMetrics(metrics) {
|
|
|
5259
5987
|
return metricsByNode;
|
|
5260
5988
|
}
|
|
5261
5989
|
|
|
5262
|
-
// src/check/patterns.ts
|
|
5263
|
-
function patternToRegex(pattern) {
|
|
5264
|
-
if (!pattern.includes("*")) {
|
|
5265
|
-
return new RegExp(escapeRegex(pattern));
|
|
5266
|
-
}
|
|
5267
|
-
let out = "";
|
|
5268
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
5269
|
-
const c = pattern[i];
|
|
5270
|
-
if (c === "*" && pattern[i + 1] === "*") {
|
|
5271
|
-
out += ".*";
|
|
5272
|
-
i++;
|
|
5273
|
-
} else if (c === "*") {
|
|
5274
|
-
out += "[^/]*";
|
|
5275
|
-
} else {
|
|
5276
|
-
out += escapeRegex(c);
|
|
5277
|
-
}
|
|
5278
|
-
}
|
|
5279
|
-
return new RegExp(`^${out}$`);
|
|
5280
|
-
}
|
|
5281
|
-
function compilePatterns(patterns) {
|
|
5282
|
-
if (!patterns) return [];
|
|
5283
|
-
return patterns.map(patternToRegex);
|
|
5284
|
-
}
|
|
5285
|
-
function matchesAny(value, patterns) {
|
|
5286
|
-
for (const rx of patterns) {
|
|
5287
|
-
if (rx.test(value)) return true;
|
|
5288
|
-
}
|
|
5289
|
-
return false;
|
|
5290
|
-
}
|
|
5291
|
-
function escapeRegex(s) {
|
|
5292
|
-
return s.replace(/[.+?(){}|^$\\[\]]/g, "\\$&");
|
|
5293
|
-
}
|
|
5294
|
-
|
|
5295
5990
|
// src/check/rule-helpers.ts
|
|
5296
5991
|
function severityOf(rule) {
|
|
5297
5992
|
return rule.severity ?? "error";
|
|
@@ -5300,8 +5995,8 @@ function formatNumber(n) {
|
|
|
5300
5995
|
if (Number.isInteger(n)) return String(n);
|
|
5301
5996
|
return n.toFixed(3).replace(/\.?0+$/, "");
|
|
5302
5997
|
}
|
|
5303
|
-
function isImportEdge(
|
|
5304
|
-
return
|
|
5998
|
+
function isImportEdge(edge2) {
|
|
5999
|
+
return edge2.kind === "imports" || edge2.kind === "re-exports";
|
|
5305
6000
|
}
|
|
5306
6001
|
function longestPrefixMatcher(prefixes) {
|
|
5307
6002
|
const byLength = [...prefixes].sort((a, b) => b.length - a.length);
|
|
@@ -5324,11 +6019,17 @@ function layerIndex(layers) {
|
|
|
5324
6019
|
function runLayeredDepsRule(rule, ctx) {
|
|
5325
6020
|
const packageLayer = layerIndex(rule.layers);
|
|
5326
6021
|
const packageOf = longestPrefixMatcher([...packageLayer.keys()]);
|
|
6022
|
+
const excludedRoles = new Set(rule.excludeRoles ?? []);
|
|
6023
|
+
const hasExcludedRole = (id) => {
|
|
6024
|
+
const role = ctx.nodesById.get(id)?.role;
|
|
6025
|
+
return role !== void 0 && excludedRoles.has(role);
|
|
6026
|
+
};
|
|
5327
6027
|
const out = [];
|
|
5328
|
-
for (const
|
|
5329
|
-
if (!isImportEdge(
|
|
5330
|
-
|
|
5331
|
-
const
|
|
6028
|
+
for (const edge2 of ctx.edges) {
|
|
6029
|
+
if (!isImportEdge(edge2)) continue;
|
|
6030
|
+
if (hasExcludedRole(edge2.srcId) || hasExcludedRole(edge2.dstId)) continue;
|
|
6031
|
+
const srcPkg = packageOf(edge2.srcId);
|
|
6032
|
+
const dstPkg = packageOf(edge2.dstId);
|
|
5332
6033
|
if (srcPkg === null || dstPkg === null) continue;
|
|
5333
6034
|
const srcLayer = packageLayer.get(srcPkg);
|
|
5334
6035
|
const dstLayer = packageLayer.get(dstPkg);
|
|
@@ -5336,9 +6037,9 @@ function runLayeredDepsRule(rule, ctx) {
|
|
|
5336
6037
|
out.push({
|
|
5337
6038
|
ruleId: rule.id,
|
|
5338
6039
|
severity: severityOf(rule),
|
|
5339
|
-
nodeId:
|
|
5340
|
-
destinationId:
|
|
5341
|
-
message: `${srcPkg}
|
|
6040
|
+
nodeId: edge2.srcId,
|
|
6041
|
+
destinationId: edge2.dstId,
|
|
6042
|
+
message: `${edge2.srcId} (${srcPkg}, layer ${srcLayer}) imports ${edge2.dstId} (${dstPkg}, layer ${dstLayer})`
|
|
5342
6043
|
});
|
|
5343
6044
|
}
|
|
5344
6045
|
return out;
|
|
@@ -5346,31 +6047,32 @@ function runLayeredDepsRule(rule, ctx) {
|
|
|
5346
6047
|
function runForbidImportRule(rule, ctx) {
|
|
5347
6048
|
const fromRx = compilePatterns([rule.from]);
|
|
5348
6049
|
const toRx = compilePatterns([rule.to]);
|
|
6050
|
+
const exceptRx = compilePatterns(rule.except);
|
|
5349
6051
|
const out = [];
|
|
5350
|
-
for (const
|
|
5351
|
-
if (!isImportEdge(
|
|
5352
|
-
if (!matchesAny(
|
|
5353
|
-
if (!matchesAny(
|
|
6052
|
+
for (const edge2 of ctx.edges) {
|
|
6053
|
+
if (!isImportEdge(edge2)) continue;
|
|
6054
|
+
if (!matchesAny(edge2.srcId, fromRx)) continue;
|
|
6055
|
+
if (!matchesAny(edge2.dstId, toRx) || matchesAny(edge2.dstId, exceptRx)) continue;
|
|
5354
6056
|
out.push({
|
|
5355
6057
|
ruleId: rule.id,
|
|
5356
6058
|
severity: severityOf(rule),
|
|
5357
|
-
nodeId:
|
|
5358
|
-
destinationId:
|
|
5359
|
-
message: `${
|
|
6059
|
+
nodeId: edge2.srcId,
|
|
6060
|
+
destinationId: edge2.dstId,
|
|
6061
|
+
message: `${edge2.srcId} imports ${edge2.dstId} (forbidden: ${rule.from} \u2192 ${rule.to})`
|
|
5360
6062
|
});
|
|
5361
6063
|
}
|
|
5362
6064
|
return out;
|
|
5363
6065
|
}
|
|
5364
6066
|
function importersByDestination(edges) {
|
|
5365
6067
|
const importersByDst = /* @__PURE__ */ new Map();
|
|
5366
|
-
for (const
|
|
5367
|
-
if (!isImportEdge(
|
|
5368
|
-
let list = importersByDst.get(
|
|
6068
|
+
for (const edge2 of edges) {
|
|
6069
|
+
if (!isImportEdge(edge2)) continue;
|
|
6070
|
+
let list = importersByDst.get(edge2.dstId);
|
|
5369
6071
|
if (!list) {
|
|
5370
6072
|
list = [];
|
|
5371
|
-
importersByDst.set(
|
|
6073
|
+
importersByDst.set(edge2.dstId, list);
|
|
5372
6074
|
}
|
|
5373
|
-
list.push(
|
|
6075
|
+
list.push(edge2.srcId);
|
|
5374
6076
|
}
|
|
5375
6077
|
return importersByDst;
|
|
5376
6078
|
}
|
|
@@ -5605,13 +6307,6 @@ function collectBaselineKeys(store, baselineSnapshotId, snapshotId, rules) {
|
|
|
5605
6307
|
}
|
|
5606
6308
|
return keys;
|
|
5607
6309
|
}
|
|
5608
|
-
function violationKey(v) {
|
|
5609
|
-
return v.destinationId ? `${v.ruleId}|${v.nodeId}|${v.destinationId}` : `${v.ruleId}|${v.nodeId}`;
|
|
5610
|
-
}
|
|
5611
|
-
function rebasedViolationKey(v, resolve4) {
|
|
5612
|
-
const destinationId = v.destinationId ? resolve4(v.destinationId) : v.destinationId;
|
|
5613
|
-
return violationKey({ ...v, nodeId: resolve4(v.nodeId), destinationId });
|
|
5614
|
-
}
|
|
5615
6310
|
function countByOriginAndSeverity(violations) {
|
|
5616
6311
|
const counts = { newErrors: 0, newWarnings: 0, carryoverErrors: 0, carryoverWarnings: 0 };
|
|
5617
6312
|
for (const v of violations) {
|
|
@@ -5829,7 +6524,7 @@ function validateRule(raw, index, warn) {
|
|
|
5829
6524
|
case "forbid-import":
|
|
5830
6525
|
return assertForbidImport(r);
|
|
5831
6526
|
case "layered-deps":
|
|
5832
|
-
return assertLayeredDeps(r);
|
|
6527
|
+
return assertLayeredDeps(r, warn);
|
|
5833
6528
|
case "no-internal-only-barrels":
|
|
5834
6529
|
return assertNoInternalOnlyBarrels(r);
|
|
5835
6530
|
default:
|
|
@@ -5851,11 +6546,11 @@ function assertNoInternalOnlyBarrels(r) {
|
|
|
5851
6546
|
type: "no-internal-only-barrels",
|
|
5852
6547
|
id: r.id,
|
|
5853
6548
|
packageRoots: r.packageRoots,
|
|
5854
|
-
severity: r
|
|
5855
|
-
exclude:
|
|
6549
|
+
severity: parseSeverity(r),
|
|
6550
|
+
exclude: parseExclude(r)
|
|
5856
6551
|
};
|
|
5857
6552
|
}
|
|
5858
|
-
function assertLayeredDeps(r) {
|
|
6553
|
+
function assertLayeredDeps(r, warn) {
|
|
5859
6554
|
if (!Array.isArray(r.layers) || r.layers.length < 2) {
|
|
5860
6555
|
throw new Error(`${r.id}: layers must be an array of 2+ string arrays`);
|
|
5861
6556
|
}
|
|
@@ -5880,7 +6575,8 @@ function assertLayeredDeps(r) {
|
|
|
5880
6575
|
type: "layered-deps",
|
|
5881
6576
|
id: r.id,
|
|
5882
6577
|
layers: r.layers,
|
|
5883
|
-
severity: r
|
|
6578
|
+
severity: parseSeverity(r),
|
|
6579
|
+
excludeRoles: parseRoleArray(r.id, r.excludeRoles, warn)
|
|
5884
6580
|
};
|
|
5885
6581
|
}
|
|
5886
6582
|
function assertMetricProductMax(r, warn) {
|
|
@@ -5899,9 +6595,9 @@ function assertMetricProductMax(r, warn) {
|
|
|
5899
6595
|
id: ruleId,
|
|
5900
6596
|
metrics: r.metrics.map((m) => healMetricName(m, ruleId, warn)),
|
|
5901
6597
|
max: r.max,
|
|
5902
|
-
kind: r
|
|
5903
|
-
severity: r
|
|
5904
|
-
exclude:
|
|
6598
|
+
kind: parseOptionalKind(r),
|
|
6599
|
+
severity: parseSeverity(r),
|
|
6600
|
+
exclude: parseExclude(r),
|
|
5905
6601
|
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
5906
6602
|
};
|
|
5907
6603
|
}
|
|
@@ -5914,9 +6610,9 @@ function assertMetricMax(r, warn) {
|
|
|
5914
6610
|
id: ruleId,
|
|
5915
6611
|
metric: healMetricName(r.metric, ruleId, warn),
|
|
5916
6612
|
max: r.max,
|
|
5917
|
-
kind: r
|
|
5918
|
-
severity: r
|
|
5919
|
-
exclude:
|
|
6613
|
+
kind: parseOptionalKind(r),
|
|
6614
|
+
severity: parseSeverity(r),
|
|
6615
|
+
exclude: parseExclude(r),
|
|
5920
6616
|
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
5921
6617
|
};
|
|
5922
6618
|
}
|
|
@@ -5929,18 +6625,16 @@ function assertMetricMin(r, warn) {
|
|
|
5929
6625
|
id: ruleId,
|
|
5930
6626
|
metric: healMetricName(r.metric, ruleId, warn),
|
|
5931
6627
|
min: r.min,
|
|
5932
|
-
kind: r
|
|
5933
|
-
severity: r
|
|
5934
|
-
exclude:
|
|
6628
|
+
kind: parseOptionalKind(r),
|
|
6629
|
+
severity: parseSeverity(r),
|
|
6630
|
+
exclude: parseExclude(r),
|
|
5935
6631
|
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
5936
6632
|
};
|
|
5937
6633
|
}
|
|
5938
6634
|
var NODE_KINDS = /* @__PURE__ */ new Set(["package", "module", "file", "symbol", "external"]);
|
|
5939
6635
|
function assertMetricOutlier(r, warn) {
|
|
5940
6636
|
if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
|
|
5941
|
-
if (
|
|
5942
|
-
throw new Error(`${r.id}: kind must be one of ${[...NODE_KINDS].join(", ")}`);
|
|
5943
|
-
}
|
|
6637
|
+
if (!isNodeKind(r.kind)) throw kindError(r.id);
|
|
5944
6638
|
if (typeof r.percentile !== "number" || r.percentile < 50 || r.percentile > 100) {
|
|
5945
6639
|
throw new Error(`${r.id}: percentile must be a number from 50 to 100`);
|
|
5946
6640
|
}
|
|
@@ -5963,7 +6657,7 @@ function assertMetricOutlier(r, warn) {
|
|
|
5963
6657
|
minSample: r.minSample,
|
|
5964
6658
|
floor: r.floor,
|
|
5965
6659
|
rankNonZero: r.rankNonZero,
|
|
5966
|
-
severity: r
|
|
6660
|
+
severity: parseSeverity(r)
|
|
5967
6661
|
};
|
|
5968
6662
|
}
|
|
5969
6663
|
function assertForbidImport(r) {
|
|
@@ -5974,15 +6668,43 @@ function assertForbidImport(r) {
|
|
|
5974
6668
|
id: r.id,
|
|
5975
6669
|
from: r.from,
|
|
5976
6670
|
to: r.to,
|
|
5977
|
-
|
|
6671
|
+
except: parseStringList(r, "except"),
|
|
6672
|
+
severity: parseSeverity(r)
|
|
5978
6673
|
};
|
|
5979
6674
|
}
|
|
5980
|
-
function
|
|
5981
|
-
return
|
|
6675
|
+
function isNodeKind(value) {
|
|
6676
|
+
return typeof value === "string" && NODE_KINDS.has(value);
|
|
6677
|
+
}
|
|
6678
|
+
function kindError(ruleId) {
|
|
6679
|
+
return new Error(`${ruleId}: kind must be one of ${[...NODE_KINDS].join(", ")}`);
|
|
6680
|
+
}
|
|
6681
|
+
function parseOptionalKind(r) {
|
|
6682
|
+
if (r.kind === void 0) return void 0;
|
|
6683
|
+
if (!isNodeKind(r.kind)) throw kindError(r.id);
|
|
6684
|
+
return r.kind;
|
|
6685
|
+
}
|
|
6686
|
+
function parseSeverity(r) {
|
|
6687
|
+
if (r.severity === void 0 || r.severity === "error" || r.severity === "warning") {
|
|
6688
|
+
return r.severity;
|
|
6689
|
+
}
|
|
6690
|
+
throw new Error(`${r.id}: severity must be "error" or "warning"`);
|
|
6691
|
+
}
|
|
6692
|
+
function parseExclude(r) {
|
|
6693
|
+
return parseStringList(r, "exclude");
|
|
6694
|
+
}
|
|
6695
|
+
function parseStringList(r, key) {
|
|
6696
|
+
const value = r[key];
|
|
6697
|
+
if (value === void 0) return void 0;
|
|
6698
|
+
if (!Array.isArray(value) || !value.every((e) => typeof e === "string")) {
|
|
6699
|
+
throw new Error(`${r.id}: ${key} must be an array of strings`);
|
|
6700
|
+
}
|
|
6701
|
+
return value;
|
|
5982
6702
|
}
|
|
5983
6703
|
var ROLE_VALUES = /* @__PURE__ */ new Set([
|
|
5984
6704
|
"test",
|
|
5985
6705
|
"fixture",
|
|
6706
|
+
"story",
|
|
6707
|
+
"lab",
|
|
5986
6708
|
"barrel",
|
|
5987
6709
|
"types",
|
|
5988
6710
|
"config",
|
|
@@ -6013,7 +6735,7 @@ function healRole(entry, ruleId, warn) {
|
|
|
6013
6735
|
}
|
|
6014
6736
|
|
|
6015
6737
|
// src/check/run.ts
|
|
6016
|
-
import { readFile
|
|
6738
|
+
import { readFile } from "fs/promises";
|
|
6017
6739
|
function resolveSnapshot(store, spec) {
|
|
6018
6740
|
if (typeof spec === "number" || /^\d+$/.test(spec)) {
|
|
6019
6741
|
const snap2 = store.getSnapshot(Number(spec));
|
|
@@ -6034,18 +6756,18 @@ function checkSnapshot(store, options) {
|
|
|
6034
6756
|
});
|
|
6035
6757
|
return { snapshot, baselineSnapshot, result };
|
|
6036
6758
|
}
|
|
6037
|
-
async function loadCheckRules(
|
|
6759
|
+
async function loadCheckRules(path14, options = {}) {
|
|
6038
6760
|
let raw;
|
|
6039
6761
|
try {
|
|
6040
|
-
raw = await
|
|
6762
|
+
raw = await readFile(path14, "utf8");
|
|
6041
6763
|
} catch (err) {
|
|
6042
|
-
throw new Error(`Cannot read rules file at ${
|
|
6764
|
+
throw new Error(`Cannot read rules file at ${path14}: ${errorMessage(err)}`);
|
|
6043
6765
|
}
|
|
6044
6766
|
let parsed;
|
|
6045
6767
|
try {
|
|
6046
6768
|
parsed = JSON.parse(raw);
|
|
6047
6769
|
} catch (err) {
|
|
6048
|
-
throw new Error(`Invalid JSON in ${
|
|
6770
|
+
throw new Error(`Invalid JSON in ${path14}: ${errorMessage(err)}`);
|
|
6049
6771
|
}
|
|
6050
6772
|
return validateRules(parsed, options);
|
|
6051
6773
|
}
|
|
@@ -6071,13 +6793,13 @@ function indexEdges(edges) {
|
|
|
6071
6793
|
function diffNodes(fromNodes, toNodes, aliases) {
|
|
6072
6794
|
const toIndex = new Map(toNodes.map((n) => [n.id, n]));
|
|
6073
6795
|
const matchedToIds = /* @__PURE__ */ new Set();
|
|
6074
|
-
const
|
|
6796
|
+
const removed2 = [];
|
|
6075
6797
|
const renamed = [];
|
|
6076
6798
|
for (const fromNode of fromNodes) {
|
|
6077
6799
|
const newId = resolveId(fromNode.id, aliases);
|
|
6078
6800
|
const toNode = toIndex.get(newId);
|
|
6079
6801
|
if (!toNode) {
|
|
6080
|
-
|
|
6802
|
+
removed2.push(fromNode);
|
|
6081
6803
|
continue;
|
|
6082
6804
|
}
|
|
6083
6805
|
matchedToIds.add(newId);
|
|
@@ -6086,7 +6808,7 @@ function diffNodes(fromNodes, toNodes, aliases) {
|
|
|
6086
6808
|
}
|
|
6087
6809
|
}
|
|
6088
6810
|
const added = toNodes.filter((toNode) => !matchedToIds.has(toNode.id));
|
|
6089
|
-
return { added, removed, renamed, commonNewIds: matchedToIds };
|
|
6811
|
+
return { added, removed: removed2, renamed, commonNewIds: matchedToIds };
|
|
6090
6812
|
}
|
|
6091
6813
|
function diffEdges(fromEdges, toEdges, aliases) {
|
|
6092
6814
|
const remappedKey = (e) => edgeKey2(resolveId(e.srcId, aliases), resolveId(e.dstId, aliases), e.kind);
|
|
@@ -6096,8 +6818,8 @@ function diffEdges(fromEdges, toEdges, aliases) {
|
|
|
6096
6818
|
for (const [k, e] of toIndex) {
|
|
6097
6819
|
if (!fromKeys.has(k)) added.push(e);
|
|
6098
6820
|
}
|
|
6099
|
-
const
|
|
6100
|
-
return { added, removed };
|
|
6821
|
+
const removed2 = fromEdges.filter((e) => !toIndex.has(remappedKey(e)));
|
|
6822
|
+
return { added, removed: removed2 };
|
|
6101
6823
|
}
|
|
6102
6824
|
function indexFromMetrics(metrics, aliases) {
|
|
6103
6825
|
const fromIndex = /* @__PURE__ */ new Map();
|
|
@@ -6175,44 +6897,16 @@ function diffCheckResults(store, options) {
|
|
|
6175
6897
|
fromSnapshotId: options.fromSnapshotId,
|
|
6176
6898
|
toSnapshotId: options.toSnapshotId,
|
|
6177
6899
|
rulesEvaluated: options.rules.length,
|
|
6178
|
-
...bucketViolations(
|
|
6900
|
+
...bucketViolations(from.violations, to.violations, chain.resolve)
|
|
6179
6901
|
};
|
|
6180
6902
|
}
|
|
6181
|
-
function bucketViolations(fromByKey, toByKey) {
|
|
6182
|
-
const buckets = { newViolations: [], resolvedViolations: [], unchanged: [], worsened: [], improved: [] };
|
|
6183
|
-
for (const [key, toV] of toByKey) {
|
|
6184
|
-
const fromV = fromByKey.get(key);
|
|
6185
|
-
if (!fromV) {
|
|
6186
|
-
buckets.newViolations.push(toV);
|
|
6187
|
-
continue;
|
|
6188
|
-
}
|
|
6189
|
-
const delta = computeDelta(fromV, toV);
|
|
6190
|
-
const entry = { from: fromV, to: toV, delta };
|
|
6191
|
-
buckets.unchanged.push(entry);
|
|
6192
|
-
if (delta !== null && delta > 0) buckets.worsened.push(entry);
|
|
6193
|
-
else if (delta !== null && delta < 0) buckets.improved.push(entry);
|
|
6194
|
-
}
|
|
6195
|
-
for (const [key, fromV] of fromByKey) {
|
|
6196
|
-
if (!toByKey.has(key)) buckets.resolvedViolations.push(fromV);
|
|
6197
|
-
}
|
|
6198
|
-
return buckets;
|
|
6199
|
-
}
|
|
6200
|
-
function indexByKey(violations, resolve4) {
|
|
6201
|
-
const out = /* @__PURE__ */ new Map();
|
|
6202
|
-
for (const v of violations) out.set(resolve4 ? rebasedViolationKey(v, resolve4) : violationKey(v), v);
|
|
6203
|
-
return out;
|
|
6204
|
-
}
|
|
6205
|
-
function computeDelta(from, to) {
|
|
6206
|
-
if (from.value === void 0 || to.value === void 0) return null;
|
|
6207
|
-
return to.value - from.value;
|
|
6208
|
-
}
|
|
6209
6903
|
|
|
6210
6904
|
// src/diff/footprint.ts
|
|
6211
|
-
var
|
|
6905
|
+
var DEFAULT_MIN_CO_IMPORTS = 2;
|
|
6212
6906
|
function computeFootprints(graph, options = {}) {
|
|
6213
6907
|
const edges = referenceEdges(graph.edges, options.ignoreConsumer);
|
|
6214
6908
|
const consumers = consumersBySymbol(edges);
|
|
6215
|
-
const partners = partnersBySymbol(edges, options.minCoImports ??
|
|
6909
|
+
const partners = partnersBySymbol(edges, options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS);
|
|
6216
6910
|
const out = /* @__PURE__ */ new Map();
|
|
6217
6911
|
for (const node of graph.nodes) {
|
|
6218
6912
|
if (node.kind !== "symbol") continue;
|
|
@@ -6264,6 +6958,119 @@ function partnersBySymbol(edges, minCoImports) {
|
|
|
6264
6958
|
return new Map([...partners].map(([id, set]) => [id, [...set].sort()]));
|
|
6265
6959
|
}
|
|
6266
6960
|
|
|
6961
|
+
// src/diff/footprint-diff.ts
|
|
6962
|
+
var PART_REASONS = ["signature", "consumers", "coupling"];
|
|
6963
|
+
function diffFootprints(store, options) {
|
|
6964
|
+
const { fromSnapshotId, toSnapshotId } = options;
|
|
6965
|
+
const resolve5 = symbolAwareResolver(aliasChain(store, fromSnapshotId, toSnapshotId));
|
|
6966
|
+
const fromGraph = loadSymbolLayer(store, fromSnapshotId);
|
|
6967
|
+
const toGraph = loadSymbolLayer(store, toSnapshotId);
|
|
6968
|
+
const { previousIds, mergedAway } = mergeSources(fromGraph.nodes, resolve5);
|
|
6969
|
+
const before = computeFootprints(remapGraph(fromGraph, resolve5, new Set(mergedAway)), options);
|
|
6970
|
+
const after = computeFootprints(toGraph, options);
|
|
6971
|
+
const changes = [
|
|
6972
|
+
...changedOrAdded(after, before, previousIds),
|
|
6973
|
+
...removed(before, after, previousIds, resolve5),
|
|
6974
|
+
...mergedAway.map((id) => ({ symbolId: id, status: "removed", reasons: [], fileId: resolve5(declaringFile(id)) }))
|
|
6975
|
+
].sort((a, b) => compare3(a.symbolId, b.symbolId));
|
|
6976
|
+
const files = [...new Set(changes.map((c) => c.fileId))].sort(compare3);
|
|
6977
|
+
return { fromSnapshotId, toSnapshotId, changes, files };
|
|
6978
|
+
}
|
|
6979
|
+
function loadSymbolLayer(store, snapshotId) {
|
|
6980
|
+
return {
|
|
6981
|
+
nodes: store.listNodes(snapshotId, { includeSymbols: true }).filter((n) => n.kind === "symbol"),
|
|
6982
|
+
edges: store.listEdges(snapshotId, { includeReferences: true })
|
|
6983
|
+
};
|
|
6984
|
+
}
|
|
6985
|
+
function symbolAwareResolver(aliases) {
|
|
6986
|
+
return (id) => {
|
|
6987
|
+
const direct = aliases.resolve(id);
|
|
6988
|
+
if (direct !== id) return direct;
|
|
6989
|
+
const parsed = parseSymbolId(id);
|
|
6990
|
+
return parsed ? symbolId(aliases.resolve(parsed.fileId), parsed.name) : id;
|
|
6991
|
+
};
|
|
6992
|
+
}
|
|
6993
|
+
function remapGraph(graph, resolve5, losers) {
|
|
6994
|
+
return {
|
|
6995
|
+
nodes: graph.nodes.filter((n) => !losers.has(n.id)).map((n) => ({ ...n, id: resolve5(n.id) })),
|
|
6996
|
+
edges: graph.edges.filter((e) => !losers.has(e.srcId) && !losers.has(e.dstId)).map((e) => ({ ...e, srcId: resolve5(e.srcId), dstId: resolve5(e.dstId) }))
|
|
6997
|
+
};
|
|
6998
|
+
}
|
|
6999
|
+
function mergeSources(nodes, resolve5) {
|
|
7000
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7001
|
+
for (const node of nodes) {
|
|
7002
|
+
const id = resolve5(node.id);
|
|
7003
|
+
groups.set(id, [...groups.get(id) ?? [], node.id]);
|
|
7004
|
+
}
|
|
7005
|
+
const previousIds = /* @__PURE__ */ new Map();
|
|
7006
|
+
const mergedAway = [];
|
|
7007
|
+
for (const [id, sources] of groups) {
|
|
7008
|
+
const keeper = sources.includes(id) ? id : [...sources].sort(compare3)[0];
|
|
7009
|
+
if (keeper !== id) previousIds.set(id, keeper);
|
|
7010
|
+
mergedAway.push(...sources.filter((source) => source !== keeper));
|
|
7011
|
+
}
|
|
7012
|
+
return { previousIds, mergedAway };
|
|
7013
|
+
}
|
|
7014
|
+
function changedOrAdded(after, before, previousIds) {
|
|
7015
|
+
const out = [];
|
|
7016
|
+
for (const [id, footprint] of after) {
|
|
7017
|
+
const prior = before.get(id);
|
|
7018
|
+
const previousId = previousIds.get(id);
|
|
7019
|
+
if (!prior) {
|
|
7020
|
+
out.push({ symbolId: id, status: "added", reasons: [], fileId: declaringFile(id) });
|
|
7021
|
+
continue;
|
|
7022
|
+
}
|
|
7023
|
+
const reasons = reasonsFor(prior, footprint, previousId !== void 0);
|
|
7024
|
+
if (reasons.length === 0) continue;
|
|
7025
|
+
out.push({ symbolId: id, ...previousId ? { previousId } : {}, status: "changed", reasons, fileId: declaringFile(id) });
|
|
7026
|
+
}
|
|
7027
|
+
return out;
|
|
7028
|
+
}
|
|
7029
|
+
function reasonsFor(prior, current, renamed) {
|
|
7030
|
+
const reasons = PART_REASONS.filter((part) => prior.parts[part] !== current.parts[part]);
|
|
7031
|
+
if (renamed) reasons.push("renamed");
|
|
7032
|
+
return reasons;
|
|
7033
|
+
}
|
|
7034
|
+
function removed(before, after, previousIds, resolve5) {
|
|
7035
|
+
const out = [];
|
|
7036
|
+
for (const id of before.keys()) {
|
|
7037
|
+
if (after.has(id)) continue;
|
|
7038
|
+
const fromId = previousIds.get(id) ?? id;
|
|
7039
|
+
out.push({ symbolId: fromId, status: "removed", reasons: [], fileId: resolve5(declaringFile(fromId)) });
|
|
7040
|
+
}
|
|
7041
|
+
return out;
|
|
7042
|
+
}
|
|
7043
|
+
function declaringFile(id) {
|
|
7044
|
+
return parseSymbolId(id)?.fileId ?? id;
|
|
7045
|
+
}
|
|
7046
|
+
function compare3(a, b) {
|
|
7047
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
7048
|
+
}
|
|
7049
|
+
|
|
7050
|
+
// src/diff/gate.ts
|
|
7051
|
+
function unitProvenance(input) {
|
|
7052
|
+
return {
|
|
7053
|
+
unitId: input.unit.unitId,
|
|
7054
|
+
commit: input.snapshot.commitHash,
|
|
7055
|
+
symbolSetHash: symbolSetHash(input.unit, input.footprints),
|
|
7056
|
+
model: input.model ?? null
|
|
7057
|
+
};
|
|
7058
|
+
}
|
|
7059
|
+
function gateUnits(input) {
|
|
7060
|
+
const priorHash = new Map(input.prior.map((p) => [p.unitId, p.symbolSetHash]));
|
|
7061
|
+
const current = new Set(input.units.map((u) => u.unitId));
|
|
7062
|
+
const regenerate = [];
|
|
7063
|
+
const skip = [];
|
|
7064
|
+
for (const unit of input.units) {
|
|
7065
|
+
const before = priorHash.get(unit.unitId);
|
|
7066
|
+
if (before === void 0) regenerate.push({ unitId: unit.unitId, reason: "new" });
|
|
7067
|
+
else if (before !== symbolSetHash(unit, input.footprints)) regenerate.push({ unitId: unit.unitId, reason: "changed" });
|
|
7068
|
+
else skip.push(unit.unitId);
|
|
7069
|
+
}
|
|
7070
|
+
const orphaned = [...priorHash.keys()].filter((id) => !current.has(id));
|
|
7071
|
+
return { regenerate, skip, orphaned };
|
|
7072
|
+
}
|
|
7073
|
+
|
|
6267
7074
|
// src/embeddings/corpus.ts
|
|
6268
7075
|
import { contentHashOf } from "@titan-design/store-sqlite";
|
|
6269
7076
|
function buildEmbedText(signature, purpose) {
|
|
@@ -6334,17 +7141,17 @@ function storeVectors(store, model, rows) {
|
|
|
6334
7141
|
async function embedTextsCached(store, embedder, texts) {
|
|
6335
7142
|
const hashes = texts.map((t) => hashEmbedText(t));
|
|
6336
7143
|
const byHash = loadVectorsByHash(store, embedder.model, hashes);
|
|
6337
|
-
const
|
|
7144
|
+
const missing2 = /* @__PURE__ */ new Map();
|
|
6338
7145
|
texts.forEach((text, i) => {
|
|
6339
|
-
if (!byHash.has(hashes[i]))
|
|
7146
|
+
if (!byHash.has(hashes[i])) missing2.set(hashes[i], text);
|
|
6340
7147
|
});
|
|
6341
|
-
const pending = [...
|
|
7148
|
+
const pending = [...missing2.entries()];
|
|
6342
7149
|
for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
|
|
6343
7150
|
const rows = await embedBatch(embedder, pending.slice(i, i + EMBED_BATCH_SIZE));
|
|
6344
7151
|
storeVectors(store, embedder.model, rows);
|
|
6345
7152
|
for (const r of rows) byHash.set(r.textHash, r.vector);
|
|
6346
7153
|
}
|
|
6347
|
-
return { byHash, newHashes: new Set(
|
|
7154
|
+
return { byHash, newHashes: new Set(missing2.keys()) };
|
|
6348
7155
|
}
|
|
6349
7156
|
async function embedBatch(embedder, batch) {
|
|
6350
7157
|
const vectors = await embedder.embed(batch.map(([, text]) => text), { role: "document" });
|
|
@@ -6514,7 +7321,7 @@ function pushMulti(map, key, value) {
|
|
|
6514
7321
|
}
|
|
6515
7322
|
|
|
6516
7323
|
// src/conventions/areas.ts
|
|
6517
|
-
import * as
|
|
7324
|
+
import * as path13 from "path";
|
|
6518
7325
|
import { contentHashOf as contentHashOf2 } from "@titan-design/store-sqlite";
|
|
6519
7326
|
var DEFAULT_MIN_SIZE = 3;
|
|
6520
7327
|
var PROMPT_FILE_CAP = 30;
|
|
@@ -6598,7 +7405,7 @@ function rankByDegree(members, edges) {
|
|
|
6598
7405
|
function dominantDirectory(files) {
|
|
6599
7406
|
const counts = /* @__PURE__ */ new Map();
|
|
6600
7407
|
for (const f of files) {
|
|
6601
|
-
const dir =
|
|
7408
|
+
const dir = path13.posix.dirname(f);
|
|
6602
7409
|
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
6603
7410
|
}
|
|
6604
7411
|
const dirs = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
@@ -6718,14 +7525,21 @@ function toMatch(area, score) {
|
|
|
6718
7525
|
export {
|
|
6719
7526
|
ALIAS_BASE_ATTR,
|
|
6720
7527
|
ALL_ROLES,
|
|
7528
|
+
BUNDLE_SCHEMA_VERSION,
|
|
6721
7529
|
COMMUNITY_SUMMARY_NAMESPACE,
|
|
7530
|
+
SCHEMA_VERSION2 as CONTEXT_SCHEMA_VERSION,
|
|
6722
7531
|
COVERAGE_METRIC_NAME,
|
|
6723
7532
|
CodeGraphStore,
|
|
6724
7533
|
DEAD_CODE_METRIC_NAMES,
|
|
6725
7534
|
DEFAULT_CHURN_WINDOWS,
|
|
7535
|
+
DEFAULT_HEALTH_WEIGHTS,
|
|
7536
|
+
DEFAULT_MAX_LINE_CHARS,
|
|
7537
|
+
DEFAULT_MAX_PACKAGE_SIZE,
|
|
6726
7538
|
DEFAULT_OUTLIER_MIN_SAMPLE,
|
|
6727
7539
|
DOMAIN_DDL,
|
|
7540
|
+
ELISION_MARKER,
|
|
6728
7541
|
EXCEPTION_METRIC_NAMES,
|
|
7542
|
+
EXTERNAL_BUCKET,
|
|
6729
7543
|
FINDING_DDL,
|
|
6730
7544
|
GROWTH_RISK_METRIC_NAMES,
|
|
6731
7545
|
INDEX_VERSION,
|
|
@@ -6740,35 +7554,55 @@ export {
|
|
|
6740
7554
|
SYMBOL_EMBEDDING_NAMESPACE,
|
|
6741
7555
|
SYMBOL_ID_SEP,
|
|
6742
7556
|
SYMBOL_METRIC_NAMES,
|
|
7557
|
+
TRUNCATION_SUFFIX,
|
|
6743
7558
|
TsMorphGraphExtractor,
|
|
7559
|
+
UNIMPORTED_ROLES,
|
|
7560
|
+
aggregateEdges,
|
|
6744
7561
|
aggregateMetrics,
|
|
6745
7562
|
aliasChain,
|
|
6746
7563
|
annotateRoles,
|
|
6747
7564
|
attributeCoverage,
|
|
7565
|
+
bucketFilesByPackage,
|
|
7566
|
+
bucketViolations,
|
|
6748
7567
|
buildAliases,
|
|
7568
|
+
buildBlastRadius,
|
|
7569
|
+
buildCentralFiles,
|
|
7570
|
+
buildContextBundle,
|
|
7571
|
+
buildContextDossier,
|
|
6749
7572
|
buildConventionAreas,
|
|
6750
7573
|
buildEmbedText,
|
|
6751
7574
|
buildFileModuleNodes,
|
|
7575
|
+
buildHotExports,
|
|
6752
7576
|
buildIndexerMetrics,
|
|
6753
7577
|
buildLineage,
|
|
7578
|
+
buildNodeMetrics,
|
|
7579
|
+
buildReportContext,
|
|
7580
|
+
buildSymbolCouplingPayload,
|
|
7581
|
+
busFactorOf,
|
|
6754
7582
|
canonicalEdgeKind,
|
|
6755
7583
|
canonicalMetricName,
|
|
6756
7584
|
canonicalRole,
|
|
6757
7585
|
carryForwardVerdicts,
|
|
6758
7586
|
checkSnapshot,
|
|
7587
|
+
classifyCoupling,
|
|
6759
7588
|
classifyRole,
|
|
6760
7589
|
collectDeclaredNames,
|
|
6761
7590
|
collectDeclaredSpans,
|
|
7591
|
+
collectNodeMetrics,
|
|
7592
|
+
collectSymbolUtil,
|
|
6762
7593
|
compilePatterns,
|
|
7594
|
+
computeArch,
|
|
6763
7595
|
computeDeadCodeMetrics,
|
|
6764
7596
|
computeDeepAst,
|
|
6765
7597
|
computeFootprints,
|
|
6766
7598
|
computeGrowthRiskMetrics,
|
|
7599
|
+
computeHealth,
|
|
6767
7600
|
computeMetrics,
|
|
6768
7601
|
computePageRank,
|
|
6769
7602
|
computePartitionQuality,
|
|
6770
7603
|
computeRecencyWindows,
|
|
6771
7604
|
computeRelevance,
|
|
7605
|
+
computeReportDrift,
|
|
6772
7606
|
computeRoleHints,
|
|
6773
7607
|
computeSourceMetrics,
|
|
6774
7608
|
computeSymbolConsumers,
|
|
@@ -6783,6 +7617,7 @@ export {
|
|
|
6783
7617
|
detectGitToplevel,
|
|
6784
7618
|
detectRenames,
|
|
6785
7619
|
diffCheckResults,
|
|
7620
|
+
diffFootprints,
|
|
6786
7621
|
diffSnapshots,
|
|
6787
7622
|
edgeWeight,
|
|
6788
7623
|
embedSnapshot,
|
|
@@ -6790,22 +7625,29 @@ export {
|
|
|
6790
7625
|
externalId,
|
|
6791
7626
|
externalToFinding,
|
|
6792
7627
|
fileId,
|
|
7628
|
+
filteredFileIds,
|
|
6793
7629
|
findConventions,
|
|
6794
7630
|
findSimilarCapability,
|
|
6795
7631
|
findingKey,
|
|
7632
|
+
gateUnits,
|
|
6796
7633
|
getConventionMap,
|
|
6797
7634
|
getEdgeWeight,
|
|
6798
7635
|
getLanguageFromPath3 as getLanguageFromPath,
|
|
6799
7636
|
getSupportedLanguages,
|
|
7637
|
+
gitTreeSource,
|
|
6800
7638
|
groupTestsBySource,
|
|
6801
7639
|
hashContent,
|
|
6802
7640
|
hashEmbedText,
|
|
6803
7641
|
hashText,
|
|
7642
|
+
hotspotScoreOf,
|
|
7643
|
+
importMarker,
|
|
6804
7644
|
indexPaths,
|
|
6805
7645
|
invertBuckets,
|
|
6806
7646
|
isGeneratedByHeuristic,
|
|
6807
7647
|
isGeneratedFile,
|
|
6808
7648
|
isInsideGitRepo,
|
|
7649
|
+
isMinifyLanguage,
|
|
7650
|
+
keepNode,
|
|
6809
7651
|
keyFindings,
|
|
6810
7652
|
lineagePath,
|
|
6811
7653
|
linkTestsToSources,
|
|
@@ -6821,21 +7663,32 @@ export {
|
|
|
6821
7663
|
loadGeneratedPatterns,
|
|
6822
7664
|
loadHistoryMetrics,
|
|
6823
7665
|
loadLineage,
|
|
7666
|
+
loadRoleGlobs,
|
|
7667
|
+
lookupMetric,
|
|
6824
7668
|
matchesAny,
|
|
7669
|
+
minifySource,
|
|
6825
7670
|
moduleId,
|
|
6826
7671
|
normalizeFlaggedText,
|
|
6827
7672
|
openCodeGraph,
|
|
7673
|
+
originalLine,
|
|
6828
7674
|
packageId,
|
|
7675
|
+
packagesReferencedByEdges,
|
|
7676
|
+
pairKey,
|
|
6829
7677
|
parentModuleId,
|
|
6830
|
-
|
|
7678
|
+
parseFile3 as parseFile,
|
|
6831
7679
|
parseSymbolId,
|
|
6832
7680
|
patternToRegex,
|
|
6833
7681
|
percentileOf,
|
|
6834
7682
|
planPrune,
|
|
6835
7683
|
priorSnapshotForRef,
|
|
6836
7684
|
pruneDanglingReferences,
|
|
7685
|
+
publicApiFiles,
|
|
6837
7686
|
readSourceFiles,
|
|
6838
7687
|
rebasedViolationKey,
|
|
7688
|
+
referencedNodes,
|
|
7689
|
+
renderBundleText,
|
|
7690
|
+
renderContextMarkdown,
|
|
7691
|
+
renderSignatureTree,
|
|
6839
7692
|
resolveAlias,
|
|
6840
7693
|
resolveBarrelEdges,
|
|
6841
7694
|
resolveChurnWindows,
|
|
@@ -6845,7 +7698,7 @@ export {
|
|
|
6845
7698
|
runPrune,
|
|
6846
7699
|
saveFindings,
|
|
6847
7700
|
saveVerdicts,
|
|
6848
|
-
|
|
7701
|
+
shouldIncludeFile3 as shouldIncludeFile,
|
|
6849
7702
|
snapshotPageRank,
|
|
6850
7703
|
snapshotReferenceEdges,
|
|
6851
7704
|
snapshotRelevance,
|
|
@@ -6858,10 +7711,21 @@ export {
|
|
|
6858
7711
|
symbolSetHash,
|
|
6859
7712
|
testCoverageCountMetrics,
|
|
6860
7713
|
toFindings,
|
|
7714
|
+
toSortedEdges,
|
|
7715
|
+
topBusFactorRisks,
|
|
7716
|
+
topCentralFiles,
|
|
7717
|
+
topDeadModules,
|
|
7718
|
+
topGrowthRisks,
|
|
7719
|
+
topHotspots,
|
|
7720
|
+
topTestCoverageRisks,
|
|
7721
|
+
topUntestedRisks,
|
|
7722
|
+
topUnusedExports,
|
|
6861
7723
|
tryEmbedSnapshot,
|
|
7724
|
+
unitProvenance,
|
|
6862
7725
|
validateRules,
|
|
6863
7726
|
violationKey,
|
|
6864
7727
|
walkSourceFiles,
|
|
6865
|
-
windowSuffix
|
|
7728
|
+
windowSuffix,
|
|
7729
|
+
workingTreeSource
|
|
6866
7730
|
};
|
|
6867
7731
|
//# sourceMappingURL=index.js.map
|