@orangepro/orangepro-mcp 0.2.32 → 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.
@@ -2312,7 +2312,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2312
2312
  continue;
2313
2313
  }
2314
2314
  try {
2315
- const result = parsePlannedScenariosStrict(rawPlan, 20);
2315
+ const result = parsePlannedScenariosStrict(rawPlan, 2);
2316
2316
  scenarios = result.scenarios;
2317
2317
  if (result.dropped > 0) {
2318
2318
  warnings.push(`Dropped ${result.dropped} invalid v5 planned scenario(s) for "${gc.ctx.behavior_title}": ${result.dropSummary.join("; ")}.`);
@@ -2343,7 +2343,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2343
2343
  maxTokens: 1600,
2344
2344
  temperature: 0
2345
2345
  });
2346
- const result = parsePlannedScenariosStrict(repaired, 20);
2346
+ const result = parsePlannedScenariosStrict(repaired, 2);
2347
2347
  // Keep ONLY repaired scenarios that tie back to the ORIGINAL malformed text; drop any the
2348
2348
  // model invented. If none tie back, fail closed — never generate from an invented plan.
2349
2349
  const tiedBack = result.scenarios.filter((s) => scenarioTiesBackToRaw(s, rawPlan));
@@ -2393,6 +2393,36 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2393
2393
  // with several scenarios, leaving later high-risk behaviors untouched.
2394
2394
  const targetLimit = explicitMulti ? Math.max(1, Math.floor(remainingSlots / remainingTargets)) : remainingSlots;
2395
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
+ };
2396
2426
  const completions = [];
2397
2427
  try {
2398
2428
  reportProgress(`Generating "${gc.ctx.behavior_title}" [v5 batch: ${selected.length}]…`);
@@ -2422,6 +2452,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2422
2452
  const scenarioById = new Map(selected.map((s) => [s.id, s]));
2423
2453
  const parsed = completions.flatMap(parseBatchGeneratedTests);
2424
2454
  const seenScenarioIds = new Set();
2455
+ const emittedScenarioIds = new Set();
2456
+ const failureReasonByScenarioId = new Map();
2425
2457
  const relatedFiles = relatedFilePaths(graph, behavior).files;
2426
2458
  for (let i = 0; i < parsed.length && generated.length < limit; i++) {
2427
2459
  const parsedTest = parsed[i];
@@ -2455,23 +2487,27 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2455
2487
  warnings.push(`Redacted ${sanitized.redactedLines} echoed source-excerpt line(s) from the v5 generated test for "${gc.ctx.behavior_title}".`);
2456
2488
  }
2457
2489
  if (!hasExecutableContent(sanitized.body, framework)) {
2490
+ const reason = `V5 generated no executable code for scenario "${scenario.title}".`;
2458
2491
  warnings.push(`Dropped v5 generated test for "${gc.ctx.behavior_title}" / "${scenario.title}": no executable test code.`);
2459
2492
  missing.push({
2460
2493
  external_id: behavior.external_id,
2461
2494
  title: gc.ctx.behavior_title,
2462
- reason: `V5 generated no executable code for scenario "${scenario.title}".`,
2495
+ reason,
2463
2496
  needed: ["a non-empty runnable test body"]
2464
2497
  });
2498
+ failureReasonByScenarioId.set(scenario.id, reason);
2465
2499
  continue;
2466
2500
  }
2467
2501
  if (!generatedBodyAlignsWithScenario(sanitized.body, scenario)) {
2502
+ const reason = `V5 generated test did not align with scenario "${scenario.title}".`;
2468
2503
  warnings.push(`Dropped v5 generated test for "${gc.ctx.behavior_title}" / "${scenario.title}": body did not reference the planned assertion target.`);
2469
2504
  missing.push({
2470
2505
  external_id: behavior.external_id,
2471
2506
  title: gc.ctx.behavior_title,
2472
- reason: `V5 generated test did not align with scenario "${scenario.title}".`,
2507
+ reason,
2473
2508
  needed: ["a generated test body that asserts the planned scenario target"]
2474
2509
  });
2510
+ failureReasonByScenarioId.set(scenario.id, reason);
2475
2511
  continue;
2476
2512
  }
2477
2513
  let cleanBody = sanitized.body;
@@ -2540,43 +2576,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2540
2576
  reason,
2541
2577
  needed: ["a compiling generated test with a real assertion and resolvable subject import"]
2542
2578
  });
2543
- // Preserve the grounded INTENT in English, never the rejected code.
2544
- // The scenario fields were authored by a model that saw source
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
- });
2579
+ generated.push(manualDraftForScenario(scenario, reason));
2580
+ emittedScenarioIds.add(scenario.id);
2580
2581
  continue;
2581
2582
  }
2582
2583
  generated.push({
@@ -2600,6 +2601,17 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2600
2601
  : {}),
2601
2602
  runnable: true
2602
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));
2603
2615
  }
2604
2616
  }
2605
2617
  }
@@ -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 all scenarios by risk_rank (1 = highest blast radius × likelihood × coverage absence).",
78
- "- Find all gaps. No cap. If evidence justifies 3, output 3. If 30, output 30.",
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
  "",
@@ -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
  }
@@ -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 (let i = 0; i < targetIds.length; i += START_GENERATE_BATCH_LIMIT) {
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
- target_ids: batch,
1656
- limit: batch.length,
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 analyze/start would REBUILD the graph and drop them, so this is
1929
- // the only command that can surface them. Display-only refresh; a render
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 });
@@ -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
- // Mutually exclusive display: when ANY runnable generated test exists for
417
- // this target, English intents are suppressed intents are strictly the
418
- // fallback for environments where runnable code was withheld. Never mix.
419
- const runnableLinked = linked.filter(({ t }) => t.runnable !== false);
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
- `${refs} weighted incoming reference${risk.incoming_refs === 1 ? "" : "s"}, ${risk.fan_out ?? 0} downstream call${(risk.fan_out ?? 0) === 1 ? "" : "s"}, ${churn} — and no test proves its behavior.`
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 !== "BEHAVIOR")
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 !== "BEHAVIOR"
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([`${fmtRefs(risk.incoming_refs)} weighted refs`, "info"]);
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() ?? "BEHAVIOR";
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 generated output",generatedRiskCount+"/"+D.risks.length],
939
- ["missing","Flows without generated output",D.risks.filter(r=>!(r.generatedTests&&r.generatedTests.length)).length+"/"+D.risks.length]
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 generated output” filter to review them first.</div>
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 &rarr;</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.32",
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",