@orangepro/orangepro-mcp 0.2.32 → 0.2.34
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.
|
@@ -2169,7 +2169,10 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2169
2169
|
const systemPrompt = opts.systemPrompt ?? buildSystemPrompt();
|
|
2170
2170
|
const promptVersion = inputMode === "graph_grounded" && opts.prompt_version === "v5" ? PROMPT_VERSION_V5 : PROMPT_VERSION;
|
|
2171
2171
|
const created_at = clock();
|
|
2172
|
-
const runSeed = shortHash(created_at +
|
|
2172
|
+
const runSeed = shortHash(created_at +
|
|
2173
|
+
provider.modelName +
|
|
2174
|
+
runTargets.map((t) => t.external_id).join(",") +
|
|
2175
|
+
JSON.stringify(opts.existing_generated_test_titles ?? []));
|
|
2173
2176
|
const run_id = `local-gen-${runSeed}`;
|
|
2174
2177
|
const generated = [];
|
|
2175
2178
|
const missing = [];
|
|
@@ -2287,6 +2290,10 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2287
2290
|
if (generated.length >= limit)
|
|
2288
2291
|
break;
|
|
2289
2292
|
const gc = gatherContext(graph, behavior, framework, fileReader);
|
|
2293
|
+
const existingGeneratedTitles = dedupe(opts.existing_generated_test_titles ?? []);
|
|
2294
|
+
if (existingGeneratedTitles.length) {
|
|
2295
|
+
gc.ctx.existing_tests = dedupe([...gc.ctx.existing_tests, ...existingGeneratedTitles]);
|
|
2296
|
+
}
|
|
2290
2297
|
reportProgress(`Planning "${gc.ctx.behavior_title}" [v5]…`);
|
|
2291
2298
|
let scenarios = [];
|
|
2292
2299
|
// Transport first: a network/timeout failure is NOT malformed JSON, so it does
|
|
@@ -2312,7 +2319,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2312
2319
|
continue;
|
|
2313
2320
|
}
|
|
2314
2321
|
try {
|
|
2315
|
-
const result = parsePlannedScenariosStrict(rawPlan,
|
|
2322
|
+
const result = parsePlannedScenariosStrict(rawPlan, 2);
|
|
2316
2323
|
scenarios = result.scenarios;
|
|
2317
2324
|
if (result.dropped > 0) {
|
|
2318
2325
|
warnings.push(`Dropped ${result.dropped} invalid v5 planned scenario(s) for "${gc.ctx.behavior_title}": ${result.dropSummary.join("; ")}.`);
|
|
@@ -2343,7 +2350,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2343
2350
|
maxTokens: 1600,
|
|
2344
2351
|
temperature: 0
|
|
2345
2352
|
});
|
|
2346
|
-
const result = parsePlannedScenariosStrict(repaired,
|
|
2353
|
+
const result = parsePlannedScenariosStrict(repaired, 2);
|
|
2347
2354
|
// Keep ONLY repaired scenarios that tie back to the ORIGINAL malformed text; drop any the
|
|
2348
2355
|
// model invented. If none tie back, fail closed — never generate from an invented plan.
|
|
2349
2356
|
const tiedBack = result.scenarios.filter((s) => scenarioTiesBackToRaw(s, rawPlan));
|
|
@@ -2377,6 +2384,19 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2377
2384
|
continue;
|
|
2378
2385
|
}
|
|
2379
2386
|
}
|
|
2387
|
+
if (existingGeneratedTitles.length) {
|
|
2388
|
+
const normalizedExistingTitles = new Set(existingGeneratedTitles.map((title) => title.trim().toLowerCase()));
|
|
2389
|
+
const beforeDuplicateFilter = scenarios.length;
|
|
2390
|
+
scenarios = scenarios.filter((scenario) => {
|
|
2391
|
+
const scenarioTitle = scenario.title.trim().toLowerCase();
|
|
2392
|
+
const fullTitle = `${gc.ctx.behavior_title} — ${scenario.title}`.trim().toLowerCase();
|
|
2393
|
+
return !normalizedExistingTitles.has(scenarioTitle) && !normalizedExistingTitles.has(fullTitle);
|
|
2394
|
+
});
|
|
2395
|
+
const duplicateCount = beforeDuplicateFilter - scenarios.length;
|
|
2396
|
+
if (duplicateCount > 0) {
|
|
2397
|
+
warnings.push(`Dropped ${duplicateCount} already-generated v5 scenario(s) for "${gc.ctx.behavior_title}" during top-up.`);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2380
2400
|
if (scenarios.length === 0) {
|
|
2381
2401
|
missing.push({
|
|
2382
2402
|
external_id: behavior.external_id,
|
|
@@ -2393,6 +2413,36 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2393
2413
|
// with several scenarios, leaving later high-risk behaviors untouched.
|
|
2394
2414
|
const targetLimit = explicitMulti ? Math.max(1, Math.floor(remainingSlots / remainingTargets)) : remainingSlots;
|
|
2395
2415
|
const selected = scenarios.slice(0, targetLimit);
|
|
2416
|
+
const manualDraftForScenario = (scenario, reason) => {
|
|
2417
|
+
const manualBody = sanitizeGeneratedBody([
|
|
2418
|
+
`Scenario: ${scenario.title}`,
|
|
2419
|
+
...(scenario.steps && scenario.steps.length
|
|
2420
|
+
? ["Steps:", ...scenario.steps.map((st, n) => ` ${n + 1}. ${st}`)]
|
|
2421
|
+
: []),
|
|
2422
|
+
...(scenario.test_data ? [`Test data: ${scenario.test_data}`] : []),
|
|
2423
|
+
...(scenario.assertion_targets.length ? [`Expected: ${scenario.assertion_targets.join("; ")}`] : []),
|
|
2424
|
+
...(scenario.rationale ? [`Why this test: ${scenario.rationale}`] : []),
|
|
2425
|
+
"",
|
|
2426
|
+
`Blocked by: ${reason.split(" — ")[0]}`,
|
|
2427
|
+
`Fix: ${generatedDraftRemediation(reason)}`
|
|
2428
|
+
].join("\n"), gc.ctx.source_excerpts, "//").body;
|
|
2429
|
+
return {
|
|
2430
|
+
id: `${run_id}-t${generated.length + 1}`,
|
|
2431
|
+
run_id,
|
|
2432
|
+
title: `${gc.ctx.behavior_title} — ${scenario.title}`,
|
|
2433
|
+
test_type: gc.ctx.test_layer,
|
|
2434
|
+
framework_hint: framework,
|
|
2435
|
+
body: manualBody,
|
|
2436
|
+
bucket: bucketForV5Scenario(scenario),
|
|
2437
|
+
prompt_version: PROMPT_VERSION_V5,
|
|
2438
|
+
grounding: { entity_ids: gc.entityIds, source_refs: [], weak_relationships_used: [] },
|
|
2439
|
+
weak_evidence_used: false,
|
|
2440
|
+
target_symbol_external_id: behavior.external_id,
|
|
2441
|
+
...(fingerprintOf(behavior) ? { target_fingerprint: fingerprintOf(behavior) } : {}),
|
|
2442
|
+
runnable: false,
|
|
2443
|
+
unresolved_reason: reason
|
|
2444
|
+
};
|
|
2445
|
+
};
|
|
2396
2446
|
const completions = [];
|
|
2397
2447
|
try {
|
|
2398
2448
|
reportProgress(`Generating "${gc.ctx.behavior_title}" [v5 batch: ${selected.length}]…`);
|
|
@@ -2422,6 +2472,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2422
2472
|
const scenarioById = new Map(selected.map((s) => [s.id, s]));
|
|
2423
2473
|
const parsed = completions.flatMap(parseBatchGeneratedTests);
|
|
2424
2474
|
const seenScenarioIds = new Set();
|
|
2475
|
+
const emittedScenarioIds = new Set();
|
|
2476
|
+
const failureReasonByScenarioId = new Map();
|
|
2425
2477
|
const relatedFiles = relatedFilePaths(graph, behavior).files;
|
|
2426
2478
|
for (let i = 0; i < parsed.length && generated.length < limit; i++) {
|
|
2427
2479
|
const parsedTest = parsed[i];
|
|
@@ -2455,23 +2507,27 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2455
2507
|
warnings.push(`Redacted ${sanitized.redactedLines} echoed source-excerpt line(s) from the v5 generated test for "${gc.ctx.behavior_title}".`);
|
|
2456
2508
|
}
|
|
2457
2509
|
if (!hasExecutableContent(sanitized.body, framework)) {
|
|
2510
|
+
const reason = `V5 generated no executable code for scenario "${scenario.title}".`;
|
|
2458
2511
|
warnings.push(`Dropped v5 generated test for "${gc.ctx.behavior_title}" / "${scenario.title}": no executable test code.`);
|
|
2459
2512
|
missing.push({
|
|
2460
2513
|
external_id: behavior.external_id,
|
|
2461
2514
|
title: gc.ctx.behavior_title,
|
|
2462
|
-
reason
|
|
2515
|
+
reason,
|
|
2463
2516
|
needed: ["a non-empty runnable test body"]
|
|
2464
2517
|
});
|
|
2518
|
+
failureReasonByScenarioId.set(scenario.id, reason);
|
|
2465
2519
|
continue;
|
|
2466
2520
|
}
|
|
2467
2521
|
if (!generatedBodyAlignsWithScenario(sanitized.body, scenario)) {
|
|
2522
|
+
const reason = `V5 generated test did not align with scenario "${scenario.title}".`;
|
|
2468
2523
|
warnings.push(`Dropped v5 generated test for "${gc.ctx.behavior_title}" / "${scenario.title}": body did not reference the planned assertion target.`);
|
|
2469
2524
|
missing.push({
|
|
2470
2525
|
external_id: behavior.external_id,
|
|
2471
2526
|
title: gc.ctx.behavior_title,
|
|
2472
|
-
reason
|
|
2527
|
+
reason,
|
|
2473
2528
|
needed: ["a generated test body that asserts the planned scenario target"]
|
|
2474
2529
|
});
|
|
2530
|
+
failureReasonByScenarioId.set(scenario.id, reason);
|
|
2475
2531
|
continue;
|
|
2476
2532
|
}
|
|
2477
2533
|
let cleanBody = sanitized.body;
|
|
@@ -2540,43 +2596,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2540
2596
|
reason,
|
|
2541
2597
|
needed: ["a compiling generated test with a real assertion and resolvable subject import"]
|
|
2542
2598
|
});
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
// excerpts — scrub the composed body with the same guard as code.
|
|
2546
|
-
// The scenario plan (title / assertion targets / rationale) is the
|
|
2547
|
-
// reviewable half of the draft; withholding the body entirely also
|
|
2548
|
-
// removes any residual source-echo risk. runnable:false + the reason
|
|
2549
|
-
// keep this honestly a draft — it ships with no run command and can
|
|
2550
|
-
// never enter the proof-ready set.
|
|
2551
|
-
const manualBody = sanitizeGeneratedBody([
|
|
2552
|
-
`Scenario: ${scenario.title}`,
|
|
2553
|
-
...(scenario.steps && scenario.steps.length
|
|
2554
|
-
? ["Steps:", ...scenario.steps.map((st, n) => ` ${n + 1}. ${st}`)]
|
|
2555
|
-
: []),
|
|
2556
|
-
...(scenario.test_data ? [`Test data: ${scenario.test_data}`] : []),
|
|
2557
|
-
...(scenario.assertion_targets.length ? [`Expected: ${scenario.assertion_targets.join("; ")}`] : []),
|
|
2558
|
-
...(scenario.rationale ? [`Why this test: ${scenario.rationale}`] : []),
|
|
2559
|
-
"",
|
|
2560
|
-
// Concise blocker: first clause only — the full remedy is one line.
|
|
2561
|
-
`Blocked by: ${reason.split(" — ")[0]}`,
|
|
2562
|
-
`Fix: ${generatedDraftRemediation(reason)}`
|
|
2563
|
-
].join("\n"), gc.ctx.source_excerpts, "//").body;
|
|
2564
|
-
generated.push({
|
|
2565
|
-
id: `${run_id}-t${generated.length + 1}`,
|
|
2566
|
-
run_id,
|
|
2567
|
-
title: `${gc.ctx.behavior_title} — ${scenario.title}`,
|
|
2568
|
-
test_type: gc.ctx.test_layer,
|
|
2569
|
-
framework_hint: framework,
|
|
2570
|
-
body: manualBody,
|
|
2571
|
-
bucket: bucketForV5Scenario(scenario),
|
|
2572
|
-
prompt_version: PROMPT_VERSION_V5,
|
|
2573
|
-
grounding: { entity_ids: gc.entityIds, source_refs: [], weak_relationships_used: [] },
|
|
2574
|
-
weak_evidence_used: false,
|
|
2575
|
-
target_symbol_external_id: behavior.external_id,
|
|
2576
|
-
...(fingerprintOf(behavior) ? { target_fingerprint: fingerprintOf(behavior) } : {}),
|
|
2577
|
-
runnable: false,
|
|
2578
|
-
unresolved_reason: reason
|
|
2579
|
-
});
|
|
2599
|
+
generated.push(manualDraftForScenario(scenario, reason));
|
|
2600
|
+
emittedScenarioIds.add(scenario.id);
|
|
2580
2601
|
continue;
|
|
2581
2602
|
}
|
|
2582
2603
|
generated.push({
|
|
@@ -2600,6 +2621,17 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2600
2621
|
: {}),
|
|
2601
2622
|
runnable: true
|
|
2602
2623
|
});
|
|
2624
|
+
emittedScenarioIds.add(scenario.id);
|
|
2625
|
+
}
|
|
2626
|
+
// A valid plan is still useful when code generation is empty, malformed,
|
|
2627
|
+
// or rejected. Retain that grounded scenario as an honest manual test so
|
|
2628
|
+
// the report never loses the test idea merely because code was unusable.
|
|
2629
|
+
for (const scenario of selected) {
|
|
2630
|
+
if (generated.length >= limit || emittedScenarioIds.has(scenario.id))
|
|
2631
|
+
continue;
|
|
2632
|
+
const reason = failureReasonByScenarioId.get(scenario.id) ??
|
|
2633
|
+
`No accepted codified test was returned for planned scenario "${scenario.title}".`;
|
|
2634
|
+
generated.push(manualDraftForScenario(scenario, reason));
|
|
2603
2635
|
}
|
|
2604
2636
|
}
|
|
2605
2637
|
}
|
|
@@ -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
|
@@ -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
|
}
|
|
@@ -1630,36 +1669,67 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1630
1669
|
else if (!opts.noAuto && generationProviderConfigured && opts.ai !== false && generationLimit > 0) {
|
|
1631
1670
|
try {
|
|
1632
1671
|
const graphForGeneration = loadGraph(workspacePaths(root).graphPath);
|
|
1633
|
-
const
|
|
1634
|
-
const
|
|
1672
|
+
const generatedTestsByTarget = new Map();
|
|
1673
|
+
for (const test of graphForGeneration.generated_tests ?? []) {
|
|
1674
|
+
const targetId = test.target_symbol_external_id;
|
|
1675
|
+
if (!targetId || test.stale === true)
|
|
1676
|
+
continue;
|
|
1677
|
+
const tests = generatedTestsByTarget.get(targetId) ?? [];
|
|
1678
|
+
tests.push(test);
|
|
1679
|
+
generatedTestsByTarget.set(targetId, tests);
|
|
1680
|
+
}
|
|
1681
|
+
const targetPlans = rankPriorityGaps(graphForGeneration, {
|
|
1635
1682
|
repoRoot: root,
|
|
1636
1683
|
limit: generationLimit,
|
|
1637
1684
|
provenIds: provenSymbolIds(graphForGeneration, loadLedger(root))
|
|
1638
1685
|
})
|
|
1639
|
-
.map((gap) =>
|
|
1640
|
-
.
|
|
1641
|
-
|
|
1686
|
+
.map((gap) => {
|
|
1687
|
+
const existingTests = generatedTestsByTarget.get(gap.id) ?? [];
|
|
1688
|
+
return {
|
|
1689
|
+
id: gap.id,
|
|
1690
|
+
existingTests,
|
|
1691
|
+
deficit: Math.max(0, 2 - existingTests.length)
|
|
1692
|
+
};
|
|
1693
|
+
})
|
|
1694
|
+
.filter((target) => target.deficit > 0);
|
|
1695
|
+
if (!targetPlans.length) {
|
|
1642
1696
|
generationResult = {
|
|
1643
1697
|
...generationResult,
|
|
1644
1698
|
status: "no_targets",
|
|
1645
|
-
reason: "
|
|
1699
|
+
reason: "Every eligible priority flow already has two generated tests."
|
|
1646
1700
|
};
|
|
1647
1701
|
}
|
|
1648
1702
|
else {
|
|
1649
|
-
reportProgress(`generate:
|
|
1703
|
+
reportProgress(`generate: filling test gaps for ${targetPlans.length} priority flow(s)`, { current: 6, total: 8 });
|
|
1650
1704
|
const generatedDrafts = [];
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1705
|
+
const incompleteTargets = [];
|
|
1706
|
+
for (const target of targetPlans) {
|
|
1707
|
+
let remaining = target.deficit;
|
|
1708
|
+
const existingTitles = target.existingTests.map((test) => test.title);
|
|
1709
|
+
// A provider can return one accepted scenario when two were requested.
|
|
1710
|
+
// Make at most one bounded follow-up for the remaining slot, carrying
|
|
1711
|
+
// prior titles so the planner cannot silently duplicate the first test.
|
|
1712
|
+
for (let attempt = 0; attempt < 2 && remaining > 0; attempt++) {
|
|
1713
|
+
const generated = await opGenerate(root, {
|
|
1714
|
+
...providerOpts,
|
|
1715
|
+
target_ids: [target.id],
|
|
1716
|
+
limit: remaining,
|
|
1717
|
+
pin_unchanged: false,
|
|
1718
|
+
existing_generated_test_titles: existingTitles,
|
|
1719
|
+
// The offline deterministic stand-in emits the established v2 scaffold; v5 is
|
|
1720
|
+
// a two-phase model planning protocol and must not be selected implicitly for it.
|
|
1721
|
+
prompt_version: opts.promptVersion ?? (deterministicGeneration ? "v2" : "v5")
|
|
1722
|
+
}, providerDeps);
|
|
1723
|
+
const freshForTarget = generated.generated_tests.filter((test) => test.target_symbol_external_id === target.id && !test.pinned);
|
|
1724
|
+
generatedDrafts.push(...freshForTarget);
|
|
1725
|
+
warnings.push(...generated.warnings.map((w) => `generate: ${w}`));
|
|
1726
|
+
if (freshForTarget.length === 0)
|
|
1727
|
+
break;
|
|
1728
|
+
existingTitles.push(...freshForTarget.map((test) => test.title));
|
|
1729
|
+
remaining = Math.max(0, remaining - freshForTarget.length);
|
|
1730
|
+
}
|
|
1731
|
+
if (remaining > 0)
|
|
1732
|
+
incompleteTargets.push(target.id);
|
|
1663
1733
|
}
|
|
1664
1734
|
const blockers = {};
|
|
1665
1735
|
for (const draft of generatedDrafts.filter((test) => test.runnable === false)) {
|
|
@@ -1667,22 +1737,27 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1667
1737
|
blockers[blocker] = (blockers[blocker] ?? 0) + 1;
|
|
1668
1738
|
}
|
|
1669
1739
|
const runnable = generatedDrafts.filter((test) => test.runnable !== false).length;
|
|
1740
|
+
const shortfallReason = incompleteTargets.length
|
|
1741
|
+
? `${incompleteTargets.length} priority flow(s) remain below two generated tests because the provider returned no additional accepted distinct scenario.`
|
|
1742
|
+
: undefined;
|
|
1670
1743
|
generationResult = {
|
|
1671
1744
|
status: generatedDrafts.length === 0
|
|
1672
1745
|
? "no_results"
|
|
1673
|
-
: generatedDrafts.some((test) => test.runnable === false)
|
|
1746
|
+
: generatedDrafts.some((test) => test.runnable === false) || incompleteTargets.length > 0
|
|
1674
1747
|
? "completed_with_blockers"
|
|
1675
1748
|
: "completed",
|
|
1676
|
-
requested:
|
|
1749
|
+
requested: targetPlans.length,
|
|
1677
1750
|
generated: generatedDrafts.length,
|
|
1678
1751
|
runnable,
|
|
1679
1752
|
drafts: generatedDrafts.length - runnable,
|
|
1680
1753
|
blockers,
|
|
1681
1754
|
...(generatedDrafts.length === 0
|
|
1682
|
-
? { reason: "The provider returned no generated-test drafts for the selected risk targets." }
|
|
1683
|
-
:
|
|
1755
|
+
? { reason: shortfallReason ?? "The provider returned no generated-test drafts for the selected risk targets." }
|
|
1756
|
+
: shortfallReason
|
|
1757
|
+
? { reason: shortfallReason }
|
|
1758
|
+
: {})
|
|
1684
1759
|
};
|
|
1685
|
-
if (
|
|
1760
|
+
if (generationResult.reason)
|
|
1686
1761
|
warnings.push(`generate: ${generationResult.reason}`);
|
|
1687
1762
|
}
|
|
1688
1763
|
}
|
|
@@ -1856,7 +1931,6 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1856
1931
|
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
1932
|
const START_RTM_LIMIT = 500;
|
|
1858
1933
|
const START_GENERATE_RISK_LIMIT = 20;
|
|
1859
|
-
const START_GENERATE_BATCH_LIMIT = 5;
|
|
1860
1934
|
const EMPTY_EVIDENCE_SUMMARY = {
|
|
1861
1935
|
tests: 0,
|
|
1862
1936
|
tests_with_proof: 0,
|
|
@@ -1909,6 +1983,7 @@ export async function opGenerate(root, opts = {}, deps = defaultDeps()) {
|
|
|
1909
1983
|
limit: opts.limit,
|
|
1910
1984
|
input_mode: opts.input_mode,
|
|
1911
1985
|
prompt_version: opts.prompt_version,
|
|
1986
|
+
existing_generated_test_titles: opts.existing_generated_test_titles,
|
|
1912
1987
|
// Persisting lane: a runnable draft for an unchanged target is reused as-is
|
|
1913
1988
|
// rather than re-bought from the model on every run.
|
|
1914
1989
|
pin_unchanged: opts.pin_unchanged ?? true
|
|
@@ -1925,8 +2000,8 @@ export async function opGenerate(root, opts = {}, deps = defaultDeps()) {
|
|
|
1925
2000
|
};
|
|
1926
2001
|
saveGraph(paths.graphPath, next);
|
|
1927
2002
|
// Keep the behavior report in sync with the freshly persisted generated
|
|
1928
|
-
// tests
|
|
1929
|
-
//
|
|
2003
|
+
// tests immediately. A later analyze/start preserves them only while their
|
|
2004
|
+
// exact target fingerprint remains current. Display-only refresh; a render
|
|
1930
2005
|
// failure must never fail generate.
|
|
1931
2006
|
try {
|
|
1932
2007
|
opBehaviorCoverageHtml(root, `${WORKSPACE_DIR}/behavior-coverage.html`, undefined, { persistBaseline: false });
|
|
@@ -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)),
|
|
@@ -935,8 +935,8 @@ function riskMatchesFilter(r){
|
|
|
935
935
|
function renderRiskFilters(){
|
|
936
936
|
const options=[
|
|
937
937
|
["all","All",D.risks.length],
|
|
938
|
-
["generated","Flows with
|
|
939
|
-
["missing","
|
|
938
|
+
["generated","Flows with tests",generatedRiskCount],
|
|
939
|
+
["missing","No generated tests",D.risks.filter(r=>!(r.generatedTests&&r.generatedTests.length)).length]
|
|
940
940
|
];
|
|
941
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("");
|
|
942
942
|
}
|
|
@@ -989,7 +989,7 @@ function renderRisks(){
|
|
|
989
989
|
riskList.append(el("div","paywall",
|
|
990
990
|
hiddenGeneratedFlows
|
|
991
991
|
? \`<div class="paywall-num">\${hiddenGeneratedFlows} more flows with generated output</div>
|
|
992
|
-
<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>
|
|
993
993
|
<a class="paywall-btn" href="https://orangepro.ai/get-started" target="_blank">View all on OrangePro Platform →</a>\`
|
|
994
994
|
: remainingRiskFlows
|
|
995
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.34",
|
|
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",
|