@orangepro/orangepro-mcp 0.2.45 → 0.2.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/local/analyze/analyzer.js +5 -1
- package/dist/local/analyze/treeSitter/engine.js +86 -18
- package/dist/local/operations.js +17 -2
- 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 +31 -4
- package/dist/local/viz/behaviorReportHtml.js +3 -3
- package/package.json +1 -1
|
@@ -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/operations.js
CHANGED
|
@@ -1690,7 +1690,8 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1690
1690
|
: "auto-prove: driving generate → prove on the top provable targets", { current: 6, total: 8 });
|
|
1691
1691
|
let autoProveResult;
|
|
1692
1692
|
try {
|
|
1693
|
-
|
|
1693
|
+
const scopedFiles = autoProveChangedScope(loadGraph(workspacePaths(root).graphPath), changed, opts.baseRef);
|
|
1694
|
+
const runAutoProve = (changedFiles) => autoProve(root, {
|
|
1694
1695
|
autoLimit: opts.proofLimit ?? opts.autoLimit,
|
|
1695
1696
|
proof_existing_limit: opts.proofLimit ?? opts.autoLimit ?? loadedRiskConfig.config.proof.attempt_limit,
|
|
1696
1697
|
proof_gen_limit: loadedRiskConfig.config.proof.baseline_green_target,
|
|
@@ -1698,8 +1699,19 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1698
1699
|
provider: providerOpts.provider,
|
|
1699
1700
|
model: providerOpts.model,
|
|
1700
1701
|
prompt_version: opts.promptVersion,
|
|
1701
|
-
changedFiles
|
|
1702
|
+
changedFiles
|
|
1702
1703
|
}, { ...providerDeps, proveLoop: opProveLoop });
|
|
1704
|
+
autoProveResult = await runAutoProve(scopedFiles);
|
|
1705
|
+
if (scopedFiles && autoProveResult.attempted === 0 && !opts.baseRef && !opts.noAuto) {
|
|
1706
|
+
// An uncommitted working tree implicitly scoped the pass, but nothing in that
|
|
1707
|
+
// scope was provable. Fall back to the global top targets instead of reporting
|
|
1708
|
+
// "not attempted" for a reason the user cannot see. --base keeps strict scoping.
|
|
1709
|
+
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.`);
|
|
1710
|
+
autoProveResult = await runAutoProve(undefined);
|
|
1711
|
+
}
|
|
1712
|
+
else if (scopedFiles) {
|
|
1713
|
+
autoProveResult = { ...autoProveResult, scoped_changed_files: scopedFiles.length };
|
|
1714
|
+
}
|
|
1703
1715
|
for (const skip of autoProveResult.skipped)
|
|
1704
1716
|
warnings.push(`auto-prove skipped ${skip.target_symbol ?? skip.title}: ${skip.reason}`);
|
|
1705
1717
|
}
|
|
@@ -1944,6 +1956,9 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1944
1956
|
const dynForReport = {
|
|
1945
1957
|
attempted: autoProveResult.attempted,
|
|
1946
1958
|
proven: autoProveResult.proven,
|
|
1959
|
+
status: autoProveResult.status,
|
|
1960
|
+
reason: autoProveResult.reason,
|
|
1961
|
+
...(autoProveResult.scoped_changed_files !== undefined ? { scopedChangedFiles: autoProveResult.scoped_changed_files } : {}),
|
|
1947
1962
|
needsSetup: autoProveResult.needs_setup
|
|
1948
1963
|
.filter((a) => !a.deduped)
|
|
1949
1964
|
.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"
|
|
@@ -524,6 +533,7 @@ export function riskContext(risk, churnWindowDays, churnMeta) {
|
|
|
524
533
|
const seen = [
|
|
525
534
|
pos + (sens ? ` on ${sens} paths` : ""),
|
|
526
535
|
...(sink ? [`reaches \`${sink}\` within two calls`] : []),
|
|
536
|
+
...(sink && sameSinkAs ? [`same delete call as \`${sameSinkAs}\` (ranked higher)`] : []),
|
|
527
537
|
...(scheduled ? ["scheduled / queue-triggered"] : []),
|
|
528
538
|
`${risk.fan_out ?? 0} downstream call${(risk.fan_out ?? 0) === 1 ? "" : "s"}`,
|
|
529
539
|
churn,
|
|
@@ -941,6 +951,12 @@ function riskRows(risks, graph, churnWindowDays, churnMeta) {
|
|
|
941
951
|
for (const r of risks)
|
|
942
952
|
if (!firstRowForFile.has(r.file))
|
|
943
953
|
firstRowForFile.set(r.file, r.id);
|
|
954
|
+
// R8.10 (display only): rows that reach the SAME destructive call site through
|
|
955
|
+
// forwarding wrappers name the higher-ranked row. Order and membership unchanged.
|
|
956
|
+
const firstRowForSink = new Map();
|
|
957
|
+
for (const r of risks)
|
|
958
|
+
if (r.sink_owner && !firstRowForSink.has(r.sink_owner))
|
|
959
|
+
firstRowForSink.set(r.sink_owner, { id: r.id, title: r.title });
|
|
944
960
|
// Ambiguity-qualified display: identical titles from DIFFERENT files
|
|
945
961
|
// (multi-program repos: 76 x main) get their top-level dir as a prefix.
|
|
946
962
|
// Purely display; single-file titles render unchanged everywhere else.
|
|
@@ -990,7 +1006,9 @@ function riskRows(risks, graph, churnWindowDays, churnMeta) {
|
|
|
990
1006
|
todo: riskTodo(risk, verb, path, generatedTests)
|
|
991
1007
|
};
|
|
992
1008
|
})(),
|
|
993
|
-
context: riskContext(risk, churnWindowDays, churnMeta)
|
|
1009
|
+
context: riskContext(risk, churnWindowDays, churnMeta, risk.sink_owner && firstRowForSink.get(risk.sink_owner) && firstRowForSink.get(risk.sink_owner).id !== risk.id
|
|
1010
|
+
? firstRowForSink.get(risk.sink_owner).title
|
|
1011
|
+
: undefined),
|
|
994
1012
|
desc: risk.reasons.join(" · "),
|
|
995
1013
|
tags
|
|
996
1014
|
};
|
|
@@ -1040,7 +1058,16 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
1040
1058
|
provenIds,
|
|
1041
1059
|
includeAssociated: true
|
|
1042
1060
|
}).filter((r) => r.sink_callee).slice(0, 20)
|
|
1043
|
-
.map((r
|
|
1061
|
+
.map((r, _index, rows) => {
|
|
1062
|
+
const first = rows.find((x) => x.sink_owner && x.sink_owner === r.sink_owner);
|
|
1063
|
+
return {
|
|
1064
|
+
path: r.title,
|
|
1065
|
+
file: r.file,
|
|
1066
|
+
score: r.risk_score,
|
|
1067
|
+
sink: r.sink_callee ?? "",
|
|
1068
|
+
...(first && first.id !== r.id ? { sameSinkAs: first.title } : {})
|
|
1069
|
+
};
|
|
1070
|
+
});
|
|
1044
1071
|
const riskHealth = inspectRiskInputHealth(repoRoot);
|
|
1045
1072
|
const codeFiles = Object.entries(graph.manifest.files).filter(([, file]) => file.kind === "code").map(([file]) => file);
|
|
1046
1073
|
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.46",
|
|
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",
|