@orangepro/orangepro-mcp 0.2.45 → 0.2.47
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 +5 -1
- package/dist/local/analyze/treeSitter/engine.js +86 -18
- package/dist/local/autoProve.js +68 -4
- package/dist/local/generate/generator.js +12 -3
- package/dist/local/operations.js +25 -3
- package/dist/local/proofDoctor.js +3 -0
- package/dist/local/provenance.js +1 -1
- package/dist/local/score/risk.js +76 -28
- package/dist/local/score/riskConfig.js +5 -1
- package/dist/local/viz/behaviorReportData.js +35 -5
- package/dist/local/viz/behaviorReportHtml.js +3 -3
- package/package.json +1 -1
- package/scripts/spikes/dynamic-proof-spike.mjs +36 -2
- package/scripts/spikes/go-dynamic-proof-spike.mjs +22 -10
- package/scripts/spikes/java-dynamic-proof-spike.mjs +22 -10
|
@@ -982,7 +982,11 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
982
982
|
...(notEntryPointAdjacent ? { denominator_reason_code: "not_entry_point_adjacent" } : {}),
|
|
983
983
|
...(pythonRankingExclusion
|
|
984
984
|
? {
|
|
985
|
-
ranking_exclusion_code: pythonRankingExclusion.code === "
|
|
985
|
+
ranking_exclusion_code: pythonRankingExclusion.code === "python_trivial_constructor" || pythonRankingExclusion.code === "python_trivial_thread_local_initializer"
|
|
986
|
+
? "trivial_constructor"
|
|
987
|
+
: pythonRankingExclusion.code === "python_data_shape_declaration"
|
|
988
|
+
? "data_shape_declaration"
|
|
989
|
+
: "enum_declaration",
|
|
986
990
|
ranking_exclusion_reason_code: pythonRankingExclusion.code,
|
|
987
991
|
ranking_exclusion_reason: pythonRankingExclusion.reason
|
|
988
992
|
}
|
|
@@ -2687,17 +2687,18 @@ function pythonImportsBefore(root, stopAt) {
|
|
|
2687
2687
|
return bindings;
|
|
2688
2688
|
}
|
|
2689
2689
|
function pythonBaseIsStdlib(base, bindings, module, allowed) {
|
|
2690
|
+
const modules = typeof module === "string" ? [module] : module;
|
|
2690
2691
|
const dotted = pythonDottedName(base);
|
|
2691
2692
|
if (!dotted)
|
|
2692
2693
|
return false;
|
|
2693
2694
|
const parts = dotted.split(".");
|
|
2694
2695
|
if (parts.length === 1) {
|
|
2695
2696
|
const binding = bindings.get(parts[0]);
|
|
2696
|
-
return Boolean(binding?.kind === "named" && binding.module
|
|
2697
|
+
return Boolean(binding?.kind === "named" && modules.includes(binding.module) && binding.imported && allowed.has(binding.imported));
|
|
2697
2698
|
}
|
|
2698
2699
|
if (parts.length === 2 && allowed.has(parts[1])) {
|
|
2699
2700
|
const binding = bindings.get(parts[0]);
|
|
2700
|
-
return Boolean(binding?.kind === "module" && binding.module
|
|
2701
|
+
return Boolean(binding?.kind === "module" && modules.includes(binding.module));
|
|
2701
2702
|
}
|
|
2702
2703
|
return false;
|
|
2703
2704
|
}
|
|
@@ -2741,6 +2742,9 @@ function pythonLiteralOrParameter(node, params) {
|
|
|
2741
2742
|
return false;
|
|
2742
2743
|
if (node.type === "identifier")
|
|
2743
2744
|
return params.has(node.text);
|
|
2745
|
+
// An f-string with {interpolation} computes a value; only plain literals qualify.
|
|
2746
|
+
if ((node.type === "string" || node.type === "concatenated_string") && node.descendantsOfType("interpolation").length > 0)
|
|
2747
|
+
return false;
|
|
2744
2748
|
return new Set(["none", "true", "false", "integer", "float", "string", "concatenated_string"]).has(node.type);
|
|
2745
2749
|
}
|
|
2746
2750
|
function pythonDocstringStatement(statement) {
|
|
@@ -2749,11 +2753,18 @@ function pythonDocstringStatement(statement) {
|
|
|
2749
2753
|
const expression = namedChildren(statement)[0];
|
|
2750
2754
|
return expression?.type === "string" || expression?.type === "concatenated_string";
|
|
2751
2755
|
}
|
|
2752
|
-
function pythonSuperInitStatement(statement) {
|
|
2756
|
+
function pythonSuperInitStatement(statement, params = new Set()) {
|
|
2753
2757
|
if (statement.type !== "expression_statement")
|
|
2754
2758
|
return false;
|
|
2755
2759
|
const call = namedChildren(statement)[0];
|
|
2756
|
-
if (call?.type !== "call"
|
|
2760
|
+
if (call?.type !== "call")
|
|
2761
|
+
return false;
|
|
2762
|
+
// super().__init__(...) may only FORWARD this constructor's own parameters or
|
|
2763
|
+
// literals (positionally or by keyword). Any computed argument is behavior.
|
|
2764
|
+
const forwardsOnly = pythonCallArguments(call).every((arg) => arg.type === "keyword_argument"
|
|
2765
|
+
? pythonLiteralOrParameter(arg.childForFieldName("value"), params)
|
|
2766
|
+
: pythonLiteralOrParameter(arg, params));
|
|
2767
|
+
if (!forwardsOnly)
|
|
2757
2768
|
return false;
|
|
2758
2769
|
const fn = call.childForFieldName("function");
|
|
2759
2770
|
if (fn?.type !== "attribute" || fn.childForFieldName("attribute")?.text !== "__init__")
|
|
@@ -2774,7 +2785,7 @@ function pythonTrivialSelfAssignment(statement, params) {
|
|
|
2774
2785
|
return false;
|
|
2775
2786
|
return pythonLiteralOrParameter(assignment.childForFieldName("right"), params);
|
|
2776
2787
|
}
|
|
2777
|
-
function
|
|
2788
|
+
function pythonTrivialInitializer(classNode) {
|
|
2778
2789
|
const body = classNode.childForFieldName("body");
|
|
2779
2790
|
if (!body)
|
|
2780
2791
|
return null;
|
|
@@ -2800,40 +2811,97 @@ function pythonTrivialThreadLocalInitializer(classNode) {
|
|
|
2800
2811
|
const statement = statements[i];
|
|
2801
2812
|
if (i === 0 && pythonDocstringStatement(statement))
|
|
2802
2813
|
continue;
|
|
2803
|
-
if (statement.type === "pass_statement" || pythonSuperInitStatement(statement) || pythonTrivialSelfAssignment(statement, params))
|
|
2814
|
+
if (statement.type === "pass_statement" || pythonSuperInitStatement(statement, params) || pythonTrivialSelfAssignment(statement, params))
|
|
2804
2815
|
continue;
|
|
2805
2816
|
return null;
|
|
2806
2817
|
}
|
|
2807
2818
|
return pythonQualifiedFunctionName(fn) ?? null;
|
|
2808
2819
|
}
|
|
2820
|
+
const PYTHON_TYPING_SHAPES = new Set(["TypedDict", "NamedTuple", "Protocol"]);
|
|
2821
|
+
const PYTHON_TYPING_MODULES = ["typing", "typing_extensions"];
|
|
2822
|
+
const PYTHON_PYDANTIC_SHAPES = new Set(["BaseModel"]);
|
|
2823
|
+
const PYTHON_PYDANTIC_MODULES = ["pydantic", "pydantic.main", "pydantic.v1"];
|
|
2824
|
+
const PYTHON_DATACLASS_MODULES = ["dataclasses", "pydantic.dataclasses"];
|
|
2825
|
+
/** `@dataclass`, `@dataclass(...)`, `@dataclasses.dataclass(...)` (stdlib or pydantic's drop-in). */
|
|
2826
|
+
function pythonIsStdlibDataclassDecorator(decorator, bindings) {
|
|
2827
|
+
let expression = namedChildren(decorator)[0] ?? null;
|
|
2828
|
+
if (expression?.type === "call")
|
|
2829
|
+
expression = expression.childForFieldName("function");
|
|
2830
|
+
return Boolean(expression && pythonBaseIsStdlib(expression, bindings, PYTHON_DATACLASS_MODULES, new Set(["dataclass"])));
|
|
2831
|
+
}
|
|
2832
|
+
/** Class body holds only field declarations: annotations (with or without a default),
|
|
2833
|
+
* plain attribute assignments, a docstring, or `pass`. No methods, no nested classes,
|
|
2834
|
+
* no statements that run logic. */
|
|
2835
|
+
function pythonFieldOnlyBody(classNode) {
|
|
2836
|
+
const body = classNode.childForFieldName("body");
|
|
2837
|
+
if (!body)
|
|
2838
|
+
return false;
|
|
2839
|
+
const statements = blockStatements(body);
|
|
2840
|
+
if (statements.length === 0)
|
|
2841
|
+
return false;
|
|
2842
|
+
return statements.every((statement, index) => {
|
|
2843
|
+
if (statement.type === "pass_statement")
|
|
2844
|
+
return true;
|
|
2845
|
+
if (index === 0 && pythonDocstringStatement(statement))
|
|
2846
|
+
return true;
|
|
2847
|
+
if (statement.type !== "expression_statement")
|
|
2848
|
+
return false;
|
|
2849
|
+
const inner = namedChildren(statement);
|
|
2850
|
+
// A lambda in a field value (e.g. custom JSON encoders) is behavior, not data.
|
|
2851
|
+
return inner.length === 1 && inner[0].type === "assignment" && inner[0].childForFieldName("left")?.type === "identifier"
|
|
2852
|
+
&& inner[0].descendantsOfType("lambda").length === 0;
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2809
2855
|
function extractPythonDeclarationRankingExclusions(root) {
|
|
2810
2856
|
const out = [];
|
|
2811
2857
|
for (const classNode of root.descendantsOfType("class_definition")) {
|
|
2812
|
-
if (!classNode || hasPythonFunctionAncestor(classNode)
|
|
2858
|
+
if (!classNode || hasPythonFunctionAncestor(classNode))
|
|
2859
|
+
continue;
|
|
2860
|
+
// Module-level classes only (decorated or not): nested-class symbol names are
|
|
2861
|
+
// not guaranteed to line up with the analyzer's symbol table.
|
|
2862
|
+
const decorated = classNode.parent?.type === "decorated_definition" ? classNode.parent : null;
|
|
2863
|
+
if ((decorated ?? classNode).parent?.type !== "module")
|
|
2813
2864
|
continue;
|
|
2814
2865
|
const name = classNode.childForFieldName("name")?.text;
|
|
2815
|
-
|
|
2816
|
-
if (!name || !bases)
|
|
2866
|
+
if (!name)
|
|
2817
2867
|
continue;
|
|
2818
|
-
const
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2868
|
+
const bases = classNode.childForFieldName("superclasses");
|
|
2869
|
+
const bindings = pythonImportsBefore(root, (decorated ?? classNode).startIndex);
|
|
2870
|
+
const baseNodes = bases ? namedChildren(bases) : [];
|
|
2871
|
+
const hasMethod = pythonClassHasMethod(classNode);
|
|
2872
|
+
if (baseNodes.some((base) => pythonBaseIsStdlib(base, bindings, "enum", PYTHON_ENUM_TYPES)) && !hasMethod) {
|
|
2821
2873
|
out.push({
|
|
2822
2874
|
symbol: name,
|
|
2823
2875
|
code: "python_stdlib_enum_declaration",
|
|
2824
2876
|
reason: "Standard-library enum declaration without methods — retained in the graph but excluded from priority ranking."
|
|
2825
2877
|
});
|
|
2878
|
+
continue;
|
|
2826
2879
|
}
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2880
|
+
// Data-shape declarations: a TypedDict/NamedTuple/Protocol, a pydantic model, or a
|
|
2881
|
+
// stdlib @dataclass whose body only declares fields. No method ⇒ no behavior to test.
|
|
2882
|
+
if (!hasMethod && pythonFieldOnlyBody(classNode)) {
|
|
2883
|
+
const typedShape = baseNodes.some((base) => pythonBaseIsStdlib(base, bindings, PYTHON_TYPING_MODULES, PYTHON_TYPING_SHAPES) ||
|
|
2884
|
+
pythonBaseIsStdlib(base, bindings, PYTHON_PYDANTIC_MODULES, PYTHON_PYDANTIC_SHAPES));
|
|
2885
|
+
const dataclassShape = Boolean(decorated) && namedChildren(decorated).some((child) => child.type === "decorator" && pythonIsStdlibDataclassDecorator(child, bindings));
|
|
2886
|
+
if (typedShape || dataclassShape) {
|
|
2830
2887
|
out.push({
|
|
2831
|
-
symbol:
|
|
2832
|
-
code: "
|
|
2833
|
-
reason: "
|
|
2888
|
+
symbol: name,
|
|
2889
|
+
code: "python_data_shape_declaration",
|
|
2890
|
+
reason: "Data-shape declaration (fields only, no methods) — retained in the graph but excluded from priority ranking."
|
|
2834
2891
|
});
|
|
2892
|
+
continue;
|
|
2835
2893
|
}
|
|
2836
2894
|
}
|
|
2895
|
+
// Trivial constructor on ANY class: the body only stores parameters/literals on
|
|
2896
|
+
// self, forwards parameters/literals to super().__init__, or passes.
|
|
2897
|
+
const initializer = pythonTrivialInitializer(classNode);
|
|
2898
|
+
if (initializer) {
|
|
2899
|
+
out.push({
|
|
2900
|
+
symbol: initializer,
|
|
2901
|
+
code: "python_trivial_constructor",
|
|
2902
|
+
reason: "Trivial constructor (stores or forwards its parameters only) — retained in the graph but excluded from priority ranking."
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2837
2905
|
}
|
|
2838
2906
|
return out;
|
|
2839
2907
|
}
|
package/dist/local/autoProve.js
CHANGED
|
@@ -17,6 +17,7 @@ import { basename, dirname, join, posix, relative, resolve, sep } from "node:pat
|
|
|
17
17
|
import { generateTests, readDeclaredDeps, unresolvedLocalImports } from "./generate/generator.js";
|
|
18
18
|
import { GENERATED_DIR, runHintsFor } from "./generate/runHints.js";
|
|
19
19
|
import { rankRiskGaps } from "./score/risk.js";
|
|
20
|
+
import { globToRegExp, loadRiskConfig } from "./score/riskConfig.js";
|
|
20
21
|
import { resolveProviderConfig } from "./localConfig.js";
|
|
21
22
|
import { buildProvider, DeterministicProvider } from "./generate/providers.js";
|
|
22
23
|
import { resolveContained } from "./reprove/paths.js";
|
|
@@ -371,6 +372,14 @@ const GEN_WINDOW = 5;
|
|
|
371
372
|
// file (every eligible symbol × every importing test) cannot starve the shared budget before
|
|
372
373
|
// a provable hard-edge symbol is tried. Small K; promote to a flag if a repo needs a wider sweep.
|
|
373
374
|
const EXISTING_LANE_MAX_WEAK_PER_SYMBOL = 3;
|
|
375
|
+
// A runner that crashes without returning a result (no JSON) fails for the whole
|
|
376
|
+
// project root, not for one test: test-specific problems come back as a classified
|
|
377
|
+
// result. Three crashes in a row in one root stop further calls there instead of
|
|
378
|
+
// walking every remaining linked target (litellm-scale repos queue 1,000+).
|
|
379
|
+
const RUNNER_CRASH_LIMIT = 3;
|
|
380
|
+
const RUNNER_CRASH_RE = /did not return JSON/;
|
|
381
|
+
// Out of disk space is machine-wide: every later sandbox copy fails the same way.
|
|
382
|
+
const DISK_FULL_RE = /\bENOSPC\b|no space left on device/i;
|
|
374
383
|
const RUNNER_ROOT_MARKERS = {
|
|
375
384
|
typescript: ["package.json"],
|
|
376
385
|
javascript: ["package.json"],
|
|
@@ -867,6 +876,13 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
|
|
|
867
876
|
let nonPythonCountedAttempts = 0;
|
|
868
877
|
const pythonPreflightGreen = new Set();
|
|
869
878
|
const pythonBlockedCounts = new Map();
|
|
879
|
+
// classification.rank_exclude_paths removes paths from ranking; proof follows the
|
|
880
|
+
// same scope so excluded areas never cost runner calls or sandbox copies.
|
|
881
|
+
const proofExcludeRe = loadRiskConfig(sourceRoot).config.classification.rank_exclude_paths.map(globToRegExp);
|
|
882
|
+
const excludedTargets = new Set();
|
|
883
|
+
const runnerCrashes = new Map();
|
|
884
|
+
const crashBlocked = new Map();
|
|
885
|
+
let diskFull;
|
|
870
886
|
const changed = opts.changedFiles && opts.changedFiles.length > 0 ? new Set(opts.changedFiles) : null;
|
|
871
887
|
const reader = fileReaderFor(sourceRoot); // R-2: source scan for the node:sqlite env profile
|
|
872
888
|
// Fix 3: hard TESTED_BY/COVERS pairs first, weak MAY_* pairs after and capped per symbol.
|
|
@@ -884,6 +900,12 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
|
|
|
884
900
|
continue;
|
|
885
901
|
if (changed && !changed.has(symbolFileOf(node)))
|
|
886
902
|
continue;
|
|
903
|
+
if (diskFull)
|
|
904
|
+
break;
|
|
905
|
+
if (proofExcludeRe.length > 0 && proofExcludeRe.some((re) => re.test(symbolFileOf(node)))) {
|
|
906
|
+
excludedTargets.add(symId);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
887
909
|
// Fix 1: already Proven for the CURRENT code (fingerprint match under #162) → skip. Re-closing
|
|
888
910
|
// it would overstate newly-proven and append a redundant closed cert every run.
|
|
889
911
|
if (alreadyProven.has(symId))
|
|
@@ -894,6 +916,12 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
|
|
|
894
916
|
const targetFileRel = symbolFileOf(node);
|
|
895
917
|
const targetLanguageName = language ?? targetLanguage(symId);
|
|
896
918
|
const runnerRoot = projectRoot ?? proofRunnerRoot(sourceRoot, targetFileRel, testRel);
|
|
919
|
+
const crashKey = `${targetLanguageName}\u0000${runnerRoot}`;
|
|
920
|
+
const crashBlock = crashBlocked.get(crashKey);
|
|
921
|
+
if (crashBlock) {
|
|
922
|
+
crashBlock.skipped++;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
897
925
|
// A prior candidate in this exact runner project hit a classified project-wide
|
|
898
926
|
// toolchain/environment failure. Preserve the budget and explain the skip.
|
|
899
927
|
const isPython = isPythonFile(targetFileRel);
|
|
@@ -980,9 +1008,22 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
|
|
|
980
1008
|
attempted--;
|
|
981
1009
|
if (!isPython)
|
|
982
1010
|
nonPythonCountedAttempts--;
|
|
983
|
-
|
|
1011
|
+
const message = redactSecrets(errMsg(e));
|
|
1012
|
+
reportProgress(`proof skipped: ${symId}: ${message}`);
|
|
1013
|
+
if (DISK_FULL_RE.test(message)) {
|
|
1014
|
+
diskFull = message;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
if (RUNNER_CRASH_RE.test(message)) {
|
|
1018
|
+
const crashes = (runnerCrashes.get(crashKey) ?? 0) + 1;
|
|
1019
|
+
runnerCrashes.set(crashKey, crashes);
|
|
1020
|
+
if (crashes >= RUNNER_CRASH_LIMIT) {
|
|
1021
|
+
crashBlocked.set(crashKey, { language: targetLanguageName, root: runnerRoot, skipped: 0, lastError: message.slice(0, 200) });
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
984
1024
|
continue;
|
|
985
1025
|
}
|
|
1026
|
+
runnerCrashes.delete(crashKey);
|
|
986
1027
|
const { classification, reason, category } = classifyProof(result, { sourceRoot, targetFileRel });
|
|
987
1028
|
const baselineGreen = baselineGreenOf(result);
|
|
988
1029
|
if (isPython) {
|
|
@@ -1036,7 +1077,28 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
|
|
|
1036
1077
|
project_root: root,
|
|
1037
1078
|
blocked_by: block.blockedBy
|
|
1038
1079
|
}));
|
|
1039
|
-
|
|
1080
|
+
for (const block of crashBlocked.values()) {
|
|
1081
|
+
skipped.push({
|
|
1082
|
+
title: `${block.language} project ${block.root}`,
|
|
1083
|
+
reason: `the proof runner crashed ${RUNNER_CRASH_LIMIT} times in a row without returning a result; ${block.skipped} remaining linked target${block.skipped === 1 ? " was" : "s were"} not attempted in this root. Last error: ${block.lastError}`,
|
|
1084
|
+
language: block.language,
|
|
1085
|
+
project_root: block.root
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
if (excludedTargets.size > 0) {
|
|
1089
|
+
const n = excludedTargets.size;
|
|
1090
|
+
skipped.push({
|
|
1091
|
+
title: "Excluded paths",
|
|
1092
|
+
reason: `${n} linked target${n === 1 ? " is" : "s are"} under classification.rank_exclude_paths and ${n === 1 ? "was" : "were"} not attempted.`
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
if (diskFull) {
|
|
1096
|
+
skipped.push({
|
|
1097
|
+
title: "Disk full",
|
|
1098
|
+
reason: `the disk ran out of space while preparing a proof sandbox; all remaining proof attempts were stopped. Free disk space and rerun. (${diskFull.slice(0, 200)})`
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
return { attempts, needsSetup, skipped, proven, attempted, nonPythonAttempted: nonPythonCountedAttempts, provenSymbols, ...(diskFull ? { diskFull } : {}) };
|
|
1040
1102
|
}
|
|
1041
1103
|
/**
|
|
1042
1104
|
* Drive prove for the top provable TS/JS targets. Two lanes: (1) prove the repo's OWN
|
|
@@ -1083,12 +1145,14 @@ export async function autoProve(root, opts, deps) {
|
|
|
1083
1145
|
const genLimit = Math.max(1, Math.min(MAX_AUTO_LIMIT, Math.floor(opts.proof_gen_limit ?? DEFAULT_PROOF_GEN_LIMIT)));
|
|
1084
1146
|
// ── Lane 1: existing associated tests — NO key required, runs FIRST (PR 1.5). ──
|
|
1085
1147
|
const ex = proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, proveLoop, proveDeps, alreadyProven, projectBlocked, existingLimit, genLimit, autoLimit);
|
|
1086
|
-
if (opts.existingOnly) {
|
|
1148
|
+
if (opts.existingOnly || ex.diskFull) {
|
|
1087
1149
|
const status = ex.proven > 0 ? "proven-run" : ex.attempted > 0 ? "ran-no-proof" : "no-targets";
|
|
1088
1150
|
return {
|
|
1089
1151
|
ran: ex.attempted > 0,
|
|
1090
1152
|
status,
|
|
1091
|
-
reason:
|
|
1153
|
+
reason: ex.diskFull
|
|
1154
|
+
? "Proof stopped: the disk ran out of space while preparing a proof sandbox. Free disk space and rerun."
|
|
1155
|
+
: "existing-tests-only: generation disabled.",
|
|
1092
1156
|
attempted: ex.attempted,
|
|
1093
1157
|
proven: ex.proven,
|
|
1094
1158
|
needs_setup: ex.needsSetup,
|
|
@@ -26,7 +26,7 @@ const MAX_RELATED_FILES = 4;
|
|
|
26
26
|
const SYMBOL_EXCERPT_CONTEXT_LINES = 3;
|
|
27
27
|
const MAX_EXCERPT_CHARS = 8000;
|
|
28
28
|
const MAX_TARGET_TYPE_EXCERPT_CHARS = 5000;
|
|
29
|
-
const STATIC_CHECK_TIMEOUT_MS =
|
|
29
|
+
const STATIC_CHECK_TIMEOUT_MS = 10000;
|
|
30
30
|
// Large Go monorepos can spend tens of seconds populating a cold module cache
|
|
31
31
|
// before the target package is compiled. Keep the check authoritative instead
|
|
32
32
|
// of downgrading valid generated tests while dependencies are still downloading.
|
|
@@ -71,6 +71,11 @@ function testFilesForSourceFile(graph, sourceFile) {
|
|
|
71
71
|
}
|
|
72
72
|
return [...out];
|
|
73
73
|
}
|
|
74
|
+
/** `Model provider HTTP 429: {...body...}` → `Model provider HTTP 429 (response body omitted)`. */
|
|
75
|
+
export function persistedProviderError(message) {
|
|
76
|
+
const match = /^(Model provider HTTP \d+):/.exec(message);
|
|
77
|
+
return match ? `${match[1]} (response body omitted)` : message;
|
|
78
|
+
}
|
|
74
79
|
/**
|
|
75
80
|
* Strip a Markdown code fence (and any prose around it) from a model body. Real
|
|
76
81
|
* BYOK models frequently wrap the test in ```ts … ``` with a prose preamble/suffix;
|
|
@@ -2389,14 +2394,18 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2389
2394
|
}
|
|
2390
2395
|
catch (callErr) {
|
|
2391
2396
|
const msg = redactSecrets(callErr instanceof Error ? callErr.message : String(callErr));
|
|
2397
|
+
// The terminal warning keeps the provider's response for diagnosis. Persisted
|
|
2398
|
+
// drafts and missing-evidence rows end up in shareable reports, so they carry
|
|
2399
|
+
// only the status line, never the provider's response body (account/billing text).
|
|
2400
|
+
const persisted = persistedProviderError(msg);
|
|
2392
2401
|
warnings.push(`V5 planning call failed for "${gc.ctx.behavior_title}": ${msg} — no test emitted.`);
|
|
2393
2402
|
missing.push({
|
|
2394
2403
|
external_id: behavior.external_id,
|
|
2395
2404
|
title: gc.ctx.behavior_title,
|
|
2396
|
-
reason: `V5 planning call failed: ${
|
|
2405
|
+
reason: `V5 planning call failed: ${persisted}`,
|
|
2397
2406
|
needed: ["a reachable model provider"]
|
|
2398
2407
|
});
|
|
2399
|
-
emitManualPlanningFallback(`V5 planning call failed: ${
|
|
2408
|
+
emitManualPlanningFallback(`V5 planning call failed: ${persisted}.`);
|
|
2400
2409
|
continue;
|
|
2401
2410
|
}
|
|
2402
2411
|
try {
|
package/dist/local/operations.js
CHANGED
|
@@ -1687,10 +1687,13 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1687
1687
|
// Proof is minted only by opProveLoop's UNCHANGED oracle; no key ⇒ no files, no proof.
|
|
1688
1688
|
reportProgress(opts.noAuto
|
|
1689
1689
|
? "auto-prove: disabled (--no-auto)"
|
|
1690
|
-
:
|
|
1690
|
+
: opts.ai === false
|
|
1691
|
+
? "auto-prove: proving with existing tests only (--no-ai: no test generation)"
|
|
1692
|
+
: "auto-prove: driving generate → prove on the top provable targets", { current: 6, total: 8 });
|
|
1691
1693
|
let autoProveResult;
|
|
1692
1694
|
try {
|
|
1693
|
-
|
|
1695
|
+
const scopedFiles = autoProveChangedScope(loadGraph(workspacePaths(root).graphPath), changed, opts.baseRef);
|
|
1696
|
+
const runAutoProve = (changedFiles) => autoProve(root, {
|
|
1694
1697
|
autoLimit: opts.proofLimit ?? opts.autoLimit,
|
|
1695
1698
|
proof_existing_limit: opts.proofLimit ?? opts.autoLimit ?? loadedRiskConfig.config.proof.attempt_limit,
|
|
1696
1699
|
proof_gen_limit: loadedRiskConfig.config.proof.baseline_green_target,
|
|
@@ -1698,8 +1701,24 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1698
1701
|
provider: providerOpts.provider,
|
|
1699
1702
|
model: providerOpts.model,
|
|
1700
1703
|
prompt_version: opts.promptVersion,
|
|
1701
|
-
changedFiles
|
|
1704
|
+
changedFiles,
|
|
1705
|
+
// --no-ai means no model calls anywhere in the run. The generation lane is gated
|
|
1706
|
+
// only by a key in the environment, so with a real provider configured it must
|
|
1707
|
+
// honor the flag explicitly. Without a key (or with the offline deterministic
|
|
1708
|
+
// provider) no model is called anyway and the released no-key path is unchanged.
|
|
1709
|
+
existingOnly: opts.ai === false && generationProviderConfigured && !deterministicGeneration
|
|
1702
1710
|
}, { ...providerDeps, proveLoop: opProveLoop });
|
|
1711
|
+
autoProveResult = await runAutoProve(scopedFiles);
|
|
1712
|
+
if (scopedFiles && autoProveResult.attempted === 0 && !opts.baseRef && !opts.noAuto) {
|
|
1713
|
+
// An uncommitted working tree implicitly scoped the pass, but nothing in that
|
|
1714
|
+
// scope was provable. Fall back to the global top targets instead of reporting
|
|
1715
|
+
// "not attempted" for a reason the user cannot see. --base keeps strict scoping.
|
|
1716
|
+
warnings.push(`auto-prove: none of the ${scopedFiles.length} uncommitted changed file(s) had a provable target with a linked test; ran the global top targets instead.`);
|
|
1717
|
+
autoProveResult = await runAutoProve(undefined);
|
|
1718
|
+
}
|
|
1719
|
+
else if (scopedFiles) {
|
|
1720
|
+
autoProveResult = { ...autoProveResult, scoped_changed_files: scopedFiles.length };
|
|
1721
|
+
}
|
|
1703
1722
|
for (const skip of autoProveResult.skipped)
|
|
1704
1723
|
warnings.push(`auto-prove skipped ${skip.target_symbol ?? skip.title}: ${skip.reason}`);
|
|
1705
1724
|
}
|
|
@@ -1944,6 +1963,9 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1944
1963
|
const dynForReport = {
|
|
1945
1964
|
attempted: autoProveResult.attempted,
|
|
1946
1965
|
proven: autoProveResult.proven,
|
|
1966
|
+
status: autoProveResult.status,
|
|
1967
|
+
reason: autoProveResult.reason,
|
|
1968
|
+
...(autoProveResult.scoped_changed_files !== undefined ? { scopedChangedFiles: autoProveResult.scoped_changed_files } : {}),
|
|
1947
1969
|
needsSetup: autoProveResult.needs_setup
|
|
1948
1970
|
.filter((a) => !a.deduped)
|
|
1949
1971
|
.map((a) => ({ category: a.category, reason: a.reason }))
|
|
@@ -107,6 +107,9 @@ export function distillProofAttempts(auto, meta) {
|
|
|
107
107
|
artifact_identity: meta.graph.artifact_identity,
|
|
108
108
|
attempted: auto.attempted,
|
|
109
109
|
proven: auto.proven,
|
|
110
|
+
...(auto.status ? { status: auto.status } : {}),
|
|
111
|
+
...(auto.reason ? { reason: redactSecrets(auto.reason) } : {}),
|
|
112
|
+
...(auto.scoped_changed_files !== undefined ? { scoped_changed_files: auto.scoped_changed_files } : {}),
|
|
110
113
|
attempts: auto.attempts.filter((a) => !a.deduped).map((a) => ({
|
|
111
114
|
target_symbol: a.target_symbol,
|
|
112
115
|
test_path: a.test_path || undefined,
|
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.v6";
|
|
7
7
|
export const PROOF_ORACLE_VERSION = "orangepro.targeted_mutation_oracle.v3";
|
|
8
8
|
function stable(value) {
|
|
9
9
|
if (value === null || typeof value !== "object")
|
package/dist/local/score/risk.js
CHANGED
|
@@ -78,15 +78,18 @@ function confirmedBehaviorIds(graph, provenIds) {
|
|
|
78
78
|
}
|
|
79
79
|
return ids;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
/** Default bounds for repository-wide history acquisition (overridable via
|
|
82
|
+
* tuning.churn_max_commits / tuning.churn_timeout_seconds, always disclosed).
|
|
83
|
+
* Sized so an active monorepo's full 180-day window completes (a 5,000-commit /
|
|
84
|
+
* 60 s bound truncated windows that 0.2.44 scanned completely). */
|
|
85
|
+
export const GIT_CHURN_MAX_COMMITS = 20_000;
|
|
86
|
+
export const GIT_CHURN_TIMEOUT_MS = 300_000;
|
|
84
87
|
const GIT_FIRST_COMMIT_BATCH = 200;
|
|
85
88
|
/**
|
|
86
89
|
* This worker intentionally uses spawn, not execFileSync: stdout is parsed as it
|
|
87
|
-
* arrives, so a
|
|
88
|
-
* completed chunks. It
|
|
89
|
-
*
|
|
90
|
+
* arrives, so a time bound returns the values already observed instead of erasing
|
|
91
|
+
* completed chunks. It keeps one counter per path the window touched (bounded by
|
|
92
|
+
* the window, not by total history) so a single scan serves every caller. The scoring API
|
|
90
93
|
* is synchronous, therefore the worker signals only after writing its result file.
|
|
91
94
|
*/
|
|
92
95
|
const GIT_CHURN_WORKER_SOURCE = String.raw `
|
|
@@ -95,7 +98,7 @@ const { writeFileSync } = require("node:fs");
|
|
|
95
98
|
const { workerData } = require("node:worker_threads");
|
|
96
99
|
const state = new Int32Array(workerData.signal);
|
|
97
100
|
const values = new Map();
|
|
98
|
-
const trackedFiles = new Set(workerData.files);
|
|
101
|
+
const trackedFiles = Array.isArray(workerData.files) ? new Set(workerData.files) : null;
|
|
99
102
|
let commitsScanned = 0;
|
|
100
103
|
let commitLimitReached = false;
|
|
101
104
|
let expectingTimestamp = false;
|
|
@@ -110,7 +113,7 @@ let child;
|
|
|
110
113
|
let childClosed = false;
|
|
111
114
|
const firstLine = (text) => String(text || "").split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "";
|
|
112
115
|
const add = (path, adds, dels) => {
|
|
113
|
-
if (!path || commitLimitReached || !trackedFiles.has(path)) return;
|
|
116
|
+
if (!path || commitLimitReached || (trackedFiles && !trackedFiles.has(path))) return;
|
|
114
117
|
values.set(path, (values.get(path) || 0) + adds + dels);
|
|
115
118
|
};
|
|
116
119
|
const consume = (token) => {
|
|
@@ -260,12 +263,17 @@ function gitChurn(root, files, window, health = inspectRiskInputHealth(root, win
|
|
|
260
263
|
// Values are retained only for the requested paths, so that set and clone
|
|
261
264
|
// state are part of cache identity. A checkout can be marked partial at an
|
|
262
265
|
// unchanged HEAD and must then remain truthfully provisional.
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
+
const requested = new Set(files);
|
|
267
|
+
const tuning = loadRiskConfig(root).config.tuning;
|
|
268
|
+
const maxCommits = tuning.churn_max_commits ?? GIT_CHURN_MAX_COMMITS;
|
|
269
|
+
const timeoutMs = (tuning.churn_timeout_seconds ?? GIT_CHURN_TIMEOUT_MS / 1000) * 1000;
|
|
270
|
+
// ONE bounded scan per repository state: values are kept for every path the
|
|
271
|
+
// window touched and filtered per caller, so the ranking, the report header and
|
|
272
|
+
// the static snapshot never each pay for (and each time out on) the same scan.
|
|
273
|
+
const key = `${health.gitRoot ?? root}\0${health.commit ?? ""}\0${health.history}\0${window}\0${maxCommits}\0${timeoutMs}`;
|
|
266
274
|
const cached = historyCache.get(key);
|
|
267
275
|
if (cached)
|
|
268
|
-
return { ...cached, values: new Map(cached.values) };
|
|
276
|
+
return { ...cached, values: new Map([...cached.values].filter(([file]) => requested.has(file))) };
|
|
269
277
|
const dir = mkdtempSync(join(tmpdir(), "orangepro-churn-"));
|
|
270
278
|
const resultPath = join(dir, "result.json");
|
|
271
279
|
const signal = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
|
|
@@ -277,9 +285,9 @@ function gitChurn(root, files, window, health = inspectRiskInputHealth(root, win
|
|
|
277
285
|
workerData: {
|
|
278
286
|
root,
|
|
279
287
|
window,
|
|
280
|
-
files:
|
|
281
|
-
maxCommits
|
|
282
|
-
timeoutMs
|
|
288
|
+
files: null,
|
|
289
|
+
maxCommits,
|
|
290
|
+
timeoutMs,
|
|
283
291
|
cloneState: health.history,
|
|
284
292
|
resultPath,
|
|
285
293
|
signal
|
|
@@ -287,7 +295,7 @@ function gitChurn(root, files, window, health = inspectRiskInputHealth(root, win
|
|
|
287
295
|
});
|
|
288
296
|
// The worker publishes a partial result 100ms after its child timeout; leave
|
|
289
297
|
// explicit scheduling room before the synchronous API forces it down.
|
|
290
|
-
Atomics.wait(state, 0, 0,
|
|
298
|
+
Atomics.wait(state, 0, 0, timeoutMs + 2_000);
|
|
291
299
|
if (Atomics.load(state, 0) === 1)
|
|
292
300
|
parsed = JSON.parse(readFileSync(resultPath, "utf8"));
|
|
293
301
|
void worker.terminate().catch(() => undefined);
|
|
@@ -519,6 +527,10 @@ const IMPORTED_REPLACEMENT_CALL_RE = /^replace[A-Za-z0-9_]*$/i;
|
|
|
519
527
|
// removals (`pollers.Remove`, `RemoveSpeculativeWorkflowTaskTimeout`) are not.
|
|
520
528
|
const REMOVE_CALL_RE = /^remove(?![A-Za-z0-9_]*(?:listener|handler|observer|callback|attribute|attr|class|child|style|hook|timeout|timer)$)[A-Za-z0-9_]*$/i;
|
|
521
529
|
const PERSISTENCE_FIELD_RE = /(store|client|db|repo|repository|persistence|manager|queue|bucket|index|table|storage|dao)/i;
|
|
530
|
+
// The browser Web Storage API (`sessionStorage` / `localStorage`, optionally via
|
|
531
|
+
// `window.`) is a per-tab/per-device client cache, not a durable data store. Its
|
|
532
|
+
// `removeItem` matches the persistence vocabulary ("storage") by name only.
|
|
533
|
+
const BROWSER_WEB_STORAGE_FIELD_RE = /(^|\.)(sessionStorage|localStorage)$/;
|
|
522
534
|
const SCHEDULED_ENTRY_NAME_RE = /(^|\.)(run|execute|handle|process|tick|scan)$/i;
|
|
523
535
|
const SCHEDULED_ENTRY_PATH_RE = /(^|\/)(jobs?|workers?|scanners?|scavengers?|cron|schedulers?|processors?|consumers?|reconcil\w*)(\/|$)/i;
|
|
524
536
|
/** A time- or queue-triggered entry: nothing calls it synchronously, nobody waits
|
|
@@ -646,28 +658,48 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
646
658
|
// attributed; CALLS already has an exact symbol target. Older graphs without
|
|
647
659
|
// these metadata facts fail closed instead of redistributing imports.
|
|
648
660
|
const incoming = new Map();
|
|
661
|
+
// Display-only breakdown of the same total, so a reader can reconcile the
|
|
662
|
+
// number against the graph: CALLS edges + named imports + module.attr references.
|
|
663
|
+
const incomingParts = new Map();
|
|
664
|
+
const addIncoming = (id, kind, n) => {
|
|
665
|
+
incoming.set(id, (incoming.get(id) ?? 0) + n);
|
|
666
|
+
const parts = incomingParts.get(id) ?? { calls: 0, named: 0, attrs: 0 };
|
|
667
|
+
parts[kind] += n;
|
|
668
|
+
incomingParts.set(id, parts);
|
|
669
|
+
};
|
|
649
670
|
for (const e of graph.edges) {
|
|
650
671
|
if (e.relationship_type === "CALLS" && symbolIds.has(e.to_external_id)) {
|
|
651
|
-
|
|
672
|
+
addIncoming(e.to_external_id, "calls", 1);
|
|
652
673
|
}
|
|
653
674
|
else if (e.relationship_type === "IMPORTS") {
|
|
654
675
|
const named = e.properties?.symbol_imports;
|
|
655
676
|
if (Array.isArray(named))
|
|
656
677
|
for (const id of new Set(named)) {
|
|
657
678
|
if (typeof id === "string" && symbolIds.has(id) && fileBySymbolId.get(id) === e.to_external_id) {
|
|
658
|
-
|
|
679
|
+
addIncoming(id, "named", 1);
|
|
659
680
|
}
|
|
660
681
|
}
|
|
661
682
|
const refs = e.properties?.symbol_references;
|
|
662
683
|
if (refs && typeof refs === "object" && !Array.isArray(refs)) {
|
|
663
684
|
for (const [id, count] of Object.entries(refs)) {
|
|
664
685
|
if (symbolIds.has(id) && typeof count === "number" && Number.isSafeInteger(count) && count > 0 && fileBySymbolId.get(id) === e.to_external_id) {
|
|
665
|
-
|
|
686
|
+
addIncoming(id, "attrs", count);
|
|
666
687
|
}
|
|
667
688
|
}
|
|
668
689
|
}
|
|
669
690
|
}
|
|
670
691
|
}
|
|
692
|
+
const incomingBreakdown = (id) => {
|
|
693
|
+
const parts = incomingParts.get(id);
|
|
694
|
+
if (!parts || (parts.named === 0 && parts.attrs === 0))
|
|
695
|
+
return "";
|
|
696
|
+
const bits = [`${parts.calls} call${parts.calls === 1 ? "" : "s"}`];
|
|
697
|
+
if (parts.named > 0)
|
|
698
|
+
bits.push(`${parts.named} named import${parts.named === 1 ? "" : "s"}`);
|
|
699
|
+
if (parts.attrs > 0)
|
|
700
|
+
bits.push(`${parts.attrs} module.attribute reference${parts.attrs === 1 ? "" : "s"}`);
|
|
701
|
+
return ` (${bits.join(" + ")})`;
|
|
702
|
+
};
|
|
671
703
|
// Per-symbol churn share: file churn weighted by the symbol's line span so one
|
|
672
704
|
// hot file no longer awards its full churn to every method it contains.
|
|
673
705
|
const fileComplexityTotals = new Map();
|
|
@@ -726,20 +758,21 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
726
758
|
// segment. `q.Clock().Now().Truncate` is a chain of return values, not a surface.
|
|
727
759
|
const viaField = (c) => !c.slice(0, c.lastIndexOf(".")).includes("(");
|
|
728
760
|
const fieldOf = (c) => c.slice(0, c.lastIndexOf("."));
|
|
761
|
+
const persistentRemove = (c) => REMOVE_CALL_RE.test(lastSeg(c)) && PERSISTENCE_FIELD_RE.test(fieldOf(c)) && !BROWSER_WEB_STORAGE_FIELD_RE.test(fieldOf(c));
|
|
729
762
|
const destructive = ext.find((c) => viaField(c) && (isDestructiveTerminal(lastSeg(c)) ||
|
|
730
|
-
(
|
|
763
|
+
persistentRemove(c) ||
|
|
731
764
|
extraSinks.some((re) => re.test(lastSeg(c)))));
|
|
732
765
|
if (destructive)
|
|
733
766
|
return destructive;
|
|
734
767
|
return importedStatic.find((c) => viaField(c) && (isDestructiveTerminal(lastSeg(c)) ||
|
|
735
768
|
IMPORTED_REPLACEMENT_CALL_RE.test(lastSeg(c)) ||
|
|
736
|
-
(
|
|
769
|
+
persistentRemove(c) ||
|
|
737
770
|
extraSinks.some((re) => re.test(lastSeg(c)))));
|
|
738
771
|
};
|
|
739
772
|
const reachedSinkFrom = (id, depth, seen) => {
|
|
740
773
|
const own = sinkCallee(id);
|
|
741
774
|
if (own)
|
|
742
|
-
return own;
|
|
775
|
+
return { callee: own, owner: id };
|
|
743
776
|
if (depth === 0)
|
|
744
777
|
return undefined;
|
|
745
778
|
for (const t of fanOutTargets.get(id) ?? []) {
|
|
@@ -752,7 +785,12 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
752
785
|
}
|
|
753
786
|
return undefined;
|
|
754
787
|
};
|
|
755
|
-
const
|
|
788
|
+
const endpointHandlers = new Set(graph.edges
|
|
789
|
+
.filter((e) => e.relationship_type === "IMPLEMENTED_IN" && nodeById.get(e.from_external_id)?.kind === "Endpoint")
|
|
790
|
+
.map((e) => e.to_external_id));
|
|
791
|
+
const frameworkInvoked = (node) => node.properties.cli_entrypoint === true || endpointHandlers.has(node.external_id);
|
|
792
|
+
const sinkHits = new Map(normalizationSymbols.map((s) => [s.external_id, reachedSinkFrom(s.external_id, 2, new Set([s.external_id]))]));
|
|
793
|
+
const sinkReached = new Map([...sinkHits].map(([k, v]) => [k, v?.callee]));
|
|
756
794
|
const reachesSink = new Map([...sinkReached].map(([k, v]) => [k, v !== undefined]));
|
|
757
795
|
const depthCtx = buildFlowDepthContext(graph);
|
|
758
796
|
const staticLinked = staticTestLinkedIds(graph, symbolIds);
|
|
@@ -766,7 +804,13 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
766
804
|
if (node.properties.ranking_exclusion_code === "python_class_with_methods" || node.properties.ranking_exclusion_code === "python_structural_container" || node.properties.ranking_exclusion_reason === "python_structural_container")
|
|
767
805
|
return true;
|
|
768
806
|
const code = node.properties.ranking_exclusion_reason_code;
|
|
769
|
-
if (node.properties.ranking_exclusion_code === "trivial_constructor" ||
|
|
807
|
+
if (node.properties.ranking_exclusion_code === "trivial_constructor" ||
|
|
808
|
+
node.properties.ranking_exclusion_code === "enum_declaration" ||
|
|
809
|
+
node.properties.ranking_exclusion_code === "data_shape_declaration" ||
|
|
810
|
+
code === "python_trivial_thread_local_initializer" ||
|
|
811
|
+
code === "python_trivial_constructor" ||
|
|
812
|
+
code === "python_stdlib_enum_declaration" ||
|
|
813
|
+
code === "python_data_shape_declaration")
|
|
770
814
|
return true;
|
|
771
815
|
return (node.properties.ranking_exclusion_code === "thin_external_delegate" || code === "python_thin_external_delegate")
|
|
772
816
|
&& deriveDataSensitivity(node) <= 1
|
|
@@ -848,12 +892,16 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
848
892
|
const d = raw.d;
|
|
849
893
|
const detectionTier = detectionFor(s.external_id);
|
|
850
894
|
let score = Math.round(pExact * iExact * d * 10) / 10;
|
|
851
|
-
|
|
895
|
+
// A framework-invoked entry (a registered CLI command, or a handler bound to an
|
|
896
|
+
// Endpoint) has no static caller BY CONSTRUCTION; "disconnected" means dead-code
|
|
897
|
+
// shaped, which an entry the framework calls is not. Before per-symbol fan-in,
|
|
898
|
+
// such entries escaped this dampener only through file-split import counts.
|
|
899
|
+
const disconnected = incoming_refs === 0 && fan_out === 0 && !frameworkInvoked(s);
|
|
852
900
|
if (disconnected)
|
|
853
901
|
score = Math.round(score * 0.25 * 10) / 10;
|
|
854
902
|
const reasons = [
|
|
855
903
|
`ORS ${score} ≈ P${p} × I${i} × D${d}`,
|
|
856
|
-
`${incoming_refs} incoming symbol-specific structural reference${incoming_refs === 1 ? "" : "s"}`,
|
|
904
|
+
`${incoming_refs} incoming symbol-specific structural reference${incoming_refs === 1 ? "" : "s"}${incomingBreakdown(s.external_id)}`,
|
|
857
905
|
churnAvailable
|
|
858
906
|
? `${git_churn} git churn line${git_churn === 1 ? "" : "s"} attributed to this symbol in recent history`
|
|
859
907
|
: churnResult.state === "partial"
|
|
@@ -880,7 +928,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
880
928
|
reasons.push(`config override (${override.action}): ${override.reason}`);
|
|
881
929
|
return {
|
|
882
930
|
...(override && override.action !== "suppress" ? { override: { action: override.action, reason: override.reason } } : {}),
|
|
883
|
-
...(
|
|
931
|
+
...(sinkHits.get(s.external_id) ? { sink_callee: sinkHits.get(s.external_id).callee, sink_owner: sinkHits.get(s.external_id).owner } : {}),
|
|
884
932
|
...(raw.scheduledEntry ? { scheduled_entry: true } : {}),
|
|
885
933
|
detection_tier: detectionTier,
|
|
886
934
|
id: s.external_id,
|
|
@@ -1000,7 +1048,7 @@ export function configDisclosureFor(graph, repoRoot) {
|
|
|
1000
1048
|
rankExcludePaths: cfg.classification.rank_exclude_paths,
|
|
1001
1049
|
floor: cfg.tuning.irreversibility_floor,
|
|
1002
1050
|
silence: cfg.tuning.silence_multiplier,
|
|
1003
|
-
tuning: { churn_window_days: cfg.tuning.churn_window_days },
|
|
1051
|
+
tuning: { churn_window_days: cfg.tuning.churn_window_days, churn_max_commits: cfg.tuning.churn_max_commits, churn_timeout_seconds: cfg.tuning.churn_timeout_seconds },
|
|
1004
1052
|
proof: cfg.proof,
|
|
1005
1053
|
classification: {
|
|
1006
1054
|
test_support_paths: cfg.classification.test_support_paths,
|
|
@@ -11,7 +11,7 @@ import { join } from "node:path";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
export const DEFAULT_RISK_CONFIG = {
|
|
13
13
|
classification: { test_support_paths: [], scheduled_entry_paths: [], destructive_sinks: [], sensitivity_ignore: [], rank_exclude_paths: [] },
|
|
14
|
-
tuning: { irreversibility_floor: true, silence_multiplier: true, churn_window_days: 180 },
|
|
14
|
+
tuning: { irreversibility_floor: true, silence_multiplier: true, churn_window_days: 180, churn_max_commits: 20_000, churn_timeout_seconds: 300 },
|
|
15
15
|
proof: { python_runner: "auto", attempt_limit: 20, baseline_green_target: 5 },
|
|
16
16
|
overrides: []
|
|
17
17
|
};
|
|
@@ -51,6 +51,10 @@ function applyFile(cfg, file, warnings, label) {
|
|
|
51
51
|
cfg.tuning.silence_multiplier = tun.silence_multiplier;
|
|
52
52
|
if (tun.churn_window_days !== undefined)
|
|
53
53
|
cfg.tuning.churn_window_days = boundedInt(tun.churn_window_days, cfg.tuning.churn_window_days, 1, 3_650);
|
|
54
|
+
if (tun.churn_max_commits !== undefined)
|
|
55
|
+
cfg.tuning.churn_max_commits = boundedInt(tun.churn_max_commits, cfg.tuning.churn_max_commits, 100, 200_000);
|
|
56
|
+
if (tun.churn_timeout_seconds !== undefined)
|
|
57
|
+
cfg.tuning.churn_timeout_seconds = boundedInt(tun.churn_timeout_seconds, cfg.tuning.churn_timeout_seconds, 10, 1_800);
|
|
54
58
|
const proof = (raw.proof ?? {});
|
|
55
59
|
if (typeof proof.python_runner === "string" && proof.python_runner.trim() !== "") {
|
|
56
60
|
cfg.proof.python_runner = proof.python_runner.trim();
|
|
@@ -178,10 +178,19 @@ function proofGuidance(ledger, summary, dyn) {
|
|
|
178
178
|
// — behaviors, flows, Statically Linked signals — is unaffected; only the dynamic pass is small.
|
|
179
179
|
if (dyn) {
|
|
180
180
|
if (dyn.attempted === 0) {
|
|
181
|
+
const why = dyn.status === "disabled"
|
|
182
|
+
? " Auto-prove was disabled for this run (--no-auto)."
|
|
183
|
+
: dyn.scopedChangedFiles !== undefined
|
|
184
|
+
? ` The pass was scoped to ${dyn.scopedChangedFiles.toLocaleString()} changed file${dyn.scopedChangedFiles === 1 ? "" : "s"} in the working tree, and none had a provable target with a linked test.`
|
|
185
|
+
: dyn.status === "no-targets"
|
|
186
|
+
? " No provable target with a linked existing test was found."
|
|
187
|
+
: dyn.reason
|
|
188
|
+
? ` ${dyn.reason}`
|
|
189
|
+
: "";
|
|
181
190
|
return {
|
|
182
191
|
state: "not_started",
|
|
183
192
|
title: "0 Dynamically Proven — dynamic proof was not attempted",
|
|
184
|
-
body:
|
|
193
|
+
body: `Dynamic proof was not attempted in this run.${why} Static behavior mapping and Statically Linked signals are still available.`,
|
|
185
194
|
action: HANDOFF_ACTION
|
|
186
195
|
};
|
|
187
196
|
}
|
|
@@ -486,7 +495,7 @@ function displayTitle(title, file) {
|
|
|
486
495
|
}
|
|
487
496
|
/** Deterministic 1–2 line behavior context from graph facts only — no LLM.
|
|
488
497
|
* Sensitivity label mirrors deriveDataSensitivity's tiers. */
|
|
489
|
-
export function riskContext(risk, churnWindowDays, churnMeta) {
|
|
498
|
+
export function riskContext(risk, churnWindowDays, churnMeta, sameSinkAs) {
|
|
490
499
|
const sens = (risk.data_sensitivity ?? 1) >= 10 ? "payment/billing-sensitive"
|
|
491
500
|
: (risk.data_sensitivity ?? 1) >= 9 ? "auth/session-sensitive"
|
|
492
501
|
: (risk.data_sensitivity ?? 1) >= 7 ? "order/transaction"
|
|
@@ -507,7 +516,10 @@ export function riskContext(risk, churnWindowDays, churnMeta) {
|
|
|
507
516
|
: "change history unavailable";
|
|
508
517
|
const tier = risk.detection_tier ?? "";
|
|
509
518
|
const evidence = tier === "candidate"
|
|
510
|
-
|
|
519
|
+
// Candidate = a name-level signal only. The analyzer did not credit an asserted
|
|
520
|
+
// call; it cannot claim the test never calls the code (stored-result asserts and
|
|
521
|
+
// pytest.raises are not yet credited for Python).
|
|
522
|
+
? "no confirmed test link (a similarly-named test exists, but no asserted call to it was recognized)"
|
|
511
523
|
: tier === "associated" ? "a test calls it but nothing proves it fails when broken" : "no test links here";
|
|
512
524
|
const sinkShort = sink ? sink.split(".").pop() ?? sink : "";
|
|
513
525
|
// Line 1 — the consequence, in plain English, from the signals only.
|
|
@@ -524,6 +536,7 @@ export function riskContext(risk, churnWindowDays, churnMeta) {
|
|
|
524
536
|
const seen = [
|
|
525
537
|
pos + (sens ? ` on ${sens} paths` : ""),
|
|
526
538
|
...(sink ? [`reaches \`${sink}\` within two calls`] : []),
|
|
539
|
+
...(sink && sameSinkAs ? [`same delete call as \`${sameSinkAs}\` (ranked higher)`] : []),
|
|
527
540
|
...(scheduled ? ["scheduled / queue-triggered"] : []),
|
|
528
541
|
`${risk.fan_out ?? 0} downstream call${(risk.fan_out ?? 0) === 1 ? "" : "s"}`,
|
|
529
542
|
churn,
|
|
@@ -941,6 +954,12 @@ function riskRows(risks, graph, churnWindowDays, churnMeta) {
|
|
|
941
954
|
for (const r of risks)
|
|
942
955
|
if (!firstRowForFile.has(r.file))
|
|
943
956
|
firstRowForFile.set(r.file, r.id);
|
|
957
|
+
// R8.10 (display only): rows that reach the SAME destructive call site through
|
|
958
|
+
// forwarding wrappers name the higher-ranked row. Order and membership unchanged.
|
|
959
|
+
const firstRowForSink = new Map();
|
|
960
|
+
for (const r of risks)
|
|
961
|
+
if (r.sink_owner && !firstRowForSink.has(r.sink_owner))
|
|
962
|
+
firstRowForSink.set(r.sink_owner, { id: r.id, title: r.title });
|
|
944
963
|
// Ambiguity-qualified display: identical titles from DIFFERENT files
|
|
945
964
|
// (multi-program repos: 76 x main) get their top-level dir as a prefix.
|
|
946
965
|
// Purely display; single-file titles render unchanged everywhere else.
|
|
@@ -990,7 +1009,9 @@ function riskRows(risks, graph, churnWindowDays, churnMeta) {
|
|
|
990
1009
|
todo: riskTodo(risk, verb, path, generatedTests)
|
|
991
1010
|
};
|
|
992
1011
|
})(),
|
|
993
|
-
context: riskContext(risk, churnWindowDays, churnMeta)
|
|
1012
|
+
context: riskContext(risk, churnWindowDays, churnMeta, risk.sink_owner && firstRowForSink.get(risk.sink_owner) && firstRowForSink.get(risk.sink_owner).id !== risk.id
|
|
1013
|
+
? firstRowForSink.get(risk.sink_owner).title
|
|
1014
|
+
: undefined),
|
|
994
1015
|
desc: risk.reasons.join(" · "),
|
|
995
1016
|
tags
|
|
996
1017
|
};
|
|
@@ -1040,7 +1061,16 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
1040
1061
|
provenIds,
|
|
1041
1062
|
includeAssociated: true
|
|
1042
1063
|
}).filter((r) => r.sink_callee).slice(0, 20)
|
|
1043
|
-
.map((r
|
|
1064
|
+
.map((r, _index, rows) => {
|
|
1065
|
+
const first = rows.find((x) => x.sink_owner && x.sink_owner === r.sink_owner);
|
|
1066
|
+
return {
|
|
1067
|
+
path: r.title,
|
|
1068
|
+
file: r.file,
|
|
1069
|
+
score: r.risk_score,
|
|
1070
|
+
sink: r.sink_callee ?? "",
|
|
1071
|
+
...(first && first.id !== r.id ? { sameSinkAs: first.title } : {})
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1044
1074
|
const riskHealth = inspectRiskInputHealth(repoRoot);
|
|
1045
1075
|
const codeFiles = Object.entries(graph.manifest.files).filter(([, file]) => file.kind === "code").map(([file]) => file);
|
|
1046
1076
|
const churnMeta = inspectRiskChurn(repoRoot, codeFiles, riskHealth.churnWindow);
|
|
@@ -1060,13 +1060,13 @@ renderRisks();
|
|
|
1060
1060
|
const P=C.proof||{};
|
|
1061
1061
|
const T=C.tuning||{};
|
|
1062
1062
|
const clsSet=["test_support_paths","scheduled_entry_paths","destructive_sinks","sensitivity_ignore"].filter(k=>(K[k]||[]).length>0);
|
|
1063
|
-
const tuned=C.overridesActive>0||(C.rankExcludePaths||[]).length>0||!C.floor||!C.silence||clsSet.length>0||(T.churn_window_days??180)!==180||P.python_runner!=="auto"||P.attempt_limit!==20||P.baseline_green_target!==5;
|
|
1063
|
+
const tuned=C.overridesActive>0||(C.rankExcludePaths||[]).length>0||!C.floor||!C.silence||clsSet.length>0||(T.churn_window_days??180)!==180||(T.churn_max_commits??20000)!==20000||(T.churn_timeout_seconds??300)!==300||P.python_runner!=="auto"||P.attempt_limit!==20||P.baseline_green_target!==5;
|
|
1064
1064
|
const bits=["config <b>"+esc(C.hash)+"</b>","overrides active <b>"+C.overridesActive+"</b>","suppressed <b>"+(C.suppressed||[]).length+"</b>"];
|
|
1065
1065
|
if((C.rankExcludePaths||[]).length)bits.push("ranking excludes <b>"+esc(C.rankExcludePaths.join(", "))+"</b>");
|
|
1066
1066
|
if(!C.floor)bits.push("<b>irreversibility floor OFF</b>");
|
|
1067
1067
|
if(!C.silence)bits.push("<b>silence multiplier OFF</b>");
|
|
1068
1068
|
let html="<div>"+bits.join(" · ")+"</div>";
|
|
1069
|
-
html+='<span class="cfg-row">Churn window: <b>'+esc(String(T.churn_window_days??180))+' days</b></span>';
|
|
1069
|
+
html+='<span class="cfg-row">Churn window: <b>'+esc(String(T.churn_window_days??180))+' days</b> · history bound <b>'+esc(String(T.churn_max_commits??20000))+' commits / '+esc(String(T.churn_timeout_seconds??300))+' s</b></span>';
|
|
1070
1070
|
html+='<span class="cfg-row">Python proof: runner <b>'+esc(P.python_runner||"auto")+'</b> · attempt limit <b>'+esc(String(P.attempt_limit??20))+'</b> · green baseline target <b>'+esc(String(P.baseline_green_target??5))+'</b></span>';
|
|
1071
1071
|
clsSet.forEach(k=>{html+='<span class="cfg-row">'+esc(k)+': <b>'+esc(K[k].join(", "))+'</b></span>';});
|
|
1072
1072
|
if((C.sensitivityIgnored||[]).length)html+='<span class="cfg-row">sensitivity ignored by config (score changed): <b>'+esc(C.sensitivityIgnored.join(", "))+'</b></span>';
|
|
@@ -1086,7 +1086,7 @@ renderRisks();
|
|
|
1086
1086
|
return \`<div class="wl-row" data-path="\${esc(path).replace(/"/g,'"')}" title="\${inTop?'in the ranked list below — click to jump':'ranked, but below the top-20 cut'}"><span class="wl-path">\${esc(path)}</span><span class="wl-meta">\${esc(meta)}</span></div>\`;
|
|
1087
1087
|
};
|
|
1088
1088
|
const cf=(W.changeFrontier||[]).map(r=>row(r.path,"changes "+(r.probability>=7?"a lot":r.probability>=4?"often":"some"))).join("")||'<div class="wl-empty">nothing changing fast and unproven</div>';
|
|
1089
|
-
const ir=(W.irreversible||[]).map(r=>row(r.path,"→ "+(r.sink||"").split(".").pop())).join("")||'<div class="wl-empty">no unproven path reaches a delete</div>';
|
|
1089
|
+
const ir=(W.irreversible||[]).map(r=>row(r.path,"→ "+(r.sink||"").split(".").pop()+(r.sameSinkAs?" · same delete as "+r.sameSinkAs:""))).join("")||'<div class="wl-empty">no unproven path reaches a delete</div>';
|
|
1090
1090
|
host.innerHTML=\`<div class="wl-grid">
|
|
1091
1091
|
<div class="wl-card"><p class="wl-title wl-change">Changing fast · unproven</p><p class="wl-sub">Where the code moves most with nothing proving it. The place a bug is most likely to have just arrived.</p>\${cf}</div>
|
|
1092
1092
|
<div class="wl-card"><p class="wl-title wl-irrev">Can destroy data · unproven</p><p class="wl-sub">Paths that reach a delete or purge with nothing proving they do the right thing. Rarely changing — which is why nobody looks.</p>\${ir}</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.47",
|
|
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",
|
|
@@ -646,8 +646,33 @@ function copyRuntimeAliasTarget(absTarget, dest, isFile) {
|
|
|
646
646
|
});
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
-
|
|
649
|
+
// A failed copy (e.g. ENOSPC) must not leave a partial multi-GB sandbox behind:
|
|
650
|
+
// remove this copy's temp dir before rethrowing.
|
|
651
|
+
function copyFixtureRoot(root, label, options) {
|
|
650
652
|
const tmpRoot = mkdtempSync(path.join(tmpdir(), `opro-dynamic-proof-${label}-`));
|
|
653
|
+
try {
|
|
654
|
+
return populateFixtureRoot(tmpRoot, root, options);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Directories that are never JS test inputs but can be very large when the analyzed
|
|
662
|
+
// root is a polyglot repo: Python environments and caches, and Rust build output
|
|
663
|
+
// (`target/` next to a Cargo.toml). Copying them made each sandbox several GB.
|
|
664
|
+
const NON_JS_HEAVY_DIRS = new Set([".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".tox"]);
|
|
665
|
+
function isNonJsHeavyDir(source, name) {
|
|
666
|
+
if (!NON_JS_HEAVY_DIRS.has(name) && name !== "target") {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
if (!lstatSync(source).isDirectory()) {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
return name !== "target" || existsSync(path.join(path.dirname(source), "Cargo.toml"));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function populateFixtureRoot(tmpRoot, root, { linkNodeModules, workspaceRoot }) {
|
|
651
676
|
const monoRoot = path.join(tmpRoot, "mono");
|
|
652
677
|
// On any policy violation, mirror nothing → package lands alone at tmpRoot/mono with its
|
|
653
678
|
// dangling extends / unresolved alias → baseline fails to transform/resolve → honest
|
|
@@ -730,6 +755,9 @@ function copyFixtureRoot(root, label, { linkNodeModules, workspaceRoot }) {
|
|
|
730
755
|
if (source !== root && lstatSync(source).isSymbolicLink()) {
|
|
731
756
|
return false;
|
|
732
757
|
}
|
|
758
|
+
if (source !== root && isNonJsHeavyDir(source, name)) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
733
761
|
return name !== "node_modules" && name !== ".git" && name !== ".orangepro";
|
|
734
762
|
}
|
|
735
763
|
});
|
|
@@ -2283,7 +2311,13 @@ function main() {
|
|
|
2283
2311
|
const mochaBin = args.mochaBin ? path.resolve(args.mochaBin) : defaultMochaBin(root, workspaceRoot);
|
|
2284
2312
|
|
|
2285
2313
|
const baselineCopy = copyFixtureRoot(root, "baseline", { linkNodeModules: args.linkNodeModules, workspaceRoot });
|
|
2286
|
-
|
|
2314
|
+
let mutantCopy;
|
|
2315
|
+
try {
|
|
2316
|
+
mutantCopy = copyFixtureRoot(root, "mutant", { linkNodeModules: args.linkNodeModules, workspaceRoot });
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
rmSync(baselineCopy.tmpRoot, { recursive: true, force: true });
|
|
2319
|
+
throw error;
|
|
2320
|
+
}
|
|
2287
2321
|
try {
|
|
2288
2322
|
const baseline = runTest({ runner, repoRoot: baselineCopy.repoRoot, monoRoot: baselineCopy.monoRoot, testRel, vitestBin, jestBin, mochaBin, vitestConfigRel, jestConfigRel, timeoutMs, testEnv, aliases: baselineCopy.aliases });
|
|
2289
2323
|
mutateMethod(path.join(mutantCopy.repoRoot, targetRel), args.method, replacementBody);
|
|
@@ -126,16 +126,22 @@ function resolveInside(root, relOrAbs) {
|
|
|
126
126
|
function copyModuleRoot(root, label) {
|
|
127
127
|
const tmpRoot = mkdtempSync(path.join(tmpdir(), `opro-go-proof-${label}-`));
|
|
128
128
|
const repoRoot = path.join(tmpRoot, "module");
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
try {
|
|
130
|
+
cpSync(root, repoRoot, {
|
|
131
|
+
recursive: true,
|
|
132
|
+
filter(source) {
|
|
133
|
+
const name = path.basename(source);
|
|
134
|
+
if (source !== root && lstatSync(source).isSymbolicLink()) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return name !== "node_modules" && name !== ".git" && name !== ".orangepro";
|
|
135
138
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// A failed copy (e.g. ENOSPC) must not leave a partial sandbox behind.
|
|
142
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
139
145
|
return { tmpRoot, repoRoot };
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -558,7 +564,13 @@ function main() {
|
|
|
558
564
|
const pkgPath = pkgDirRel === "." ? "./" : `./${pkgDirRel.split(path.sep).join("/")}`;
|
|
559
565
|
|
|
560
566
|
const baselineCopy = copyModuleRoot(root, "baseline");
|
|
561
|
-
|
|
567
|
+
let mutantCopy;
|
|
568
|
+
try {
|
|
569
|
+
mutantCopy = copyModuleRoot(root, "mutant");
|
|
570
|
+
} catch (error) {
|
|
571
|
+
rmSync(baselineCopy.tmpRoot, { recursive: true, force: true });
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
562
574
|
// The mutant sandbox's package directory: Go prints failure-frame files as
|
|
563
575
|
// basenames relative to it, so FIX 2 resolves the test source there.
|
|
564
576
|
const pkgDirAbs = path.join(mutantCopy.repoRoot, path.dirname(targetRel));
|
|
@@ -106,16 +106,22 @@ function resolveInside(root, relOrAbs) {
|
|
|
106
106
|
function copyModuleRoot(root, label) {
|
|
107
107
|
const tmpRoot = mkdtempSync(path.join(tmpdir(), `opro-java-proof-${label}-`));
|
|
108
108
|
const repoRoot = path.join(tmpRoot, "module");
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
try {
|
|
110
|
+
cpSync(root, repoRoot, {
|
|
111
|
+
recursive: true,
|
|
112
|
+
filter(source) {
|
|
113
|
+
const name = path.basename(source);
|
|
114
|
+
if (source !== root && lstatSync(source).isSymbolicLink()) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return name !== "node_modules" && name !== ".git" && name !== ".orangepro" && name !== "target";
|
|
115
118
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// A failed copy (e.g. ENOSPC) must not leave a partial sandbox behind.
|
|
122
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
119
125
|
return { tmpRoot, repoRoot };
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -506,7 +512,13 @@ function main() {
|
|
|
506
512
|
const timeoutMs = parseTimeoutMs(args.timeoutMs);
|
|
507
513
|
|
|
508
514
|
const baselineCopy = copyModuleRoot(root, "baseline");
|
|
509
|
-
|
|
515
|
+
let mutantCopy;
|
|
516
|
+
try {
|
|
517
|
+
mutantCopy = copyModuleRoot(root, "mutant");
|
|
518
|
+
} catch (error) {
|
|
519
|
+
rmSync(baselineCopy.tmpRoot, { recursive: true, force: true });
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
510
522
|
try {
|
|
511
523
|
const baseline = runSurefire({
|
|
512
524
|
repoRoot: baselineCopy.repoRoot,
|