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