@titan-design/code-graph 0.1.0 → 0.2.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 +209 -15
- package/dist/change-coupling-CyqHgRsm.d.ts +73 -0
- package/dist/chunk-PFI5XMUG.js +332 -0
- package/dist/chunk-PFI5XMUG.js.map +1 -0
- package/dist/history/index.d.ts +56 -0
- package/dist/history/index.js +29 -0
- package/dist/history/index.js.map +1 -0
- package/dist/index.d.ts +583 -38
- package/dist/index.js +1904 -216
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/dist/index.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
aggregateChurnWindows,
|
|
3
|
+
authorLinesByPath,
|
|
4
|
+
computeChangeCoupling,
|
|
5
|
+
computeOwnership,
|
|
6
|
+
couplingFor,
|
|
7
|
+
detectGitToplevel,
|
|
8
|
+
entriesWithin,
|
|
9
|
+
loadChurnEntries,
|
|
10
|
+
loadFileFirstSeen,
|
|
11
|
+
runGit,
|
|
12
|
+
summarizeOwnership
|
|
13
|
+
} from "./chunk-PFI5XMUG.js";
|
|
14
|
+
|
|
1
15
|
// src/store.ts
|
|
2
16
|
import { openDatabase, runMigrations } from "@titan-design/store-sqlite";
|
|
3
17
|
|
|
@@ -53,6 +67,7 @@ var MIGRATIONS = [
|
|
|
53
67
|
{ version: 2, name: "code graph columns", up: (db) => db.exec(KIT_EXTENSIONS) },
|
|
54
68
|
{ version: 3, name: "code graph tables", up: (db) => db.exec(DOMAIN_DDL) }
|
|
55
69
|
];
|
|
70
|
+
var SCHEMA_VERSION = Math.max(...MIGRATIONS.map((m) => m.version));
|
|
56
71
|
|
|
57
72
|
// src/rows.ts
|
|
58
73
|
function rowToSnapshot(row) {
|
|
@@ -270,126 +285,18 @@ function prepareStatements(db) {
|
|
|
270
285
|
};
|
|
271
286
|
}
|
|
272
287
|
function openCodeGraph(dbPath) {
|
|
273
|
-
const db = openDatabase(dbPath);
|
|
288
|
+
const db = openDatabase(dbPath, { schemaVersion: SCHEMA_VERSION });
|
|
274
289
|
runMigrations(db, MIGRATIONS);
|
|
275
290
|
return new CodeGraphStore(db);
|
|
276
291
|
}
|
|
277
292
|
|
|
278
293
|
// src/indexer.ts
|
|
279
294
|
import { realpathSync } from "fs";
|
|
280
|
-
import * as
|
|
281
|
-
|
|
282
|
-
// src/parser/parser.ts
|
|
283
|
-
import { Parser, Language } from "web-tree-sitter";
|
|
284
|
-
import { createRequire } from "module";
|
|
285
|
-
var require2 = createRequire(import.meta.url);
|
|
286
|
-
var initialized = false;
|
|
287
|
-
var parsers = /* @__PURE__ */ new Map();
|
|
288
|
-
var languages = /* @__PURE__ */ new Map();
|
|
289
|
-
function resolveWasmPath(language) {
|
|
290
|
-
switch (language) {
|
|
291
|
-
case "typescript":
|
|
292
|
-
return require2.resolve("tree-sitter-typescript/tree-sitter-typescript.wasm");
|
|
293
|
-
case "tsx":
|
|
294
|
-
return require2.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
|
|
295
|
-
case "python":
|
|
296
|
-
return require2.resolve("tree-sitter-python/tree-sitter-python.wasm");
|
|
297
|
-
default:
|
|
298
|
-
throw new Error(`Unsupported language: ${language}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
async function ensureInitialized() {
|
|
302
|
-
if (initialized) return;
|
|
303
|
-
await Parser.init();
|
|
304
|
-
initialized = true;
|
|
305
|
-
}
|
|
306
|
-
async function getLanguage(language) {
|
|
307
|
-
const cached = languages.get(language);
|
|
308
|
-
if (cached) return cached;
|
|
309
|
-
const wasmPath = resolveWasmPath(language);
|
|
310
|
-
const lang = await Language.load(wasmPath);
|
|
311
|
-
languages.set(language, lang);
|
|
312
|
-
return lang;
|
|
313
|
-
}
|
|
314
|
-
async function getParser(language) {
|
|
315
|
-
const cached = parsers.get(language);
|
|
316
|
-
if (cached) return cached;
|
|
317
|
-
resolveWasmPath(language);
|
|
318
|
-
await ensureInitialized();
|
|
319
|
-
const lang = await getLanguage(language);
|
|
320
|
-
const parser = new Parser();
|
|
321
|
-
parser.setLanguage(lang);
|
|
322
|
-
parsers.set(language, parser);
|
|
323
|
-
return parser;
|
|
324
|
-
}
|
|
325
|
-
async function parseFile(content, filePath, language) {
|
|
326
|
-
const parser = await getParser(language);
|
|
327
|
-
const tree = parser.parse(content);
|
|
328
|
-
if (!tree) {
|
|
329
|
-
throw new Error(`Failed to parse ${filePath}`);
|
|
330
|
-
}
|
|
331
|
-
return { tree, content, filePath, language };
|
|
332
|
-
}
|
|
333
|
-
function getSupportedLanguages() {
|
|
334
|
-
return ["typescript", "tsx", "python"];
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// src/parser/file-filter.ts
|
|
338
|
-
import * as path from "path";
|
|
339
|
-
var EXCLUDED_DIRS = [
|
|
340
|
-
"node_modules",
|
|
341
|
-
"vendor",
|
|
342
|
-
"dist",
|
|
343
|
-
"build",
|
|
344
|
-
".next",
|
|
345
|
-
"__generated__",
|
|
346
|
-
".git",
|
|
347
|
-
".claude",
|
|
348
|
-
"coverage"
|
|
349
|
-
];
|
|
350
|
-
var EXCLUDED_PATTERNS = [
|
|
351
|
-
/\.min\.[jt]sx?$/,
|
|
352
|
-
/\.d\.ts$/,
|
|
353
|
-
/\.map$/,
|
|
354
|
-
/lock\.(json|yaml)$/,
|
|
355
|
-
/pnpm-lock\.yaml$/,
|
|
356
|
-
/package-lock\.json$/,
|
|
357
|
-
/yarn\.lock$/
|
|
358
|
-
];
|
|
359
|
-
var LANGUAGE_EXTENSIONS = {
|
|
360
|
-
typescript: [".ts", ".tsx"],
|
|
361
|
-
javascript: [".js", ".jsx"],
|
|
362
|
-
python: [".py"]
|
|
363
|
-
};
|
|
364
|
-
function isExcludedDir(dirName) {
|
|
365
|
-
return EXCLUDED_DIRS.includes(dirName);
|
|
366
|
-
}
|
|
367
|
-
function shouldIncludeFile(filePath, languages2) {
|
|
368
|
-
const segments = filePath.split("/");
|
|
369
|
-
if (segments.some((s) => EXCLUDED_DIRS.includes(s))) {
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
if (EXCLUDED_PATTERNS.some((p) => p.test(filePath))) {
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
const ext = path.extname(filePath);
|
|
376
|
-
const allowedExtensions = languages2.flatMap(
|
|
377
|
-
(lang) => LANGUAGE_EXTENSIONS[lang] ?? []
|
|
378
|
-
);
|
|
379
|
-
return allowedExtensions.includes(ext);
|
|
380
|
-
}
|
|
381
|
-
function getLanguageFromPath(filePath) {
|
|
382
|
-
const ext = path.extname(filePath);
|
|
383
|
-
for (const [language, extensions] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
384
|
-
if (extensions.includes(ext)) {
|
|
385
|
-
return language;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
295
|
+
import * as path9 from "path";
|
|
296
|
+
import { parseFile } from "@titan-design/code-parser";
|
|
390
297
|
|
|
391
298
|
// src/extractors/ts-morph-extractor.ts
|
|
392
|
-
import * as
|
|
299
|
+
import * as path4 from "path";
|
|
393
300
|
import { existsSync } from "fs";
|
|
394
301
|
import {
|
|
395
302
|
Project,
|
|
@@ -399,16 +306,17 @@ import {
|
|
|
399
306
|
} from "ts-morph";
|
|
400
307
|
|
|
401
308
|
// src/extractors/file-nodes.ts
|
|
402
|
-
import * as
|
|
309
|
+
import * as path2 from "path";
|
|
310
|
+
import { getLanguageFromPath } from "@titan-design/code-parser";
|
|
403
311
|
|
|
404
312
|
// src/extractors/ids.ts
|
|
405
|
-
import * as
|
|
313
|
+
import * as path from "path";
|
|
406
314
|
var MODULE_EXT_RE = /\.(?:tsx|ts|jsx|js|mts|cts|mjs|cjs|py)$/;
|
|
407
315
|
function toPosix(p) {
|
|
408
|
-
return p.split(
|
|
316
|
+
return p.split(path.sep).join("/");
|
|
409
317
|
}
|
|
410
318
|
function repoRelative(repoRoot, absPath) {
|
|
411
|
-
const rel =
|
|
319
|
+
const rel = path.relative(repoRoot, absPath);
|
|
412
320
|
return toPosix(rel);
|
|
413
321
|
}
|
|
414
322
|
function fileId(repoRoot, absPath) {
|
|
@@ -427,8 +335,8 @@ function packageId(name) {
|
|
|
427
335
|
return name;
|
|
428
336
|
}
|
|
429
337
|
var SYMBOL_ID_SEP = "#";
|
|
430
|
-
function symbolId(fileId2,
|
|
431
|
-
return `${fileId2}${SYMBOL_ID_SEP}${
|
|
338
|
+
function symbolId(fileId2, qualifiedName) {
|
|
339
|
+
return `${fileId2}${SYMBOL_ID_SEP}${qualifiedName}`;
|
|
432
340
|
}
|
|
433
341
|
function parseSymbolId(id) {
|
|
434
342
|
const idx = id.indexOf(SYMBOL_ID_SEP);
|
|
@@ -457,8 +365,8 @@ function buildFileModuleNodes(repoRoot, absPath) {
|
|
|
457
365
|
const parentId = parentModuleId(mId) ?? void 0;
|
|
458
366
|
const language = getLanguageFromPath(absPath) ?? "typescript";
|
|
459
367
|
return [
|
|
460
|
-
{ id: fId, kind: "file", name:
|
|
461
|
-
{ id: mId, kind: "module", name:
|
|
368
|
+
{ id: fId, kind: "file", name: path2.basename(fId), parentId: mId, language },
|
|
369
|
+
{ id: mId, kind: "module", name: path2.basename(mId), parentId, language }
|
|
462
370
|
];
|
|
463
371
|
}
|
|
464
372
|
|
|
@@ -534,6 +442,65 @@ function isPropertyNamePosition(id) {
|
|
|
534
442
|
return false;
|
|
535
443
|
}
|
|
536
444
|
|
|
445
|
+
// src/scope-path.ts
|
|
446
|
+
var TS_NAMED_SCOPES = /* @__PURE__ */ new Set([
|
|
447
|
+
"class_declaration",
|
|
448
|
+
"abstract_class_declaration",
|
|
449
|
+
"class",
|
|
450
|
+
"function_declaration",
|
|
451
|
+
"generator_function_declaration",
|
|
452
|
+
"method_definition",
|
|
453
|
+
"internal_module"
|
|
454
|
+
]);
|
|
455
|
+
var TS_BOUND_SCOPES = /* @__PURE__ */ new Set([
|
|
456
|
+
"class",
|
|
457
|
+
"arrow_function",
|
|
458
|
+
"function_expression",
|
|
459
|
+
"generator_function",
|
|
460
|
+
"object"
|
|
461
|
+
]);
|
|
462
|
+
var PY_NAMED_SCOPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
|
|
463
|
+
var ANONYMOUS_SCOPE = "<anonymous>";
|
|
464
|
+
function qualify(scope, name) {
|
|
465
|
+
return scope ? `${scope}.${name}` : name;
|
|
466
|
+
}
|
|
467
|
+
function bindingName(node) {
|
|
468
|
+
const parent = node.parent;
|
|
469
|
+
if (!parent) return null;
|
|
470
|
+
if (parent.type === "variable_declarator" || parent.type === "public_field_definition") {
|
|
471
|
+
return parent.childForFieldName("name")?.text ?? null;
|
|
472
|
+
}
|
|
473
|
+
if (parent.type === "pair") return parent.childForFieldName("key")?.text ?? null;
|
|
474
|
+
if (parent.type === "export_statement") return "default";
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
function scopeSegment(node, python) {
|
|
478
|
+
if (python) {
|
|
479
|
+
return PY_NAMED_SCOPES.has(node.type) ? node.childForFieldName("name")?.text ?? null : null;
|
|
480
|
+
}
|
|
481
|
+
const own = TS_NAMED_SCOPES.has(node.type) ? node.childForFieldName("name")?.text : void 0;
|
|
482
|
+
if (own) return own;
|
|
483
|
+
if (!TS_BOUND_SCOPES.has(node.type)) return null;
|
|
484
|
+
return bindingName(node) ?? ANONYMOUS_SCOPE;
|
|
485
|
+
}
|
|
486
|
+
function enter(scope, segment) {
|
|
487
|
+
if (!segment) return scope;
|
|
488
|
+
if (segment === ANONYMOUS_SCOPE && (scope === ANONYMOUS_SCOPE || scope.endsWith(`.${ANONYMOUS_SCOPE}`))) {
|
|
489
|
+
return scope;
|
|
490
|
+
}
|
|
491
|
+
return qualify(scope, segment);
|
|
492
|
+
}
|
|
493
|
+
function walkScopes(root, python, visit) {
|
|
494
|
+
const step = (node, scope) => {
|
|
495
|
+
visit(node, scope);
|
|
496
|
+
const inner = enter(scope, scopeSegment(node, python));
|
|
497
|
+
for (const child of node.children) {
|
|
498
|
+
if (child) step(child, inner);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
step(root, "");
|
|
502
|
+
}
|
|
503
|
+
|
|
537
504
|
// src/declared-names.ts
|
|
538
505
|
var TS_DECL_TYPES = /* @__PURE__ */ new Set([
|
|
539
506
|
"function_declaration",
|
|
@@ -541,22 +508,24 @@ var TS_DECL_TYPES = /* @__PURE__ */ new Set([
|
|
|
541
508
|
"class_declaration"
|
|
542
509
|
]);
|
|
543
510
|
var PY_DECL_TYPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
|
|
511
|
+
function collectDeclarations(file) {
|
|
512
|
+
const python = file.language === "python";
|
|
513
|
+
const declTypes = python ? PY_DECL_TYPES : TS_DECL_TYPES;
|
|
514
|
+
const out = [];
|
|
515
|
+
walkScopes(file.tree.rootNode, python, (node, scope) => {
|
|
516
|
+
const named = declaredNodeAt(node, declTypes);
|
|
517
|
+
if (!named) return;
|
|
518
|
+
out.push({
|
|
519
|
+
name: named.name,
|
|
520
|
+
qualifiedName: qualify(scope, named.name),
|
|
521
|
+
span: { startLine: named.node.startPosition.row + 1, endLine: named.node.endPosition.row + 1 }
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
544
526
|
function collectDeclaredSpans(file) {
|
|
545
|
-
const declTypes = file.language === "python" ? PY_DECL_TYPES : TS_DECL_TYPES;
|
|
546
527
|
const spans = /* @__PURE__ */ new Map();
|
|
547
|
-
const
|
|
548
|
-
const named = declaredNodeAt(node, declTypes);
|
|
549
|
-
if (named) {
|
|
550
|
-
spans.set(named.name, {
|
|
551
|
-
startLine: named.node.startPosition.row + 1,
|
|
552
|
-
endLine: named.node.endPosition.row + 1
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
for (const child of node.children) {
|
|
556
|
-
if (child) visit(child);
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
visit(file.tree.rootNode);
|
|
528
|
+
for (const d of collectDeclarations(file)) spans.set(d.qualifiedName, d.span);
|
|
560
529
|
return spans;
|
|
561
530
|
}
|
|
562
531
|
function collectDeclaredNames(file) {
|
|
@@ -754,7 +723,7 @@ function recordDynamicSymbolRefEdges(sourceFile, srcFileId, agg, resolveSpecifie
|
|
|
754
723
|
}
|
|
755
724
|
|
|
756
725
|
// src/extractors/module-resolution.ts
|
|
757
|
-
import * as
|
|
726
|
+
import * as path3 from "path";
|
|
758
727
|
var RESOLVABLE_EXTS = [
|
|
759
728
|
".ts",
|
|
760
729
|
".tsx",
|
|
@@ -776,20 +745,20 @@ function* relativeResolutionCandidates(base) {
|
|
|
776
745
|
const stem = base.slice(0, base.length - jsExt[0].length);
|
|
777
746
|
for (const ext of RESOLVABLE_EXTS) yield stem + ext;
|
|
778
747
|
}
|
|
779
|
-
for (const ext of RESOLVABLE_EXTS) yield
|
|
748
|
+
for (const ext of RESOLVABLE_EXTS) yield path3.join(base, "index" + ext);
|
|
780
749
|
}
|
|
781
750
|
function resolveRelativeAbs(fs3, fromAbs, specifier) {
|
|
782
751
|
if (!isRelativeSpecifier(specifier)) return null;
|
|
783
|
-
const base =
|
|
752
|
+
const base = path3.resolve(path3.dirname(fromAbs), specifier);
|
|
784
753
|
for (const candidate of relativeResolutionCandidates(base)) {
|
|
785
754
|
if (fs3.fileExistsSync(candidate)) return candidate;
|
|
786
755
|
}
|
|
787
756
|
return null;
|
|
788
757
|
}
|
|
789
758
|
function inRepoFileId(repoRoot, abs) {
|
|
790
|
-
const relative4 =
|
|
791
|
-
if (relative4.startsWith("..") ||
|
|
792
|
-
if (relative4.split(
|
|
759
|
+
const relative4 = path3.relative(repoRoot, abs);
|
|
760
|
+
if (relative4.startsWith("..") || path3.isAbsolute(relative4)) return null;
|
|
761
|
+
if (relative4.split(path3.sep).includes("node_modules")) return null;
|
|
793
762
|
return fileId(repoRoot, abs);
|
|
794
763
|
}
|
|
795
764
|
|
|
@@ -1028,7 +997,7 @@ function remapDistToSrc(abs) {
|
|
|
1028
997
|
const base = m[1];
|
|
1029
998
|
const sub = m[2];
|
|
1030
999
|
for (const ext of [".ts", ".tsx"]) {
|
|
1031
|
-
const candidate =
|
|
1000
|
+
const candidate = path4.join(base, "src", sub + ext);
|
|
1032
1001
|
if (existsSync(candidate)) return candidate;
|
|
1033
1002
|
}
|
|
1034
1003
|
return abs;
|
|
@@ -1038,11 +1007,11 @@ function isTypeScriptFile(file) {
|
|
|
1038
1007
|
}
|
|
1039
1008
|
|
|
1040
1009
|
// src/extractors/python-extractor.ts
|
|
1041
|
-
import * as
|
|
1010
|
+
import * as path5 from "path";
|
|
1042
1011
|
import { existsSync as existsSync2 } from "fs";
|
|
1043
1012
|
function* candidates(base) {
|
|
1044
1013
|
yield `${base}.py`;
|
|
1045
|
-
yield
|
|
1014
|
+
yield path5.join(base, "__init__.py");
|
|
1046
1015
|
}
|
|
1047
1016
|
var PythonGraphExtractor = class {
|
|
1048
1017
|
constructor(repoRoot) {
|
|
@@ -1079,10 +1048,10 @@ var PythonGraphExtractor = class {
|
|
|
1079
1048
|
resolveDotted(fromAbs, specifier) {
|
|
1080
1049
|
const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
|
|
1081
1050
|
const parts = specifier.slice(leadingDots).split(".").filter(Boolean);
|
|
1082
|
-
const relativeBase =
|
|
1083
|
-
const roots = leadingDots > 0 ? [relativeBase] : [
|
|
1051
|
+
const relativeBase = path5.resolve(path5.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
|
|
1052
|
+
const roots = leadingDots > 0 ? [relativeBase] : [path5.dirname(fromAbs), this.repoRoot];
|
|
1084
1053
|
for (const root of roots) {
|
|
1085
|
-
for (const candidate of candidates(
|
|
1054
|
+
for (const candidate of candidates(path5.join(root, ...parts))) {
|
|
1086
1055
|
const id = inRepoFileId(this.repoRoot, candidate);
|
|
1087
1056
|
if (id !== null && existsSync2(candidate)) return id;
|
|
1088
1057
|
}
|
|
@@ -1090,6 +1059,9 @@ var PythonGraphExtractor = class {
|
|
|
1090
1059
|
return null;
|
|
1091
1060
|
}
|
|
1092
1061
|
};
|
|
1062
|
+
function isPublicName(qualifiedName) {
|
|
1063
|
+
return !qualifiedName.slice(qualifiedName.lastIndexOf(".") + 1).startsWith("_");
|
|
1064
|
+
}
|
|
1093
1065
|
function symbolNodes(fId, file) {
|
|
1094
1066
|
return [...collectDeclaredSpans(file)].map(([name, span]) => ({
|
|
1095
1067
|
id: symbolId(fId, name),
|
|
@@ -1097,7 +1069,7 @@ function symbolNodes(fId, file) {
|
|
|
1097
1069
|
name,
|
|
1098
1070
|
parentId: fId,
|
|
1099
1071
|
language: "python",
|
|
1100
|
-
attrs: { exported:
|
|
1072
|
+
attrs: { exported: isPublicName(name), startLine: span.startLine, endLine: span.endLine }
|
|
1101
1073
|
}));
|
|
1102
1074
|
}
|
|
1103
1075
|
function importSpecifiers(root) {
|
|
@@ -1130,17 +1102,13 @@ var LanguageExtractor = class {
|
|
|
1130
1102
|
};
|
|
1131
1103
|
|
|
1132
1104
|
// src/git-renames.ts
|
|
1133
|
-
import
|
|
1134
|
-
import * as path7 from "path";
|
|
1105
|
+
import * as path6 from "path";
|
|
1135
1106
|
function detectGitHead(repoRoot) {
|
|
1136
1107
|
return runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
1137
1108
|
}
|
|
1138
1109
|
function isInsideGitRepo(repoRoot) {
|
|
1139
1110
|
return runGit(repoRoot, ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
1140
1111
|
}
|
|
1141
|
-
function detectGitToplevel(cwd) {
|
|
1142
|
-
return runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1143
|
-
}
|
|
1144
1112
|
function detectRenames(options) {
|
|
1145
1113
|
const threshold = options.similarityThreshold ?? 80;
|
|
1146
1114
|
const args = [
|
|
@@ -1172,12 +1140,12 @@ function parseRenameOutput(text) {
|
|
|
1172
1140
|
return out;
|
|
1173
1141
|
}
|
|
1174
1142
|
function classifyRename(oldPath, newPath) {
|
|
1175
|
-
return
|
|
1143
|
+
return path6.posix.dirname(oldPath) === path6.posix.dirname(newPath) ? "rename" : "move";
|
|
1176
1144
|
}
|
|
1177
1145
|
function renameToAliases(repoRoot, pair) {
|
|
1178
1146
|
const reason = classifyRename(pair.oldPath, pair.newPath);
|
|
1179
|
-
const oldAbs =
|
|
1180
|
-
const newAbs =
|
|
1147
|
+
const oldAbs = path6.join(repoRoot, pair.oldPath);
|
|
1148
|
+
const newAbs = path6.join(repoRoot, pair.newPath);
|
|
1181
1149
|
return [
|
|
1182
1150
|
{ oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
|
|
1183
1151
|
{ oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
|
|
@@ -1195,29 +1163,10 @@ function buildAliases(repoRoot, pairs) {
|
|
|
1195
1163
|
}
|
|
1196
1164
|
return out;
|
|
1197
1165
|
}
|
|
1198
|
-
function discoveryEnv() {
|
|
1199
|
-
const env = { ...process.env };
|
|
1200
|
-
delete env.GIT_DIR;
|
|
1201
|
-
delete env.GIT_INDEX_FILE;
|
|
1202
|
-
delete env.GIT_WORK_TREE;
|
|
1203
|
-
return env;
|
|
1204
|
-
}
|
|
1205
|
-
function runGit(cwd, args) {
|
|
1206
|
-
try {
|
|
1207
|
-
return execFileSync("git", [...args], {
|
|
1208
|
-
cwd,
|
|
1209
|
-
encoding: "utf-8",
|
|
1210
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1211
|
-
env: discoveryEnv()
|
|
1212
|
-
}).trim();
|
|
1213
|
-
} catch {
|
|
1214
|
-
return null;
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
1166
|
|
|
1218
1167
|
// src/generated.ts
|
|
1219
1168
|
import { readFileSync } from "fs";
|
|
1220
|
-
import * as
|
|
1169
|
+
import * as path7 from "path";
|
|
1221
1170
|
var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
|
|
1222
1171
|
var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
|
|
1223
1172
|
function isGeneratedByHeuristic(id) {
|
|
@@ -1229,7 +1178,7 @@ function isGeneratedFile(id, patterns) {
|
|
|
1229
1178
|
}
|
|
1230
1179
|
function loadGeneratedPatterns(rootDir) {
|
|
1231
1180
|
try {
|
|
1232
|
-
const content = readFileSync(
|
|
1181
|
+
const content = readFileSync(path7.join(rootDir, ".gitattributes"), "utf-8");
|
|
1233
1182
|
return parseGeneratedPatterns(content);
|
|
1234
1183
|
} catch {
|
|
1235
1184
|
return [];
|
|
@@ -1332,25 +1281,26 @@ function annotateRoles(nodes, options) {
|
|
|
1332
1281
|
|
|
1333
1282
|
// src/file-walk.ts
|
|
1334
1283
|
import * as fs from "fs/promises";
|
|
1335
|
-
import * as
|
|
1336
|
-
|
|
1284
|
+
import * as path8 from "path";
|
|
1285
|
+
import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
|
|
1286
|
+
async function walkSourceFiles(rootDirs, languages) {
|
|
1337
1287
|
const seen = /* @__PURE__ */ new Set();
|
|
1338
1288
|
const out = [];
|
|
1339
1289
|
for (const rootDir of rootDirs) {
|
|
1340
|
-
await collectFiles(rootDir, rootDir,
|
|
1290
|
+
await collectFiles(rootDir, rootDir, languages, seen, out);
|
|
1341
1291
|
}
|
|
1342
1292
|
return out;
|
|
1343
1293
|
}
|
|
1344
|
-
async function collectFiles(dir, rootDir,
|
|
1294
|
+
async function collectFiles(dir, rootDir, languages, seen, out) {
|
|
1345
1295
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1346
1296
|
for (const entry of entries) {
|
|
1347
|
-
const full =
|
|
1297
|
+
const full = path8.join(dir, entry.name);
|
|
1348
1298
|
if (entry.isDirectory()) {
|
|
1349
1299
|
if (isExcludedDir(entry.name)) continue;
|
|
1350
|
-
await collectFiles(full, rootDir,
|
|
1300
|
+
await collectFiles(full, rootDir, languages, seen, out);
|
|
1351
1301
|
} else if (entry.isFile() && !seen.has(full)) {
|
|
1352
|
-
const relative4 =
|
|
1353
|
-
if (shouldIncludeFile(relative4, [...
|
|
1302
|
+
const relative4 = path8.relative(rootDir, full);
|
|
1303
|
+
if (shouldIncludeFile(relative4, [...languages])) {
|
|
1354
1304
|
seen.add(full);
|
|
1355
1305
|
out.push(full);
|
|
1356
1306
|
}
|
|
@@ -1421,6 +1371,7 @@ function resolveBarrelEdges(nodes, edges) {
|
|
|
1421
1371
|
// src/incremental.ts
|
|
1422
1372
|
import { createHash } from "crypto";
|
|
1423
1373
|
import * as fs2 from "fs/promises";
|
|
1374
|
+
import { getLanguageFromPath as getLanguageFromPath2 } from "@titan-design/code-parser";
|
|
1424
1375
|
|
|
1425
1376
|
// src/cognitive-complexity.ts
|
|
1426
1377
|
function cognitiveComplexityOf(body, language) {
|
|
@@ -1812,21 +1763,16 @@ function countLoc(content) {
|
|
|
1812
1763
|
function analyzeFunctions(file) {
|
|
1813
1764
|
const stats = [];
|
|
1814
1765
|
const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_TYPES;
|
|
1815
|
-
|
|
1766
|
+
walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
|
|
1816
1767
|
const fn = functionAt(node, fnTypes);
|
|
1817
|
-
if (fn)
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
for (const child of node.children) {
|
|
1826
|
-
if (child) visit(child);
|
|
1827
|
-
}
|
|
1828
|
-
};
|
|
1829
|
-
visit(file.tree.rootNode);
|
|
1768
|
+
if (!fn) return;
|
|
1769
|
+
stats.push({
|
|
1770
|
+
name: fn.name === null ? null : qualify(scope, fn.name),
|
|
1771
|
+
cyclomatic: cyclomaticOf(fn.body, file.language),
|
|
1772
|
+
cognitive: cognitiveComplexityOf(fn.body, file.language),
|
|
1773
|
+
nestingDepth: nestingDepthOf(fn.body, file.language, 0)
|
|
1774
|
+
});
|
|
1775
|
+
});
|
|
1830
1776
|
return stats;
|
|
1831
1777
|
}
|
|
1832
1778
|
function functionAt(node, fnTypes) {
|
|
@@ -1872,6 +1818,259 @@ function cyclomaticOf(body, language) {
|
|
|
1872
1818
|
return complexity;
|
|
1873
1819
|
}
|
|
1874
1820
|
|
|
1821
|
+
// src/analysis/dead-code.ts
|
|
1822
|
+
var DEAD_CODE_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
1823
|
+
"unreachable_statements",
|
|
1824
|
+
"unused_locals",
|
|
1825
|
+
"unused_params"
|
|
1826
|
+
]);
|
|
1827
|
+
var FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
1828
|
+
"function_declaration",
|
|
1829
|
+
"function_expression",
|
|
1830
|
+
"arrow_function",
|
|
1831
|
+
"method_definition",
|
|
1832
|
+
"generator_function_declaration",
|
|
1833
|
+
"generator_function"
|
|
1834
|
+
]);
|
|
1835
|
+
var PARAM_TYPES = /* @__PURE__ */ new Set(["required_parameter", "optional_parameter"]);
|
|
1836
|
+
var TERMINALS = /* @__PURE__ */ new Set([
|
|
1837
|
+
"return_statement",
|
|
1838
|
+
"throw_statement",
|
|
1839
|
+
"break_statement",
|
|
1840
|
+
"continue_statement"
|
|
1841
|
+
]);
|
|
1842
|
+
function computeDeadCodeMetrics(files, fileIdOf) {
|
|
1843
|
+
const out = [];
|
|
1844
|
+
for (const file of files) {
|
|
1845
|
+
if (file.language !== "typescript" && file.language !== "tsx") continue;
|
|
1846
|
+
const id = fileIdOf(file.filePath);
|
|
1847
|
+
const unreachable = countUnreachable(file.tree.rootNode);
|
|
1848
|
+
if (unreachable > 0) {
|
|
1849
|
+
out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
|
|
1850
|
+
}
|
|
1851
|
+
const { locals, params } = countUnusedBindings(file.tree.rootNode);
|
|
1852
|
+
if (locals > 0) {
|
|
1853
|
+
out.push({ nodeId: id, name: "unused_locals", value: locals, unit: "count" });
|
|
1854
|
+
}
|
|
1855
|
+
if (params > 0) {
|
|
1856
|
+
out.push({ nodeId: id, name: "unused_params", value: params, unit: "count" });
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
return out;
|
|
1860
|
+
}
|
|
1861
|
+
function countUnreachable(root) {
|
|
1862
|
+
let count = 0;
|
|
1863
|
+
const visit = (node) => {
|
|
1864
|
+
if (node.type === "statement_block") count += unreachableInBlock(node);
|
|
1865
|
+
for (const child of node.namedChildren) {
|
|
1866
|
+
if (child) visit(child);
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
visit(root);
|
|
1870
|
+
return count;
|
|
1871
|
+
}
|
|
1872
|
+
function unreachableInBlock(block) {
|
|
1873
|
+
let seenTerminal = false;
|
|
1874
|
+
let count = 0;
|
|
1875
|
+
for (const child of block.namedChildren) {
|
|
1876
|
+
if (!child) continue;
|
|
1877
|
+
if (seenTerminal && child.type !== "comment" && child.type !== "function_declaration") {
|
|
1878
|
+
count++;
|
|
1879
|
+
}
|
|
1880
|
+
if (TERMINALS.has(child.type)) seenTerminal = true;
|
|
1881
|
+
}
|
|
1882
|
+
return count;
|
|
1883
|
+
}
|
|
1884
|
+
function countUnusedBindings(root) {
|
|
1885
|
+
let locals = 0;
|
|
1886
|
+
let params = 0;
|
|
1887
|
+
const visit = (node) => {
|
|
1888
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
1889
|
+
const r = analyzeFunction(node);
|
|
1890
|
+
locals += r.locals;
|
|
1891
|
+
params += r.params;
|
|
1892
|
+
}
|
|
1893
|
+
for (const child of node.namedChildren) {
|
|
1894
|
+
if (child) visit(child);
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
visit(root);
|
|
1898
|
+
return { locals, params };
|
|
1899
|
+
}
|
|
1900
|
+
function analyzeFunction(fn) {
|
|
1901
|
+
const declIds = /* @__PURE__ */ new Set();
|
|
1902
|
+
const nameCount = /* @__PURE__ */ new Map();
|
|
1903
|
+
collectBindingDecls(fn, declIds, nameCount);
|
|
1904
|
+
const refs = /* @__PURE__ */ new Map();
|
|
1905
|
+
countReferences(fn, declIds, refs);
|
|
1906
|
+
const isUnused = (name) => !name.startsWith("_") && (nameCount.get(name) ?? 0) <= 1 && (refs.get(name) ?? 0) === 0;
|
|
1907
|
+
const paramNames = ownParamNames(fn);
|
|
1908
|
+
let params = 0;
|
|
1909
|
+
for (let i = paramNames.length - 1; i >= 0; i--) {
|
|
1910
|
+
const name = paramNames[i] ?? null;
|
|
1911
|
+
if (name !== null && isUnused(name)) params++;
|
|
1912
|
+
else break;
|
|
1913
|
+
}
|
|
1914
|
+
let locals = 0;
|
|
1915
|
+
for (const name of ownLocalNames(fn)) {
|
|
1916
|
+
if (isUnused(name)) locals++;
|
|
1917
|
+
}
|
|
1918
|
+
return { locals, params };
|
|
1919
|
+
}
|
|
1920
|
+
function ownParamNames(fn) {
|
|
1921
|
+
const params = fn.childForFieldName("parameters");
|
|
1922
|
+
if (!params) return [];
|
|
1923
|
+
const out = [];
|
|
1924
|
+
for (const p of params.namedChildren) {
|
|
1925
|
+
if (!p || !PARAM_TYPES.has(p.type)) continue;
|
|
1926
|
+
const first = p.namedChild(0);
|
|
1927
|
+
out.push(first?.type === "identifier" ? first.text : null);
|
|
1928
|
+
}
|
|
1929
|
+
return out;
|
|
1930
|
+
}
|
|
1931
|
+
function ownLocalNames(fn) {
|
|
1932
|
+
const body = fn.childForFieldName("body");
|
|
1933
|
+
if (!body) return [];
|
|
1934
|
+
const out = [];
|
|
1935
|
+
const walk = (node) => {
|
|
1936
|
+
if (FUNCTION_TYPES.has(node.type)) return;
|
|
1937
|
+
if (node.type === "variable_declarator") {
|
|
1938
|
+
const name = node.childForFieldName("name");
|
|
1939
|
+
if (name?.type === "identifier") out.push(name.text);
|
|
1940
|
+
}
|
|
1941
|
+
for (const child of node.namedChildren) {
|
|
1942
|
+
if (child) walk(child);
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
walk(body);
|
|
1946
|
+
return out;
|
|
1947
|
+
}
|
|
1948
|
+
function collectBindingDecls(fn, ids, nameCount) {
|
|
1949
|
+
const mark = (n) => {
|
|
1950
|
+
if (n?.type !== "identifier") return;
|
|
1951
|
+
ids.add(n.id);
|
|
1952
|
+
nameCount.set(n.text, (nameCount.get(n.text) ?? 0) + 1);
|
|
1953
|
+
};
|
|
1954
|
+
const walk = (node) => {
|
|
1955
|
+
if (PARAM_TYPES.has(node.type)) mark(node.namedChild(0));
|
|
1956
|
+
else if (node.type === "variable_declarator") mark(node.childForFieldName("name"));
|
|
1957
|
+
for (const child of node.namedChildren) {
|
|
1958
|
+
if (child) walk(child);
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
walk(fn);
|
|
1962
|
+
}
|
|
1963
|
+
function countReferences(fn, declIds, out) {
|
|
1964
|
+
const walk = (node) => {
|
|
1965
|
+
if ((node.type === "identifier" || node.type === "shorthand_property_identifier") && !declIds.has(node.id)) {
|
|
1966
|
+
out.set(node.text, (out.get(node.text) ?? 0) + 1);
|
|
1967
|
+
}
|
|
1968
|
+
for (const child of node.namedChildren) {
|
|
1969
|
+
if (child) walk(child);
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
walk(fn);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// src/analysis/growth-risk.ts
|
|
1976
|
+
var GROWTH_RISK_METRIC_NAMES = /* @__PURE__ */ new Set([
|
|
1977
|
+
"loop_depth",
|
|
1978
|
+
"recursive_functions",
|
|
1979
|
+
"search_in_loop"
|
|
1980
|
+
]);
|
|
1981
|
+
var TS_LOOP_TYPES = /* @__PURE__ */ new Set([
|
|
1982
|
+
"for_statement",
|
|
1983
|
+
"for_in_statement",
|
|
1984
|
+
"while_statement",
|
|
1985
|
+
"do_statement"
|
|
1986
|
+
]);
|
|
1987
|
+
var PY_LOOP_TYPES = /* @__PURE__ */ new Set(["for_statement", "while_statement"]);
|
|
1988
|
+
var NAMED_FN_TYPES = /* @__PURE__ */ new Set([
|
|
1989
|
+
"function_declaration",
|
|
1990
|
+
"generator_function_declaration",
|
|
1991
|
+
"method_definition",
|
|
1992
|
+
"function_definition"
|
|
1993
|
+
]);
|
|
1994
|
+
var SEARCH_METHODS = /* @__PURE__ */ new Set([
|
|
1995
|
+
"includes",
|
|
1996
|
+
"indexOf",
|
|
1997
|
+
"lastIndexOf",
|
|
1998
|
+
"find",
|
|
1999
|
+
"findIndex",
|
|
2000
|
+
"some",
|
|
2001
|
+
"every",
|
|
2002
|
+
"filter"
|
|
2003
|
+
]);
|
|
2004
|
+
function computeGrowthRiskMetrics(files, fileIdOf) {
|
|
2005
|
+
const out = [];
|
|
2006
|
+
for (const file of files) {
|
|
2007
|
+
const loopTypes = file.language === "python" ? PY_LOOP_TYPES : TS_LOOP_TYPES;
|
|
2008
|
+
const root = file.tree.rootNode;
|
|
2009
|
+
const id = fileIdOf(file.filePath);
|
|
2010
|
+
const depth = maxLoopDepth(root, loopTypes, 0);
|
|
2011
|
+
if (depth >= 2) {
|
|
2012
|
+
out.push({ nodeId: id, name: "loop_depth", value: depth, unit: "count" });
|
|
2013
|
+
}
|
|
2014
|
+
const recursion = countDirectRecursion(root);
|
|
2015
|
+
if (recursion > 0) {
|
|
2016
|
+
out.push({ nodeId: id, name: "recursive_functions", value: recursion, unit: "count" });
|
|
2017
|
+
}
|
|
2018
|
+
const searches = countSearchInLoop(root, loopTypes, false);
|
|
2019
|
+
if (searches > 0) {
|
|
2020
|
+
out.push({ nodeId: id, name: "search_in_loop", value: searches, unit: "count" });
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return out;
|
|
2024
|
+
}
|
|
2025
|
+
function countDirectRecursion(root) {
|
|
2026
|
+
let count = 0;
|
|
2027
|
+
const visit = (node) => {
|
|
2028
|
+
if (NAMED_FN_TYPES.has(node.type)) {
|
|
2029
|
+
const name = node.childForFieldName("name")?.text;
|
|
2030
|
+
const body = node.childForFieldName("body");
|
|
2031
|
+
if (name && body && callsName(body, name)) count++;
|
|
2032
|
+
}
|
|
2033
|
+
for (const child of node.namedChildren) {
|
|
2034
|
+
if (child) visit(child);
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
visit(root);
|
|
2038
|
+
return count;
|
|
2039
|
+
}
|
|
2040
|
+
function callsName(node, name) {
|
|
2041
|
+
if (node.type === "call_expression") {
|
|
2042
|
+
const fn = node.childForFieldName("function");
|
|
2043
|
+
if (fn?.type === "identifier" && fn.text === name) return true;
|
|
2044
|
+
}
|
|
2045
|
+
for (const child of node.namedChildren) {
|
|
2046
|
+
if (child && callsName(child, name)) return true;
|
|
2047
|
+
}
|
|
2048
|
+
return false;
|
|
2049
|
+
}
|
|
2050
|
+
function countSearchInLoop(node, loopTypes, inLoop) {
|
|
2051
|
+
let count = 0;
|
|
2052
|
+
if (inLoop && node.type === "call_expression") {
|
|
2053
|
+
const fn = node.childForFieldName("function");
|
|
2054
|
+
const prop = fn?.type === "member_expression" ? fn.childForFieldName("property") : null;
|
|
2055
|
+
if (prop && SEARCH_METHODS.has(prop.text)) count++;
|
|
2056
|
+
}
|
|
2057
|
+
const childInLoop = inLoop || loopTypes.has(node.type);
|
|
2058
|
+
for (const child of node.namedChildren) {
|
|
2059
|
+
if (child) count += countSearchInLoop(child, loopTypes, childInLoop);
|
|
2060
|
+
}
|
|
2061
|
+
return count;
|
|
2062
|
+
}
|
|
2063
|
+
function maxLoopDepth(node, loopTypes, depth) {
|
|
2064
|
+
let max = depth;
|
|
2065
|
+
for (const child of node.namedChildren) {
|
|
2066
|
+
if (!child) continue;
|
|
2067
|
+
const next = loopTypes.has(child.type) ? depth + 1 : depth;
|
|
2068
|
+
const childMax = maxLoopDepth(child, loopTypes, next);
|
|
2069
|
+
if (childMax > max) max = childMax;
|
|
2070
|
+
}
|
|
2071
|
+
return max;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
1875
2074
|
// src/reconstruct.ts
|
|
1876
2075
|
function reconstructFragment(repoRoot, absPath, fileId2, basis) {
|
|
1877
2076
|
const nodes = buildFileModuleNodes(repoRoot, absPath);
|
|
@@ -1922,7 +2121,7 @@ function structuralSignature(file) {
|
|
|
1922
2121
|
return hashContent(parts.join(""));
|
|
1923
2122
|
}
|
|
1924
2123
|
function sourceLanguage(filePath) {
|
|
1925
|
-
const language =
|
|
2124
|
+
const language = getLanguageFromPath2(filePath);
|
|
1926
2125
|
return language === "typescript" || language === "python" ? language : null;
|
|
1927
2126
|
}
|
|
1928
2127
|
async function readSourceFiles(filePaths) {
|
|
@@ -2019,7 +2218,8 @@ function loadReuseBasis(db, indexVersion) {
|
|
|
2019
2218
|
}
|
|
2020
2219
|
const sourceMetricsByFile = /* @__PURE__ */ new Map();
|
|
2021
2220
|
for (const m of db.listMetrics(snap.id)) {
|
|
2022
|
-
if (!SOURCE_METRIC_NAMES.has(m.name))
|
|
2221
|
+
if (!SOURCE_METRIC_NAMES.has(m.name) && !DEAD_CODE_METRIC_NAMES.has(m.name) && !GROWTH_RISK_METRIC_NAMES.has(m.name))
|
|
2222
|
+
continue;
|
|
2023
2223
|
const node = nodesById.get(m.nodeId);
|
|
2024
2224
|
const fileKey = node?.kind === "symbol" && node.parentId ? node.parentId : m.nodeId;
|
|
2025
2225
|
const bucket = sourceMetricsByFile.get(fileKey);
|
|
@@ -2163,6 +2363,199 @@ function computeMetrics(nodes, edges) {
|
|
|
2163
2363
|
return out;
|
|
2164
2364
|
}
|
|
2165
2365
|
|
|
2366
|
+
// src/analysis/test-linker.ts
|
|
2367
|
+
var DEFAULT_MIN_CO_EDIT_COUNT = 2;
|
|
2368
|
+
var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
|
|
2369
|
+
var TESTS_DIR_RE = /(^|\/)(?:__tests__|tests?)\//;
|
|
2370
|
+
function pathCandidates(testId) {
|
|
2371
|
+
const out = /* @__PURE__ */ new Set();
|
|
2372
|
+
const deInfixed = testId.replace(TEST_INFIX_RE, "$1");
|
|
2373
|
+
if (deInfixed !== testId) out.add(deInfixed);
|
|
2374
|
+
const deDir = deInfixed.replace(TESTS_DIR_RE, "$1");
|
|
2375
|
+
if (deDir !== testId) out.add(deDir);
|
|
2376
|
+
return [...out];
|
|
2377
|
+
}
|
|
2378
|
+
function splitFileIds(nodes) {
|
|
2379
|
+
const testIds = [];
|
|
2380
|
+
const nonTestIds = /* @__PURE__ */ new Set();
|
|
2381
|
+
for (const n of nodes) {
|
|
2382
|
+
if (n.kind !== "file") continue;
|
|
2383
|
+
if (n.role === "test") testIds.push(n.id);
|
|
2384
|
+
else nonTestIds.add(n.id);
|
|
2385
|
+
}
|
|
2386
|
+
return { testIds, nonTestIds };
|
|
2387
|
+
}
|
|
2388
|
+
function linkTestsToSources(nodes, coEditPairs, options = {}) {
|
|
2389
|
+
const minCount = options.minCoEditCount ?? DEFAULT_MIN_CO_EDIT_COUNT;
|
|
2390
|
+
const { testIds, nonTestIds } = splitFileIds(nodes);
|
|
2391
|
+
const links = [];
|
|
2392
|
+
for (const testId of testIds) {
|
|
2393
|
+
const matched = pathCandidates(testId).filter((c) => nonTestIds.has(c));
|
|
2394
|
+
if (matched.length > 0) {
|
|
2395
|
+
for (const sourceId of matched) {
|
|
2396
|
+
links.push({ testId, sourceId, method: "path" });
|
|
2397
|
+
}
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
const partner = couplingFor(coEditPairs, testId).find((p) => p.count >= minCount && nonTestIds.has(p.partner));
|
|
2401
|
+
if (partner) {
|
|
2402
|
+
links.push({ testId, sourceId: partner.partner, method: "coedit" });
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
return links;
|
|
2406
|
+
}
|
|
2407
|
+
function testCoverageCountMetrics(links) {
|
|
2408
|
+
const testsBySource = groupTestsBySource(links);
|
|
2409
|
+
const out = [];
|
|
2410
|
+
for (const [sourceId, tests] of testsBySource) {
|
|
2411
|
+
out.push({
|
|
2412
|
+
nodeId: sourceId,
|
|
2413
|
+
name: "linked_test_count",
|
|
2414
|
+
value: tests.size,
|
|
2415
|
+
unit: "count"
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
return out;
|
|
2419
|
+
}
|
|
2420
|
+
function groupTestsBySource(links) {
|
|
2421
|
+
const out = /* @__PURE__ */ new Map();
|
|
2422
|
+
for (const l of links) {
|
|
2423
|
+
let set = out.get(l.sourceId);
|
|
2424
|
+
if (!set) {
|
|
2425
|
+
set = /* @__PURE__ */ new Set();
|
|
2426
|
+
out.set(l.sourceId, set);
|
|
2427
|
+
}
|
|
2428
|
+
set.add(l.testId);
|
|
2429
|
+
}
|
|
2430
|
+
return out;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// src/history-recency.ts
|
|
2434
|
+
var DAY_SECONDS = 86400;
|
|
2435
|
+
function windowSuffix(window) {
|
|
2436
|
+
return window === "lifetime" ? "lifetime" : `${window}d`;
|
|
2437
|
+
}
|
|
2438
|
+
function computeRecencyWindows(firstSeen, churnedIdsByWindow, nowEpoch) {
|
|
2439
|
+
const out = [];
|
|
2440
|
+
const ageEmitted = /* @__PURE__ */ new Set();
|
|
2441
|
+
for (const [windowDays, ids] of churnedIdsByWindow) {
|
|
2442
|
+
const suffix = windowSuffix(windowDays);
|
|
2443
|
+
for (const id of ids) {
|
|
2444
|
+
const seen = firstSeen.get(id);
|
|
2445
|
+
let recency = 1;
|
|
2446
|
+
if (seen !== void 0) {
|
|
2447
|
+
const ageDays = Math.max(0, (nowEpoch - seen) / DAY_SECONDS);
|
|
2448
|
+
if (windowDays !== "lifetime") recency = Math.min(1, ageDays / windowDays);
|
|
2449
|
+
if (!ageEmitted.has(id)) {
|
|
2450
|
+
out.push({ nodeId: id, name: "file_age_days", value: Math.round(ageDays), unit: "days" });
|
|
2451
|
+
ageEmitted.add(id);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
out.push({ nodeId: id, name: `recency_${suffix}`, value: round3(recency), unit: "ratio" });
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return out;
|
|
2458
|
+
}
|
|
2459
|
+
function round3(n) {
|
|
2460
|
+
return Math.round(n * 1e3) / 1e3;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// src/history-metrics.ts
|
|
2464
|
+
var DEFAULT_CHURN_WINDOWS = [30, 90, 180];
|
|
2465
|
+
function resolveChurnWindows(requested, primaryWindow, includeLifetime) {
|
|
2466
|
+
const base = requested && requested.length > 0 ? requested : DEFAULT_CHURN_WINDOWS;
|
|
2467
|
+
const finite = [.../* @__PURE__ */ new Set([primaryWindow, ...base])].filter((w) => w > 0).sort((a, b) => a - b);
|
|
2468
|
+
return includeLifetime ? [...finite, "lifetime"] : finite;
|
|
2469
|
+
}
|
|
2470
|
+
function loadHistoryMetrics(nodes, idRoot, options = {}) {
|
|
2471
|
+
const knownPaths = collectFileIds(nodes);
|
|
2472
|
+
const primaryWindow = options.churnWindowDays ?? 30;
|
|
2473
|
+
const windows = resolveChurnWindows(options.churnWindows, primaryWindow, options.includeLifetime === true);
|
|
2474
|
+
const wide = loadChurnEntries({ repoRoot: idRoot, windowDays: windows[windows.length - 1] });
|
|
2475
|
+
if (wide === null) return null;
|
|
2476
|
+
const nowEpoch = options.nowEpoch ?? Math.floor(Date.now() / 1e3);
|
|
2477
|
+
const churnByWindow = aggregateChurnWindows(wide, windows, nowEpoch, knownPaths);
|
|
2478
|
+
const primaryEntries = windows.at(-1) === primaryWindow ? wide : entriesWithin(wide, primaryWindow, nowEpoch);
|
|
2479
|
+
const metrics = [
|
|
2480
|
+
...churnMetrics(churnByWindow),
|
|
2481
|
+
...ownershipMetrics(primaryEntries, primaryWindow, knownPaths),
|
|
2482
|
+
...recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch)
|
|
2483
|
+
];
|
|
2484
|
+
if (options.includeLifetime === true) metrics.push(...ownershipMetrics(wide, "lifetime", knownPaths));
|
|
2485
|
+
return { metrics, primaryEntries };
|
|
2486
|
+
}
|
|
2487
|
+
function collectFileIds(nodes) {
|
|
2488
|
+
const out = /* @__PURE__ */ new Set();
|
|
2489
|
+
for (const n of nodes) {
|
|
2490
|
+
if (n.kind === "file") out.add(n.id);
|
|
2491
|
+
}
|
|
2492
|
+
return out;
|
|
2493
|
+
}
|
|
2494
|
+
function churnMetrics(churnByWindow) {
|
|
2495
|
+
const out = [];
|
|
2496
|
+
for (const [window, byPath] of churnByWindow) {
|
|
2497
|
+
const suffix = windowSuffix(window);
|
|
2498
|
+
for (const [nodeId, churn] of byPath) {
|
|
2499
|
+
out.push(
|
|
2500
|
+
{ nodeId, name: `churn_${suffix}`, value: churn.lines, unit: "lines" },
|
|
2501
|
+
{ nodeId, name: `churn_${suffix}_commits`, value: churn.commits, unit: "count" },
|
|
2502
|
+
{ nodeId, name: `churn_${suffix}_authors`, value: churn.authors, unit: "count" }
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
return out;
|
|
2507
|
+
}
|
|
2508
|
+
function ownershipMetrics(entries, window, knownPaths) {
|
|
2509
|
+
const suffix = windowSuffix(window);
|
|
2510
|
+
const out = [];
|
|
2511
|
+
for (const [nodeId, owner] of computeOwnership(entries, { knownPaths })) {
|
|
2512
|
+
out.push(
|
|
2513
|
+
{ nodeId, name: `bus_factor_${suffix}`, value: owner.busFactor, unit: "count" },
|
|
2514
|
+
{ nodeId, name: `top_author_share_${suffix}`, value: round3(owner.topAuthorShare), unit: "ratio" }
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
return out;
|
|
2518
|
+
}
|
|
2519
|
+
function computeTestCoverageOwnership(entries, links, options = {}) {
|
|
2520
|
+
const suffix = windowSuffix(options.windowDays ?? 30);
|
|
2521
|
+
const testsBySource = groupTestsBySource(links);
|
|
2522
|
+
const testIds = /* @__PURE__ */ new Set();
|
|
2523
|
+
for (const tests of testsBySource.values()) {
|
|
2524
|
+
for (const t of tests) testIds.add(t);
|
|
2525
|
+
}
|
|
2526
|
+
const linesByTest = authorLinesByPath(entries, testIds);
|
|
2527
|
+
const out = [];
|
|
2528
|
+
for (const [nodeId, tests] of testsBySource) {
|
|
2529
|
+
const summary = summarizeOwnership(mergeAuthorChurn(tests, linesByTest), options.busFactorThreshold);
|
|
2530
|
+
if (summary === null) continue;
|
|
2531
|
+
out.push(
|
|
2532
|
+
{ nodeId, name: `test_bus_factor_${suffix}`, value: summary.busFactor, unit: "count" },
|
|
2533
|
+
{ nodeId, name: `test_top_author_share_${suffix}`, value: round3(summary.topAuthorShare), unit: "ratio" }
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
return out;
|
|
2537
|
+
}
|
|
2538
|
+
function mergeAuthorChurn(tests, linesByTest) {
|
|
2539
|
+
const byAuthor = /* @__PURE__ */ new Map();
|
|
2540
|
+
for (const testId of tests) {
|
|
2541
|
+
const fileAuthors = linesByTest.get(testId);
|
|
2542
|
+
if (!fileAuthors) continue;
|
|
2543
|
+
for (const [author, lines] of fileAuthors) {
|
|
2544
|
+
byAuthor.set(author, (byAuthor.get(author) ?? 0) + lines);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return byAuthor;
|
|
2548
|
+
}
|
|
2549
|
+
function recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch) {
|
|
2550
|
+
const churnedByWindow = /* @__PURE__ */ new Map();
|
|
2551
|
+
for (const [window, byPath] of churnByWindow) {
|
|
2552
|
+
if (byPath.size > 0) churnedByWindow.set(window, new Set(byPath.keys()));
|
|
2553
|
+
}
|
|
2554
|
+
if (churnedByWindow.size === 0) return [];
|
|
2555
|
+
const firstSeen = loadFileFirstSeen({ repoRoot: idRoot, knownPaths }) ?? /* @__PURE__ */ new Map();
|
|
2556
|
+
return computeRecencyWindows(firstSeen, churnedByWindow, nowEpoch);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2166
2559
|
// src/index-metrics.ts
|
|
2167
2560
|
function symbolNamesByFile(nodes) {
|
|
2168
2561
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -2176,6 +2569,7 @@ function symbolNamesByFile(nodes) {
|
|
|
2176
2569
|
}
|
|
2177
2570
|
function buildIndexerMetrics(input) {
|
|
2178
2571
|
const nodeList = [...input.nodes.values()];
|
|
2572
|
+
const history = input.history ? loadHistoryMetrics(nodeList, input.idRoot, input.history) : null;
|
|
2179
2573
|
return [
|
|
2180
2574
|
...computeMetrics(nodeList, [...input.edges.values()]),
|
|
2181
2575
|
...computeSourceMetrics(
|
|
@@ -2183,9 +2577,23 @@ function buildIndexerMetrics(input) {
|
|
|
2183
2577
|
(p) => fileId(input.idRoot, p),
|
|
2184
2578
|
symbolNamesByFile(nodeList)
|
|
2185
2579
|
),
|
|
2186
|
-
...input.
|
|
2580
|
+
...computeDeadCodeMetrics(input.parsedFiles, (p) => fileId(input.idRoot, p)),
|
|
2581
|
+
...computeGrowthRiskMetrics(input.parsedFiles, (p) => fileId(input.idRoot, p)),
|
|
2582
|
+
...input.reusedSourceMetrics,
|
|
2583
|
+
...history?.metrics ?? [],
|
|
2584
|
+
...computeTestCoverage(nodeList, history?.primaryEntries ?? null, input.history?.churnWindowDays)
|
|
2187
2585
|
];
|
|
2188
2586
|
}
|
|
2587
|
+
function computeTestCoverage(nodes, entries, windowDays) {
|
|
2588
|
+
const coEditPairs = entries ? computeChangeCoupling(entries, { knownPaths: collectFileIds(nodes) }).pairs : [];
|
|
2589
|
+
const links = linkTestsToSources(nodes, coEditPairs);
|
|
2590
|
+
if (links.length === 0) return [];
|
|
2591
|
+
const out = testCoverageCountMetrics(links);
|
|
2592
|
+
if (entries) {
|
|
2593
|
+
out.push(...computeTestCoverageOwnership(entries, links, { windowDays }));
|
|
2594
|
+
}
|
|
2595
|
+
return out;
|
|
2596
|
+
}
|
|
2189
2597
|
|
|
2190
2598
|
// src/merge.ts
|
|
2191
2599
|
function edgeKey(edge) {
|
|
@@ -2204,19 +2612,56 @@ function mergeFragments(fragments) {
|
|
|
2204
2612
|
return { nodes, edges };
|
|
2205
2613
|
}
|
|
2206
2614
|
|
|
2207
|
-
// src/
|
|
2208
|
-
var
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2615
|
+
// src/symbol-aliases.ts
|
|
2616
|
+
var QUALIFIED_SYMBOLS_SINCE = "0.14.0";
|
|
2617
|
+
function versionParts(version) {
|
|
2618
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
2619
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
2620
|
+
}
|
|
2621
|
+
function predatesQualifiedSymbols(indexVersion) {
|
|
2622
|
+
const have = versionParts(indexVersion);
|
|
2623
|
+
if (!have) return true;
|
|
2624
|
+
const since = versionParts(QUALIFIED_SYMBOLS_SINCE);
|
|
2625
|
+
for (let i = 0; i < 3; i++) {
|
|
2626
|
+
if (have[i] !== since[i]) return have[i] < since[i];
|
|
2215
2627
|
}
|
|
2628
|
+
return false;
|
|
2216
2629
|
}
|
|
2217
|
-
function
|
|
2630
|
+
function qualifiedSymbolAliases(files, nodeIds) {
|
|
2631
|
+
const out = [];
|
|
2632
|
+
for (const { fileId: fileId2, file } of files) {
|
|
2633
|
+
for (const [name, qualified] of qualifiedByName(file)) {
|
|
2634
|
+
const oldId = symbolId(fileId2, name);
|
|
2635
|
+
if (qualified.size !== 1 || nodeIds.has(oldId)) continue;
|
|
2636
|
+
const newId = symbolId(fileId2, [...qualified][0]);
|
|
2637
|
+
if (nodeIds.has(newId)) out.push({ oldId, newId, reason: "requalify" });
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return out;
|
|
2641
|
+
}
|
|
2642
|
+
function qualifiedByName(file) {
|
|
2643
|
+
const out = /* @__PURE__ */ new Map();
|
|
2644
|
+
for (const d of collectDeclarations(file)) {
|
|
2645
|
+
const bucket = out.get(d.name);
|
|
2646
|
+
if (bucket) bucket.add(d.qualifiedName);
|
|
2647
|
+
else out.set(d.name, /* @__PURE__ */ new Set([d.qualifiedName]));
|
|
2648
|
+
}
|
|
2649
|
+
return out;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// src/indexer.ts
|
|
2653
|
+
var INDEX_VERSION = "0.14.0";
|
|
2654
|
+
var LANGUAGES = ["typescript", "python"];
|
|
2655
|
+
function canonicalizePath(p) {
|
|
2656
|
+
try {
|
|
2657
|
+
return realpathSync(p);
|
|
2658
|
+
} catch {
|
|
2659
|
+
return p;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
function normalizePaths(paths) {
|
|
2218
2663
|
if (paths.length === 0) throw new Error("indexPaths requires at least one path");
|
|
2219
|
-
return [...new Set(paths.map((p) =>
|
|
2664
|
+
return [...new Set(paths.map((p) => path9.resolve(p)))];
|
|
2220
2665
|
}
|
|
2221
2666
|
function countByKind(items) {
|
|
2222
2667
|
const out = {};
|
|
@@ -2243,6 +2688,12 @@ function resolveAliases(rootDir, idRoot, options, store) {
|
|
|
2243
2688
|
if (target === priorCommit) return [];
|
|
2244
2689
|
return buildAliases(idRoot, detectRenames({ repoRoot: rootDir, fromCommit: priorCommit, toCommit: target }));
|
|
2245
2690
|
}
|
|
2691
|
+
function resolveSymbolAliases(store, idRoot, parsedByPath, nodeIds) {
|
|
2692
|
+
const prior = store.listSnapshots({ limit: 1 })[0];
|
|
2693
|
+
if (!prior || !predatesQualifiedSymbols(prior.indexVersion)) return [];
|
|
2694
|
+
const files = [...parsedByPath.values()].map((file) => ({ fileId: fileId(idRoot, file.filePath), file }));
|
|
2695
|
+
return qualifiedSymbolAliases(files, nodeIds);
|
|
2696
|
+
}
|
|
2246
2697
|
function resolveRoots(paths) {
|
|
2247
2698
|
const raw = normalizePaths(paths);
|
|
2248
2699
|
const gitToplevel = detectGitToplevel(raw[0]);
|
|
@@ -2256,6 +2707,13 @@ async function parseAll(toParse) {
|
|
|
2256
2707
|
}
|
|
2257
2708
|
return parsed;
|
|
2258
2709
|
}
|
|
2710
|
+
function historyOptions(options) {
|
|
2711
|
+
return {
|
|
2712
|
+
churnWindowDays: options.churnWindowDays,
|
|
2713
|
+
churnWindows: options.churnWindows,
|
|
2714
|
+
includeLifetime: options.lifetime === true
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2259
2717
|
async function indexPaths(store, options) {
|
|
2260
2718
|
const { rootDirs, idRoot } = resolveRoots(options.paths);
|
|
2261
2719
|
const readFiles = await readSourceFiles(await walkSourceFiles(rootDirs, LANGUAGES));
|
|
@@ -2283,10 +2741,14 @@ async function indexPaths(store, options) {
|
|
|
2283
2741
|
edges: accumulator.edges,
|
|
2284
2742
|
parsedFiles: [...parsedByPath.values()],
|
|
2285
2743
|
reusedSourceMetrics: reuse ? reusedFileIds.flatMap((id) => reuse.sourceMetricsByFile.get(id) ?? []) : [],
|
|
2286
|
-
idRoot
|
|
2744
|
+
idRoot,
|
|
2745
|
+
history: options.computeChurn === false ? void 0 : historyOptions(options)
|
|
2287
2746
|
});
|
|
2288
2747
|
const rootDir = rootDirs[0];
|
|
2289
|
-
const aliases =
|
|
2748
|
+
const aliases = [
|
|
2749
|
+
...resolveAliases(rootDir, idRoot, options, store),
|
|
2750
|
+
...resolveSymbolAliases(store, idRoot, parsedByPath, new Set(accumulator.nodes.keys()))
|
|
2751
|
+
];
|
|
2290
2752
|
const commitHash = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
|
|
2291
2753
|
const snapshotId = persist(store, options.ref ?? "wd", commitHash, accumulator, aliases, metrics);
|
|
2292
2754
|
store.insertFingerprints(snapshotId, buildFingerprints(readFiles, idRoot, classified.structuralByFileId));
|
|
@@ -2305,6 +2767,9 @@ async function indexPaths(store, options) {
|
|
|
2305
2767
|
};
|
|
2306
2768
|
}
|
|
2307
2769
|
|
|
2770
|
+
// src/index.ts
|
|
2771
|
+
import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as parseFile2, shouldIncludeFile as shouldIncludeFile2 } from "@titan-design/code-parser";
|
|
2772
|
+
|
|
2308
2773
|
// src/aliases.ts
|
|
2309
2774
|
var METRIC_ALIASES = /* @__PURE__ */ new Map([
|
|
2310
2775
|
["lines", "loc"],
|
|
@@ -2334,6 +2799,9 @@ var EDGE_KIND_ALIASES = /* @__PURE__ */ new Map([
|
|
|
2334
2799
|
["depends_on", "depends-on"],
|
|
2335
2800
|
["dependson", "depends-on"]
|
|
2336
2801
|
]);
|
|
2802
|
+
function deprecatedTarget(map, name) {
|
|
2803
|
+
return map.get(name);
|
|
2804
|
+
}
|
|
2337
2805
|
function canonicalMetricName(name) {
|
|
2338
2806
|
return METRIC_ALIASES.get(name) ?? name;
|
|
2339
2807
|
}
|
|
@@ -2343,6 +2811,332 @@ function canonicalRole(name) {
|
|
|
2343
2811
|
function canonicalEdgeKind(name) {
|
|
2344
2812
|
return EDGE_KIND_ALIASES.get(name) ?? name;
|
|
2345
2813
|
}
|
|
2814
|
+
function metricAliasTarget(name) {
|
|
2815
|
+
return deprecatedTarget(METRIC_ALIASES, name);
|
|
2816
|
+
}
|
|
2817
|
+
function roleAliasTarget(name) {
|
|
2818
|
+
return deprecatedTarget(ROLE_ALIASES, name);
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// src/analysis/coverage.ts
|
|
2822
|
+
var COVERAGE_METRIC_NAME = "coverage_pct";
|
|
2823
|
+
function isFileCoverage(v) {
|
|
2824
|
+
return typeof v === "object" && v !== null && typeof v.fnMap === "object" && typeof v.f === "object";
|
|
2825
|
+
}
|
|
2826
|
+
function innermostContaining(symbols, line) {
|
|
2827
|
+
let best = null;
|
|
2828
|
+
for (const s of symbols) {
|
|
2829
|
+
if (s.startLine <= line && line <= s.endLine) {
|
|
2830
|
+
if (!best || s.endLine - s.startLine < best.endLine - best.startLine) best = s;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
return best;
|
|
2834
|
+
}
|
|
2835
|
+
function attributeCoverage(coverage, fileIdOf, symbolsByFile) {
|
|
2836
|
+
const out = [];
|
|
2837
|
+
for (const [absPath, fc] of Object.entries(coverage)) {
|
|
2838
|
+
if (!isFileCoverage(fc)) continue;
|
|
2839
|
+
const fileId2 = fileIdOf(absPath);
|
|
2840
|
+
if (!fileId2) continue;
|
|
2841
|
+
const fnIds = Object.keys(fc.fnMap);
|
|
2842
|
+
if (fnIds.length === 0) continue;
|
|
2843
|
+
const covered = fnIds.filter((k) => (fc.f[k] ?? 0) > 0).length;
|
|
2844
|
+
out.push(pctMetric(fileId2, covered, fnIds.length));
|
|
2845
|
+
out.push(...symbolCoverage(fc, fnIds, symbolsByFile.get(fileId2) ?? []));
|
|
2846
|
+
}
|
|
2847
|
+
return out;
|
|
2848
|
+
}
|
|
2849
|
+
function symbolCoverage(fc, fnIds, symbols) {
|
|
2850
|
+
const perSymbol = /* @__PURE__ */ new Map();
|
|
2851
|
+
for (const k of fnIds) {
|
|
2852
|
+
const sym = innermostContaining(symbols, fc.fnMap[k].loc.start.line);
|
|
2853
|
+
if (!sym) continue;
|
|
2854
|
+
const acc = perSymbol.get(sym.id) ?? { hit: 0, total: 0 };
|
|
2855
|
+
acc.total++;
|
|
2856
|
+
if ((fc.f[k] ?? 0) > 0) acc.hit++;
|
|
2857
|
+
perSymbol.set(sym.id, acc);
|
|
2858
|
+
}
|
|
2859
|
+
return [...perSymbol].map(([id, { hit, total }]) => pctMetric(id, hit, total));
|
|
2860
|
+
}
|
|
2861
|
+
function pctMetric(nodeId, covered, total) {
|
|
2862
|
+
return {
|
|
2863
|
+
nodeId,
|
|
2864
|
+
name: COVERAGE_METRIC_NAME,
|
|
2865
|
+
value: Math.round(100 * covered / total),
|
|
2866
|
+
unit: "percent"
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
// src/analysis/pagerank.ts
|
|
2871
|
+
var DEFAULT_DAMPING = 0.85;
|
|
2872
|
+
var DEFAULT_TOLERANCE = 1e-6;
|
|
2873
|
+
var DEFAULT_MAX_ITERS = 100;
|
|
2874
|
+
var DEFAULT_EDGE_WEIGHTS = {
|
|
2875
|
+
imports: 1,
|
|
2876
|
+
"re-exports": 0.5,
|
|
2877
|
+
calls: 1.5,
|
|
2878
|
+
extends: 1,
|
|
2879
|
+
implements: 1,
|
|
2880
|
+
references: 1,
|
|
2881
|
+
"depends-on": 1
|
|
2882
|
+
};
|
|
2883
|
+
function getEdgeWeight(kind, overrides) {
|
|
2884
|
+
return overrides?.[kind] ?? DEFAULT_EDGE_WEIGHTS[kind] ?? 1;
|
|
2885
|
+
}
|
|
2886
|
+
function computePageRank(nodes, edges, options = {}) {
|
|
2887
|
+
const n = nodes.length;
|
|
2888
|
+
if (n === 0) return { rows: [], iterations: 0, converged: true };
|
|
2889
|
+
const damping = options.damping ?? DEFAULT_DAMPING;
|
|
2890
|
+
const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
|
|
2891
|
+
const maxIters = options.maxIterations ?? DEFAULT_MAX_ITERS;
|
|
2892
|
+
const weights = { ...DEFAULT_EDGE_WEIGHTS, ...options.edgeWeights ?? {} };
|
|
2893
|
+
const idx = buildIdIndex(nodes);
|
|
2894
|
+
const adj = buildAdjacency(idx, edges, weights, n);
|
|
2895
|
+
const pers = buildPersonalization(idx, n, options.personalization);
|
|
2896
|
+
const { rank, iterations, converged } = powerIterate(
|
|
2897
|
+
pers,
|
|
2898
|
+
adj,
|
|
2899
|
+
damping,
|
|
2900
|
+
tolerance,
|
|
2901
|
+
maxIters
|
|
2902
|
+
);
|
|
2903
|
+
return { rows: rankToRows(nodes, rank), iterations, converged };
|
|
2904
|
+
}
|
|
2905
|
+
function buildIdIndex(nodes) {
|
|
2906
|
+
const idx = /* @__PURE__ */ new Map();
|
|
2907
|
+
for (let i = 0; i < nodes.length; i++) idx.set(nodes[i].id, i);
|
|
2908
|
+
return idx;
|
|
2909
|
+
}
|
|
2910
|
+
function buildAdjacency(idx, edges, weights, n) {
|
|
2911
|
+
const outNeighbors = Array.from(
|
|
2912
|
+
{ length: n },
|
|
2913
|
+
() => []
|
|
2914
|
+
);
|
|
2915
|
+
const outWeightSum = new Array(n).fill(0);
|
|
2916
|
+
for (const e of edges) {
|
|
2917
|
+
const si = idx.get(e.srcId);
|
|
2918
|
+
const di = idx.get(e.dstId);
|
|
2919
|
+
if (si === void 0 || di === void 0) continue;
|
|
2920
|
+
const w = weights[e.kind] ?? 1;
|
|
2921
|
+
if (w <= 0) continue;
|
|
2922
|
+
outNeighbors[si].push({ to: di, w });
|
|
2923
|
+
outWeightSum[si] += w;
|
|
2924
|
+
}
|
|
2925
|
+
return { outNeighbors, outWeightSum };
|
|
2926
|
+
}
|
|
2927
|
+
function powerIterate(pers, adj, damping, tolerance, maxIters) {
|
|
2928
|
+
const n = pers.length;
|
|
2929
|
+
let rank = pers.slice();
|
|
2930
|
+
let next = new Array(n).fill(0);
|
|
2931
|
+
let iterations = 0;
|
|
2932
|
+
let converged = false;
|
|
2933
|
+
for (let iter = 0; iter < maxIters; iter++) {
|
|
2934
|
+
iterations = iter + 1;
|
|
2935
|
+
const dangling = sumDangling(rank, adj.outWeightSum);
|
|
2936
|
+
const teleport = 1 - damping + damping * dangling;
|
|
2937
|
+
seedTeleport(next, pers, teleport);
|
|
2938
|
+
distributeRank(next, rank, adj, damping);
|
|
2939
|
+
const diff = l1Diff(rank, next);
|
|
2940
|
+
[rank, next] = [next, rank];
|
|
2941
|
+
next.fill(0);
|
|
2942
|
+
if (diff < tolerance) {
|
|
2943
|
+
converged = true;
|
|
2944
|
+
break;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
return { rank, iterations, converged };
|
|
2948
|
+
}
|
|
2949
|
+
function sumDangling(rank, outWeightSum) {
|
|
2950
|
+
let total = 0;
|
|
2951
|
+
for (let i = 0; i < rank.length; i++) {
|
|
2952
|
+
if (outWeightSum[i] === 0) total += rank[i];
|
|
2953
|
+
}
|
|
2954
|
+
return total;
|
|
2955
|
+
}
|
|
2956
|
+
function seedTeleport(next, pers, teleport) {
|
|
2957
|
+
for (let i = 0; i < next.length; i++) next[i] = teleport * pers[i];
|
|
2958
|
+
}
|
|
2959
|
+
function distributeRank(next, rank, adj, damping) {
|
|
2960
|
+
for (let i = 0; i < rank.length; i++) {
|
|
2961
|
+
const s = adj.outWeightSum[i];
|
|
2962
|
+
if (s === 0 || rank[i] === 0) continue;
|
|
2963
|
+
const factor = damping * rank[i] / s;
|
|
2964
|
+
const list = adj.outNeighbors[i];
|
|
2965
|
+
for (let k = 0; k < list.length; k++) {
|
|
2966
|
+
next[list[k].to] += factor * list[k].w;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
function l1Diff(a, b) {
|
|
2971
|
+
let diff = 0;
|
|
2972
|
+
for (let i = 0; i < a.length; i++) diff += Math.abs(b[i] - a[i]);
|
|
2973
|
+
return diff;
|
|
2974
|
+
}
|
|
2975
|
+
function rankToRows(nodes, rank) {
|
|
2976
|
+
return nodes.map((node, i) => ({ nodeId: node.id, score: rank[i] })).sort(compareRows);
|
|
2977
|
+
}
|
|
2978
|
+
function compareRows(a, b) {
|
|
2979
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
2980
|
+
return a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0;
|
|
2981
|
+
}
|
|
2982
|
+
function buildPersonalization(idx, n, pers) {
|
|
2983
|
+
if (!pers || pers.size === 0) return uniformVector(n);
|
|
2984
|
+
const out = new Array(n).fill(0);
|
|
2985
|
+
const total = applySeedWeights(out, idx, pers);
|
|
2986
|
+
if (total === 0) return uniformVector(n);
|
|
2987
|
+
for (let i = 0; i < n; i++) out[i] /= total;
|
|
2988
|
+
return out;
|
|
2989
|
+
}
|
|
2990
|
+
function uniformVector(n) {
|
|
2991
|
+
return new Array(n).fill(1 / n);
|
|
2992
|
+
}
|
|
2993
|
+
function applySeedWeights(out, idx, pers) {
|
|
2994
|
+
let total = 0;
|
|
2995
|
+
for (const [id, w] of pers) {
|
|
2996
|
+
if (!isValidWeight(w)) continue;
|
|
2997
|
+
const i = idx.get(id);
|
|
2998
|
+
if (i === void 0) continue;
|
|
2999
|
+
out[i] = w;
|
|
3000
|
+
total += w;
|
|
3001
|
+
}
|
|
3002
|
+
return total;
|
|
3003
|
+
}
|
|
3004
|
+
function isValidWeight(w) {
|
|
3005
|
+
return Number.isFinite(w) && w > 0;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
// src/analysis/relevance.ts
|
|
3009
|
+
function computeRelevance(nodes, edges, seedIds, options = {}) {
|
|
3010
|
+
const seeds = seedIds.filter((id) => id.length > 0);
|
|
3011
|
+
if (seeds.length === 0) return /* @__PURE__ */ new Map();
|
|
3012
|
+
const personalization = new Map(seeds.map((id) => [id, 1]));
|
|
3013
|
+
const result = computePageRank(nodes, symmetrize(edges), {
|
|
3014
|
+
personalization,
|
|
3015
|
+
damping: options.damping,
|
|
3016
|
+
edgeWeights: options.edgeWeights
|
|
3017
|
+
});
|
|
3018
|
+
const out = /* @__PURE__ */ new Map();
|
|
3019
|
+
for (const row of result.rows) out.set(row.nodeId, row.score);
|
|
3020
|
+
return out;
|
|
3021
|
+
}
|
|
3022
|
+
function symmetrize(edges) {
|
|
3023
|
+
const out = [];
|
|
3024
|
+
for (const e of edges) {
|
|
3025
|
+
out.push(e);
|
|
3026
|
+
out.push({ ...e, srcId: e.dstId, dstId: e.srcId });
|
|
3027
|
+
}
|
|
3028
|
+
return out;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// src/analysis/symbol-coupling.ts
|
|
3032
|
+
var DEFAULT_MIN_CO_IMPORTS = 2;
|
|
3033
|
+
var DEFAULT_LARGE_IMPORTER_THRESHOLD = 40;
|
|
3034
|
+
function computeSymbolConsumers(edges) {
|
|
3035
|
+
const bySymbol = /* @__PURE__ */ new Map();
|
|
3036
|
+
for (const e of edges) {
|
|
3037
|
+
let set = bySymbol.get(e.dstId);
|
|
3038
|
+
if (!set) {
|
|
3039
|
+
set = /* @__PURE__ */ new Set();
|
|
3040
|
+
bySymbol.set(e.dstId, set);
|
|
3041
|
+
}
|
|
3042
|
+
set.add(e.srcId);
|
|
3043
|
+
}
|
|
3044
|
+
const out = [];
|
|
3045
|
+
for (const [id, consumers] of bySymbol) {
|
|
3046
|
+
const parsed = parseSymbolId(id);
|
|
3047
|
+
if (!parsed) continue;
|
|
3048
|
+
out.push({
|
|
3049
|
+
symbolId: id,
|
|
3050
|
+
fileId: parsed.fileId,
|
|
3051
|
+
name: parsed.name,
|
|
3052
|
+
consumers: [...consumers].sort()
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
return out.sort(compareConsumers);
|
|
3056
|
+
}
|
|
3057
|
+
function compareConsumers(a, b) {
|
|
3058
|
+
if (b.consumers.length !== a.consumers.length) {
|
|
3059
|
+
return b.consumers.length - a.consumers.length;
|
|
3060
|
+
}
|
|
3061
|
+
return a.symbolId < b.symbolId ? -1 : a.symbolId > b.symbolId ? 1 : 0;
|
|
3062
|
+
}
|
|
3063
|
+
function computeSymbolCoupling(edges, options = {}) {
|
|
3064
|
+
const minCoImports = options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS;
|
|
3065
|
+
const largeThreshold = options.largeImporterThreshold ?? DEFAULT_LARGE_IMPORTER_THRESHOLD;
|
|
3066
|
+
const bySource = groupSymbolsBySource(edges);
|
|
3067
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3068
|
+
for (const symbols of bySource.values()) {
|
|
3069
|
+
if (symbols.length < 2 || symbols.length > largeThreshold) continue;
|
|
3070
|
+
accumulateSymbolPairs(symbols, counts);
|
|
3071
|
+
}
|
|
3072
|
+
return finalizePairs(counts, minCoImports);
|
|
3073
|
+
}
|
|
3074
|
+
function groupSymbolsBySource(edges) {
|
|
3075
|
+
const perSource = /* @__PURE__ */ new Map();
|
|
3076
|
+
for (const e of edges) {
|
|
3077
|
+
if (!parseSymbolId(e.dstId)) continue;
|
|
3078
|
+
let set = perSource.get(e.srcId);
|
|
3079
|
+
if (!set) {
|
|
3080
|
+
set = /* @__PURE__ */ new Set();
|
|
3081
|
+
perSource.set(e.srcId, set);
|
|
3082
|
+
}
|
|
3083
|
+
set.add(e.dstId);
|
|
3084
|
+
}
|
|
3085
|
+
const out = /* @__PURE__ */ new Map();
|
|
3086
|
+
for (const [src, set] of perSource) out.set(src, [...set].sort());
|
|
3087
|
+
return out;
|
|
3088
|
+
}
|
|
3089
|
+
function accumulateSymbolPairs(symbols, counts) {
|
|
3090
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
3091
|
+
for (let j = i + 1; j < symbols.length; j++) {
|
|
3092
|
+
const key = `${symbols[i]} ${symbols[j]}`;
|
|
3093
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
function finalizePairs(counts, minCoImports) {
|
|
3098
|
+
const out = [];
|
|
3099
|
+
for (const [key, coImports] of counts) {
|
|
3100
|
+
if (coImports < minCoImports) continue;
|
|
3101
|
+
const [aId, bId] = key.split(" ");
|
|
3102
|
+
const a = parseSymbolId(aId);
|
|
3103
|
+
const b = parseSymbolId(bId);
|
|
3104
|
+
out.push({
|
|
3105
|
+
aId,
|
|
3106
|
+
aFile: a.fileId,
|
|
3107
|
+
aName: a.name,
|
|
3108
|
+
bId,
|
|
3109
|
+
bFile: b.fileId,
|
|
3110
|
+
bName: b.name,
|
|
3111
|
+
coImports,
|
|
3112
|
+
crossFile: a.fileId !== b.fileId
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
return out.sort(comparePairs);
|
|
3116
|
+
}
|
|
3117
|
+
function comparePairs(a, b) {
|
|
3118
|
+
if (b.coImports !== a.coImports) return b.coImports - a.coImports;
|
|
3119
|
+
if (a.crossFile !== b.crossFile) return a.crossFile ? -1 : 1;
|
|
3120
|
+
if (a.aId !== b.aId) return a.aId < b.aId ? -1 : 1;
|
|
3121
|
+
return a.bId < b.bId ? -1 : a.bId > b.bId ? 1 : 0;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// src/analysis/snapshot.ts
|
|
3125
|
+
function snapshotPageRank(store, snapshotId, options = {}) {
|
|
3126
|
+
return computePageRank(store.listNodes(snapshotId), store.listEdges(snapshotId), options);
|
|
3127
|
+
}
|
|
3128
|
+
function snapshotRelevance(store, snapshotId, seedIds, options = {}) {
|
|
3129
|
+
return computeRelevance(store.listNodes(snapshotId), store.listEdges(snapshotId), seedIds, options);
|
|
3130
|
+
}
|
|
3131
|
+
function snapshotReferenceEdges(store, snapshotId) {
|
|
3132
|
+
return store.listEdges(snapshotId, { includeReferences: true }).filter((e) => e.kind === "references").map((e) => ({ srcId: e.srcId, dstId: e.dstId }));
|
|
3133
|
+
}
|
|
3134
|
+
function snapshotSymbolConsumers(store, snapshotId) {
|
|
3135
|
+
return computeSymbolConsumers(snapshotReferenceEdges(store, snapshotId));
|
|
3136
|
+
}
|
|
3137
|
+
function snapshotSymbolCoupling(store, snapshotId, options = {}) {
|
|
3138
|
+
return computeSymbolCoupling(snapshotReferenceEdges(store, snapshotId), options);
|
|
3139
|
+
}
|
|
2346
3140
|
|
|
2347
3141
|
// src/read.ts
|
|
2348
3142
|
function listNodes(store, snapshotId, opts) {
|
|
@@ -2354,60 +3148,954 @@ function listEdges(store, snapshotId, opts) {
|
|
|
2354
3148
|
function listMetrics(store, snapshotId) {
|
|
2355
3149
|
return store.listMetrics(snapshotId);
|
|
2356
3150
|
}
|
|
3151
|
+
|
|
3152
|
+
// src/check/context.ts
|
|
3153
|
+
function buildRuleContext(store, snapshotId) {
|
|
3154
|
+
const nodes = store.listNodes(snapshotId);
|
|
3155
|
+
const edges = store.listEdges(snapshotId);
|
|
3156
|
+
const nodesById = /* @__PURE__ */ new Map();
|
|
3157
|
+
for (const n of nodes) nodesById.set(n.id, n);
|
|
3158
|
+
return { nodes, nodesById, metricsByNode: indexMetrics(store.listMetrics(snapshotId)), edges };
|
|
3159
|
+
}
|
|
3160
|
+
function indexMetrics(metrics) {
|
|
3161
|
+
const metricsByNode = /* @__PURE__ */ new Map();
|
|
3162
|
+
for (const m of metrics) {
|
|
3163
|
+
if (m.value === null) continue;
|
|
3164
|
+
let inner = metricsByNode.get(m.nodeId);
|
|
3165
|
+
if (!inner) {
|
|
3166
|
+
inner = /* @__PURE__ */ new Map();
|
|
3167
|
+
metricsByNode.set(m.nodeId, inner);
|
|
3168
|
+
}
|
|
3169
|
+
inner.set(m.name, m.value);
|
|
3170
|
+
}
|
|
3171
|
+
return metricsByNode;
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
// src/check/patterns.ts
|
|
3175
|
+
function patternToRegex(pattern) {
|
|
3176
|
+
if (!pattern.includes("*")) {
|
|
3177
|
+
return new RegExp(escapeRegex(pattern));
|
|
3178
|
+
}
|
|
3179
|
+
let out = "";
|
|
3180
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
3181
|
+
const c = pattern[i];
|
|
3182
|
+
if (c === "*" && pattern[i + 1] === "*") {
|
|
3183
|
+
out += ".*";
|
|
3184
|
+
i++;
|
|
3185
|
+
} else if (c === "*") {
|
|
3186
|
+
out += "[^/]*";
|
|
3187
|
+
} else {
|
|
3188
|
+
out += escapeRegex(c);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
return new RegExp(`^${out}$`);
|
|
3192
|
+
}
|
|
3193
|
+
function compilePatterns(patterns) {
|
|
3194
|
+
if (!patterns) return [];
|
|
3195
|
+
return patterns.map(patternToRegex);
|
|
3196
|
+
}
|
|
3197
|
+
function matchesAny(value, patterns) {
|
|
3198
|
+
for (const rx of patterns) {
|
|
3199
|
+
if (rx.test(value)) return true;
|
|
3200
|
+
}
|
|
3201
|
+
return false;
|
|
3202
|
+
}
|
|
3203
|
+
function escapeRegex(s) {
|
|
3204
|
+
return s.replace(/[.+?(){}|^$\\[\]]/g, "\\$&");
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
// src/check/rule-helpers.ts
|
|
3208
|
+
function severityOf(rule) {
|
|
3209
|
+
return rule.severity ?? "error";
|
|
3210
|
+
}
|
|
3211
|
+
function formatNumber(n) {
|
|
3212
|
+
if (Number.isInteger(n)) return String(n);
|
|
3213
|
+
return n.toFixed(3).replace(/\.?0+$/, "");
|
|
3214
|
+
}
|
|
3215
|
+
function isImportEdge(edge) {
|
|
3216
|
+
return edge.kind === "imports" || edge.kind === "re-exports";
|
|
3217
|
+
}
|
|
3218
|
+
function longestPrefixMatcher(prefixes) {
|
|
3219
|
+
const byLength = [...prefixes].sort((a, b) => b.length - a.length);
|
|
3220
|
+
return (id) => {
|
|
3221
|
+
for (const p of byLength) {
|
|
3222
|
+
if (id === p || id.startsWith(`${p}/`)) return p;
|
|
3223
|
+
}
|
|
3224
|
+
return null;
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
// src/check/import-rules.ts
|
|
3229
|
+
function layerIndex(layers) {
|
|
3230
|
+
const packageLayer = /* @__PURE__ */ new Map();
|
|
3231
|
+
for (let i = 0; i < layers.length; i++) {
|
|
3232
|
+
for (const pkg of layers[i]) packageLayer.set(pkg, i);
|
|
3233
|
+
}
|
|
3234
|
+
return packageLayer;
|
|
3235
|
+
}
|
|
3236
|
+
function runLayeredDepsRule(rule, ctx) {
|
|
3237
|
+
const packageLayer = layerIndex(rule.layers);
|
|
3238
|
+
const packageOf = longestPrefixMatcher([...packageLayer.keys()]);
|
|
3239
|
+
const out = [];
|
|
3240
|
+
for (const edge of ctx.edges) {
|
|
3241
|
+
if (!isImportEdge(edge)) continue;
|
|
3242
|
+
const srcPkg = packageOf(edge.srcId);
|
|
3243
|
+
const dstPkg = packageOf(edge.dstId);
|
|
3244
|
+
if (srcPkg === null || dstPkg === null) continue;
|
|
3245
|
+
const srcLayer = packageLayer.get(srcPkg);
|
|
3246
|
+
const dstLayer = packageLayer.get(dstPkg);
|
|
3247
|
+
if (srcLayer >= dstLayer) continue;
|
|
3248
|
+
out.push({
|
|
3249
|
+
ruleId: rule.id,
|
|
3250
|
+
severity: severityOf(rule),
|
|
3251
|
+
nodeId: edge.srcId,
|
|
3252
|
+
destinationId: edge.dstId,
|
|
3253
|
+
message: `${srcPkg} (layer ${srcLayer}) imports ${dstPkg} (layer ${dstLayer})`
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
return out;
|
|
3257
|
+
}
|
|
3258
|
+
function runForbidImportRule(rule, ctx) {
|
|
3259
|
+
const fromRx = compilePatterns([rule.from]);
|
|
3260
|
+
const toRx = compilePatterns([rule.to]);
|
|
3261
|
+
const out = [];
|
|
3262
|
+
for (const edge of ctx.edges) {
|
|
3263
|
+
if (!isImportEdge(edge)) continue;
|
|
3264
|
+
if (!matchesAny(edge.srcId, fromRx)) continue;
|
|
3265
|
+
if (!matchesAny(edge.dstId, toRx)) continue;
|
|
3266
|
+
out.push({
|
|
3267
|
+
ruleId: rule.id,
|
|
3268
|
+
severity: severityOf(rule),
|
|
3269
|
+
nodeId: edge.srcId,
|
|
3270
|
+
destinationId: edge.dstId,
|
|
3271
|
+
message: `${edge.srcId} imports ${edge.dstId} (forbidden: ${rule.from} \u2192 ${rule.to})`
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
return out;
|
|
3275
|
+
}
|
|
3276
|
+
function importersByDestination(edges) {
|
|
3277
|
+
const importersByDst = /* @__PURE__ */ new Map();
|
|
3278
|
+
for (const edge of edges) {
|
|
3279
|
+
if (!isImportEdge(edge)) continue;
|
|
3280
|
+
let list = importersByDst.get(edge.dstId);
|
|
3281
|
+
if (!list) {
|
|
3282
|
+
list = [];
|
|
3283
|
+
importersByDst.set(edge.dstId, list);
|
|
3284
|
+
}
|
|
3285
|
+
list.push(edge.srcId);
|
|
3286
|
+
}
|
|
3287
|
+
return importersByDst;
|
|
3288
|
+
}
|
|
3289
|
+
function runNoInternalOnlyBarrelsRule(rule, ctx) {
|
|
3290
|
+
const packageOf = longestPrefixMatcher(rule.packageRoots);
|
|
3291
|
+
const excluders = compilePatterns(rule.exclude);
|
|
3292
|
+
const importersByDst = importersByDestination(ctx.edges);
|
|
3293
|
+
const out = [];
|
|
3294
|
+
for (const node of ctx.nodes) {
|
|
3295
|
+
if (node.kind !== "file") continue;
|
|
3296
|
+
if (node.role !== "barrel") continue;
|
|
3297
|
+
if (matchesAny(node.id, excluders)) continue;
|
|
3298
|
+
const barrelPkg = packageOf(node.id);
|
|
3299
|
+
if (barrelPkg === null) continue;
|
|
3300
|
+
const importers = importersByDst.get(node.id) ?? [];
|
|
3301
|
+
const externalCount = importers.reduce((n, src) => packageOf(src) !== barrelPkg ? n + 1 : n, 0);
|
|
3302
|
+
if (externalCount > 0) continue;
|
|
3303
|
+
out.push({
|
|
3304
|
+
ruleId: rule.id,
|
|
3305
|
+
severity: severityOf(rule),
|
|
3306
|
+
nodeId: node.id,
|
|
3307
|
+
message: barrelMessage(barrelPkg, importers.length)
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
return out;
|
|
3311
|
+
}
|
|
3312
|
+
function barrelMessage(barrelPkg, importerCount) {
|
|
3313
|
+
return importerCount === 0 ? `barrel in package "${barrelPkg}" has no importers` : `barrel in package "${barrelPkg}" has ${importerCount} internal importer(s) but zero external`;
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
// src/check/metric-rules.ts
|
|
3317
|
+
function nodeFilter(rule) {
|
|
3318
|
+
return {
|
|
3319
|
+
kind: rule.kind,
|
|
3320
|
+
excluders: compilePatterns(rule.exclude),
|
|
3321
|
+
excludedRoles: new Set(rule.excludeRoles ?? [])
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
function passesFilter(node, filter) {
|
|
3325
|
+
if (filter.kind && node.kind !== filter.kind) return false;
|
|
3326
|
+
if (matchesAny(node.id, filter.excluders)) return false;
|
|
3327
|
+
if (node.role && filter.excludedRoles.has(node.role)) return false;
|
|
3328
|
+
return true;
|
|
3329
|
+
}
|
|
3330
|
+
function runMetricMaxRule(rule, ctx) {
|
|
3331
|
+
const filter = nodeFilter(rule);
|
|
3332
|
+
const out = [];
|
|
3333
|
+
for (const node of ctx.nodes) {
|
|
3334
|
+
if (!passesFilter(node, filter)) continue;
|
|
3335
|
+
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
3336
|
+
if (value === void 0) continue;
|
|
3337
|
+
if (value <= rule.max) continue;
|
|
3338
|
+
out.push({
|
|
3339
|
+
ruleId: rule.id,
|
|
3340
|
+
severity: severityOf(rule),
|
|
3341
|
+
nodeId: node.id,
|
|
3342
|
+
metric: rule.metric,
|
|
3343
|
+
value,
|
|
3344
|
+
threshold: rule.max,
|
|
3345
|
+
message: `${rule.metric}=${formatNumber(value)} > ${formatNumber(rule.max)}`
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
return out;
|
|
3349
|
+
}
|
|
3350
|
+
function runMetricMinRule(rule, ctx) {
|
|
3351
|
+
const filter = nodeFilter(rule);
|
|
3352
|
+
const out = [];
|
|
3353
|
+
for (const node of ctx.nodes) {
|
|
3354
|
+
if (!passesFilter(node, filter)) continue;
|
|
3355
|
+
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
3356
|
+
if (value === void 0) continue;
|
|
3357
|
+
if (value >= rule.min) continue;
|
|
3358
|
+
out.push({
|
|
3359
|
+
ruleId: rule.id,
|
|
3360
|
+
severity: severityOf(rule),
|
|
3361
|
+
nodeId: node.id,
|
|
3362
|
+
metric: rule.metric,
|
|
3363
|
+
value,
|
|
3364
|
+
threshold: rule.min,
|
|
3365
|
+
message: `${rule.metric}=${formatNumber(value)} < ${formatNumber(rule.min)}`
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
return out;
|
|
3369
|
+
}
|
|
3370
|
+
function runMetricProductMaxRule(rule, ctx) {
|
|
3371
|
+
const filter = nodeFilter(rule);
|
|
3372
|
+
const out = [];
|
|
3373
|
+
for (const node of ctx.nodes) {
|
|
3374
|
+
if (!passesFilter(node, filter)) continue;
|
|
3375
|
+
const inner = ctx.metricsByNode.get(node.id);
|
|
3376
|
+
if (!inner) continue;
|
|
3377
|
+
const components = collectComponents(rule.metrics, inner);
|
|
3378
|
+
if (!components) continue;
|
|
3379
|
+
const product = components.reduce((a, b) => a * b, 1);
|
|
3380
|
+
if (product <= rule.max) continue;
|
|
3381
|
+
out.push(productViolation(rule, node.id, components, product));
|
|
3382
|
+
}
|
|
3383
|
+
return out;
|
|
3384
|
+
}
|
|
3385
|
+
function productViolation(rule, nodeId, components, product) {
|
|
3386
|
+
const detail = rule.metrics.map((m, i) => `${m}=${formatNumber(components[i])}`).join(" * ");
|
|
3387
|
+
return {
|
|
3388
|
+
ruleId: rule.id,
|
|
3389
|
+
severity: severityOf(rule),
|
|
3390
|
+
nodeId,
|
|
3391
|
+
metric: rule.metrics.join(" * "),
|
|
3392
|
+
value: product,
|
|
3393
|
+
threshold: rule.max,
|
|
3394
|
+
message: `${detail} = ${formatNumber(product)} > ${formatNumber(rule.max)}`
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
function collectComponents(metrics, values) {
|
|
3398
|
+
const out = [];
|
|
3399
|
+
for (const m of metrics) {
|
|
3400
|
+
const v = values.get(m);
|
|
3401
|
+
if (v === void 0) return null;
|
|
3402
|
+
out.push(v);
|
|
3403
|
+
}
|
|
3404
|
+
return out;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
// src/check/rules.ts
|
|
3408
|
+
function runRule(rule, ctx) {
|
|
3409
|
+
switch (rule.type) {
|
|
3410
|
+
case "metric-max":
|
|
3411
|
+
return runMetricMaxRule(rule, ctx);
|
|
3412
|
+
case "metric-min":
|
|
3413
|
+
return runMetricMinRule(rule, ctx);
|
|
3414
|
+
case "metric-product-max":
|
|
3415
|
+
return runMetricProductMaxRule(rule, ctx);
|
|
3416
|
+
case "forbid-import":
|
|
3417
|
+
return runForbidImportRule(rule, ctx);
|
|
3418
|
+
case "layered-deps":
|
|
3419
|
+
return runLayeredDepsRule(rule, ctx);
|
|
3420
|
+
case "no-internal-only-barrels":
|
|
3421
|
+
return runNoInternalOnlyBarrelsRule(rule, ctx);
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
// src/check/check.ts
|
|
3426
|
+
function runChecks(store, options) {
|
|
3427
|
+
const ctx = buildRuleContext(store, options.snapshotId);
|
|
3428
|
+
const violations = [];
|
|
3429
|
+
for (const rule of options.rules) {
|
|
3430
|
+
violations.push(...runRule(rule, ctx));
|
|
3431
|
+
}
|
|
3432
|
+
if (options.baselineSnapshotId) {
|
|
3433
|
+
const baselineKeys = collectBaselineKeys(store, options.baselineSnapshotId, options.rules);
|
|
3434
|
+
for (const v of violations) {
|
|
3435
|
+
if (baselineKeys.has(violationKey(v))) v.isCarryover = true;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
const counts = countByOriginAndSeverity(violations);
|
|
3439
|
+
return {
|
|
3440
|
+
snapshotId: options.snapshotId,
|
|
3441
|
+
baselineSnapshotId: options.baselineSnapshotId,
|
|
3442
|
+
rulesEvaluated: options.rules.length,
|
|
3443
|
+
nodesEvaluated: ctx.nodes.length,
|
|
3444
|
+
violations,
|
|
3445
|
+
...counts,
|
|
3446
|
+
passed: counts.newErrors === 0
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
function collectBaselineKeys(store, baselineSnapshotId, rules) {
|
|
3450
|
+
const ctx = buildRuleContext(store, baselineSnapshotId);
|
|
3451
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3452
|
+
for (const rule of rules) {
|
|
3453
|
+
for (const v of runRule(rule, ctx)) keys.add(violationKey(v));
|
|
3454
|
+
}
|
|
3455
|
+
return keys;
|
|
3456
|
+
}
|
|
3457
|
+
function violationKey(v) {
|
|
3458
|
+
return v.destinationId ? `${v.ruleId}|${v.nodeId}|${v.destinationId}` : `${v.ruleId}|${v.nodeId}`;
|
|
3459
|
+
}
|
|
3460
|
+
function countByOriginAndSeverity(violations) {
|
|
3461
|
+
const counts = { newErrors: 0, newWarnings: 0, carryoverErrors: 0, carryoverWarnings: 0 };
|
|
3462
|
+
for (const v of violations) {
|
|
3463
|
+
const isError = v.severity === "error";
|
|
3464
|
+
if (v.isCarryover) {
|
|
3465
|
+
if (isError) counts.carryoverErrors++;
|
|
3466
|
+
else counts.carryoverWarnings++;
|
|
3467
|
+
} else {
|
|
3468
|
+
if (isError) counts.newErrors++;
|
|
3469
|
+
else counts.newWarnings++;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
return counts;
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// src/check/validate.ts
|
|
3476
|
+
function validateRules(input, options = {}) {
|
|
3477
|
+
if (!input || typeof input !== "object") {
|
|
3478
|
+
throw new Error("rules file must be an object with a `rules` array");
|
|
3479
|
+
}
|
|
3480
|
+
const obj = input;
|
|
3481
|
+
if (!Array.isArray(obj.rules)) {
|
|
3482
|
+
throw new Error("rules file must have a `rules` array");
|
|
3483
|
+
}
|
|
3484
|
+
const warn = options.onWarn ?? (() => {
|
|
3485
|
+
});
|
|
3486
|
+
return obj.rules.map((r, i) => validateRule(r, i, warn));
|
|
3487
|
+
}
|
|
3488
|
+
function healMetricName(name, ruleId, warn) {
|
|
3489
|
+
const target = metricAliasTarget(name);
|
|
3490
|
+
if (target) {
|
|
3491
|
+
warn(`${ruleId}: metric "${name}" is deprecated \u2014 using "${target}"`);
|
|
3492
|
+
return target;
|
|
3493
|
+
}
|
|
3494
|
+
return name;
|
|
3495
|
+
}
|
|
3496
|
+
function validateRule(raw, index, warn) {
|
|
3497
|
+
if (!raw || typeof raw !== "object") {
|
|
3498
|
+
throw new Error(`rule[${index}] must be an object`);
|
|
3499
|
+
}
|
|
3500
|
+
const r = raw;
|
|
3501
|
+
if (typeof r.id !== "string" || !r.id) {
|
|
3502
|
+
throw new Error(`rule[${index}] missing string id`);
|
|
3503
|
+
}
|
|
3504
|
+
if (typeof r.type !== "string") {
|
|
3505
|
+
throw new Error(`rule[${index}] (${r.id}) missing string type`);
|
|
3506
|
+
}
|
|
3507
|
+
switch (r.type) {
|
|
3508
|
+
case "metric-max":
|
|
3509
|
+
return assertMetricMax(r, warn);
|
|
3510
|
+
case "metric-min":
|
|
3511
|
+
return assertMetricMin(r, warn);
|
|
3512
|
+
case "metric-product-max":
|
|
3513
|
+
return assertMetricProductMax(r, warn);
|
|
3514
|
+
case "forbid-import":
|
|
3515
|
+
return assertForbidImport(r);
|
|
3516
|
+
case "layered-deps":
|
|
3517
|
+
return assertLayeredDeps(r);
|
|
3518
|
+
case "no-internal-only-barrels":
|
|
3519
|
+
return assertNoInternalOnlyBarrels(r);
|
|
3520
|
+
default:
|
|
3521
|
+
throw new Error(`rule[${index}] (${r.id}) unknown type "${r.type}"`);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
function assertNoInternalOnlyBarrels(r) {
|
|
3525
|
+
if (!Array.isArray(r.packageRoots) || r.packageRoots.length === 0) {
|
|
3526
|
+
throw new Error(
|
|
3527
|
+
`${r.id}: packageRoots must be a non-empty array of path-prefix strings`
|
|
3528
|
+
);
|
|
3529
|
+
}
|
|
3530
|
+
for (const p of r.packageRoots) {
|
|
3531
|
+
if (typeof p !== "string" || !p) {
|
|
3532
|
+
throw new Error(`${r.id}: each packageRoots entry must be a non-empty string`);
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
return {
|
|
3536
|
+
type: "no-internal-only-barrels",
|
|
3537
|
+
id: r.id,
|
|
3538
|
+
packageRoots: r.packageRoots,
|
|
3539
|
+
severity: r.severity,
|
|
3540
|
+
exclude: parseStringArray(r.exclude)
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
function assertLayeredDeps(r) {
|
|
3544
|
+
if (!Array.isArray(r.layers) || r.layers.length < 2) {
|
|
3545
|
+
throw new Error(`${r.id}: layers must be an array of 2+ string arrays`);
|
|
3546
|
+
}
|
|
3547
|
+
for (const layer of r.layers) {
|
|
3548
|
+
if (!Array.isArray(layer) || !layer.every((p) => typeof p === "string")) {
|
|
3549
|
+
throw new Error(`${r.id}: each layer must be a string[]`);
|
|
3550
|
+
}
|
|
3551
|
+
if (layer.length === 0) {
|
|
3552
|
+
throw new Error(`${r.id}: empty layers not allowed`);
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3556
|
+
for (const layer of r.layers) {
|
|
3557
|
+
for (const pkg of layer) {
|
|
3558
|
+
if (seen.has(pkg)) {
|
|
3559
|
+
throw new Error(`${r.id}: package "${pkg}" appears in more than one layer`);
|
|
3560
|
+
}
|
|
3561
|
+
seen.add(pkg);
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
return {
|
|
3565
|
+
type: "layered-deps",
|
|
3566
|
+
id: r.id,
|
|
3567
|
+
layers: r.layers,
|
|
3568
|
+
severity: r.severity
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
function assertMetricProductMax(r, warn) {
|
|
3572
|
+
if (!Array.isArray(r.metrics) || r.metrics.length < 2) {
|
|
3573
|
+
throw new Error(`${r.id}: metrics must be an array of 2+ metric names`);
|
|
3574
|
+
}
|
|
3575
|
+
if (!r.metrics.every((m) => typeof m === "string")) {
|
|
3576
|
+
throw new Error(`${r.id}: metrics must be strings`);
|
|
3577
|
+
}
|
|
3578
|
+
if (typeof r.max !== "number") {
|
|
3579
|
+
throw new Error(`${r.id}: max must be a number`);
|
|
3580
|
+
}
|
|
3581
|
+
const ruleId = r.id;
|
|
3582
|
+
return {
|
|
3583
|
+
type: "metric-product-max",
|
|
3584
|
+
id: ruleId,
|
|
3585
|
+
metrics: r.metrics.map((m) => healMetricName(m, ruleId, warn)),
|
|
3586
|
+
max: r.max,
|
|
3587
|
+
kind: r.kind,
|
|
3588
|
+
severity: r.severity,
|
|
3589
|
+
exclude: parseStringArray(r.exclude),
|
|
3590
|
+
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
function assertMetricMax(r, warn) {
|
|
3594
|
+
if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
|
|
3595
|
+
if (typeof r.max !== "number") throw new Error(`${r.id}: max must be a number`);
|
|
3596
|
+
const ruleId = r.id;
|
|
3597
|
+
return {
|
|
3598
|
+
type: "metric-max",
|
|
3599
|
+
id: ruleId,
|
|
3600
|
+
metric: healMetricName(r.metric, ruleId, warn),
|
|
3601
|
+
max: r.max,
|
|
3602
|
+
kind: r.kind,
|
|
3603
|
+
severity: r.severity,
|
|
3604
|
+
exclude: parseStringArray(r.exclude),
|
|
3605
|
+
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
function assertMetricMin(r, warn) {
|
|
3609
|
+
if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
|
|
3610
|
+
if (typeof r.min !== "number") throw new Error(`${r.id}: min must be a number`);
|
|
3611
|
+
const ruleId = r.id;
|
|
3612
|
+
return {
|
|
3613
|
+
type: "metric-min",
|
|
3614
|
+
id: ruleId,
|
|
3615
|
+
metric: healMetricName(r.metric, ruleId, warn),
|
|
3616
|
+
min: r.min,
|
|
3617
|
+
kind: r.kind,
|
|
3618
|
+
severity: r.severity,
|
|
3619
|
+
exclude: parseStringArray(r.exclude),
|
|
3620
|
+
excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
function assertForbidImport(r) {
|
|
3624
|
+
if (typeof r.from !== "string") throw new Error(`${r.id}: from must be a string`);
|
|
3625
|
+
if (typeof r.to !== "string") throw new Error(`${r.id}: to must be a string`);
|
|
3626
|
+
return {
|
|
3627
|
+
type: "forbid-import",
|
|
3628
|
+
id: r.id,
|
|
3629
|
+
from: r.from,
|
|
3630
|
+
to: r.to,
|
|
3631
|
+
severity: r.severity
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
function parseStringArray(value) {
|
|
3635
|
+
return Array.isArray(value) ? value : void 0;
|
|
3636
|
+
}
|
|
3637
|
+
var ROLE_VALUES = /* @__PURE__ */ new Set([
|
|
3638
|
+
"test",
|
|
3639
|
+
"fixture",
|
|
3640
|
+
"barrel",
|
|
3641
|
+
"types",
|
|
3642
|
+
"config",
|
|
3643
|
+
"entry",
|
|
3644
|
+
"source"
|
|
3645
|
+
]);
|
|
3646
|
+
function parseRoleArray(ruleId, value, warn) {
|
|
3647
|
+
if (value === void 0) return void 0;
|
|
3648
|
+
if (!Array.isArray(value)) {
|
|
3649
|
+
throw new Error(`${ruleId}: excludeRoles must be an array`);
|
|
3650
|
+
}
|
|
3651
|
+
return value.map((entry) => healRole(entry, ruleId, warn));
|
|
3652
|
+
}
|
|
3653
|
+
function healRole(entry, ruleId, warn) {
|
|
3654
|
+
if (typeof entry === "string" && ROLE_VALUES.has(entry)) {
|
|
3655
|
+
return entry;
|
|
3656
|
+
}
|
|
3657
|
+
if (typeof entry === "string") {
|
|
3658
|
+
const target = roleAliasTarget(entry);
|
|
3659
|
+
if (target) {
|
|
3660
|
+
warn(`${ruleId}: role "${entry}" is deprecated \u2014 using "${target}"`);
|
|
3661
|
+
return target;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
throw new Error(
|
|
3665
|
+
`${ruleId}: unknown role "${entry}" \u2014 valid: ${[...ROLE_VALUES].join(", ")}`
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// src/check/run.ts
|
|
3670
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
3671
|
+
function resolveSnapshot(store, spec) {
|
|
3672
|
+
if (typeof spec === "number" || /^\d+$/.test(spec)) {
|
|
3673
|
+
const snap2 = store.getSnapshot(Number(spec));
|
|
3674
|
+
if (!snap2) throw new Error(`no snapshot with id ${spec}`);
|
|
3675
|
+
return snap2;
|
|
3676
|
+
}
|
|
3677
|
+
const snap = store.getLatestSnapshotByRef(spec);
|
|
3678
|
+
if (!snap) throw new Error(`no snapshot found for ref "${spec}"`);
|
|
3679
|
+
return snap;
|
|
3680
|
+
}
|
|
3681
|
+
function checkSnapshot(store, options) {
|
|
3682
|
+
const snapshot = resolveSnapshot(store, options.snapshot);
|
|
3683
|
+
const baselineSnapshot = options.baseline !== void 0 ? resolveSnapshot(store, options.baseline) : void 0;
|
|
3684
|
+
const result = runChecks(store, {
|
|
3685
|
+
snapshotId: snapshot.id,
|
|
3686
|
+
rules: options.rules,
|
|
3687
|
+
baselineSnapshotId: baselineSnapshot?.id
|
|
3688
|
+
});
|
|
3689
|
+
return { snapshot, baselineSnapshot, result };
|
|
3690
|
+
}
|
|
3691
|
+
async function loadCheckRules(path10, options = {}) {
|
|
3692
|
+
let raw;
|
|
3693
|
+
try {
|
|
3694
|
+
raw = await readFile2(path10, "utf8");
|
|
3695
|
+
} catch (err) {
|
|
3696
|
+
throw new Error(`Cannot read rules file at ${path10}: ${errorMessage(err)}`);
|
|
3697
|
+
}
|
|
3698
|
+
let parsed;
|
|
3699
|
+
try {
|
|
3700
|
+
parsed = JSON.parse(raw);
|
|
3701
|
+
} catch (err) {
|
|
3702
|
+
throw new Error(`Invalid JSON in ${path10}: ${errorMessage(err)}`);
|
|
3703
|
+
}
|
|
3704
|
+
return validateRules(parsed, options);
|
|
3705
|
+
}
|
|
3706
|
+
function errorMessage(err) {
|
|
3707
|
+
return err instanceof Error ? err.message : String(err);
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
// src/diff/diff.ts
|
|
3711
|
+
function aliasMap(aliases) {
|
|
3712
|
+
const m = /* @__PURE__ */ new Map();
|
|
3713
|
+
for (const a of aliases) m.set(a.oldId, a);
|
|
3714
|
+
return m;
|
|
3715
|
+
}
|
|
3716
|
+
function resolveId(id, aliases) {
|
|
3717
|
+
return aliases.get(id)?.newId ?? id;
|
|
3718
|
+
}
|
|
3719
|
+
function edgeKey2(srcId, dstId, kind) {
|
|
3720
|
+
return JSON.stringify([srcId, dstId, canonicalEdgeKind(kind)]);
|
|
3721
|
+
}
|
|
3722
|
+
function metricKey(nodeId, name) {
|
|
3723
|
+
return JSON.stringify([nodeId, canonicalMetricName(name)]);
|
|
3724
|
+
}
|
|
3725
|
+
function indexEdges(edges) {
|
|
3726
|
+
const m = /* @__PURE__ */ new Map();
|
|
3727
|
+
for (const e of edges) m.set(edgeKey2(e.srcId, e.dstId, e.kind), e);
|
|
3728
|
+
return m;
|
|
3729
|
+
}
|
|
3730
|
+
function diffNodes(fromNodes, toNodes, aliases) {
|
|
3731
|
+
const toIndex = new Map(toNodes.map((n) => [n.id, n]));
|
|
3732
|
+
const matchedToIds = /* @__PURE__ */ new Set();
|
|
3733
|
+
const removed = [];
|
|
3734
|
+
const renamed = [];
|
|
3735
|
+
for (const fromNode of fromNodes) {
|
|
3736
|
+
const newId = resolveId(fromNode.id, aliases);
|
|
3737
|
+
const toNode = toIndex.get(newId);
|
|
3738
|
+
if (!toNode) {
|
|
3739
|
+
removed.push(fromNode);
|
|
3740
|
+
continue;
|
|
3741
|
+
}
|
|
3742
|
+
matchedToIds.add(newId);
|
|
3743
|
+
if (newId !== fromNode.id) {
|
|
3744
|
+
renamed.push({ oldId: fromNode.id, newId, reason: aliases.get(fromNode.id).reason, node: toNode });
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
const added = toNodes.filter((toNode) => !matchedToIds.has(toNode.id));
|
|
3748
|
+
return { added, removed, renamed, commonNewIds: matchedToIds };
|
|
3749
|
+
}
|
|
3750
|
+
function diffEdges(fromEdges, toEdges, aliases) {
|
|
3751
|
+
const remappedKey = (e) => edgeKey2(resolveId(e.srcId, aliases), resolveId(e.dstId, aliases), e.kind);
|
|
3752
|
+
const fromKeys = new Set(fromEdges.map(remappedKey));
|
|
3753
|
+
const toIndex = indexEdges(toEdges);
|
|
3754
|
+
const added = [];
|
|
3755
|
+
for (const [k, e] of toIndex) {
|
|
3756
|
+
if (!fromKeys.has(k)) added.push(e);
|
|
3757
|
+
}
|
|
3758
|
+
const removed = fromEdges.filter((e) => !toIndex.has(remappedKey(e)));
|
|
3759
|
+
return { added, removed };
|
|
3760
|
+
}
|
|
3761
|
+
function indexFromMetrics(metrics, aliases) {
|
|
3762
|
+
const fromIndex = /* @__PURE__ */ new Map();
|
|
3763
|
+
for (const m of metrics) {
|
|
3764
|
+
const newId = resolveId(m.nodeId, aliases);
|
|
3765
|
+
fromIndex.set(metricKey(newId, m.name), { metric: m, nodeId: newId });
|
|
3766
|
+
}
|
|
3767
|
+
return fromIndex;
|
|
3768
|
+
}
|
|
3769
|
+
function deltaOf(before, after) {
|
|
3770
|
+
if (before === null || after === null) return null;
|
|
3771
|
+
return after - before;
|
|
3772
|
+
}
|
|
3773
|
+
function metricDelta(nodeId, name, before, after) {
|
|
3774
|
+
return { nodeId, name: canonicalMetricName(name), before, after, delta: deltaOf(before, after) };
|
|
3775
|
+
}
|
|
3776
|
+
function diffMetrics(fromMetrics, toMetrics, aliases, commonNewIds) {
|
|
3777
|
+
const fromIndex = indexFromMetrics(fromMetrics, aliases);
|
|
3778
|
+
const toIndex = new Map(toMetrics.map((m) => [metricKey(m.nodeId, m.name), m]));
|
|
3779
|
+
const out = [];
|
|
3780
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3781
|
+
for (const [key, { metric: before, nodeId }] of fromIndex) {
|
|
3782
|
+
if (!commonNewIds.has(nodeId)) continue;
|
|
3783
|
+
seen.add(key);
|
|
3784
|
+
const afterVal = toIndex.get(key)?.value ?? null;
|
|
3785
|
+
if (before.value !== afterVal) out.push(metricDelta(nodeId, before.name, before.value, afterVal));
|
|
3786
|
+
}
|
|
3787
|
+
for (const [key, after] of toIndex) {
|
|
3788
|
+
if (seen.has(key) || !commonNewIds.has(after.nodeId)) continue;
|
|
3789
|
+
out.push(metricDelta(after.nodeId, after.name, null, after.value));
|
|
3790
|
+
}
|
|
3791
|
+
return out;
|
|
3792
|
+
}
|
|
3793
|
+
function diffSnapshots(store, options) {
|
|
3794
|
+
const { fromSnapshotId, toSnapshotId } = options;
|
|
3795
|
+
const aliases = aliasMap(store.listAliases(toSnapshotId));
|
|
3796
|
+
const nodeDiff = diffNodes(store.listNodes(fromSnapshotId), store.listNodes(toSnapshotId), aliases);
|
|
3797
|
+
const edgeDiff = diffEdges(store.listEdges(fromSnapshotId), store.listEdges(toSnapshotId), aliases);
|
|
3798
|
+
const metricDeltas = diffMetrics(
|
|
3799
|
+
store.listMetrics(fromSnapshotId),
|
|
3800
|
+
store.listMetrics(toSnapshotId),
|
|
3801
|
+
aliases,
|
|
3802
|
+
nodeDiff.commonNewIds
|
|
3803
|
+
);
|
|
3804
|
+
return {
|
|
3805
|
+
summary: summarize(options, nodeDiff, edgeDiff, metricDeltas),
|
|
3806
|
+
addedNodes: nodeDiff.added,
|
|
3807
|
+
removedNodes: nodeDiff.removed,
|
|
3808
|
+
renamedNodes: nodeDiff.renamed,
|
|
3809
|
+
addedEdges: edgeDiff.added,
|
|
3810
|
+
removedEdges: edgeDiff.removed,
|
|
3811
|
+
metricDeltas
|
|
3812
|
+
};
|
|
3813
|
+
}
|
|
3814
|
+
function summarize({ fromSnapshotId, toSnapshotId }, nodeDiff, edgeDiff, metricDeltas) {
|
|
3815
|
+
return {
|
|
3816
|
+
fromSnapshotId,
|
|
3817
|
+
toSnapshotId,
|
|
3818
|
+
addedNodes: nodeDiff.added.length,
|
|
3819
|
+
removedNodes: nodeDiff.removed.length,
|
|
3820
|
+
renamedNodes: nodeDiff.renamed.length,
|
|
3821
|
+
unchangedNodes: nodeDiff.commonNewIds.size - nodeDiff.renamed.length,
|
|
3822
|
+
addedEdges: edgeDiff.added.length,
|
|
3823
|
+
removedEdges: edgeDiff.removed.length,
|
|
3824
|
+
metricChanges: metricDeltas.length
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
// src/diff/check-diff.ts
|
|
3829
|
+
function diffCheckResults(store, options) {
|
|
3830
|
+
const from = runChecks(store, { snapshotId: options.fromSnapshotId, rules: options.rules });
|
|
3831
|
+
const to = runChecks(store, { snapshotId: options.toSnapshotId, rules: options.rules });
|
|
3832
|
+
return {
|
|
3833
|
+
fromSnapshotId: options.fromSnapshotId,
|
|
3834
|
+
toSnapshotId: options.toSnapshotId,
|
|
3835
|
+
rulesEvaluated: options.rules.length,
|
|
3836
|
+
...bucketViolations(indexByKey(from.violations), indexByKey(to.violations))
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
function bucketViolations(fromByKey, toByKey) {
|
|
3840
|
+
const buckets = { newViolations: [], resolvedViolations: [], unchanged: [], worsened: [], improved: [] };
|
|
3841
|
+
for (const [key, toV] of toByKey) {
|
|
3842
|
+
const fromV = fromByKey.get(key);
|
|
3843
|
+
if (!fromV) {
|
|
3844
|
+
buckets.newViolations.push(toV);
|
|
3845
|
+
continue;
|
|
3846
|
+
}
|
|
3847
|
+
const delta = computeDelta(fromV, toV);
|
|
3848
|
+
const entry = { from: fromV, to: toV, delta };
|
|
3849
|
+
buckets.unchanged.push(entry);
|
|
3850
|
+
if (delta !== null && delta > 0) buckets.worsened.push(entry);
|
|
3851
|
+
else if (delta !== null && delta < 0) buckets.improved.push(entry);
|
|
3852
|
+
}
|
|
3853
|
+
for (const [key, fromV] of fromByKey) {
|
|
3854
|
+
if (!toByKey.has(key)) buckets.resolvedViolations.push(fromV);
|
|
3855
|
+
}
|
|
3856
|
+
return buckets;
|
|
3857
|
+
}
|
|
3858
|
+
function indexByKey(violations) {
|
|
3859
|
+
const out = /* @__PURE__ */ new Map();
|
|
3860
|
+
for (const v of violations) out.set(violationKey(v), v);
|
|
3861
|
+
return out;
|
|
3862
|
+
}
|
|
3863
|
+
function computeDelta(from, to) {
|
|
3864
|
+
if (from.value === void 0 || to.value === void 0) return null;
|
|
3865
|
+
return to.value - from.value;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// src/embeddings/corpus.ts
|
|
3869
|
+
import { contentHashOf } from "@titan-design/store-sqlite";
|
|
3870
|
+
function buildEmbedText(signature, purpose) {
|
|
3871
|
+
return purpose ? `${signature} -- ${purpose}` : signature;
|
|
3872
|
+
}
|
|
3873
|
+
function hashEmbedText(text) {
|
|
3874
|
+
return contentHashOf(text);
|
|
3875
|
+
}
|
|
3876
|
+
var TEST_OR_FIXTURE_PATH = /\.test\.|\.spec\.|__tests__|\/fixtures\//;
|
|
3877
|
+
var EXCLUDED_FILE_ROLES = /* @__PURE__ */ new Set(["test", "generated"]);
|
|
3878
|
+
function listEmbeddableSymbols(store, snapshotId) {
|
|
3879
|
+
const nodes = store.listNodes(snapshotId, { includeSymbols: true });
|
|
3880
|
+
const fileRoles = /* @__PURE__ */ new Map();
|
|
3881
|
+
for (const n of nodes) if (n.kind === "file") fileRoles.set(n.id, n.role ?? null);
|
|
3882
|
+
const out = [];
|
|
3883
|
+
for (const n of nodes) {
|
|
3884
|
+
const symbol = toEmbeddable(n, fileRoles);
|
|
3885
|
+
if (symbol) out.push(symbol);
|
|
3886
|
+
}
|
|
3887
|
+
return out;
|
|
3888
|
+
}
|
|
3889
|
+
function toEmbeddable(node, fileRoles) {
|
|
3890
|
+
if (node.kind !== "symbol") return null;
|
|
3891
|
+
const attrs = node.attrs ?? {};
|
|
3892
|
+
if (attrs.exported !== true || !attrs.signature) return null;
|
|
3893
|
+
const file = node.parentId ?? node.id.split("#")[0] ?? node.id;
|
|
3894
|
+
if (TEST_OR_FIXTURE_PATH.test(file)) return null;
|
|
3895
|
+
const role = fileRoles.get(file);
|
|
3896
|
+
if (role && EXCLUDED_FILE_ROLES.has(role)) return null;
|
|
3897
|
+
const text = buildEmbedText(attrs.signature, attrs.purpose);
|
|
3898
|
+
return {
|
|
3899
|
+
id: node.id,
|
|
3900
|
+
name: node.name,
|
|
3901
|
+
file,
|
|
3902
|
+
signature: attrs.signature,
|
|
3903
|
+
purpose: attrs.purpose,
|
|
3904
|
+
text,
|
|
3905
|
+
textHash: hashEmbedText(text)
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
// src/embeddings/cache.ts
|
|
3910
|
+
import { fromFloat32Buffer, normalize, toFloat32Buffer } from "@titan-design/embed";
|
|
3911
|
+
import { CacheBlobTable } from "@titan-design/store-sqlite";
|
|
3912
|
+
var SYMBOL_EMBEDDING_NAMESPACE = "code-graph/symbol-embedding";
|
|
3913
|
+
var EMBED_BATCH_SIZE = 64;
|
|
3914
|
+
function symbolVectorCache(store) {
|
|
3915
|
+
return new CacheBlobTable(store.db, { name: KIT.cacheBlob });
|
|
3916
|
+
}
|
|
3917
|
+
function loadVectorsByHash(store, model, hashes) {
|
|
3918
|
+
const cache = symbolVectorCache(store);
|
|
3919
|
+
const found = /* @__PURE__ */ new Map();
|
|
3920
|
+
for (const contentHash of new Set(hashes)) {
|
|
3921
|
+
const hit = cache.get({ namespace: SYMBOL_EMBEDDING_NAMESPACE, model, contentHash });
|
|
3922
|
+
if (hit) found.set(contentHash, fromFloat32Buffer(hit.value));
|
|
3923
|
+
}
|
|
3924
|
+
return found;
|
|
3925
|
+
}
|
|
3926
|
+
function storeVectors(store, model, rows) {
|
|
3927
|
+
const cache = symbolVectorCache(store);
|
|
3928
|
+
store.db.transaction(() => {
|
|
3929
|
+
for (const r of rows) {
|
|
3930
|
+
const key = { namespace: SYMBOL_EMBEDDING_NAMESPACE, model, contentHash: r.textHash };
|
|
3931
|
+
cache.put(key, toFloat32Buffer(r.vector), { dims: r.vector.length });
|
|
3932
|
+
}
|
|
3933
|
+
})();
|
|
3934
|
+
}
|
|
3935
|
+
async function embedTextsCached(store, embedder, texts) {
|
|
3936
|
+
const hashes = texts.map((t) => hashEmbedText(t));
|
|
3937
|
+
const byHash = loadVectorsByHash(store, embedder.model, hashes);
|
|
3938
|
+
const missing = /* @__PURE__ */ new Map();
|
|
3939
|
+
texts.forEach((text, i) => {
|
|
3940
|
+
if (!byHash.has(hashes[i])) missing.set(hashes[i], text);
|
|
3941
|
+
});
|
|
3942
|
+
const pending = [...missing.entries()];
|
|
3943
|
+
for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
|
|
3944
|
+
const rows = await embedBatch(embedder, pending.slice(i, i + EMBED_BATCH_SIZE));
|
|
3945
|
+
storeVectors(store, embedder.model, rows);
|
|
3946
|
+
for (const r of rows) byHash.set(r.textHash, r.vector);
|
|
3947
|
+
}
|
|
3948
|
+
return { byHash, newHashes: new Set(missing.keys()) };
|
|
3949
|
+
}
|
|
3950
|
+
async function embedBatch(embedder, batch) {
|
|
3951
|
+
const vectors = await embedder.embed(batch.map(([, text]) => text), { role: "document" });
|
|
3952
|
+
if (vectors.length !== batch.length) {
|
|
3953
|
+
throw new Error(`Embedder returned ${vectors.length} vectors for ${batch.length} texts`);
|
|
3954
|
+
}
|
|
3955
|
+
return batch.map(([textHash], j) => ({ textHash, vector: normalize(vectors[j]) }));
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
// src/embeddings/embeddings.ts
|
|
3959
|
+
import { BruteForceVectorIndex, vectorRetriever } from "@titan-design/retrieval";
|
|
3960
|
+
async function embedSnapshot(store, snapshotId, embedder) {
|
|
3961
|
+
const symbols = listEmbeddableSymbols(store, snapshotId);
|
|
3962
|
+
const { newHashes } = await embedTextsCached(store, embedder, symbols.map((s) => s.text));
|
|
3963
|
+
return {
|
|
3964
|
+
model: embedder.model,
|
|
3965
|
+
symbols: symbols.length,
|
|
3966
|
+
withPurpose: symbols.filter((s) => s.purpose).length,
|
|
3967
|
+
embedded: symbols.length,
|
|
3968
|
+
newlyEmbedded: newHashes.size,
|
|
3969
|
+
reused: symbols.filter((s) => !newHashes.has(s.textHash)).length
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
async function tryEmbedSnapshot(store, snapshotId, embedder) {
|
|
3973
|
+
try {
|
|
3974
|
+
return { ok: true, result: await embedSnapshot(store, snapshotId, embedder) };
|
|
3975
|
+
} catch (err) {
|
|
3976
|
+
return { ok: false, model: embedder.model, error: err instanceof Error ? err.message : String(err) };
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
async function findSimilarCapability(store, snapshotId, query, embedder, opts = {}) {
|
|
3980
|
+
const symbols = listEmbeddableSymbols(store, snapshotId);
|
|
3981
|
+
const index = buildSymbolIndex(store, embedder.model, symbols);
|
|
3982
|
+
if (index.size === 0) {
|
|
3983
|
+
throw new Error(
|
|
3984
|
+
`No capability embeddings stored for snapshot ${snapshotId} (model ${embedder.model}); run embedSnapshot first.`
|
|
3985
|
+
);
|
|
3986
|
+
}
|
|
3987
|
+
const retriever = vectorRetriever(embedder, index);
|
|
3988
|
+
const hits = await retriever.retrieve(query, { limit: opts.limit ?? 10 });
|
|
3989
|
+
const byId = new Map(symbols.map((s) => [s.id, s]));
|
|
3990
|
+
return {
|
|
3991
|
+
query,
|
|
3992
|
+
model: embedder.model,
|
|
3993
|
+
coverage: { symbols: symbols.length, embedded: index.size, withPurpose: symbols.filter((s) => s.purpose).length },
|
|
3994
|
+
candidates: hits.map((h) => toCandidate(byId.get(h.id), h.score ?? 0))
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
function buildSymbolIndex(store, model, symbols) {
|
|
3998
|
+
const vectors = loadVectorsByHash(store, model, symbols.map((s) => s.textHash));
|
|
3999
|
+
const index = new BruteForceVectorIndex();
|
|
4000
|
+
for (const s of symbols) {
|
|
4001
|
+
const vector = vectors.get(s.textHash);
|
|
4002
|
+
if (vector) index.add(s.id, vector);
|
|
4003
|
+
}
|
|
4004
|
+
return index;
|
|
4005
|
+
}
|
|
4006
|
+
function toCandidate(s, score) {
|
|
4007
|
+
return { id: s.id, name: s.name, file: s.file, signature: s.signature, purpose: s.purpose, score };
|
|
4008
|
+
}
|
|
2357
4009
|
export {
|
|
2358
4010
|
ALL_ROLES,
|
|
4011
|
+
COVERAGE_METRIC_NAME,
|
|
2359
4012
|
CodeGraphStore,
|
|
4013
|
+
DEAD_CODE_METRIC_NAMES,
|
|
2360
4014
|
DOMAIN_DDL,
|
|
4015
|
+
GROWTH_RISK_METRIC_NAMES,
|
|
2361
4016
|
INDEX_VERSION,
|
|
2362
4017
|
KIT,
|
|
2363
4018
|
LanguageExtractor,
|
|
2364
4019
|
MIGRATIONS,
|
|
2365
4020
|
PythonGraphExtractor,
|
|
4021
|
+
SCHEMA_VERSION,
|
|
2366
4022
|
SOURCE_METRIC_NAMES,
|
|
4023
|
+
SYMBOL_EMBEDDING_NAMESPACE,
|
|
2367
4024
|
SYMBOL_ID_SEP,
|
|
2368
4025
|
TsMorphGraphExtractor,
|
|
2369
4026
|
annotateRoles,
|
|
4027
|
+
attributeCoverage,
|
|
2370
4028
|
buildAliases,
|
|
4029
|
+
buildEmbedText,
|
|
2371
4030
|
buildFileModuleNodes,
|
|
2372
4031
|
buildIndexerMetrics,
|
|
2373
4032
|
canonicalEdgeKind,
|
|
2374
4033
|
canonicalMetricName,
|
|
2375
4034
|
canonicalRole,
|
|
4035
|
+
checkSnapshot,
|
|
2376
4036
|
classifyRole,
|
|
2377
4037
|
collectDeclaredNames,
|
|
2378
4038
|
collectDeclaredSpans,
|
|
4039
|
+
computeDeadCodeMetrics,
|
|
4040
|
+
computeGrowthRiskMetrics,
|
|
2379
4041
|
computeMetrics,
|
|
4042
|
+
computePageRank,
|
|
4043
|
+
computeRelevance,
|
|
2380
4044
|
computeRoleHints,
|
|
2381
4045
|
computeSourceMetrics,
|
|
4046
|
+
computeSymbolConsumers,
|
|
4047
|
+
computeSymbolCoupling,
|
|
4048
|
+
computeTestCoverageOwnership,
|
|
2382
4049
|
detectGitHead,
|
|
2383
4050
|
detectGitToplevel,
|
|
2384
4051
|
detectRenames,
|
|
4052
|
+
diffCheckResults,
|
|
4053
|
+
diffSnapshots,
|
|
2385
4054
|
edgeWeight,
|
|
4055
|
+
embedSnapshot,
|
|
4056
|
+
embedTextsCached,
|
|
2386
4057
|
externalId,
|
|
2387
4058
|
fileId,
|
|
2388
|
-
|
|
4059
|
+
findSimilarCapability,
|
|
4060
|
+
getEdgeWeight,
|
|
4061
|
+
getLanguageFromPath3 as getLanguageFromPath,
|
|
2389
4062
|
getSupportedLanguages,
|
|
4063
|
+
groupTestsBySource,
|
|
2390
4064
|
hashContent,
|
|
4065
|
+
hashEmbedText,
|
|
2391
4066
|
indexPaths,
|
|
2392
4067
|
isGeneratedByHeuristic,
|
|
2393
4068
|
isGeneratedFile,
|
|
2394
4069
|
isInsideGitRepo,
|
|
4070
|
+
linkTestsToSources,
|
|
2395
4071
|
listEdges,
|
|
4072
|
+
listEmbeddableSymbols,
|
|
2396
4073
|
listMetrics,
|
|
2397
4074
|
listNodes,
|
|
4075
|
+
loadCheckRules,
|
|
2398
4076
|
loadGeneratedPatterns,
|
|
2399
4077
|
moduleId,
|
|
2400
4078
|
openCodeGraph,
|
|
2401
4079
|
packageId,
|
|
2402
4080
|
parentModuleId,
|
|
2403
|
-
parseFile,
|
|
4081
|
+
parseFile2 as parseFile,
|
|
2404
4082
|
parseSymbolId,
|
|
2405
4083
|
pruneDanglingReferences,
|
|
2406
4084
|
readSourceFiles,
|
|
2407
4085
|
resolveBarrelEdges,
|
|
2408
|
-
|
|
4086
|
+
resolveSnapshot,
|
|
4087
|
+
runChecks,
|
|
4088
|
+
shouldIncludeFile2 as shouldIncludeFile,
|
|
4089
|
+
snapshotPageRank,
|
|
4090
|
+
snapshotReferenceEdges,
|
|
4091
|
+
snapshotRelevance,
|
|
4092
|
+
snapshotSymbolConsumers,
|
|
4093
|
+
snapshotSymbolCoupling,
|
|
2409
4094
|
structuralSignature,
|
|
2410
4095
|
symbolId,
|
|
4096
|
+
testCoverageCountMetrics,
|
|
4097
|
+
tryEmbedSnapshot,
|
|
4098
|
+
validateRules,
|
|
2411
4099
|
walkSourceFiles
|
|
2412
4100
|
};
|
|
2413
4101
|
//# sourceMappingURL=index.js.map
|