@orangepro/orangepro-mcp 0.2.44 → 0.2.45

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.
@@ -952,6 +952,7 @@ export function analyzeRepo(root, opts = {}) {
952
952
  const behaviorSurfaceExcluded = callableBehaviorCandidate && surfaceExclusionReason !== null && !cliDenominatorOverride;
953
953
  const notEntryPointAdjacent = callableBehaviorCandidate && !eligible && !behaviorSurfaceExcluded;
954
954
  const rankingExclusion = builtInRankExclusion(file.relPath);
955
+ const pythonRankingExclusion = treeStructure?.pythonRankingExclusions?.find((candidate) => candidate.symbol === sym.name);
955
956
  if (isBoilerplate)
956
957
  excludedBoilerplate++;
957
958
  nodes.push(makeNode({
@@ -979,12 +980,18 @@ export function analyzeRepo(root, opts = {}) {
979
980
  : {}),
980
981
  ...(behaviorSurfaceExcluded ? { denominator_reason_code: "infra_behavior_surface" } : {}),
981
982
  ...(notEntryPointAdjacent ? { denominator_reason_code: "not_entry_point_adjacent" } : {}),
982
- ...(rankingExclusion
983
+ ...(pythonRankingExclusion
983
984
  ? {
984
- ranking_exclusion_reason_code: rankingExclusion.code,
985
- ranking_exclusion_reason: rankingExclusion.reason
985
+ ranking_exclusion_code: pythonRankingExclusion.code === "python_trivial_thread_local_initializer" ? "trivial_constructor" : "enum_declaration",
986
+ ranking_exclusion_reason_code: pythonRankingExclusion.code,
987
+ ranking_exclusion_reason: pythonRankingExclusion.reason
986
988
  }
987
- : {})
989
+ : rankingExclusion
990
+ ? {
991
+ ranking_exclusion_reason_code: rankingExclusion.code,
992
+ ranking_exclusion_reason: rankingExclusion.reason
993
+ }
994
+ : {})
988
995
  },
989
996
  evidence_strength: "hard",
990
997
  review_status: "auto_detected",
@@ -1168,6 +1175,7 @@ export function analyzeRepo(root, opts = {}) {
1168
1175
  // edge, while the SAME module pair reached later by a runtime import must
1169
1176
  // still produce the test->source linkage.
1170
1177
  const seenImports = new Set();
1178
+ const tsImportFacts = new Map();
1171
1179
  const seenTestLinks = new Set();
1172
1180
  // Per-file import bindings (local name → {imported, target file+abs}).
1173
1181
  // Runtime bindings resolve direct calls; type bindings resolve injected-field
@@ -1221,14 +1229,29 @@ export function analyzeRepo(root, opts = {}) {
1221
1229
  const pairKey = `${fromRel}|${targetRel}`;
1222
1230
  if (!seenImports.has(pairKey)) {
1223
1231
  seenImports.add(pairKey);
1224
- edges.push(makeEdge({
1232
+ const importEdge = makeEdge({
1225
1233
  from_external_id: fromRel,
1226
1234
  to_external_id: targetRel,
1227
1235
  relationship_type: "IMPORTS",
1228
1236
  evidence_strength: "hard",
1229
1237
  review_status: "auto_detected",
1230
1238
  provenance: prov(fromRel)
1231
- }));
1239
+ });
1240
+ edges.push(importEdge);
1241
+ tsImportFacts.set(pairKey, importEdge);
1242
+ }
1243
+ if (e.importKind === "runtime") {
1244
+ const importEdge = tsImportFacts.get(pairKey);
1245
+ for (const binding of e.bindings) {
1246
+ if (binding.imported === "*" || binding.imported === "default")
1247
+ continue;
1248
+ const targetId = `sym:${targetRel}#${binding.imported}`;
1249
+ if (!importEdge || !codeSymbolIds.has(targetId) || isNonProductFile(targetRel))
1250
+ continue;
1251
+ const named = (importEdge.properties?.symbol_imports ?? []);
1252
+ if (!named.includes(targetId))
1253
+ importEdge.properties = { ...importEdge.properties, symbol_imports: [...named, targetId] };
1254
+ }
1232
1255
  }
1233
1256
  if (e.fromRole === "test" && e.importKind === "runtime" && e.targetRole !== "test") {
1234
1257
  importResolvedTests.add(fromRel);
@@ -1637,6 +1660,7 @@ export function analyzeRepo(root, opts = {}) {
1637
1660
  }
1638
1661
  const importBindingsByFile = new Map();
1639
1662
  const seenImports = new Set(edges.filter((e) => e.relationship_type === "IMPORTS").map((e) => `${e.from_external_id}|${e.to_external_id}`));
1663
+ const nonTsImportFacts = new Map(edges.filter((e) => e.relationship_type === "IMPORTS").map((e) => [`${e.from_external_id}|${e.to_external_id}`, e]));
1640
1664
  const seenCalls = new Set(edges.filter((e) => e.relationship_type === "CALLS").map((e) => `${e.from_external_id}|${e.to_external_id}`));
1641
1665
  const emitImport = (fromRel, targetRel) => {
1642
1666
  if (fromRel === targetRel || !codeFileSet.has(targetRel) || isNonProductFile(targetRel))
@@ -1645,14 +1669,16 @@ export function analyzeRepo(root, opts = {}) {
1645
1669
  if (seenImports.has(key))
1646
1670
  return;
1647
1671
  seenImports.add(key);
1648
- edges.push(makeEdge({
1672
+ const importEdge = makeEdge({
1649
1673
  from_external_id: fromRel,
1650
1674
  to_external_id: targetRel,
1651
1675
  relationship_type: "IMPORTS",
1652
1676
  evidence_strength: "hard",
1653
1677
  review_status: "auto_detected",
1654
1678
  provenance: prov(fromRel)
1655
- }));
1679
+ });
1680
+ edges.push(importEdge);
1681
+ nonTsImportFacts.set(key, importEdge);
1656
1682
  };
1657
1683
  const emitCall = (callerId, calleeId, fromRel) => {
1658
1684
  if (callerId === calleeId || !codeSymbolIds.has(callerId) || !codeSymbolIds.has(calleeId))
@@ -1704,6 +1730,13 @@ export function analyzeRepo(root, opts = {}) {
1704
1730
  return null;
1705
1731
  };
1706
1732
  const resolvePythonImport = (fromRel, mod) => (mod.startsWith(".") ? resolveRelativePython(fromRel, mod) : uniquePythonModule(mod));
1733
+ const pythonModulePresentInRepo = (mod) => {
1734
+ if (!mod || mod.startsWith("."))
1735
+ return true;
1736
+ const modulePath = mod.replace(/\./g, "/");
1737
+ const prefixes = [modulePath, `src/${modulePath}`];
1738
+ return [...codeFileSet].some((file) => prefixes.some((prefix) => file === `${prefix}.py` || file.startsWith(`${prefix}/`)));
1739
+ };
1707
1740
  const resolveRelativeModule = (fromRel, mod, exts) => {
1708
1741
  const raw = mod.replace(/^\.\//, "");
1709
1742
  const base = path.posix.normalize(path.posix.join(dirOf(fromRel), raw));
@@ -1756,6 +1789,8 @@ export function analyzeRepo(root, opts = {}) {
1756
1789
  const shouldRetainPythonReceiverFieldSink = (qualifier, callee) => {
1757
1790
  if (!/^(?:delete|destroy|purge|drop|erase|wipe|truncate|terminate|revoke|remove|replace|overwrite)/i.test(callee))
1758
1791
  return false;
1792
+ if (/^(?:deleted|purged|dropped|destroyed|removed)[A-Za-z0-9_]*$/i.test(callee))
1793
+ return false;
1759
1794
  const tokens = qualifier
1760
1795
  .replace(/\([^)]*\)/g, "")
1761
1796
  .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
@@ -1767,6 +1802,26 @@ export function analyzeRepo(root, opts = {}) {
1767
1802
  "mongo", "sql", "session", "store", "storage", "table"
1768
1803
  ]).has(token));
1769
1804
  };
1805
+ const isPythonConstructorTerminal = (callee, chain) => {
1806
+ const destructive = /^(?:delete(?!d)|purge|drop|destroy)[A-Za-z0-9_]*$/i.test(callee)
1807
+ && !/^(?:deleted|purged|dropped|destroyed)[A-Za-z0-9_]*$/i.test(callee);
1808
+ const receiverFields = chain
1809
+ .slice(0, chain.lastIndexOf("."))
1810
+ .replace(/^[A-Z][A-Za-z0-9_]*\([^()]*\)\.?/, "")
1811
+ .split(".")
1812
+ .filter(Boolean);
1813
+ const persistentRemove = /^remove[A-Za-z0-9_]*$/i.test(callee)
1814
+ && receiverFields.some((attribute) => /(?:store|client|db|repo|repository|persistence|manager|queue|bucket|index|table|storage|dao)/i.test(attribute));
1815
+ return destructive || persistentRemove;
1816
+ };
1817
+ const resolvePythonConstructorBinding = (fromRel, local) => {
1818
+ if (symbolKind(fromRel, local) === "class")
1819
+ return `sym:${fromRel}#${local}`;
1820
+ const binding = importBindingsByFile.get(fromRel)?.get(local);
1821
+ if (!binding || binding.kind !== "named" || !binding.targetRel || !binding.imported || isNonProductFile(binding.targetRel))
1822
+ return null;
1823
+ return symbolKind(binding.targetRel, binding.imported) === "class" ? `sym:${binding.targetRel}#${binding.imported}` : null;
1824
+ };
1770
1825
  const guardedCsharpMemberTarget = (fromRel, className, methodName) => {
1771
1826
  const ownNamespace = nonTsStructureByFile.get(fromRel)?.structure.moduleName;
1772
1827
  const namespaces = new Set([...(ownNamespace ? [ownNamespace] : []), ...(csharpUsingNamespacesByFile.get(fromRel) ?? [])]);
@@ -1935,35 +1990,142 @@ export function analyzeRepo(root, opts = {}) {
1935
1990
  const targetRel = resolvePythonImport(testRel, binding.module) ?? undefined;
1936
1991
  return { targetRel, imported: binding.imported, kind: binding.kind };
1937
1992
  };
1993
+ const resolvePythonClassExport = (fromRel, targetRel, className) => {
1994
+ if (!targetRel || isNonProductFile(targetRel))
1995
+ return null;
1996
+ if (symbolKind(targetRel, className) === "class")
1997
+ return { targetRel, className };
1998
+ // One explicit, unique re-export hop only. The export must bind the same
1999
+ // class name (no aliases/wildcards), and its terminal must be in-repo.
2000
+ const exportStructure = nonTsStructureByFile.get(targetRel)?.structure;
2001
+ const exports = (exportStructure?.imports ?? []).filter((candidate) => candidate.kind === "named" && candidate.local === className && candidate.imported === className);
2002
+ if (exports.length !== 1)
2003
+ return null;
2004
+ const terminalRel = resolvePythonImport(targetRel, exports[0].module);
2005
+ return terminalRel && terminalRel !== targetRel && !isNonProductFile(terminalRel) && symbolKind(terminalRel, className) === "class"
2006
+ ? { targetRel: terminalRel, className }
2007
+ : null;
2008
+ };
1938
2009
  const resolvePythonClassTarget = (testRel, structure, classLocal) => {
1939
2010
  const binding = pythonProofImportBinding(testRel, structure, classLocal);
1940
2011
  if (!binding)
1941
2012
  return null;
1942
2013
  if (binding.kind !== "named" || !binding.targetRel || !binding.imported || isNonProductFile(binding.targetRel))
1943
2014
  return null;
1944
- if (symbolKind(binding.targetRel, binding.imported) === "class") {
1945
- return { targetRel: binding.targetRel, className: binding.imported };
2015
+ // An aliased import is exact only when its local name does not impersonate
2016
+ // another class declared by the imported module. `Other as Calculator`
2017
+ // otherwise makes constructor syntax look like the real Calculator class.
2018
+ if (classLocal !== binding.imported && symbolKind(binding.targetRel, classLocal) === "class")
2019
+ return null;
2020
+ return resolvePythonClassExport(testRel, binding.targetRel, binding.imported);
2021
+ };
2022
+ /** Resolve a structural class reference such as `Imported`, `mod.Imported`. */
2023
+ const resolvePythonClassReference = (fromRel, structure, classRef) => {
2024
+ const parts = classRef.split(".");
2025
+ if (parts.length === 1)
2026
+ return resolvePythonClassTarget(fromRel, structure, classRef);
2027
+ if (parts.length !== 2)
2028
+ return null;
2029
+ const binding = pythonProofImportBinding(fromRel, structure, parts[0]);
2030
+ if (!binding || binding.kind !== "module" || !binding.targetRel)
2031
+ return null;
2032
+ return resolvePythonClassExport(fromRel, binding.targetRel, parts[1]);
2033
+ };
2034
+ /** Exact in-repo base class resolution for a production class declaration. */
2035
+ const resolvePythonBaseClass = (fromRel, baseRef) => {
2036
+ const source = nonTsStructureByFile.get(fromRel)?.structure;
2037
+ if (!source)
2038
+ return null;
2039
+ if (!baseRef.includes(".") && symbolKind(fromRel, baseRef) === "class")
2040
+ return { targetRel: fromRel, className: baseRef };
2041
+ return resolvePythonClassReference(fromRel, source, baseRef);
2042
+ };
2043
+ const pythonClassInfo = (targetRel, className) => nonTsStructureByFile.get(targetRel)?.structure.pythonClasses?.find((info) => info.name === className);
2044
+ /**
2045
+ * Resolve an instance or class/static method through only explicitly declared
2046
+ * in-repo base classes. A declaration on the concrete class wins MRO lookup;
2047
+ * when no declaration exists there, multiple base hits are ambiguity, not a
2048
+ * reason to pick one.
2049
+ */
2050
+ const resolvePythonMroMethod = (targetRel, className, methodName, classStaticOnly, visited = new Set()) => {
2051
+ const key = `${targetRel}#${className}`;
2052
+ if (visited.has(key))
2053
+ return null;
2054
+ visited.add(key);
2055
+ const own = pythonQualifiedMemberId(targetRel, className, methodName);
2056
+ const info = pythonClassInfo(targetRel, className);
2057
+ if (own) {
2058
+ return !classStaticOnly || info?.classStaticMethods.includes(methodName) ? own : null;
1946
2059
  }
1947
- // One explicit, unique package re-export hop (`pkg.__init__: from .impl import Cls`).
1948
- // Ambiguity, wildcard exports, cycles, or a second hop fail closed.
1949
- const exportStructure = nonTsStructureByFile.get(binding.targetRel)?.structure;
1950
- const exports = (exportStructure?.imports ?? []).filter((candidate) => candidate.kind === "named" && candidate.local === binding.imported && candidate.imported === binding.imported);
1951
- if (exports.length !== 1)
2060
+ if (!info || info.bases.length === 0)
1952
2061
  return null;
1953
- const targetRel = resolvePythonImport(binding.targetRel, exports[0].module);
1954
- return targetRel && targetRel !== binding.targetRel && !isNonProductFile(targetRel) && symbolKind(targetRel, binding.imported) === "class"
1955
- ? { targetRel, className: binding.imported }
1956
- : null;
2062
+ const baseHits = info.bases
2063
+ .map((base) => resolvePythonBaseClass(targetRel, base))
2064
+ .filter((base) => Boolean(base))
2065
+ .map((base) => resolvePythonMroMethod(base.targetRel, base.className, methodName, classStaticOnly, new Set(visited)))
2066
+ .filter((id) => Boolean(id));
2067
+ return [...new Set(baseHits)].length === 1 ? baseHits[0] : null;
2068
+ };
2069
+ const PYTHON_UNIQUE_METHOD_STOPLIST = new Set(["get", "set", "run", "call", "update", "close", "start", "stop", "process", "handle", "execute"]);
2070
+ const uniqueEligiblePythonMethod = (methodName) => {
2071
+ if (PYTHON_UNIQUE_METHOD_STOPLIST.has(methodName) || /^__.*__$/.test(methodName))
2072
+ return null;
2073
+ const matches = [];
2074
+ for (const node of nodes) {
2075
+ if (node.kind !== "CodeSymbol" || node.properties.symbol_kind !== "method" || typeof node.properties.file !== "string")
2076
+ continue;
2077
+ const targetRel = node.properties.file;
2078
+ if (!targetRel.endsWith(".py") || isNonProductFile(targetRel) || node.title?.split(".").pop() !== methodName)
2079
+ continue;
2080
+ const name = node.title ?? "";
2081
+ const owner = typeof node.properties.member_of === "string" ? node.properties.member_of : null;
2082
+ // Uniqueness is global to in-repo production class methods; denominator
2083
+ // eligibility decides whether the sole candidate may become a proof edge,
2084
+ // never whether a conflicting owner is ignored.
2085
+ if (owner)
2086
+ matches.push({ symId: node.external_id, targetRel, owner, eligible: Boolean(eligiblePythonSymbol(targetRel, name)) });
2087
+ }
2088
+ const match = matches.length === 1 ? matches[0] : null;
2089
+ return match?.eligible ? match : null;
1957
2090
  };
1958
- const resolvePythonProofTarget = (testRel, structure, qualifier, callee, shadowed, instanceClass) => {
2091
+ const importsUniquePythonMethodOwner = (testRel, structure, candidate) => structure.imports.some((binding) => {
2092
+ const targetRel = resolvePythonImport(testRel, binding.module);
2093
+ if (targetRel !== candidate.targetRel)
2094
+ return false;
2095
+ return binding.kind === "module" || (binding.kind === "named" && binding.imported === candidate.owner);
2096
+ });
2097
+ const resolvePythonProofTarget = (testRel, structure, qualifier, callee, shadowed, instanceClass, associationRule) => {
1959
2098
  const conv = conventionSibling(testRel, "python", codeFileSet);
1960
2099
  const conventionTargetRel = conv?.relPath && !isNonProductFile(conv.relPath) ? conv.relPath : null;
1961
2100
  const hasWildcardImport = structure.imports.some((i) => i.imported === "*");
1962
2101
  if (instanceClass) {
1963
2102
  if (!qualifier || shadowed.has(instanceClass) || hasWildcardImport)
1964
2103
  return null;
1965
- const resolved = resolvePythonClassTarget(testRel, structure, instanceClass);
1966
- return resolved ? pythonQualifiedMemberId(resolved.targetRel, resolved.className, callee) : null;
2104
+ const resolved = resolvePythonClassReference(testRel, structure, instanceClass);
2105
+ if (!resolved)
2106
+ return null; // constructor/class origin must be exact; uniqueness alone never suffices.
2107
+ const classStaticOnly = associationRule === "class_static_method";
2108
+ const owned = resolvePythonMroMethod(resolved.targetRel, resolved.className, callee, classStaticOnly);
2109
+ if (owned)
2110
+ return owned;
2111
+ // `Class.method()` has a distinct, strict contract: if the exact class/MRO
2112
+ // declares that method but not as @classmethod/@staticmethod, uniqueness
2113
+ // elsewhere must not silently turn an invalid instance call into coverage.
2114
+ if (classStaticOnly && resolvePythonMroMethod(resolved.targetRel, resolved.className, callee, false))
2115
+ return null;
2116
+ // An exact receiver origin owns this attempt. Missing methods, an MRO
2117
+ // miss, or a class/static eligibility failure must never fall through to
2118
+ // a same-named method on another class.
2119
+ return null;
2120
+ }
2121
+ if (associationRule === "unique_method_name_import") {
2122
+ // The extractor admits only one lower-case local receiver here; local
2123
+ // assignment necessarily appears in `shadowed`, so do not apply the
2124
+ // free-call shadow guard to the qualifier itself.
2125
+ if (!qualifier || !/^[a-z_][A-Za-z0-9_]*$/.test(qualifier) || shadowed.has(callee) || hasWildcardImport)
2126
+ return null;
2127
+ const candidate = uniqueEligiblePythonMethod(callee);
2128
+ return candidate && importsUniquePythonMethodOwner(testRel, structure, candidate) ? candidate.symId : null;
1967
2129
  }
1968
2130
  if (!qualifier) {
1969
2131
  if (shadowed.has(callee))
@@ -2035,11 +2197,71 @@ export function analyzeRepo(root, opts = {}) {
2035
2197
  }
2036
2198
  continue;
2037
2199
  }
2038
- if (targetRel)
2200
+ if (targetRel) {
2039
2201
  emitImport(rel, targetRel);
2202
+ if (language === "python" && i.kind === "named" && i.imported && i.imported !== "*" && symbolKind(targetRel, i.imported) !== undefined) {
2203
+ const importEdge = nonTsImportFacts.get(`${rel}|${targetRel}`);
2204
+ const targetId = `sym:${targetRel}#${i.imported}`;
2205
+ const named = (importEdge?.properties?.symbol_imports ?? []);
2206
+ if (importEdge && !named.includes(targetId))
2207
+ importEdge.properties = { ...importEdge.properties, symbol_imports: [...named, targetId] };
2208
+ }
2209
+ }
2040
2210
  addImportBinding(rel, i.local, { ...(targetRel ? { targetRel } : {}), ...(targetDir ? { targetDir } : {}) }, i.imported, i.kind, i.module);
2041
2211
  }
2042
2212
  }
2213
+ // Attribute references are structural facts only when the parser identified a
2214
+ // direct module binding, that binding resolves uniquely, and its name was not
2215
+ // shadowed. Calls are already represented by CALLS and must not count twice.
2216
+ for (const [rel, { language, structure }] of nonTsStructureByFile) {
2217
+ if (language !== "python")
2218
+ continue;
2219
+ for (const ref of structure.pythonModuleAttributes ?? []) {
2220
+ const binding = importBindingsByFile.get(rel)?.get(ref.local);
2221
+ if (binding?.kind !== "module" || !binding.targetRel || isNonProductFile(binding.targetRel))
2222
+ continue;
2223
+ const targetId = `sym:${binding.targetRel}#${ref.attribute}`;
2224
+ if (!codeSymbolIds.has(targetId))
2225
+ continue;
2226
+ const importEdge = nonTsImportFacts.get(`${rel}|${binding.targetRel}`);
2227
+ if (!importEdge)
2228
+ continue;
2229
+ const refs = { ...importEdge.properties?.symbol_references };
2230
+ refs[targetId] = (refs[targetId] ?? 0) + 1;
2231
+ importEdge.properties = { ...importEdge.properties, symbol_references: refs };
2232
+ }
2233
+ }
2234
+ // Ranking facts stay graph-only. Declaration/initializer shapes were proved by
2235
+ // tree-sitter; thin delegates are admitted here only when their exact import
2236
+ // binding has no in-repo target. The scorer never reads source to reconstruct it.
2237
+ for (const [rel, { language, structure }] of nonTsStructureByFile) {
2238
+ if (language !== "python")
2239
+ continue;
2240
+ for (const delegate of structure.pythonThinDelegates ?? []) {
2241
+ const binding = importBindingsByFile.get(rel)?.get(delegate.importLocal);
2242
+ const node = nodeByExternalId.get(`sym:${rel}#${delegate.symbol}`);
2243
+ if (!node || !binding || binding.targetRel || binding.targetDir || pythonModulePresentInRepo(binding.module ?? ""))
2244
+ continue;
2245
+ node.properties.ranking_exclusion_code = "thin_external_delegate";
2246
+ node.properties.ranking_exclusion_reason_code = "python_thin_external_delegate";
2247
+ node.properties.ranking_exclusion_reason = "Thin external delegate with one bare-parameter return call — retained in the graph but excluded from priority ranking.";
2248
+ }
2249
+ for (const call of structure.pythonConstructorChainCalls ?? []) {
2250
+ if (!isPythonConstructorTerminal(call.callee.split(".").pop() ?? "", call.callee))
2251
+ continue;
2252
+ const constructorSymbol = resolvePythonConstructorBinding(rel, call.constructorLocal);
2253
+ const node = nodeByExternalId.get(`sym:${rel}#${call.caller}`);
2254
+ if (!node || !constructorSymbol)
2255
+ continue;
2256
+ recordExternalCallee(node.external_id, call.callee);
2257
+ const current = Array.isArray(node.properties.constructor_chain_sinks)
2258
+ ? node.properties.constructor_chain_sinks.filter((value) => Boolean(value && typeof value === "object" && typeof value.callee === "string" && typeof value.constructor_symbol === "string"))
2259
+ : [];
2260
+ if (!current.some((value) => value.callee === call.callee && value.constructor_symbol === constructorSymbol) && current.length < 8) {
2261
+ node.properties.constructor_chain_sinks = [...current, { callee: call.callee, constructor_symbol: constructorSymbol }];
2262
+ }
2263
+ }
2264
+ }
2043
2265
  // Go package-level method index (dir → "Recv.M" → file) for receiver calls that
2044
2266
  // land in a sibling file of the same package.
2045
2267
  const goPkgMethods = new Map();
@@ -2116,7 +2338,7 @@ export function analyzeRepo(root, opts = {}) {
2116
2338
  const qualifier = b.kind === "named" ? c.qualifier.replace(/\([^)]*\)/g, "") : c.qualifier;
2117
2339
  recordImportedStaticCallee(callerId, `${qualifier}.${c.callee}`);
2118
2340
  }
2119
- else if (!b && c.qualifier.includes(".") && shouldRetainPythonReceiverFieldSink(c.qualifier, c.callee)) {
2341
+ else if (!b && !c.qualifier.includes("(") && c.qualifier.includes(".") && shouldRetainPythonReceiverFieldSink(c.qualifier, c.callee)) {
2120
2342
  // Attribute-chain receiver (`self.repo.delete`, `store.client.replace`):
2121
2343
  // retain a name fact only. Scoring applies its own persistence + verb
2122
2344
  // policy; no graph edge or evidence claim is created here.
@@ -2193,6 +2415,36 @@ export function analyzeRepo(root, opts = {}) {
2193
2415
  ? `sym:${binding.targetRel}#${targetName}`
2194
2416
  : null;
2195
2417
  };
2418
+ const resolvePythonMockTargetId = (testRel, structure, reference) => {
2419
+ const parts = reference.split(".").filter(Boolean);
2420
+ if (parts.length < 2)
2421
+ return null;
2422
+ // `patch.object(ImportedClass, "method")` / `ImportedClass.method = MagicMock()`.
2423
+ const binding = structure.imports.filter((entry) => entry.local === parts[0]);
2424
+ if (binding.length === 1) {
2425
+ const imported = binding[0];
2426
+ const targetRel = resolvePythonImport(testRel, imported.module);
2427
+ const targetName = imported.kind === "module"
2428
+ ? parts.slice(1).join(".")
2429
+ : [imported.imported, ...parts.slice(1)].filter(Boolean).join(".");
2430
+ if (targetRel && !isNonProductFile(targetRel) && symbolsByFile.get(targetRel)?.has(targetName)) {
2431
+ return `sym:${targetRel}#${targetName}`;
2432
+ }
2433
+ }
2434
+ // `patch("package.module.Class.method")`: find one exact in-repo module
2435
+ // prefix, then require the remaining class-method symbol to exist there.
2436
+ for (let i = 1; i < parts.length - 1; i++) {
2437
+ const targetRel = uniquePythonModule(parts.slice(0, i).join("."));
2438
+ const targetName = parts.slice(i).join(".");
2439
+ if (targetRel && !isNonProductFile(targetRel) && symbolsByFile.get(targetRel)?.has(targetName)) {
2440
+ return `sym:${targetRel}#${targetName}`;
2441
+ }
2442
+ }
2443
+ return null;
2444
+ };
2445
+ const mockedPythonTargetIds = (testRel, structure) => new Set((structure.pythonMockedMethods ?? [])
2446
+ .map((reference) => resolvePythonMockTargetId(testRel, structure, reference))
2447
+ .filter((id) => Boolean(id)));
2196
2448
  const seenPythonDependencyEdges = new Set();
2197
2449
  for (const [rel, { language, structure }] of nonTsStructureByFile) {
2198
2450
  if (language !== "python")
@@ -2305,7 +2557,6 @@ export function analyzeRepo(root, opts = {}) {
2305
2557
  const pythonOwnersWithMethods = new Set(nodes
2306
2558
  .filter((n) => n.kind === "CodeSymbol" &&
2307
2559
  n.properties.symbol_kind === "method" &&
2308
- n.denominator_eligible === true &&
2309
2560
  typeof n.properties.file === "string" &&
2310
2561
  n.properties.file.endsWith(".py") &&
2311
2562
  typeof n.properties.member_of === "string")
@@ -2313,10 +2564,10 @@ export function analyzeRepo(root, opts = {}) {
2313
2564
  for (const node of nodes) {
2314
2565
  if (node.kind === "CodeSymbol" &&
2315
2566
  node.properties.symbol_kind === "class" &&
2316
- node.denominator_eligible === true &&
2317
2567
  typeof node.properties.file === "string" &&
2318
2568
  pythonOwnersWithMethods.has(`${node.properties.file}#${node.title ?? ""}`)) {
2319
- node.properties.ranking_exclusion_reason = "python_structural_container";
2569
+ node.properties.ranking_exclusion_code = "python_class_with_methods";
2570
+ node.properties.ranking_exclusion_reason = "Python class has mapped method symbols — retained in graph and denominator, but methods own its priority ranking.";
2320
2571
  pythonStructuralContainersExcluded++;
2321
2572
  }
2322
2573
  }
@@ -2403,9 +2654,33 @@ export function analyzeRepo(root, opts = {}) {
2403
2654
  const testExternalId = `test:${testRel}`;
2404
2655
  for (const proof of structure.pythonProofCalls ?? []) {
2405
2656
  pythonProofAttempted++;
2406
- const symId = resolvePythonProofTarget(testRel, structure, proof.qualifier, proof.callee, new Set(proof.shadowed), proof.instanceClass);
2657
+ const symId = resolvePythonProofTarget(testRel, structure, proof.qualifier, proof.callee, new Set(proof.shadowed), proof.instanceClass, proof.associationRule);
2407
2658
  if (!symId)
2408
2659
  continue;
2660
+ // A sibling test's mock is irrelevant. Include only this test's body and
2661
+ // decorators, its recursively used fixtures, and module autouse fixtures.
2662
+ // Opaque fixture selection fails closed; the AST traversal resolves finite,
2663
+ // locally declared parametrized fixtures and their actual mocks before this gate.
2664
+ const unknownMockScope = structure.pythonUnknownMockScopeTests?.includes(proof.testName) ?? false;
2665
+ if (unknownMockScope)
2666
+ continue;
2667
+ const references = structure.pythonMocksByTest?.[proof.testName] ?? structure.pythonMockedMethods ?? [];
2668
+ const scopedStructure = { ...structure, pythonMockedMethods: references };
2669
+ const mockedTargets = mockedPythonTargetIds(testRel, scopedStructure);
2670
+ const mockedReferences = new Set(references);
2671
+ const unresolvedMockedMethodNames = new Set(references.filter((reference) => !resolvePythonMockTargetId(testRel, structure, reference))
2672
+ .map((reference) => reference.split(".").pop())
2673
+ .filter((name) => Boolean(name)));
2674
+ if (mockedTargets.has(symId))
2675
+ continue;
2676
+ // Keep exact unresolved local receiver and unique-method-name negatives.
2677
+ const localMockReference = proof.qualifier
2678
+ ? `${pythonQualifierRoot(proof.qualifier)}.${proof.callee}`
2679
+ : null;
2680
+ if (localMockReference && mockedReferences.has(localMockReference))
2681
+ continue;
2682
+ if (unresolvedMockedMethodNames.has(proof.callee) && uniqueEligiblePythonMethod(proof.callee)?.symId === symId)
2683
+ continue;
2409
2684
  const edgeKey = `${testExternalId}|${symId}`;
2410
2685
  if (seenPythonProof.has(edgeKey))
2411
2686
  continue;
@@ -2414,12 +2689,25 @@ export function analyzeRepo(root, opts = {}) {
2414
2689
  const pythonEdgeProps = /^(?:Test[A-Za-z0-9_]*::)?test_[A-Za-z0-9_]+$/.test(proof.testName)
2415
2690
  ? { test_name: proof.testName }
2416
2691
  : undefined;
2692
+ const pythonAssociationProps = proof.associationRule
2693
+ ? {
2694
+ static_association: {
2695
+ detector: proof.associationRule === "unique_method_name_import" ? "unique_method_name_import" : "python_static_association",
2696
+ rule: proof.associationRule,
2697
+ reason: proof.associationRule === "unique_method_name_import"
2698
+ ? "Unresolved receiver, one unique imported in-repo method owner, one direct invocation, and one direct pytest assertion; this is Association only, never dynamic Proven."
2699
+ : "Exact static receiver origin and direct invocation; this is Association only, never dynamic Proven."
2700
+ }
2701
+ }
2702
+ : undefined;
2417
2703
  edges.push(...makeProofEdges({
2418
2704
  testRel,
2419
2705
  symId,
2420
- provenance: prov(testRel, hashString(`${symId}:${proof.assertion}`)),
2706
+ provenance: proof.associationRule
2707
+ ? { ...prov(testRel, hashString(`${symId}:${proof.assertion}`)), detector: proof.associationRule === "unique_method_name_import" ? "unique_method_name_import" : "python_static_association" }
2708
+ : prov(testRel, hashString(`${symId}:${proof.assertion}`)),
2421
2709
  lastVerified: proofVerifiedAt,
2422
- ...(pythonEdgeProps ? { properties: pythonEdgeProps } : {})
2710
+ ...(pythonEdgeProps || pythonAssociationProps ? { properties: { ...pythonEdgeProps, ...pythonAssociationProps } } : {})
2423
2711
  }));
2424
2712
  }
2425
2713
  }