@titan-design/code-graph 0.11.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 +129 -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 +331 -353
- package/dist/index.js +1658 -926
- 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;
|
|
@@ -1230,12 +1392,15 @@ var TsMorphGraphExtractor = class {
|
|
|
1230
1392
|
tsConfigPath;
|
|
1231
1393
|
project;
|
|
1232
1394
|
ownsProject;
|
|
1395
|
+
source;
|
|
1233
1396
|
retired = [];
|
|
1397
|
+
fileExists = (abs) => this.source.fileExists(abs);
|
|
1234
1398
|
constructor(options) {
|
|
1235
1399
|
this.repoRoot = options.repoRoot;
|
|
1236
1400
|
this.tsConfigPath = options.tsConfigPath;
|
|
1237
1401
|
this.project = options.project;
|
|
1238
1402
|
this.ownsProject = options.project === void 0;
|
|
1403
|
+
this.source = options.source ?? workingTreeSource();
|
|
1239
1404
|
}
|
|
1240
1405
|
extract(file) {
|
|
1241
1406
|
if (!isTypeScriptFile(file)) return [];
|
|
@@ -1259,13 +1424,16 @@ var TsMorphGraphExtractor = class {
|
|
|
1259
1424
|
const calls = collectTsCallEdges(sourceFile, {
|
|
1260
1425
|
repoRoot: this.repoRoot,
|
|
1261
1426
|
srcFileId: fileId(this.repoRoot, sourceFile.getFilePath()),
|
|
1262
|
-
localSymbols: new Set(symbolNodes2.map((n) => n.name))
|
|
1427
|
+
localSymbols: new Set(symbolNodes2.map((n) => n.name)),
|
|
1428
|
+
fileExists: this.fileExists
|
|
1263
1429
|
});
|
|
1264
1430
|
return [{ nodes: [...nodes, ...symbolNodes2, ...externalNodes], edges: [...edges, ...calls] }];
|
|
1265
1431
|
}
|
|
1266
1432
|
ensureProject() {
|
|
1267
1433
|
if (this.project) return this.project;
|
|
1268
|
-
|
|
1434
|
+
const fileSystem = this.source.fileSystem;
|
|
1435
|
+
this.project = this.tsConfigPath ? new Project2({ tsConfigFilePath: this.tsConfigPath, fileSystem }) : new Project2({
|
|
1436
|
+
fileSystem,
|
|
1269
1437
|
compilerOptions: {
|
|
1270
1438
|
allowJs: true,
|
|
1271
1439
|
target: ScriptTarget.ESNext,
|
|
@@ -1366,7 +1534,7 @@ var TsMorphGraphExtractor = class {
|
|
|
1366
1534
|
if (targetSf) {
|
|
1367
1535
|
const decls = targetSf.getExportedDeclarations().get(importedName);
|
|
1368
1536
|
const originSf = decls && decls.length > 0 ? decls[0].getSourceFile() : targetSf;
|
|
1369
|
-
const originId = this.inRepoFileId(remapDistToSrc(originSf.getFilePath()));
|
|
1537
|
+
const originId = this.inRepoFileId(remapDistToSrc(originSf.getFilePath(), this.fileExists));
|
|
1370
1538
|
return originId ? symbolId(originId, importedName) : null;
|
|
1371
1539
|
}
|
|
1372
1540
|
return resolveReExportOrigin(
|
|
@@ -1405,7 +1573,7 @@ var TsMorphGraphExtractor = class {
|
|
|
1405
1573
|
}
|
|
1406
1574
|
resolveInternal(target) {
|
|
1407
1575
|
if (!target) return null;
|
|
1408
|
-
return this.inRepoFileId(remapDistToSrc(target.getFilePath()));
|
|
1576
|
+
return this.inRepoFileId(remapDistToSrc(target.getFilePath(), this.fileExists));
|
|
1409
1577
|
}
|
|
1410
1578
|
/**
|
|
1411
1579
|
* Resolve a relative specifier ts-morph failed to link by walking the
|
|
@@ -1433,14 +1601,13 @@ function isTypeScriptFile(file) {
|
|
|
1433
1601
|
}
|
|
1434
1602
|
|
|
1435
1603
|
// src/extractors/python-extractor.ts
|
|
1436
|
-
import * as
|
|
1437
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1604
|
+
import * as path5 from "path";
|
|
1438
1605
|
|
|
1439
1606
|
// src/extractors/python-calls.ts
|
|
1440
|
-
function collectPythonCallEdges(file, fileId2,
|
|
1607
|
+
function collectPythonCallEdges(file, fileId2, resolve5) {
|
|
1441
1608
|
const declarations = /* @__PURE__ */ new Map();
|
|
1442
1609
|
forEachDeclaration(file, (_name, qualifiedName, node) => declarations.set(qualifiedName, node));
|
|
1443
|
-
const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode,
|
|
1610
|
+
const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode, resolve5) };
|
|
1444
1611
|
const agg = /* @__PURE__ */ new Map();
|
|
1445
1612
|
walkScopes(file.tree.rootNode, true, (node, scope) => {
|
|
1446
1613
|
if (node.type !== "call") return;
|
|
@@ -1504,11 +1671,11 @@ function parameterNames(def) {
|
|
|
1504
1671
|
}
|
|
1505
1672
|
return out;
|
|
1506
1673
|
}
|
|
1507
|
-
function fromImports(root,
|
|
1674
|
+
function fromImports(root, resolve5) {
|
|
1508
1675
|
const out = /* @__PURE__ */ new Map();
|
|
1509
1676
|
for (const stmt of root.descendantsOfType("import_from_statement")) {
|
|
1510
1677
|
const moduleName = stmt?.childForFieldName("module_name")?.text;
|
|
1511
|
-
const moduleFile = moduleName ?
|
|
1678
|
+
const moduleFile = moduleName ? resolve5(moduleName) : null;
|
|
1512
1679
|
if (!stmt || !moduleFile) continue;
|
|
1513
1680
|
for (const imported of stmt.childrenForFieldName("name")) {
|
|
1514
1681
|
const alias = imported?.type === "aliased_import" ? imported.childForFieldName("alias")?.text : void 0;
|
|
@@ -1550,13 +1717,15 @@ function addKeyword(site, arg) {
|
|
|
1550
1717
|
// src/extractors/python-extractor.ts
|
|
1551
1718
|
function* candidates(base) {
|
|
1552
1719
|
yield `${base}.py`;
|
|
1553
|
-
yield
|
|
1720
|
+
yield path5.join(base, "__init__.py");
|
|
1554
1721
|
}
|
|
1555
1722
|
var PythonGraphExtractor = class {
|
|
1556
|
-
constructor(repoRoot) {
|
|
1723
|
+
constructor(repoRoot, source = workingTreeSource()) {
|
|
1557
1724
|
this.repoRoot = repoRoot;
|
|
1725
|
+
this.source = source;
|
|
1558
1726
|
}
|
|
1559
1727
|
repoRoot;
|
|
1728
|
+
source;
|
|
1560
1729
|
name = "python-graph";
|
|
1561
1730
|
extract(file) {
|
|
1562
1731
|
if (file.language !== "python") return [];
|
|
@@ -1588,12 +1757,12 @@ var PythonGraphExtractor = class {
|
|
|
1588
1757
|
resolveDotted(fromAbs, specifier) {
|
|
1589
1758
|
const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
|
|
1590
1759
|
const parts = specifier.slice(leadingDots).split(".").filter(Boolean);
|
|
1591
|
-
const relativeBase =
|
|
1592
|
-
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];
|
|
1593
1762
|
for (const root of roots) {
|
|
1594
|
-
for (const candidate of candidates(
|
|
1763
|
+
for (const candidate of candidates(path5.join(root, ...parts))) {
|
|
1595
1764
|
const id = inRepoFileId(this.repoRoot, candidate);
|
|
1596
|
-
if (id !== null &&
|
|
1765
|
+
if (id !== null && this.source.fileExists(candidate)) return id;
|
|
1597
1766
|
}
|
|
1598
1767
|
}
|
|
1599
1768
|
return null;
|
|
@@ -1637,9 +1806,10 @@ var LanguageExtractor = class {
|
|
|
1637
1806
|
name = "language-graph";
|
|
1638
1807
|
delegates;
|
|
1639
1808
|
constructor(options) {
|
|
1809
|
+
const { repoRoot, tsConfigPath, source } = options;
|
|
1640
1810
|
this.delegates = [
|
|
1641
|
-
new TsMorphGraphExtractor({ repoRoot
|
|
1642
|
-
new PythonGraphExtractor(
|
|
1811
|
+
new TsMorphGraphExtractor({ repoRoot, tsConfigPath, source }),
|
|
1812
|
+
new PythonGraphExtractor(repoRoot, source)
|
|
1643
1813
|
];
|
|
1644
1814
|
}
|
|
1645
1815
|
extract(file) {
|
|
@@ -1648,7 +1818,7 @@ var LanguageExtractor = class {
|
|
|
1648
1818
|
};
|
|
1649
1819
|
|
|
1650
1820
|
// src/git-renames.ts
|
|
1651
|
-
import * as
|
|
1821
|
+
import * as path6 from "path";
|
|
1652
1822
|
function detectGitHead(repoRoot) {
|
|
1653
1823
|
return runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
1654
1824
|
}
|
|
@@ -1692,12 +1862,12 @@ function parseRenameOutput(text) {
|
|
|
1692
1862
|
return out;
|
|
1693
1863
|
}
|
|
1694
1864
|
function classifyRename(oldPath, newPath) {
|
|
1695
|
-
return
|
|
1865
|
+
return path6.posix.dirname(oldPath) === path6.posix.dirname(newPath) ? "rename" : "move";
|
|
1696
1866
|
}
|
|
1697
1867
|
function renameToAliases(repoRoot, pair) {
|
|
1698
1868
|
const reason = classifyRename(pair.oldPath, pair.newPath);
|
|
1699
|
-
const oldAbs =
|
|
1700
|
-
const newAbs =
|
|
1869
|
+
const oldAbs = path6.join(repoRoot, pair.oldPath);
|
|
1870
|
+
const newAbs = path6.join(repoRoot, pair.newPath);
|
|
1701
1871
|
return [
|
|
1702
1872
|
{ oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
|
|
1703
1873
|
{ oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
|
|
@@ -1790,8 +1960,8 @@ function walk(requestedId, maps) {
|
|
|
1790
1960
|
}
|
|
1791
1961
|
function chainSteps(input) {
|
|
1792
1962
|
if (input.from === null) return { steps: rootPath(input.lineage, input.to, input.maxHops), connected: true };
|
|
1793
|
-
const
|
|
1794
|
-
if (
|
|
1963
|
+
const path14 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
|
|
1964
|
+
if (path14) return { steps: path14, connected: true };
|
|
1795
1965
|
return { steps: [{ snapshotId: input.to, direction: "forward" }], connected: false };
|
|
1796
1966
|
}
|
|
1797
1967
|
function createAliasChain(input) {
|
|
@@ -1876,9 +2046,11 @@ function computeAliasBridge(store, input) {
|
|
|
1876
2046
|
return { baseSnapshotId: base.id, aliases: [...fileAliases, ...symbols] };
|
|
1877
2047
|
}
|
|
1878
2048
|
|
|
2049
|
+
// src/roles.ts
|
|
2050
|
+
import * as path8 from "path";
|
|
2051
|
+
|
|
1879
2052
|
// src/generated.ts
|
|
1880
|
-
import
|
|
1881
|
-
import * as path6 from "path";
|
|
2053
|
+
import * as path7 from "path";
|
|
1882
2054
|
var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
|
|
1883
2055
|
var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
|
|
1884
2056
|
function isGeneratedByHeuristic(id) {
|
|
@@ -1888,59 +2060,112 @@ function isGeneratedFile(id, patterns) {
|
|
|
1888
2060
|
for (const rx of patterns) if (rx.test(id)) return true;
|
|
1889
2061
|
return isGeneratedByHeuristic(id);
|
|
1890
2062
|
}
|
|
1891
|
-
function loadGeneratedPatterns(rootDir) {
|
|
2063
|
+
function loadGeneratedPatterns(rootDir, source = workingTreeSource()) {
|
|
1892
2064
|
try {
|
|
1893
|
-
const content =
|
|
2065
|
+
const content = source.readFile(path7.join(rootDir, ".gitattributes"));
|
|
1894
2066
|
return parseGeneratedPatterns(content);
|
|
1895
2067
|
} catch {
|
|
1896
2068
|
return [];
|
|
1897
2069
|
}
|
|
1898
2070
|
}
|
|
1899
2071
|
function parseGeneratedPatterns(content) {
|
|
2072
|
+
const rules = parseGeneratedRules(content);
|
|
1900
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 = [];
|
|
1901
2083
|
for (const raw of content.split("\n")) {
|
|
1902
2084
|
const line = raw.trim();
|
|
1903
2085
|
if (!line || line.startsWith("#")) continue;
|
|
1904
2086
|
const parsed = /^("(?:[^"\\]|\\.)*"|\S+)\s*(.*)$/.exec(line);
|
|
1905
2087
|
if (!parsed) continue;
|
|
1906
|
-
const
|
|
1907
|
-
if (
|
|
1908
|
-
|
|
1909
|
-
}
|
|
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 });
|
|
1910
2091
|
}
|
|
1911
|
-
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})`);
|
|
1912
2098
|
}
|
|
1913
2099
|
function generatedAttr(attrs) {
|
|
1914
2100
|
let value;
|
|
1915
2101
|
for (const a of attrs) {
|
|
1916
2102
|
if (a === "linguist-generated" || a === "linguist-generated=true") value = true;
|
|
1917
|
-
else if (a
|
|
2103
|
+
else if (OPT_OUT_TOKENS.has(a)) value = false;
|
|
1918
2104
|
}
|
|
1919
2105
|
return value;
|
|
1920
2106
|
}
|
|
2107
|
+
var OPT_OUT_TOKENS = /* @__PURE__ */ new Set([
|
|
2108
|
+
"-linguist-generated",
|
|
2109
|
+
"linguist-generated=false",
|
|
2110
|
+
"!linguist-generated"
|
|
2111
|
+
]);
|
|
1921
2112
|
function unquote(token) {
|
|
1922
2113
|
return token.startsWith('"') && token.endsWith('"') ? token.slice(1, -1).replace(/\\(.)/g, "$1") : token;
|
|
1923
2114
|
}
|
|
2115
|
+
var NEVER_MATCHES = /(?!)/;
|
|
1924
2116
|
function gitattributesPatternToRegex(pattern) {
|
|
1925
2117
|
let p = pattern;
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
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;
|
|
1940
2164
|
}
|
|
1941
2165
|
|
|
1942
2166
|
// src/roles.ts
|
|
1943
2167
|
var TEST_RE = /(?:^|\/)(?:__tests__\/|tests?\/)|\.(?:test|spec)(?:\.[a-z]+)?$/;
|
|
2168
|
+
var STORY_RE = /\.stories(?:\.[cm]?[jt]sx?)?$|\.mdx$/;
|
|
1944
2169
|
var FIXTURE_RE = /(?:^|\/)fixtures(?:\/|$)/;
|
|
1945
2170
|
var SCRIPT_RE = /(?:^|\/)(?:scripts|archive)(?:\/|$)/;
|
|
1946
2171
|
var BARREL_RE = /(?:^|\/)index(?:\.[a-z]+)?$/;
|
|
@@ -1949,6 +2174,8 @@ var CONFIG_RE = /\.config(?:\.[a-z]+)?$/;
|
|
|
1949
2174
|
var ALL_ROLES = [
|
|
1950
2175
|
"test",
|
|
1951
2176
|
"fixture",
|
|
2177
|
+
"story",
|
|
2178
|
+
"lab",
|
|
1952
2179
|
"barrel",
|
|
1953
2180
|
"types",
|
|
1954
2181
|
"config",
|
|
@@ -1959,7 +2186,9 @@ var ALL_ROLES = [
|
|
|
1959
2186
|
];
|
|
1960
2187
|
function classifyRole(id, hints) {
|
|
1961
2188
|
if (hints?.isGenerated) return "generated";
|
|
2189
|
+
if (hints?.configuredRole) return hints.configuredRole;
|
|
1962
2190
|
if (TEST_RE.test(id)) return "test";
|
|
2191
|
+
if (STORY_RE.test(id)) return "story";
|
|
1963
2192
|
if (FIXTURE_RE.test(id)) return "fixture";
|
|
1964
2193
|
if (SCRIPT_RE.test(id)) return "script";
|
|
1965
2194
|
if (hints?.hasShebang) return "entry";
|
|
@@ -1968,8 +2197,14 @@ function classifyRole(id, hints) {
|
|
|
1968
2197
|
if (CONFIG_RE.test(id)) return "config";
|
|
1969
2198
|
return "source";
|
|
1970
2199
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
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);
|
|
1973
2208
|
const shebangIds = /* @__PURE__ */ new Set();
|
|
1974
2209
|
const generatedIds = /* @__PURE__ */ new Set();
|
|
1975
2210
|
for (const rf of readFiles) {
|
|
@@ -1977,47 +2212,320 @@ function computeRoleHints(readFiles, idRoot, toId) {
|
|
|
1977
2212
|
if (rf.content.startsWith("#!")) shebangIds.add(id);
|
|
1978
2213
|
if (isGeneratedFile(id, patterns)) generatedIds.add(id);
|
|
1979
2214
|
}
|
|
1980
|
-
return { shebangIds, generatedIds };
|
|
2215
|
+
return { shebangIds, generatedIds, roleGlobs: loadRoleGlobs(idRoot, source) };
|
|
1981
2216
|
}
|
|
1982
2217
|
function annotateRoles(nodes, options) {
|
|
2218
|
+
const configuredRole = roleGlobMatcher(options?.roleGlobs, ALL_ROLES);
|
|
1983
2219
|
return nodes.map(
|
|
1984
2220
|
(n) => n.kind === "file" || n.kind === "module" ? {
|
|
1985
2221
|
...n,
|
|
1986
2222
|
role: n.role ?? classifyRole(n.id, {
|
|
1987
2223
|
hasShebang: options?.shebangIds?.has(n.id) ?? false,
|
|
1988
|
-
isGenerated: options?.generatedIds?.has(n.id) ?? false
|
|
2224
|
+
isGenerated: options?.generatedIds?.has(n.id) ?? false,
|
|
2225
|
+
configuredRole: configuredRole(n.id)
|
|
1989
2226
|
})
|
|
1990
2227
|
} : n
|
|
1991
2228
|
);
|
|
1992
2229
|
}
|
|
1993
2230
|
|
|
1994
|
-
// src/
|
|
1995
|
-
import
|
|
1996
|
-
import * as
|
|
1997
|
-
import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
|
|
1998
|
-
|
|
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) {
|
|
1999
2436
|
const seen = /* @__PURE__ */ new Set();
|
|
2000
|
-
const out = [];
|
|
2001
2437
|
for (const rootDir of rootDirs) {
|
|
2002
|
-
|
|
2438
|
+
for (const abs of ingestedUnder(tree, rootDir, languages)) seen.add(abs);
|
|
2003
2439
|
}
|
|
2004
|
-
return
|
|
2440
|
+
return [...seen].sort();
|
|
2005
2441
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
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));
|
|
2020
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 };
|
|
2021
2529
|
}
|
|
2022
2530
|
|
|
2023
2531
|
// src/barrel-resolve.ts
|
|
@@ -2026,9 +2534,9 @@ function edgeWeight(e) {
|
|
|
2026
2534
|
return typeof w === "number" && w > 0 ? w : 1;
|
|
2027
2535
|
}
|
|
2028
2536
|
function pruneDanglingReferences(nodes, edges) {
|
|
2029
|
-
for (const [key,
|
|
2030
|
-
const symbolLayer =
|
|
2031
|
-
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);
|
|
2032
2540
|
}
|
|
2033
2541
|
}
|
|
2034
2542
|
function resolveBarrelEdges(nodes, edges) {
|
|
@@ -2043,7 +2551,7 @@ function resolveBarrelEdges(nodes, edges) {
|
|
|
2043
2551
|
forwards.set(e.srcId, arr);
|
|
2044
2552
|
}
|
|
2045
2553
|
const cache = /* @__PURE__ */ new Map();
|
|
2046
|
-
function
|
|
2554
|
+
function resolve5(id, seen) {
|
|
2047
2555
|
if (!barrels.has(id)) return /* @__PURE__ */ new Map([[id, 1]]);
|
|
2048
2556
|
const cached = cache.get(id);
|
|
2049
2557
|
if (cached) return cached;
|
|
@@ -2054,7 +2562,7 @@ function resolveBarrelEdges(nodes, edges) {
|
|
|
2054
2562
|
const nextSeen = new Set(seen).add(id);
|
|
2055
2563
|
for (const t of targets) {
|
|
2056
2564
|
const share = t.weight / total;
|
|
2057
|
-
for (const [real, frac] of
|
|
2565
|
+
for (const [real, frac] of resolve5(t.dst, nextSeen)) {
|
|
2058
2566
|
out.set(real, (out.get(real) ?? 0) + share * frac);
|
|
2059
2567
|
}
|
|
2060
2568
|
}
|
|
@@ -2069,7 +2577,7 @@ function resolveBarrelEdges(nodes, edges) {
|
|
|
2069
2577
|
continue;
|
|
2070
2578
|
}
|
|
2071
2579
|
const w = edgeWeight(e);
|
|
2072
|
-
for (const [real, frac] of
|
|
2580
|
+
for (const [real, frac] of resolve5(e.dstId, /* @__PURE__ */ new Set())) {
|
|
2073
2581
|
if (real === e.srcId) continue;
|
|
2074
2582
|
resolved.push({
|
|
2075
2583
|
...e,
|
|
@@ -2083,7 +2591,6 @@ function resolveBarrelEdges(nodes, edges) {
|
|
|
2083
2591
|
|
|
2084
2592
|
// src/incremental.ts
|
|
2085
2593
|
import { createHash } from "crypto";
|
|
2086
|
-
import * as fs2 from "fs/promises";
|
|
2087
2594
|
import { getLanguageFromPath as getLanguageFromPath2 } from "@titan-design/code-parser";
|
|
2088
2595
|
|
|
2089
2596
|
// src/analysis/comment-lines.ts
|
|
@@ -2314,6 +2821,25 @@ function isLoggingCall(node) {
|
|
|
2314
2821
|
return false;
|
|
2315
2822
|
}
|
|
2316
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
|
+
|
|
2317
2843
|
// src/cognitive-complexity.ts
|
|
2318
2844
|
function cognitiveComplexityOf(body, language) {
|
|
2319
2845
|
const handler = language === "python" ? PY_HANDLER : TS_HANDLER;
|
|
@@ -2347,9 +2873,8 @@ var TS_NESTING = /* @__PURE__ */ new Set([
|
|
|
2347
2873
|
"do_statement",
|
|
2348
2874
|
"switch_statement",
|
|
2349
2875
|
"catch_clause",
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
"arrow_function"
|
|
2876
|
+
...TS_FUNCTION_DECL_TYPES,
|
|
2877
|
+
...TS_BOUND_FUNCTION_TYPES
|
|
2353
2878
|
]);
|
|
2354
2879
|
var PY_NESTING = /* @__PURE__ */ new Set([
|
|
2355
2880
|
"if_statement",
|
|
@@ -2441,7 +2966,7 @@ function computeLcomMetrics(file, fileId2) {
|
|
|
2441
2966
|
return out;
|
|
2442
2967
|
}
|
|
2443
2968
|
function collectClasses(file) {
|
|
2444
|
-
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);
|
|
2445
2970
|
const out = [];
|
|
2446
2971
|
const visit = (node) => {
|
|
2447
2972
|
if (isClass(node)) out.push(node);
|
|
@@ -2479,14 +3004,21 @@ function collectPythonMethods(classNode) {
|
|
|
2479
3004
|
if (!body) return [];
|
|
2480
3005
|
const out = [];
|
|
2481
3006
|
for (const child of body.namedChildren) {
|
|
2482
|
-
|
|
2483
|
-
|
|
3007
|
+
const fn = child ? pythonMethodNode(child) : null;
|
|
3008
|
+
if (!fn) continue;
|
|
3009
|
+
const name = fn.childForFieldName("name")?.text;
|
|
2484
3010
|
if (!name || name === "__init__") continue;
|
|
2485
|
-
|
|
2486
|
-
out.push({ name, usedNames: thisRefs(child, "self") });
|
|
3011
|
+
out.push({ name, usedNames: thisRefs(fn, "self") });
|
|
2487
3012
|
}
|
|
2488
3013
|
return out;
|
|
2489
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
|
+
}
|
|
2490
3022
|
function methodName(methodNode) {
|
|
2491
3023
|
const nameNode = methodNode.childForFieldName("name");
|
|
2492
3024
|
return nameNode?.text ?? null;
|
|
@@ -2497,16 +3029,10 @@ function hasStaticModifier(methodNode) {
|
|
|
2497
3029
|
}
|
|
2498
3030
|
return false;
|
|
2499
3031
|
}
|
|
2500
|
-
function isPythonStaticOrClassMethod(
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
if (text.includes("@staticmethod") || text.includes("@classmethod")) {
|
|
2505
|
-
return true;
|
|
2506
|
-
}
|
|
2507
|
-
prev = prev.previousNamedSibling;
|
|
2508
|
-
}
|
|
2509
|
-
return false;
|
|
3032
|
+
function isPythonStaticOrClassMethod(decorated) {
|
|
3033
|
+
return decorated.namedChildren.some(
|
|
3034
|
+
(c) => c?.type === "decorator" && (c.text === "@staticmethod" || c.text === "@classmethod")
|
|
3035
|
+
);
|
|
2510
3036
|
}
|
|
2511
3037
|
function thisRefs(methodNode, selfKeyword) {
|
|
2512
3038
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -2651,6 +3177,7 @@ var SYMBOL_METRICS = [
|
|
|
2651
3177
|
{ name: "symbol_cyclomatic", stat: "cyclomatic", unit: "count" },
|
|
2652
3178
|
{ name: "symbol_loc", stat: "loc", unit: "lines" },
|
|
2653
3179
|
{ name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" },
|
|
3180
|
+
{ name: "symbol_jsx_depth", stat: "jsxDepth", unit: "count", omitZero: true },
|
|
2654
3181
|
{ name: "symbol_comment_lines", stat: "commentLines", unit: "lines" },
|
|
2655
3182
|
{ name: "symbol_docstring_lines", stat: "docstringLines", unit: "lines" },
|
|
2656
3183
|
{ name: "symbol_body_lines", stat: "bodyLines", unit: "lines" },
|
|
@@ -2675,6 +3202,7 @@ function symbolMetrics(fileId2, stats, symbolNames) {
|
|
|
2675
3202
|
for (const [name, values] of byName) {
|
|
2676
3203
|
const nodeId = symbolId(fileId2, name);
|
|
2677
3204
|
for (const m of SYMBOL_METRICS) {
|
|
3205
|
+
if (m.omitZero && values[m.stat] === 0) continue;
|
|
2678
3206
|
out.push({ nodeId, name: m.name, value: values[m.stat], unit: m.unit });
|
|
2679
3207
|
}
|
|
2680
3208
|
}
|
|
@@ -2689,10 +3217,6 @@ function functionShapeStats(fn, body, lines) {
|
|
|
2689
3217
|
}
|
|
2690
3218
|
|
|
2691
3219
|
// src/source-metrics.ts
|
|
2692
|
-
var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
2693
|
-
"function_declaration",
|
|
2694
|
-
"method_definition"
|
|
2695
|
-
]);
|
|
2696
3220
|
var PY_FUNCTION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
|
|
2697
3221
|
var TS_NESTING_TYPES = /* @__PURE__ */ new Set([
|
|
2698
3222
|
"if_statement",
|
|
@@ -2735,6 +3259,7 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
|
2735
3259
|
"cognitive_max",
|
|
2736
3260
|
"cognitive_sum",
|
|
2737
3261
|
"max_nesting_depth",
|
|
3262
|
+
"jsx_depth_max",
|
|
2738
3263
|
"class_count",
|
|
2739
3264
|
"lcom4_max",
|
|
2740
3265
|
...EXCEPTION_METRIC_NAMES,
|
|
@@ -2745,10 +3270,10 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
|
2745
3270
|
...SYMBOL_METRIC_NAMES
|
|
2746
3271
|
]);
|
|
2747
3272
|
var EMPTY_NAMES = /* @__PURE__ */ new Set();
|
|
2748
|
-
function computeSourceMetrics(files,
|
|
3273
|
+
function computeSourceMetrics(files, fileIdOf2, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
|
|
2749
3274
|
const out = [];
|
|
2750
3275
|
for (const file of files) {
|
|
2751
|
-
const id =
|
|
3276
|
+
const id = fileIdOf2(file.filePath);
|
|
2752
3277
|
out.push(...metricsForFile(id, file, symbolNamesByFile2.get(id) ?? EMPTY_NAMES));
|
|
2753
3278
|
}
|
|
2754
3279
|
return out;
|
|
@@ -2796,6 +3321,8 @@ function metricsForFile(nodeId, file, symbolNames) {
|
|
|
2796
3321
|
unit: "count"
|
|
2797
3322
|
});
|
|
2798
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" });
|
|
2799
3326
|
out.push(...symbolMetrics(nodeId, stats, symbolNames));
|
|
2800
3327
|
out.push(...computeLcomMetrics(file, nodeId));
|
|
2801
3328
|
out.push(...exceptionMetrics(nodeId, file.tree.rootNode, loc));
|
|
@@ -2806,7 +3333,7 @@ function countLoc(content) {
|
|
|
2806
3333
|
}
|
|
2807
3334
|
function analyzeFunctions(file) {
|
|
2808
3335
|
const stats = [];
|
|
2809
|
-
const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES :
|
|
3336
|
+
const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_DECL_TYPES;
|
|
2810
3337
|
const lines = file.content.split("\n");
|
|
2811
3338
|
walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
|
|
2812
3339
|
const fn = functionAt(node, fnTypes);
|
|
@@ -2816,6 +3343,7 @@ function analyzeFunctions(file) {
|
|
|
2816
3343
|
cyclomatic: cyclomaticOf(fn.body, file.language),
|
|
2817
3344
|
cognitive: cognitiveComplexityOf(fn.body, file.language),
|
|
2818
3345
|
nestingDepth: nestingDepthOf(fn.body, file.language, 0),
|
|
3346
|
+
jsxDepth: jsxDepthIn(file, fn.body),
|
|
2819
3347
|
loc: fn.node.endPosition.row - fn.node.startPosition.row + 1,
|
|
2820
3348
|
...functionShapeStats(fn.node, fn.body, lines)
|
|
2821
3349
|
});
|
|
@@ -2827,13 +3355,17 @@ function functionAt(node, fnTypes) {
|
|
|
2827
3355
|
const body = node.childForFieldName("body");
|
|
2828
3356
|
return body ? { name: node.childForFieldName("name")?.text ?? null, body, node } : null;
|
|
2829
3357
|
}
|
|
2830
|
-
if ((node.type
|
|
3358
|
+
if (TS_BOUND_FUNCTION_TYPES.has(node.type) && node.parent?.type === "variable_declarator") {
|
|
2831
3359
|
const body = node.childForFieldName("body");
|
|
2832
3360
|
if (!body) return null;
|
|
2833
3361
|
return { name: node.parent.childForFieldName("name")?.text ?? null, body, node };
|
|
2834
3362
|
}
|
|
2835
3363
|
return null;
|
|
2836
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
|
+
}
|
|
2837
3369
|
function nestingDepthOf(node, language, depth) {
|
|
2838
3370
|
const nestingTypes = language === "python" ? PY_NESTING_TYPES : TS_NESTING_TYPES;
|
|
2839
3371
|
let maxDepth = depth;
|
|
@@ -3026,12 +3558,12 @@ var TERMINALS2 = /* @__PURE__ */ new Set([
|
|
|
3026
3558
|
"break_statement",
|
|
3027
3559
|
"continue_statement"
|
|
3028
3560
|
]);
|
|
3029
|
-
function computeDeadCodeMetrics(files,
|
|
3561
|
+
function computeDeadCodeMetrics(files, fileIdOf2) {
|
|
3030
3562
|
const out = [];
|
|
3031
3563
|
for (const file of files) {
|
|
3032
3564
|
const counts = deadCodeOf(file);
|
|
3033
3565
|
if (!counts) continue;
|
|
3034
|
-
const id =
|
|
3566
|
+
const id = fileIdOf2(file.filePath);
|
|
3035
3567
|
const { unreachable, locals, params } = counts;
|
|
3036
3568
|
if (unreachable > 0) {
|
|
3037
3569
|
out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
|
|
@@ -3194,12 +3726,12 @@ var SEARCH_METHODS = /* @__PURE__ */ new Set([
|
|
|
3194
3726
|
"every",
|
|
3195
3727
|
"filter"
|
|
3196
3728
|
]);
|
|
3197
|
-
function computeGrowthRiskMetrics(files,
|
|
3729
|
+
function computeGrowthRiskMetrics(files, fileIdOf2) {
|
|
3198
3730
|
const out = [];
|
|
3199
3731
|
for (const file of files) {
|
|
3200
3732
|
const loopTypes = file.language === "python" ? PY_LOOP_TYPES : TS_LOOP_TYPES;
|
|
3201
3733
|
const root = file.tree.rootNode;
|
|
3202
|
-
const id =
|
|
3734
|
+
const id = fileIdOf2(file.filePath);
|
|
3203
3735
|
const depth = maxLoopDepth(root, loopTypes, 0);
|
|
3204
3736
|
if (depth >= 2) {
|
|
3205
3737
|
out.push({ nodeId: id, name: "loop_depth", value: depth, unit: "count" });
|
|
@@ -3270,8 +3802,8 @@ function reconstructFragment(repoRoot, absPath, fileId2, basis) {
|
|
|
3270
3802
|
nodes.push(...basis.symbolsByFile.get(fileId2) ?? []);
|
|
3271
3803
|
const edges = basis.edgesBySrc.get(fileId2) ?? [];
|
|
3272
3804
|
const seenExternals = /* @__PURE__ */ new Set();
|
|
3273
|
-
for (const
|
|
3274
|
-
const dst = basis.nodesById.get(
|
|
3805
|
+
for (const edge2 of edges) {
|
|
3806
|
+
const dst = basis.nodesById.get(edge2.dstId);
|
|
3275
3807
|
if (dst?.kind === "external" && !seenExternals.has(dst.id)) {
|
|
3276
3808
|
seenExternals.add(dst.id);
|
|
3277
3809
|
nodes.push({ id: dst.id, kind: "external", name: dst.name });
|
|
@@ -3317,12 +3849,12 @@ function sourceLanguage(filePath) {
|
|
|
3317
3849
|
const language = getLanguageFromPath2(filePath);
|
|
3318
3850
|
return language === "typescript" || language === "python" ? language : null;
|
|
3319
3851
|
}
|
|
3320
|
-
async function readSourceFiles(filePaths) {
|
|
3852
|
+
async function readSourceFiles(filePaths, source = workingTreeSource()) {
|
|
3321
3853
|
const out = [];
|
|
3322
3854
|
for (const filePath of filePaths) {
|
|
3323
3855
|
const language = sourceLanguage(filePath);
|
|
3324
3856
|
if (language === null) continue;
|
|
3325
|
-
const content =
|
|
3857
|
+
const content = source.readFile(filePath);
|
|
3326
3858
|
out.push({ filePath, language, content, hash: hashContent(content) });
|
|
3327
3859
|
}
|
|
3328
3860
|
return out;
|
|
@@ -3472,8 +4004,8 @@ function resolveRelativeId(srcFileId, specifier, fileIds) {
|
|
|
3472
4004
|
}
|
|
3473
4005
|
return null;
|
|
3474
4006
|
}
|
|
3475
|
-
function targetFileId(
|
|
3476
|
-
return parseSymbolId(
|
|
4007
|
+
function targetFileId(edge2) {
|
|
4008
|
+
return parseSymbolId(edge2.dstId)?.fileId ?? edge2.dstId;
|
|
3477
4009
|
}
|
|
3478
4010
|
function computeDeltaAffected(basis, currentFileIds) {
|
|
3479
4011
|
const affected = /* @__PURE__ */ new Set();
|
|
@@ -3485,8 +4017,8 @@ function computeDeltaAffected(basis, currentFileIds) {
|
|
|
3485
4017
|
if (!added) return affected;
|
|
3486
4018
|
for (const [srcId, edges] of basis.edgesBySrc) {
|
|
3487
4019
|
if (!currentFileIds.has(srcId)) continue;
|
|
3488
|
-
for (const
|
|
3489
|
-
if (isEdgeAffected(
|
|
4020
|
+
for (const edge2 of edges) {
|
|
4021
|
+
if (isEdgeAffected(edge2, srcId, removed2, currentFileIds)) {
|
|
3490
4022
|
affected.add(srcId);
|
|
3491
4023
|
break;
|
|
3492
4024
|
}
|
|
@@ -3494,11 +4026,11 @@ function computeDeltaAffected(basis, currentFileIds) {
|
|
|
3494
4026
|
}
|
|
3495
4027
|
return affected;
|
|
3496
4028
|
}
|
|
3497
|
-
function isEdgeAffected(
|
|
3498
|
-
if (removed2.has(targetFileId(
|
|
3499
|
-
const specifier =
|
|
4029
|
+
function isEdgeAffected(edge2, srcId, removed2, currentFileIds) {
|
|
4030
|
+
if (removed2.has(targetFileId(edge2))) return true;
|
|
4031
|
+
const specifier = edge2.attrs?.specifier;
|
|
3500
4032
|
if (!specifier || !isRelative(specifier)) return false;
|
|
3501
|
-
return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(
|
|
4033
|
+
return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(edge2);
|
|
3502
4034
|
}
|
|
3503
4035
|
|
|
3504
4036
|
// src/metrics.ts
|
|
@@ -3569,142 +4101,45 @@ function computeCallMetrics(nodes, edges) {
|
|
|
3569
4101
|
out.push({ nodeId: node.id, name: "symbol_caller_count", value: callers.size, unit: "count" });
|
|
3570
4102
|
const single = isPrivateHelper(node) && callers.size === 1 && isSymbolCaller(callers);
|
|
3571
4103
|
out.push({ nodeId: node.id, name: "symbol_single_caller_helper", value: single ? 1 : 0, unit: "count" });
|
|
3572
|
-
const shape = readParamShape(node.attrs);
|
|
3573
|
-
if (shape && sites.length >= 2) {
|
|
3574
|
-
out.push({ nodeId: node.id, name: "symbol_constant_params", value: constantParams(shape, sites), unit: "count" });
|
|
3575
|
-
}
|
|
3576
|
-
}
|
|
3577
|
-
return out;
|
|
3578
|
-
}
|
|
3579
|
-
function collectInbound(edges) {
|
|
3580
|
-
const inbound = /* @__PURE__ */ new Map();
|
|
3581
|
-
for (const edge of edges) {
|
|
3582
|
-
if (edge.kind !== "calls") continue;
|
|
3583
|
-
let entry = inbound.get(edge.dstId);
|
|
3584
|
-
if (!entry) inbound.set(edge.dstId, entry = { callers: /* @__PURE__ */ new Set(), sites: [] });
|
|
3585
|
-
if (edge.srcId !== edge.dstId) entry.callers.add(edge.srcId);
|
|
3586
|
-
entry.sites.push(...edge.attrs?.sites ?? []);
|
|
3587
|
-
}
|
|
3588
|
-
return inbound;
|
|
3589
|
-
}
|
|
3590
|
-
function isPrivateHelper(node) {
|
|
3591
|
-
const own = node.name.slice(node.name.lastIndexOf(".") + 1);
|
|
3592
|
-
return node.attrs?.exported === false && !(own.startsWith("__") && own.endsWith("__"));
|
|
3593
|
-
}
|
|
3594
|
-
function isSymbolCaller(callers) {
|
|
3595
|
-
return [...callers].every((id) => parseSymbolId(id) !== null);
|
|
3596
|
-
}
|
|
3597
|
-
function constantParams(shape, sites) {
|
|
3598
|
-
let count = 0;
|
|
3599
|
-
shape.params.forEach((name, index) => {
|
|
3600
|
-
const values = sites.map((site) => argumentAt(site, name, index < shape.positional ? index : -1));
|
|
3601
|
-
if (values.every((v) => v !== null && v === values[0])) count++;
|
|
3602
|
-
});
|
|
3603
|
-
return count;
|
|
3604
|
-
}
|
|
3605
|
-
function argumentAt(site, name, position) {
|
|
3606
|
-
if (position >= 0 && position < site.args.length) return site.args[position] ?? null;
|
|
3607
|
-
if (site.kwargs && Object.hasOwn(site.kwargs, name)) return site.kwargs[name] ?? null;
|
|
3608
|
-
if (site.kwSplat) return null;
|
|
3609
|
-
if (position >= 0 && site.spreadFrom !== void 0 && position >= site.spreadFrom) return null;
|
|
3610
|
-
return ABSENT;
|
|
3611
|
-
}
|
|
3612
|
-
|
|
3613
|
-
// src/analysis/test-linker.ts
|
|
3614
|
-
var DEFAULT_MIN_CO_EDIT_COUNT = 2;
|
|
3615
|
-
var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
|
|
3616
|
-
var TESTS_DIR_RE = /(^|\/)(?:__tests__|tests?)\//;
|
|
3617
|
-
function pathCandidates(testId) {
|
|
3618
|
-
const out = /* @__PURE__ */ new Set();
|
|
3619
|
-
const deInfixed = testId.replace(TEST_INFIX_RE, "$1");
|
|
3620
|
-
if (deInfixed !== testId) out.add(deInfixed);
|
|
3621
|
-
const deDir = deInfixed.replace(TESTS_DIR_RE, "$1");
|
|
3622
|
-
if (deDir !== testId) out.add(deDir);
|
|
3623
|
-
return [...out];
|
|
3624
|
-
}
|
|
3625
|
-
function splitFileIds(nodes) {
|
|
3626
|
-
const testIds = [];
|
|
3627
|
-
const nonTestIds = /* @__PURE__ */ new Set();
|
|
3628
|
-
for (const n of nodes) {
|
|
3629
|
-
if (n.kind !== "file") continue;
|
|
3630
|
-
if (n.role === "test") testIds.push(n.id);
|
|
3631
|
-
else nonTestIds.add(n.id);
|
|
3632
|
-
}
|
|
3633
|
-
return { testIds, nonTestIds };
|
|
3634
|
-
}
|
|
3635
|
-
function linkTestsToSources(nodes, coEditPairs, options = {}) {
|
|
3636
|
-
const minCount = options.minCoEditCount ?? DEFAULT_MIN_CO_EDIT_COUNT;
|
|
3637
|
-
const { testIds, nonTestIds } = splitFileIds(nodes);
|
|
3638
|
-
const links = [];
|
|
3639
|
-
for (const testId of testIds) {
|
|
3640
|
-
const matched = pathCandidates(testId).filter((c) => nonTestIds.has(c));
|
|
3641
|
-
if (matched.length > 0) {
|
|
3642
|
-
for (const sourceId of matched) {
|
|
3643
|
-
links.push({ testId, sourceId, method: "path" });
|
|
3644
|
-
}
|
|
3645
|
-
continue;
|
|
3646
|
-
}
|
|
3647
|
-
const partner = couplingFor(coEditPairs, testId).find((p) => p.count >= minCount && nonTestIds.has(p.partner));
|
|
3648
|
-
if (partner) {
|
|
3649
|
-
links.push({ testId, sourceId: partner.partner, method: "coedit" });
|
|
3650
|
-
}
|
|
3651
|
-
}
|
|
3652
|
-
return links;
|
|
3653
|
-
}
|
|
3654
|
-
function testCoverageCountMetrics(links) {
|
|
3655
|
-
const testsBySource = groupTestsBySource(links);
|
|
3656
|
-
const out = [];
|
|
3657
|
-
for (const [sourceId, tests] of testsBySource) {
|
|
3658
|
-
out.push({
|
|
3659
|
-
nodeId: sourceId,
|
|
3660
|
-
name: "linked_test_count",
|
|
3661
|
-
value: tests.size,
|
|
3662
|
-
unit: "count"
|
|
3663
|
-
});
|
|
3664
|
-
}
|
|
3665
|
-
return out;
|
|
3666
|
-
}
|
|
3667
|
-
function groupTestsBySource(links) {
|
|
3668
|
-
const out = /* @__PURE__ */ new Map();
|
|
3669
|
-
for (const l of links) {
|
|
3670
|
-
let set = out.get(l.sourceId);
|
|
3671
|
-
if (!set) {
|
|
3672
|
-
set = /* @__PURE__ */ new Set();
|
|
3673
|
-
out.set(l.sourceId, set);
|
|
3674
|
-
}
|
|
3675
|
-
set.add(l.testId);
|
|
3676
|
-
}
|
|
3677
|
-
return out;
|
|
3678
|
-
}
|
|
3679
|
-
|
|
3680
|
-
// src/history-recency.ts
|
|
3681
|
-
var DAY_SECONDS = 86400;
|
|
3682
|
-
function windowSuffix(window) {
|
|
3683
|
-
return window === "lifetime" ? "lifetime" : `${window}d`;
|
|
3684
|
-
}
|
|
3685
|
-
function computeRecencyWindows(firstSeen, churnedIdsByWindow, nowEpoch) {
|
|
3686
|
-
const out = [];
|
|
3687
|
-
const ageEmitted = /* @__PURE__ */ new Set();
|
|
3688
|
-
for (const [windowDays, ids] of churnedIdsByWindow) {
|
|
3689
|
-
const suffix = windowSuffix(windowDays);
|
|
3690
|
-
for (const id of ids) {
|
|
3691
|
-
const seen = firstSeen.get(id);
|
|
3692
|
-
let recency = 1;
|
|
3693
|
-
if (seen !== void 0) {
|
|
3694
|
-
const ageDays = Math.max(0, (nowEpoch - seen) / DAY_SECONDS);
|
|
3695
|
-
if (windowDays !== "lifetime") recency = Math.min(1, ageDays / windowDays);
|
|
3696
|
-
if (!ageEmitted.has(id)) {
|
|
3697
|
-
out.push({ nodeId: id, name: "file_age_days", value: Math.round(ageDays), unit: "days" });
|
|
3698
|
-
ageEmitted.add(id);
|
|
3699
|
-
}
|
|
3700
|
-
}
|
|
3701
|
-
out.push({ nodeId: id, name: `recency_${suffix}`, value: round3(recency), unit: "ratio" });
|
|
4104
|
+
const shape = readParamShape(node.attrs);
|
|
4105
|
+
if (shape && sites.length >= 2) {
|
|
4106
|
+
out.push({ nodeId: node.id, name: "symbol_constant_params", value: constantParams(shape, sites), unit: "count" });
|
|
3702
4107
|
}
|
|
3703
4108
|
}
|
|
3704
4109
|
return out;
|
|
3705
4110
|
}
|
|
3706
|
-
function
|
|
3707
|
-
|
|
4111
|
+
function collectInbound(edges) {
|
|
4112
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
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 ?? []);
|
|
4119
|
+
}
|
|
4120
|
+
return inbound;
|
|
4121
|
+
}
|
|
4122
|
+
function isPrivateHelper(node) {
|
|
4123
|
+
const own = node.name.slice(node.name.lastIndexOf(".") + 1);
|
|
4124
|
+
return node.attrs?.exported === false && !(own.startsWith("__") && own.endsWith("__"));
|
|
4125
|
+
}
|
|
4126
|
+
function isSymbolCaller(callers) {
|
|
4127
|
+
return [...callers].every((id) => parseSymbolId(id) !== null);
|
|
4128
|
+
}
|
|
4129
|
+
function constantParams(shape, sites) {
|
|
4130
|
+
let count = 0;
|
|
4131
|
+
shape.params.forEach((name, index) => {
|
|
4132
|
+
const values = sites.map((site) => argumentAt(site, name, index < shape.positional ? index : -1));
|
|
4133
|
+
if (values.every((v) => v !== null && v === values[0])) count++;
|
|
4134
|
+
});
|
|
4135
|
+
return count;
|
|
4136
|
+
}
|
|
4137
|
+
function argumentAt(site, name, position) {
|
|
4138
|
+
if (position >= 0 && position < site.args.length) return site.args[position] ?? null;
|
|
4139
|
+
if (site.kwargs && Object.hasOwn(site.kwargs, name)) return site.kwargs[name] ?? null;
|
|
4140
|
+
if (site.kwSplat) return null;
|
|
4141
|
+
if (position >= 0 && site.spreadFrom !== void 0 && position >= site.spreadFrom) return null;
|
|
4142
|
+
return ABSENT;
|
|
3708
4143
|
}
|
|
3709
4144
|
|
|
3710
4145
|
// src/history-metrics.ts
|
|
@@ -3718,7 +4153,12 @@ function loadHistoryMetrics(nodes, idRoot, options = {}) {
|
|
|
3718
4153
|
const knownPaths = collectFileIds(nodes);
|
|
3719
4154
|
const primaryWindow = options.churnWindowDays ?? 30;
|
|
3720
4155
|
const windows = resolveChurnWindows(options.churnWindows, primaryWindow, options.includeLifetime === true);
|
|
3721
|
-
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
|
+
});
|
|
3722
4162
|
if (wide === null) return null;
|
|
3723
4163
|
const nowEpoch = options.nowEpoch ?? Math.floor(Date.now() / 1e3);
|
|
3724
4164
|
const churnByWindow = aggregateChurnWindows(wide, windows, nowEpoch, knownPaths);
|
|
@@ -3726,7 +4166,7 @@ function loadHistoryMetrics(nodes, idRoot, options = {}) {
|
|
|
3726
4166
|
const metrics = [
|
|
3727
4167
|
...churnMetrics(churnByWindow),
|
|
3728
4168
|
...ownershipMetrics(primaryEntries, primaryWindow, knownPaths),
|
|
3729
|
-
...recencyMetrics(idRoot,
|
|
4169
|
+
...recencyMetrics({ idRoot, rev: options.rev, knownPaths, nowEpoch }, churnByWindow)
|
|
3730
4170
|
];
|
|
3731
4171
|
if (options.includeLifetime === true) metrics.push(...ownershipMetrics(wide, "lifetime", knownPaths));
|
|
3732
4172
|
return { metrics, primaryEntries };
|
|
@@ -3793,14 +4233,14 @@ function mergeAuthorChurn(tests, linesByTest) {
|
|
|
3793
4233
|
}
|
|
3794
4234
|
return byAuthor;
|
|
3795
4235
|
}
|
|
3796
|
-
function recencyMetrics(
|
|
4236
|
+
function recencyMetrics(at, churnByWindow) {
|
|
3797
4237
|
const churnedByWindow = /* @__PURE__ */ new Map();
|
|
3798
4238
|
for (const [window, byPath] of churnByWindow) {
|
|
3799
4239
|
if (byPath.size > 0) churnedByWindow.set(window, new Set(byPath.keys()));
|
|
3800
4240
|
}
|
|
3801
4241
|
if (churnedByWindow.size === 0) return [];
|
|
3802
|
-
const firstSeen = loadFileFirstSeen({ repoRoot: idRoot, knownPaths }) ?? /* @__PURE__ */ new Map();
|
|
3803
|
-
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);
|
|
3804
4244
|
}
|
|
3805
4245
|
|
|
3806
4246
|
// src/index-metrics.ts
|
|
@@ -3844,17 +4284,17 @@ function computeTestCoverage(nodes, entries, windowDays) {
|
|
|
3844
4284
|
}
|
|
3845
4285
|
|
|
3846
4286
|
// src/merge.ts
|
|
3847
|
-
function edgeKey(
|
|
3848
|
-
return JSON.stringify([
|
|
4287
|
+
function edgeKey(edge2) {
|
|
4288
|
+
return JSON.stringify([edge2.srcId, edge2.dstId, edge2.kind]);
|
|
3849
4289
|
}
|
|
3850
4290
|
function mergeFragments(fragments) {
|
|
3851
4291
|
const nodes = /* @__PURE__ */ new Map();
|
|
3852
4292
|
const edges = /* @__PURE__ */ new Map();
|
|
3853
4293
|
for (const fragment of fragments) {
|
|
3854
4294
|
for (const node of fragment.nodes) if (!nodes.has(node.id)) nodes.set(node.id, node);
|
|
3855
|
-
for (const
|
|
3856
|
-
const key = edgeKey(
|
|
3857
|
-
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);
|
|
3858
4298
|
}
|
|
3859
4299
|
}
|
|
3860
4300
|
return { nodes, edges };
|
|
@@ -3898,18 +4338,18 @@ function qualifiedByName(file) {
|
|
|
3898
4338
|
}
|
|
3899
4339
|
|
|
3900
4340
|
// src/indexer.ts
|
|
3901
|
-
var INDEX_VERSION = "0.
|
|
4341
|
+
var INDEX_VERSION = "0.22.0";
|
|
3902
4342
|
var LANGUAGES = ["typescript", "python"];
|
|
3903
4343
|
function canonicalizePath(p) {
|
|
3904
4344
|
try {
|
|
3905
|
-
return
|
|
4345
|
+
return realpathSync2(p);
|
|
3906
4346
|
} catch {
|
|
3907
4347
|
return p;
|
|
3908
4348
|
}
|
|
3909
4349
|
}
|
|
3910
4350
|
function normalizePaths(paths) {
|
|
3911
4351
|
if (paths.length === 0) throw new Error("indexPaths requires at least one path");
|
|
3912
|
-
return [...new Set(paths.map((p) =>
|
|
4352
|
+
return [...new Set(paths.map((p) => path10.resolve(p)))];
|
|
3913
4353
|
}
|
|
3914
4354
|
function countByKind(items) {
|
|
3915
4355
|
const out = {};
|
|
@@ -3932,8 +4372,9 @@ function resolveSymbolAliases(store, idRoot, parsedByPath, nodeIds) {
|
|
|
3932
4372
|
const files = [...parsedByPath.values()].map((file) => ({ fileId: fileId(idRoot, file.filePath), file }));
|
|
3933
4373
|
return qualifiedSymbolAliases(files, nodeIds);
|
|
3934
4374
|
}
|
|
3935
|
-
function resolveRoots(paths) {
|
|
4375
|
+
function resolveRoots(paths, source) {
|
|
3936
4376
|
const raw = normalizePaths(paths);
|
|
4377
|
+
if (source.revision) return rootsAtRevision(raw, source.revision);
|
|
3937
4378
|
const gitToplevel = detectGitToplevel(raw[0]);
|
|
3938
4379
|
const rootDirs = gitToplevel !== null ? raw.map(canonicalizePath) : raw;
|
|
3939
4380
|
return { rootDirs, idRoot: gitToplevel ?? rootDirs[0] };
|
|
@@ -3945,16 +4386,19 @@ async function parseAll(toParse) {
|
|
|
3945
4386
|
}
|
|
3946
4387
|
return parsed;
|
|
3947
4388
|
}
|
|
3948
|
-
function historyOptions(options) {
|
|
4389
|
+
function historyOptions(options, source) {
|
|
3949
4390
|
return {
|
|
3950
4391
|
churnWindowDays: options.churnWindowDays,
|
|
3951
4392
|
churnWindows: options.churnWindows,
|
|
3952
|
-
includeLifetime: options.lifetime === true
|
|
4393
|
+
includeLifetime: options.lifetime === true,
|
|
4394
|
+
rev: source.revision?.commit,
|
|
4395
|
+
nowEpoch: source.revision?.commitEpoch
|
|
3953
4396
|
};
|
|
3954
4397
|
}
|
|
3955
4398
|
async function indexPaths(store, options) {
|
|
3956
|
-
const
|
|
3957
|
-
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);
|
|
3958
4402
|
const reuse = options.incremental !== false ? loadReuseBasis(store, INDEX_VERSION) : null;
|
|
3959
4403
|
const currentFileIds = new Set(readFiles.map((rf) => fileId(idRoot, rf.filePath)));
|
|
3960
4404
|
const affected = reuse ? computeDeltaAffected(reuse, currentFileIds) : /* @__PURE__ */ new Set();
|
|
@@ -3968,10 +4412,11 @@ async function indexPaths(store, options) {
|
|
|
3968
4412
|
parsedByPath,
|
|
3969
4413
|
reuse,
|
|
3970
4414
|
cosmeticFileIds: classified.cosmeticFileIds,
|
|
3971
|
-
extractor: new LanguageExtractor({ repoRoot: idRoot, tsConfigPath: options.tsConfig })
|
|
4415
|
+
extractor: new LanguageExtractor({ repoRoot: idRoot, tsConfigPath: options.tsConfig, source })
|
|
3972
4416
|
})
|
|
3973
4417
|
);
|
|
3974
|
-
const
|
|
4418
|
+
const roleHints = computeRoleHints(readFiles, idRoot, fileId, source);
|
|
4419
|
+
const annotated = annotateRoles([...accumulator.nodes.values()], roleHints);
|
|
3975
4420
|
accumulator.nodes = new Map(annotated.map((n) => [n.id, n]));
|
|
3976
4421
|
pruneDanglingReferences(accumulator.nodes, accumulator.edges);
|
|
3977
4422
|
const metrics = options.computeMetrics === false ? [] : buildIndexerMetrics({
|
|
@@ -3980,19 +4425,23 @@ async function indexPaths(store, options) {
|
|
|
3980
4425
|
parsedFiles: [...parsedByPath.values()],
|
|
3981
4426
|
reusedSourceMetrics: reuse ? reusedFileIds.flatMap((id) => reuse.sourceMetricsByFile.get(id) ?? []) : [],
|
|
3982
4427
|
idRoot,
|
|
3983
|
-
history: options.computeChurn === false ? void 0 : historyOptions(options)
|
|
4428
|
+
history: options.computeChurn === false ? void 0 : historyOptions(options, source)
|
|
3984
4429
|
});
|
|
3985
|
-
const rootDir = rootDirs[0];
|
|
4430
|
+
const rootDir = source.revision ? idRoot : rootDirs[0];
|
|
3986
4431
|
const ref = options.ref ?? "wd";
|
|
3987
|
-
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 });
|
|
3988
4434
|
const aliases = [
|
|
3989
4435
|
...bridge.aliases,
|
|
3990
4436
|
...resolveSymbolAliases(store, idRoot, parsedByPath, new Set(accumulator.nodes.keys()))
|
|
3991
4437
|
];
|
|
3992
|
-
const commitHash = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
|
|
3993
4438
|
const snapshot = { ref, commitHash, aliasBase: bridge.baseSnapshotId };
|
|
3994
|
-
const
|
|
3995
|
-
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
|
+
});
|
|
3996
4445
|
return {
|
|
3997
4446
|
snapshotId,
|
|
3998
4447
|
files: readFiles.length,
|
|
@@ -4009,14 +4458,14 @@ async function indexPaths(store, options) {
|
|
|
4009
4458
|
}
|
|
4010
4459
|
|
|
4011
4460
|
// src/index.ts
|
|
4012
|
-
import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as
|
|
4461
|
+
import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as parseFile3, shouldIncludeFile as shouldIncludeFile3 } from "@titan-design/code-parser";
|
|
4013
4462
|
|
|
4014
4463
|
// src/extractors/deep-ast.ts
|
|
4015
4464
|
import { readFileSync as readFileSync2 } from "fs";
|
|
4016
|
-
import * as
|
|
4465
|
+
import * as path11 from "path";
|
|
4017
4466
|
import {
|
|
4018
4467
|
Node as Node6,
|
|
4019
|
-
Project as
|
|
4468
|
+
Project as Project4,
|
|
4020
4469
|
ScriptTarget as ScriptTarget2
|
|
4021
4470
|
} from "ts-morph";
|
|
4022
4471
|
function computeDeepAst(input) {
|
|
@@ -4028,11 +4477,11 @@ function computeDeepAst(input) {
|
|
|
4028
4477
|
function loadSourceFile(absPath) {
|
|
4029
4478
|
try {
|
|
4030
4479
|
const text = readFileSync2(absPath, "utf8");
|
|
4031
|
-
const project = new
|
|
4480
|
+
const project = new Project4({
|
|
4032
4481
|
useInMemoryFileSystem: true,
|
|
4033
4482
|
compilerOptions: { target: ScriptTarget2.Latest, allowJs: true }
|
|
4034
4483
|
});
|
|
4035
|
-
return project.createSourceFile(`f${
|
|
4484
|
+
return project.createSourceFile(`f${path11.extname(absPath) || ".ts"}`, text);
|
|
4036
4485
|
} catch {
|
|
4037
4486
|
return null;
|
|
4038
4487
|
}
|
|
@@ -4210,11 +4659,11 @@ function innermostContaining(symbols, line) {
|
|
|
4210
4659
|
}
|
|
4211
4660
|
return best;
|
|
4212
4661
|
}
|
|
4213
|
-
function attributeCoverage(coverage,
|
|
4662
|
+
function attributeCoverage(coverage, fileIdOf2, symbolsByFile) {
|
|
4214
4663
|
const out = [];
|
|
4215
4664
|
for (const [absPath, fc] of Object.entries(coverage)) {
|
|
4216
4665
|
if (!isFileCoverage(fc)) continue;
|
|
4217
|
-
const fileId2 =
|
|
4666
|
+
const fileId2 = fileIdOf2(absPath);
|
|
4218
4667
|
if (!fileId2) continue;
|
|
4219
4668
|
const fnIds = Object.keys(fc.fnMap);
|
|
4220
4669
|
if (fnIds.length === 0) continue;
|
|
@@ -4245,142 +4694,196 @@ function pctMetric(nodeId, covered, total) {
|
|
|
4245
4694
|
};
|
|
4246
4695
|
}
|
|
4247
4696
|
|
|
4248
|
-
// src/analysis/
|
|
4249
|
-
|
|
4250
|
-
var
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
"re-exports": 0.5,
|
|
4255
|
-
calls: 1.5,
|
|
4256
|
-
extends: 1,
|
|
4257
|
-
implements: 1,
|
|
4258
|
-
references: 1,
|
|
4259
|
-
"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"])
|
|
4260
4703
|
};
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
const
|
|
4269
|
-
|
|
4270
|
-
const weights = { ...DEFAULT_EDGE_WEIGHTS, ...options.edgeWeights ?? {} };
|
|
4271
|
-
const idx = buildIdIndex(nodes);
|
|
4272
|
-
const adj = buildAdjacency(idx, edges, weights, n);
|
|
4273
|
-
const pers = buildPersonalization(idx, n, options.personalization);
|
|
4274
|
-
const { rank, iterations, converged } = powerIterate(
|
|
4275
|
-
pers,
|
|
4276
|
-
adj,
|
|
4277
|
-
damping,
|
|
4278
|
-
tolerance,
|
|
4279
|
-
maxIters
|
|
4280
|
-
);
|
|
4281
|
-
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"}`;
|
|
4282
4713
|
}
|
|
4283
|
-
function
|
|
4284
|
-
|
|
4285
|
-
for (let i = 0; i < nodes.length; i++) idx.set(nodes[i].id, i);
|
|
4286
|
-
return idx;
|
|
4714
|
+
function originalLine(minified, outputLine) {
|
|
4715
|
+
return minified.lineMap[outputLine - 1];
|
|
4287
4716
|
}
|
|
4288
|
-
function
|
|
4289
|
-
|
|
4290
|
-
{ length: n },
|
|
4291
|
-
() => []
|
|
4292
|
-
);
|
|
4293
|
-
const outWeightSum = new Array(n).fill(0);
|
|
4294
|
-
for (const e of edges) {
|
|
4295
|
-
const si = idx.get(e.srcId);
|
|
4296
|
-
const di = idx.get(e.dstId);
|
|
4297
|
-
if (si === void 0 || di === void 0) continue;
|
|
4298
|
-
const w = weights[e.kind] ?? 1;
|
|
4299
|
-
if (w <= 0) continue;
|
|
4300
|
-
outNeighbors[si].push({ to: di, w });
|
|
4301
|
-
outWeightSum[si] += w;
|
|
4302
|
-
}
|
|
4303
|
-
return { outNeighbors, outWeightSum };
|
|
4304
|
-
}
|
|
4305
|
-
function powerIterate(pers, adj, damping, tolerance, maxIters) {
|
|
4306
|
-
const n = pers.length;
|
|
4307
|
-
let rank = pers.slice();
|
|
4308
|
-
let next = new Array(n).fill(0);
|
|
4309
|
-
let iterations = 0;
|
|
4310
|
-
let converged = false;
|
|
4311
|
-
for (let iter = 0; iter < maxIters; iter++) {
|
|
4312
|
-
iterations = iter + 1;
|
|
4313
|
-
const dangling = sumDangling(rank, adj.outWeightSum);
|
|
4314
|
-
const teleport = 1 - damping + damping * dangling;
|
|
4315
|
-
seedTeleport(next, pers, teleport);
|
|
4316
|
-
distributeRank(next, rank, adj, damping);
|
|
4317
|
-
const diff = l1Diff(rank, next);
|
|
4318
|
-
[rank, next] = [next, rank];
|
|
4319
|
-
next.fill(0);
|
|
4320
|
-
if (diff < tolerance) {
|
|
4321
|
-
converged = true;
|
|
4322
|
-
break;
|
|
4323
|
-
}
|
|
4324
|
-
}
|
|
4325
|
-
return { rank, iterations, converged };
|
|
4717
|
+
function isMinifyLanguage(language) {
|
|
4718
|
+
return language === "typescript" || language === "tsx" || language === "python";
|
|
4326
4719
|
}
|
|
4327
|
-
function
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
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();
|
|
4331
4727
|
}
|
|
4332
|
-
return total;
|
|
4333
4728
|
}
|
|
4334
|
-
function
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
next[list[k].to] += factor * list[k].w;
|
|
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;
|
|
4345
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
|
+
];
|
|
4346
4757
|
}
|
|
4758
|
+
return [...comments.map(spanOf), ...pythonDocstrings(root).map(docstringSpan)];
|
|
4347
4759
|
}
|
|
4348
|
-
function
|
|
4349
|
-
|
|
4350
|
-
for (let i = 0; i < a.length; i++) diff += Math.abs(b[i] - a[i]);
|
|
4351
|
-
return diff;
|
|
4760
|
+
function spanOf(node) {
|
|
4761
|
+
return { start: node.startPosition, end: node.endPosition };
|
|
4352
4762
|
}
|
|
4353
|
-
function
|
|
4354
|
-
|
|
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 };
|
|
4355
4767
|
}
|
|
4356
|
-
function
|
|
4357
|
-
|
|
4358
|
-
|
|
4768
|
+
function jsxCommentExpressions(root) {
|
|
4769
|
+
return descendantsOfType(root, "jsx_expression").filter(
|
|
4770
|
+
(n) => n.namedChildCount > 0 && n.namedChildren.every((c) => c?.type === "comment")
|
|
4771
|
+
);
|
|
4359
4772
|
}
|
|
4360
|
-
function
|
|
4361
|
-
|
|
4362
|
-
const
|
|
4363
|
-
const
|
|
4364
|
-
|
|
4365
|
-
for (let i = 0; i < n; i++) out[i] /= total;
|
|
4366
|
-
return out;
|
|
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);
|
|
4367
4778
|
}
|
|
4368
|
-
function
|
|
4369
|
-
|
|
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;
|
|
4806
|
+
}
|
|
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;
|
|
4370
4815
|
}
|
|
4371
|
-
function
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
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);
|
|
4379
4876
|
}
|
|
4380
|
-
|
|
4877
|
+
while (out.length > 0 && out[out.length - 1]?.text === "" && !out[out.length - 1]?.verbatim) out.pop();
|
|
4878
|
+
return out;
|
|
4381
4879
|
}
|
|
4382
|
-
function
|
|
4383
|
-
|
|
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
|
+
};
|
|
4384
4887
|
}
|
|
4385
4888
|
|
|
4386
4889
|
// src/analysis/relevance.ts
|
|
@@ -4406,290 +4909,428 @@ function symmetrize(edges) {
|
|
|
4406
4909
|
return out;
|
|
4407
4910
|
}
|
|
4408
4911
|
|
|
4409
|
-
// src/analysis/
|
|
4410
|
-
var
|
|
4411
|
-
var
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
set.add(e.srcId);
|
|
4421
|
-
}
|
|
4422
|
-
const out = [];
|
|
4423
|
-
for (const [id, consumers] of bySymbol) {
|
|
4424
|
-
const parsed = parseSymbolId(id);
|
|
4425
|
-
if (!parsed) continue;
|
|
4426
|
-
out.push({
|
|
4427
|
-
symbolId: id,
|
|
4428
|
-
fileId: parsed.fileId,
|
|
4429
|
-
name: parsed.name,
|
|
4430
|
-
consumers: [...consumers].sort()
|
|
4431
|
-
});
|
|
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));
|
|
4432
4923
|
}
|
|
4433
|
-
return
|
|
4924
|
+
return lines.join("\n");
|
|
4434
4925
|
}
|
|
4435
|
-
function
|
|
4436
|
-
|
|
4437
|
-
|
|
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);
|
|
4438
4935
|
}
|
|
4439
|
-
return a
|
|
4936
|
+
return [...byFile].sort(([a], [b]) => compare2(a, b));
|
|
4440
4937
|
}
|
|
4441
|
-
function
|
|
4442
|
-
const
|
|
4443
|
-
const
|
|
4444
|
-
|
|
4445
|
-
const counts = /* @__PURE__ */ new Map();
|
|
4446
|
-
for (const symbols of bySource.values()) {
|
|
4447
|
-
if (symbols.length < 2 || symbols.length > largeThreshold) continue;
|
|
4448
|
-
accumulateSymbolPairs(symbols, counts);
|
|
4449
|
-
}
|
|
4450
|
-
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 };
|
|
4451
4942
|
}
|
|
4452
|
-
function
|
|
4453
|
-
const
|
|
4454
|
-
|
|
4455
|
-
if (!parseSymbolId(e.dstId)) continue;
|
|
4456
|
-
let set = perSource.get(e.srcId);
|
|
4457
|
-
if (!set) {
|
|
4458
|
-
set = /* @__PURE__ */ new Set();
|
|
4459
|
-
perSource.set(e.srcId, set);
|
|
4460
|
-
}
|
|
4461
|
-
set.add(e.dstId);
|
|
4462
|
-
}
|
|
4463
|
-
const out = /* @__PURE__ */ new Map();
|
|
4464
|
-
for (const [src, set] of perSource) out.set(src, [...set].sort());
|
|
4465
|
-
return out;
|
|
4943
|
+
function numberAttr(node, key) {
|
|
4944
|
+
const value = node.attrs?.[key];
|
|
4945
|
+
return typeof value === "number" ? value : void 0;
|
|
4466
4946
|
}
|
|
4467
|
-
function
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
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);
|
|
4472
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);
|
|
4473
4957
|
}
|
|
4958
|
+
return lines;
|
|
4474
4959
|
}
|
|
4475
|
-
function
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
bName: b.name,
|
|
4489
|
-
coImports,
|
|
4490
|
-
crossFile: a.fileId !== b.fileId
|
|
4491
|
-
});
|
|
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;
|
|
4492
4973
|
}
|
|
4493
|
-
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;
|
|
4494
4978
|
}
|
|
4495
|
-
function
|
|
4496
|
-
if (
|
|
4497
|
-
if (
|
|
4498
|
-
|
|
4499
|
-
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;
|
|
4500
4983
|
}
|
|
4501
4984
|
|
|
4502
|
-
// src/analysis/
|
|
4503
|
-
var
|
|
4504
|
-
var
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
barrelTargets,
|
|
4516
|
-
resolveBarrels
|
|
4517
|
-
);
|
|
4518
|
-
const counts = countByPackage(resolvedPairs);
|
|
4519
|
-
const totalEdges = resolvedPairs.length;
|
|
4520
|
-
const filesByPkg = filesPerPackage(input.fileByPackage);
|
|
4521
|
-
const abstractByPkg = countAbstractFiles(input.nodes, pkgByFile);
|
|
4522
|
-
const perPackage = input.packages.map(
|
|
4523
|
-
(p) => buildPackageStats(p.id, counts, filesByPkg, abstractByPkg)
|
|
4524
|
-
);
|
|
4525
|
-
const pairCoupling = buildPairCoupling(resolvedPairs, filesByPkg);
|
|
4526
|
-
const modularityQ = computeModularity(perPackage, totalEdges);
|
|
4527
|
-
const flagsCount = perPackage.reduce((acc, p) => acc + p.flags.length, 0) + pairCoupling.filter((c) => c.flag === "tight").length;
|
|
4528
|
-
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
|
+
};
|
|
4529
4998
|
}
|
|
4530
|
-
function
|
|
4999
|
+
function consumerCounts(edges) {
|
|
4531
5000
|
const out = /* @__PURE__ */ new Map();
|
|
4532
|
-
for (const
|
|
4533
|
-
if (pkgId === "") continue;
|
|
4534
|
-
for (const f of files) out.set(f, pkgId);
|
|
4535
|
-
}
|
|
5001
|
+
for (const c of computeSymbolConsumers(edges)) out.set(c.symbolId, c.consumers);
|
|
4536
5002
|
return out;
|
|
4537
5003
|
}
|
|
4538
|
-
function
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
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
|
+
});
|
|
4542
5049
|
}
|
|
4543
|
-
return
|
|
5050
|
+
return sortSymbols(lines);
|
|
4544
5051
|
}
|
|
4545
|
-
function
|
|
4546
|
-
const
|
|
4547
|
-
|
|
4548
|
-
);
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
const
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
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)
|
|
4577
5098
|
};
|
|
4578
|
-
for (const id of barrelIds) resolve4(id, /* @__PURE__ */ new Set());
|
|
4579
|
-
return resolved;
|
|
4580
5099
|
}
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
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("");
|
|
4592
5156
|
}
|
|
4593
5157
|
return out;
|
|
4594
5158
|
}
|
|
4595
|
-
function
|
|
4596
|
-
const
|
|
4597
|
-
const
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
}
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
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}`);
|
|
4611
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("");
|
|
4612
5186
|
}
|
|
4613
5187
|
return out;
|
|
4614
5188
|
}
|
|
4615
|
-
function
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
const
|
|
4621
|
-
const
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
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
|
+
}));
|
|
4626
5254
|
return {
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
incomingEdges: c.incoming,
|
|
4632
|
-
cohesion,
|
|
4633
|
-
instability,
|
|
4634
|
-
abstractness,
|
|
4635
|
-
layer,
|
|
4636
|
-
flags
|
|
5255
|
+
callers: rankEdges(callers, ctx),
|
|
5256
|
+
dependencies: rankEdges(dependencies, ctx),
|
|
5257
|
+
coupledWith: rankEdges(coupledWith, ctx),
|
|
5258
|
+
note: EDGE_NOTE
|
|
4637
5259
|
};
|
|
4638
5260
|
}
|
|
4639
|
-
function
|
|
4640
|
-
const
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
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 };
|
|
4645
5293
|
}
|
|
4646
|
-
return out;
|
|
4647
5294
|
}
|
|
4648
|
-
function
|
|
4649
|
-
return
|
|
5295
|
+
function sliceSpan(content, span) {
|
|
5296
|
+
return content.split("\n").slice(span.startLine - 1, span.endLine).join("\n");
|
|
4650
5297
|
}
|
|
4651
|
-
function
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
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
|
+
};
|
|
4655
5309
|
}
|
|
4656
|
-
function
|
|
4657
|
-
const
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
}
|
|
4679
|
-
|
|
4680
|
-
if (a.from !== b.from) return a.from < b.from ? -1 : 1;
|
|
4681
|
-
return a.to < b.to ? -1 : 1;
|
|
4682
|
-
}
|
|
4683
|
-
function computeModularity(perPackage, totalEdges) {
|
|
4684
|
-
if (totalEdges === 0) return 0;
|
|
4685
|
-
let Q = 0;
|
|
4686
|
-
for (const p of perPackage) {
|
|
4687
|
-
const e_cc = p.internalEdges / totalEdges;
|
|
4688
|
-
const touching = p.internalEdges * 2 + p.outgoingEdges + p.incomingEdges;
|
|
4689
|
-
const a_c = touching / (2 * totalEdges);
|
|
4690
|
-
Q += e_cc - a_c * a_c;
|
|
4691
|
-
}
|
|
4692
|
-
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})`;
|
|
4693
5334
|
}
|
|
4694
5335
|
|
|
4695
5336
|
// src/analysis/snapshot.ts
|
|
@@ -4709,6 +5350,57 @@ function snapshotSymbolCoupling(store, snapshotId, options = {}) {
|
|
|
4709
5350
|
return computeSymbolCoupling(snapshotReferenceEdges(store, snapshotId), options);
|
|
4710
5351
|
}
|
|
4711
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
|
+
|
|
4712
5404
|
// src/read.ts
|
|
4713
5405
|
function listNodes(store, snapshotId, opts) {
|
|
4714
5406
|
return store.listNodes(snapshotId, opts);
|
|
@@ -4804,7 +5496,7 @@ var SOURCE = [
|
|
|
4804
5496
|
direction: "higher-worse",
|
|
4805
5497
|
absent: "exclude",
|
|
4806
5498
|
source: "source-metrics",
|
|
4807
|
-
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."
|
|
4808
5500
|
},
|
|
4809
5501
|
{
|
|
4810
5502
|
name: "cyclomatic_sum",
|
|
@@ -4814,7 +5506,7 @@ var SOURCE = [
|
|
|
4814
5506
|
direction: "higher-worse",
|
|
4815
5507
|
absent: "zero",
|
|
4816
5508
|
source: "source-metrics",
|
|
4817
|
-
description: "Cyclomatic complexity summed over the file's functions."
|
|
5509
|
+
description: "Cyclomatic complexity summed over the file's functions; points attention at branchy code."
|
|
4818
5510
|
},
|
|
4819
5511
|
{
|
|
4820
5512
|
name: "cognitive_max",
|
|
@@ -4824,7 +5516,7 @@ var SOURCE = [
|
|
|
4824
5516
|
direction: "higher-worse",
|
|
4825
5517
|
absent: "exclude",
|
|
4826
5518
|
source: "source-metrics",
|
|
4827
|
-
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."
|
|
4828
5520
|
},
|
|
4829
5521
|
{
|
|
4830
5522
|
name: "cognitive_sum",
|
|
@@ -4834,7 +5526,7 @@ var SOURCE = [
|
|
|
4834
5526
|
direction: "higher-worse",
|
|
4835
5527
|
absent: "zero",
|
|
4836
5528
|
source: "source-metrics",
|
|
4837
|
-
description: "Cognitive complexity summed over the file's functions."
|
|
5529
|
+
description: "Cognitive complexity summed over the file's functions, an estimate of comprehension friction."
|
|
4838
5530
|
},
|
|
4839
5531
|
{
|
|
4840
5532
|
name: "max_nesting_depth",
|
|
@@ -4846,6 +5538,16 @@ var SOURCE = [
|
|
|
4846
5538
|
source: "source-metrics",
|
|
4847
5539
|
description: "Deepest block nesting inside any function in the file."
|
|
4848
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
|
+
},
|
|
4849
5551
|
{
|
|
4850
5552
|
name: "symbol_cognitive",
|
|
4851
5553
|
unit: "count",
|
|
@@ -4854,7 +5556,7 @@ var SOURCE = [
|
|
|
4854
5556
|
direction: "higher-worse",
|
|
4855
5557
|
absent: "exclude",
|
|
4856
5558
|
source: "source-metrics",
|
|
4857
|
-
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."
|
|
4858
5560
|
},
|
|
4859
5561
|
{
|
|
4860
5562
|
name: "symbol_cyclomatic",
|
|
@@ -4864,7 +5566,7 @@ var SOURCE = [
|
|
|
4864
5566
|
direction: "higher-worse",
|
|
4865
5567
|
absent: "exclude",
|
|
4866
5568
|
source: "source-metrics",
|
|
4867
|
-
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."
|
|
4868
5570
|
},
|
|
4869
5571
|
{
|
|
4870
5572
|
name: "symbol_loc",
|
|
@@ -4886,6 +5588,16 @@ var SOURCE = [
|
|
|
4886
5588
|
source: "source-metrics",
|
|
4887
5589
|
description: "Deepest block nesting inside the function a symbol names; the max when several share the name."
|
|
4888
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
|
+
},
|
|
4889
5601
|
{
|
|
4890
5602
|
name: "symbol_comment_lines",
|
|
4891
5603
|
unit: "lines",
|
|
@@ -5132,7 +5844,7 @@ var HISTORY = [
|
|
|
5132
5844
|
absent: "exclude",
|
|
5133
5845
|
source: "history",
|
|
5134
5846
|
windowed: true,
|
|
5135
|
-
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."
|
|
5136
5848
|
},
|
|
5137
5849
|
{
|
|
5138
5850
|
name: "file_age_days",
|
|
@@ -5275,39 +5987,6 @@ function indexMetrics(metrics) {
|
|
|
5275
5987
|
return metricsByNode;
|
|
5276
5988
|
}
|
|
5277
5989
|
|
|
5278
|
-
// src/check/patterns.ts
|
|
5279
|
-
function patternToRegex(pattern) {
|
|
5280
|
-
if (!pattern.includes("*")) {
|
|
5281
|
-
return new RegExp(escapeRegex(pattern));
|
|
5282
|
-
}
|
|
5283
|
-
let out = "";
|
|
5284
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
5285
|
-
const c = pattern[i];
|
|
5286
|
-
if (c === "*" && pattern[i + 1] === "*") {
|
|
5287
|
-
out += ".*";
|
|
5288
|
-
i++;
|
|
5289
|
-
} else if (c === "*") {
|
|
5290
|
-
out += "[^/]*";
|
|
5291
|
-
} else {
|
|
5292
|
-
out += escapeRegex(c);
|
|
5293
|
-
}
|
|
5294
|
-
}
|
|
5295
|
-
return new RegExp(`^${out}$`);
|
|
5296
|
-
}
|
|
5297
|
-
function compilePatterns(patterns) {
|
|
5298
|
-
if (!patterns) return [];
|
|
5299
|
-
return patterns.map(patternToRegex);
|
|
5300
|
-
}
|
|
5301
|
-
function matchesAny(value, patterns) {
|
|
5302
|
-
for (const rx of patterns) {
|
|
5303
|
-
if (rx.test(value)) return true;
|
|
5304
|
-
}
|
|
5305
|
-
return false;
|
|
5306
|
-
}
|
|
5307
|
-
function escapeRegex(s) {
|
|
5308
|
-
return s.replace(/[.+?(){}|^$\\[\]]/g, "\\$&");
|
|
5309
|
-
}
|
|
5310
|
-
|
|
5311
5990
|
// src/check/rule-helpers.ts
|
|
5312
5991
|
function severityOf(rule) {
|
|
5313
5992
|
return rule.severity ?? "error";
|
|
@@ -5316,8 +5995,8 @@ function formatNumber(n) {
|
|
|
5316
5995
|
if (Number.isInteger(n)) return String(n);
|
|
5317
5996
|
return n.toFixed(3).replace(/\.?0+$/, "");
|
|
5318
5997
|
}
|
|
5319
|
-
function isImportEdge(
|
|
5320
|
-
return
|
|
5998
|
+
function isImportEdge(edge2) {
|
|
5999
|
+
return edge2.kind === "imports" || edge2.kind === "re-exports";
|
|
5321
6000
|
}
|
|
5322
6001
|
function longestPrefixMatcher(prefixes) {
|
|
5323
6002
|
const byLength = [...prefixes].sort((a, b) => b.length - a.length);
|
|
@@ -5340,11 +6019,17 @@ function layerIndex(layers) {
|
|
|
5340
6019
|
function runLayeredDepsRule(rule, ctx) {
|
|
5341
6020
|
const packageLayer = layerIndex(rule.layers);
|
|
5342
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
|
+
};
|
|
5343
6027
|
const out = [];
|
|
5344
|
-
for (const
|
|
5345
|
-
if (!isImportEdge(
|
|
5346
|
-
|
|
5347
|
-
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);
|
|
5348
6033
|
if (srcPkg === null || dstPkg === null) continue;
|
|
5349
6034
|
const srcLayer = packageLayer.get(srcPkg);
|
|
5350
6035
|
const dstLayer = packageLayer.get(dstPkg);
|
|
@@ -5352,9 +6037,9 @@ function runLayeredDepsRule(rule, ctx) {
|
|
|
5352
6037
|
out.push({
|
|
5353
6038
|
ruleId: rule.id,
|
|
5354
6039
|
severity: severityOf(rule),
|
|
5355
|
-
nodeId:
|
|
5356
|
-
destinationId:
|
|
5357
|
-
message: `${srcPkg}
|
|
6040
|
+
nodeId: edge2.srcId,
|
|
6041
|
+
destinationId: edge2.dstId,
|
|
6042
|
+
message: `${edge2.srcId} (${srcPkg}, layer ${srcLayer}) imports ${edge2.dstId} (${dstPkg}, layer ${dstLayer})`
|
|
5358
6043
|
});
|
|
5359
6044
|
}
|
|
5360
6045
|
return out;
|
|
@@ -5362,31 +6047,32 @@ function runLayeredDepsRule(rule, ctx) {
|
|
|
5362
6047
|
function runForbidImportRule(rule, ctx) {
|
|
5363
6048
|
const fromRx = compilePatterns([rule.from]);
|
|
5364
6049
|
const toRx = compilePatterns([rule.to]);
|
|
6050
|
+
const exceptRx = compilePatterns(rule.except);
|
|
5365
6051
|
const out = [];
|
|
5366
|
-
for (const
|
|
5367
|
-
if (!isImportEdge(
|
|
5368
|
-
if (!matchesAny(
|
|
5369
|
-
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;
|
|
5370
6056
|
out.push({
|
|
5371
6057
|
ruleId: rule.id,
|
|
5372
6058
|
severity: severityOf(rule),
|
|
5373
|
-
nodeId:
|
|
5374
|
-
destinationId:
|
|
5375
|
-
message: `${
|
|
6059
|
+
nodeId: edge2.srcId,
|
|
6060
|
+
destinationId: edge2.dstId,
|
|
6061
|
+
message: `${edge2.srcId} imports ${edge2.dstId} (forbidden: ${rule.from} \u2192 ${rule.to})`
|
|
5376
6062
|
});
|
|
5377
6063
|
}
|
|
5378
6064
|
return out;
|
|
5379
6065
|
}
|
|
5380
6066
|
function importersByDestination(edges) {
|
|
5381
6067
|
const importersByDst = /* @__PURE__ */ new Map();
|
|
5382
|
-
for (const
|
|
5383
|
-
if (!isImportEdge(
|
|
5384
|
-
let list = importersByDst.get(
|
|
6068
|
+
for (const edge2 of edges) {
|
|
6069
|
+
if (!isImportEdge(edge2)) continue;
|
|
6070
|
+
let list = importersByDst.get(edge2.dstId);
|
|
5385
6071
|
if (!list) {
|
|
5386
6072
|
list = [];
|
|
5387
|
-
importersByDst.set(
|
|
6073
|
+
importersByDst.set(edge2.dstId, list);
|
|
5388
6074
|
}
|
|
5389
|
-
list.push(
|
|
6075
|
+
list.push(edge2.srcId);
|
|
5390
6076
|
}
|
|
5391
6077
|
return importersByDst;
|
|
5392
6078
|
}
|
|
@@ -5621,13 +6307,6 @@ function collectBaselineKeys(store, baselineSnapshotId, snapshotId, rules) {
|
|
|
5621
6307
|
}
|
|
5622
6308
|
return keys;
|
|
5623
6309
|
}
|
|
5624
|
-
function violationKey(v) {
|
|
5625
|
-
return v.destinationId ? `${v.ruleId}|${v.nodeId}|${v.destinationId}` : `${v.ruleId}|${v.nodeId}`;
|
|
5626
|
-
}
|
|
5627
|
-
function rebasedViolationKey(v, resolve4) {
|
|
5628
|
-
const destinationId = v.destinationId ? resolve4(v.destinationId) : v.destinationId;
|
|
5629
|
-
return violationKey({ ...v, nodeId: resolve4(v.nodeId), destinationId });
|
|
5630
|
-
}
|
|
5631
6310
|
function countByOriginAndSeverity(violations) {
|
|
5632
6311
|
const counts = { newErrors: 0, newWarnings: 0, carryoverErrors: 0, carryoverWarnings: 0 };
|
|
5633
6312
|
for (const v of violations) {
|
|
@@ -5845,7 +6524,7 @@ function validateRule(raw, index, warn) {
|
|
|
5845
6524
|
case "forbid-import":
|
|
5846
6525
|
return assertForbidImport(r);
|
|
5847
6526
|
case "layered-deps":
|
|
5848
|
-
return assertLayeredDeps(r);
|
|
6527
|
+
return assertLayeredDeps(r, warn);
|
|
5849
6528
|
case "no-internal-only-barrels":
|
|
5850
6529
|
return assertNoInternalOnlyBarrels(r);
|
|
5851
6530
|
default:
|
|
@@ -5867,11 +6546,11 @@ function assertNoInternalOnlyBarrels(r) {
|
|
|
5867
6546
|
type: "no-internal-only-barrels",
|
|
5868
6547
|
id: r.id,
|
|
5869
6548
|
packageRoots: r.packageRoots,
|
|
5870
|
-
severity: r
|
|
5871
|
-
exclude:
|
|
6549
|
+
severity: parseSeverity(r),
|
|
6550
|
+
exclude: parseExclude(r)
|
|
5872
6551
|
};
|
|
5873
6552
|
}
|
|
5874
|
-
function assertLayeredDeps(r) {
|
|
6553
|
+
function assertLayeredDeps(r, warn) {
|
|
5875
6554
|
if (!Array.isArray(r.layers) || r.layers.length < 2) {
|
|
5876
6555
|
throw new Error(`${r.id}: layers must be an array of 2+ string arrays`);
|
|
5877
6556
|
}
|
|
@@ -5896,7 +6575,8 @@ function assertLayeredDeps(r) {
|
|
|
5896
6575
|
type: "layered-deps",
|
|
5897
6576
|
id: r.id,
|
|
5898
6577
|
layers: r.layers,
|
|
5899
|
-
severity: r
|
|
6578
|
+
severity: parseSeverity(r),
|
|
6579
|
+
excludeRoles: parseRoleArray(r.id, r.excludeRoles, warn)
|
|
5900
6580
|
};
|
|
5901
6581
|
}
|
|
5902
6582
|
function assertMetricProductMax(r, warn) {
|
|
@@ -5915,9 +6595,9 @@ function assertMetricProductMax(r, warn) {
|
|
|
5915
6595
|
id: ruleId,
|
|
5916
6596
|
metrics: r.metrics.map((m) => healMetricName(m, ruleId, warn)),
|
|
5917
6597
|
max: r.max,
|
|
5918
|
-
kind: r
|
|
5919
|
-
severity: r
|
|
5920
|
-
exclude:
|
|
6598
|
+
kind: parseOptionalKind(r),
|
|
6599
|
+
severity: parseSeverity(r),
|
|
6600
|
+
exclude: parseExclude(r),
|
|
5921
6601
|
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
5922
6602
|
};
|
|
5923
6603
|
}
|
|
@@ -5930,9 +6610,9 @@ function assertMetricMax(r, warn) {
|
|
|
5930
6610
|
id: ruleId,
|
|
5931
6611
|
metric: healMetricName(r.metric, ruleId, warn),
|
|
5932
6612
|
max: r.max,
|
|
5933
|
-
kind: r
|
|
5934
|
-
severity: r
|
|
5935
|
-
exclude:
|
|
6613
|
+
kind: parseOptionalKind(r),
|
|
6614
|
+
severity: parseSeverity(r),
|
|
6615
|
+
exclude: parseExclude(r),
|
|
5936
6616
|
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
5937
6617
|
};
|
|
5938
6618
|
}
|
|
@@ -5945,18 +6625,16 @@ function assertMetricMin(r, warn) {
|
|
|
5945
6625
|
id: ruleId,
|
|
5946
6626
|
metric: healMetricName(r.metric, ruleId, warn),
|
|
5947
6627
|
min: r.min,
|
|
5948
|
-
kind: r
|
|
5949
|
-
severity: r
|
|
5950
|
-
exclude:
|
|
6628
|
+
kind: parseOptionalKind(r),
|
|
6629
|
+
severity: parseSeverity(r),
|
|
6630
|
+
exclude: parseExclude(r),
|
|
5951
6631
|
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
5952
6632
|
};
|
|
5953
6633
|
}
|
|
5954
6634
|
var NODE_KINDS = /* @__PURE__ */ new Set(["package", "module", "file", "symbol", "external"]);
|
|
5955
6635
|
function assertMetricOutlier(r, warn) {
|
|
5956
6636
|
if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
|
|
5957
|
-
if (
|
|
5958
|
-
throw new Error(`${r.id}: kind must be one of ${[...NODE_KINDS].join(", ")}`);
|
|
5959
|
-
}
|
|
6637
|
+
if (!isNodeKind(r.kind)) throw kindError(r.id);
|
|
5960
6638
|
if (typeof r.percentile !== "number" || r.percentile < 50 || r.percentile > 100) {
|
|
5961
6639
|
throw new Error(`${r.id}: percentile must be a number from 50 to 100`);
|
|
5962
6640
|
}
|
|
@@ -5979,7 +6657,7 @@ function assertMetricOutlier(r, warn) {
|
|
|
5979
6657
|
minSample: r.minSample,
|
|
5980
6658
|
floor: r.floor,
|
|
5981
6659
|
rankNonZero: r.rankNonZero,
|
|
5982
|
-
severity: r
|
|
6660
|
+
severity: parseSeverity(r)
|
|
5983
6661
|
};
|
|
5984
6662
|
}
|
|
5985
6663
|
function assertForbidImport(r) {
|
|
@@ -5990,15 +6668,43 @@ function assertForbidImport(r) {
|
|
|
5990
6668
|
id: r.id,
|
|
5991
6669
|
from: r.from,
|
|
5992
6670
|
to: r.to,
|
|
5993
|
-
|
|
6671
|
+
except: parseStringList(r, "except"),
|
|
6672
|
+
severity: parseSeverity(r)
|
|
5994
6673
|
};
|
|
5995
6674
|
}
|
|
5996
|
-
function
|
|
5997
|
-
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;
|
|
5998
6702
|
}
|
|
5999
6703
|
var ROLE_VALUES = /* @__PURE__ */ new Set([
|
|
6000
6704
|
"test",
|
|
6001
6705
|
"fixture",
|
|
6706
|
+
"story",
|
|
6707
|
+
"lab",
|
|
6002
6708
|
"barrel",
|
|
6003
6709
|
"types",
|
|
6004
6710
|
"config",
|
|
@@ -6029,7 +6735,7 @@ function healRole(entry, ruleId, warn) {
|
|
|
6029
6735
|
}
|
|
6030
6736
|
|
|
6031
6737
|
// src/check/run.ts
|
|
6032
|
-
import { readFile
|
|
6738
|
+
import { readFile } from "fs/promises";
|
|
6033
6739
|
function resolveSnapshot(store, spec) {
|
|
6034
6740
|
if (typeof spec === "number" || /^\d+$/.test(spec)) {
|
|
6035
6741
|
const snap2 = store.getSnapshot(Number(spec));
|
|
@@ -6050,18 +6756,18 @@ function checkSnapshot(store, options) {
|
|
|
6050
6756
|
});
|
|
6051
6757
|
return { snapshot, baselineSnapshot, result };
|
|
6052
6758
|
}
|
|
6053
|
-
async function loadCheckRules(
|
|
6759
|
+
async function loadCheckRules(path14, options = {}) {
|
|
6054
6760
|
let raw;
|
|
6055
6761
|
try {
|
|
6056
|
-
raw = await
|
|
6762
|
+
raw = await readFile(path14, "utf8");
|
|
6057
6763
|
} catch (err) {
|
|
6058
|
-
throw new Error(`Cannot read rules file at ${
|
|
6764
|
+
throw new Error(`Cannot read rules file at ${path14}: ${errorMessage(err)}`);
|
|
6059
6765
|
}
|
|
6060
6766
|
let parsed;
|
|
6061
6767
|
try {
|
|
6062
6768
|
parsed = JSON.parse(raw);
|
|
6063
6769
|
} catch (err) {
|
|
6064
|
-
throw new Error(`Invalid JSON in ${
|
|
6770
|
+
throw new Error(`Invalid JSON in ${path14}: ${errorMessage(err)}`);
|
|
6065
6771
|
}
|
|
6066
6772
|
return validateRules(parsed, options);
|
|
6067
6773
|
}
|
|
@@ -6191,44 +6897,16 @@ function diffCheckResults(store, options) {
|
|
|
6191
6897
|
fromSnapshotId: options.fromSnapshotId,
|
|
6192
6898
|
toSnapshotId: options.toSnapshotId,
|
|
6193
6899
|
rulesEvaluated: options.rules.length,
|
|
6194
|
-
...bucketViolations(
|
|
6900
|
+
...bucketViolations(from.violations, to.violations, chain.resolve)
|
|
6195
6901
|
};
|
|
6196
6902
|
}
|
|
6197
|
-
function bucketViolations(fromByKey, toByKey) {
|
|
6198
|
-
const buckets = { newViolations: [], resolvedViolations: [], unchanged: [], worsened: [], improved: [] };
|
|
6199
|
-
for (const [key, toV] of toByKey) {
|
|
6200
|
-
const fromV = fromByKey.get(key);
|
|
6201
|
-
if (!fromV) {
|
|
6202
|
-
buckets.newViolations.push(toV);
|
|
6203
|
-
continue;
|
|
6204
|
-
}
|
|
6205
|
-
const delta = computeDelta(fromV, toV);
|
|
6206
|
-
const entry = { from: fromV, to: toV, delta };
|
|
6207
|
-
buckets.unchanged.push(entry);
|
|
6208
|
-
if (delta !== null && delta > 0) buckets.worsened.push(entry);
|
|
6209
|
-
else if (delta !== null && delta < 0) buckets.improved.push(entry);
|
|
6210
|
-
}
|
|
6211
|
-
for (const [key, fromV] of fromByKey) {
|
|
6212
|
-
if (!toByKey.has(key)) buckets.resolvedViolations.push(fromV);
|
|
6213
|
-
}
|
|
6214
|
-
return buckets;
|
|
6215
|
-
}
|
|
6216
|
-
function indexByKey(violations, resolve4) {
|
|
6217
|
-
const out = /* @__PURE__ */ new Map();
|
|
6218
|
-
for (const v of violations) out.set(resolve4 ? rebasedViolationKey(v, resolve4) : violationKey(v), v);
|
|
6219
|
-
return out;
|
|
6220
|
-
}
|
|
6221
|
-
function computeDelta(from, to) {
|
|
6222
|
-
if (from.value === void 0 || to.value === void 0) return null;
|
|
6223
|
-
return to.value - from.value;
|
|
6224
|
-
}
|
|
6225
6903
|
|
|
6226
6904
|
// src/diff/footprint.ts
|
|
6227
|
-
var
|
|
6905
|
+
var DEFAULT_MIN_CO_IMPORTS = 2;
|
|
6228
6906
|
function computeFootprints(graph, options = {}) {
|
|
6229
6907
|
const edges = referenceEdges(graph.edges, options.ignoreConsumer);
|
|
6230
6908
|
const consumers = consumersBySymbol(edges);
|
|
6231
|
-
const partners = partnersBySymbol(edges, options.minCoImports ??
|
|
6909
|
+
const partners = partnersBySymbol(edges, options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS);
|
|
6232
6910
|
const out = /* @__PURE__ */ new Map();
|
|
6233
6911
|
for (const node of graph.nodes) {
|
|
6234
6912
|
if (node.kind !== "symbol") continue;
|
|
@@ -6284,18 +6962,18 @@ function partnersBySymbol(edges, minCoImports) {
|
|
|
6284
6962
|
var PART_REASONS = ["signature", "consumers", "coupling"];
|
|
6285
6963
|
function diffFootprints(store, options) {
|
|
6286
6964
|
const { fromSnapshotId, toSnapshotId } = options;
|
|
6287
|
-
const
|
|
6965
|
+
const resolve5 = symbolAwareResolver(aliasChain(store, fromSnapshotId, toSnapshotId));
|
|
6288
6966
|
const fromGraph = loadSymbolLayer(store, fromSnapshotId);
|
|
6289
6967
|
const toGraph = loadSymbolLayer(store, toSnapshotId);
|
|
6290
|
-
const { previousIds, mergedAway } = mergeSources(fromGraph.nodes,
|
|
6291
|
-
const before = computeFootprints(remapGraph(fromGraph,
|
|
6968
|
+
const { previousIds, mergedAway } = mergeSources(fromGraph.nodes, resolve5);
|
|
6969
|
+
const before = computeFootprints(remapGraph(fromGraph, resolve5, new Set(mergedAway)), options);
|
|
6292
6970
|
const after = computeFootprints(toGraph, options);
|
|
6293
6971
|
const changes = [
|
|
6294
6972
|
...changedOrAdded(after, before, previousIds),
|
|
6295
|
-
...removed(before, after, previousIds,
|
|
6296
|
-
...mergedAway.map((id) => ({ symbolId: id, status: "removed", reasons: [], fileId:
|
|
6297
|
-
].sort((a, b) =>
|
|
6298
|
-
const files = [...new Set(changes.map((c) => c.fileId))].sort(
|
|
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);
|
|
6299
6977
|
return { fromSnapshotId, toSnapshotId, changes, files };
|
|
6300
6978
|
}
|
|
6301
6979
|
function loadSymbolLayer(store, snapshotId) {
|
|
@@ -6312,22 +6990,22 @@ function symbolAwareResolver(aliases) {
|
|
|
6312
6990
|
return parsed ? symbolId(aliases.resolve(parsed.fileId), parsed.name) : id;
|
|
6313
6991
|
};
|
|
6314
6992
|
}
|
|
6315
|
-
function remapGraph(graph,
|
|
6993
|
+
function remapGraph(graph, resolve5, losers) {
|
|
6316
6994
|
return {
|
|
6317
|
-
nodes: graph.nodes.filter((n) => !losers.has(n.id)).map((n) => ({ ...n, id:
|
|
6318
|
-
edges: graph.edges.filter((e) => !losers.has(e.srcId) && !losers.has(e.dstId)).map((e) => ({ ...e, srcId:
|
|
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) }))
|
|
6319
6997
|
};
|
|
6320
6998
|
}
|
|
6321
|
-
function mergeSources(nodes,
|
|
6999
|
+
function mergeSources(nodes, resolve5) {
|
|
6322
7000
|
const groups = /* @__PURE__ */ new Map();
|
|
6323
7001
|
for (const node of nodes) {
|
|
6324
|
-
const id =
|
|
7002
|
+
const id = resolve5(node.id);
|
|
6325
7003
|
groups.set(id, [...groups.get(id) ?? [], node.id]);
|
|
6326
7004
|
}
|
|
6327
7005
|
const previousIds = /* @__PURE__ */ new Map();
|
|
6328
7006
|
const mergedAway = [];
|
|
6329
7007
|
for (const [id, sources] of groups) {
|
|
6330
|
-
const keeper = sources.includes(id) ? id : [...sources].sort(
|
|
7008
|
+
const keeper = sources.includes(id) ? id : [...sources].sort(compare3)[0];
|
|
6331
7009
|
if (keeper !== id) previousIds.set(id, keeper);
|
|
6332
7010
|
mergedAway.push(...sources.filter((source) => source !== keeper));
|
|
6333
7011
|
}
|
|
@@ -6353,19 +7031,19 @@ function reasonsFor(prior, current, renamed) {
|
|
|
6353
7031
|
if (renamed) reasons.push("renamed");
|
|
6354
7032
|
return reasons;
|
|
6355
7033
|
}
|
|
6356
|
-
function removed(before, after, previousIds,
|
|
7034
|
+
function removed(before, after, previousIds, resolve5) {
|
|
6357
7035
|
const out = [];
|
|
6358
7036
|
for (const id of before.keys()) {
|
|
6359
7037
|
if (after.has(id)) continue;
|
|
6360
7038
|
const fromId = previousIds.get(id) ?? id;
|
|
6361
|
-
out.push({ symbolId: fromId, status: "removed", reasons: [], fileId:
|
|
7039
|
+
out.push({ symbolId: fromId, status: "removed", reasons: [], fileId: resolve5(declaringFile(fromId)) });
|
|
6362
7040
|
}
|
|
6363
7041
|
return out;
|
|
6364
7042
|
}
|
|
6365
7043
|
function declaringFile(id) {
|
|
6366
7044
|
return parseSymbolId(id)?.fileId ?? id;
|
|
6367
7045
|
}
|
|
6368
|
-
function
|
|
7046
|
+
function compare3(a, b) {
|
|
6369
7047
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
6370
7048
|
}
|
|
6371
7049
|
|
|
@@ -6463,17 +7141,17 @@ function storeVectors(store, model, rows) {
|
|
|
6463
7141
|
async function embedTextsCached(store, embedder, texts) {
|
|
6464
7142
|
const hashes = texts.map((t) => hashEmbedText(t));
|
|
6465
7143
|
const byHash = loadVectorsByHash(store, embedder.model, hashes);
|
|
6466
|
-
const
|
|
7144
|
+
const missing2 = /* @__PURE__ */ new Map();
|
|
6467
7145
|
texts.forEach((text, i) => {
|
|
6468
|
-
if (!byHash.has(hashes[i]))
|
|
7146
|
+
if (!byHash.has(hashes[i])) missing2.set(hashes[i], text);
|
|
6469
7147
|
});
|
|
6470
|
-
const pending = [...
|
|
7148
|
+
const pending = [...missing2.entries()];
|
|
6471
7149
|
for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
|
|
6472
7150
|
const rows = await embedBatch(embedder, pending.slice(i, i + EMBED_BATCH_SIZE));
|
|
6473
7151
|
storeVectors(store, embedder.model, rows);
|
|
6474
7152
|
for (const r of rows) byHash.set(r.textHash, r.vector);
|
|
6475
7153
|
}
|
|
6476
|
-
return { byHash, newHashes: new Set(
|
|
7154
|
+
return { byHash, newHashes: new Set(missing2.keys()) };
|
|
6477
7155
|
}
|
|
6478
7156
|
async function embedBatch(embedder, batch) {
|
|
6479
7157
|
const vectors = await embedder.embed(batch.map(([, text]) => text), { role: "document" });
|
|
@@ -6643,7 +7321,7 @@ function pushMulti(map, key, value) {
|
|
|
6643
7321
|
}
|
|
6644
7322
|
|
|
6645
7323
|
// src/conventions/areas.ts
|
|
6646
|
-
import * as
|
|
7324
|
+
import * as path13 from "path";
|
|
6647
7325
|
import { contentHashOf as contentHashOf2 } from "@titan-design/store-sqlite";
|
|
6648
7326
|
var DEFAULT_MIN_SIZE = 3;
|
|
6649
7327
|
var PROMPT_FILE_CAP = 30;
|
|
@@ -6727,7 +7405,7 @@ function rankByDegree(members, edges) {
|
|
|
6727
7405
|
function dominantDirectory(files) {
|
|
6728
7406
|
const counts = /* @__PURE__ */ new Map();
|
|
6729
7407
|
for (const f of files) {
|
|
6730
|
-
const dir =
|
|
7408
|
+
const dir = path13.posix.dirname(f);
|
|
6731
7409
|
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
6732
7410
|
}
|
|
6733
7411
|
const dirs = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
@@ -6847,14 +7525,21 @@ function toMatch(area, score) {
|
|
|
6847
7525
|
export {
|
|
6848
7526
|
ALIAS_BASE_ATTR,
|
|
6849
7527
|
ALL_ROLES,
|
|
7528
|
+
BUNDLE_SCHEMA_VERSION,
|
|
6850
7529
|
COMMUNITY_SUMMARY_NAMESPACE,
|
|
7530
|
+
SCHEMA_VERSION2 as CONTEXT_SCHEMA_VERSION,
|
|
6851
7531
|
COVERAGE_METRIC_NAME,
|
|
6852
7532
|
CodeGraphStore,
|
|
6853
7533
|
DEAD_CODE_METRIC_NAMES,
|
|
6854
7534
|
DEFAULT_CHURN_WINDOWS,
|
|
7535
|
+
DEFAULT_HEALTH_WEIGHTS,
|
|
7536
|
+
DEFAULT_MAX_LINE_CHARS,
|
|
7537
|
+
DEFAULT_MAX_PACKAGE_SIZE,
|
|
6855
7538
|
DEFAULT_OUTLIER_MIN_SAMPLE,
|
|
6856
7539
|
DOMAIN_DDL,
|
|
7540
|
+
ELISION_MARKER,
|
|
6857
7541
|
EXCEPTION_METRIC_NAMES,
|
|
7542
|
+
EXTERNAL_BUCKET,
|
|
6858
7543
|
FINDING_DDL,
|
|
6859
7544
|
GROWTH_RISK_METRIC_NAMES,
|
|
6860
7545
|
INDEX_VERSION,
|
|
@@ -6869,35 +7554,55 @@ export {
|
|
|
6869
7554
|
SYMBOL_EMBEDDING_NAMESPACE,
|
|
6870
7555
|
SYMBOL_ID_SEP,
|
|
6871
7556
|
SYMBOL_METRIC_NAMES,
|
|
7557
|
+
TRUNCATION_SUFFIX,
|
|
6872
7558
|
TsMorphGraphExtractor,
|
|
7559
|
+
UNIMPORTED_ROLES,
|
|
7560
|
+
aggregateEdges,
|
|
6873
7561
|
aggregateMetrics,
|
|
6874
7562
|
aliasChain,
|
|
6875
7563
|
annotateRoles,
|
|
6876
7564
|
attributeCoverage,
|
|
7565
|
+
bucketFilesByPackage,
|
|
7566
|
+
bucketViolations,
|
|
6877
7567
|
buildAliases,
|
|
7568
|
+
buildBlastRadius,
|
|
7569
|
+
buildCentralFiles,
|
|
7570
|
+
buildContextBundle,
|
|
7571
|
+
buildContextDossier,
|
|
6878
7572
|
buildConventionAreas,
|
|
6879
7573
|
buildEmbedText,
|
|
6880
7574
|
buildFileModuleNodes,
|
|
7575
|
+
buildHotExports,
|
|
6881
7576
|
buildIndexerMetrics,
|
|
6882
7577
|
buildLineage,
|
|
7578
|
+
buildNodeMetrics,
|
|
7579
|
+
buildReportContext,
|
|
7580
|
+
buildSymbolCouplingPayload,
|
|
7581
|
+
busFactorOf,
|
|
6883
7582
|
canonicalEdgeKind,
|
|
6884
7583
|
canonicalMetricName,
|
|
6885
7584
|
canonicalRole,
|
|
6886
7585
|
carryForwardVerdicts,
|
|
6887
7586
|
checkSnapshot,
|
|
7587
|
+
classifyCoupling,
|
|
6888
7588
|
classifyRole,
|
|
6889
7589
|
collectDeclaredNames,
|
|
6890
7590
|
collectDeclaredSpans,
|
|
7591
|
+
collectNodeMetrics,
|
|
7592
|
+
collectSymbolUtil,
|
|
6891
7593
|
compilePatterns,
|
|
7594
|
+
computeArch,
|
|
6892
7595
|
computeDeadCodeMetrics,
|
|
6893
7596
|
computeDeepAst,
|
|
6894
7597
|
computeFootprints,
|
|
6895
7598
|
computeGrowthRiskMetrics,
|
|
7599
|
+
computeHealth,
|
|
6896
7600
|
computeMetrics,
|
|
6897
7601
|
computePageRank,
|
|
6898
7602
|
computePartitionQuality,
|
|
6899
7603
|
computeRecencyWindows,
|
|
6900
7604
|
computeRelevance,
|
|
7605
|
+
computeReportDrift,
|
|
6901
7606
|
computeRoleHints,
|
|
6902
7607
|
computeSourceMetrics,
|
|
6903
7608
|
computeSymbolConsumers,
|
|
@@ -6920,6 +7625,7 @@ export {
|
|
|
6920
7625
|
externalId,
|
|
6921
7626
|
externalToFinding,
|
|
6922
7627
|
fileId,
|
|
7628
|
+
filteredFileIds,
|
|
6923
7629
|
findConventions,
|
|
6924
7630
|
findSimilarCapability,
|
|
6925
7631
|
findingKey,
|
|
@@ -6928,15 +7634,20 @@ export {
|
|
|
6928
7634
|
getEdgeWeight,
|
|
6929
7635
|
getLanguageFromPath3 as getLanguageFromPath,
|
|
6930
7636
|
getSupportedLanguages,
|
|
7637
|
+
gitTreeSource,
|
|
6931
7638
|
groupTestsBySource,
|
|
6932
7639
|
hashContent,
|
|
6933
7640
|
hashEmbedText,
|
|
6934
7641
|
hashText,
|
|
7642
|
+
hotspotScoreOf,
|
|
7643
|
+
importMarker,
|
|
6935
7644
|
indexPaths,
|
|
6936
7645
|
invertBuckets,
|
|
6937
7646
|
isGeneratedByHeuristic,
|
|
6938
7647
|
isGeneratedFile,
|
|
6939
7648
|
isInsideGitRepo,
|
|
7649
|
+
isMinifyLanguage,
|
|
7650
|
+
keepNode,
|
|
6940
7651
|
keyFindings,
|
|
6941
7652
|
lineagePath,
|
|
6942
7653
|
linkTestsToSources,
|
|
@@ -6952,21 +7663,32 @@ export {
|
|
|
6952
7663
|
loadGeneratedPatterns,
|
|
6953
7664
|
loadHistoryMetrics,
|
|
6954
7665
|
loadLineage,
|
|
7666
|
+
loadRoleGlobs,
|
|
7667
|
+
lookupMetric,
|
|
6955
7668
|
matchesAny,
|
|
7669
|
+
minifySource,
|
|
6956
7670
|
moduleId,
|
|
6957
7671
|
normalizeFlaggedText,
|
|
6958
7672
|
openCodeGraph,
|
|
7673
|
+
originalLine,
|
|
6959
7674
|
packageId,
|
|
7675
|
+
packagesReferencedByEdges,
|
|
7676
|
+
pairKey,
|
|
6960
7677
|
parentModuleId,
|
|
6961
|
-
|
|
7678
|
+
parseFile3 as parseFile,
|
|
6962
7679
|
parseSymbolId,
|
|
6963
7680
|
patternToRegex,
|
|
6964
7681
|
percentileOf,
|
|
6965
7682
|
planPrune,
|
|
6966
7683
|
priorSnapshotForRef,
|
|
6967
7684
|
pruneDanglingReferences,
|
|
7685
|
+
publicApiFiles,
|
|
6968
7686
|
readSourceFiles,
|
|
6969
7687
|
rebasedViolationKey,
|
|
7688
|
+
referencedNodes,
|
|
7689
|
+
renderBundleText,
|
|
7690
|
+
renderContextMarkdown,
|
|
7691
|
+
renderSignatureTree,
|
|
6970
7692
|
resolveAlias,
|
|
6971
7693
|
resolveBarrelEdges,
|
|
6972
7694
|
resolveChurnWindows,
|
|
@@ -6976,7 +7698,7 @@ export {
|
|
|
6976
7698
|
runPrune,
|
|
6977
7699
|
saveFindings,
|
|
6978
7700
|
saveVerdicts,
|
|
6979
|
-
|
|
7701
|
+
shouldIncludeFile3 as shouldIncludeFile,
|
|
6980
7702
|
snapshotPageRank,
|
|
6981
7703
|
snapshotReferenceEdges,
|
|
6982
7704
|
snapshotRelevance,
|
|
@@ -6989,11 +7711,21 @@ export {
|
|
|
6989
7711
|
symbolSetHash,
|
|
6990
7712
|
testCoverageCountMetrics,
|
|
6991
7713
|
toFindings,
|
|
7714
|
+
toSortedEdges,
|
|
7715
|
+
topBusFactorRisks,
|
|
7716
|
+
topCentralFiles,
|
|
7717
|
+
topDeadModules,
|
|
7718
|
+
topGrowthRisks,
|
|
7719
|
+
topHotspots,
|
|
7720
|
+
topTestCoverageRisks,
|
|
7721
|
+
topUntestedRisks,
|
|
7722
|
+
topUnusedExports,
|
|
6992
7723
|
tryEmbedSnapshot,
|
|
6993
7724
|
unitProvenance,
|
|
6994
7725
|
validateRules,
|
|
6995
7726
|
violationKey,
|
|
6996
7727
|
walkSourceFiles,
|
|
6997
|
-
windowSuffix
|
|
7728
|
+
windowSuffix,
|
|
7729
|
+
workingTreeSource
|
|
6998
7730
|
};
|
|
6999
7731
|
//# sourceMappingURL=index.js.map
|