@orangepro/orangepro-mcp 0.2.42 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -842,10 +842,18 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
842
842
|
}
|
|
843
843
|
const content = readSafe(file.absPath);
|
|
844
844
|
const isGenerated = content ? isGeneratedCode(content) : false;
|
|
845
|
+
const treeStructure = content && isTreeSitterLanguage(language) && treeSitterReady(language)
|
|
846
|
+
? extractTreeSitterStructure(content, language, file.relPath)
|
|
847
|
+
: undefined;
|
|
845
848
|
if (isGenerated)
|
|
846
849
|
generatedCodeFiles.add(file.relPath);
|
|
847
|
-
if (content &&
|
|
848
|
-
|
|
850
|
+
if (content && !isGenerated && !isNonProductPath(file.relPath)) {
|
|
851
|
+
const behaviorContracts = language === "typescript" || language === "javascript"
|
|
852
|
+
? extractBehaviorContracts(content, file.relPath)
|
|
853
|
+
: language === "python"
|
|
854
|
+
? (treeStructure?.pythonBehaviorContracts ?? [])
|
|
855
|
+
: [];
|
|
856
|
+
for (const contract of behaviorContracts) {
|
|
849
857
|
if (behaviorSurfaceExclusionReason(file.relPath, contract.handler ?? contract.title))
|
|
850
858
|
continue;
|
|
851
859
|
behaviorContractsTotal++;
|
|
@@ -868,7 +876,9 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
868
876
|
file: contract.file,
|
|
869
877
|
source: contract.source,
|
|
870
878
|
...(contract.handler ? { handler: contract.handler } : {}),
|
|
871
|
-
...(contract.controller ? { controller: contract.controller } : {})
|
|
879
|
+
...(contract.controller ? { controller: contract.controller } : {}),
|
|
880
|
+
...(contract.route_prefix_status ? { route_prefix_status: contract.route_prefix_status } : {}),
|
|
881
|
+
...(contract.route_prefix ? { route_prefix: contract.route_prefix } : {})
|
|
872
882
|
},
|
|
873
883
|
evidence_strength: "hard",
|
|
874
884
|
review_status: "auto_detected",
|
|
@@ -924,7 +934,8 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
924
934
|
// Trivial accessors (Java getId/toString, Python __repr__) are real
|
|
925
935
|
// symbols but carry no testable behavior — kept in the graph, dropped
|
|
926
936
|
// from the denominator with disclosure so coverage is not under-claimed.
|
|
927
|
-
const
|
|
937
|
+
const boilerplateName = language === "python" ? (sym.name.split(".").pop() ?? sym.name) : sym.name;
|
|
938
|
+
const isBoilerplate = !isDts && !isInfra && !isGeneratedSymbol && callableKind && isBoilerplateSymbol(boilerplateName, language, sym.symbol_kind, sym.trivial_accessor);
|
|
928
939
|
const callableBehaviorCandidate = !isDts && !isInfra && !isGeneratedSymbol && !isBoilerplate && behaviorCallableKind;
|
|
929
940
|
const surfaceExclusionReason = callableBehaviorCandidate
|
|
930
941
|
? behaviorSurfaceExclusionReason(file.relPath, sym.name, sym.member_of)
|
|
@@ -1069,9 +1080,8 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1069
1080
|
if (generatedServices.length > 0)
|
|
1070
1081
|
medusaGeneratedServicesByFile.set(file.relPath, generatedServices);
|
|
1071
1082
|
}
|
|
1072
|
-
else if (
|
|
1073
|
-
|
|
1074
|
-
nonTsStructureByFile.set(file.relPath, { language, structure });
|
|
1083
|
+
else if (treeStructure && !isNonProductFile(file.relPath)) {
|
|
1084
|
+
nonTsStructureByFile.set(file.relPath, { language, structure: treeStructure });
|
|
1075
1085
|
}
|
|
1076
1086
|
}
|
|
1077
1087
|
}
|
|
@@ -1516,6 +1526,9 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1516
1526
|
let javaProofAttempted = 0;
|
|
1517
1527
|
let pythonProofConfirmedPairs = 0;
|
|
1518
1528
|
let pythonProofAttempted = 0;
|
|
1529
|
+
let pythonDependencyEdges = 0;
|
|
1530
|
+
let pythonEndpointReachablePromoted = 0;
|
|
1531
|
+
let pythonStructuralContainersExcluded = 0;
|
|
1519
1532
|
if (nonTsStructureByFile.size > 0) {
|
|
1520
1533
|
const nodeByExternalId = new Map(nodes.map((n) => [n.external_id, n]));
|
|
1521
1534
|
const codeFilesByLanguage = new Map();
|
|
@@ -1661,12 +1674,12 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1661
1674
|
}
|
|
1662
1675
|
}));
|
|
1663
1676
|
};
|
|
1664
|
-
const addImportBinding = (fromRel, local, target, imported, kind) => {
|
|
1677
|
+
const addImportBinding = (fromRel, local, target, imported, kind, module) => {
|
|
1665
1678
|
let map = importBindingsByFile.get(fromRel);
|
|
1666
1679
|
if (!map)
|
|
1667
1680
|
importBindingsByFile.set(fromRel, (map = new Map()));
|
|
1668
1681
|
if (!map.has(local))
|
|
1669
|
-
map.set(local, { ...target, imported, kind });
|
|
1682
|
+
map.set(local, { ...target, imported, kind, module });
|
|
1670
1683
|
};
|
|
1671
1684
|
const uniquePythonModule = (mod) => {
|
|
1672
1685
|
const set = pythonModuleToFiles.get(mod);
|
|
@@ -1721,6 +1734,39 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1721
1734
|
return null;
|
|
1722
1735
|
return symbolKind(targetRel, methodName) === "method" ? targetRel : null;
|
|
1723
1736
|
};
|
|
1737
|
+
const pythonQualifiedMemberId = (targetRel, className, methodName) => {
|
|
1738
|
+
if (!targetRel)
|
|
1739
|
+
return null;
|
|
1740
|
+
const member = `${className}.${methodName}`;
|
|
1741
|
+
return symbolsByFile.get(targetRel)?.has(member) && symbolKind(targetRel, member) === "method"
|
|
1742
|
+
? `sym:${targetRel}#${member}`
|
|
1743
|
+
: null;
|
|
1744
|
+
};
|
|
1745
|
+
const shouldRetainPythonImportedSink = (binding, callee) => {
|
|
1746
|
+
if (!/^(?:delete|destroy|purge|drop|erase|wipe|truncate|terminate|revoke|remove|replace|overwrite)/i.test(callee))
|
|
1747
|
+
return false;
|
|
1748
|
+
const topModule = (binding.module ?? "").replace(/^\.+/, "").split(".")[0]?.toLowerCase();
|
|
1749
|
+
if (new Set(["os", "pathlib", "shutil", "tempfile"]).has(topModule ?? ""))
|
|
1750
|
+
return false;
|
|
1751
|
+
const moduleName = (binding.module ?? "").toLowerCase();
|
|
1752
|
+
const owner = binding.imported ?? "";
|
|
1753
|
+
return /(?:^|[._])(db|database|model|orm|prisma|repo|repository|record|redis|mongo|sql|store|storage|table)(?:$|[._])/.test(moduleName)
|
|
1754
|
+
|| /(?:Model|Repository|Repo|Store|Client|Table|Database|DB)$/.test(owner);
|
|
1755
|
+
};
|
|
1756
|
+
const shouldRetainPythonReceiverFieldSink = (qualifier, callee) => {
|
|
1757
|
+
if (!/^(?:delete|destroy|purge|drop|erase|wipe|truncate|terminate|revoke|remove|replace|overwrite)/i.test(callee))
|
|
1758
|
+
return false;
|
|
1759
|
+
const tokens = qualifier
|
|
1760
|
+
.replace(/\([^)]*\)/g, "")
|
|
1761
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
1762
|
+
.toLowerCase()
|
|
1763
|
+
.split(/[^a-z0-9]+/)
|
|
1764
|
+
.filter(Boolean);
|
|
1765
|
+
return tokens.some((token) => new Set([
|
|
1766
|
+
"db", "database", "model", "orm", "prisma", "repo", "repository", "record", "redis",
|
|
1767
|
+
"mongo", "sql", "session", "store", "storage", "table"
|
|
1768
|
+
]).has(token));
|
|
1769
|
+
};
|
|
1724
1770
|
const guardedCsharpMemberTarget = (fromRel, className, methodName) => {
|
|
1725
1771
|
const ownNamespace = nonTsStructureByFile.get(fromRel)?.structure.moduleName;
|
|
1726
1772
|
const namespaces = new Set([...(ownNamespace ? [ownNamespace] : []), ...(csharpUsingNamespacesByFile.get(fromRel) ?? [])]);
|
|
@@ -1924,7 +1970,7 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1924
1970
|
return null;
|
|
1925
1971
|
if (!classTargetRel || symbolKind(classTargetRel, rootQualifier) !== "class")
|
|
1926
1972
|
return null;
|
|
1927
|
-
return eligiblePythonSymbol(classTargetRel, callee);
|
|
1973
|
+
return eligiblePythonSymbol(classTargetRel, `${rootQualifier}.${callee}`);
|
|
1928
1974
|
};
|
|
1929
1975
|
for (const [rel, { language, structure }] of nonTsStructureByFile) {
|
|
1930
1976
|
for (const i of structure.imports) {
|
|
@@ -1953,11 +1999,19 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1953
1999
|
}
|
|
1954
2000
|
else if (language === "c" || language === "cpp")
|
|
1955
2001
|
targetRel = resolveRelativeModule(rel, i.module, [".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx"]);
|
|
1956
|
-
if (!targetRel && !targetDir)
|
|
2002
|
+
if (!targetRel && !targetDir) {
|
|
2003
|
+
// Python external runtime imports do not produce graph edges, but the
|
|
2004
|
+
// binding still anchors `Model.delete()`/`client.replace()` as an
|
|
2005
|
+
// imported external call. TYPE_CHECKING imports were removed by the
|
|
2006
|
+
// AST extractor, so they cannot become sink provenance here.
|
|
2007
|
+
if (language === "python") {
|
|
2008
|
+
addImportBinding(rel, i.local, {}, i.imported, i.kind, i.module);
|
|
2009
|
+
}
|
|
1957
2010
|
continue;
|
|
2011
|
+
}
|
|
1958
2012
|
if (targetRel)
|
|
1959
2013
|
emitImport(rel, targetRel);
|
|
1960
|
-
addImportBinding(rel, i.local, { ...(targetRel ? { targetRel } : {}), ...(targetDir ? { targetDir } : {}) }, i.imported, i.kind);
|
|
2014
|
+
addImportBinding(rel, i.local, { ...(targetRel ? { targetRel } : {}), ...(targetDir ? { targetDir } : {}) }, i.imported, i.kind, i.module);
|
|
1961
2015
|
}
|
|
1962
2016
|
}
|
|
1963
2017
|
// Go package-level method index (dir → "Recv.M" → file) for receiver calls that
|
|
@@ -2000,8 +2054,10 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2000
2054
|
emitCall(callerId, `sym:${rel}#${c.callee}`, rel);
|
|
2001
2055
|
continue;
|
|
2002
2056
|
}
|
|
2003
|
-
if (language === "python" && localSyms.has(c.callee))
|
|
2057
|
+
if (language === "python" && localSyms.has(c.callee)) {
|
|
2058
|
+
emitCall(callerId, `sym:${rel}#${c.callee}`, rel);
|
|
2004
2059
|
continue;
|
|
2060
|
+
}
|
|
2005
2061
|
const b = imports?.get(c.callee);
|
|
2006
2062
|
if (language === "php" && b?.targetRel && b.imported && symbolKind(b.targetRel, b.imported) !== "function")
|
|
2007
2063
|
continue;
|
|
@@ -2010,11 +2066,38 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2010
2066
|
}
|
|
2011
2067
|
}
|
|
2012
2068
|
else if (c.qualifier) {
|
|
2013
|
-
const rootQualifier = c.qualifier.split(".")[0];
|
|
2014
|
-
if (shadowed.has(rootQualifier))
|
|
2069
|
+
const rootQualifier = language === "python" ? pythonQualifierRoot(c.qualifier) : c.qualifier.split(".")[0];
|
|
2070
|
+
if (shadowed.has(rootQualifier) && !(language === "python" && (rootQualifier === "self" || rootQualifier === "cls")))
|
|
2015
2071
|
continue;
|
|
2016
2072
|
const b = imports?.get(rootQualifier);
|
|
2017
|
-
if (language === "
|
|
2073
|
+
if (language === "python") {
|
|
2074
|
+
const callerNode = nodeByExternalId.get(callerId);
|
|
2075
|
+
const callerOwner = typeof callerNode?.properties.member_of === "string" ? callerNode.properties.member_of : undefined;
|
|
2076
|
+
const selfMember = (rootQualifier === "self" || rootQualifier === "cls") && callerOwner
|
|
2077
|
+
? pythonQualifiedMemberId(rel, callerOwner, c.callee)
|
|
2078
|
+
: null;
|
|
2079
|
+
const localClassMember = pythonQualifiedMemberId(rel, rootQualifier.replace(/\(.*$/, ""), c.callee);
|
|
2080
|
+
const importedClassMember = b?.kind === "named" && b.imported
|
|
2081
|
+
? pythonQualifiedMemberId(b.targetRel, b.imported, c.callee)
|
|
2082
|
+
: null;
|
|
2083
|
+
if (selfMember || localClassMember || importedClassMember) {
|
|
2084
|
+
emitCall(callerId, selfMember ?? localClassMember ?? importedClassMember, rel);
|
|
2085
|
+
}
|
|
2086
|
+
else if (b?.kind === "module" && b.targetRel && symbolsByFile.get(b.targetRel)?.has(c.callee)) {
|
|
2087
|
+
emitCall(callerId, `sym:${b.targetRel}#${c.callee}`, rel);
|
|
2088
|
+
}
|
|
2089
|
+
else if (b && !b.targetRel && shouldRetainPythonImportedSink(b, c.callee)) {
|
|
2090
|
+
const qualifier = b.kind === "named" ? c.qualifier.replace(/\([^)]*\)/g, "") : c.qualifier;
|
|
2091
|
+
recordImportedStaticCallee(callerId, `${qualifier}.${c.callee}`);
|
|
2092
|
+
}
|
|
2093
|
+
else if (!b && c.qualifier.includes(".") && shouldRetainPythonReceiverFieldSink(c.qualifier, c.callee)) {
|
|
2094
|
+
// Attribute-chain receiver (`self.repo.delete`, `store.client.replace`):
|
|
2095
|
+
// retain a name fact only. Scoring applies its own persistence + verb
|
|
2096
|
+
// policy; no graph edge or evidence claim is created here.
|
|
2097
|
+
recordExternalCallee(callerId, `${c.qualifier}.${c.callee}`);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
else if (language === "go" && c.receiverVar && c.receiverType && c.qualifier === c.receiverVar) {
|
|
2018
2101
|
// Go receiver-method call: inside `func (t *task) Run()`, `t.validate()` calls
|
|
2019
2102
|
// `task.validate`. The receiver's type is DECLARED in the method signature, so
|
|
2020
2103
|
// this is exact resolution: same file first, then the same package (methods of
|
|
@@ -2064,6 +2147,153 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2064
2147
|
}
|
|
2065
2148
|
}
|
|
2066
2149
|
}
|
|
2150
|
+
const resolvePythonReferenceId = (fromRel, reference) => {
|
|
2151
|
+
const parts = reference.split(".").filter(Boolean);
|
|
2152
|
+
if (parts.length === 0)
|
|
2153
|
+
return null;
|
|
2154
|
+
const local = parts[0];
|
|
2155
|
+
if (parts.length === 1 && symbolsByFile.get(fromRel)?.has(local))
|
|
2156
|
+
return `sym:${fromRel}#${local}`;
|
|
2157
|
+
const localMember = parts.length > 1 ? parts.join(".") : "";
|
|
2158
|
+
if (localMember && symbolsByFile.get(fromRel)?.has(localMember))
|
|
2159
|
+
return `sym:${fromRel}#${localMember}`;
|
|
2160
|
+
const binding = importBindingsByFile.get(fromRel)?.get(local);
|
|
2161
|
+
if (!binding?.targetRel || isNonProductFile(binding.targetRel))
|
|
2162
|
+
return null;
|
|
2163
|
+
const targetName = binding.kind === "module"
|
|
2164
|
+
? parts.slice(1).join(".")
|
|
2165
|
+
: [binding.imported, ...parts.slice(1)].filter(Boolean).join(".");
|
|
2166
|
+
return targetName && symbolsByFile.get(binding.targetRel)?.has(targetName)
|
|
2167
|
+
? `sym:${binding.targetRel}#${targetName}`
|
|
2168
|
+
: null;
|
|
2169
|
+
};
|
|
2170
|
+
const seenPythonDependencyEdges = new Set();
|
|
2171
|
+
for (const [rel, { language, structure }] of nonTsStructureByFile) {
|
|
2172
|
+
if (language !== "python")
|
|
2173
|
+
continue;
|
|
2174
|
+
for (const dependency of structure.pythonDependencies ?? []) {
|
|
2175
|
+
const handlerId = `sym:${rel}#${dependency.handler}`;
|
|
2176
|
+
const dependencyId = resolvePythonReferenceId(rel, dependency.dependency);
|
|
2177
|
+
if (!codeSymbolIds.has(handlerId) || !dependencyId || !codeSymbolIds.has(dependencyId))
|
|
2178
|
+
continue;
|
|
2179
|
+
const callKey = `${handlerId}|${dependencyId}`;
|
|
2180
|
+
if (!seenCalls.has(callKey)) {
|
|
2181
|
+
seenCalls.add(callKey);
|
|
2182
|
+
edges.push(makeEdge({
|
|
2183
|
+
from_external_id: handlerId,
|
|
2184
|
+
to_external_id: dependencyId,
|
|
2185
|
+
relationship_type: "CALLS",
|
|
2186
|
+
evidence_strength: "framework-derived",
|
|
2187
|
+
review_status: "auto_detected",
|
|
2188
|
+
provenance: prov(rel),
|
|
2189
|
+
properties: { call_via: "fastapi_dependency", resolution: "tree_sitter_exact" }
|
|
2190
|
+
}));
|
|
2191
|
+
}
|
|
2192
|
+
for (const contract of behaviorContractsByFile.get(rel) ?? []) {
|
|
2193
|
+
if (contract.framework !== "fastapi" || contract.handler !== dependency.handler)
|
|
2194
|
+
continue;
|
|
2195
|
+
const key = `${contract.id}|${dependencyId}`;
|
|
2196
|
+
if (seenPythonDependencyEdges.has(key))
|
|
2197
|
+
continue;
|
|
2198
|
+
seenPythonDependencyEdges.add(key);
|
|
2199
|
+
pythonDependencyEdges++;
|
|
2200
|
+
edges.push(makeEdge({
|
|
2201
|
+
from_external_id: contract.id,
|
|
2202
|
+
to_external_id: dependencyId,
|
|
2203
|
+
relationship_type: "DEPENDS_ON",
|
|
2204
|
+
evidence_strength: "framework-derived",
|
|
2205
|
+
review_status: "auto_detected",
|
|
2206
|
+
provenance: prov(rel),
|
|
2207
|
+
properties: { framework: "fastapi", dependency: dependency.dependency }
|
|
2208
|
+
}));
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
// Python's first-pass denominator is intentionally conservative. Promote only
|
|
2213
|
+
// concrete production methods/functions reached from a framework handler by
|
|
2214
|
+
// exact hard/framework CALLS edges. This repairs denominator visibility without
|
|
2215
|
+
// admitting every class or utility into the score population.
|
|
2216
|
+
const pythonNodeIds = new Set(nodes
|
|
2217
|
+
.filter((n) => n.kind === "CodeSymbol" && typeof n.properties.file === "string" && n.properties.file.endsWith(".py"))
|
|
2218
|
+
.map((n) => n.external_id));
|
|
2219
|
+
const pythonAdjacency = new Map();
|
|
2220
|
+
for (const edge of edges) {
|
|
2221
|
+
if (edge.relationship_type !== "CALLS" || !pythonNodeIds.has(edge.from_external_id) || !pythonNodeIds.has(edge.to_external_id))
|
|
2222
|
+
continue;
|
|
2223
|
+
const list = pythonAdjacency.get(edge.from_external_id);
|
|
2224
|
+
const step = { target: edge.to_external_id, edgeKind: edge.relationship_type };
|
|
2225
|
+
if (list)
|
|
2226
|
+
list.push(step);
|
|
2227
|
+
else
|
|
2228
|
+
pythonAdjacency.set(edge.from_external_id, [step]);
|
|
2229
|
+
}
|
|
2230
|
+
const pythonEntrypoints = [...new Set(edges
|
|
2231
|
+
.filter((e) => e.relationship_type === "IMPLEMENTED_IN" && pythonNodeIds.has(e.to_external_id))
|
|
2232
|
+
.map((e) => e.to_external_id))].sort();
|
|
2233
|
+
const reachablePython = new Set(pythonEntrypoints);
|
|
2234
|
+
const pythonWitness = new Map(pythonEntrypoints.map((seed) => [seed, { seed, edgeKinds: [], hopCount: 0 }]));
|
|
2235
|
+
let frontier = [...pythonEntrypoints];
|
|
2236
|
+
for (let depth = 0; depth < 3 && frontier.length > 0; depth++) {
|
|
2237
|
+
const next = [];
|
|
2238
|
+
for (const current of frontier) {
|
|
2239
|
+
const currentWitness = pythonWitness.get(current);
|
|
2240
|
+
if (!currentWitness)
|
|
2241
|
+
continue;
|
|
2242
|
+
for (const step of pythonAdjacency.get(current) ?? []) {
|
|
2243
|
+
if (reachablePython.has(step.target))
|
|
2244
|
+
continue;
|
|
2245
|
+
reachablePython.add(step.target);
|
|
2246
|
+
pythonWitness.set(step.target, {
|
|
2247
|
+
seed: currentWitness.seed,
|
|
2248
|
+
edgeKinds: [...currentWitness.edgeKinds, step.edgeKind],
|
|
2249
|
+
hopCount: currentWitness.hopCount + 1
|
|
2250
|
+
});
|
|
2251
|
+
next.push(step.target);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
frontier = next;
|
|
2255
|
+
}
|
|
2256
|
+
for (const id of reachablePython) {
|
|
2257
|
+
const node = nodeByExternalId.get(id);
|
|
2258
|
+
if (!node || node.properties.symbol_kind === "class")
|
|
2259
|
+
continue;
|
|
2260
|
+
const witness = pythonWitness.get(id);
|
|
2261
|
+
node.properties.endpoint_reachable = true;
|
|
2262
|
+
if (witness) {
|
|
2263
|
+
node.properties.denominator_witness = {
|
|
2264
|
+
seed: witness.seed,
|
|
2265
|
+
edge_kinds: witness.edgeKinds,
|
|
2266
|
+
hop_count: witness.hopCount
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
if (node.denominator_eligible === true)
|
|
2270
|
+
continue;
|
|
2271
|
+
const callableName = (node.title ?? "").split(".").pop() ?? node.title ?? "";
|
|
2272
|
+
if (callableName.startsWith("_") || node.properties.denominator_reason_code !== "not_entry_point_adjacent")
|
|
2273
|
+
continue;
|
|
2274
|
+
node.denominator_eligible = true;
|
|
2275
|
+
node.denominator_reason = "Transitively reachable from a Python framework entrypoint through exact static calls.";
|
|
2276
|
+
node.properties.denominator_promotion = "python_entrypoint_reachable";
|
|
2277
|
+
pythonEndpointReachablePromoted++;
|
|
2278
|
+
}
|
|
2279
|
+
const pythonOwnersWithMethods = new Set(nodes
|
|
2280
|
+
.filter((n) => n.kind === "CodeSymbol" &&
|
|
2281
|
+
n.properties.symbol_kind === "method" &&
|
|
2282
|
+
n.denominator_eligible === true &&
|
|
2283
|
+
typeof n.properties.file === "string" &&
|
|
2284
|
+
n.properties.file.endsWith(".py") &&
|
|
2285
|
+
typeof n.properties.member_of === "string")
|
|
2286
|
+
.map((n) => `${n.properties.file}#${n.properties.member_of}`));
|
|
2287
|
+
for (const node of nodes) {
|
|
2288
|
+
if (node.kind === "CodeSymbol" &&
|
|
2289
|
+
node.properties.symbol_kind === "class" &&
|
|
2290
|
+
node.denominator_eligible === true &&
|
|
2291
|
+
typeof node.properties.file === "string" &&
|
|
2292
|
+
pythonOwnersWithMethods.has(`${node.properties.file}#${node.title ?? ""}`)) {
|
|
2293
|
+
node.properties.ranking_exclusion_reason = "python_structural_container";
|
|
2294
|
+
pythonStructuralContainersExcluded++;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2067
2297
|
const seenGoProof = new Set();
|
|
2068
2298
|
const proofVerifiedAt = scanStartMs;
|
|
2069
2299
|
for (const [testRel, structure] of goTestStructureByFile) {
|
|
@@ -2566,6 +2796,15 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
2566
2796
|
}
|
|
2567
2797
|
}
|
|
2568
2798
|
: {}),
|
|
2799
|
+
...(pythonDependencyEdges || pythonEndpointReachablePromoted || pythonStructuralContainersExcluded
|
|
2800
|
+
? {
|
|
2801
|
+
python_structural_recalibration: {
|
|
2802
|
+
dependency_edges: pythonDependencyEdges,
|
|
2803
|
+
endpoint_reachable_promoted: pythonEndpointReachablePromoted,
|
|
2804
|
+
structural_containers_rank_excluded: pythonStructuralContainersExcluded
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
: {}),
|
|
2569
2808
|
flows,
|
|
2570
2809
|
structural_clusters: structuralClusters,
|
|
2571
2810
|
...(tsStatus.loaded.length || tsStatus.failed.length
|
|
@@ -8,6 +8,8 @@ import { createRequire } from "node:module";
|
|
|
8
8
|
import { Parser, Language } from "web-tree-sitter";
|
|
9
9
|
import { MAX_SYMBOLS_PER_FILE } from "../symbols.js";
|
|
10
10
|
import { TS_LANG_CONFIGS } from "./languages.js";
|
|
11
|
+
import { slugify } from "../../util/ids.js";
|
|
12
|
+
import { shortHash } from "../../util/hash.js";
|
|
11
13
|
const require = createRequire(import.meta.url);
|
|
12
14
|
let initPromise = null;
|
|
13
15
|
const loaded = new Map();
|
|
@@ -212,6 +214,39 @@ function symbolName(node, cfg) {
|
|
|
212
214
|
function nodeLines(node) {
|
|
213
215
|
return { start_line: node.startPosition.row + 1, end_line: node.endPosition.row + 1 };
|
|
214
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Nearest lexical Python class for a function. A function boundary reached first
|
|
219
|
+
* means this is a nested local function, not a class method.
|
|
220
|
+
*/
|
|
221
|
+
function pythonOwnerClass(node) {
|
|
222
|
+
const owners = [];
|
|
223
|
+
let parent = node.parent;
|
|
224
|
+
while (parent) {
|
|
225
|
+
if (parent.type === "function_definition" || parent.type === "lambda")
|
|
226
|
+
return undefined;
|
|
227
|
+
if (parent.type === "class_definition") {
|
|
228
|
+
const name = parent.childForFieldName("name")?.text;
|
|
229
|
+
if (name)
|
|
230
|
+
owners.unshift(name);
|
|
231
|
+
}
|
|
232
|
+
parent = parent.parent;
|
|
233
|
+
}
|
|
234
|
+
return owners.length > 0 ? owners.join(".") : undefined;
|
|
235
|
+
}
|
|
236
|
+
function hasPythonFunctionAncestor(node) {
|
|
237
|
+
let parent = node.parent;
|
|
238
|
+
while (parent) {
|
|
239
|
+
if (parent.type === "function_definition" || parent.type === "lambda")
|
|
240
|
+
return true;
|
|
241
|
+
parent = parent.parent;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
function pythonQualifiedFunctionName(node) {
|
|
246
|
+
const name = functionName(node, "python");
|
|
247
|
+
const owner = pythonOwnerClass(node);
|
|
248
|
+
return name && owner ? `${owner}.${name}` : name;
|
|
249
|
+
}
|
|
215
250
|
/**
|
|
216
251
|
* Extract class/function/method symbol metadata (names + a trivial-accessor flag —
|
|
217
252
|
* never source bodies) from a tree-sitter-supported language. Sync; returns [] when
|
|
@@ -249,16 +284,22 @@ export function extractTreeSitterSymbols(content, language) {
|
|
|
249
284
|
kind = "method";
|
|
250
285
|
else if (cfg.functionTypes.has(node.type))
|
|
251
286
|
kind = "function";
|
|
252
|
-
|
|
287
|
+
const pythonOwner = language === "python" && node.type === "function_definition" ? pythonOwnerClass(node) : undefined;
|
|
288
|
+
if (pythonOwner && kind === "function")
|
|
289
|
+
kind = "method";
|
|
290
|
+
const localPythonDeclaration = language === "python" && (node.type === "function_definition" || node.type === "class_definition") && hasPythonFunctionAncestor(node);
|
|
291
|
+
if (kind && !localPythonDeclaration && shouldEmitSymbol(node, language)) {
|
|
253
292
|
const name = symbolName(node, cfg);
|
|
254
293
|
if (name && !RESERVED_SYMBOL_NAMES.has(name)) {
|
|
255
294
|
// Go methods mint receiver-qualified names (`Recv.M`, mirroring TS/JS
|
|
256
295
|
// `Class.method` + member_of) so same-named methods on different receivers
|
|
257
296
|
// stay distinct symbols; an underivable receiver falls back to the bare name.
|
|
258
|
-
const recv = kind === "method" && language === "go" ? goReceiverBaseName(node) : undefined;
|
|
297
|
+
const recv = kind === "method" && language === "go" ? goReceiverBaseName(node) : language === "python" ? pythonOwner : undefined;
|
|
298
|
+
const pythonClassOwner = kind === "class" && language === "python" ? pythonOwnerClass(node) : undefined;
|
|
299
|
+
const emittedName = pythonClassOwner ? `${pythonClassOwner}.${name}` : name;
|
|
259
300
|
add(kind === "method"
|
|
260
301
|
? { name: recv ? `${recv}.${name}` : name, ...(recv ? { member_of: recv } : {}), symbol_kind: kind, trivial_accessor: isTrivialAccessorBody(node, language), ...nodeLines(node) }
|
|
261
|
-
: { name, symbol_kind: kind, ...nodeLines(node) });
|
|
302
|
+
: { name: emittedName, symbol_kind: kind, ...nodeLines(node) });
|
|
262
303
|
}
|
|
263
304
|
}
|
|
264
305
|
for (let i = 0; i < node.childCount; i++) {
|
|
@@ -464,6 +505,21 @@ function kotlinTopLevelSymbols(root) {
|
|
|
464
505
|
}
|
|
465
506
|
return [...out].sort();
|
|
466
507
|
}
|
|
508
|
+
function underPythonTypeCheckingGuard(node) {
|
|
509
|
+
let parent = node.parent;
|
|
510
|
+
while (parent) {
|
|
511
|
+
if (parent.type === "if_statement") {
|
|
512
|
+
const condition = parent.childForFieldName("condition");
|
|
513
|
+
const text = condition?.text.trim() ?? "";
|
|
514
|
+
if (text === "TYPE_CHECKING" || /^[A-Za-z_]\w*\.TYPE_CHECKING$/.test(text))
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
if (parent.type === "function_definition" || parent.type === "class_definition")
|
|
518
|
+
break;
|
|
519
|
+
parent = parent.parent;
|
|
520
|
+
}
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
467
523
|
function extractImports(root, language) {
|
|
468
524
|
const imports = [];
|
|
469
525
|
const add = (binding) => {
|
|
@@ -472,6 +528,9 @@ function extractImports(root, language) {
|
|
|
472
528
|
imports.push(binding);
|
|
473
529
|
};
|
|
474
530
|
const walk = (node) => {
|
|
531
|
+
if (language === "python" && (node.type === "import_statement" || node.type === "import_from_statement") && underPythonTypeCheckingGuard(node)) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
475
534
|
if (language === "java" && node.type === "import_declaration") {
|
|
476
535
|
const spec = namedChildren(node).find((n) => n.type.endsWith("identifier"))?.text;
|
|
477
536
|
if (spec && !spec.endsWith(".*"))
|
|
@@ -1430,6 +1489,360 @@ function extractJavaProofCalls(root, imports) {
|
|
|
1430
1489
|
}
|
|
1431
1490
|
return out;
|
|
1432
1491
|
}
|
|
1492
|
+
const FASTAPI_HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
1493
|
+
function pythonStaticString(node) {
|
|
1494
|
+
if (!node || node.type !== "string")
|
|
1495
|
+
return null;
|
|
1496
|
+
const contents = node.descendantsOfType("string_content").filter((n) => Boolean(n));
|
|
1497
|
+
if (contents.length !== 1)
|
|
1498
|
+
return null;
|
|
1499
|
+
return contents[0]?.text ?? null;
|
|
1500
|
+
}
|
|
1501
|
+
function pythonDottedName(node) {
|
|
1502
|
+
if (!node)
|
|
1503
|
+
return null;
|
|
1504
|
+
if (node.type === "identifier" || node.type === "dotted_name")
|
|
1505
|
+
return node.text;
|
|
1506
|
+
if (node.type !== "attribute")
|
|
1507
|
+
return null;
|
|
1508
|
+
const object = pythonDottedName(node.childForFieldName("object"));
|
|
1509
|
+
const attribute = node.childForFieldName("attribute")?.text;
|
|
1510
|
+
return object && attribute ? `${object}.${attribute}` : null;
|
|
1511
|
+
}
|
|
1512
|
+
function pythonCallArguments(call) {
|
|
1513
|
+
const args = call.childForFieldName("arguments");
|
|
1514
|
+
return args ? namedChildren(args).filter((n) => n.type !== "comment") : [];
|
|
1515
|
+
}
|
|
1516
|
+
function pythonKeywordArgument(call, name) {
|
|
1517
|
+
for (const arg of pythonCallArguments(call)) {
|
|
1518
|
+
if (arg.type !== "keyword_argument" || arg.childForFieldName("name")?.text !== name)
|
|
1519
|
+
continue;
|
|
1520
|
+
return arg.childForFieldName("value") ?? namedChildren(arg)[1] ?? null;
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
function pythonPositionalArguments(call) {
|
|
1525
|
+
return pythonCallArguments(call).filter((n) => n.type !== "keyword_argument" && n.type !== "list_splat" && n.type !== "dictionary_splat");
|
|
1526
|
+
}
|
|
1527
|
+
function pythonStaticHttpMethods(node, fallback = ["GET"]) {
|
|
1528
|
+
if (!node)
|
|
1529
|
+
return fallback;
|
|
1530
|
+
if (!new Set(["list", "tuple", "set"]).has(node.type))
|
|
1531
|
+
return null;
|
|
1532
|
+
const elements = namedChildren(node).filter((child) => child.type !== "comment");
|
|
1533
|
+
if (elements.length === 0)
|
|
1534
|
+
return null;
|
|
1535
|
+
const methods = elements.map((element) => pythonStaticString(element));
|
|
1536
|
+
if (methods.some((method) => method === null))
|
|
1537
|
+
return null;
|
|
1538
|
+
const normalized = methods.map((method) => method.toLowerCase());
|
|
1539
|
+
return normalized.every((method) => FASTAPI_HTTP_METHODS.has(method)) ? normalized.map((method) => method.toUpperCase()) : null;
|
|
1540
|
+
}
|
|
1541
|
+
function normalizePythonRoutePath(...parts) {
|
|
1542
|
+
const joined = parts.map((p) => p.trim().replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
|
|
1543
|
+
return joined ? `/${joined}` : "/";
|
|
1544
|
+
}
|
|
1545
|
+
function pythonImportedFromFastapi(imports, local, imported) {
|
|
1546
|
+
return imports.some((b) => b.local === local && b.module === "fastapi" && b.kind === "named" && b.imported === imported);
|
|
1547
|
+
}
|
|
1548
|
+
function pythonCallIsFastapiConstructor(call, imports, imported) {
|
|
1549
|
+
const parts = callParts(call, "python");
|
|
1550
|
+
if (!parts)
|
|
1551
|
+
return false;
|
|
1552
|
+
if (!parts.qualifier)
|
|
1553
|
+
return pythonImportedFromFastapi(imports, parts.callee, imported);
|
|
1554
|
+
if (parts.callee !== imported)
|
|
1555
|
+
return false;
|
|
1556
|
+
const root = parts.qualifier.split(".")[0] ?? "";
|
|
1557
|
+
return imports.some((b) => b.local === root && b.kind === "module" && b.module === "fastapi");
|
|
1558
|
+
}
|
|
1559
|
+
function pythonBindingTargetNames(target) {
|
|
1560
|
+
if (!target)
|
|
1561
|
+
return [];
|
|
1562
|
+
if (target.type === "identifier")
|
|
1563
|
+
return [target.text];
|
|
1564
|
+
if (!new Set(["pattern_list", "tuple_pattern", "list_pattern", "tuple", "list"]).has(target.type))
|
|
1565
|
+
return [];
|
|
1566
|
+
return namedChildren(target).flatMap((child) => pythonBindingTargetNames(child));
|
|
1567
|
+
}
|
|
1568
|
+
function pythonFastapiReceivers(root, imports) {
|
|
1569
|
+
const mutationCount = new Map();
|
|
1570
|
+
const candidates = new Map();
|
|
1571
|
+
const mutations = [];
|
|
1572
|
+
for (const type of ["assignment", "annotated_assignment", "augmented_assignment", "delete_statement"]) {
|
|
1573
|
+
mutations.push(...root.descendantsOfType(type).filter((node) => Boolean(node)));
|
|
1574
|
+
}
|
|
1575
|
+
mutations.sort((a, b) => a.startIndex - b.startIndex);
|
|
1576
|
+
for (const assignment of mutations) {
|
|
1577
|
+
if (!pythonDirectModuleExecution(assignment))
|
|
1578
|
+
continue;
|
|
1579
|
+
const left = assignment.childForFieldName("left");
|
|
1580
|
+
const targets = assignment.type === "delete_statement"
|
|
1581
|
+
? namedChildren(assignment).flatMap((child) => pythonBindingTargetNames(child))
|
|
1582
|
+
: pythonBindingTargetNames(left);
|
|
1583
|
+
for (const target of targets)
|
|
1584
|
+
mutationCount.set(target, (mutationCount.get(target) ?? 0) + 1);
|
|
1585
|
+
const right = assignment.childForFieldName("right");
|
|
1586
|
+
if (left?.type !== "identifier")
|
|
1587
|
+
continue;
|
|
1588
|
+
const kind = right?.type === "call" && pythonCallIsFastapiConstructor(right, imports, "APIRouter")
|
|
1589
|
+
? "router"
|
|
1590
|
+
: right?.type === "call" && pythonCallIsFastapiConstructor(right, imports, "FastAPI")
|
|
1591
|
+
? "app"
|
|
1592
|
+
: null;
|
|
1593
|
+
if (!kind || right?.type !== "call")
|
|
1594
|
+
continue;
|
|
1595
|
+
const prefixNode = kind === "router" ? pythonKeywordArgument(right, "prefix") : null;
|
|
1596
|
+
const staticPrefix = pythonStaticString(prefixNode);
|
|
1597
|
+
candidates.set(left.text, {
|
|
1598
|
+
kind,
|
|
1599
|
+
prefix: staticPrefix ?? "",
|
|
1600
|
+
prefixResolved: !prefixNode || staticPrefix !== null,
|
|
1601
|
+
boundAt: assignment.startIndex
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
const out = new Map();
|
|
1605
|
+
for (const [name, receiver] of candidates)
|
|
1606
|
+
if (mutationCount.get(name) === 1)
|
|
1607
|
+
out.set(name, receiver);
|
|
1608
|
+
return out;
|
|
1609
|
+
}
|
|
1610
|
+
const PYTHON_NONDETERMINISTIC_SCOPES = new Set([
|
|
1611
|
+
"function_definition", "class_definition", "if_statement", "for_statement", "while_statement",
|
|
1612
|
+
"try_statement", "with_statement", "match_statement", "lambda", "list_comprehension",
|
|
1613
|
+
"dictionary_comprehension", "set_comprehension", "generator_expression"
|
|
1614
|
+
]);
|
|
1615
|
+
function pythonDirectModuleExecution(node) {
|
|
1616
|
+
let parent = node.parent;
|
|
1617
|
+
while (parent) {
|
|
1618
|
+
if (PYTHON_NONDETERMINISTIC_SCOPES.has(parent.type))
|
|
1619
|
+
return false;
|
|
1620
|
+
if (parent.type === "module")
|
|
1621
|
+
return true;
|
|
1622
|
+
parent = parent.parent;
|
|
1623
|
+
}
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1626
|
+
function pythonRouterIncludePrefixes(root, receivers) {
|
|
1627
|
+
const out = new Map();
|
|
1628
|
+
for (const call of root.descendantsOfType("call")) {
|
|
1629
|
+
if (!call || !pythonDirectModuleExecution(call))
|
|
1630
|
+
continue;
|
|
1631
|
+
const parts = callParts(call, "python");
|
|
1632
|
+
const app = parts?.qualifier?.split(".")[0];
|
|
1633
|
+
if (!parts || parts.callee !== "include_router" || !app || receivers.get(app)?.kind !== "app")
|
|
1634
|
+
continue;
|
|
1635
|
+
const router = pythonDottedName(pythonPositionalArguments(call)[0] ?? null);
|
|
1636
|
+
if (!router || !receivers.has(router) || receivers.get(app).boundAt >= call.startIndex || receivers.get(router).boundAt >= call.startIndex)
|
|
1637
|
+
continue;
|
|
1638
|
+
const includePrefix = pythonStaticString(pythonKeywordArgument(call, "prefix"));
|
|
1639
|
+
if (includePrefix === null && pythonKeywordArgument(call, "prefix"))
|
|
1640
|
+
continue;
|
|
1641
|
+
let prefixes = out.get(router);
|
|
1642
|
+
if (!prefixes)
|
|
1643
|
+
out.set(router, (prefixes = new Set()));
|
|
1644
|
+
prefixes.add(includePrefix ?? "");
|
|
1645
|
+
}
|
|
1646
|
+
return out;
|
|
1647
|
+
}
|
|
1648
|
+
function makePythonBehaviorContract(input) {
|
|
1649
|
+
const identityInput = `${input.method}|${input.path}|${input.file}|${input.handler ?? ""}`;
|
|
1650
|
+
return {
|
|
1651
|
+
id: `endpoint:${slugify(`${input.method}-${input.path}-${input.file}-${input.handler ?? ""}`)}-${shortHash(identityInput)}`,
|
|
1652
|
+
title: `${input.method} ${input.path}`,
|
|
1653
|
+
kind: "http_endpoint",
|
|
1654
|
+
framework: "fastapi",
|
|
1655
|
+
method: input.method,
|
|
1656
|
+
path: input.path,
|
|
1657
|
+
file: input.file,
|
|
1658
|
+
handler: input.handler,
|
|
1659
|
+
route_prefix_status: input.prefixStatus,
|
|
1660
|
+
...(input.prefix ? { route_prefix: input.prefix } : {}),
|
|
1661
|
+
source: "framework"
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
function dedupePythonContracts(contracts) {
|
|
1665
|
+
const seen = new Set();
|
|
1666
|
+
return contracts.filter((contract) => {
|
|
1667
|
+
const key = `${contract.method}|${contract.path}|${contract.handler ?? ""}`;
|
|
1668
|
+
if (seen.has(key))
|
|
1669
|
+
return false;
|
|
1670
|
+
seen.add(key);
|
|
1671
|
+
return true;
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
function pythonDirectModuleHandlers(root) {
|
|
1675
|
+
const counts = new Map();
|
|
1676
|
+
for (const fn of root.descendantsOfType("function_definition")) {
|
|
1677
|
+
if (!fn || hasPythonFunctionAncestor(fn))
|
|
1678
|
+
continue;
|
|
1679
|
+
const declaration = fn.parent?.type === "decorated_definition" ? fn.parent : fn;
|
|
1680
|
+
if (declaration.parent?.type !== "module")
|
|
1681
|
+
continue;
|
|
1682
|
+
const name = pythonQualifiedFunctionName(fn);
|
|
1683
|
+
if (name)
|
|
1684
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
1685
|
+
}
|
|
1686
|
+
for (const type of ["assignment", "annotated_assignment", "augmented_assignment", "delete_statement"]) {
|
|
1687
|
+
for (const mutation of root.descendantsOfType(type)) {
|
|
1688
|
+
if (!mutation || !pythonDirectModuleExecution(mutation))
|
|
1689
|
+
continue;
|
|
1690
|
+
const left = mutation.childForFieldName("left");
|
|
1691
|
+
const targets = mutation.type === "delete_statement"
|
|
1692
|
+
? namedChildren(mutation).flatMap((child) => pythonBindingTargetNames(child))
|
|
1693
|
+
: pythonBindingTargetNames(left);
|
|
1694
|
+
for (const target of targets)
|
|
1695
|
+
if (counts.has(target))
|
|
1696
|
+
counts.set(target, counts.get(target) + 1);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return new Set([...counts].filter(([, count]) => count === 1).map(([name]) => name));
|
|
1700
|
+
}
|
|
1701
|
+
function extractPythonBehaviorContracts(root, imports, file) {
|
|
1702
|
+
const receivers = pythonFastapiReceivers(root, imports);
|
|
1703
|
+
if (receivers.size === 0)
|
|
1704
|
+
return [];
|
|
1705
|
+
const directHandlers = pythonDirectModuleHandlers(root);
|
|
1706
|
+
const includes = pythonRouterIncludePrefixes(root, receivers);
|
|
1707
|
+
const out = [];
|
|
1708
|
+
const add = (receiverName, method, routePath, registrationAt, handler) => {
|
|
1709
|
+
const receiver = receivers.get(receiverName);
|
|
1710
|
+
if (!receiver || receiver.boundAt >= registrationAt)
|
|
1711
|
+
return;
|
|
1712
|
+
const includePrefixes = receiver.kind === "app" ? new Set([""]) : includes.get(receiverName);
|
|
1713
|
+
const prefixes = includePrefixes && includePrefixes.size > 0 ? [...includePrefixes] : [""];
|
|
1714
|
+
const status = receiver.prefixResolved && (receiver.kind === "app" || Boolean(includePrefixes?.size)) ? "resolved" : "unresolved";
|
|
1715
|
+
for (const includePrefix of prefixes) {
|
|
1716
|
+
const prefix = normalizePythonRoutePath(includePrefix, receiver.prefix);
|
|
1717
|
+
const normalizedPrefix = prefix === "/" ? "" : prefix;
|
|
1718
|
+
out.push(makePythonBehaviorContract({
|
|
1719
|
+
file,
|
|
1720
|
+
method: method.toUpperCase(),
|
|
1721
|
+
path: normalizePythonRoutePath(normalizedPrefix, routePath),
|
|
1722
|
+
handler,
|
|
1723
|
+
prefixStatus: status,
|
|
1724
|
+
prefix: normalizedPrefix
|
|
1725
|
+
}));
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
for (const fn of root.descendantsOfType("function_definition")) {
|
|
1729
|
+
if (!fn || hasPythonFunctionAncestor(fn))
|
|
1730
|
+
continue;
|
|
1731
|
+
const handler = pythonQualifiedFunctionName(fn);
|
|
1732
|
+
const decorated = fn.parent?.type === "decorated_definition" ? fn.parent : null;
|
|
1733
|
+
if (!handler || !directHandlers.has(handler) || !decorated || decorated.parent?.type !== "module")
|
|
1734
|
+
continue;
|
|
1735
|
+
for (const decorator of namedChildren(decorated).filter((n) => n.type === "decorator")) {
|
|
1736
|
+
const call = namedChildren(decorator).find((n) => n.type === "call");
|
|
1737
|
+
const parts = call ? callParts(call, "python") : null;
|
|
1738
|
+
const receiver = parts?.qualifier?.split(".")[0];
|
|
1739
|
+
const route = call ? pythonStaticString(pythonPositionalArguments(call)[0] ?? null) : null;
|
|
1740
|
+
if (!call || !parts || !receiver || route === null)
|
|
1741
|
+
continue;
|
|
1742
|
+
if (FASTAPI_HTTP_METHODS.has(parts.callee)) {
|
|
1743
|
+
add(receiver, parts.callee, route, call.startIndex, handler);
|
|
1744
|
+
}
|
|
1745
|
+
else if (parts.callee === "api_route") {
|
|
1746
|
+
const methods = pythonStaticHttpMethods(pythonKeywordArgument(call, "methods"));
|
|
1747
|
+
if (!methods)
|
|
1748
|
+
continue;
|
|
1749
|
+
for (const method of methods)
|
|
1750
|
+
add(receiver, method, route, call.startIndex, handler);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
for (const call of root.descendantsOfType("call")) {
|
|
1755
|
+
if (!call || !pythonDirectModuleExecution(call))
|
|
1756
|
+
continue;
|
|
1757
|
+
const parts = callParts(call, "python");
|
|
1758
|
+
const receiver = parts?.qualifier?.split(".")[0];
|
|
1759
|
+
if (!parts || parts.callee !== "add_api_route" || !receiver || !receivers.has(receiver))
|
|
1760
|
+
continue;
|
|
1761
|
+
const positional = pythonPositionalArguments(call);
|
|
1762
|
+
const route = pythonStaticString(positional[0] ?? null);
|
|
1763
|
+
const handler = pythonDottedName(positional[1] ?? null);
|
|
1764
|
+
if (route === null || !handler || !directHandlers.has(handler))
|
|
1765
|
+
continue;
|
|
1766
|
+
const methods = pythonStaticHttpMethods(pythonKeywordArgument(call, "methods"));
|
|
1767
|
+
if (!methods)
|
|
1768
|
+
continue;
|
|
1769
|
+
for (const method of methods)
|
|
1770
|
+
add(receiver, method, route, call.startIndex, handler);
|
|
1771
|
+
}
|
|
1772
|
+
return dedupePythonContracts(out);
|
|
1773
|
+
}
|
|
1774
|
+
function isFastapiDependsCall(call, imports) {
|
|
1775
|
+
const parts = callParts(call, "python");
|
|
1776
|
+
if (!parts)
|
|
1777
|
+
return false;
|
|
1778
|
+
if (!parts.qualifier) {
|
|
1779
|
+
return pythonImportedFromFastapi(imports, parts.callee, "Depends") || pythonImportedFromFastapi(imports, parts.callee, "Security");
|
|
1780
|
+
}
|
|
1781
|
+
if (parts.callee !== "Depends" && parts.callee !== "Security")
|
|
1782
|
+
return false;
|
|
1783
|
+
const root = parts.qualifier.split(".")[0] ?? "";
|
|
1784
|
+
return imports.some((b) => b.local === root && b.kind === "module" && b.module === "fastapi");
|
|
1785
|
+
}
|
|
1786
|
+
function extractPythonDependencies(root, imports, acceptedHandlers) {
|
|
1787
|
+
const out = [];
|
|
1788
|
+
const seen = new Set();
|
|
1789
|
+
const receivers = pythonFastapiReceivers(root, imports);
|
|
1790
|
+
const directHandlers = pythonDirectModuleHandlers(root);
|
|
1791
|
+
const addFromNode = (handler, node) => {
|
|
1792
|
+
if (!node)
|
|
1793
|
+
return;
|
|
1794
|
+
const calls = [...(node.type === "call" ? [node] : []), ...node.descendantsOfType("call").filter((n) => Boolean(n))];
|
|
1795
|
+
for (const call of calls) {
|
|
1796
|
+
if (!isFastapiDependsCall(call, imports))
|
|
1797
|
+
continue;
|
|
1798
|
+
const dependency = pythonDottedName(pythonPositionalArguments(call)[0] ?? null);
|
|
1799
|
+
if (!dependency)
|
|
1800
|
+
continue;
|
|
1801
|
+
const key = `${handler}|${dependency}`;
|
|
1802
|
+
if (seen.has(key))
|
|
1803
|
+
continue;
|
|
1804
|
+
seen.add(key);
|
|
1805
|
+
out.push({ handler, dependency });
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
for (const fn of root.descendantsOfType("function_definition")) {
|
|
1809
|
+
if (!fn || hasPythonFunctionAncestor(fn))
|
|
1810
|
+
continue;
|
|
1811
|
+
const handler = pythonQualifiedFunctionName(fn);
|
|
1812
|
+
const params = fn.childForFieldName("parameters");
|
|
1813
|
+
const declaration = fn.parent?.type === "decorated_definition" ? fn.parent : fn;
|
|
1814
|
+
if (!handler || !params || !directHandlers.has(handler) || declaration.parent?.type !== "module" || !acceptedHandlers.has(handler))
|
|
1815
|
+
continue;
|
|
1816
|
+
addFromNode(handler, params);
|
|
1817
|
+
const decorated = fn.parent?.type === "decorated_definition" ? fn.parent : null;
|
|
1818
|
+
for (const decorator of decorated ? namedChildren(decorated).filter((n) => n.type === "decorator") : []) {
|
|
1819
|
+
const routeCall = namedChildren(decorator).find((n) => n.type === "call");
|
|
1820
|
+
const parts = routeCall ? callParts(routeCall, "python") : null;
|
|
1821
|
+
const receiver = parts?.qualifier?.split(".")[0];
|
|
1822
|
+
if (!routeCall || !parts || !receiver || !receivers.has(receiver) || receivers.get(receiver).boundAt >= routeCall.startIndex)
|
|
1823
|
+
continue;
|
|
1824
|
+
if (!FASTAPI_HTTP_METHODS.has(parts.callee) && (parts.callee !== "api_route" || !pythonStaticHttpMethods(pythonKeywordArgument(routeCall, "methods"))))
|
|
1825
|
+
continue;
|
|
1826
|
+
addFromNode(handler, pythonKeywordArgument(routeCall, "dependencies"));
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
for (const call of root.descendantsOfType("call")) {
|
|
1830
|
+
if (!call || !pythonDirectModuleExecution(call))
|
|
1831
|
+
continue;
|
|
1832
|
+
const parts = callParts(call, "python");
|
|
1833
|
+
const receiver = parts?.qualifier?.split(".")[0];
|
|
1834
|
+
if (!parts || parts.callee !== "add_api_route" || !receiver || !receivers.has(receiver) || receivers.get(receiver).boundAt >= call.startIndex)
|
|
1835
|
+
continue;
|
|
1836
|
+
const positional = pythonPositionalArguments(call);
|
|
1837
|
+
const route = pythonStaticString(positional[0] ?? null);
|
|
1838
|
+
const handler = pythonDottedName(positional[1] ?? null);
|
|
1839
|
+
const methods = pythonStaticHttpMethods(pythonKeywordArgument(call, "methods"));
|
|
1840
|
+
if (route !== null && handler && methods && directHandlers.has(handler) && acceptedHandlers.has(handler)) {
|
|
1841
|
+
addFromNode(handler, pythonKeywordArgument(call, "dependencies"));
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return out;
|
|
1845
|
+
}
|
|
1433
1846
|
function directPythonAssertCall(assertion) {
|
|
1434
1847
|
const subject = namedChildren(assertion)[0];
|
|
1435
1848
|
if (!subject)
|
|
@@ -1490,7 +1903,7 @@ function extractPythonProofCalls(root) {
|
|
|
1490
1903
|
walk(root, false, null);
|
|
1491
1904
|
return out;
|
|
1492
1905
|
}
|
|
1493
|
-
export function extractTreeSitterStructure(content, language) {
|
|
1906
|
+
export function extractTreeSitterStructure(content, language, relPath = "") {
|
|
1494
1907
|
const parser = parserFor(language);
|
|
1495
1908
|
if (!parser)
|
|
1496
1909
|
return { imports: [], calls: [] };
|
|
@@ -1530,6 +1943,9 @@ export function extractTreeSitterStructure(content, language) {
|
|
|
1530
1943
|
callerReceivers.set(name, { receiverVar: rv, receiverType: recv });
|
|
1531
1944
|
}
|
|
1532
1945
|
}
|
|
1946
|
+
if (name && language === "python" && node.type === "function_definition") {
|
|
1947
|
+
name = pythonQualifiedFunctionName(node) ?? name;
|
|
1948
|
+
}
|
|
1533
1949
|
if (name) {
|
|
1534
1950
|
nextCaller = name;
|
|
1535
1951
|
nextShadowed = localBindings(node, language);
|
|
@@ -1546,6 +1962,10 @@ export function extractTreeSitterStructure(content, language) {
|
|
|
1546
1962
|
};
|
|
1547
1963
|
visit(root, null, new Set(), false);
|
|
1548
1964
|
const imports = extractImports(root, language);
|
|
1965
|
+
const pythonBehaviorContracts = language === "python" ? extractPythonBehaviorContracts(root, imports, relPath) : undefined;
|
|
1966
|
+
const pythonDependencies = language === "python"
|
|
1967
|
+
? extractPythonDependencies(root, imports, new Set((pythonBehaviorContracts ?? []).map((contract) => contract.handler).filter((handler) => Boolean(handler))))
|
|
1968
|
+
: undefined;
|
|
1549
1969
|
const result = {
|
|
1550
1970
|
...(language === "java" ? { packageName: javaPackage(root), javaClasses: javaClassInfos(root), javaProofCalls: extractJavaProofCalls(root, imports) } : {}),
|
|
1551
1971
|
...(language === "go" ? { packageName: goPackage(root), goCtorResults: extractGoCtorResults(root) } : {}),
|
|
@@ -1555,7 +1975,7 @@ export function extractTreeSitterStructure(content, language) {
|
|
|
1555
1975
|
imports,
|
|
1556
1976
|
calls,
|
|
1557
1977
|
...(language === "go" ? { goProofCalls: extractGoProofCalls(root, imports) } : {}),
|
|
1558
|
-
...(language === "python" ? { pythonProofCalls: extractPythonProofCalls(root) } : {})
|
|
1978
|
+
...(language === "python" ? { pythonProofCalls: extractPythonProofCalls(root), pythonBehaviorContracts, pythonDependencies } : {})
|
|
1559
1979
|
};
|
|
1560
1980
|
tree.delete();
|
|
1561
1981
|
return result;
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* The graph is built directly by OrangePro; it does not depend on any
|
|
10
10
|
* third-party graph product or format.
|
|
11
11
|
*/
|
|
12
|
-
//
|
|
13
|
-
// graphs hold
|
|
14
|
-
export const LOCAL_GRAPH_SCHEMA_VERSION = "orangepro.local_graph.
|
|
12
|
+
// v3: Python method symbol ids are owner-qualified (`sym:file.py#Class.method`) —
|
|
13
|
+
// old graphs hold collision-prone bare method ids and must force-rebuild.
|
|
14
|
+
export const LOCAL_GRAPH_SCHEMA_VERSION = "orangepro.local_graph.v3";
|
|
15
15
|
/** Node kinds that map to behaviors/requirements for scoring + gaps + generation. */
|
|
16
16
|
export const BEHAVIOR_KINDS = new Set([
|
|
17
17
|
"Requirement",
|
package/dist/local/provenance.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ORS_VERSION } from "./score/risk.js";
|
|
|
3
3
|
import { ORANGEPRO_VERSION } from "./version.js";
|
|
4
4
|
import { hashString } from "./util/hash.js";
|
|
5
5
|
export const ARTIFACT_IDENTITY_VERSION = "orangepro.artifact_identity.v1";
|
|
6
|
-
export const ANALYZER_VERSION = "orangepro.analyzer.
|
|
6
|
+
export const ANALYZER_VERSION = "orangepro.analyzer.v2";
|
|
7
7
|
export const PROOF_ORACLE_VERSION = "orangepro.targeted_mutation_oracle.v1";
|
|
8
8
|
function stable(value) {
|
|
9
9
|
if (value === null || typeof value !== "object")
|
package/dist/local/score/risk.js
CHANGED
|
@@ -446,6 +446,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
446
446
|
// worklist but do not rescale an unchanged peer.
|
|
447
447
|
const normalizationSymbols = graph.nodes.filter((n) => n.kind === "CodeSymbol" && n.denominator_eligible === true && !n.stale && rankEligible(n) && !isSuppressed(n.external_id));
|
|
448
448
|
const symbols = normalizationSymbols.filter((n) => !confirmed.has(n.external_id));
|
|
449
|
+
const worklistSymbols = symbols.filter((n) => n.properties.ranking_exclusion_reason !== "python_structural_container");
|
|
449
450
|
const symbolIds = new Set(normalizationSymbols.map((s) => s.external_id));
|
|
450
451
|
const symbolsByFile = new Map();
|
|
451
452
|
for (const s of normalizationSymbols) {
|
|
@@ -603,7 +604,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
603
604
|
for (const s of syms)
|
|
604
605
|
legacyIncoming.set(s.external_id, (legacyIncoming.get(s.external_id) ?? 0) + share);
|
|
605
606
|
}
|
|
606
|
-
return
|
|
607
|
+
return worklistSymbols
|
|
607
608
|
.map((s) => {
|
|
608
609
|
const file = symbolFile(s);
|
|
609
610
|
const incoming_refs = legacyIncoming.get(s.external_id) ?? 0;
|
|
@@ -649,7 +650,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
649
650
|
const rawById = new Map(normalizationSymbols.map((s, idx) => [s.external_id, rawScores[idx]]));
|
|
650
651
|
const pById = new Map(normalizationSymbols.map((s, idx) => [s.external_id, pScores[idx]]));
|
|
651
652
|
const iById = new Map(normalizationSymbols.map((s, idx) => [s.external_id, iScores[idx]]));
|
|
652
|
-
const ranked =
|
|
653
|
+
const ranked = worklistSymbols
|
|
653
654
|
.map((s) => {
|
|
654
655
|
const file = symbolFile(s);
|
|
655
656
|
const incoming_refs = Math.round((incoming.get(s.external_id) ?? 0) * 10) / 10;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OrangePro (`opro`) — a local-first, BYOK CLI + MCP server that builds an evidence graph from a local checkout, ingests runtime coverage, and generates grounded tests. Metadata-only exports; no source upload; generated tests stay local.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"smoke:generate-prove-v5": "npm run build && node scripts/smoke-generate-prove-v5.mjs",
|
|
102
102
|
"smoke:db-sqljs": "npm run build && node scripts/smoke-db-sqljs-prove.mjs",
|
|
103
103
|
"release": "node scripts/release.mjs",
|
|
104
|
-
"prepublishOnly": "npm run build && npm test"
|
|
104
|
+
"prepublishOnly": "npm run typecheck && npm run build && npm test"
|
|
105
105
|
},
|
|
106
106
|
"engines": {
|
|
107
107
|
"node": ">=20"
|