@orangepro/orangepro-mcp 0.2.29 → 0.2.31

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.
@@ -108,16 +108,17 @@ export class OpenAICompatibleProvider {
108
108
  const data = (await postJson(this.fetchImpl, `${this.cfg.baseUrl}/chat/completions`, { Authorization: `Bearer ${this.cfg.apiKey ?? ""}` }, body, providerTimeoutMs(this.cfg.model)));
109
109
  const choice = data.choices?.[0];
110
110
  const content = choice?.message?.content ?? "";
111
- // Reasoning starvation: the model spent the ENTIRE completion budget on
112
- // hidden reasoning and returned no visible content (finish_reason
113
- // "length"). Long grounded prompts trigger this on gpt-5/o-series at the
114
- // 4000-token seed. Retry ONCE with 4x the budget; if it still comes back
115
- // empty, return "" and let the caller refuse to emit an empty test.
116
- if (!content.trim() && choice?.finish_reason === "length" && !tried.has("starved")) {
117
- tried.add("starved");
111
+ // A length stop means the completion is truncated even when it contains
112
+ // visible text. Returning that prefix turns cut-off source into a fake
113
+ // compiler/setup problem. Retry once with more room, then fail closed.
114
+ if (choice?.finish_reason === "length" && !tried.has("length-retry")) {
115
+ tried.add("length-retry");
118
116
  maxTokens = maxTokens * 4;
119
117
  continue;
120
118
  }
119
+ if (choice?.finish_reason === "length") {
120
+ throw new Error("Model output was truncated at the token limit after one retry; no generated code was accepted.");
121
+ }
121
122
  return content;
122
123
  }
123
124
  catch (e) {
@@ -176,14 +177,26 @@ export class AnthropicProvider {
176
177
  return this.cfg.model;
177
178
  }
178
179
  async complete(req) {
179
- const data = (await postJson(this.fetchImpl, `${this.cfg.baseUrl}/messages`, { "x-api-key": this.cfg.apiKey ?? "", "anthropic-version": "2023-06-01" }, {
180
- model: this.cfg.model,
181
- max_tokens: req.maxTokens ?? 900,
182
- temperature: req.temperature ?? 0.2,
183
- system: req.system,
184
- messages: [{ role: "user", content: req.user }]
185
- }));
186
- return (data.content ?? []).map((c) => c.text ?? "").join("");
180
+ let maxTokens = req.maxTokens ?? 900;
181
+ let retriedLength = false;
182
+ for (;;) {
183
+ const data = (await postJson(this.fetchImpl, `${this.cfg.baseUrl}/messages`, { "x-api-key": this.cfg.apiKey ?? "", "anthropic-version": "2023-06-01" }, {
184
+ model: this.cfg.model,
185
+ max_tokens: maxTokens,
186
+ temperature: req.temperature ?? 0.2,
187
+ system: req.system,
188
+ messages: [{ role: "user", content: req.user }]
189
+ }, providerTimeoutMs(this.cfg.model)));
190
+ if (data.stop_reason === "max_tokens" && !retriedLength) {
191
+ retriedLength = true;
192
+ maxTokens *= 4;
193
+ continue;
194
+ }
195
+ if (data.stop_reason === "max_tokens") {
196
+ throw new Error("Model output was truncated at the token limit after one retry; no generated code was accepted.");
197
+ }
198
+ return (data.content ?? []).map((c) => c.text ?? "").join("");
199
+ }
187
200
  }
188
201
  }
189
202
  /**
@@ -21,6 +21,7 @@ import { doctorGraph } from "./score/doctor.js";
21
21
  import { findGaps } from "./gaps/gaps.js";
22
22
  import { rankRiskGaps } from "./score/risk.js";
23
23
  import { generateTests } from "./generate/generator.js";
24
+ import { classifyGeneratedDraftBlocker } from "./generate/draftGuidance.js";
24
25
  import { autoProve, NO_KEY_MESSAGE, isEligibleProvableTarget } from "./autoProve.js";
25
26
  import { AGENT_RUN_WORKFLOW, GROUNDING_CONTRACT, runnableRunHintsFor } from "./generate/runHints.js";
26
27
  import { buildProvider, DeterministicProvider } from "./generate/providers.js";
@@ -1514,10 +1515,13 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
1514
1515
  const warnings = [...analyze.warnings];
1515
1516
  reportProgress("start: deterministic graph is ready", { current: 4, total: 8 });
1516
1517
  const staticSnapshot = writeStartStaticSnapshot(root, opts.baseRef, warnings);
1517
- const providerConfigured = deps.aiProvider !== undefined || resolveProviderConfig(providerEnv, providerOpts) !== null;
1518
+ const aiProviderConfigured = deps.aiProvider !== undefined || resolveProviderConfig(providerEnv, providerOpts) !== null;
1519
+ const resolvedGenerationProvider = deps.aiProvider ?? resolveGenerationProvider(providerEnv, providerOpts);
1520
+ const generationProviderConfigured = resolvedGenerationProvider !== null;
1521
+ const deterministicGeneration = resolvedGenerationProvider?.providerName === "deterministic";
1518
1522
  let aiLinks = { status: "skipped", reason: "AI candidate links disabled for this run." };
1519
1523
  if (opts.ai !== false) {
1520
- if (!providerConfigured) {
1524
+ if (!aiProviderConfigured) {
1521
1525
  reportProgress("ai: skipped — no local provider key/base URL found", { current: 5, total: 8 });
1522
1526
  aiLinks = {
1523
1527
  status: "skipped",
@@ -1542,7 +1546,7 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
1542
1546
  }
1543
1547
  let aiFlows = { status: "skipped", reason: "AI candidate flows disabled for this run." };
1544
1548
  if (opts.ai !== false && opts.aiFlows !== false) {
1545
- if (!providerConfigured) {
1549
+ if (!aiProviderConfigured) {
1546
1550
  aiFlows = {
1547
1551
  status: "skipped",
1548
1552
  reason: "No model provider configured; set OPENAI_API_KEY, ANTHROPIC_API_KEY, or OLLAMA_BASE_URL to auto-apply AI candidate flows."
@@ -1574,7 +1578,7 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
1574
1578
  let autoProveResult;
1575
1579
  try {
1576
1580
  autoProveResult = await autoProve(root, {
1577
- autoLimit: opts.autoLimit,
1581
+ autoLimit: opts.proofLimit ?? opts.autoLimit,
1578
1582
  noAuto: opts.noAuto,
1579
1583
  provider: providerOpts.provider,
1580
1584
  model: providerOpts.model,
@@ -1599,40 +1603,115 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
1599
1603
  };
1600
1604
  warnings.push(`auto-prove skipped: ${reason}`);
1601
1605
  }
1602
- if (!opts.noAuto && providerConfigured && opts.ai !== false) {
1606
+ const generationLimit = Math.max(0, Math.min(50, Math.floor(opts.generateLimit ?? START_GENERATE_RISK_LIMIT)));
1607
+ let generationResult = {
1608
+ status: "disabled",
1609
+ requested: generationLimit,
1610
+ generated: 0,
1611
+ runnable: 0,
1612
+ drafts: 0,
1613
+ blockers: {},
1614
+ reason: opts.noAuto
1615
+ ? "Test generation was disabled by --no-auto."
1616
+ : opts.ai === false
1617
+ ? "Test generation was disabled by --no-ai."
1618
+ : generationLimit === 0
1619
+ ? "Test generation budget is 0 (--generate-limit 0)."
1620
+ : undefined
1621
+ };
1622
+ if (!opts.noAuto && opts.ai !== false && generationLimit > 0 && !generationProviderConfigured) {
1623
+ generationResult = {
1624
+ ...generationResult,
1625
+ status: "no_provider",
1626
+ reason: NO_PROVIDER_MESSAGE
1627
+ };
1628
+ warnings.push(`generate: ${NO_PROVIDER_MESSAGE}`);
1629
+ }
1630
+ else if (!opts.noAuto && generationProviderConfigured && opts.ai !== false && generationLimit > 0) {
1603
1631
  try {
1604
1632
  const graphForGeneration = loadGraph(workspacePaths(root).graphPath);
1605
1633
  const generatedTargets = new Set((graphForGeneration.generated_tests ?? []).map((t) => t.target_symbol_external_id).filter((id) => Boolean(id)));
1606
1634
  const targetIds = rankRiskGaps(graphForGeneration, {
1607
1635
  repoRoot: root,
1608
- limit: START_GENERATE_RISK_LIMIT,
1636
+ limit: generationLimit,
1609
1637
  provenIds: provenSymbolIds(graphForGeneration, loadLedger(root))
1610
1638
  })
1611
1639
  .map((gap) => gap.id)
1612
1640
  .filter((id) => !generatedTargets.has(id));
1613
- if (targetIds.length) {
1641
+ if (!targetIds.length) {
1642
+ generationResult = {
1643
+ ...generationResult,
1644
+ status: "no_targets",
1645
+ reason: "No eligible ungenerated risk targets were found."
1646
+ };
1647
+ }
1648
+ else {
1614
1649
  reportProgress(`generate: drafting tests for top ${targetIds.length} risk target(s)`, { current: 6, total: 8 });
1615
- let accepted = 0;
1650
+ const generatedDrafts = [];
1616
1651
  for (let i = 0; i < targetIds.length; i += START_GENERATE_BATCH_LIMIT) {
1617
1652
  const batch = targetIds.slice(i, i + START_GENERATE_BATCH_LIMIT);
1618
1653
  const generated = await opGenerate(root, {
1619
1654
  ...providerOpts,
1620
1655
  target_ids: batch,
1621
1656
  limit: batch.length,
1622
- prompt_version: opts.promptVersion ?? "v5"
1657
+ // The offline deterministic stand-in emits the established v2 scaffold; v5 is
1658
+ // a two-phase model planning protocol and must not be selected implicitly for it.
1659
+ prompt_version: opts.promptVersion ?? (deterministicGeneration ? "v2" : "v5")
1623
1660
  }, providerDeps);
1624
- accepted += generated.generated_tests.filter((t) => t.runnable !== false).length;
1661
+ generatedDrafts.push(...generated.generated_tests);
1625
1662
  warnings.push(...generated.warnings.map((w) => `generate: ${w}`));
1626
1663
  }
1627
- if (accepted === 0)
1628
- warnings.push("generate: provider returned no accepted tests for the top risk targets.");
1664
+ const blockers = {};
1665
+ for (const draft of generatedDrafts.filter((test) => test.runnable === false)) {
1666
+ const blocker = classifyGeneratedDraftBlocker(draft.unresolved_reason);
1667
+ blockers[blocker] = (blockers[blocker] ?? 0) + 1;
1668
+ }
1669
+ const runnable = generatedDrafts.filter((test) => test.runnable !== false).length;
1670
+ generationResult = {
1671
+ status: generatedDrafts.length === 0
1672
+ ? "no_results"
1673
+ : generatedDrafts.some((test) => test.runnable === false)
1674
+ ? "completed_with_blockers"
1675
+ : "completed",
1676
+ requested: targetIds.length,
1677
+ generated: generatedDrafts.length,
1678
+ runnable,
1679
+ drafts: generatedDrafts.length - runnable,
1680
+ blockers,
1681
+ ...(generatedDrafts.length === 0
1682
+ ? { reason: "The provider returned no generated-test drafts for the selected risk targets." }
1683
+ : {})
1684
+ };
1685
+ if (generatedDrafts.length === 0)
1686
+ warnings.push(`generate: ${generationResult.reason}`);
1629
1687
  }
1630
1688
  }
1631
1689
  catch (err) {
1632
- const reason = err instanceof Error ? err.message : String(err);
1690
+ const reason = redactSecrets(err instanceof Error ? err.message : String(err));
1691
+ generationResult = {
1692
+ ...generationResult,
1693
+ status: "failed",
1694
+ reason
1695
+ };
1633
1696
  warnings.push(`generate skipped: ${reason}`);
1634
1697
  }
1635
1698
  }
1699
+ try {
1700
+ const graphWithGeneration = loadGraph(workspacePaths(root).graphPath);
1701
+ if (!graphWithGeneration.analysis)
1702
+ throw new Error("analysis metadata is missing after analyze");
1703
+ saveGraph(workspacePaths(root).graphPath, {
1704
+ ...graphWithGeneration,
1705
+ updated_at: deps.clock(),
1706
+ analysis: {
1707
+ ...graphWithGeneration.analysis,
1708
+ start_generation: generationResult
1709
+ }
1710
+ });
1711
+ }
1712
+ catch (error) {
1713
+ warnings.push(`generation outcome not persisted: ${error instanceof Error ? error.message : String(error)}`);
1714
+ }
1636
1715
  // G1: persist the distilled, already-redacted attempt classifications so
1637
1716
  // `opro doctor --proof` and standalone report regens can explain blockers
1638
1717
  // after this process exits. Sidecar only — never read by the oracle, RTM,
@@ -1767,6 +1846,7 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
1767
1846
  changed,
1768
1847
  gaps,
1769
1848
  auto_prove: autoProveResult,
1849
+ generation: generationResult,
1770
1850
  next_actions: nextActions,
1771
1851
  agent_workflow: AGENT_RUN_WORKFLOW,
1772
1852
  grounding_contract: GROUNDING_CONTRACT,
@@ -74,12 +74,22 @@ export function renderCoverageReport(graph, ledger = emptyLedger()) {
74
74
  lines.push(`| ${language} | ${row.covered} / ${row.eligible} | ${row.covered_pct}% | ${row.symbols_with_spans} |`);
75
75
  }
76
76
  lines.push("");
77
+ lines.push("Coverage by test suite (inclusive counts; unit + integration must not be added because overlap is reported separately):");
78
+ lines.push("");
79
+ lines.push("| suite | covered symbols | eligible % |");
80
+ lines.push("| --- | ---: | ---: |");
81
+ lines.push(`| unit | ${runtime.by_suite.unit} | ${runtime.by_suite.unit_pct}% |`);
82
+ lines.push(`| integration | ${runtime.by_suite.integration} | ${runtime.by_suite.integration_pct}% |`);
83
+ lines.push(`| unit + integration overlap | ${runtime.by_suite.overlap} | ${runtime.by_suite.overlap_pct}% |`);
84
+ lines.push(`| unclassified | ${runtime.by_suite.unclassified} | ${runtime.by_suite.unclassified_pct}% |`);
85
+ lines.push(`| combined union | ${runtime.by_suite.union} | ${runtime.by_suite.union_pct}% |`);
86
+ lines.push("");
77
87
  lines.push("Artifacts:");
78
88
  lines.push("");
79
- lines.push("| artifact | format | files | covered ranges |");
80
- lines.push("| --- | --- | ---: | ---: |");
89
+ lines.push("| artifact | suite | provenance | command | format | files | covered ranges |");
90
+ lines.push("| --- | --- | --- | --- | --- | ---: | ---: |");
81
91
  for (const artifact of runtime.artifacts) {
82
- lines.push(`| ${escapeMd(artifact.path)} | ${artifact.format} | ${artifact.files} | ${artifact.covered_ranges} |`);
92
+ lines.push(`| ${escapeMd(artifact.path)} | ${artifact.suite} | ${artifact.suite_source} | ${escapeMd(artifact.command ?? "-")} | ${artifact.format} | ${artifact.files} | ${artifact.covered_ranges} |`);
83
93
  }
84
94
  lines.push("");
85
95
  if (runtime.skipped_artifacts?.length) {
@@ -4,6 +4,7 @@ import { buildRtm } from "../rtm.js";
4
4
  import { inspectRiskInputHealth, isEntryPoint, rankRiskGaps } from "../score/risk.js";
5
5
  import { ORANGEPRO_VERSION } from "../version.js";
6
6
  import { PROOF_BLOCKER_GUIDE } from "../proofDoctor.js";
7
+ import { classifyGeneratedDraftBlocker } from "../generate/draftGuidance.js";
7
8
  /** Short human phrase per R-1 needs_setup category, for the "blocked because: …" panel copy. */
8
9
  const BLOCK_CATEGORY_LABEL = {
9
10
  module_not_found: "a missing module or dependency in the sandbox",
@@ -229,6 +230,7 @@ function scanBlock(graph, rows) {
229
230
  const integration = tests.filter((n) => n.properties.test_layer === "integration" || n.properties.test_layer === "api" || n.properties.test_layer === "e2e").length;
230
231
  const unit = tests.filter((n) => n.properties.test_layer === "unit" || n.properties.test_layer === "component").length;
231
232
  const denominator = graph.analysis?.denominator;
233
+ const runtime = graph.analysis?.runtime_coverage;
232
234
  const excludedCount = (denominator?.excluded_boilerplate ?? 0) +
233
235
  (denominator?.excluded_infra ?? 0) +
234
236
  (denominator?.excluded_generated ?? 0) +
@@ -236,7 +238,22 @@ function scanBlock(graph, rows) {
236
238
  return {
237
239
  services: [...services.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 50),
238
240
  serviceTotal: services.size,
239
- tests: { total: tests.length, integration, unit },
241
+ tests: { total: tests.length, integration, unit, unclassified: Math.max(0, tests.length - integration - unit) },
242
+ runtimeCoverage: runtime
243
+ ? {
244
+ eligible: runtime.total_eligible_symbols,
245
+ unit: runtime.by_suite?.unit ?? 0,
246
+ integration: runtime.by_suite?.integration ?? 0,
247
+ overlap: runtime.by_suite?.overlap ?? 0,
248
+ unclassified: runtime.by_suite?.unclassified ?? runtime.covered_symbols,
249
+ union: runtime.by_suite?.union ?? runtime.covered_symbols,
250
+ unitPct: runtime.by_suite?.unit_pct ?? 0,
251
+ integrationPct: runtime.by_suite?.integration_pct ?? 0,
252
+ overlapPct: runtime.by_suite?.overlap_pct ?? 0,
253
+ unclassifiedPct: runtime.by_suite?.unclassified_pct ?? runtime.covered_pct,
254
+ unionPct: runtime.by_suite?.union_pct ?? runtime.covered_pct
255
+ }
256
+ : null,
240
257
  excluded: {
241
258
  count: excludedCount > 0 ? String(excludedCount) : "0",
242
259
  text: "non-behavior symbols were excluded from the behavior count — generated code, framework internals, test-inferred flows, and infrastructure plumbing."
@@ -415,7 +432,8 @@ function riskGeneratedTests(graph, gap, riskIds, isFirstRowForFile) {
415
432
  .filter(Boolean)
416
433
  .join(" · "),
417
434
  code: t.body,
418
- runnable: t.runnable !== false
435
+ runnable: t.runnable !== false,
436
+ ...(t.runnable === false ? { blocker: classifyGeneratedDraftBlocker(t.unresolved_reason) } : {})
419
437
  }));
420
438
  }
421
439
  /** Incoming refs are method-attributed and can be fractional when a file-level
@@ -758,7 +776,23 @@ function riskTodo(risk, verb, path, generatedTests) {
758
776
  return "Run the generated test below in your repo; follow its prove handoff so a mutation failure can mint Dynamically Proven.";
759
777
  }
760
778
  if (generatedTests.length) {
761
- return "Install this repo's dependencies / set up the test runner, then re-run `opro start` to turn the English intents below into runnable tests.";
779
+ const blockers = new Set(generatedTests.map((t) => t.blocker ?? "unknown"));
780
+ if (blockers.size === 1) {
781
+ const blocker = [...blockers][0];
782
+ if (blocker === "generated_code") {
783
+ return "OrangePro withheld the generated code because compile validation found invalid or invented generated code. Review the blocker below, then regenerate or repair the draft; installing dependencies is not the fix.";
784
+ }
785
+ if (blocker === "unresolved_import") {
786
+ return "OrangePro withheld the generated code because an import path did not resolve. Verify that the import exists and is declared by this repo; install dependencies only when the repo expects it, then regenerate.";
787
+ }
788
+ if (blocker === "toolchain_or_runner") {
789
+ return "OrangePro could not validate the generated test because the named toolchain or test runner is unavailable. Install/configure it, then re-run `opro start`.";
790
+ }
791
+ if (blocker === "validation_timeout") {
792
+ return "OrangePro's generated-test validation timed out. Warm the dependency cache or raise the named validation timeout, then re-run `opro start`.";
793
+ }
794
+ }
795
+ 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.";
762
796
  }
763
797
  const call = verb !== "BEHAVIOR"
764
798
  ? `issues ${verb} ${path}`
@@ -908,6 +942,9 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
908
942
  }
909
943
  },
910
944
  generatedTotal: graph.generated_tests?.length ?? 0,
945
+ generatedRunnableTotal: (graph.generated_tests ?? []).filter((t) => t.runnable !== false).length,
946
+ generatedDraftTotal: (graph.generated_tests ?? []).filter((t) => t.runnable === false).length,
947
+ generationOutcome: graph.analysis?.start_generation ?? null,
911
948
  shownCount: risks.reduce((acc, r) => acc + r.generatedTests.length, 0)
912
949
  };
913
950
  }
@@ -366,9 +366,26 @@ body[data-mode="expert"] .simple-only{display:none!important}
366
366
  <div class="split2">
367
367
  <div><div class="v vb" id="test-int">—</div><div class="k">Integration</div></div>
368
368
  <div><div class="v vp" id="test-unit">—</div><div class="k">Unit</div></div>
369
+ <div><div class="v" id="test-unknown">—</div><div class="k">Unclassified</div></div>
369
370
  </div>
370
371
  </div>
371
372
  </div>
373
+ <div class="card" id="runtime-suite-card" hidden style="margin-top:16px">
374
+ <p class="card-lbl">Runtime coverage by suite — inclusive symbol counts; unit + integration overlap is shown separately and must not be added.</p>
375
+ <div class="split2">
376
+ <div><div class="v vp" id="runtime-unit">—</div><div class="k">Unit</div></div>
377
+ <div><div class="v vb" id="runtime-integration">—</div><div class="k">Integration</div></div>
378
+ <div><div class="v" id="runtime-overlap">—</div><div class="k">Both</div></div>
379
+ <div><div class="v" id="runtime-unclassified">—</div><div class="k">Unclassified</div></div>
380
+ <div><div class="v vg" id="runtime-union">—</div><div class="k">Combined union</div></div>
381
+ </div>
382
+ </div>
383
+ <div class="card" id="generation-outcome-card" hidden style="margin-top:16px">
384
+ <p class="card-lbl">Latest automated test-generation run</p>
385
+ <div class="v" id="generation-outcome-status">—</div>
386
+ <p class="card-sub" id="generation-outcome-counts"></p>
387
+ <p class="card-sub" id="generation-outcome-reason"></p>
388
+ </div>
372
389
  </section>
373
390
 
374
391
  <!-- TAB 2: BEHAVIORS — bridge from "methods" to "behaviors" -->
@@ -697,6 +714,23 @@ D.scan.services.forEach(([nm,ct])=>$("#svc-list").append(el("div","svc",\`<span
697
714
  $("#test-total").textContent=D.scan.tests.total;
698
715
  $("#test-int").textContent=D.scan.tests.integration;
699
716
  $("#test-unit").textContent=D.scan.tests.unit;
717
+ $("#test-unknown").textContent=D.scan.tests.unclassified;
718
+ if(D.scan.runtimeCoverage){
719
+ const R=D.scan.runtimeCoverage;
720
+ $("#runtime-suite-card").hidden=false;
721
+ $("#runtime-unit").textContent=\`\${R.unit} (\${R.unitPct}%)\`;
722
+ $("#runtime-integration").textContent=\`\${R.integration} (\${R.integrationPct}%)\`;
723
+ $("#runtime-overlap").textContent=\`\${R.overlap} (\${R.overlapPct}%)\`;
724
+ $("#runtime-unclassified").textContent=\`\${R.unclassified} (\${R.unclassifiedPct}%)\`;
725
+ $("#runtime-union").textContent=\`\${R.union} (\${R.unionPct}%)\`;
726
+ }
727
+ if(D.generationOutcome){
728
+ const G=D.generationOutcome;
729
+ $("#generation-outcome-card").hidden=false;
730
+ $("#generation-outcome-status").textContent=String(G.status||"unknown").replaceAll("_"," ");
731
+ $("#generation-outcome-counts").textContent=G.generated+" draft(s): "+G.runnable+" runnable, "+Math.max(0,G.drafts-G.runnable)+" blocked; "+G.requested+" target(s) requested.";
732
+ $("#generation-outcome-reason").textContent=G.reason||"Generation completed; inspect each draft for its terminal validation result.";
733
+ }
700
734
 
701
735
  // behaviors
702
736
  function humanizeSig(sig){
@@ -883,6 +917,10 @@ const riskTopBanner=el("div","platform-top-banner",
883
917
  <a class="platform-footer-btn" href="https://orangepro.ai/get-started" target="_blank">Unlock Full Analysis &rarr;</a>\`);
884
918
  riskList.before(riskTopBanner);
885
919
  const generatedRiskCount=D.risks.filter(r=>r.generatedTests&&r.generatedTests.length).length;
920
+ const generatedOutputCopy=[
921
+ D.generatedRunnableTotal?\`\${D.generatedRunnableTotal} runnable generated test\${D.generatedRunnableTotal===1?"":"s"}\`:'',
922
+ D.generatedDraftTotal?\`\${D.generatedDraftTotal} grounded draft\${D.generatedDraftTotal===1?"":"s"} with code withheld\`:'',
923
+ ].filter(Boolean).join(' and ');
886
924
  let activeRiskFilter=generatedRiskCount?"generated":"all";
887
925
  function riskMatchesFilter(r){
888
926
  const hasGenerated=Boolean(r.generatedTests&&r.generatedTests.length);
@@ -893,7 +931,7 @@ function riskMatchesFilter(r){
893
931
  function renderRiskFilters(){
894
932
  const options=[
895
933
  ["all","All",D.risks.length],
896
- ["generated","Flows with tests",generatedRiskCount],
934
+ ["generated","Flows with generated output",generatedRiskCount],
897
935
  ["missing","No generated tests",D.risks.filter(r=>!(r.generatedTests&&r.generatedTests.length)).length]
898
936
  ];
899
937
  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("");
@@ -922,7 +960,7 @@ function riskCardHtml(r){
922
960
  // per-test chips whenever the generator produced unit tests.
923
961
  const allIntent=r.generatedTests.every(t=>t.runnable===false);
924
962
  const kinds=[...new Set(r.generatedTests.map(t=>t.concern).filter(Boolean))];
925
- const kindLbl=allIntent?" (Manual testsenv setup needed)":kinds.length===1?" ("+kinds[0].replace(/_/g," ")+")":kinds.length>1?" (mixed)":"";
963
+ const kindLbl=allIntent?" (Generated draftscode withheld)":kinds.length===1?" ("+kinds[0].replace(/_/g," ")+")":kinds.length>1?" (mixed)":"";
926
964
  testsHtml=\`<div class="gen-tests"><div class="gen-tests-lbl">Generated tests\${esc(kindLbl)}</div>\`;
927
965
  r.generatedTests.forEach(t=>{
928
966
  const cBadge=t.concern?\`<span class="badge b-info" style="margin-left:6px;font-size:9px">\${esc(t.concern.replace('_',' '))}</span>\`:'';
@@ -946,12 +984,12 @@ function renderRisks(){
946
984
  const remainingRiskFlows=Math.max(0,D.risks.length-generatedRiskCount);
947
985
  riskList.append(el("div","paywall",
948
986
  hiddenGeneratedFlows
949
- ? \`<div class="paywall-num">\${hiddenGeneratedFlows} more flows with tests</div>
950
- <div class="paywall-txt">OrangePro accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Use the “Flows with tests” filter to review them first.</div>
987
+ ? \`<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 generated output” filter to review them first.</div>
951
989
  <a class="paywall-btn" href="https://orangepro.ai/get-started" target="_blank">View all on OrangePro Platform &rarr;</a>\`
952
990
  : remainingRiskFlows
953
991
  ? \`<div class="paywall-num">\${remainingRiskFlows} high-risk flows left</div>
954
- <div class="paywall-txt">The local MCP accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Generate the remaining high-risk flow tests on OrangePro Platform.</div>
992
+ <div class="paywall-txt">The local MCP produced \${generatedOutputCopy} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Generate the remaining high-risk flow tests on OrangePro Platform.</div>
955
993
  <a class="paywall-btn" href="https://orangepro.ai/get-started" target="_blank">Generate remaining tests on Platform &rarr;</a>\`
956
994
  : \`<div class="paywall-num">All generated tests are shown</div>
957
995
  <div class="paywall-txt">OrangePro generated tests for every high-risk flow in this report, and every generated test is visible here.</div>\`));
@@ -264,6 +264,14 @@ local kit ingests Go coverprofiles, JS/TS `lcov.info`, Python coverage.py XML, a
264
264
  XML. These artifacts show executed lines inside symbols; they are not assertion-level proof and
265
265
  never promote Associated signal or No integration signal rows to Proven.
266
266
 
267
+ For the highest-signal report in a repository with its own build/test workflow, bootstrap the
268
+ repository first, run its native coverage target, optionally use `opro coverage .` to confirm the
269
+ artifact is detected, and then run `opro start .`. The `coverage` command is discovery/generation
270
+ only; `analyze` and `start` ingest detected artifacts while rebuilding the graph. Running
271
+ `opro analyze .` immediately before `opro start .` is redundant. `opro start . --generate-coverage`
272
+ is a convenience for conventional repositories; prefer the repository's native coverage command
273
+ when it needs custom build tags, generated assets, services, or test-runner flags.
274
+
267
275
  ## Readiness score
268
276
 
269
277
  A 0–100 **readiness** signal (not a proof of lift), in bands: `thin` (0–39),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangepro/orangepro-mcp",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
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",