@orangepro/orangepro-mcp 0.2.31 → 0.2.33
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/generate/draftGuidance.js +1 -1
- package/dist/local/generate/generator.js +70 -45
- package/dist/local/generate/promptV5.js +2 -2
- package/dist/local/operations.js +49 -10
- package/dist/local/score/risk.js +8 -0
- package/dist/local/viz/behaviorReportData.js +18 -19
- package/dist/local/viz/behaviorReportHtml.js +11 -7
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ export function classifyGeneratedDraftBlocker(reason) {
|
|
|
10
10
|
if (/no required module provides package|cannot find module|module not found|unresolved import|imports module-path package/i.test(text)) {
|
|
11
11
|
return "unresolved_import";
|
|
12
12
|
}
|
|
13
|
-
if (/syntax check failed|undefined:|unknown field|redeclared in this block|expected (?:declaration|operand|'[^']+'|"[^"]+"|[^ ]+),? found|literal not terminated|cannot assign/i.test(text))
|
|
13
|
+
if (/syntax check failed|undefined:|unknown field|redeclared in this block|expected (?:declaration|operand|'[^']+'|"[^"]+"|[^ ]+),? found|literal not terminated|cannot assign|cannot use .+ as |missing ['",].+argument list/i.test(text))
|
|
14
14
|
return "generated_code";
|
|
15
15
|
return "unknown";
|
|
16
16
|
}
|
|
@@ -1515,7 +1515,13 @@ function commandAvailable(command) {
|
|
|
1515
1515
|
}
|
|
1516
1516
|
}
|
|
1517
1517
|
function shortStaticDiag(message) {
|
|
1518
|
-
|
|
1518
|
+
const compact = message.replace(/\s+/g, " ").trim();
|
|
1519
|
+
if (compact.length <= 240)
|
|
1520
|
+
return compact;
|
|
1521
|
+
// Compiler output starts with package/build boilerplate and puts the useful
|
|
1522
|
+
// file:line diagnostic at the end. Preserve both ends so classification and
|
|
1523
|
+
// remediation do not degrade to "unknown" on long package names.
|
|
1524
|
+
return `${compact.slice(0, 72)} … ${compact.slice(-165)}`;
|
|
1519
1525
|
}
|
|
1520
1526
|
function escapeRegExp(value) {
|
|
1521
1527
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -2158,6 +2164,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2158
2164
|
const framework = pickFramework(graph, opts, targets, fileReader);
|
|
2159
2165
|
const runSelection = targetsForFramework(graph, targets, framework);
|
|
2160
2166
|
let runTargets = runSelection.targets;
|
|
2167
|
+
const explicitMulti = Boolean(opts.target_ids && opts.target_ids.length > 1);
|
|
2161
2168
|
warnings.push(...runSelection.warnings);
|
|
2162
2169
|
const systemPrompt = opts.systemPrompt ?? buildSystemPrompt();
|
|
2163
2170
|
const promptVersion = inputMode === "graph_grounded" && opts.prompt_version === "v5" ? PROMPT_VERSION_V5 : PROMPT_VERSION;
|
|
@@ -2275,7 +2282,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2275
2282
|
}
|
|
2276
2283
|
else if (opts.prompt_version === "v5") {
|
|
2277
2284
|
const declaredDeps = readDeclaredDeps(graph.workspace.root);
|
|
2278
|
-
for (
|
|
2285
|
+
for (let targetIndex = 0; targetIndex < runTargets.length; targetIndex++) {
|
|
2286
|
+
const behavior = runTargets[targetIndex];
|
|
2279
2287
|
if (generated.length >= limit)
|
|
2280
2288
|
break;
|
|
2281
2289
|
const gc = gatherContext(graph, behavior, framework, fileReader);
|
|
@@ -2304,7 +2312,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2304
2312
|
continue;
|
|
2305
2313
|
}
|
|
2306
2314
|
try {
|
|
2307
|
-
const result = parsePlannedScenariosStrict(rawPlan,
|
|
2315
|
+
const result = parsePlannedScenariosStrict(rawPlan, 2);
|
|
2308
2316
|
scenarios = result.scenarios;
|
|
2309
2317
|
if (result.dropped > 0) {
|
|
2310
2318
|
warnings.push(`Dropped ${result.dropped} invalid v5 planned scenario(s) for "${gc.ctx.behavior_title}": ${result.dropSummary.join("; ")}.`);
|
|
@@ -2335,7 +2343,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2335
2343
|
maxTokens: 1600,
|
|
2336
2344
|
temperature: 0
|
|
2337
2345
|
});
|
|
2338
|
-
const result = parsePlannedScenariosStrict(repaired,
|
|
2346
|
+
const result = parsePlannedScenariosStrict(repaired, 2);
|
|
2339
2347
|
// Keep ONLY repaired scenarios that tie back to the ORIGINAL malformed text; drop any the
|
|
2340
2348
|
// model invented. If none tie back, fail closed — never generate from an invented plan.
|
|
2341
2349
|
const tiedBack = result.scenarios.filter((s) => scenarioTiesBackToRaw(s, rawPlan));
|
|
@@ -2378,7 +2386,43 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2378
2386
|
});
|
|
2379
2387
|
continue;
|
|
2380
2388
|
}
|
|
2381
|
-
const
|
|
2389
|
+
const remainingSlots = Math.max(1, limit - generated.length);
|
|
2390
|
+
const remainingTargets = Math.max(1, runTargets.length - targetIndex);
|
|
2391
|
+
// In explicit multi-target mode, reserve a fair share for every remaining
|
|
2392
|
+
// target. Previously the first behavior could consume the entire batch
|
|
2393
|
+
// with several scenarios, leaving later high-risk behaviors untouched.
|
|
2394
|
+
const targetLimit = explicitMulti ? Math.max(1, Math.floor(remainingSlots / remainingTargets)) : remainingSlots;
|
|
2395
|
+
const selected = scenarios.slice(0, targetLimit);
|
|
2396
|
+
const manualDraftForScenario = (scenario, reason) => {
|
|
2397
|
+
const manualBody = sanitizeGeneratedBody([
|
|
2398
|
+
`Scenario: ${scenario.title}`,
|
|
2399
|
+
...(scenario.steps && scenario.steps.length
|
|
2400
|
+
? ["Steps:", ...scenario.steps.map((st, n) => ` ${n + 1}. ${st}`)]
|
|
2401
|
+
: []),
|
|
2402
|
+
...(scenario.test_data ? [`Test data: ${scenario.test_data}`] : []),
|
|
2403
|
+
...(scenario.assertion_targets.length ? [`Expected: ${scenario.assertion_targets.join("; ")}`] : []),
|
|
2404
|
+
...(scenario.rationale ? [`Why this test: ${scenario.rationale}`] : []),
|
|
2405
|
+
"",
|
|
2406
|
+
`Blocked by: ${reason.split(" — ")[0]}`,
|
|
2407
|
+
`Fix: ${generatedDraftRemediation(reason)}`
|
|
2408
|
+
].join("\n"), gc.ctx.source_excerpts, "//").body;
|
|
2409
|
+
return {
|
|
2410
|
+
id: `${run_id}-t${generated.length + 1}`,
|
|
2411
|
+
run_id,
|
|
2412
|
+
title: `${gc.ctx.behavior_title} — ${scenario.title}`,
|
|
2413
|
+
test_type: gc.ctx.test_layer,
|
|
2414
|
+
framework_hint: framework,
|
|
2415
|
+
body: manualBody,
|
|
2416
|
+
bucket: bucketForV5Scenario(scenario),
|
|
2417
|
+
prompt_version: PROMPT_VERSION_V5,
|
|
2418
|
+
grounding: { entity_ids: gc.entityIds, source_refs: [], weak_relationships_used: [] },
|
|
2419
|
+
weak_evidence_used: false,
|
|
2420
|
+
target_symbol_external_id: behavior.external_id,
|
|
2421
|
+
...(fingerprintOf(behavior) ? { target_fingerprint: fingerprintOf(behavior) } : {}),
|
|
2422
|
+
runnable: false,
|
|
2423
|
+
unresolved_reason: reason
|
|
2424
|
+
};
|
|
2425
|
+
};
|
|
2382
2426
|
const completions = [];
|
|
2383
2427
|
try {
|
|
2384
2428
|
reportProgress(`Generating "${gc.ctx.behavior_title}" [v5 batch: ${selected.length}]…`);
|
|
@@ -2408,6 +2452,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2408
2452
|
const scenarioById = new Map(selected.map((s) => [s.id, s]));
|
|
2409
2453
|
const parsed = completions.flatMap(parseBatchGeneratedTests);
|
|
2410
2454
|
const seenScenarioIds = new Set();
|
|
2455
|
+
const emittedScenarioIds = new Set();
|
|
2456
|
+
const failureReasonByScenarioId = new Map();
|
|
2411
2457
|
const relatedFiles = relatedFilePaths(graph, behavior).files;
|
|
2412
2458
|
for (let i = 0; i < parsed.length && generated.length < limit; i++) {
|
|
2413
2459
|
const parsedTest = parsed[i];
|
|
@@ -2441,23 +2487,27 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2441
2487
|
warnings.push(`Redacted ${sanitized.redactedLines} echoed source-excerpt line(s) from the v5 generated test for "${gc.ctx.behavior_title}".`);
|
|
2442
2488
|
}
|
|
2443
2489
|
if (!hasExecutableContent(sanitized.body, framework)) {
|
|
2490
|
+
const reason = `V5 generated no executable code for scenario "${scenario.title}".`;
|
|
2444
2491
|
warnings.push(`Dropped v5 generated test for "${gc.ctx.behavior_title}" / "${scenario.title}": no executable test code.`);
|
|
2445
2492
|
missing.push({
|
|
2446
2493
|
external_id: behavior.external_id,
|
|
2447
2494
|
title: gc.ctx.behavior_title,
|
|
2448
|
-
reason
|
|
2495
|
+
reason,
|
|
2449
2496
|
needed: ["a non-empty runnable test body"]
|
|
2450
2497
|
});
|
|
2498
|
+
failureReasonByScenarioId.set(scenario.id, reason);
|
|
2451
2499
|
continue;
|
|
2452
2500
|
}
|
|
2453
2501
|
if (!generatedBodyAlignsWithScenario(sanitized.body, scenario)) {
|
|
2502
|
+
const reason = `V5 generated test did not align with scenario "${scenario.title}".`;
|
|
2454
2503
|
warnings.push(`Dropped v5 generated test for "${gc.ctx.behavior_title}" / "${scenario.title}": body did not reference the planned assertion target.`);
|
|
2455
2504
|
missing.push({
|
|
2456
2505
|
external_id: behavior.external_id,
|
|
2457
2506
|
title: gc.ctx.behavior_title,
|
|
2458
|
-
reason
|
|
2507
|
+
reason,
|
|
2459
2508
|
needed: ["a generated test body that asserts the planned scenario target"]
|
|
2460
2509
|
});
|
|
2510
|
+
failureReasonByScenarioId.set(scenario.id, reason);
|
|
2461
2511
|
continue;
|
|
2462
2512
|
}
|
|
2463
2513
|
let cleanBody = sanitized.body;
|
|
@@ -2526,43 +2576,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2526
2576
|
reason,
|
|
2527
2577
|
needed: ["a compiling generated test with a real assertion and resolvable subject import"]
|
|
2528
2578
|
});
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
// excerpts — scrub the composed body with the same guard as code.
|
|
2532
|
-
// The scenario plan (title / assertion targets / rationale) is the
|
|
2533
|
-
// reviewable half of the draft; withholding the body entirely also
|
|
2534
|
-
// removes any residual source-echo risk. runnable:false + the reason
|
|
2535
|
-
// keep this honestly a draft — it ships with no run command and can
|
|
2536
|
-
// never enter the proof-ready set.
|
|
2537
|
-
const manualBody = sanitizeGeneratedBody([
|
|
2538
|
-
`Scenario: ${scenario.title}`,
|
|
2539
|
-
...(scenario.steps && scenario.steps.length
|
|
2540
|
-
? ["Steps:", ...scenario.steps.map((st, n) => ` ${n + 1}. ${st}`)]
|
|
2541
|
-
: []),
|
|
2542
|
-
...(scenario.test_data ? [`Test data: ${scenario.test_data}`] : []),
|
|
2543
|
-
...(scenario.assertion_targets.length ? [`Expected: ${scenario.assertion_targets.join("; ")}`] : []),
|
|
2544
|
-
...(scenario.rationale ? [`Why this test: ${scenario.rationale}`] : []),
|
|
2545
|
-
"",
|
|
2546
|
-
// Concise blocker: first clause only — the full remedy is one line.
|
|
2547
|
-
`Blocked by: ${reason.split(" — ")[0]}`,
|
|
2548
|
-
`Fix: ${generatedDraftRemediation(reason)}`
|
|
2549
|
-
].join("\n"), gc.ctx.source_excerpts, "//").body;
|
|
2550
|
-
generated.push({
|
|
2551
|
-
id: `${run_id}-t${generated.length + 1}`,
|
|
2552
|
-
run_id,
|
|
2553
|
-
title: `${gc.ctx.behavior_title} — ${scenario.title}`,
|
|
2554
|
-
test_type: gc.ctx.test_layer,
|
|
2555
|
-
framework_hint: framework,
|
|
2556
|
-
body: manualBody,
|
|
2557
|
-
bucket: bucketForV5Scenario(scenario),
|
|
2558
|
-
prompt_version: PROMPT_VERSION_V5,
|
|
2559
|
-
grounding: { entity_ids: gc.entityIds, source_refs: [], weak_relationships_used: [] },
|
|
2560
|
-
weak_evidence_used: false,
|
|
2561
|
-
target_symbol_external_id: behavior.external_id,
|
|
2562
|
-
...(fingerprintOf(behavior) ? { target_fingerprint: fingerprintOf(behavior) } : {}),
|
|
2563
|
-
runnable: false,
|
|
2564
|
-
unresolved_reason: reason
|
|
2565
|
-
});
|
|
2579
|
+
generated.push(manualDraftForScenario(scenario, reason));
|
|
2580
|
+
emittedScenarioIds.add(scenario.id);
|
|
2566
2581
|
continue;
|
|
2567
2582
|
}
|
|
2568
2583
|
generated.push({
|
|
@@ -2586,12 +2601,22 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2586
2601
|
: {}),
|
|
2587
2602
|
runnable: true
|
|
2588
2603
|
});
|
|
2604
|
+
emittedScenarioIds.add(scenario.id);
|
|
2605
|
+
}
|
|
2606
|
+
// A valid plan is still useful when code generation is empty, malformed,
|
|
2607
|
+
// or rejected. Retain that grounded scenario as an honest manual test so
|
|
2608
|
+
// the report never loses the test idea merely because code was unusable.
|
|
2609
|
+
for (const scenario of selected) {
|
|
2610
|
+
if (generated.length >= limit || emittedScenarioIds.has(scenario.id))
|
|
2611
|
+
continue;
|
|
2612
|
+
const reason = failureReasonByScenarioId.get(scenario.id) ??
|
|
2613
|
+
`No accepted codified test was returned for planned scenario "${scenario.title}".`;
|
|
2614
|
+
generated.push(manualDraftForScenario(scenario, reason));
|
|
2589
2615
|
}
|
|
2590
2616
|
}
|
|
2591
2617
|
}
|
|
2592
2618
|
else {
|
|
2593
2619
|
// Default: target-focused, bucket-diverse generation (one test per local bucket).
|
|
2594
|
-
const explicitMulti = Boolean(opts.target_ids && opts.target_ids.length > 1);
|
|
2595
2620
|
const plan = planGroundedBuckets(graph, runTargets, framework, fileReader, limit, explicitMulti, missing, warnings);
|
|
2596
2621
|
// Repo dependency names (read once) — used by the runnable check to tell a missing
|
|
2597
2622
|
// baseUrl-local import from a genuine external package the agent has installed.
|
|
@@ -74,8 +74,8 @@ export function buildPlanningSystemPromptV5() {
|
|
|
74
74
|
"- Only propose scenarios justified by evidence. No evidence = skip concern.",
|
|
75
75
|
"- Each scenario must be distinct.",
|
|
76
76
|
"- Existing tests are already covered. Never re-propose them.",
|
|
77
|
-
"- Rank
|
|
78
|
-
"-
|
|
77
|
+
"- Rank scenarios by risk_rank (1 = highest blast radius × likelihood × coverage absence).",
|
|
78
|
+
"- Return at most 2 scenarios: the two most critical missing tests justified by the evidence.",
|
|
79
79
|
"- When FLOW CHAIN exists, prioritize gaps at service boundaries.",
|
|
80
80
|
`- technique must be exactly one of: ${Object.keys(TECHNIQUE_DESC).join(", ")}.`,
|
|
81
81
|
"",
|
package/dist/local/operations.js
CHANGED
|
@@ -19,7 +19,7 @@ import { enrichFromContent } from "./enrich/index.js";
|
|
|
19
19
|
import { scoreGraph } from "./score/score.js";
|
|
20
20
|
import { doctorGraph } from "./score/doctor.js";
|
|
21
21
|
import { findGaps } from "./gaps/gaps.js";
|
|
22
|
-
import { rankRiskGaps } from "./score/risk.js";
|
|
22
|
+
import { rankPriorityGaps, rankRiskGaps } from "./score/risk.js";
|
|
23
23
|
import { generateTests } from "./generate/generator.js";
|
|
24
24
|
import { classifyGeneratedDraftBlocker } from "./generate/draftGuidance.js";
|
|
25
25
|
import { autoProve, NO_KEY_MESSAGE, isEligibleProvableTarget } from "./autoProve.js";
|
|
@@ -548,6 +548,45 @@ function preserveCandidateFlows(graphPath, graph) {
|
|
|
548
548
|
return graph;
|
|
549
549
|
}
|
|
550
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Carry generated tests across a deterministic re-analysis only while the exact
|
|
553
|
+
* target still exists and its source fingerprint is unchanged. This keeps
|
|
554
|
+
* `opro start` additive for unchanged code without presenting an old draft as
|
|
555
|
+
* current after its target changes or disappears.
|
|
556
|
+
*/
|
|
557
|
+
function preserveGeneratedArtifacts(graphPath, graph) {
|
|
558
|
+
let previous;
|
|
559
|
+
try {
|
|
560
|
+
if (!existsSync(graphPath))
|
|
561
|
+
return graph;
|
|
562
|
+
previous = loadGraph(graphPath);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
return graph;
|
|
566
|
+
}
|
|
567
|
+
const currentSymbols = new Set(graph.nodes.filter((node) => node.kind === "CodeSymbol").map((node) => node.external_id));
|
|
568
|
+
const byId = new Map();
|
|
569
|
+
for (const test of previous.generated_tests ?? []) {
|
|
570
|
+
const target = test.target_symbol_external_id;
|
|
571
|
+
if (!target || !currentSymbols.has(target) || !test.target_fingerprint || test.stale === true)
|
|
572
|
+
continue;
|
|
573
|
+
if (targetFingerprint(graph, target) !== test.target_fingerprint)
|
|
574
|
+
continue;
|
|
575
|
+
if (!byId.has(test.id))
|
|
576
|
+
byId.set(test.id, { ...test, pinned: undefined });
|
|
577
|
+
}
|
|
578
|
+
const generatedTests = [...byId.values()];
|
|
579
|
+
if (generatedTests.length === 0)
|
|
580
|
+
return graph;
|
|
581
|
+
const retainedIds = new Set(generatedTests.map((test) => test.id));
|
|
582
|
+
const generationRuns = (previous.generation_runs ?? [])
|
|
583
|
+
.map((run) => ({
|
|
584
|
+
...run,
|
|
585
|
+
generated_test_ids: run.generated_test_ids.filter((id) => retainedIds.has(id))
|
|
586
|
+
}))
|
|
587
|
+
.filter((run) => run.generated_test_ids.length > 0);
|
|
588
|
+
return { ...graph, generation_runs: generationRuns, generated_tests: generatedTests };
|
|
589
|
+
}
|
|
551
590
|
export function opAnalyze(root, opts = {}, deps = defaultDeps()) {
|
|
552
591
|
const now = deps.clock();
|
|
553
592
|
const paths = workspacePaths(root);
|
|
@@ -605,7 +644,7 @@ export function opAnalyze(root, opts = {}, deps = defaultDeps()) {
|
|
|
605
644
|
// Applied AI candidate flows must SURVIVE re-analysis, but the stored lane is
|
|
606
645
|
// untrusted (any process can rewrite graph.json) — preserve+re-validate it
|
|
607
646
|
// without ever letting a malformed lane fail analyze.
|
|
608
|
-
const graph = preserveCandidateFlows(paths.graphPath, builtGraph);
|
|
647
|
+
const graph = preserveGeneratedArtifacts(paths.graphPath, preserveCandidateFlows(paths.graphPath, builtGraph));
|
|
609
648
|
if (!opts.suppressProgress) {
|
|
610
649
|
reportProgress("analyze: writing graph.json", { current: opts.generateCoverage ? 4 : 3, total: opts.generateCoverage ? 4 : 3 });
|
|
611
650
|
}
|
|
@@ -1631,7 +1670,7 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1631
1670
|
try {
|
|
1632
1671
|
const graphForGeneration = loadGraph(workspacePaths(root).graphPath);
|
|
1633
1672
|
const generatedTargets = new Set((graphForGeneration.generated_tests ?? []).map((t) => t.target_symbol_external_id).filter((id) => Boolean(id)));
|
|
1634
|
-
const targetIds =
|
|
1673
|
+
const targetIds = rankPriorityGaps(graphForGeneration, {
|
|
1635
1674
|
repoRoot: root,
|
|
1636
1675
|
limit: generationLimit,
|
|
1637
1676
|
provenIds: provenSymbolIds(graphForGeneration, loadLedger(root))
|
|
@@ -1648,12 +1687,13 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1648
1687
|
else {
|
|
1649
1688
|
reportProgress(`generate: drafting tests for top ${targetIds.length} risk target(s)`, { current: 6, total: 8 });
|
|
1650
1689
|
const generatedDrafts = [];
|
|
1651
|
-
for (
|
|
1652
|
-
const batch = targetIds.slice(i, i + START_GENERATE_BATCH_LIMIT);
|
|
1690
|
+
for (const targetId of targetIds) {
|
|
1653
1691
|
const generated = await opGenerate(root, {
|
|
1654
1692
|
...providerOpts,
|
|
1655
|
-
|
|
1656
|
-
|
|
1693
|
+
// One ranked flow per generation run. Give each flow room for its
|
|
1694
|
+
// two highest-risk missing tests instead of sharing a batch budget.
|
|
1695
|
+
target_ids: [targetId],
|
|
1696
|
+
limit: 2,
|
|
1657
1697
|
// The offline deterministic stand-in emits the established v2 scaffold; v5 is
|
|
1658
1698
|
// a two-phase model planning protocol and must not be selected implicitly for it.
|
|
1659
1699
|
prompt_version: opts.promptVersion ?? (deterministicGeneration ? "v2" : "v5")
|
|
@@ -1856,7 +1896,6 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1856
1896
|
const NO_PROVIDER_MESSAGE = 'No model provider configured. Set OPENAI_API_KEY (or OLLAMA_BASE_URL / ANTHROPIC_API_KEY) in your shell environment or a .env.provider.local file to generate with your own model, or pass provider="deterministic" (or set ORANGEPRO_ALLOW_DETERMINISTIC=1) to use the offline deterministic stand-in. No tests were generated.';
|
|
1857
1897
|
const START_RTM_LIMIT = 500;
|
|
1858
1898
|
const START_GENERATE_RISK_LIMIT = 20;
|
|
1859
|
-
const START_GENERATE_BATCH_LIMIT = 5;
|
|
1860
1899
|
const EMPTY_EVIDENCE_SUMMARY = {
|
|
1861
1900
|
tests: 0,
|
|
1862
1901
|
tests_with_proof: 0,
|
|
@@ -1925,8 +1964,8 @@ export async function opGenerate(root, opts = {}, deps = defaultDeps()) {
|
|
|
1925
1964
|
};
|
|
1926
1965
|
saveGraph(paths.graphPath, next);
|
|
1927
1966
|
// Keep the behavior report in sync with the freshly persisted generated
|
|
1928
|
-
// tests
|
|
1929
|
-
//
|
|
1967
|
+
// tests immediately. A later analyze/start preserves them only while their
|
|
1968
|
+
// exact target fingerprint remains current. Display-only refresh; a render
|
|
1930
1969
|
// failure must never fail generate.
|
|
1931
1970
|
try {
|
|
1932
1971
|
opBehaviorCoverageHtml(root, `${WORKSPACE_DIR}/behavior-coverage.html`, undefined, { persistBaseline: false });
|
package/dist/local/score/risk.js
CHANGED
|
@@ -580,3 +580,11 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
580
580
|
.sort((a, b) => b.risk_score - a.risk_score || b.incoming_refs - a.incoming_refs || b.git_churn - a.git_churn || a.id.localeCompare(b.id))
|
|
581
581
|
.slice(0, limit);
|
|
582
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Canonical priority-gap portfolio shown to a local user and used for automatic
|
|
585
|
+
* generation. Keeping this policy in one function prevents `opro start` from
|
|
586
|
+
* generating for a different "top N" than behavior-coverage.html displays.
|
|
587
|
+
*/
|
|
588
|
+
export function rankPriorityGaps(graph, opts = {}) {
|
|
589
|
+
return rankRiskGaps(graph, { ...opts, maxPerFile: 3, maxPerTitle: 1 });
|
|
590
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { buildRtm } from "../rtm.js";
|
|
4
|
-
import { inspectRiskInputHealth, isEntryPoint,
|
|
4
|
+
import { inspectRiskInputHealth, isEntryPoint, rankPriorityGaps } from "../score/risk.js";
|
|
5
5
|
import { ORANGEPRO_VERSION } from "../version.js";
|
|
6
6
|
import { PROOF_BLOCKER_GUIDE } from "../proofDoctor.js";
|
|
7
7
|
import { classifyGeneratedDraftBlocker } from "../generate/draftGuidance.js";
|
|
@@ -413,12 +413,10 @@ function riskGeneratedTests(graph, gap, riskIds, isFirstRowForFile) {
|
|
|
413
413
|
return [];
|
|
414
414
|
return fileOf(t.target_symbol_external_id) === gap.file ? [{ t, sameFile: true }] : [];
|
|
415
415
|
});
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
const shown = runnableLinked.length > 0 ? runnableLinked : linked;
|
|
421
|
-
return shown.slice(0, 2).map(({ t, sameFile }) => ({
|
|
416
|
+
// Keep the two generated scenarios visible even when only one survives
|
|
417
|
+
// compile validation. Hiding the manual sibling makes an unchanged rerun
|
|
418
|
+
// appear to have lost a generated test and obscures the real blocker.
|
|
419
|
+
return linked.slice(0, 2).map(({ t, sameFile }) => ({
|
|
422
420
|
name: t.title,
|
|
423
421
|
concern: t.test_type && t.test_type !== "unknown" ? t.test_type : undefined,
|
|
424
422
|
bucket: t.bucket,
|
|
@@ -436,11 +434,6 @@ function riskGeneratedTests(graph, gap, riskIds, isFirstRowForFile) {
|
|
|
436
434
|
...(t.runnable === false ? { blocker: classifyGeneratedDraftBlocker(t.unresolved_reason) } : {})
|
|
437
435
|
}));
|
|
438
436
|
}
|
|
439
|
-
/** Incoming refs are method-attributed and can be fractional when a file-level
|
|
440
|
-
* reference is split across its symbols. Preserve that weighting honestly. */
|
|
441
|
-
function fmtRefs(n) {
|
|
442
|
-
return Number.isInteger(n) ? String(n) : n.toFixed(1);
|
|
443
|
-
}
|
|
444
437
|
function displayTitle(title, file) {
|
|
445
438
|
if (title.includes("."))
|
|
446
439
|
return title;
|
|
@@ -461,13 +454,12 @@ function riskContext(risk) {
|
|
|
461
454
|
: (risk.flow_position ?? 0) >= 3
|
|
462
455
|
? `${5 - (risk.flow_position ?? 0)} call${5 - (risk.flow_position ?? 0) === 1 ? "" : "s"} from the nearest entry point`
|
|
463
456
|
: "deep in the call graph";
|
|
464
|
-
const refs = fmtRefs(risk.incoming_refs);
|
|
465
457
|
const churn = risk.churn_available !== false
|
|
466
458
|
? `${risk.git_churn} line${risk.git_churn === 1 ? "" : "s"} changed in 180 days`
|
|
467
459
|
: "Git churn unavailable (provisional static-only ranking)";
|
|
468
460
|
const parts = [
|
|
469
461
|
`Sits at ${pos}${sens ? ` on ${sens} paths` : ""}.`,
|
|
470
|
-
|
|
462
|
+
`ORS ${risk.risk_score} (P${risk.probability ?? "?"} × I${risk.impact ?? "?"} × D${risk.detection_difficulty ?? "?"}) reflects flow position, change activity, complexity, impact, and test evidence; ${risk.fan_out ?? 0} downstream call${(risk.fan_out ?? 0) === 1 ? "" : "s"}, ${churn} — and no test proves this flow.`
|
|
471
463
|
];
|
|
472
464
|
return parts.join(" ");
|
|
473
465
|
}
|
|
@@ -749,7 +741,7 @@ function riskApplicableConcerns(risk, verb) {
|
|
|
749
741
|
out.add("authorization_safety");
|
|
750
742
|
if (sens >= 6)
|
|
751
743
|
out.add("data_integrity");
|
|
752
|
-
if (risk.entry_point || verb !== "
|
|
744
|
+
if (risk.entry_point || verb !== "FLOW")
|
|
753
745
|
out.add("boundary_limits"); // external inputs cross here
|
|
754
746
|
if ((risk.flow_position ?? 0) >= 3 || (risk.fan_out ?? 0) >= 1)
|
|
755
747
|
out.add("integration_flow");
|
|
@@ -775,6 +767,10 @@ function riskTodo(risk, verb, path, generatedTests) {
|
|
|
775
767
|
if (generatedTests.length && generatedTests.every((t) => t.runnable !== false)) {
|
|
776
768
|
return "Run the generated test below in your repo; follow its prove handoff so a mutation failure can mint Dynamically Proven.";
|
|
777
769
|
}
|
|
770
|
+
if (generatedTests.some((t) => t.runnable !== false) &&
|
|
771
|
+
generatedTests.some((t) => t.runnable === false)) {
|
|
772
|
+
return "Run the validated generated test below, and review the second scenario's blocker before repairing or regenerating its draft. Both generated scenarios remain visible so an unchanged rerun does not appear to lose work.";
|
|
773
|
+
}
|
|
778
774
|
if (generatedTests.length) {
|
|
779
775
|
const blockers = new Set(generatedTests.map((t) => t.blocker ?? "unknown"));
|
|
780
776
|
if (blockers.size === 1) {
|
|
@@ -794,7 +790,7 @@ function riskTodo(risk, verb, path, generatedTests) {
|
|
|
794
790
|
}
|
|
795
791
|
return "OrangePro withheld generated code for the reasons shown below. Review each blocker, then repair or regenerate the drafts; do not assume repository dependencies are missing.";
|
|
796
792
|
}
|
|
797
|
-
const call = verb !== "
|
|
793
|
+
const call = verb !== "FLOW"
|
|
798
794
|
? `issues ${verb} ${path}`
|
|
799
795
|
: risk.entry_point
|
|
800
796
|
? `invokes ${displayTitle(risk.title, risk.file)} through its entry point`
|
|
@@ -843,14 +839,14 @@ function riskRows(risks, graph) {
|
|
|
843
839
|
tags.push(["provisional rank", "info"]);
|
|
844
840
|
else if (bucket)
|
|
845
841
|
tags.push([`${bucket} risk`, "risk"]);
|
|
846
|
-
tags.push([
|
|
842
|
+
tags.push([`ORS ${risk.risk_score}`, "info"]);
|
|
847
843
|
if (risk.entry_point)
|
|
848
844
|
tags.push(["Entry point", "entry"]);
|
|
849
845
|
return {
|
|
850
846
|
rank: idx + 1,
|
|
851
847
|
...(() => {
|
|
852
848
|
const generatedTests = riskGeneratedTests(graph, risk, riskIds, firstRowForFile.get(risk.file) === risk.id);
|
|
853
|
-
const verb = methodMatch?.[1]?.toUpperCase() ?? "
|
|
849
|
+
const verb = methodMatch?.[1]?.toUpperCase() ?? "FLOW";
|
|
854
850
|
const path = qualify(risk, methodMatch?.[2] ?? displayTitle(risk.title, risk.file));
|
|
855
851
|
const generatedCategories = [...new Set([
|
|
856
852
|
...generatedTests.map((t) => (t.bucket ? BUCKET_TO_CONCERN[t.bucket] : undefined)),
|
|
@@ -892,7 +888,10 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
892
888
|
const flowIds = flowSymbolIds(graph);
|
|
893
889
|
const summary = summaryFromRows(rows, flowIds);
|
|
894
890
|
const repoRoot = opts.repoRoot ?? graph.workspace.root;
|
|
895
|
-
const
|
|
891
|
+
const provenIds = new Set(rows
|
|
892
|
+
.filter((row) => row.evidence_tier === "proven" && Boolean(row.code_symbol))
|
|
893
|
+
.map((row) => row.code_symbol));
|
|
894
|
+
const riskGaps = rankPriorityGaps(graph, { repoRoot, limit: opts.riskLimit ?? 20, provenIds });
|
|
896
895
|
const riskHealth = inspectRiskInputHealth(repoRoot);
|
|
897
896
|
const churnAvailable = riskHealth.churnAvailable && riskGaps.every((risk) => risk.churn_available !== false);
|
|
898
897
|
const provenance = {
|
|
@@ -911,16 +911,20 @@ if(D.viewMeta){
|
|
|
911
911
|
if(rm&&rm.scored>rm.shown)$("#risk-cap-note").textContent="Showing the top "+rm.shown+" of "+rm.scored.toLocaleString()+" scored behaviors — every behavior is scored; only the highest-risk are surfaced here. Full ranking: opro gaps --limit N, or .orangepro/graph.json.";
|
|
912
912
|
if(fm&&fm.shown>0&&fm.prunedByCaps>0)$("#flow-cap-note").textContent="Showing "+fm.shown.toLocaleString()+" flows, endpoint-anchored first. "+fm.prunedByCaps.toLocaleString()+" additional branch expansions were pruned by depth/branch/global rendering caps — pruning affects display only, not scoring.";
|
|
913
913
|
}
|
|
914
|
-
// Platform CTA: top banner in risk panel
|
|
915
|
-
const riskTopBanner=el("div","platform-top-banner",
|
|
916
|
-
\`<span class="platform-top-banner-text">Local scan shows <b>\${D.risks.length}</b> priority gaps. Full ranked list, incident correlation, and CI merge gate on Platform.</span>
|
|
917
|
-
<a class="platform-footer-btn" href="https://orangepro.ai/get-started" target="_blank">Unlock Full Analysis →</a>\`);
|
|
918
|
-
riskList.before(riskTopBanner);
|
|
919
914
|
const generatedRiskCount=D.risks.filter(r=>r.generatedTests&&r.generatedTests.length).length;
|
|
920
915
|
const generatedOutputCopy=[
|
|
921
916
|
D.generatedRunnableTotal?\`\${D.generatedRunnableTotal} runnable generated test\${D.generatedRunnableTotal===1?"":"s"}\`:'',
|
|
922
917
|
D.generatedDraftTotal?\`\${D.generatedDraftTotal} grounded draft\${D.generatedDraftTotal===1?"":"s"} with code withheld\`:'',
|
|
923
918
|
].filter(Boolean).join(' and ');
|
|
919
|
+
const generationSummary=D.generatedTotal
|
|
920
|
+
?\` Generated output: <b>\${generatedOutputCopy}</b>; <b>\${D.shownCount}</b> shown inline across <b>\${generatedRiskCount} of \${D.risks.length}</b> priority flows.\`
|
|
921
|
+
:'';
|
|
922
|
+
// Platform CTA: top banner in risk panel. Keep generated-output totals beside
|
|
923
|
+
// the flow count so a user cannot mistake "5 flows" for "5 generated tests".
|
|
924
|
+
const riskTopBanner=el("div","platform-top-banner",
|
|
925
|
+
\`<span class="platform-top-banner-text">Local scan shows <b>\${D.risks.length}</b> priority gaps.\${generationSummary} Full ranked list, incident correlation, and CI merge gate on Platform.</span>
|
|
926
|
+
<a class="platform-footer-btn" href="https://orangepro.ai/get-started" target="_blank">Unlock Full Analysis →</a>\`);
|
|
927
|
+
riskList.before(riskTopBanner);
|
|
924
928
|
let activeRiskFilter=generatedRiskCount?"generated":"all";
|
|
925
929
|
function riskMatchesFilter(r){
|
|
926
930
|
const hasGenerated=Boolean(r.generatedTests&&r.generatedTests.length);
|
|
@@ -931,7 +935,7 @@ function riskMatchesFilter(r){
|
|
|
931
935
|
function renderRiskFilters(){
|
|
932
936
|
const options=[
|
|
933
937
|
["all","All",D.risks.length],
|
|
934
|
-
["generated","Flows with
|
|
938
|
+
["generated","Flows with tests",generatedRiskCount],
|
|
935
939
|
["missing","No generated tests",D.risks.filter(r=>!(r.generatedTests&&r.generatedTests.length)).length]
|
|
936
940
|
];
|
|
937
941
|
riskTools.innerHTML=options.map(([key,label,count])=>\`<button class="risk-filter" type="button" data-risk-filter="\${key}" aria-pressed="\${key===activeRiskFilter}">\${label} <span class="gc">\${count}</span></button>\`).join("");
|
|
@@ -985,7 +989,7 @@ function renderRisks(){
|
|
|
985
989
|
riskList.append(el("div","paywall",
|
|
986
990
|
hiddenGeneratedFlows
|
|
987
991
|
? \`<div class="paywall-num">\${hiddenGeneratedFlows} more flows with generated output</div>
|
|
988
|
-
<div class="paywall-txt">OrangePro produced \${generatedOutputCopy} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Use the “Flows with
|
|
992
|
+
<div class="paywall-txt">OrangePro produced \${generatedOutputCopy} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Use the “Flows with tests” filter to review them first.</div>
|
|
989
993
|
<a class="paywall-btn" href="https://orangepro.ai/get-started" target="_blank">View all on OrangePro Platform →</a>\`
|
|
990
994
|
: remainingRiskFlows
|
|
991
995
|
? \`<div class="paywall-num">\${remainingRiskFlows} high-risk flows left</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.33",
|
|
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",
|