@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/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 SNAPSHOT_SCOPED_TABLES = ["node", "edge", "metric", "id_alias", "file_fingerprint"];
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((e) => e.kind !== "references");
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; `references` edges are excluded unless asked, as in {@link listEdges}. */
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((e) => e.kind !== "references");
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 path9 from "path";
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
- walkScopes(file.tree.rootNode, python, (node, scope) => {
671
- const named = declaredNodeAt(node, declTypes);
672
- if (!named) return;
711
+ forEachDeclaration(file, (name, qualifiedName, node) => {
673
712
  out.push({
674
- name: named.name,
675
- qualifiedName: qualify(scope, named.name),
676
- span: { startLine: named.node.startPosition.row + 1, endLine: named.node.endPosition.row + 1 }
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
- return [{ nodes: [...nodes, ...symbolNodes2, ...externalNodes], edges }];
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 path5 from "path";
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 path5.join(base, "__init__.py");
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
- return [{ nodes: [...nodes, ...externals], edges }];
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 = path5.resolve(path5.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
1207
- const roots = leadingDots > 0 ? [relativeBase] : [path5.dirname(fromAbs), this.repoRoot];
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(path5.join(root, ...parts))) {
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: { exported: isPublicName(name), startLine: span.startLine, endLine: span.endLine }
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 path6 from "path";
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 path6.posix.dirname(oldPath) === path6.posix.dirname(newPath) ? "rename" : "move";
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 = path6.join(repoRoot, pair.oldPath);
1306
- const newAbs = path6.join(repoRoot, pair.newPath);
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 path12 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
1400
- if (path12) return { steps: path12, connected: true };
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 path7 from "path";
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(path7.join(rootDir, ".gitattributes"), "utf-8");
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 path8 from "path";
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 = path8.join(dir, entry.name);
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 = path8.relative(rootDir, full);
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
- if (edge.kind === "references" && !nodes.has(edge.dstId)) edges.delete(key);
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 bucket = edgesBySrc.get(e.srcId);
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(e.srcId, [e]);
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.17.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) => path9.resolve(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 path10 from "path";
3983
+ import * as path9 from "path";
3547
3984
  import {
3548
- Node as Node4,
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${path10.extname(absPath) || ".ts"}`, text);
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 (Node4.isFunctionDeclaration(decl) || Node4.isMethodDeclaration(decl)) return decl;
3608
- if (Node4.isVariableDeclaration(decl)) {
4044
+ if (Node6.isFunctionDeclaration(decl) || Node6.isMethodDeclaration(decl)) return decl;
4045
+ if (Node6.isVariableDeclaration(decl)) {
3609
4046
  const init = decl.getInitializer();
3610
- if (init && (Node4.isArrowFunction(init) || Node4.isFunctionExpression(init))) return 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 (Node4.isClassDeclaration(decl)) return classMembers(decl);
3627
- if (Node4.isInterfaceDeclaration(decl)) return interfaceMembers(decl);
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: Node4.isStaticable(m) ? m.isStatic() : false
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 (Node4.isConstructorDeclaration(m)) return "constructor";
3662
- return Node4.hasName(m) ? m.getName() : "";
4098
+ if (Node6.isConstructorDeclaration(m)) return "constructor";
4099
+ return Node6.hasName(m) ? m.getName() : "";
3663
4100
  }
3664
4101
  function memberSignature(m) {
3665
- if (Node4.isMethodDeclaration(m) || Node4.isMethodSignature(m) || Node4.isConstructorDeclaration(m)) {
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 = Node4.isConstructorDeclaration(m) ? "constructor" : memberName(m);
4105
+ const name = Node6.isConstructorDeclaration(m) ? "constructor" : memberName(m);
3669
4106
  return oneLine2(`${name}(${params})${ret ? `: ${ret}` : ""}`);
3670
4107
  }
3671
- if (Node4.isPropertyDeclaration(m) || Node4.isPropertySignature(m)) {
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
- if (carriers.length < (rule.minSample ?? DEFAULT_OUTLIER_MIN_SAMPLE)) return [];
4943
- const threshold = percentileOf(carriers.map((c) => c.value), rule.percentile);
4944
- return carriers.filter((c) => c.value > threshold).map((c) => toViolation(rule, c.node, c.value, threshold));
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 bound = `p${formatNumber(rule.percentile)} ${formatNumber(threshold)}`;
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(path12, options = {}) {
6020
+ async function loadCheckRules(path11, options = {}) {
5423
6021
  let raw;
5424
6022
  try {
5425
- raw = await readFile2(path12, "utf8");
6023
+ raw = await readFile2(path11, "utf8");
5426
6024
  } catch (err) {
5427
- throw new Error(`Cannot read rules file at ${path12}: ${errorMessage(err)}`);
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 ${path12}: ${errorMessage(err)}`);
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 path11 from "path";
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 = path11.posix.dirname(f);
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,