@orangepro/orangepro-mcp 0.2.43 → 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.
- package/dist/local/analyze/analyzer.js +331 -17
- package/dist/local/analyze/treeSitter/engine.js +1157 -13
- package/dist/local/autoProve.js +377 -97
- package/dist/local/operations.js +65 -16
- package/dist/local/proofDoctor.js +27 -5
- package/dist/local/provenance.js +2 -2
- package/dist/local/score/risk.js +284 -104
- package/dist/local/score/riskConfig.js +17 -1
- package/dist/local/viz/behaviorReportData.js +54 -18
- package/dist/local/viz/behaviorReportHtml.js +12 -1
- 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,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
|
-
...(
|
|
983
|
+
...(pythonRankingExclusion
|
|
983
984
|
? {
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,10 +1990,143 @@ 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
|
};
|
|
1938
|
-
const
|
|
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
|
+
};
|
|
2009
|
+
const resolvePythonClassTarget = (testRel, structure, classLocal) => {
|
|
2010
|
+
const binding = pythonProofImportBinding(testRel, structure, classLocal);
|
|
2011
|
+
if (!binding)
|
|
2012
|
+
return null;
|
|
2013
|
+
if (binding.kind !== "named" || !binding.targetRel || !binding.imported || isNonProductFile(binding.targetRel))
|
|
2014
|
+
return null;
|
|
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;
|
|
2059
|
+
}
|
|
2060
|
+
if (!info || info.bases.length === 0)
|
|
2061
|
+
return 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;
|
|
2090
|
+
};
|
|
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) => {
|
|
1939
2098
|
const conv = conventionSibling(testRel, "python", codeFileSet);
|
|
1940
2099
|
const conventionTargetRel = conv?.relPath && !isNonProductFile(conv.relPath) ? conv.relPath : null;
|
|
1941
2100
|
const hasWildcardImport = structure.imports.some((i) => i.imported === "*");
|
|
2101
|
+
if (instanceClass) {
|
|
2102
|
+
if (!qualifier || shadowed.has(instanceClass) || hasWildcardImport)
|
|
2103
|
+
return 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;
|
|
2129
|
+
}
|
|
1942
2130
|
if (!qualifier) {
|
|
1943
2131
|
if (shadowed.has(callee))
|
|
1944
2132
|
return null;
|
|
@@ -2009,11 +2197,71 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2009
2197
|
}
|
|
2010
2198
|
continue;
|
|
2011
2199
|
}
|
|
2012
|
-
if (targetRel)
|
|
2200
|
+
if (targetRel) {
|
|
2013
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
|
+
}
|
|
2014
2210
|
addImportBinding(rel, i.local, { ...(targetRel ? { targetRel } : {}), ...(targetDir ? { targetDir } : {}) }, i.imported, i.kind, i.module);
|
|
2015
2211
|
}
|
|
2016
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
|
+
}
|
|
2017
2265
|
// Go package-level method index (dir → "Recv.M" → file) for receiver calls that
|
|
2018
2266
|
// land in a sibling file of the same package.
|
|
2019
2267
|
const goPkgMethods = new Map();
|
|
@@ -2090,7 +2338,7 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2090
2338
|
const qualifier = b.kind === "named" ? c.qualifier.replace(/\([^)]*\)/g, "") : c.qualifier;
|
|
2091
2339
|
recordImportedStaticCallee(callerId, `${qualifier}.${c.callee}`);
|
|
2092
2340
|
}
|
|
2093
|
-
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)) {
|
|
2094
2342
|
// Attribute-chain receiver (`self.repo.delete`, `store.client.replace`):
|
|
2095
2343
|
// retain a name fact only. Scoring applies its own persistence + verb
|
|
2096
2344
|
// policy; no graph edge or evidence claim is created here.
|
|
@@ -2167,6 +2415,36 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2167
2415
|
? `sym:${binding.targetRel}#${targetName}`
|
|
2168
2416
|
: null;
|
|
2169
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)));
|
|
2170
2448
|
const seenPythonDependencyEdges = new Set();
|
|
2171
2449
|
for (const [rel, { language, structure }] of nonTsStructureByFile) {
|
|
2172
2450
|
if (language !== "python")
|
|
@@ -2279,7 +2557,6 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2279
2557
|
const pythonOwnersWithMethods = new Set(nodes
|
|
2280
2558
|
.filter((n) => n.kind === "CodeSymbol" &&
|
|
2281
2559
|
n.properties.symbol_kind === "method" &&
|
|
2282
|
-
n.denominator_eligible === true &&
|
|
2283
2560
|
typeof n.properties.file === "string" &&
|
|
2284
2561
|
n.properties.file.endsWith(".py") &&
|
|
2285
2562
|
typeof n.properties.member_of === "string")
|
|
@@ -2287,10 +2564,10 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2287
2564
|
for (const node of nodes) {
|
|
2288
2565
|
if (node.kind === "CodeSymbol" &&
|
|
2289
2566
|
node.properties.symbol_kind === "class" &&
|
|
2290
|
-
node.denominator_eligible === true &&
|
|
2291
2567
|
typeof node.properties.file === "string" &&
|
|
2292
2568
|
pythonOwnersWithMethods.has(`${node.properties.file}#${node.title ?? ""}`)) {
|
|
2293
|
-
node.properties.
|
|
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.";
|
|
2294
2571
|
pythonStructuralContainersExcluded++;
|
|
2295
2572
|
}
|
|
2296
2573
|
}
|
|
@@ -2377,9 +2654,33 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2377
2654
|
const testExternalId = `test:${testRel}`;
|
|
2378
2655
|
for (const proof of structure.pythonProofCalls ?? []) {
|
|
2379
2656
|
pythonProofAttempted++;
|
|
2380
|
-
const symId = resolvePythonProofTarget(testRel, structure, proof.qualifier, proof.callee, new Set(proof.shadowed));
|
|
2657
|
+
const symId = resolvePythonProofTarget(testRel, structure, proof.qualifier, proof.callee, new Set(proof.shadowed), proof.instanceClass, proof.associationRule);
|
|
2381
2658
|
if (!symId)
|
|
2382
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;
|
|
2383
2684
|
const edgeKey = `${testExternalId}|${symId}`;
|
|
2384
2685
|
if (seenPythonProof.has(edgeKey))
|
|
2385
2686
|
continue;
|
|
@@ -2388,12 +2689,25 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2388
2689
|
const pythonEdgeProps = /^(?:Test[A-Za-z0-9_]*::)?test_[A-Za-z0-9_]+$/.test(proof.testName)
|
|
2389
2690
|
? { test_name: proof.testName }
|
|
2390
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;
|
|
2391
2703
|
edges.push(...makeProofEdges({
|
|
2392
2704
|
testRel,
|
|
2393
2705
|
symId,
|
|
2394
|
-
provenance:
|
|
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}`)),
|
|
2395
2709
|
lastVerified: proofVerifiedAt,
|
|
2396
|
-
...(pythonEdgeProps ? { properties: pythonEdgeProps } : {})
|
|
2710
|
+
...(pythonEdgeProps || pythonAssociationProps ? { properties: { ...pythonEdgeProps, ...pythonAssociationProps } } : {})
|
|
2397
2711
|
}));
|
|
2398
2712
|
}
|
|
2399
2713
|
}
|