@titan-design/code-graph 0.7.0 → 0.9.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 +22 -4
- package/dist/index.d.ts +83 -11
- package/dist/index.js +689 -81
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -62,11 +62,53 @@ var DOMAIN_DDL = `
|
|
|
62
62
|
PRIMARY KEY (snapshot_id, file_id)
|
|
63
63
|
);
|
|
64
64
|
`;
|
|
65
|
-
var
|
|
65
|
+
var FINDING_DDL = `
|
|
66
|
+
CREATE TABLE IF NOT EXISTS finding (
|
|
67
|
+
snapshot_id INTEGER NOT NULL,
|
|
68
|
+
key TEXT NOT NULL,
|
|
69
|
+
finding_id TEXT NOT NULL,
|
|
70
|
+
tool TEXT NOT NULL,
|
|
71
|
+
signal TEXT NOT NULL,
|
|
72
|
+
path TEXT NOT NULL,
|
|
73
|
+
line_start INTEGER,
|
|
74
|
+
line_end INTEGER,
|
|
75
|
+
severity TEXT NOT NULL,
|
|
76
|
+
excerpt_hash TEXT,
|
|
77
|
+
attrs TEXT NOT NULL,
|
|
78
|
+
PRIMARY KEY (snapshot_id, key)
|
|
79
|
+
);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_finding_path ON finding(snapshot_id, path);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS verdict (
|
|
83
|
+
snapshot_id INTEGER NOT NULL,
|
|
84
|
+
key TEXT NOT NULL,
|
|
85
|
+
verdict TEXT NOT NULL CHECK (verdict IN ('confirmed', 'justified', 'unclear')),
|
|
86
|
+
rationale TEXT NOT NULL,
|
|
87
|
+
excerpt_hash TEXT NOT NULL,
|
|
88
|
+
model TEXT,
|
|
89
|
+
run_id TEXT,
|
|
90
|
+
cost_usd REAL,
|
|
91
|
+
provenance TEXT,
|
|
92
|
+
control_run TEXT,
|
|
93
|
+
carried_from INTEGER,
|
|
94
|
+
attrs TEXT NOT NULL,
|
|
95
|
+
PRIMARY KEY (snapshot_id, key)
|
|
96
|
+
);
|
|
97
|
+
`;
|
|
98
|
+
var SNAPSHOT_SCOPED_TABLES = [
|
|
99
|
+
"node",
|
|
100
|
+
"edge",
|
|
101
|
+
"metric",
|
|
102
|
+
"id_alias",
|
|
103
|
+
"file_fingerprint",
|
|
104
|
+
"finding",
|
|
105
|
+
"verdict"
|
|
106
|
+
];
|
|
66
107
|
var MIGRATIONS = [
|
|
67
108
|
kitMigration(1, { snapshot: KIT.snapshot, entitySnap: KIT.entitySnap, cacheBlob: KIT.cacheBlob }),
|
|
68
109
|
{ version: 2, name: "code graph columns", up: (db) => db.exec(KIT_EXTENSIONS) },
|
|
69
|
-
{ version: 3, name: "code graph tables", up: (db) => db.exec(DOMAIN_DDL) }
|
|
110
|
+
{ version: 3, name: "code graph tables", up: (db) => db.exec(DOMAIN_DDL) },
|
|
111
|
+
{ version: 4, name: "findings and verdicts", up: (db) => db.exec(FINDING_DDL) }
|
|
70
112
|
];
|
|
71
113
|
var SCHEMA_VERSION = Math.max(...MIGRATIONS.map((m) => m.version));
|
|
72
114
|
|
|
@@ -189,6 +231,9 @@ function readMetricAggregates(stmts, snapshotId, name) {
|
|
|
189
231
|
|
|
190
232
|
// src/store.ts
|
|
191
233
|
var NODE_COLS = "id, kind, name, parent_id, language, role, attrs";
|
|
234
|
+
function isStructuralLayer(edge) {
|
|
235
|
+
return edge.kind !== "references" && edge.kind !== "calls";
|
|
236
|
+
}
|
|
192
237
|
var CodeGraphStore = class {
|
|
193
238
|
constructor(db) {
|
|
194
239
|
this.db = db;
|
|
@@ -307,10 +352,10 @@ var CodeGraphStore = class {
|
|
|
307
352
|
const nodes = this.statements.listNodes.all(snapshotId).map(rowToNode);
|
|
308
353
|
return opts?.includeSymbols ? nodes : nodes.filter((n) => n.kind !== "symbol");
|
|
309
354
|
}
|
|
310
|
-
/** See {@link listNodes}: `references` edges are the symbol layer, excluded by default. */
|
|
355
|
+
/** See {@link listNodes}: `references` and `calls` edges are the symbol layer, excluded by default. */
|
|
311
356
|
listEdges(snapshotId, opts) {
|
|
312
357
|
const edges = this.statements.listEdges.all(snapshotId).map(rowToEdge);
|
|
313
|
-
return opts?.includeReferences ? edges : edges.filter(
|
|
358
|
+
return opts?.includeReferences ? edges : edges.filter(isStructuralLayer);
|
|
314
359
|
}
|
|
315
360
|
listMetrics(snapshotId) {
|
|
316
361
|
return this.statements.listMetrics.all(snapshotId).map(rowToMetric);
|
|
@@ -327,10 +372,10 @@ var CodeGraphStore = class {
|
|
|
327
372
|
topByMetric(opts) {
|
|
328
373
|
return readTopByMetric(this.targeted, opts);
|
|
329
374
|
}
|
|
330
|
-
/** Edges into or out of one node;
|
|
375
|
+
/** Edges into or out of one node; symbol-layer edges are excluded unless asked, as in {@link listEdges}. */
|
|
331
376
|
listEdgesTouching(snapshotId, nodeId, opts) {
|
|
332
377
|
const edges = readEdgesTouching(this.targeted, snapshotId, nodeId);
|
|
333
|
-
return opts?.includeReferences ? edges : edges.filter(
|
|
378
|
+
return opts?.includeReferences ? edges : edges.filter(isStructuralLayer);
|
|
334
379
|
}
|
|
335
380
|
/** Count, sum, min, and max per metric name and node kind; pass `name` for one metric. */
|
|
336
381
|
aggregateMetrics(snapshotId, opts) {
|
|
@@ -447,12 +492,10 @@ function runPrune(store, options = {}) {
|
|
|
447
492
|
|
|
448
493
|
// src/indexer.ts
|
|
449
494
|
import { realpathSync } from "fs";
|
|
450
|
-
import * as
|
|
495
|
+
import * as path8 from "path";
|
|
451
496
|
import { parseFile } from "@titan-design/code-parser";
|
|
452
497
|
|
|
453
498
|
// src/extractors/ts-morph-extractor.ts
|
|
454
|
-
import * as path4 from "path";
|
|
455
|
-
import { existsSync } from "fs";
|
|
456
499
|
import {
|
|
457
500
|
Project,
|
|
458
501
|
ScriptTarget,
|
|
@@ -664,20 +707,24 @@ var TS_DECL_TYPES = /* @__PURE__ */ new Set([
|
|
|
664
707
|
]);
|
|
665
708
|
var PY_DECL_TYPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
|
|
666
709
|
function collectDeclarations(file) {
|
|
667
|
-
const python = file.language === "python";
|
|
668
|
-
const declTypes = python ? PY_DECL_TYPES : TS_DECL_TYPES;
|
|
669
710
|
const out = [];
|
|
670
|
-
|
|
671
|
-
const named = declaredNodeAt(node, declTypes);
|
|
672
|
-
if (!named) return;
|
|
711
|
+
forEachDeclaration(file, (name, qualifiedName, node) => {
|
|
673
712
|
out.push({
|
|
674
|
-
name
|
|
675
|
-
qualifiedName
|
|
676
|
-
span: { startLine:
|
|
713
|
+
name,
|
|
714
|
+
qualifiedName,
|
|
715
|
+
span: { startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 }
|
|
677
716
|
});
|
|
678
717
|
});
|
|
679
718
|
return out;
|
|
680
719
|
}
|
|
720
|
+
function forEachDeclaration(file, visit) {
|
|
721
|
+
const python = file.language === "python";
|
|
722
|
+
const declTypes = python ? PY_DECL_TYPES : TS_DECL_TYPES;
|
|
723
|
+
walkScopes(file.tree.rootNode, python, (node, scope) => {
|
|
724
|
+
const named = declaredNodeAt(node, declTypes);
|
|
725
|
+
if (named) visit(named.name, qualify(scope, named.name), named.node);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
681
728
|
function collectDeclaredSpans(file) {
|
|
682
729
|
const spans = /* @__PURE__ */ new Map();
|
|
683
730
|
for (const d of collectDeclarations(file)) spans.set(d.qualifiedName, d.span);
|
|
@@ -698,6 +745,80 @@ function declaredNodeAt(node, declTypes) {
|
|
|
698
745
|
return null;
|
|
699
746
|
}
|
|
700
747
|
|
|
748
|
+
// src/extractors/call-sites.ts
|
|
749
|
+
function addCallSite(agg, srcId, dstId, site) {
|
|
750
|
+
const key = JSON.stringify([srcId, dstId]);
|
|
751
|
+
const existing = agg.get(key);
|
|
752
|
+
if (existing) existing.attrs.sites.push(site);
|
|
753
|
+
else agg.set(key, { srcId, dstId, kind: "calls", attrs: { sites: [site] } });
|
|
754
|
+
}
|
|
755
|
+
function paramAttrs(shape) {
|
|
756
|
+
if (!shape) return {};
|
|
757
|
+
return shape.positional === shape.params.length ? { params: shape.params } : { params: shape.params, positionalParams: shape.positional };
|
|
758
|
+
}
|
|
759
|
+
function readParamShape(attrs) {
|
|
760
|
+
const params = attrs?.params;
|
|
761
|
+
if (!Array.isArray(params)) return void 0;
|
|
762
|
+
const positional = typeof attrs?.positionalParams === "number" ? attrs.positionalParams : params.length;
|
|
763
|
+
return { params, positional };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/extractors/callable-params.ts
|
|
767
|
+
var TS_PARAM_TYPES = /* @__PURE__ */ new Set(["required_parameter", "optional_parameter"]);
|
|
768
|
+
function collectParamShapes(file) {
|
|
769
|
+
const out = /* @__PURE__ */ new Map();
|
|
770
|
+
forEachDeclaration(file, (_name, qualifiedName, node) => {
|
|
771
|
+
const shape = file.language === "python" ? pythonShape(node) : tsShape(node);
|
|
772
|
+
if (shape) out.set(qualifiedName, shape);
|
|
773
|
+
});
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
function tsShape(fn) {
|
|
777
|
+
if (fn.type === "class_declaration") return null;
|
|
778
|
+
const single = fn.childForFieldName("parameter");
|
|
779
|
+
if (single) return { params: [single.text], positional: 1 };
|
|
780
|
+
const params = [];
|
|
781
|
+
for (const param of fn.childForFieldName("parameters")?.namedChildren ?? []) {
|
|
782
|
+
if (!param || !TS_PARAM_TYPES.has(param.type)) continue;
|
|
783
|
+
const pattern = param.childForFieldName("pattern");
|
|
784
|
+
if (!pattern || pattern.type === "this") continue;
|
|
785
|
+
if (pattern.type === "rest_pattern") break;
|
|
786
|
+
params.push(pattern.type === "identifier" ? pattern.text : `$${params.length}`);
|
|
787
|
+
}
|
|
788
|
+
return { params, positional: params.length };
|
|
789
|
+
}
|
|
790
|
+
function pythonShape(fn) {
|
|
791
|
+
if (fn.type !== "function_definition") return null;
|
|
792
|
+
const names = [];
|
|
793
|
+
let positionalEnd = null;
|
|
794
|
+
for (const param of fn.childForFieldName("parameters")?.namedChildren ?? []) {
|
|
795
|
+
if (!param) continue;
|
|
796
|
+
if (param.type === "keyword_separator" || param.type === "list_splat_pattern") positionalEnd ??= names.length;
|
|
797
|
+
const name = pythonParamName(param);
|
|
798
|
+
if (name) names.push(name);
|
|
799
|
+
}
|
|
800
|
+
const receiver = hasReceiver(fn) && names.length > 0 ? 1 : 0;
|
|
801
|
+
return { params: names.slice(receiver), positional: Math.max((positionalEnd ?? names.length) - receiver, 0) };
|
|
802
|
+
}
|
|
803
|
+
function pythonParamName(param) {
|
|
804
|
+
if (param.type === "identifier") return param.text;
|
|
805
|
+
if (param.type === "default_parameter" || param.type === "typed_default_parameter") {
|
|
806
|
+
return param.childForFieldName("name")?.text ?? null;
|
|
807
|
+
}
|
|
808
|
+
if (param.type === "typed_parameter") {
|
|
809
|
+
const first = param.namedChildren[0];
|
|
810
|
+
return first?.type === "identifier" ? first.text : null;
|
|
811
|
+
}
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
function hasReceiver(fn) {
|
|
815
|
+
const decorated = fn.parent?.type === "decorated_definition" ? fn.parent : null;
|
|
816
|
+
const block = (decorated ?? fn).parent;
|
|
817
|
+
if (block?.type !== "block" || block.parent?.type !== "class_definition") return false;
|
|
818
|
+
const decorators = decorated?.namedChildren.filter((c) => c?.type === "decorator") ?? [];
|
|
819
|
+
return !decorators.some((d) => d?.text === "@staticmethod");
|
|
820
|
+
}
|
|
821
|
+
|
|
701
822
|
// src/extractors/symbol-signature.ts
|
|
702
823
|
import {
|
|
703
824
|
Node as Node2
|
|
@@ -776,8 +897,8 @@ function clamp(s, max = MAX_SIGNATURE) {
|
|
|
776
897
|
}
|
|
777
898
|
|
|
778
899
|
// src/extractors/symbol-nodes.ts
|
|
779
|
-
function makeSymbolNode(fileId2, name, exported, span, text) {
|
|
780
|
-
const attrs = { exported };
|
|
900
|
+
function makeSymbolNode(fileId2, name, exported, span, text, shape) {
|
|
901
|
+
const attrs = { exported, ...paramAttrs(shape) };
|
|
781
902
|
if (span) {
|
|
782
903
|
attrs.startLine = span.startLine;
|
|
783
904
|
attrs.endLine = span.endLine;
|
|
@@ -796,18 +917,19 @@ function makeSymbolNode(fileId2, name, exported, span, text) {
|
|
|
796
917
|
function buildSymbolNodes(repoRoot, sourceFile, file) {
|
|
797
918
|
const fId = fileId(repoRoot, sourceFile.getFilePath());
|
|
798
919
|
const spans = collectDeclaredSpans(file);
|
|
920
|
+
const shapes = collectParamShapes(file);
|
|
799
921
|
const out = [];
|
|
800
922
|
const exported = /* @__PURE__ */ new Set();
|
|
801
923
|
for (const [name, decls] of sourceFile.getExportedDeclarations()) {
|
|
802
924
|
const own = decls.find((d) => d.getSourceFile() === sourceFile);
|
|
803
925
|
if (!own) continue;
|
|
804
926
|
exported.add(name);
|
|
805
|
-
out.push(makeSymbolNode(fId, name, true, spans.get(name), declarationText(own)));
|
|
927
|
+
out.push(makeSymbolNode(fId, name, true, spans.get(name), declarationText(own), shapes.get(name)));
|
|
806
928
|
}
|
|
807
929
|
for (const [name, span] of spans) {
|
|
808
930
|
if (exported.has(name)) continue;
|
|
809
931
|
const decl = lookupDeclaration(sourceFile, name);
|
|
810
|
-
out.push(makeSymbolNode(fId, name, false, span, decl ? declarationText(decl) : {}));
|
|
932
|
+
out.push(makeSymbolNode(fId, name, false, span, decl ? declarationText(decl) : {}, shapes.get(name)));
|
|
811
933
|
}
|
|
812
934
|
return out;
|
|
813
935
|
}
|
|
@@ -879,6 +1001,7 @@ function recordDynamicSymbolRefEdges(sourceFile, srcFileId, agg, resolveSpecifie
|
|
|
879
1001
|
|
|
880
1002
|
// src/extractors/module-resolution.ts
|
|
881
1003
|
import * as path3 from "path";
|
|
1004
|
+
import { existsSync } from "fs";
|
|
882
1005
|
var RESOLVABLE_EXTS = [
|
|
883
1006
|
".ts",
|
|
884
1007
|
".tsx",
|
|
@@ -916,6 +1039,17 @@ function inRepoFileId(repoRoot, abs) {
|
|
|
916
1039
|
if (relative4.split(path3.sep).includes("node_modules")) return null;
|
|
917
1040
|
return fileId(repoRoot, abs);
|
|
918
1041
|
}
|
|
1042
|
+
function remapDistToSrc(abs) {
|
|
1043
|
+
const m = /^(.*)[\\/]dist[\\/](.+)\.d\.ts$/.exec(abs);
|
|
1044
|
+
if (!m) return abs;
|
|
1045
|
+
const base = m[1];
|
|
1046
|
+
const sub = m[2];
|
|
1047
|
+
for (const ext of [".ts", ".tsx"]) {
|
|
1048
|
+
const candidate = path3.join(base, "src", sub + ext);
|
|
1049
|
+
if (existsSync(candidate)) return candidate;
|
|
1050
|
+
}
|
|
1051
|
+
return abs;
|
|
1052
|
+
}
|
|
919
1053
|
|
|
920
1054
|
// src/extractors/reexport-resolve.ts
|
|
921
1055
|
function resolveReExportOrigin(ctx, importerAbs, specifier, name) {
|
|
@@ -962,6 +1096,125 @@ function reExportInwardName(ed, name) {
|
|
|
962
1096
|
return name;
|
|
963
1097
|
}
|
|
964
1098
|
|
|
1099
|
+
// src/extractors/ts-calls.ts
|
|
1100
|
+
import { Node as Node5, SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
1101
|
+
|
|
1102
|
+
// src/extractors/ts-scope.ts
|
|
1103
|
+
import { Node as Node4 } from "ts-morph";
|
|
1104
|
+
function bindingName2(node) {
|
|
1105
|
+
const parent = node.getParent();
|
|
1106
|
+
if (Node4.isVariableDeclaration(parent) || Node4.isPropertyDeclaration(parent)) return parent.getName();
|
|
1107
|
+
if (Node4.isPropertyAssignment(parent)) return parent.getNameNode().getText();
|
|
1108
|
+
if (Node4.isExportAssignment(parent)) return "default";
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
function declaredName(node) {
|
|
1112
|
+
const name = node.getName();
|
|
1113
|
+
if (name) return name;
|
|
1114
|
+
return Node4.isExportable(node) && node.isDefaultExport() ? "default" : null;
|
|
1115
|
+
}
|
|
1116
|
+
function scopeSegment2(node) {
|
|
1117
|
+
if (Node4.isClassDeclaration(node) || Node4.isFunctionDeclaration(node)) return declaredName(node);
|
|
1118
|
+
if (Node4.isMethodDeclaration(node) || Node4.isGetAccessorDeclaration(node) || Node4.isSetAccessorDeclaration(node)) {
|
|
1119
|
+
return node.getName();
|
|
1120
|
+
}
|
|
1121
|
+
if (Node4.isConstructorDeclaration(node)) return "constructor";
|
|
1122
|
+
if (Node4.isModuleDeclaration(node)) return node.hasNamespaceKeyword() ? node.getName() : null;
|
|
1123
|
+
if (Node4.isClassExpression(node) && node.getName()) return node.getName() ?? null;
|
|
1124
|
+
const bound = Node4.isArrowFunction(node) || Node4.isFunctionExpression(node) || Node4.isClassExpression(node) || Node4.isObjectLiteralExpression(node);
|
|
1125
|
+
return bound ? bindingName2(node) ?? ANONYMOUS_SCOPE : null;
|
|
1126
|
+
}
|
|
1127
|
+
function scopeOf(node) {
|
|
1128
|
+
const segments = node.getAncestors().map(scopeSegment2).filter((s) => s !== null).reverse();
|
|
1129
|
+
let scope = "";
|
|
1130
|
+
for (const segment of segments) {
|
|
1131
|
+
const repeatsAnonymous = segment === ANONYMOUS_SCOPE && (scope === ANONYMOUS_SCOPE || scope.endsWith(`.${ANONYMOUS_SCOPE}`));
|
|
1132
|
+
if (!repeatsAnonymous) scope = qualify(scope, segment);
|
|
1133
|
+
}
|
|
1134
|
+
return scope;
|
|
1135
|
+
}
|
|
1136
|
+
function declarationQualifiedName(decl) {
|
|
1137
|
+
if (Node4.isVariableDeclaration(decl)) {
|
|
1138
|
+
const init = decl.getInitializer();
|
|
1139
|
+
const callable = Node4.isArrowFunction(init) || Node4.isFunctionExpression(init);
|
|
1140
|
+
return callable ? qualify(scopeOf(decl), decl.getName()) : null;
|
|
1141
|
+
}
|
|
1142
|
+
if (Node4.isArrowFunction(decl) || Node4.isFunctionExpression(decl)) {
|
|
1143
|
+
const parent = decl.getParent();
|
|
1144
|
+
return Node4.isVariableDeclaration(parent) ? qualify(scopeOf(parent), parent.getName()) : null;
|
|
1145
|
+
}
|
|
1146
|
+
const isDeclaration = Node4.isFunctionDeclaration(decl) || Node4.isClassDeclaration(decl) || Node4.isMethodDeclaration(decl) || Node4.isGetAccessorDeclaration(decl) || Node4.isSetAccessorDeclaration(decl) || Node4.isConstructorDeclaration(decl);
|
|
1147
|
+
const own = isDeclaration ? scopeSegment2(decl) : null;
|
|
1148
|
+
return own ? qualify(scopeOf(decl), own) : null;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/extractors/ts-calls.ts
|
|
1152
|
+
function collectTsCallEdges(sourceFile, ctx) {
|
|
1153
|
+
const agg = /* @__PURE__ */ new Map();
|
|
1154
|
+
sourceFile.forEachDescendant((node) => {
|
|
1155
|
+
if (!Node5.isCallExpression(node) && !Node5.isNewExpression(node)) return;
|
|
1156
|
+
const dstId = resolveCallee(node, ctx);
|
|
1157
|
+
if (dstId) addCallSite(agg, callerId(node, ctx), dstId, tsCallSite(node));
|
|
1158
|
+
});
|
|
1159
|
+
return [...agg.values()];
|
|
1160
|
+
}
|
|
1161
|
+
function resolveCallee(call, ctx) {
|
|
1162
|
+
const callee = call.getExpression();
|
|
1163
|
+
if (callee.getKind() === SyntaxKind3.SuperKeyword) return null;
|
|
1164
|
+
const symbol = callee.getSymbol();
|
|
1165
|
+
if (!symbol) return null;
|
|
1166
|
+
const target = symbol.isAlias() ? symbol.getAliasedSymbol() : symbol;
|
|
1167
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1168
|
+
for (const decl of target?.getDeclarations() ?? []) {
|
|
1169
|
+
const id = declarationId(decl, ctx);
|
|
1170
|
+
if (id === null) return null;
|
|
1171
|
+
ids.add(id);
|
|
1172
|
+
}
|
|
1173
|
+
return ids.size === 1 ? [...ids][0] : null;
|
|
1174
|
+
}
|
|
1175
|
+
function declarationId(decl, ctx) {
|
|
1176
|
+
const sf = decl.getSourceFile();
|
|
1177
|
+
const abs = remapDistToSrc(sf.getFilePath());
|
|
1178
|
+
if (sf.isDeclarationFile() && abs === sf.getFilePath()) return null;
|
|
1179
|
+
const fileId2 = inRepoFileId(ctx.repoRoot, abs);
|
|
1180
|
+
const name = fileId2 ? declarationQualifiedName(decl) : null;
|
|
1181
|
+
return fileId2 && name ? symbolId(fileId2, name) : null;
|
|
1182
|
+
}
|
|
1183
|
+
function callerId(call, ctx) {
|
|
1184
|
+
for (const ancestor of call.getAncestors()) {
|
|
1185
|
+
const name = declarationQualifiedName(ancestor);
|
|
1186
|
+
if (name && ctx.localSymbols.has(name)) return symbolId(ctx.srcFileId, name);
|
|
1187
|
+
}
|
|
1188
|
+
return ctx.srcFileId;
|
|
1189
|
+
}
|
|
1190
|
+
function tsCallSite(call) {
|
|
1191
|
+
const site = { args: [] };
|
|
1192
|
+
for (const arg of call.getArguments()) {
|
|
1193
|
+
if (Node5.isSpreadElement(arg)) {
|
|
1194
|
+
site.spreadFrom ??= site.args.length;
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
site.args.push(literalText(arg));
|
|
1198
|
+
}
|
|
1199
|
+
return site;
|
|
1200
|
+
}
|
|
1201
|
+
var LITERAL_KINDS = /* @__PURE__ */ new Set([
|
|
1202
|
+
SyntaxKind3.NumericLiteral,
|
|
1203
|
+
SyntaxKind3.StringLiteral,
|
|
1204
|
+
SyntaxKind3.NoSubstitutionTemplateLiteral,
|
|
1205
|
+
SyntaxKind3.TrueKeyword,
|
|
1206
|
+
SyntaxKind3.FalseKeyword,
|
|
1207
|
+
SyntaxKind3.NullKeyword
|
|
1208
|
+
]);
|
|
1209
|
+
function literalText(arg) {
|
|
1210
|
+
if (LITERAL_KINDS.has(arg.getKind())) return arg.getText();
|
|
1211
|
+
if (Node5.isIdentifier(arg) && arg.getText() === "undefined") return "undefined";
|
|
1212
|
+
if (Node5.isPrefixUnaryExpression(arg) && arg.getOperatorToken() === SyntaxKind3.MinusToken) {
|
|
1213
|
+
return Node5.isNumericLiteral(arg.getOperand()) ? arg.getText() : null;
|
|
1214
|
+
}
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
965
1218
|
// src/extractors/ts-morph-extractor.ts
|
|
966
1219
|
var TsMorphGraphExtractor = class {
|
|
967
1220
|
name = "ts-morph-graph";
|
|
@@ -980,7 +1233,12 @@ var TsMorphGraphExtractor = class {
|
|
|
980
1233
|
const nodes = this.buildFileAndModuleNodes(sourceFile);
|
|
981
1234
|
const symbolNodes2 = buildSymbolNodes(this.repoRoot, sourceFile, file);
|
|
982
1235
|
const { edges, externalNodes } = this.collectEdges(sourceFile);
|
|
983
|
-
|
|
1236
|
+
const calls = collectTsCallEdges(sourceFile, {
|
|
1237
|
+
repoRoot: this.repoRoot,
|
|
1238
|
+
srcFileId: fileId(this.repoRoot, sourceFile.getFilePath()),
|
|
1239
|
+
localSymbols: new Set(symbolNodes2.map((n) => n.name))
|
|
1240
|
+
});
|
|
1241
|
+
return [{ nodes: [...nodes, ...symbolNodes2, ...externalNodes], edges: [...edges, ...calls] }];
|
|
984
1242
|
}
|
|
985
1243
|
ensureProject() {
|
|
986
1244
|
if (this.project) return this.project;
|
|
@@ -1146,27 +1404,129 @@ var TsMorphGraphExtractor = class {
|
|
|
1146
1404
|
return inRepoFileId(this.repoRoot, abs);
|
|
1147
1405
|
}
|
|
1148
1406
|
};
|
|
1149
|
-
function remapDistToSrc(abs) {
|
|
1150
|
-
const m = /^(.*)[\\/]dist[\\/](.+)\.d\.ts$/.exec(abs);
|
|
1151
|
-
if (!m) return abs;
|
|
1152
|
-
const base = m[1];
|
|
1153
|
-
const sub = m[2];
|
|
1154
|
-
for (const ext of [".ts", ".tsx"]) {
|
|
1155
|
-
const candidate = path4.join(base, "src", sub + ext);
|
|
1156
|
-
if (existsSync(candidate)) return candidate;
|
|
1157
|
-
}
|
|
1158
|
-
return abs;
|
|
1159
|
-
}
|
|
1160
1407
|
function isTypeScriptFile(file) {
|
|
1161
1408
|
return file.language === "typescript" || file.language === "tsx";
|
|
1162
1409
|
}
|
|
1163
1410
|
|
|
1164
1411
|
// src/extractors/python-extractor.ts
|
|
1165
|
-
import * as
|
|
1412
|
+
import * as path4 from "path";
|
|
1166
1413
|
import { existsSync as existsSync2 } from "fs";
|
|
1414
|
+
|
|
1415
|
+
// src/extractors/python-calls.ts
|
|
1416
|
+
function collectPythonCallEdges(file, fileId2, resolve4) {
|
|
1417
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
1418
|
+
forEachDeclaration(file, (_name, qualifiedName, node) => declarations.set(qualifiedName, node));
|
|
1419
|
+
const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode, resolve4) };
|
|
1420
|
+
const agg = /* @__PURE__ */ new Map();
|
|
1421
|
+
walkScopes(file.tree.rootNode, true, (node, scope) => {
|
|
1422
|
+
if (node.type !== "call") return;
|
|
1423
|
+
const dst = resolveCallee2(node.childForFieldName("function"), scope, ctx);
|
|
1424
|
+
const args = node.childForFieldName("arguments");
|
|
1425
|
+
if (!dst || args?.type !== "argument_list") return;
|
|
1426
|
+
const srcId = declarations.has(scope) ? symbolId(fileId2, scope) : fileId2;
|
|
1427
|
+
addCallSite(agg, srcId, dst, pythonCallSite(args));
|
|
1428
|
+
});
|
|
1429
|
+
return [...agg.values()];
|
|
1430
|
+
}
|
|
1431
|
+
function resolveCallee2(callee, scope, ctx) {
|
|
1432
|
+
if (callee?.type === "identifier") return resolveBareName(callee.text, scope, ctx);
|
|
1433
|
+
if (callee?.type === "attribute") return resolveSelfMethod(callee, scope, ctx);
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
function resolveBareName(name, scope, ctx) {
|
|
1437
|
+
if (isShadowed(name, scope, ctx)) return null;
|
|
1438
|
+
const local = ctx.declarations.has(name);
|
|
1439
|
+
const imported = ctx.imports.get(name);
|
|
1440
|
+
if (local && imported) return null;
|
|
1441
|
+
if (local) return symbolId(ctx.fileId, name);
|
|
1442
|
+
return imported ? symbolId(imported.fileId, imported.name) : null;
|
|
1443
|
+
}
|
|
1444
|
+
function isShadowed(name, scope, ctx) {
|
|
1445
|
+
const segments = scope ? scope.split(".") : [];
|
|
1446
|
+
for (let depth = segments.length; depth > 0; depth--) {
|
|
1447
|
+
const enclosing = segments.slice(0, depth).join(".");
|
|
1448
|
+
if (ctx.declarations.has(`${enclosing}.${name}`)) return true;
|
|
1449
|
+
if (parameterNames(ctx.declarations.get(enclosing)).includes(name)) return true;
|
|
1450
|
+
}
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
function resolveSelfMethod(callee, scope, ctx) {
|
|
1454
|
+
const receiver = callee.childForFieldName("object");
|
|
1455
|
+
const attr = callee.childForFieldName("attribute")?.text;
|
|
1456
|
+
const method = ctx.declarations.get(scope);
|
|
1457
|
+
if (receiver?.type !== "identifier" || !attr || !method || !hasReceiver(method)) return null;
|
|
1458
|
+
if (parameterNames(method)[0] !== receiver.text) return null;
|
|
1459
|
+
const owner = findMethodOwner(scope.slice(0, scope.lastIndexOf(".")), attr, ctx, /* @__PURE__ */ new Set());
|
|
1460
|
+
return owner ? symbolId(ctx.fileId, `${owner}.${attr}`) : null;
|
|
1461
|
+
}
|
|
1462
|
+
function findMethodOwner(className, attr, ctx, seen) {
|
|
1463
|
+
const cls = ctx.declarations.get(className);
|
|
1464
|
+
if (cls?.type !== "class_definition" || seen.has(className)) return null;
|
|
1465
|
+
seen.add(className);
|
|
1466
|
+
if (ctx.declarations.get(`${className}.${attr}`)?.type === "function_definition") return className;
|
|
1467
|
+
for (const base of cls.childForFieldName("superclasses")?.namedChildren ?? []) {
|
|
1468
|
+
if (base?.type !== "identifier" || !ctx.declarations.has(base.text)) return null;
|
|
1469
|
+
const owner = findMethodOwner(base.text, attr, ctx, seen);
|
|
1470
|
+
if (owner) return owner;
|
|
1471
|
+
}
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
function parameterNames(def) {
|
|
1475
|
+
if (def?.type !== "function_definition") return [];
|
|
1476
|
+
const out = [];
|
|
1477
|
+
for (const param of def.childForFieldName("parameters")?.namedChildren ?? []) {
|
|
1478
|
+
const id = param?.type === "identifier" ? param : param?.descendantsOfType("identifier")[0];
|
|
1479
|
+
if (id) out.push(id.text);
|
|
1480
|
+
}
|
|
1481
|
+
return out;
|
|
1482
|
+
}
|
|
1483
|
+
function fromImports(root, resolve4) {
|
|
1484
|
+
const out = /* @__PURE__ */ new Map();
|
|
1485
|
+
for (const stmt of root.descendantsOfType("import_from_statement")) {
|
|
1486
|
+
const moduleName = stmt?.childForFieldName("module_name")?.text;
|
|
1487
|
+
const moduleFile = moduleName ? resolve4(moduleName) : null;
|
|
1488
|
+
if (!stmt || !moduleFile) continue;
|
|
1489
|
+
for (const imported of stmt.childrenForFieldName("name")) {
|
|
1490
|
+
const alias = imported?.type === "aliased_import" ? imported.childForFieldName("alias")?.text : void 0;
|
|
1491
|
+
const name = imported?.type === "aliased_import" ? imported.childForFieldName("name")?.text : imported?.text;
|
|
1492
|
+
if (name && !name.includes(".")) out.set(alias ?? name, { fileId: moduleFile, name });
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return out;
|
|
1496
|
+
}
|
|
1497
|
+
var LITERAL_TYPES = /* @__PURE__ */ new Set(["integer", "float", "true", "false", "none"]);
|
|
1498
|
+
function literalText2(arg) {
|
|
1499
|
+
if (LITERAL_TYPES.has(arg.type)) return arg.text;
|
|
1500
|
+
if (arg.type === "string") return arg.namedChildren.some((c) => c?.type === "interpolation") ? null : arg.text;
|
|
1501
|
+
if (arg.type === "unary_operator" && arg.text.startsWith("-")) {
|
|
1502
|
+
const operand = arg.childForFieldName("argument");
|
|
1503
|
+
return operand && (operand.type === "integer" || operand.type === "float") ? arg.text : null;
|
|
1504
|
+
}
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
function pythonCallSite(argumentList) {
|
|
1508
|
+
const site = { args: [] };
|
|
1509
|
+
for (const arg of argumentList.namedChildren) {
|
|
1510
|
+
if (!arg || arg.type === "comment") continue;
|
|
1511
|
+
if (arg.type === "list_splat") site.spreadFrom ??= site.args.length;
|
|
1512
|
+
else if (arg.type === "dictionary_splat") site.kwSplat = true;
|
|
1513
|
+
else if (arg.type === "keyword_argument") addKeyword(site, arg);
|
|
1514
|
+
else if (site.spreadFrom === void 0) site.args.push(literalText2(arg));
|
|
1515
|
+
}
|
|
1516
|
+
return site;
|
|
1517
|
+
}
|
|
1518
|
+
function addKeyword(site, arg) {
|
|
1519
|
+
const name = arg.childForFieldName("name")?.text;
|
|
1520
|
+
const value = arg.childForFieldName("value");
|
|
1521
|
+
if (!name || !value) return;
|
|
1522
|
+
site.kwargs ??= {};
|
|
1523
|
+
site.kwargs[name] = literalText2(value);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// src/extractors/python-extractor.ts
|
|
1167
1527
|
function* candidates(base) {
|
|
1168
1528
|
yield `${base}.py`;
|
|
1169
|
-
yield
|
|
1529
|
+
yield path4.join(base, "__init__.py");
|
|
1170
1530
|
}
|
|
1171
1531
|
var PythonGraphExtractor = class {
|
|
1172
1532
|
constructor(repoRoot) {
|
|
@@ -1180,7 +1540,8 @@ var PythonGraphExtractor = class {
|
|
|
1180
1540
|
const nodes = buildFileModuleNodes(this.repoRoot, file.filePath);
|
|
1181
1541
|
nodes.push(...symbolNodes(fId, file));
|
|
1182
1542
|
const { edges, externals } = this.collectImports(fId, file);
|
|
1183
|
-
|
|
1543
|
+
const calls = collectPythonCallEdges(file, fId, (specifier) => this.resolveDotted(file.filePath, specifier));
|
|
1544
|
+
return [{ nodes: [...nodes, ...externals], edges: [...edges, ...calls] }];
|
|
1184
1545
|
}
|
|
1185
1546
|
collectImports(srcId, file) {
|
|
1186
1547
|
const edges = /* @__PURE__ */ new Map();
|
|
@@ -1203,10 +1564,10 @@ var PythonGraphExtractor = class {
|
|
|
1203
1564
|
resolveDotted(fromAbs, specifier) {
|
|
1204
1565
|
const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
|
|
1205
1566
|
const parts = specifier.slice(leadingDots).split(".").filter(Boolean);
|
|
1206
|
-
const relativeBase =
|
|
1207
|
-
const roots = leadingDots > 0 ? [relativeBase] : [
|
|
1567
|
+
const relativeBase = path4.resolve(path4.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
|
|
1568
|
+
const roots = leadingDots > 0 ? [relativeBase] : [path4.dirname(fromAbs), this.repoRoot];
|
|
1208
1569
|
for (const root of roots) {
|
|
1209
|
-
for (const candidate of candidates(
|
|
1570
|
+
for (const candidate of candidates(path4.join(root, ...parts))) {
|
|
1210
1571
|
const id = inRepoFileId(this.repoRoot, candidate);
|
|
1211
1572
|
if (id !== null && existsSync2(candidate)) return id;
|
|
1212
1573
|
}
|
|
@@ -1218,13 +1579,19 @@ function isPublicName(qualifiedName) {
|
|
|
1218
1579
|
return !qualifiedName.slice(qualifiedName.lastIndexOf(".") + 1).startsWith("_");
|
|
1219
1580
|
}
|
|
1220
1581
|
function symbolNodes(fId, file) {
|
|
1582
|
+
const shapes = collectParamShapes(file);
|
|
1221
1583
|
return [...collectDeclaredSpans(file)].map(([name, span]) => ({
|
|
1222
1584
|
id: symbolId(fId, name),
|
|
1223
1585
|
kind: "symbol",
|
|
1224
1586
|
name,
|
|
1225
1587
|
parentId: fId,
|
|
1226
1588
|
language: "python",
|
|
1227
|
-
attrs: {
|
|
1589
|
+
attrs: {
|
|
1590
|
+
exported: isPublicName(name),
|
|
1591
|
+
startLine: span.startLine,
|
|
1592
|
+
endLine: span.endLine,
|
|
1593
|
+
...paramAttrs(shapes.get(name))
|
|
1594
|
+
}
|
|
1228
1595
|
}));
|
|
1229
1596
|
}
|
|
1230
1597
|
function importSpecifiers(root) {
|
|
@@ -1257,7 +1624,7 @@ var LanguageExtractor = class {
|
|
|
1257
1624
|
};
|
|
1258
1625
|
|
|
1259
1626
|
// src/git-renames.ts
|
|
1260
|
-
import * as
|
|
1627
|
+
import * as path5 from "path";
|
|
1261
1628
|
function detectGitHead(repoRoot) {
|
|
1262
1629
|
return runGit(repoRoot, ["rev-parse", "HEAD"]);
|
|
1263
1630
|
}
|
|
@@ -1298,12 +1665,12 @@ function parseRenameOutput(text) {
|
|
|
1298
1665
|
return out;
|
|
1299
1666
|
}
|
|
1300
1667
|
function classifyRename(oldPath, newPath) {
|
|
1301
|
-
return
|
|
1668
|
+
return path5.posix.dirname(oldPath) === path5.posix.dirname(newPath) ? "rename" : "move";
|
|
1302
1669
|
}
|
|
1303
1670
|
function renameToAliases(repoRoot, pair) {
|
|
1304
1671
|
const reason = classifyRename(pair.oldPath, pair.newPath);
|
|
1305
|
-
const oldAbs =
|
|
1306
|
-
const newAbs =
|
|
1672
|
+
const oldAbs = path5.join(repoRoot, pair.oldPath);
|
|
1673
|
+
const newAbs = path5.join(repoRoot, pair.newPath);
|
|
1307
1674
|
return [
|
|
1308
1675
|
{ oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
|
|
1309
1676
|
{ oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
|
|
@@ -1396,8 +1763,8 @@ function walk(requestedId, maps) {
|
|
|
1396
1763
|
}
|
|
1397
1764
|
function chainSteps(input) {
|
|
1398
1765
|
if (input.from === null) return { steps: rootPath(input.lineage, input.to, input.maxHops), connected: true };
|
|
1399
|
-
const
|
|
1400
|
-
if (
|
|
1766
|
+
const path11 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
|
|
1767
|
+
if (path11) return { steps: path11, connected: true };
|
|
1401
1768
|
return { steps: [{ snapshotId: input.to, direction: "forward" }], connected: false };
|
|
1402
1769
|
}
|
|
1403
1770
|
function createAliasChain(input) {
|
|
@@ -1478,7 +1845,7 @@ function computeAliasBridge(store, input) {
|
|
|
1478
1845
|
|
|
1479
1846
|
// src/generated.ts
|
|
1480
1847
|
import { readFileSync } from "fs";
|
|
1481
|
-
import * as
|
|
1848
|
+
import * as path6 from "path";
|
|
1482
1849
|
var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
|
|
1483
1850
|
var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
|
|
1484
1851
|
function isGeneratedByHeuristic(id) {
|
|
@@ -1490,7 +1857,7 @@ function isGeneratedFile(id, patterns) {
|
|
|
1490
1857
|
}
|
|
1491
1858
|
function loadGeneratedPatterns(rootDir) {
|
|
1492
1859
|
try {
|
|
1493
|
-
const content = readFileSync(
|
|
1860
|
+
const content = readFileSync(path6.join(rootDir, ".gitattributes"), "utf-8");
|
|
1494
1861
|
return parseGeneratedPatterns(content);
|
|
1495
1862
|
} catch {
|
|
1496
1863
|
return [];
|
|
@@ -1593,7 +1960,7 @@ function annotateRoles(nodes, options) {
|
|
|
1593
1960
|
|
|
1594
1961
|
// src/file-walk.ts
|
|
1595
1962
|
import * as fs from "fs/promises";
|
|
1596
|
-
import * as
|
|
1963
|
+
import * as path7 from "path";
|
|
1597
1964
|
import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
|
|
1598
1965
|
async function walkSourceFiles(rootDirs, languages) {
|
|
1599
1966
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1606,12 +1973,12 @@ async function walkSourceFiles(rootDirs, languages) {
|
|
|
1606
1973
|
async function collectFiles(dir, rootDir, languages, seen, out) {
|
|
1607
1974
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1608
1975
|
for (const entry of entries) {
|
|
1609
|
-
const full =
|
|
1976
|
+
const full = path7.join(dir, entry.name);
|
|
1610
1977
|
if (entry.isDirectory()) {
|
|
1611
1978
|
if (isExcludedDir(entry.name)) continue;
|
|
1612
1979
|
await collectFiles(full, rootDir, languages, seen, out);
|
|
1613
1980
|
} else if (entry.isFile() && !seen.has(full)) {
|
|
1614
|
-
const relative4 =
|
|
1981
|
+
const relative4 = path7.relative(rootDir, full);
|
|
1615
1982
|
if (shouldIncludeFile(relative4, [...languages])) {
|
|
1616
1983
|
seen.add(full);
|
|
1617
1984
|
out.push(full);
|
|
@@ -1627,7 +1994,8 @@ function edgeWeight(e) {
|
|
|
1627
1994
|
}
|
|
1628
1995
|
function pruneDanglingReferences(nodes, edges) {
|
|
1629
1996
|
for (const [key, edge] of edges) {
|
|
1630
|
-
|
|
1997
|
+
const symbolLayer = edge.kind === "references" || edge.kind === "calls";
|
|
1998
|
+
if (symbolLayer && (!nodes.has(edge.dstId) || !nodes.has(edge.srcId))) edges.delete(key);
|
|
1631
1999
|
}
|
|
1632
2000
|
}
|
|
1633
2001
|
function resolveBarrelEdges(nodes, edges) {
|
|
@@ -1867,6 +2235,8 @@ var HANDLER_TYPES = ["except_clause", "except_group_clause", "catch_clause"];
|
|
|
1867
2235
|
var SILENT_STATEMENTS = /* @__PURE__ */ new Set(["pass_statement", "continue_statement", "empty_statement"]);
|
|
1868
2236
|
var EMPTY_VALUES = /* @__PURE__ */ new Set(["None", "null", "undefined"]);
|
|
1869
2237
|
var LOGGING_TOKENS = /* @__PURE__ */ new Set(["log", "logger", "logging", "warn", "warning", "warnings", "print", "console"]);
|
|
2238
|
+
var EXCEPT_CLAUSE_TYPES = /* @__PURE__ */ new Set(["except_clause", "except_group_clause"]);
|
|
2239
|
+
var OPTIONAL_IMPORT_EXCEPT_TYPES = /* @__PURE__ */ new Set(["ImportError", "ModuleNotFoundError"]);
|
|
1870
2240
|
function exceptionMetrics(nodeId, root, loc) {
|
|
1871
2241
|
const handlers = HANDLER_TYPES.flatMap((type) => descendantsOfType(root, type));
|
|
1872
2242
|
const swallowed = handlers.filter((h) => isSwallowed(h)).length;
|
|
@@ -1877,12 +2247,24 @@ function exceptionMetrics(nodeId, root, loc) {
|
|
|
1877
2247
|
];
|
|
1878
2248
|
}
|
|
1879
2249
|
function isSwallowed(handler) {
|
|
2250
|
+
if (isOptionalImportExcept(handler)) return false;
|
|
1880
2251
|
const body = handler.childForFieldName("body") ?? handler.namedChildren.find((c) => c?.type === "block");
|
|
1881
2252
|
if (!body) return false;
|
|
1882
2253
|
const statements = bodyStatements(body, null);
|
|
1883
2254
|
if (statements.length === 0) return true;
|
|
1884
2255
|
return statements.length === 1 && isSilent(statements[0]);
|
|
1885
2256
|
}
|
|
2257
|
+
function isOptionalImportExcept(handler) {
|
|
2258
|
+
if (!EXCEPT_CLAUSE_TYPES.has(handler.type)) return false;
|
|
2259
|
+
const value = handler.childForFieldName("value");
|
|
2260
|
+
if (!value) return false;
|
|
2261
|
+
const typeExpr = value.type === "as_pattern" ? value.namedChild(0) : value;
|
|
2262
|
+
if (!typeExpr) return false;
|
|
2263
|
+
if (typeExpr.type === "identifier") return OPTIONAL_IMPORT_EXCEPT_TYPES.has(typeExpr.text);
|
|
2264
|
+
if (typeExpr.type !== "tuple") return false;
|
|
2265
|
+
const names = typeExpr.namedChildren.filter((c) => c !== null);
|
|
2266
|
+
return names.length > 0 && names.every((n) => n.type === "identifier" && OPTIONAL_IMPORT_EXCEPT_TYPES.has(n.text));
|
|
2267
|
+
}
|
|
1886
2268
|
function isSilent(statement) {
|
|
1887
2269
|
if (SILENT_STATEMENTS.has(statement.type)) return true;
|
|
1888
2270
|
const value = statement.namedChildren[0];
|
|
@@ -2990,9 +3372,10 @@ function loadReuseBasis(db, indexVersion) {
|
|
|
2990
3372
|
}
|
|
2991
3373
|
const edgesBySrc = /* @__PURE__ */ new Map();
|
|
2992
3374
|
for (const e of db.listEdges(snap.id, { includeReferences: true })) {
|
|
2993
|
-
const
|
|
3375
|
+
const fileKey = parseSymbolId(e.srcId)?.fileId ?? e.srcId;
|
|
3376
|
+
const bucket = edgesBySrc.get(fileKey);
|
|
2994
3377
|
if (bucket) bucket.push(e);
|
|
2995
|
-
else edgesBySrc.set(
|
|
3378
|
+
else edgesBySrc.set(fileKey, [e]);
|
|
2996
3379
|
}
|
|
2997
3380
|
const sourceMetricsByFile = /* @__PURE__ */ new Map();
|
|
2998
3381
|
for (const m of db.listMetrics(snap.id)) {
|
|
@@ -3111,6 +3494,7 @@ function weightedInbound(nodes, edges) {
|
|
|
3111
3494
|
const util = /* @__PURE__ */ new Map();
|
|
3112
3495
|
for (const n of nodes) util.set(n.id, 0);
|
|
3113
3496
|
for (const e of resolveBarrelEdges(nodes, edges)) {
|
|
3497
|
+
if (e.kind === "calls") continue;
|
|
3114
3498
|
if (util.has(e.dstId)) util.set(e.dstId, util.get(e.dstId) + edgeWeight(e));
|
|
3115
3499
|
}
|
|
3116
3500
|
return util;
|
|
@@ -3141,6 +3525,58 @@ function computeMetrics(nodes, edges) {
|
|
|
3141
3525
|
return out;
|
|
3142
3526
|
}
|
|
3143
3527
|
|
|
3528
|
+
// src/analysis/call-metrics.ts
|
|
3529
|
+
var ABSENT = "<absent>";
|
|
3530
|
+
function computeCallMetrics(nodes, edges) {
|
|
3531
|
+
const inbound = collectInbound(edges);
|
|
3532
|
+
const out = [];
|
|
3533
|
+
for (const node of nodes) {
|
|
3534
|
+
if (node.kind !== "symbol" || node.attrs?.startLine === void 0) continue;
|
|
3535
|
+
const { callers, sites } = inbound.get(node.id) ?? { callers: /* @__PURE__ */ new Set(), sites: [] };
|
|
3536
|
+
out.push({ nodeId: node.id, name: "symbol_caller_count", value: callers.size, unit: "count" });
|
|
3537
|
+
const single = isPrivateHelper(node) && callers.size === 1 && isSymbolCaller(callers);
|
|
3538
|
+
out.push({ nodeId: node.id, name: "symbol_single_caller_helper", value: single ? 1 : 0, unit: "count" });
|
|
3539
|
+
const shape = readParamShape(node.attrs);
|
|
3540
|
+
if (shape && sites.length >= 2) {
|
|
3541
|
+
out.push({ nodeId: node.id, name: "symbol_constant_params", value: constantParams(shape, sites), unit: "count" });
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
return out;
|
|
3545
|
+
}
|
|
3546
|
+
function collectInbound(edges) {
|
|
3547
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
3548
|
+
for (const edge of edges) {
|
|
3549
|
+
if (edge.kind !== "calls") continue;
|
|
3550
|
+
let entry = inbound.get(edge.dstId);
|
|
3551
|
+
if (!entry) inbound.set(edge.dstId, entry = { callers: /* @__PURE__ */ new Set(), sites: [] });
|
|
3552
|
+
if (edge.srcId !== edge.dstId) entry.callers.add(edge.srcId);
|
|
3553
|
+
entry.sites.push(...edge.attrs?.sites ?? []);
|
|
3554
|
+
}
|
|
3555
|
+
return inbound;
|
|
3556
|
+
}
|
|
3557
|
+
function isPrivateHelper(node) {
|
|
3558
|
+
const own = node.name.slice(node.name.lastIndexOf(".") + 1);
|
|
3559
|
+
return node.attrs?.exported === false && !(own.startsWith("__") && own.endsWith("__"));
|
|
3560
|
+
}
|
|
3561
|
+
function isSymbolCaller(callers) {
|
|
3562
|
+
return [...callers].every((id) => parseSymbolId(id) !== null);
|
|
3563
|
+
}
|
|
3564
|
+
function constantParams(shape, sites) {
|
|
3565
|
+
let count = 0;
|
|
3566
|
+
shape.params.forEach((name, index) => {
|
|
3567
|
+
const values = sites.map((site) => argumentAt(site, name, index < shape.positional ? index : -1));
|
|
3568
|
+
if (values.every((v) => v !== null && v === values[0])) count++;
|
|
3569
|
+
});
|
|
3570
|
+
return count;
|
|
3571
|
+
}
|
|
3572
|
+
function argumentAt(site, name, position) {
|
|
3573
|
+
if (position >= 0 && position < site.args.length) return site.args[position] ?? null;
|
|
3574
|
+
if (site.kwargs && Object.hasOwn(site.kwargs, name)) return site.kwargs[name] ?? null;
|
|
3575
|
+
if (site.kwSplat) return null;
|
|
3576
|
+
if (position >= 0 && site.spreadFrom !== void 0 && position >= site.spreadFrom) return null;
|
|
3577
|
+
return ABSENT;
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3144
3580
|
// src/analysis/test-linker.ts
|
|
3145
3581
|
var DEFAULT_MIN_CO_EDIT_COUNT = 2;
|
|
3146
3582
|
var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
|
|
@@ -3350,6 +3786,7 @@ function buildIndexerMetrics(input) {
|
|
|
3350
3786
|
const history = input.history ? loadHistoryMetrics(nodeList, input.idRoot, input.history) : null;
|
|
3351
3787
|
return [
|
|
3352
3788
|
...computeMetrics(nodeList, [...input.edges.values()]),
|
|
3789
|
+
...computeCallMetrics(nodeList, input.edges.values()),
|
|
3353
3790
|
...computeSourceMetrics(
|
|
3354
3791
|
input.parsedFiles,
|
|
3355
3792
|
(p) => fileId(input.idRoot, p),
|
|
@@ -3428,7 +3865,7 @@ function qualifiedByName(file) {
|
|
|
3428
3865
|
}
|
|
3429
3866
|
|
|
3430
3867
|
// src/indexer.ts
|
|
3431
|
-
var INDEX_VERSION = "0.
|
|
3868
|
+
var INDEX_VERSION = "0.18.0";
|
|
3432
3869
|
var LANGUAGES = ["typescript", "python"];
|
|
3433
3870
|
function canonicalizePath(p) {
|
|
3434
3871
|
try {
|
|
@@ -3439,7 +3876,7 @@ function canonicalizePath(p) {
|
|
|
3439
3876
|
}
|
|
3440
3877
|
function normalizePaths(paths) {
|
|
3441
3878
|
if (paths.length === 0) throw new Error("indexPaths requires at least one path");
|
|
3442
|
-
return [...new Set(paths.map((p) =>
|
|
3879
|
+
return [...new Set(paths.map((p) => path8.resolve(p)))];
|
|
3443
3880
|
}
|
|
3444
3881
|
function countByKind(items) {
|
|
3445
3882
|
const out = {};
|
|
@@ -3543,9 +3980,9 @@ import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, par
|
|
|
3543
3980
|
|
|
3544
3981
|
// src/extractors/deep-ast.ts
|
|
3545
3982
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3546
|
-
import * as
|
|
3983
|
+
import * as path9 from "path";
|
|
3547
3984
|
import {
|
|
3548
|
-
Node as
|
|
3985
|
+
Node as Node6,
|
|
3549
3986
|
Project as Project2,
|
|
3550
3987
|
ScriptTarget as ScriptTarget2
|
|
3551
3988
|
} from "ts-morph";
|
|
@@ -3562,7 +3999,7 @@ function loadSourceFile(absPath) {
|
|
|
3562
3999
|
useInMemoryFileSystem: true,
|
|
3563
4000
|
compilerOptions: { target: ScriptTarget2.Latest, allowJs: true }
|
|
3564
4001
|
});
|
|
3565
|
-
return project.createSourceFile(`f${
|
|
4002
|
+
return project.createSourceFile(`f${path9.extname(absPath) || ".ts"}`, text);
|
|
3566
4003
|
} catch {
|
|
3567
4004
|
return null;
|
|
3568
4005
|
}
|
|
@@ -3604,10 +4041,10 @@ function empty(target, kind) {
|
|
|
3604
4041
|
};
|
|
3605
4042
|
}
|
|
3606
4043
|
function callableOf(decl) {
|
|
3607
|
-
if (
|
|
3608
|
-
if (
|
|
4044
|
+
if (Node6.isFunctionDeclaration(decl) || Node6.isMethodDeclaration(decl)) return decl;
|
|
4045
|
+
if (Node6.isVariableDeclaration(decl)) {
|
|
3609
4046
|
const init = decl.getInitializer();
|
|
3610
|
-
if (init && (
|
|
4047
|
+
if (init && (Node6.isArrowFunction(init) || Node6.isFunctionExpression(init))) return init;
|
|
3611
4048
|
}
|
|
3612
4049
|
return null;
|
|
3613
4050
|
}
|
|
@@ -3623,8 +4060,8 @@ function returnTypeOf(decl) {
|
|
|
3623
4060
|
return callableOf(decl)?.getReturnTypeNode()?.getText() ?? null;
|
|
3624
4061
|
}
|
|
3625
4062
|
function membersOf(decl) {
|
|
3626
|
-
if (
|
|
3627
|
-
if (
|
|
4063
|
+
if (Node6.isClassDeclaration(decl)) return classMembers(decl);
|
|
4064
|
+
if (Node6.isInterfaceDeclaration(decl)) return interfaceMembers(decl);
|
|
3628
4065
|
return [];
|
|
3629
4066
|
}
|
|
3630
4067
|
function classMembers(cls) {
|
|
@@ -3632,7 +4069,7 @@ function classMembers(cls) {
|
|
|
3632
4069
|
name: memberName(m),
|
|
3633
4070
|
memberKind: m.getKindName(),
|
|
3634
4071
|
signature: memberSignature(m),
|
|
3635
|
-
isStatic:
|
|
4072
|
+
isStatic: Node6.isStaticable(m) ? m.isStatic() : false
|
|
3636
4073
|
}));
|
|
3637
4074
|
}
|
|
3638
4075
|
function interfaceMembers(iface) {
|
|
@@ -3658,17 +4095,17 @@ function exportedDeclarations(sf) {
|
|
|
3658
4095
|
return out;
|
|
3659
4096
|
}
|
|
3660
4097
|
function memberName(m) {
|
|
3661
|
-
if (
|
|
3662
|
-
return
|
|
4098
|
+
if (Node6.isConstructorDeclaration(m)) return "constructor";
|
|
4099
|
+
return Node6.hasName(m) ? m.getName() : "";
|
|
3663
4100
|
}
|
|
3664
4101
|
function memberSignature(m) {
|
|
3665
|
-
if (
|
|
4102
|
+
if (Node6.isMethodDeclaration(m) || Node6.isMethodSignature(m) || Node6.isConstructorDeclaration(m)) {
|
|
3666
4103
|
const params = m.getParameters().map((p) => oneLine2(p.getText())).join(", ");
|
|
3667
4104
|
const ret = m.getReturnTypeNode()?.getText();
|
|
3668
|
-
const name =
|
|
4105
|
+
const name = Node6.isConstructorDeclaration(m) ? "constructor" : memberName(m);
|
|
3669
4106
|
return oneLine2(`${name}(${params})${ret ? `: ${ret}` : ""}`);
|
|
3670
4107
|
}
|
|
3671
|
-
if (
|
|
4108
|
+
if (Node6.isPropertyDeclaration(m) || Node6.isPropertySignature(m)) {
|
|
3672
4109
|
const type = m.getTypeNode()?.getText();
|
|
3673
4110
|
return oneLine2(`${memberName(m)}${type ? `: ${type}` : ""}`);
|
|
3674
4111
|
}
|
|
@@ -4476,6 +4913,36 @@ var SOURCE = [
|
|
|
4476
4913
|
source: "source-metrics",
|
|
4477
4914
|
description: "1 when the function's body is one call forwarding every parameter, in order, as a bare argument; else 0."
|
|
4478
4915
|
},
|
|
4916
|
+
{
|
|
4917
|
+
name: "symbol_caller_count",
|
|
4918
|
+
unit: "count",
|
|
4919
|
+
appliesTo: SYMBOL,
|
|
4920
|
+
rollup: "max",
|
|
4921
|
+
direction: "neutral",
|
|
4922
|
+
absent: "exclude",
|
|
4923
|
+
source: "call-graph",
|
|
4924
|
+
description: "Distinct symbols, or files at module level, with a resolved call to this function, method or class. Unresolved calls are not counted."
|
|
4925
|
+
},
|
|
4926
|
+
{
|
|
4927
|
+
name: "symbol_single_caller_helper",
|
|
4928
|
+
unit: "count",
|
|
4929
|
+
appliesTo: SYMBOL,
|
|
4930
|
+
rollup: "sum",
|
|
4931
|
+
direction: "higher-worse",
|
|
4932
|
+
absent: "exclude",
|
|
4933
|
+
source: "call-graph",
|
|
4934
|
+
description: "1 when a non-exported symbol has exactly one resolved caller and that caller is a symbol; else 0."
|
|
4935
|
+
},
|
|
4936
|
+
{
|
|
4937
|
+
name: "symbol_constant_params",
|
|
4938
|
+
unit: "count",
|
|
4939
|
+
appliesTo: SYMBOL,
|
|
4940
|
+
rollup: "max",
|
|
4941
|
+
direction: "higher-worse",
|
|
4942
|
+
absent: "exclude",
|
|
4943
|
+
source: "call-graph",
|
|
4944
|
+
description: "Parameters that every one of at least 2 resolved call sites passes the same literal, or never passes. Rest and keyword-splat parameters are ignored."
|
|
4945
|
+
},
|
|
4479
4946
|
{
|
|
4480
4947
|
name: "except_count",
|
|
4481
4948
|
unit: "count",
|
|
@@ -4504,7 +4971,7 @@ var SOURCE = [
|
|
|
4504
4971
|
direction: "higher-worse",
|
|
4505
4972
|
absent: "zero",
|
|
4506
4973
|
source: "exception-handling",
|
|
4507
|
-
description: "Handlers whose body is empty, pass, ..., continue, a bare or empty return, or a single logging call."
|
|
4974
|
+
description: "Handlers whose body is empty, pass, ..., continue, a bare or empty return, or a single logging call. Exempts Python's `except ImportError`/`ModuleNotFoundError` (alone or paired), the standard optional-dependency idiom."
|
|
4508
4975
|
},
|
|
4509
4976
|
{
|
|
4510
4977
|
name: "class_count",
|
|
@@ -4939,9 +5406,10 @@ function runMetricOutlierRule(rule, ctx) {
|
|
|
4939
5406
|
const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
|
|
4940
5407
|
if (value !== void 0) carriers.push({ node, value });
|
|
4941
5408
|
}
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
5409
|
+
const ranked = rule.rankNonZero ? carriers.filter((c) => c.value > 0) : carriers;
|
|
5410
|
+
if (ranked.length < (rule.minSample ?? DEFAULT_OUTLIER_MIN_SAMPLE)) return [];
|
|
5411
|
+
const threshold = percentileOf(ranked.map((c) => c.value), rule.percentile);
|
|
5412
|
+
return ranked.filter((c) => c.value > threshold && (rule.floor === void 0 || c.value > rule.floor)).map((c) => toViolation(rule, c.node, c.value, threshold));
|
|
4945
5413
|
}
|
|
4946
5414
|
function percentileOf(values, percentile) {
|
|
4947
5415
|
const sorted = [...values].sort((a, b) => a - b);
|
|
@@ -4951,7 +5419,8 @@ function percentileOf(values, percentile) {
|
|
|
4951
5419
|
return sorted[lower] + (sorted[upper] - sorted[lower]) * (rank - lower);
|
|
4952
5420
|
}
|
|
4953
5421
|
function toViolation(rule, node, value, threshold) {
|
|
4954
|
-
const
|
|
5422
|
+
const floorSuffix = rule.floor === void 0 ? "" : `, floor ${formatNumber(rule.floor)}`;
|
|
5423
|
+
const bound = `p${formatNumber(rule.percentile)} ${formatNumber(threshold)}${floorSuffix}`;
|
|
4955
5424
|
return {
|
|
4956
5425
|
ruleId: rule.id,
|
|
4957
5426
|
severity: severityOf(rule),
|
|
@@ -5178,6 +5647,127 @@ function withoutUndefined(finding) {
|
|
|
5178
5647
|
return Object.fromEntries(Object.entries(finding).filter(([, v]) => v !== void 0));
|
|
5179
5648
|
}
|
|
5180
5649
|
|
|
5650
|
+
// src/check/finding-store.ts
|
|
5651
|
+
import { createHash as createHash2 } from "crypto";
|
|
5652
|
+
function normalizeFlaggedText(text) {
|
|
5653
|
+
return text.split(/\r?\n/).map((line) => line.trim().replace(/\s+/g, " ")).filter((line) => line.length > 0).join("\n");
|
|
5654
|
+
}
|
|
5655
|
+
function hashText(text) {
|
|
5656
|
+
return createHash2("sha256").update(text, "utf8").digest("hex").slice(0, 16);
|
|
5657
|
+
}
|
|
5658
|
+
function findingKey(input, ordinal = 0) {
|
|
5659
|
+
const { finding } = input;
|
|
5660
|
+
const anchor = input.anchor ?? finding.path;
|
|
5661
|
+
const textHash = hashText(normalizeFlaggedText(input.flaggedText));
|
|
5662
|
+
return `${finding.tool}:${finding.signal}:${anchor}:${textHash}#${ordinal}`;
|
|
5663
|
+
}
|
|
5664
|
+
function keyFindings(inputs) {
|
|
5665
|
+
const ordered = [...inputs].sort(compareBySource);
|
|
5666
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5667
|
+
return ordered.map((input) => {
|
|
5668
|
+
const base = findingKey(input);
|
|
5669
|
+
const ordinal = seen.get(base) ?? 0;
|
|
5670
|
+
seen.set(base, ordinal + 1);
|
|
5671
|
+
const stored = { key: findingKey(input, ordinal), finding: input.finding };
|
|
5672
|
+
if (input.excerptHash !== void 0) stored.excerptHash = input.excerptHash;
|
|
5673
|
+
return stored;
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5676
|
+
function compareBySource(a, b) {
|
|
5677
|
+
const fa = a.finding;
|
|
5678
|
+
const fb = b.finding;
|
|
5679
|
+
return fa.path.localeCompare(fb.path) || (fa.lineStart ?? 0) - (fb.lineStart ?? 0) || (fa.lineEnd ?? 0) - (fb.lineEnd ?? 0) || (fa.evidence ?? "").localeCompare(fb.evidence ?? "") || fa.id.localeCompare(fb.id);
|
|
5680
|
+
}
|
|
5681
|
+
function saveFindings(store, snapshotId, findings) {
|
|
5682
|
+
const insert = store.db.prepare(
|
|
5683
|
+
`INSERT OR REPLACE INTO finding
|
|
5684
|
+
(snapshot_id, key, finding_id, tool, signal, path, line_start, line_end, severity, excerpt_hash, attrs)
|
|
5685
|
+
VALUES (@snapshotId, @key, @findingId, @tool, @signal, @path, @lineStart, @lineEnd, @severity, @excerptHash, @attrs)`
|
|
5686
|
+
);
|
|
5687
|
+
store.db.transaction(() => {
|
|
5688
|
+
for (const { key, finding, excerptHash } of findings) {
|
|
5689
|
+
insert.run({
|
|
5690
|
+
snapshotId,
|
|
5691
|
+
key,
|
|
5692
|
+
findingId: finding.id,
|
|
5693
|
+
tool: finding.tool,
|
|
5694
|
+
signal: finding.signal,
|
|
5695
|
+
path: finding.path,
|
|
5696
|
+
lineStart: finding.lineStart ?? null,
|
|
5697
|
+
lineEnd: finding.lineEnd ?? null,
|
|
5698
|
+
severity: finding.severity,
|
|
5699
|
+
excerptHash: excerptHash ?? null,
|
|
5700
|
+
attrs: JSON.stringify(finding)
|
|
5701
|
+
});
|
|
5702
|
+
}
|
|
5703
|
+
})();
|
|
5704
|
+
}
|
|
5705
|
+
function listFindings(store, snapshotId) {
|
|
5706
|
+
const rows = store.db.prepare("SELECT key, excerpt_hash, attrs FROM finding WHERE snapshot_id = ? ORDER BY key").all(snapshotId);
|
|
5707
|
+
return rows.map((row) => {
|
|
5708
|
+
const stored = { key: row.key, finding: JSON.parse(row.attrs) };
|
|
5709
|
+
if (row.excerpt_hash !== null) stored.excerptHash = row.excerpt_hash;
|
|
5710
|
+
return stored;
|
|
5711
|
+
});
|
|
5712
|
+
}
|
|
5713
|
+
function saveVerdicts(store, snapshotId, verdicts) {
|
|
5714
|
+
const insert = store.db.prepare(
|
|
5715
|
+
`INSERT OR REPLACE INTO verdict
|
|
5716
|
+
(snapshot_id, key, verdict, rationale, excerpt_hash, model, run_id, cost_usd, provenance, control_run,
|
|
5717
|
+
carried_from, attrs)
|
|
5718
|
+
VALUES (@snapshotId, @key, @verdict, @rationale, @excerptHash, @model, @runId, @costUsd, @provenance,
|
|
5719
|
+
@controlRun, @carriedFrom, @attrs)`
|
|
5720
|
+
);
|
|
5721
|
+
store.db.transaction(() => {
|
|
5722
|
+
for (const v of verdicts) insert.run(verdictParams(snapshotId, v));
|
|
5723
|
+
})();
|
|
5724
|
+
}
|
|
5725
|
+
function verdictParams(snapshotId, v) {
|
|
5726
|
+
return {
|
|
5727
|
+
snapshotId,
|
|
5728
|
+
key: v.key,
|
|
5729
|
+
verdict: v.verdict,
|
|
5730
|
+
rationale: v.rationale,
|
|
5731
|
+
excerptHash: v.excerptHash,
|
|
5732
|
+
model: v.model ?? null,
|
|
5733
|
+
runId: v.runId ?? null,
|
|
5734
|
+
costUsd: v.costUsd ?? null,
|
|
5735
|
+
provenance: v.provenance ?? null,
|
|
5736
|
+
controlRun: v.controlRun ?? null,
|
|
5737
|
+
carriedFrom: v.carriedFrom ?? null,
|
|
5738
|
+
attrs: JSON.stringify({ citations: v.citations, dropped: v.dropped ?? [] })
|
|
5739
|
+
};
|
|
5740
|
+
}
|
|
5741
|
+
function listVerdicts(store, snapshotId) {
|
|
5742
|
+
const rows = store.db.prepare("SELECT * FROM verdict WHERE snapshot_id = ? ORDER BY key").all(snapshotId);
|
|
5743
|
+
return rows.map(rowToVerdict);
|
|
5744
|
+
}
|
|
5745
|
+
function rowToVerdict(row) {
|
|
5746
|
+
const attrs = JSON.parse(row.attrs);
|
|
5747
|
+
const verdict = {
|
|
5748
|
+
key: row.key,
|
|
5749
|
+
verdict: row.verdict,
|
|
5750
|
+
rationale: row.rationale,
|
|
5751
|
+
citations: attrs.citations,
|
|
5752
|
+
excerptHash: row.excerpt_hash
|
|
5753
|
+
};
|
|
5754
|
+
if (row.model !== null) verdict.model = row.model;
|
|
5755
|
+
if (row.cost_usd !== null) verdict.costUsd = row.cost_usd;
|
|
5756
|
+
if (row.run_id !== null) verdict.runId = row.run_id;
|
|
5757
|
+
if (row.provenance !== null) verdict.provenance = row.provenance;
|
|
5758
|
+
if (row.control_run !== null) verdict.controlRun = row.control_run;
|
|
5759
|
+
if (attrs.dropped.length > 0) verdict.dropped = attrs.dropped;
|
|
5760
|
+
if (row.carried_from !== null) verdict.carriedFrom = row.carried_from;
|
|
5761
|
+
return verdict;
|
|
5762
|
+
}
|
|
5763
|
+
function carryForwardVerdicts(store, from, to) {
|
|
5764
|
+
const targets = new Map(listFindings(store, to).map((f) => [f.key, f.excerptHash]));
|
|
5765
|
+
const existing = new Set(listVerdicts(store, to).map((v) => v.key));
|
|
5766
|
+
const carried = listVerdicts(store, from).filter((v) => !existing.has(v.key) && targets.get(v.key) === v.excerptHash).map((v) => ({ ...v, carriedFrom: v.carriedFrom ?? from }));
|
|
5767
|
+
saveVerdicts(store, to, carried);
|
|
5768
|
+
return carried.length;
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5181
5771
|
// src/check/validate.ts
|
|
5182
5772
|
function validateRules(input, options = {}) {
|
|
5183
5773
|
if (!input || typeof input !== "object") {
|
|
@@ -5340,6 +5930,12 @@ function assertMetricOutlier(r, warn) {
|
|
|
5340
5930
|
if (r.minSample !== void 0 && (!Number.isInteger(r.minSample) || r.minSample < 1)) {
|
|
5341
5931
|
throw new Error(`${r.id}: minSample must be a positive integer`);
|
|
5342
5932
|
}
|
|
5933
|
+
if (r.floor !== void 0 && (typeof r.floor !== "number" || !Number.isFinite(r.floor) || r.floor < 0)) {
|
|
5934
|
+
throw new Error(`${r.id}: floor must be a non-negative finite number`);
|
|
5935
|
+
}
|
|
5936
|
+
if (r.rankNonZero !== void 0 && typeof r.rankNonZero !== "boolean") {
|
|
5937
|
+
throw new Error(`${r.id}: rankNonZero must be a boolean`);
|
|
5938
|
+
}
|
|
5343
5939
|
const ruleId = r.id;
|
|
5344
5940
|
return {
|
|
5345
5941
|
type: "metric-outlier",
|
|
@@ -5348,6 +5944,8 @@ function assertMetricOutlier(r, warn) {
|
|
|
5348
5944
|
kind: r.kind,
|
|
5349
5945
|
percentile: r.percentile,
|
|
5350
5946
|
minSample: r.minSample,
|
|
5947
|
+
floor: r.floor,
|
|
5948
|
+
rankNonZero: r.rankNonZero,
|
|
5351
5949
|
severity: r.severity
|
|
5352
5950
|
};
|
|
5353
5951
|
}
|
|
@@ -5419,18 +6017,18 @@ function checkSnapshot(store, options) {
|
|
|
5419
6017
|
});
|
|
5420
6018
|
return { snapshot, baselineSnapshot, result };
|
|
5421
6019
|
}
|
|
5422
|
-
async function loadCheckRules(
|
|
6020
|
+
async function loadCheckRules(path11, options = {}) {
|
|
5423
6021
|
let raw;
|
|
5424
6022
|
try {
|
|
5425
|
-
raw = await readFile2(
|
|
6023
|
+
raw = await readFile2(path11, "utf8");
|
|
5426
6024
|
} catch (err) {
|
|
5427
|
-
throw new Error(`Cannot read rules file at ${
|
|
6025
|
+
throw new Error(`Cannot read rules file at ${path11}: ${errorMessage(err)}`);
|
|
5428
6026
|
}
|
|
5429
6027
|
let parsed;
|
|
5430
6028
|
try {
|
|
5431
6029
|
parsed = JSON.parse(raw);
|
|
5432
6030
|
} catch (err) {
|
|
5433
|
-
throw new Error(`Invalid JSON in ${
|
|
6031
|
+
throw new Error(`Invalid JSON in ${path11}: ${errorMessage(err)}`);
|
|
5434
6032
|
}
|
|
5435
6033
|
return validateRules(parsed, options);
|
|
5436
6034
|
}
|
|
@@ -5842,7 +6440,7 @@ function pushMulti(map, key, value) {
|
|
|
5842
6440
|
}
|
|
5843
6441
|
|
|
5844
6442
|
// src/conventions/areas.ts
|
|
5845
|
-
import * as
|
|
6443
|
+
import * as path10 from "path";
|
|
5846
6444
|
import { contentHashOf as contentHashOf2 } from "@titan-design/store-sqlite";
|
|
5847
6445
|
var DEFAULT_MIN_SIZE = 3;
|
|
5848
6446
|
var PROMPT_FILE_CAP = 30;
|
|
@@ -5926,7 +6524,7 @@ function rankByDegree(members, edges) {
|
|
|
5926
6524
|
function dominantDirectory(files) {
|
|
5927
6525
|
const counts = /* @__PURE__ */ new Map();
|
|
5928
6526
|
for (const f of files) {
|
|
5929
|
-
const dir =
|
|
6527
|
+
const dir = path10.posix.dirname(f);
|
|
5930
6528
|
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
5931
6529
|
}
|
|
5932
6530
|
const dirs = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
@@ -6054,6 +6652,7 @@ export {
|
|
|
6054
6652
|
DEFAULT_OUTLIER_MIN_SAMPLE,
|
|
6055
6653
|
DOMAIN_DDL,
|
|
6056
6654
|
EXCEPTION_METRIC_NAMES,
|
|
6655
|
+
FINDING_DDL,
|
|
6057
6656
|
GROWTH_RISK_METRIC_NAMES,
|
|
6058
6657
|
INDEX_VERSION,
|
|
6059
6658
|
KIT,
|
|
@@ -6081,6 +6680,7 @@ export {
|
|
|
6081
6680
|
canonicalEdgeKind,
|
|
6082
6681
|
canonicalMetricName,
|
|
6083
6682
|
canonicalRole,
|
|
6683
|
+
carryForwardVerdicts,
|
|
6084
6684
|
checkSnapshot,
|
|
6085
6685
|
classifyRole,
|
|
6086
6686
|
collectDeclaredNames,
|
|
@@ -6117,6 +6717,7 @@ export {
|
|
|
6117
6717
|
fileId,
|
|
6118
6718
|
findConventions,
|
|
6119
6719
|
findSimilarCapability,
|
|
6720
|
+
findingKey,
|
|
6120
6721
|
getConventionMap,
|
|
6121
6722
|
getEdgeWeight,
|
|
6122
6723
|
getLanguageFromPath3 as getLanguageFromPath,
|
|
@@ -6124,25 +6725,30 @@ export {
|
|
|
6124
6725
|
groupTestsBySource,
|
|
6125
6726
|
hashContent,
|
|
6126
6727
|
hashEmbedText,
|
|
6728
|
+
hashText,
|
|
6127
6729
|
indexPaths,
|
|
6128
6730
|
invertBuckets,
|
|
6129
6731
|
isGeneratedByHeuristic,
|
|
6130
6732
|
isGeneratedFile,
|
|
6131
6733
|
isInsideGitRepo,
|
|
6734
|
+
keyFindings,
|
|
6132
6735
|
lineagePath,
|
|
6133
6736
|
linkTestsToSources,
|
|
6134
6737
|
listEdges,
|
|
6135
6738
|
listEdgesTouching,
|
|
6136
6739
|
listEmbeddableSymbols,
|
|
6740
|
+
listFindings,
|
|
6137
6741
|
listMetrics,
|
|
6138
6742
|
listMetricsForNode,
|
|
6139
6743
|
listNodes,
|
|
6744
|
+
listVerdicts,
|
|
6140
6745
|
loadCheckRules,
|
|
6141
6746
|
loadGeneratedPatterns,
|
|
6142
6747
|
loadHistoryMetrics,
|
|
6143
6748
|
loadLineage,
|
|
6144
6749
|
matchesAny,
|
|
6145
6750
|
moduleId,
|
|
6751
|
+
normalizeFlaggedText,
|
|
6146
6752
|
openCodeGraph,
|
|
6147
6753
|
packageId,
|
|
6148
6754
|
parentModuleId,
|
|
@@ -6162,6 +6768,8 @@ export {
|
|
|
6162
6768
|
resolveSnapshot,
|
|
6163
6769
|
runChecks,
|
|
6164
6770
|
runPrune,
|
|
6771
|
+
saveFindings,
|
|
6772
|
+
saveVerdicts,
|
|
6165
6773
|
shouldIncludeFile2 as shouldIncludeFile,
|
|
6166
6774
|
snapshotPageRank,
|
|
6167
6775
|
snapshotReferenceEdges,
|