@orangepro/orangepro-mcp 0.2.41 → 0.2.43

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.
@@ -10,6 +10,7 @@ import { buildImportGraph } from "../resolve/importGraph.js";
10
10
  import { walkBarrel } from "../resolve/barrelWalker.js";
11
11
  import { resetResolverCaches } from "../resolve/resolver.js";
12
12
  import { resetExportIndexCache } from "../resolve/exportIndex.js";
13
+ import { builtInRankExclusion } from "../score/rankEligibility.js";
13
14
  import { baseName, extOf, GENERATED_CODE_REASON, isGeneratedCode, isNonProductPath, NON_PRODUCT_REASON, isTestSupportPath, languageOf, manifestKindOf, roleOf } from "./classify.js";
14
15
  import { classifyTestLayer } from "./testLayer.js";
15
16
  import { detectFromPackageJson, detectFrameworksFromManifest, detectManifestPackage, frameworkFromConfig } from "./frameworks.js";
@@ -142,6 +143,59 @@ function behaviorSurfaceReason(relPath, name, memberOf) {
142
143
  return null;
143
144
  }
144
145
  const RUNTIME_SOURCE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/i;
146
+ /**
147
+ * Return qualified `Class.run` symbols whose class extends a CLI command base
148
+ * imported from a known runtime package. This is deliberately import-scoped:
149
+ * a local class merely named Command/SfCommand never becomes an entry point.
150
+ */
151
+ function cliEntrySymbols(content) {
152
+ const out = new Map();
153
+ const sourceFile = ts.createSourceFile("cli-entry.tsx", content, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
154
+ const importedBases = new Map();
155
+ for (const stmt of sourceFile.statements) {
156
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier) || !stmt.importClause || stmt.importClause.isTypeOnly)
157
+ continue;
158
+ const source = stmt.moduleSpecifier.text;
159
+ const framework = source === "@oclif/core" ? "oclif" :
160
+ source === "@salesforce/sf-plugins-core" ? "salesforce-cli" :
161
+ source === "commander" ? "commander" :
162
+ undefined;
163
+ if (!framework)
164
+ continue;
165
+ const bindings = stmt.importClause.namedBindings;
166
+ if (!bindings || !ts.isNamedImports(bindings))
167
+ continue;
168
+ for (const element of bindings.elements) {
169
+ if (element.isTypeOnly)
170
+ continue;
171
+ const imported = element.propertyName?.text ?? element.name.text;
172
+ if (imported === "Command" || imported === "SfCommand")
173
+ importedBases.set(element.name.text, framework);
174
+ }
175
+ }
176
+ for (const stmt of sourceFile.statements) {
177
+ if (!ts.isClassDeclaration(stmt) || !stmt.name)
178
+ continue;
179
+ let framework;
180
+ for (const clause of stmt.heritageClauses ?? []) {
181
+ if (clause.token !== ts.SyntaxKind.ExtendsKeyword)
182
+ continue;
183
+ for (const type of clause.types) {
184
+ if (ts.isIdentifier(type.expression))
185
+ framework = importedBases.get(type.expression.text) ?? framework;
186
+ }
187
+ }
188
+ if (!framework)
189
+ continue;
190
+ const hasRun = stmt.members.some((member) => ts.isMethodDeclaration(member) &&
191
+ !!member.body &&
192
+ ts.isIdentifier(member.name) &&
193
+ member.name.text === "run");
194
+ if (hasRun)
195
+ out.set(`${stmt.name.text}.run`, framework);
196
+ }
197
+ return out;
198
+ }
145
199
  /**
146
200
  * Resolve package-level public entry modules from package metadata and the two
147
201
  * conventional source entry paths. This gives libraries a semantic behavior
@@ -487,6 +541,20 @@ export function analyzeRepo(root, opts = {}) {
487
541
  if (set.size < 32)
488
542
  set.add(name);
489
543
  };
544
+ // Stronger than an unanchored receiver name: the qualifier is a runtime
545
+ // binding imported from a bare external module. Preserve that provenance so
546
+ // risk scoring can recognize replacement operations without treating every
547
+ // local `replace*` method as destructive.
548
+ const importedStaticCalleesByCaller = new Map();
549
+ const recordImportedStaticCallee = (callerId, name) => {
550
+ let set = importedStaticCalleesByCaller.get(callerId);
551
+ if (!set) {
552
+ set = new Set();
553
+ importedStaticCalleesByCaller.set(callerId, set);
554
+ }
555
+ if (set.size < 32)
556
+ set.add(name);
557
+ };
490
558
  // Raw (caller, callee) call pairs per TS/JS code file, resolved after the
491
559
  // import graph is built (cross-file calls need its bindings + targets).
492
560
  const rawCallsByFile = new Map();
@@ -774,10 +842,18 @@ export function analyzeRepo(root, opts = {}) {
774
842
  }
775
843
  const content = readSafe(file.absPath);
776
844
  const isGenerated = content ? isGeneratedCode(content) : false;
845
+ const treeStructure = content && isTreeSitterLanguage(language) && treeSitterReady(language)
846
+ ? extractTreeSitterStructure(content, language, file.relPath)
847
+ : undefined;
777
848
  if (isGenerated)
778
849
  generatedCodeFiles.add(file.relPath);
779
- if (content && (language === "typescript" || language === "javascript") && !isGenerated && !isNonProductPath(file.relPath)) {
780
- for (const contract of extractBehaviorContracts(content, file.relPath)) {
850
+ if (content && !isGenerated && !isNonProductPath(file.relPath)) {
851
+ const behaviorContracts = language === "typescript" || language === "javascript"
852
+ ? extractBehaviorContracts(content, file.relPath)
853
+ : language === "python"
854
+ ? (treeStructure?.pythonBehaviorContracts ?? [])
855
+ : [];
856
+ for (const contract of behaviorContracts) {
781
857
  if (behaviorSurfaceExclusionReason(file.relPath, contract.handler ?? contract.title))
782
858
  continue;
783
859
  behaviorContractsTotal++;
@@ -800,7 +876,9 @@ export function analyzeRepo(root, opts = {}) {
800
876
  file: contract.file,
801
877
  source: contract.source,
802
878
  ...(contract.handler ? { handler: contract.handler } : {}),
803
- ...(contract.controller ? { controller: contract.controller } : {})
879
+ ...(contract.controller ? { controller: contract.controller } : {}),
880
+ ...(contract.route_prefix_status ? { route_prefix_status: contract.route_prefix_status } : {}),
881
+ ...(contract.route_prefix ? { route_prefix: contract.route_prefix } : {})
804
882
  },
805
883
  evidence_strength: "hard",
806
884
  review_status: "auto_detected",
@@ -830,6 +908,7 @@ export function analyzeRepo(root, opts = {}) {
830
908
  const extraction = opts.parseCache
831
909
  ? opts.parseCache.symbols(file.hash, `${language}#${extractionBackend(language)}`, () => extractFileSymbols(content, language))
832
910
  : extractFileSymbols(content, language);
911
+ const cliEntries = language === "typescript" || language === "javascript" ? cliEntrySymbols(content) : new Map();
833
912
  if (extraction.truncated)
834
913
  symbolFilesTruncated++;
835
914
  for (const sym of extraction.symbols) {
@@ -855,20 +934,24 @@ export function analyzeRepo(root, opts = {}) {
855
934
  // Trivial accessors (Java getId/toString, Python __repr__) are real
856
935
  // symbols but carry no testable behavior — kept in the graph, dropped
857
936
  // from the denominator with disclosure so coverage is not under-claimed.
858
- const isBoilerplate = !isDts && !isInfra && !isGeneratedSymbol && callableKind && isBoilerplateSymbol(sym.name, language, sym.symbol_kind, sym.trivial_accessor);
937
+ const boilerplateName = language === "python" ? (sym.name.split(".").pop() ?? sym.name) : sym.name;
938
+ const isBoilerplate = !isDts && !isInfra && !isGeneratedSymbol && callableKind && isBoilerplateSymbol(boilerplateName, language, sym.symbol_kind, sym.trivial_accessor);
859
939
  const callableBehaviorCandidate = !isDts && !isInfra && !isGeneratedSymbol && !isBoilerplate && behaviorCallableKind;
860
940
  const surfaceExclusionReason = callableBehaviorCandidate
861
941
  ? behaviorSurfaceExclusionReason(file.relPath, sym.name, sym.member_of)
862
942
  : null;
943
+ const cliFramework = cliEntries.get(sym.name);
944
+ const cliDenominatorOverride = Boolean(cliFramework) && callableBehaviorCandidate;
863
945
  const surfaceReason = nonTsClassBehavior
864
946
  ? "Non-TS class/constructor behavior surface — countable for language proof."
865
947
  : callableBehaviorCandidate && !surfaceExclusionReason
866
948
  ? behaviorSurfaceReason(file.relPath, sym.name, sym.member_of)
867
949
  : null;
868
950
  const publicApiEntry = callableBehaviorCandidate && !surfaceExclusionReason && publicEntryFiles.has(file.relPath);
869
- const eligible = callableBehaviorCandidate && (surfaceReason !== null || publicApiEntry);
870
- const behaviorSurfaceExcluded = callableBehaviorCandidate && surfaceExclusionReason !== null;
951
+ const eligible = cliDenominatorOverride || (callableBehaviorCandidate && (surfaceReason !== null || publicApiEntry));
952
+ const behaviorSurfaceExcluded = callableBehaviorCandidate && surfaceExclusionReason !== null && !cliDenominatorOverride;
871
953
  const notEntryPointAdjacent = callableBehaviorCandidate && !eligible && !behaviorSurfaceExcluded;
954
+ const rankingExclusion = builtInRankExclusion(file.relPath);
872
955
  if (isBoilerplate)
873
956
  excludedBoilerplate++;
874
957
  nodes.push(makeNode({
@@ -881,10 +964,27 @@ export function analyzeRepo(root, opts = {}) {
881
964
  ...(sym.start_line ? { start_line: sym.start_line } : {}),
882
965
  ...(sym.end_line ? { end_line: sym.end_line } : {}),
883
966
  ...(sym.member_of ? { member_of: sym.member_of } : {}),
967
+ ...(cliFramework ? { cli_entrypoint: true, entry_lane: "cli", cli_framework: cliFramework } : {}),
884
968
  ...(sym.callable !== undefined ? { callable_const: sym.callable } : {}),
885
- ...(surfaceReason ? { behavior_surface: "entrypoint_adjacent" } : publicApiEntry ? { behavior_surface: "public_api_entry" } : {}),
969
+ ...(cliDenominatorOverride
970
+ ? {
971
+ behavior_surface: "cli_command_entry",
972
+ denominator_reason_code: "cli_command_entry",
973
+ ...(surfaceExclusionReason ? { denominator_override_from: surfaceExclusionReason } : {})
974
+ }
975
+ : surfaceReason
976
+ ? { behavior_surface: "entrypoint_adjacent" }
977
+ : publicApiEntry
978
+ ? { behavior_surface: "public_api_entry" }
979
+ : {}),
886
980
  ...(behaviorSurfaceExcluded ? { denominator_reason_code: "infra_behavior_surface" } : {}),
887
- ...(notEntryPointAdjacent ? { denominator_reason_code: "not_entry_point_adjacent" } : {})
981
+ ...(notEntryPointAdjacent ? { denominator_reason_code: "not_entry_point_adjacent" } : {}),
982
+ ...(rankingExclusion
983
+ ? {
984
+ ranking_exclusion_reason_code: rankingExclusion.code,
985
+ ranking_exclusion_reason: rankingExclusion.reason
986
+ }
987
+ : {})
888
988
  },
889
989
  evidence_strength: "hard",
890
990
  review_status: "auto_detected",
@@ -900,17 +1000,19 @@ export function analyzeRepo(root, opts = {}) {
900
1000
  ? GENERATED_CODE_REASON
901
1001
  : isBoilerplate
902
1002
  ? BOILERPLATE_REASON
903
- : eligible && surfaceReason
904
- ? surfaceReason
905
- : publicApiEntry
906
- ? "Callable export from a package public entry module — countable library behavior surface."
907
- : behaviorSurfaceExcluded && surfaceExclusionReason
908
- ? surfaceExclusionReason
909
- : callableBehaviorCandidate
910
- ? "Callable export is not API/service/route/job-adjacent — kept for grounding, excluded from the behavior denominator."
911
- : sym.symbol_kind === "class"
912
- ? "Exported class container — kept for grounding; methods/functions carry behavior in v1."
913
- : "Exported const (not provably callable) — excluded from the denominator in v1."
1003
+ : cliDenominatorOverride
1004
+ ? `Verified ${cliFramework} CLI command run() entry — countable behavior surface${surfaceExclusionReason ? `; overrides: ${surfaceExclusionReason}` : "."}`
1005
+ : eligible && surfaceReason
1006
+ ? surfaceReason
1007
+ : publicApiEntry
1008
+ ? "Callable export from a package public entry module — countable library behavior surface."
1009
+ : behaviorSurfaceExcluded && surfaceExclusionReason
1010
+ ? surfaceExclusionReason
1011
+ : callableBehaviorCandidate
1012
+ ? "Callable export is not API/service/route/job-adjacent — kept for grounding, excluded from the behavior denominator."
1013
+ : sym.symbol_kind === "class"
1014
+ ? "Exported class container — kept for grounding; methods/functions carry behavior in v1."
1015
+ : "Exported const (not provably callable) — excluded from the denominator in v1."
914
1016
  }));
915
1017
  edges.push(makeEdge({
916
1018
  from_external_id: symId,
@@ -978,9 +1080,8 @@ export function analyzeRepo(root, opts = {}) {
978
1080
  if (generatedServices.length > 0)
979
1081
  medusaGeneratedServicesByFile.set(file.relPath, generatedServices);
980
1082
  }
981
- else if (isTreeSitterLanguage(language) && treeSitterReady(language) && !isNonProductFile(file.relPath)) {
982
- const structure = extractTreeSitterStructure(content, language);
983
- nonTsStructureByFile.set(file.relPath, { language, structure });
1083
+ else if (treeStructure && !isNonProductFile(file.relPath)) {
1084
+ nonTsStructureByFile.set(file.relPath, { language, structure: treeStructure });
984
1085
  }
985
1086
  }
986
1087
  }
@@ -1075,10 +1176,18 @@ export function analyzeRepo(root, opts = {}) {
1075
1176
  const importBindingsByFile = new Map();
1076
1177
  const typeBindingsByFile = new Map();
1077
1178
  const namespaceBindingsByFile = new Map();
1179
+ const externalRuntimeBindingsByFile = new Map();
1078
1180
  for (const e of importGraph.edges) {
1181
+ const fromRel = relByAbs.get(path.resolve(e.from));
1182
+ if (fromRel && e.importKind === "runtime" && e.category === "bare-external") {
1183
+ let bindings = externalRuntimeBindingsByFile.get(fromRel);
1184
+ if (!bindings)
1185
+ externalRuntimeBindingsByFile.set(fromRel, (bindings = new Set()));
1186
+ for (const binding of e.bindings)
1187
+ bindings.add(binding.local);
1188
+ }
1079
1189
  if (!e.resolved || e.external || !e.target)
1080
1190
  continue;
1081
- const fromRel = relByAbs.get(path.resolve(e.from));
1082
1191
  const targetRel = relByAbs.get(path.resolve(e.target));
1083
1192
  // Skip targets outside the scanned set (ignored/generated files): no node to link.
1084
1193
  if (!fromRel || !targetRel || fromRel === targetRel)
@@ -1264,6 +1373,7 @@ export function analyzeRepo(root, opts = {}) {
1264
1373
  const imports = importBindingsByFile.get(rel);
1265
1374
  const typeImports = typeBindingsByFile.get(rel);
1266
1375
  const namespaces = namespaceBindingsByFile.get(rel);
1376
+ const externalRuntimeBindings = externalRuntimeBindingsByFile.get(rel);
1267
1377
  for (const c of calls) {
1268
1378
  if (!localSyms.has(c.caller))
1269
1379
  continue; // caller must be an emitted symbol
@@ -1323,6 +1433,9 @@ export function analyzeRepo(root, opts = {}) {
1323
1433
  queueMay(callerId, cands[0], rel, 0.65, `Imported-binding member match: the qualifier is imported from the module defining the only *.method symbol.`);
1324
1434
  }
1325
1435
  }
1436
+ if (externalRuntimeBindings?.has(c.qualifier)) {
1437
+ recordImportedStaticCallee(callerId, `${c.qualifier}.${c.callee}`);
1438
+ }
1326
1439
  // A non-imported (local/param) qualifier is NOT anchored — no edge.
1327
1440
  if (!ns && !qImport)
1328
1441
  recordExternalCallee(callerId, `${c.qualifier}.${c.callee}`); // round two: retain by name
@@ -1413,6 +1526,9 @@ export function analyzeRepo(root, opts = {}) {
1413
1526
  let javaProofAttempted = 0;
1414
1527
  let pythonProofConfirmedPairs = 0;
1415
1528
  let pythonProofAttempted = 0;
1529
+ let pythonDependencyEdges = 0;
1530
+ let pythonEndpointReachablePromoted = 0;
1531
+ let pythonStructuralContainersExcluded = 0;
1416
1532
  if (nonTsStructureByFile.size > 0) {
1417
1533
  const nodeByExternalId = new Map(nodes.map((n) => [n.external_id, n]));
1418
1534
  const codeFilesByLanguage = new Map();
@@ -1558,12 +1674,12 @@ export function analyzeRepo(root, opts = {}) {
1558
1674
  }
1559
1675
  }));
1560
1676
  };
1561
- const addImportBinding = (fromRel, local, target, imported, kind) => {
1677
+ const addImportBinding = (fromRel, local, target, imported, kind, module) => {
1562
1678
  let map = importBindingsByFile.get(fromRel);
1563
1679
  if (!map)
1564
1680
  importBindingsByFile.set(fromRel, (map = new Map()));
1565
1681
  if (!map.has(local))
1566
- map.set(local, { ...target, imported, kind });
1682
+ map.set(local, { ...target, imported, kind, module });
1567
1683
  };
1568
1684
  const uniquePythonModule = (mod) => {
1569
1685
  const set = pythonModuleToFiles.get(mod);
@@ -1618,6 +1734,39 @@ export function analyzeRepo(root, opts = {}) {
1618
1734
  return null;
1619
1735
  return symbolKind(targetRel, methodName) === "method" ? targetRel : null;
1620
1736
  };
1737
+ const pythonQualifiedMemberId = (targetRel, className, methodName) => {
1738
+ if (!targetRel)
1739
+ return null;
1740
+ const member = `${className}.${methodName}`;
1741
+ return symbolsByFile.get(targetRel)?.has(member) && symbolKind(targetRel, member) === "method"
1742
+ ? `sym:${targetRel}#${member}`
1743
+ : null;
1744
+ };
1745
+ const shouldRetainPythonImportedSink = (binding, callee) => {
1746
+ if (!/^(?:delete|destroy|purge|drop|erase|wipe|truncate|terminate|revoke|remove|replace|overwrite)/i.test(callee))
1747
+ return false;
1748
+ const topModule = (binding.module ?? "").replace(/^\.+/, "").split(".")[0]?.toLowerCase();
1749
+ if (new Set(["os", "pathlib", "shutil", "tempfile"]).has(topModule ?? ""))
1750
+ return false;
1751
+ const moduleName = (binding.module ?? "").toLowerCase();
1752
+ const owner = binding.imported ?? "";
1753
+ return /(?:^|[._])(db|database|model|orm|prisma|repo|repository|record|redis|mongo|sql|store|storage|table)(?:$|[._])/.test(moduleName)
1754
+ || /(?:Model|Repository|Repo|Store|Client|Table|Database|DB)$/.test(owner);
1755
+ };
1756
+ const shouldRetainPythonReceiverFieldSink = (qualifier, callee) => {
1757
+ if (!/^(?:delete|destroy|purge|drop|erase|wipe|truncate|terminate|revoke|remove|replace|overwrite)/i.test(callee))
1758
+ return false;
1759
+ const tokens = qualifier
1760
+ .replace(/\([^)]*\)/g, "")
1761
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
1762
+ .toLowerCase()
1763
+ .split(/[^a-z0-9]+/)
1764
+ .filter(Boolean);
1765
+ return tokens.some((token) => new Set([
1766
+ "db", "database", "model", "orm", "prisma", "repo", "repository", "record", "redis",
1767
+ "mongo", "sql", "session", "store", "storage", "table"
1768
+ ]).has(token));
1769
+ };
1621
1770
  const guardedCsharpMemberTarget = (fromRel, className, methodName) => {
1622
1771
  const ownNamespace = nonTsStructureByFile.get(fromRel)?.structure.moduleName;
1623
1772
  const namespaces = new Set([...(ownNamespace ? [ownNamespace] : []), ...(csharpUsingNamespacesByFile.get(fromRel) ?? [])]);
@@ -1821,7 +1970,7 @@ export function analyzeRepo(root, opts = {}) {
1821
1970
  return null;
1822
1971
  if (!classTargetRel || symbolKind(classTargetRel, rootQualifier) !== "class")
1823
1972
  return null;
1824
- return eligiblePythonSymbol(classTargetRel, callee);
1973
+ return eligiblePythonSymbol(classTargetRel, `${rootQualifier}.${callee}`);
1825
1974
  };
1826
1975
  for (const [rel, { language, structure }] of nonTsStructureByFile) {
1827
1976
  for (const i of structure.imports) {
@@ -1850,11 +1999,19 @@ export function analyzeRepo(root, opts = {}) {
1850
1999
  }
1851
2000
  else if (language === "c" || language === "cpp")
1852
2001
  targetRel = resolveRelativeModule(rel, i.module, [".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx"]);
1853
- if (!targetRel && !targetDir)
2002
+ if (!targetRel && !targetDir) {
2003
+ // Python external runtime imports do not produce graph edges, but the
2004
+ // binding still anchors `Model.delete()`/`client.replace()` as an
2005
+ // imported external call. TYPE_CHECKING imports were removed by the
2006
+ // AST extractor, so they cannot become sink provenance here.
2007
+ if (language === "python") {
2008
+ addImportBinding(rel, i.local, {}, i.imported, i.kind, i.module);
2009
+ }
1854
2010
  continue;
2011
+ }
1855
2012
  if (targetRel)
1856
2013
  emitImport(rel, targetRel);
1857
- addImportBinding(rel, i.local, { ...(targetRel ? { targetRel } : {}), ...(targetDir ? { targetDir } : {}) }, i.imported, i.kind);
2014
+ addImportBinding(rel, i.local, { ...(targetRel ? { targetRel } : {}), ...(targetDir ? { targetDir } : {}) }, i.imported, i.kind, i.module);
1858
2015
  }
1859
2016
  }
1860
2017
  // Go package-level method index (dir → "Recv.M" → file) for receiver calls that
@@ -1897,8 +2054,10 @@ export function analyzeRepo(root, opts = {}) {
1897
2054
  emitCall(callerId, `sym:${rel}#${c.callee}`, rel);
1898
2055
  continue;
1899
2056
  }
1900
- if (language === "python" && localSyms.has(c.callee))
2057
+ if (language === "python" && localSyms.has(c.callee)) {
2058
+ emitCall(callerId, `sym:${rel}#${c.callee}`, rel);
1901
2059
  continue;
2060
+ }
1902
2061
  const b = imports?.get(c.callee);
1903
2062
  if (language === "php" && b?.targetRel && b.imported && symbolKind(b.targetRel, b.imported) !== "function")
1904
2063
  continue;
@@ -1907,11 +2066,38 @@ export function analyzeRepo(root, opts = {}) {
1907
2066
  }
1908
2067
  }
1909
2068
  else if (c.qualifier) {
1910
- const rootQualifier = c.qualifier.split(".")[0];
1911
- if (shadowed.has(rootQualifier))
2069
+ const rootQualifier = language === "python" ? pythonQualifierRoot(c.qualifier) : c.qualifier.split(".")[0];
2070
+ if (shadowed.has(rootQualifier) && !(language === "python" && (rootQualifier === "self" || rootQualifier === "cls")))
1912
2071
  continue;
1913
2072
  const b = imports?.get(rootQualifier);
1914
- if (language === "go" && c.receiverVar && c.receiverType && c.qualifier === c.receiverVar) {
2073
+ if (language === "python") {
2074
+ const callerNode = nodeByExternalId.get(callerId);
2075
+ const callerOwner = typeof callerNode?.properties.member_of === "string" ? callerNode.properties.member_of : undefined;
2076
+ const selfMember = (rootQualifier === "self" || rootQualifier === "cls") && callerOwner
2077
+ ? pythonQualifiedMemberId(rel, callerOwner, c.callee)
2078
+ : null;
2079
+ const localClassMember = pythonQualifiedMemberId(rel, rootQualifier.replace(/\(.*$/, ""), c.callee);
2080
+ const importedClassMember = b?.kind === "named" && b.imported
2081
+ ? pythonQualifiedMemberId(b.targetRel, b.imported, c.callee)
2082
+ : null;
2083
+ if (selfMember || localClassMember || importedClassMember) {
2084
+ emitCall(callerId, selfMember ?? localClassMember ?? importedClassMember, rel);
2085
+ }
2086
+ else if (b?.kind === "module" && b.targetRel && symbolsByFile.get(b.targetRel)?.has(c.callee)) {
2087
+ emitCall(callerId, `sym:${b.targetRel}#${c.callee}`, rel);
2088
+ }
2089
+ else if (b && !b.targetRel && shouldRetainPythonImportedSink(b, c.callee)) {
2090
+ const qualifier = b.kind === "named" ? c.qualifier.replace(/\([^)]*\)/g, "") : c.qualifier;
2091
+ recordImportedStaticCallee(callerId, `${qualifier}.${c.callee}`);
2092
+ }
2093
+ else if (!b && c.qualifier.includes(".") && shouldRetainPythonReceiverFieldSink(c.qualifier, c.callee)) {
2094
+ // Attribute-chain receiver (`self.repo.delete`, `store.client.replace`):
2095
+ // retain a name fact only. Scoring applies its own persistence + verb
2096
+ // policy; no graph edge or evidence claim is created here.
2097
+ recordExternalCallee(callerId, `${c.qualifier}.${c.callee}`);
2098
+ }
2099
+ }
2100
+ else if (language === "go" && c.receiverVar && c.receiverType && c.qualifier === c.receiverVar) {
1915
2101
  // Go receiver-method call: inside `func (t *task) Run()`, `t.validate()` calls
1916
2102
  // `task.validate`. The receiver's type is DECLARED in the method signature, so
1917
2103
  // this is exact resolution: same file first, then the same package (methods of
@@ -1961,14 +2147,151 @@ export function analyzeRepo(root, opts = {}) {
1961
2147
  }
1962
2148
  }
1963
2149
  }
1964
- // Fix B: persist retained callee names on their caller symbols (names only).
1965
- if (externalCalleesByCaller.size > 0) {
1966
- const byId = new Map(nodes.map((n) => [n.external_id, n]));
1967
- for (const [callerId, names] of externalCalleesByCaller) {
1968
- const n = byId.get(callerId);
1969
- if (!n)
2150
+ const resolvePythonReferenceId = (fromRel, reference) => {
2151
+ const parts = reference.split(".").filter(Boolean);
2152
+ if (parts.length === 0)
2153
+ return null;
2154
+ const local = parts[0];
2155
+ if (parts.length === 1 && symbolsByFile.get(fromRel)?.has(local))
2156
+ return `sym:${fromRel}#${local}`;
2157
+ const localMember = parts.length > 1 ? parts.join(".") : "";
2158
+ if (localMember && symbolsByFile.get(fromRel)?.has(localMember))
2159
+ return `sym:${fromRel}#${localMember}`;
2160
+ const binding = importBindingsByFile.get(fromRel)?.get(local);
2161
+ if (!binding?.targetRel || isNonProductFile(binding.targetRel))
2162
+ return null;
2163
+ const targetName = binding.kind === "module"
2164
+ ? parts.slice(1).join(".")
2165
+ : [binding.imported, ...parts.slice(1)].filter(Boolean).join(".");
2166
+ return targetName && symbolsByFile.get(binding.targetRel)?.has(targetName)
2167
+ ? `sym:${binding.targetRel}#${targetName}`
2168
+ : null;
2169
+ };
2170
+ const seenPythonDependencyEdges = new Set();
2171
+ for (const [rel, { language, structure }] of nonTsStructureByFile) {
2172
+ if (language !== "python")
2173
+ continue;
2174
+ for (const dependency of structure.pythonDependencies ?? []) {
2175
+ const handlerId = `sym:${rel}#${dependency.handler}`;
2176
+ const dependencyId = resolvePythonReferenceId(rel, dependency.dependency);
2177
+ if (!codeSymbolIds.has(handlerId) || !dependencyId || !codeSymbolIds.has(dependencyId))
2178
+ continue;
2179
+ const callKey = `${handlerId}|${dependencyId}`;
2180
+ if (!seenCalls.has(callKey)) {
2181
+ seenCalls.add(callKey);
2182
+ edges.push(makeEdge({
2183
+ from_external_id: handlerId,
2184
+ to_external_id: dependencyId,
2185
+ relationship_type: "CALLS",
2186
+ evidence_strength: "framework-derived",
2187
+ review_status: "auto_detected",
2188
+ provenance: prov(rel),
2189
+ properties: { call_via: "fastapi_dependency", resolution: "tree_sitter_exact" }
2190
+ }));
2191
+ }
2192
+ for (const contract of behaviorContractsByFile.get(rel) ?? []) {
2193
+ if (contract.framework !== "fastapi" || contract.handler !== dependency.handler)
2194
+ continue;
2195
+ const key = `${contract.id}|${dependencyId}`;
2196
+ if (seenPythonDependencyEdges.has(key))
2197
+ continue;
2198
+ seenPythonDependencyEdges.add(key);
2199
+ pythonDependencyEdges++;
2200
+ edges.push(makeEdge({
2201
+ from_external_id: contract.id,
2202
+ to_external_id: dependencyId,
2203
+ relationship_type: "DEPENDS_ON",
2204
+ evidence_strength: "framework-derived",
2205
+ review_status: "auto_detected",
2206
+ provenance: prov(rel),
2207
+ properties: { framework: "fastapi", dependency: dependency.dependency }
2208
+ }));
2209
+ }
2210
+ }
2211
+ }
2212
+ // Python's first-pass denominator is intentionally conservative. Promote only
2213
+ // concrete production methods/functions reached from a framework handler by
2214
+ // exact hard/framework CALLS edges. This repairs denominator visibility without
2215
+ // admitting every class or utility into the score population.
2216
+ const pythonNodeIds = new Set(nodes
2217
+ .filter((n) => n.kind === "CodeSymbol" && typeof n.properties.file === "string" && n.properties.file.endsWith(".py"))
2218
+ .map((n) => n.external_id));
2219
+ const pythonAdjacency = new Map();
2220
+ for (const edge of edges) {
2221
+ if (edge.relationship_type !== "CALLS" || !pythonNodeIds.has(edge.from_external_id) || !pythonNodeIds.has(edge.to_external_id))
2222
+ continue;
2223
+ const list = pythonAdjacency.get(edge.from_external_id);
2224
+ const step = { target: edge.to_external_id, edgeKind: edge.relationship_type };
2225
+ if (list)
2226
+ list.push(step);
2227
+ else
2228
+ pythonAdjacency.set(edge.from_external_id, [step]);
2229
+ }
2230
+ const pythonEntrypoints = [...new Set(edges
2231
+ .filter((e) => e.relationship_type === "IMPLEMENTED_IN" && pythonNodeIds.has(e.to_external_id))
2232
+ .map((e) => e.to_external_id))].sort();
2233
+ const reachablePython = new Set(pythonEntrypoints);
2234
+ const pythonWitness = new Map(pythonEntrypoints.map((seed) => [seed, { seed, edgeKinds: [], hopCount: 0 }]));
2235
+ let frontier = [...pythonEntrypoints];
2236
+ for (let depth = 0; depth < 3 && frontier.length > 0; depth++) {
2237
+ const next = [];
2238
+ for (const current of frontier) {
2239
+ const currentWitness = pythonWitness.get(current);
2240
+ if (!currentWitness)
1970
2241
  continue;
1971
- n.properties = { ...(n.properties ?? {}), external_callees: [...names].sort() };
2242
+ for (const step of pythonAdjacency.get(current) ?? []) {
2243
+ if (reachablePython.has(step.target))
2244
+ continue;
2245
+ reachablePython.add(step.target);
2246
+ pythonWitness.set(step.target, {
2247
+ seed: currentWitness.seed,
2248
+ edgeKinds: [...currentWitness.edgeKinds, step.edgeKind],
2249
+ hopCount: currentWitness.hopCount + 1
2250
+ });
2251
+ next.push(step.target);
2252
+ }
2253
+ }
2254
+ frontier = next;
2255
+ }
2256
+ for (const id of reachablePython) {
2257
+ const node = nodeByExternalId.get(id);
2258
+ if (!node || node.properties.symbol_kind === "class")
2259
+ continue;
2260
+ const witness = pythonWitness.get(id);
2261
+ node.properties.endpoint_reachable = true;
2262
+ if (witness) {
2263
+ node.properties.denominator_witness = {
2264
+ seed: witness.seed,
2265
+ edge_kinds: witness.edgeKinds,
2266
+ hop_count: witness.hopCount
2267
+ };
2268
+ }
2269
+ if (node.denominator_eligible === true)
2270
+ continue;
2271
+ const callableName = (node.title ?? "").split(".").pop() ?? node.title ?? "";
2272
+ if (callableName.startsWith("_") || node.properties.denominator_reason_code !== "not_entry_point_adjacent")
2273
+ continue;
2274
+ node.denominator_eligible = true;
2275
+ node.denominator_reason = "Transitively reachable from a Python framework entrypoint through exact static calls.";
2276
+ node.properties.denominator_promotion = "python_entrypoint_reachable";
2277
+ pythonEndpointReachablePromoted++;
2278
+ }
2279
+ const pythonOwnersWithMethods = new Set(nodes
2280
+ .filter((n) => n.kind === "CodeSymbol" &&
2281
+ n.properties.symbol_kind === "method" &&
2282
+ n.denominator_eligible === true &&
2283
+ typeof n.properties.file === "string" &&
2284
+ n.properties.file.endsWith(".py") &&
2285
+ typeof n.properties.member_of === "string")
2286
+ .map((n) => `${n.properties.file}#${n.properties.member_of}`));
2287
+ for (const node of nodes) {
2288
+ if (node.kind === "CodeSymbol" &&
2289
+ node.properties.symbol_kind === "class" &&
2290
+ node.denominator_eligible === true &&
2291
+ typeof node.properties.file === "string" &&
2292
+ pythonOwnersWithMethods.has(`${node.properties.file}#${node.title ?? ""}`)) {
2293
+ node.properties.ranking_exclusion_reason = "python_structural_container";
2294
+ pythonStructuralContainersExcluded++;
1972
2295
  }
1973
2296
  }
1974
2297
  const seenGoProof = new Set();
@@ -2075,6 +2398,25 @@ export function analyzeRepo(root, opts = {}) {
2075
2398
  }
2076
2399
  }
2077
2400
  }
2401
+ // Persist retained callees for every language mix. This must live outside the
2402
+ // non-TS conditional: a TypeScript-only repository still needs its unresolved
2403
+ // external and import-anchored static calls available to risk scoring.
2404
+ if (externalCalleesByCaller.size > 0 || importedStaticCalleesByCaller.size > 0) {
2405
+ const byId = new Map(nodes.map((n) => [n.external_id, n]));
2406
+ const callerIds = new Set([...externalCalleesByCaller.keys(), ...importedStaticCalleesByCaller.keys()]);
2407
+ for (const callerId of callerIds) {
2408
+ const n = byId.get(callerId);
2409
+ if (!n)
2410
+ continue;
2411
+ const external = externalCalleesByCaller.get(callerId);
2412
+ const importedStatic = importedStaticCalleesByCaller.get(callerId);
2413
+ n.properties = {
2414
+ ...(n.properties ?? {}),
2415
+ ...(external?.size ? { external_callees: [...external].sort() } : {}),
2416
+ ...(importedStatic?.size ? { imported_static_callees: [...importedStatic].sort() } : {})
2417
+ };
2418
+ }
2419
+ }
2078
2420
  // ---- Static assertion candidates (Phase 4 / Gate 2): TypeChecker-derived hard edges ----
2079
2421
  // Upgrade resolver-derived test->source candidates to HARD TESTED_BY/COVERS
2080
2422
  // ONLY where the 5-conjunct confirmer proves a runtime, asserted use of a real
@@ -2157,7 +2499,7 @@ export function analyzeRepo(root, opts = {}) {
2157
2499
  involved.add(c.implAbs);
2158
2500
  }
2159
2501
  if (candidates.length === 0) {
2160
- confirmedCoverage = { confirmed_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
2502
+ confirmedCoverage = { confirmed_pairs: 0, associated_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
2161
2503
  }
2162
2504
  else
2163
2505
  try {
@@ -2174,7 +2516,7 @@ export function analyzeRepo(root, opts = {}) {
2174
2516
  })
2175
2517
  : { candidates, riskSymbols: 0, involvedFiles: involved.size };
2176
2518
  if (involved.size > confirmBudget && scoped.candidates.length === 0) {
2177
- confirmedCoverage = { confirmed_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: involved.size };
2519
+ confirmedCoverage = { confirmed_pairs: 0, associated_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: involved.size };
2178
2520
  warnings.push(`Static confirmation skipped: ${involved.size} files exceed the confirmer budget (${confirmBudget}), and no high-risk subset fit. Use \`--base <ref>\` for PR-scoped confirmation, or raise ORANGEPRO_MAX_CONFIRM_FILES.`);
2179
2521
  }
2180
2522
  else {
@@ -2195,8 +2537,28 @@ export function analyzeRepo(root, opts = {}) {
2195
2537
  lastVerified: proofVerifiedAt
2196
2538
  }));
2197
2539
  }
2540
+ let associatedPairs = 0;
2541
+ for (const c of result.associations) {
2542
+ const edgeKey = `test:${c.testRel}|${c.symId}`;
2543
+ if (seenEdge.has(edgeKey))
2544
+ continue;
2545
+ seenEdge.add(edgeKey);
2546
+ associatedPairs++;
2547
+ edges.push(...makeProofEdges({
2548
+ testRel: c.testRel,
2549
+ symId: c.symId,
2550
+ provenance: prov(c.testRel, hashString(`${c.symId}:runtime-invocation`)),
2551
+ lastVerified: proofVerifiedAt,
2552
+ properties: {
2553
+ static_association: "runtime_invocation",
2554
+ assertion_observed: false,
2555
+ association_reason: c.reason
2556
+ }
2557
+ }));
2558
+ }
2198
2559
  confirmedCoverage = {
2199
2560
  confirmed_pairs: confirmedPairs,
2561
+ associated_pairs: associatedPairs,
2200
2562
  attempted: result.attempted,
2201
2563
  capped_downgrades: result.capped_downgrades,
2202
2564
  skipped_files_budget: involved.size > confirmBudget ? involved.size : 0,
@@ -2222,7 +2584,7 @@ export function analyzeRepo(root, opts = {}) {
2222
2584
  // or a TS compiler-API edge must NEVER crash analyze. Degrade to
2223
2585
  // candidate-only coverage with a disclosure.
2224
2586
  const msg = err instanceof Error ? err.message : String(err);
2225
- confirmedCoverage = { confirmed_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
2587
+ confirmedCoverage = { confirmed_pairs: 0, associated_pairs: 0, attempted: 0, capped_downgrades: 0, skipped_files_budget: 0 };
2226
2588
  warnings.push(`Static confirmation skipped: the structural confirmer could not run on this project (${msg.slice(0, 160)}). Coverage is reported from candidate links only.`);
2227
2589
  }
2228
2590
  }
@@ -2230,6 +2592,7 @@ export function analyzeRepo(root, opts = {}) {
2230
2592
  if (nonTsHardProofAttempted > 0) {
2231
2593
  confirmedCoverage = {
2232
2594
  confirmed_pairs: (confirmedCoverage?.confirmed_pairs ?? 0) + goProofConfirmedPairs + javaProofConfirmedPairs + pythonProofConfirmedPairs,
2595
+ associated_pairs: confirmedCoverage?.associated_pairs ?? 0,
2233
2596
  attempted: (confirmedCoverage?.attempted ?? 0) + nonTsHardProofAttempted,
2234
2597
  capped_downgrades: confirmedCoverage?.capped_downgrades ?? 0,
2235
2598
  skipped_files_budget: confirmedCoverage?.skipped_files_budget ?? 0
@@ -2412,6 +2775,7 @@ export function analyzeRepo(root, opts = {}) {
2412
2775
  files_scanned: filesProcessed,
2413
2776
  files_cap_hit: filesCapHit,
2414
2777
  max_files: maxFiles,
2778
+ max_symbols: maxSymbols,
2415
2779
  ...(budgetStopped
2416
2780
  ? { not_analyzed_due_to_budget: { files_not_analyzed: filesNotAnalyzed, elapsed_ms: elapsedMs, budget_ms: budgetMs } }
2417
2781
  : {}),
@@ -2432,6 +2796,15 @@ export function analyzeRepo(root, opts = {}) {
2432
2796
  }
2433
2797
  }
2434
2798
  : {}),
2799
+ ...(pythonDependencyEdges || pythonEndpointReachablePromoted || pythonStructuralContainersExcluded
2800
+ ? {
2801
+ python_structural_recalibration: {
2802
+ dependency_edges: pythonDependencyEdges,
2803
+ endpoint_reachable_promoted: pythonEndpointReachablePromoted,
2804
+ structural_containers_rank_excluded: pythonStructuralContainersExcluded
2805
+ }
2806
+ }
2807
+ : {}),
2435
2808
  flows,
2436
2809
  structural_clusters: structuralClusters,
2437
2810
  ...(tsStatus.loaded.length || tsStatus.failed.length