@orangepro/orangepro-mcp 0.2.41 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/local/analyze/analyzer.js +415 -42
- package/dist/local/analyze/classify.js +5 -1
- package/dist/local/analyze/confirm.js +90 -6
- package/dist/local/analyze/frameworks.js +3 -0
- package/dist/local/analyze/treeSitter/engine.js +425 -5
- package/dist/local/flows/flowWalker.js +1 -1
- package/dist/local/graph/ontology.js +3 -3
- package/dist/local/operations.js +30 -5
- package/dist/local/proofDoctor.js +5 -4
- package/dist/local/provenance.js +84 -0
- package/dist/local/resolve/importGraph.js +15 -1
- package/dist/local/score/rankEligibility.js +15 -0
- package/dist/local/score/risk.js +99 -26
- package/dist/local/viz/behaviorReportData.js +91 -12
- package/dist/local/viz/behaviorReportHtml.js +19 -2
- package/dist/local/viz/html.js +3 -3
- package/package.json +2 -2
|
@@ -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;
|
|
@@ -24,7 +24,7 @@ export function behaviorEntries(nodes, edges) {
|
|
|
24
24
|
n.denominator_eligible === true &&
|
|
25
25
|
n.stale !== true &&
|
|
26
26
|
!endpointStarts.has(n.external_id) &&
|
|
27
|
-
!incomingEligibleCalls.has(n.external_id))
|
|
27
|
+
(n.properties.cli_entrypoint === true || !incomingEligibleCalls.has(n.external_id)))
|
|
28
28
|
.map((n) => ({
|
|
29
29
|
external_id: n.external_id,
|
|
30
30
|
kind: "Behavior",
|
|
@@ -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/operations.js
CHANGED
|
@@ -20,7 +20,7 @@ import { enrichFromContent } from "./enrich/index.js";
|
|
|
20
20
|
import { scoreGraph } from "./score/score.js";
|
|
21
21
|
import { doctorGraph } from "./score/doctor.js";
|
|
22
22
|
import { findGaps } from "./gaps/gaps.js";
|
|
23
|
-
import { rankPriorityGaps, rankRiskGaps } from "./score/risk.js";
|
|
23
|
+
import { inspectRiskInputHealth, rankPriorityGaps, rankRiskGaps, riskHistoryFingerprint } from "./score/risk.js";
|
|
24
24
|
import { generateTests } from "./generate/generator.js";
|
|
25
25
|
import { classifyGeneratedDraftBlocker } from "./generate/draftGuidance.js";
|
|
26
26
|
import { autoProve, NO_KEY_MESSAGE, isEligibleProvableTarget } from "./autoProve.js";
|
|
@@ -46,6 +46,7 @@ import { prepareRuntimeCoverage } from "./analyze/coverageArtifacts.js";
|
|
|
46
46
|
import { appendLedgerRecord, canReproveLanguage, loadLedger, ledgerStats, proofEdgesFor, reproveTarget, resolveTargetSymbol, targetFingerprint, targetLanguage } from "./ledger.js";
|
|
47
47
|
import { buildRtm, provenSymbolIds, renderRtmCsv, renderRtmMarkdown } from "./rtm.js";
|
|
48
48
|
import { buildProofDoctor, distillProofAttempts, loadProofAttempts, proofAttemptsFresh, writeProofAttempts } from "./proofDoctor.js";
|
|
49
|
+
import { buildArtifactIdentity, PROOF_ORACLE_VERSION } from "./provenance.js";
|
|
49
50
|
import { tryScopedReprove } from "./reprove/scoped.js";
|
|
50
51
|
import { resolveContained, toWorkspaceRel } from "./reprove/paths.js";
|
|
51
52
|
import { hashBuffer } from "./util/hash.js";
|
|
@@ -490,7 +491,7 @@ function buildGraphFromFragments(workspaceRoot, scanRoot, analyzeFragment, enric
|
|
|
490
491
|
const sources = [...analyzeFragment.sources, ...enrichFragments.flatMap((f) => f.sources)];
|
|
491
492
|
const manifest = buildManifest(analyzeFragment.file_entries, readGitInfo(gitRunner(scanRoot)), now);
|
|
492
493
|
const prunedEdges = pruneDanglingEdges(nodes, edges);
|
|
493
|
-
|
|
494
|
+
const graph = {
|
|
494
495
|
schema_version: LOCAL_GRAPH_SCHEMA_VERSION,
|
|
495
496
|
workspace: {
|
|
496
497
|
name: config.workspace_name || deriveWorkspaceName(scanRoot),
|
|
@@ -516,6 +517,22 @@ function buildGraphFromFragments(workspaceRoot, scanRoot, analyzeFragment, enric
|
|
|
516
517
|
confirmed_by_layer: confirmedCoverageByLayer({ nodes, edges: prunedEdges })
|
|
517
518
|
}
|
|
518
519
|
};
|
|
520
|
+
graph.artifact_identity = artifactIdentityFor(graph, scanRoot);
|
|
521
|
+
return graph;
|
|
522
|
+
}
|
|
523
|
+
function artifactIdentityFor(graph, repoRoot) {
|
|
524
|
+
const health = inspectRiskInputHealth(repoRoot);
|
|
525
|
+
const codeFiles = Object.entries(graph.manifest.files).filter(([, file]) => file.kind === "code").map(([file]) => file);
|
|
526
|
+
return buildArtifactIdentity(graph, {
|
|
527
|
+
configHash: loadRiskConfig(repoRoot).hash,
|
|
528
|
+
history: {
|
|
529
|
+
state: health.history,
|
|
530
|
+
churnWindow: health.churnWindow,
|
|
531
|
+
churnAvailable: health.churnAvailable,
|
|
532
|
+
commitDate: health.commitDate,
|
|
533
|
+
inputFingerprint: riskHistoryFingerprint(repoRoot, codeFiles, health.churnWindow)
|
|
534
|
+
}
|
|
535
|
+
});
|
|
519
536
|
}
|
|
520
537
|
// ── operations ───────────────────────────────────────────────────────
|
|
521
538
|
export function opInit(root, deps = defaultDeps()) {
|
|
@@ -970,6 +987,7 @@ export function opRecordRun(root, opts, deps = defaultDeps()) {
|
|
|
970
987
|
model: opts.model,
|
|
971
988
|
prompt_version: opts.prompt_version,
|
|
972
989
|
language,
|
|
990
|
+
artifact_identity: artifactIdentityFor(preGraph, preGraph.workspace.root),
|
|
973
991
|
status,
|
|
974
992
|
reprove_mode: reproveMode,
|
|
975
993
|
reprove_reason: reproveReason,
|
|
@@ -1213,6 +1231,7 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
|
|
|
1213
1231
|
const closed = dynamicProofSucceeded(oracle);
|
|
1214
1232
|
const baselineGreen = oracle.baseline?.exitCode === 0 && oracle.baseline?.timedOut !== true;
|
|
1215
1233
|
const mutantFailedAssertion = oracle.mutant?.assertionFailure === true && oracle.mutant?.timedOut !== true;
|
|
1234
|
+
const artifactIdentity = artifactIdentityFor(graph, sourceRoot);
|
|
1216
1235
|
const dynamicProof = {
|
|
1217
1236
|
proof_kind: "dynamic_targeted",
|
|
1218
1237
|
baseline_green: baselineGreen,
|
|
@@ -1223,7 +1242,9 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
|
|
|
1223
1242
|
sentinel: oracle.replacementMode ?? replacementMode,
|
|
1224
1243
|
runner: oracle.runner ?? runner,
|
|
1225
1244
|
test_path: oracle.test ?? testRel,
|
|
1226
|
-
mutant_status: oracle.status
|
|
1245
|
+
mutant_status: oracle.status,
|
|
1246
|
+
oracle_version: PROOF_ORACLE_VERSION,
|
|
1247
|
+
run_fingerprint: artifactIdentity.run_fingerprint
|
|
1227
1248
|
};
|
|
1228
1249
|
const result = appendLedgerRecord(root, {
|
|
1229
1250
|
run_id: opts.run_id,
|
|
@@ -1235,6 +1256,7 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
|
|
|
1235
1256
|
evidence_ids: [],
|
|
1236
1257
|
language,
|
|
1237
1258
|
dynamic_proof: dynamicProof,
|
|
1259
|
+
artifact_identity: artifactIdentity,
|
|
1238
1260
|
target_fingerprint: targetFingerprint(graph, target),
|
|
1239
1261
|
status: closed ? "reproven" : "unproven",
|
|
1240
1262
|
reason: oracle.reason ?? (closed ? "Dynamic targeted proof closed." : "Dynamic targeted proof did not close."),
|
|
@@ -1623,7 +1645,9 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1623
1645
|
// Auto-prove (PR 1): key-gated generate→prove for the top provable TS/JS targets.
|
|
1624
1646
|
// Runs BEFORE the report/RTM writes below so they reflect any freshly-minted Proven.
|
|
1625
1647
|
// Proof is minted only by opProveLoop's UNCHANGED oracle; no key ⇒ no files, no proof.
|
|
1626
|
-
reportProgress(
|
|
1648
|
+
reportProgress(opts.noAuto
|
|
1649
|
+
? "auto-prove: disabled (--no-auto)"
|
|
1650
|
+
: "auto-prove: driving generate → prove on the top provable targets", { current: 6, total: 8 });
|
|
1627
1651
|
let autoProveResult;
|
|
1628
1652
|
try {
|
|
1629
1653
|
autoProveResult = await autoProve(root, {
|
|
@@ -2279,7 +2303,7 @@ export function opBehaviorCoverageHtml(root, outputPath = "orangepro-behavior-co
|
|
|
2279
2303
|
data.delta = computeReportDelta(prev, data);
|
|
2280
2304
|
}
|
|
2281
2305
|
catch {
|
|
2282
|
-
|
|
2306
|
+
// buildBehaviorReportData already provides an explicit first_run state.
|
|
2283
2307
|
}
|
|
2284
2308
|
const html = renderBehaviorReport(data);
|
|
2285
2309
|
const htmlPath = resolve(root, outputPath);
|
|
@@ -2404,6 +2428,7 @@ function incrementalMerge(root, old, changedFiles, now, maxInferredFlows, maxFil
|
|
|
2404
2428
|
confirmed_by_layer: confirmedCoverageByLayer({ nodes, edges: prunedEdges })
|
|
2405
2429
|
}
|
|
2406
2430
|
};
|
|
2431
|
+
graph.artifact_identity = artifactIdentityFor(graph, root);
|
|
2407
2432
|
return { graph, updated_entities: updatedEntities, stale_generated_tests: staleCount, warnings: freshFragment.warnings };
|
|
2408
2433
|
}
|
|
2409
2434
|
const NO_DIFF_GUIDANCE = "No changed files found. Run this on a feature branch, pass base_ref, or use the test-gaps tool for baseline opportunities (CLI: opro gaps; MCP: orangepro_find_test_gaps).";
|
|
@@ -104,6 +104,7 @@ export function distillProofAttempts(auto, meta) {
|
|
|
104
104
|
graph_generated_at: manifest?.generated_at ?? null,
|
|
105
105
|
git_commit: manifest?.git?.commit ?? null,
|
|
106
106
|
git_dirty: manifest?.git?.dirty ?? null,
|
|
107
|
+
artifact_identity: meta.graph.artifact_identity,
|
|
107
108
|
attempted: auto.attempted,
|
|
108
109
|
proven: auto.proven,
|
|
109
110
|
attempts: auto.attempts.map((a) => ({
|
|
@@ -128,11 +129,11 @@ export function writeProofAttempts(root, file) {
|
|
|
128
129
|
writeFileSync(path, JSON.stringify(file, null, 2) + "\n", "utf8");
|
|
129
130
|
return path;
|
|
130
131
|
}
|
|
131
|
-
/** True when the sidecar anchors to the CURRENT
|
|
132
|
+
/** True when the sidecar anchors to the CURRENT material run inputs. */
|
|
132
133
|
export function proofAttemptsFresh(attempts, graph) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
attempts.
|
|
134
|
+
return Boolean(attempts.artifact_identity?.run_fingerprint &&
|
|
135
|
+
graph.artifact_identity?.run_fingerprint &&
|
|
136
|
+
attempts.artifact_identity.run_fingerprint === graph.artifact_identity.run_fingerprint);
|
|
136
137
|
}
|
|
137
138
|
export function loadProofAttempts(root) {
|
|
138
139
|
const path = proofAttemptsPath(root);
|