@orangepro/orangepro-mcp 0.2.3 → 0.2.5

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.
@@ -98,6 +98,10 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
98
98
  .beh-card:hover::before{opacity:1}
99
99
  .beh-card[data-t="proven"]{--gw:var(--gbg);border-color:rgba(63,185,80,.22)}
100
100
  .beh-card[data-t="proven"]:hover{border-color:var(--gbd)}
101
+ .beh-card[data-t="candidate"]{--gw:rgba(163,113,247,.13)}
102
+ .beh-card[data-t="candidate"]:hover{border-color:rgba(163,113,247,.38)}
103
+ .beh-card[data-t="candidate"] .bc-bar{background:#a371f7}
104
+ .b-cand{color:#a371f7;background:rgba(163,113,247,.13);border-color:rgba(163,113,247,.38)} .b-cand .d{background:#a371f7}
101
105
  .beh-card[data-t="assoc"]{--gw:var(--abg)}
102
106
  .beh-card[data-t="assoc"]:hover{border-color:var(--abd)}
103
107
  .beh-card[data-t="reach"]{--gw:var(--bbg)}
@@ -165,6 +169,25 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
165
169
  .risk-filter:hover{border-color:var(--bd2);color:var(--ink2)}
166
170
  .risk-filter[aria-pressed="true"]{border-color:var(--orange);background:var(--obg);color:var(--orange)}
167
171
  .risk-rank{font-size:10px;color:var(--orange);font-weight:700;margin-bottom:3px}
172
+ .risk-ctx{font-size:12px;color:var(--ink3);line-height:1.5;margin:4px 0 6px}
173
+ #sysmap svg{display:block;width:100%;height:auto}
174
+ .sm-lane{fill:var(--s2);stroke:var(--bd2)}
175
+ .sm-lane-lbl{font:600 11px var(--mono);fill:var(--ink2)}
176
+ .sm-svc-lbl{font:600 10.5px var(--mono);fill:var(--ink2)}
177
+ .sm-sub{font:9px var(--sans,sans-serif);fill:var(--muted)}
178
+ .sm-edge{fill:none;opacity:.45}
179
+ .sm-node{cursor:pointer}
180
+ .sm-node:hover circle{stroke-width:2.5}
181
+ .sm-crumb{display:flex;gap:8px;margin-bottom:12px}
182
+ .sm-crumb-btn{appearance:none;background:var(--s2);border:1px solid var(--bd2);color:var(--ink2);font:600 11px var(--mono);padding:6px 11px;border-radius:7px;cursor:pointer}
183
+ .sm-crumb-btn:hover{background:var(--s3);color:var(--ink)}
184
+ .delta-wrap{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:14px}
185
+ .delta-chip{font:600 11px var(--mono);padding:5px 10px;border-radius:999px;border:1px solid var(--bd2);background:var(--s2);color:var(--ink2)}
186
+ .delta-chip.dc-up{border-color:var(--green);color:var(--green)}
187
+ .delta-chip.dc-risk{border-color:var(--red);color:var(--red)}
188
+ .delta-chip.dc-none{color:var(--muted)}
189
+ .beh-empty{grid-column:1/-1;padding:22px;border:1px dashed var(--bd2);border-radius:10px;color:var(--muted);font-size:12.5px;display:flex;flex-direction:column;gap:10px;align-items:flex-start}
190
+ .beh-empty p{margin:0}
168
191
  .risk-ep{font-family:var(--mono);font-size:13px;margin:0 0 6px}
169
192
  .risk-ep .v{color:var(--green);font-weight:700}
170
193
  .risk-tags{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px}
@@ -244,14 +267,19 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
244
267
 
245
268
  <nav class="tabs" role="tablist">
246
269
  <button class="tab" role="tab" aria-selected="true" data-tab="codebase">Your Code</button>
247
- <button class="tab" role="tab" aria-selected="false" data-tab="behaviors">Behaviors <span class="tc" id="t-beh">—</span></button>
248
- <button class="tab" role="tab" aria-selected="false" data-tab="flows">Flows <span class="tc" id="t-flow">—</span></button>
249
270
  <button class="tab" role="tab" aria-selected="false" data-tab="risks">Risks <span class="tc" id="t-risk">—</span></button>
271
+ <button class="tab" role="tab" aria-selected="false" data-tab="flows">Flows <span class="tc" id="t-flow">—</span></button>
272
+ <button class="tab" role="tab" aria-selected="false" data-tab="behaviors">Behaviors <span class="tc" id="t-beh">—</span></button>
250
273
  </nav>
251
274
 
252
275
  <!-- TAB 1: YOUR CODE — familiar territory -->
253
276
  <section class="panel active" id="panel-codebase" role="tabpanel">
254
277
  <p class="bridge">We scanned your repo and found <b id="br-methods">—</b> public methods across <b id="br-services">—</b> services, with <b id="br-tests">—</b> test files. Here's what we're working with.</p>
278
+ <div id="delta-banner"></div>
279
+ <div class="card" id="sysmap-card" hidden>
280
+ <p class="card-lbl">Your system, as the graph sees it — entry lanes flowing into the services they reach. Node size = flow traffic; color = dominant evidence tier; red ring = top-20 risk. Identical on every run.</p>
281
+ <div id="sysmap"></div>
282
+ </div>
255
283
  <div class="cols2">
256
284
  <div class="card">
257
285
  <p class="card-lbl">Framework</p>
@@ -288,6 +316,7 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
288
316
  <!-- TAB 3: FLOWS — bridge from "methods" to "user journeys" -->
289
317
  <section class="panel" id="panel-flows" role="tabpanel">
290
318
  <p class="bridge">A <b>flow</b> is the sequence of methods that execute when a real request hits your system. Each box is a method you already saw in the Behaviors tab — but here they're connected as a call chain. Solid lines = hard-coded calls. Dashed = framework-derived. <span style="color:var(--green)">Green boxes</span> pulse because a test proves they break if mutated.</p>
319
+ <p class="bridge" id="flow-cap-note" style="font-size:12px;opacity:.75"></p>
291
320
  <div id="flow-list"></div>
292
321
  <div class="ai-sec" id="ai-sec" hidden>
293
322
  <div class="ai-lbl">AI-suggested flows — plausible paths, not proven. Dashed borders = unverified.</div>
@@ -297,7 +326,8 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
297
326
 
298
327
  <!-- TAB 4: RISKS — actionable -->
299
328
  <section class="panel" id="panel-risks" role="tabpanel">
300
- <p class="bridge">These are the flows with the highest blast radius and the weakest test coverage. Each one tells you exactly what to do next.</p>
329
+ <p class="bridge">These are the flows with the highest blast radius and the weakest test coverage, <b>sorted highest risk first</b>. Each one tells you exactly what to do next.</p>
330
+ <p class="bridge" id="risk-cap-note" style="font-size:12px;opacity:.75"></p>
301
331
  <div class="risk-tools" id="risk-tools"></div>
302
332
  <div id="risk-list"></div>
303
333
  </section>
@@ -316,6 +346,123 @@ window.DATA = __ORANGEPRO_DATA__;
316
346
 
317
347
  (function(){
318
348
  const D=window.DATA,$=(s)=>document.querySelector(s);
349
+
350
+
351
+ // ── Delta since last run (display-only; baseline written each run) ──
352
+ (function(){
353
+ var el2=document.getElementById("delta-banner");
354
+ var d=D.delta;
355
+ if(!el2||d===undefined)return;
356
+ if(d===null){el2.innerHTML='';return;} // first run — baseline just recorded, nothing to compare
357
+ var when=new Date(d.baselineTs);
358
+ var ago=isNaN(when.getTime())?"last run":when.toLocaleString();
359
+ if(!d.changed){
360
+ el2.innerHTML='<div class="delta-wrap"><span class="delta-chip dc-none">No changes since last run ('+ago+') — identical graph, identical ranking</span></div>';
361
+ return;
362
+ }
363
+ var chips=[];
364
+ function chip(txt,cls){chips.push('<span class="delta-chip '+(cls||'')+'">'+txt+'</span>');}
365
+ chip('Since '+ago+':','dc-none');
366
+ if(d.totalDelta)chip((d.totalDelta>0?'+':'')+d.totalDelta+' behaviors');
367
+ if(d.provenDelta)chip((d.provenDelta>0?'+':'')+d.provenDelta+' proven',d.provenDelta>0?'dc-up':'dc-risk');
368
+ if(d.associatedDelta)chip((d.associatedDelta>0?'+':'')+d.associatedDelta+' test signal');
369
+ if(d.generatedDelta)chip((d.generatedDelta>0?'+':'')+d.generatedDelta+' generated tests',d.generatedDelta>0?'dc-up':'');
370
+ d.newRisks.slice(0,3).forEach(function(p){chip('new in top-20: '+p.slice(0,40),'dc-risk');});
371
+ if(d.newRisks.length>3)chip('+'+(d.newRisks.length-3)+' more new risks','dc-risk');
372
+ d.droppedRisks.slice(0,2).forEach(function(p){chip('left top-20: '+p.slice(0,40),'dc-up');});
373
+ el2.innerHTML='<div class="delta-wrap">'+chips.join('')+'</div>';
374
+ })();
375
+ // ── System map: deterministic hero drawn from D.mapModel ──
376
+ (function(){
377
+ var M=D.mapModel;
378
+ if(!M||!M.lanes||!M.lanes.length||!M.services.length)return;
379
+ document.getElementById("sysmap-card").hidden=false;
380
+ var W=760,rowH=44,laneW=118,svcX=470,padT=18;
381
+ var H=padT*2+Math.max(M.lanes.length,M.services.length)*rowH;
382
+ var laneY={},svcY={};
383
+ M.lanes.forEach(function(l,i){laneY[l.id]=padT+i*rowH+(H-2*padT-M.lanes.length*rowH)/2+rowH/2;});
384
+ M.services.forEach(function(sv,i){svcY[sv.id]=padT+i*rowH+rowH/2;});
385
+ var laneColor={graphql:"var(--purple,#a78bfa)",http:"var(--blue)",job:"var(--orange)"};
386
+ var tierColor=function(t){var tot=t.proven+t.assoc+t.candidate+t.none||1;
387
+ if(t.proven/tot>=0.5)return "var(--green)";
388
+ if((t.proven+t.assoc)/tot>=0.5)return "var(--blue)";
389
+ if(t.none/tot>=0.5)return "var(--red)";
390
+ return "var(--purple,#a78bfa)";};
391
+ var maxF=M.services[0].flows||1;
392
+ var out='<svg viewBox="0 0 '+W+' '+H+'" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="System map">';
393
+ M.edges.forEach(function(e){
394
+ var y1=laneY[e.lane],y2=svcY[e.service];if(y1==null||y2==null)return;
395
+ var w=1+3*Math.log(1+e.flows)/Math.log(1+maxF);
396
+ out+='<path class="sm-edge" d="M '+(laneW+8)+' '+y1+' C '+(laneW+150)+' '+y1+', '+(svcX-150)+' '+y2+', '+(svcX-14)+' '+y2+'" stroke="'+(laneColor[e.lane]||"var(--muted)")+'" stroke-width="'+w.toFixed(1)+'"/>';
397
+ });
398
+ M.lanes.forEach(function(l){
399
+ var y=laneY[l.id];
400
+ out+='<rect class="sm-lane" x="4" y="'+(y-15)+'" width="'+laneW+'" height="30" rx="7"/>';
401
+ out+='<text class="sm-lane-lbl" x="14" y="'+(y-1)+'">'+l.label+'</text>';
402
+ out+='<text class="sm-sub" x="14" y="'+(y+11)+'">'+l.flows+' flow'+(l.flows===1?'':'s')+'</text>';
403
+ });
404
+ M.services.forEach(function(sv){
405
+ var y=svcY[sv.id];
406
+ var r=7+7*Math.sqrt(sv.flows/maxF);
407
+ if(sv.rest){
408
+ out+='<g><circle cx="'+svcX+'" cy="'+y+'" r="'+r.toFixed(1)+'" fill="none" stroke="var(--muted)" stroke-dasharray="4 3" stroke-width="1.3"/>'
409
+ +'<text class="sm-svc-lbl" x="'+(svcX+r+10)+'" y="'+(y+1)+'" fill="var(--muted)">'+sv.label+'</text>'
410
+ +'<text class="sm-sub" x="'+(svcX+r+10)+'" y="'+(y+13)+'">'+sv.flows+' flow'+(sv.flows===1?'':'s')+' below the cut</text>'
411
+ +'</g>';
412
+ return;
413
+ }
414
+ var ring=sv.riskRanks.length?'<circle cx="'+svcX+'" cy="'+y+'" r="'+(r+3.5)+'" fill="none" stroke="var(--red)" stroke-width="'+(sv.critical?2.5:1.4)+'"'+(sv.critical?'':' stroke-dasharray="3 2"')+'/>':'';
415
+ var label=sv.label.length>30?sv.label.slice(0,29)+"…":sv.label;
416
+ var riskTxt=sv.riskRanks.length?(" · risk #"+sv.riskRanks[0]):"";
417
+ out+='<g class="sm-node" data-svc="'+sv.label.replace(/"/g,"&quot;")+'">'+ring
418
+ +'<circle cx="'+svcX+'" cy="'+y+'" r="'+r.toFixed(1)+'" fill="'+tierColor(sv.tiers)+'" fill-opacity=".85" stroke="var(--bd2)"/>'
419
+ +'<text class="sm-svc-lbl" x="'+(svcX+r+10)+'" y="'+(y+1)+'">'+label+'</text>'
420
+ +'<text class="sm-sub" x="'+(svcX+r+10)+'" y="'+(y+13)+'">'+sv.flows+' flow'+(sv.flows===1?'':'s')+riskTxt+'</text>'
421
+ +'</g>';
422
+ });
423
+ out+='</svg>';
424
+ document.getElementById("sysmap").innerHTML=out;
425
+ document.getElementById("sysmap").addEventListener("click",function(ev){
426
+ var g=ev.target.closest?ev.target.closest(".sm-node"):null;
427
+ if(!g)return;
428
+ var svc=g.getAttribute("data-svc");
429
+ var search=document.getElementById("beh-search");
430
+ var tab=document.querySelector('[data-tab="behaviors"]');
431
+ if(tab)tab.click();
432
+ if(search){search.value=svc;search.dispatchEvent(new Event("input"));}
433
+ // Breadcrumb back to the map — tab-switching under the user with no
434
+ // visible return path is a trap; refresh must never be the escape hatch.
435
+ var bc=document.getElementById("sm-breadcrumb");
436
+ if(!bc){
437
+ bc=document.createElement("div");
438
+ bc.id="sm-breadcrumb";
439
+ bc.className="sm-crumb";
440
+ var panel=document.getElementById("panel-behaviors");
441
+ panel.insertBefore(bc,panel.firstChild);
442
+ }
443
+ bc.innerHTML='';
444
+ var backBtn=document.createElement("button");
445
+ backBtn.className="sm-crumb-btn";
446
+ backBtn.textContent="\u2190 Back to system map";
447
+ backBtn.addEventListener("click",function(){
448
+ if(search){search.value="";search.dispatchEvent(new Event("input"));}
449
+ bc.remove();
450
+ var home=document.querySelector('[data-tab="codebase"]');
451
+ if(home)home.click();
452
+ window.scrollTo({top:0});
453
+ });
454
+ var clearBtn=document.createElement("button");
455
+ clearBtn.className="sm-crumb-btn";
456
+ clearBtn.textContent="\u2715 Clear filter \u201c"+svc+"\u201d";
457
+ clearBtn.addEventListener("click",function(){
458
+ if(search){search.value="";search.dispatchEvent(new Event("input"));}
459
+ bc.remove();
460
+ });
461
+ bc.appendChild(backBtn);
462
+ bc.appendChild(clearBtn);
463
+ window.scrollTo({top:0});
464
+ });
465
+ })();
319
466
  const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
320
467
  const esc=s=>String(s).replace(/[&<>]/g,m=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[m]));
321
468
  const S=D.summary;
@@ -340,7 +487,8 @@ $("#t-risk").textContent=D.risks.length;
340
487
  [
341
488
  {lbl:"Methods found",num:S.total,sub:"public, with observable outcome",t:"total"},
342
489
  {lbl:"Dynamically Proven",num:S.proven,sub:"test breaks if you change it",t:"proven"},
343
- {lbl:"Test signal",num:S.associated,sub:"test touches it, no proof",t:"signal"},
490
+ {lbl:"Test signal",num:S.associated,sub:"hard static test link, no proof",t:"signal"},
491
+ {lbl:"Candidate",num:S.candidate??0,sub:"lexical match only — unconfirmed",t:"cand"},
344
492
  {lbl:"Reachable",num:S.reachableUntested,sub:"called but untested",t:"reach"},
345
493
  {lbl:"No signal",num:S.noSignal,sub:"nothing touches it",t:"nosig"},
346
494
  ].forEach(k=>{
@@ -356,14 +504,39 @@ $("#test-int").textContent=D.scan.tests.integration;
356
504
  $("#test-unit").textContent=D.scan.tests.unit;
357
505
 
358
506
  // behaviors
507
+ function humanizeSig(sig){
508
+ const m=String(sig||"");
509
+ const method=m.includes(".")?m.slice(m.lastIndexOf(".")+1):m;
510
+ const owner=m.includes(".")?m.slice(0,m.lastIndexOf(".")):"";
511
+ const words=method.replace(/[_$]/g," ").replace(/([a-z0-9])([A-Z])/g,"$1 $2").toLowerCase().trim();
512
+ return {words:words||m, owner};
513
+ }
514
+ function describeBehavior(b){
515
+ const h=humanizeSig(b.sig);
516
+ const mod=(b.file||"").split("/").slice(0,-1).slice(-2).join("/")||b.group||"";
517
+ const reach=[];
518
+ for(const f of D.flows){
519
+ if(!f.trigger)continue;
520
+ if((f.steps||[]).some(st=>st.sig===b.sig)||f.title===b.sig){reach.push(f.trigger.verb+" "+f.trigger.path);if(reach.length>=2)break;}
521
+ }
522
+ const tierTxt=b.tier==="proven"?"Its behavior is dynamically proven: a test fails when this code is mutated."
523
+ :b.tier==="assoc"?"A real test imports and calls it, but no mutation proof exists yet."
524
+ :b.tier==="candidate"?"Only a lexically similar test file was found — an unconfirmed lead, not evidence."
525
+ :"No test signal of any kind was found for it.";
526
+ const s1="\u201c"+h.words+"\u201d"+(h.owner?" on "+h.owner:"")+" — a behavior in "+(mod||"this repo")+".";
527
+ const s2=reach.length?"A user or system reaches it via "+reach.join(" and ")+(reach.length>=2?" (and possibly more)":"")+".":(b.reachable?"It appears inside at least one statically traced call chain.":"No entry-anchored call chain reaches it in the static flow set.");
528
+ return s1+" "+s2+" "+tierTxt;
529
+ }
359
530
  function tierOf(b){
360
531
  if(b.tier==="proven")return{cls:"proven",badge:"b-proven",label:"Dynamically Proven",ax:"ax-proven"};
361
532
  if(b.tier==="assoc")return{cls:"assoc",badge:"b-signal",label:"Test signal",ax:"ax-signal"};
533
+ if(b.tier==="candidate")return{cls:"candidate",badge:"b-cand",label:"Candidate",ax:"ax-signal"};
362
534
  if(b.tier==="none"&&b.reachable)return{cls:"reach",badge:"b-reach",label:"Reachable",ax:"ax-reach"};
363
535
  return{cls:"nosig",badge:"b-nosig",label:"No signal",ax:"ax-nosig"};
364
536
  }
365
537
  function ctaOf(b){
366
538
  if(b.tier==="proven")return"This method is Dynamically Proven. A test breaks if you mutate it. Keep it green.";
539
+ if(b.tier==="candidate")return"A test file is lexically similar to this behavior, but no import or call links them. Treat as untested until confirmed — run <code>npx -y @orangepro/orangepro-mcp start</code> to confirm or prove.";
367
540
  if(b.tier==="assoc")return"A test calls this method, but doesn't prove breakage. Run <code>npx -y @orangepro/orangepro-mcp start</code> to attempt dynamic proof.";
368
541
  if(b.tier==="none"&&b.reachable)return"This method is reachable from a tested path but has no direct test. Write one.";
369
542
  return"Nothing in your test suite touches this method. Write a test that calls it and asserts the output.";
@@ -374,6 +547,31 @@ const behSide=$("#beh-groups"),behGrid=$("#beh-grid");
374
547
  function renderBeh(){
375
548
  behGrid.innerHTML="";
376
549
  const rows=D.behaviors.filter(b=>(!activeGrp||b.group===activeGrp)&&(!activeTier||tierOf(b).cls===activeTier)&&(!searchQ||(b.sig+" "+b.file).toLowerCase().includes(searchQ)));
550
+ if(rows.length===0){
551
+ const parts=[];
552
+ if(searchQ)parts.push('search \u201c'+searchQ+'\u201d');
553
+ if(activeTier)parts.push('the selected evidence tier');
554
+ if(activeGrp)parts.push('package \u201c'+activeGrp+'\u201d');
555
+ const why=parts.length?parts.join(' + '):'the current filters';
556
+ const box=el("div","beh-empty",
557
+ '<p>No behaviors match '+esc(why)+'. Filters combine \u2014 clearing one usually brings results back.</p>');
558
+ if(searchQ){
559
+ const b1=el("button","sm-crumb-btn","\u2715 Clear search");
560
+ b1.onclick=function(){searchEl.value="";searchQ="";renderBeh();};
561
+ box.append(b1);
562
+ }
563
+ if(activeTier||activeGrp){
564
+ const b2=el("button","sm-crumb-btn","Show all behaviors");
565
+ b2.onclick=function(){
566
+ activeTier=null;activeGrp=null;
567
+ [...document.querySelectorAll("#tier-filter .grp,#beh-groups .grp")].forEach(x=>x.setAttribute("aria-pressed","false"));
568
+ renderBeh();
569
+ };
570
+ box.append(b2);
571
+ }
572
+ behGrid.append(box);
573
+ return;
574
+ }
377
575
  rows.forEach(b=>{
378
576
  const t=tierOf(b);
379
577
  const c=el("div","beh-card",
@@ -389,7 +587,7 @@ const searchEl=$("#beh-search");
389
587
  searchEl.addEventListener("input",()=>{searchQ=searchEl.value.trim().toLowerCase();renderBeh();});
390
588
  // evidence-tier filter (Dynamically Proven first) so users can jump straight to a tier
391
589
  const tierWrap=$("#tier-filter");
392
- [["proven","Dynamically Proven"],["assoc","Test signal"],["reach","Reachable"],["nosig","No signal"]].forEach(([cls,label])=>{
590
+ [["proven","Dynamically Proven"],["assoc","Test signal"],["candidate","Candidate"],["reach","Reachable"],["nosig","No signal"]].forEach(([cls,label])=>{
393
591
  const n=D.behaviors.filter(b=>tierOf(b).cls===cls).length;
394
592
  const btn=el("button","grp",\`<span>\${label}</span><span class="gc">\${n}</span>\`);
395
593
  btn.setAttribute("aria-pressed","false");
@@ -417,7 +615,9 @@ const drill=$("#drill"),dc=$("#drill-content");
417
615
  function showDrill(i){
418
616
  const b=D.behaviors[i];if(!b)return;
419
617
  const t=tierOf(b);
618
+ const bDesc=describeBehavior(b);
420
619
  dc.innerHTML=\`<h2 class="drill-title">\${esc(b.sig)}</h2>
620
+ <div class="drill-row"><h4>In English</h4><p>\${esc(bDesc)}</p></div>
421
621
  <div class="drill-row"><h4>Evidence</h4><span class="badge \${t.badge}"><span class="d"></span>\${t.label}</span></div>
422
622
  <div class="drill-row"><h4>File</h4><p class="mono">\${esc(b.file)}</p></div>
423
623
  <div class="drill-row"><h4>What to do</h4><div class="abox \${t.ax}">\${ctaOf(b)}</div></div>\`;
@@ -471,6 +671,11 @@ if(cf&&cf.flows.length){
471
671
 
472
672
  // risks + generated test samples
473
673
  const riskList=$("#risk-list"),riskTools=$("#risk-tools");
674
+ if(D.viewMeta){
675
+ const rm=D.viewMeta.risks,fm=D.viewMeta.flows;
676
+ 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.";
677
+ 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.";
678
+ }
474
679
  const generatedRiskCount=D.risks.filter(r=>r.generatedTests&&r.generatedTests.length).length;
475
680
  let activeRiskFilter=generatedRiskCount?"generated":"all";
476
681
  function riskMatchesFilter(r){
@@ -492,7 +697,7 @@ function riskCardHtml(r){
492
697
  // category strip: show which concern categories apply to this flow
493
698
  let catHtml='';
494
699
  if(r.applicableCategories&&r.applicableCategories.length){
495
- const shownConcerns=new Set((r.generatedTests||[]).map(t=>t.concern).filter(Boolean));
700
+ const shownConcerns=new Set(r.coveredCategories||[]);
496
701
  const pills=r.applicableCategories.map(c=>{
497
702
  const shown=shownConcerns.has(c);
498
703
  const label=c.replace(/_/g,' ');
@@ -500,19 +705,29 @@ function riskCardHtml(r){
500
705
  }).join('');
501
706
  const shownN=r.applicableCategories.filter(c=>shownConcerns.has(c)).length;
502
707
  const totalN=r.applicableCategories.length;
503
- catHtml=\`<div class="cat-strip"><div class="cat-strip-lbl">Testing categories for this flow (\${shownN} of \${totalN} shown)</div><div class="cat-pills">\${pills}</div></div>\`;
708
+ catHtml=\`<div class="cat-strip"><div class="cat-strip-lbl">Applicable testing categories for this flow (\${shownN} of \${totalN} covered)</div><div class="cat-pills">\${pills}</div></div>\`;
504
709
  }
505
710
  let testsHtml='';
506
711
  if(r.generatedTests&&r.generatedTests.length){
507
- testsHtml=\`<div class="gen-tests"><div class="gen-tests-lbl">Generated tests (integration)</div>\`;
712
+ // Honest header: derive the kind from the attached tests themselves —
713
+ // single-function targets legitimately get UNIT tests (0-hop per the
714
+ // methodology's hop table); hardcoding "(integration)" contradicted the
715
+ // per-test chips whenever the generator produced unit tests.
716
+ const allIntent=r.generatedTests.every(t=>t.runnable===false);
717
+ const kinds=[...new Set(r.generatedTests.map(t=>t.concern).filter(Boolean))];
718
+ const kindLbl=allIntent?" (Manual tests — env setup needed)":kinds.length===1?" ("+kinds[0].replace(/_/g," ")+")":kinds.length>1?" (mixed)":"";
719
+ testsHtml=\`<div class="gen-tests"><div class="gen-tests-lbl">Generated tests\${esc(kindLbl)}</div>\`;
508
720
  r.generatedTests.forEach(t=>{
509
721
  const cBadge=t.concern?\`<span class="badge b-info" style="margin-left:6px;font-size:9px">\${esc(t.concern.replace('_',' '))}</span>\`:'';
510
- testsHtml+=\`<div class="gen-test"><div class="gen-test-head"><span class="gen-test-name">\${esc(t.name)}\${cBadge}</span><span class="gen-test-assert">\${esc(t.assertion)}</span><span class="gen-test-toggle">&#9660;</span></div><div class="gen-test-body">\${esc(t.code)}</div></div>\`;
722
+ const iBadge=t.runnable===false?\`<span class="badge b-cand" style="margin-left:6px;font-size:9px">Manual test</span>\`:'';
723
+ const bBadge=t.bucket?\`<span class="badge b-info" style="margin-left:6px;font-size:9px">\${esc(String(t.bucket).replace(/_/g,' '))}</span>\`:'';
724
+ testsHtml+=\`<div class="gen-test"><div class="gen-test-head"><span class="gen-test-name">\${esc(t.name)}\${cBadge}\${bBadge}\${iBadge}</span><span class="gen-test-assert">\${esc(t.assertion)}</span><span class="gen-test-toggle">&#9660;</span></div><div class="gen-test-body">\${esc(t.code)}</div></div>\`;
511
725
  });
512
726
  testsHtml+=\`</div>\`;
513
727
  }
728
+ const ctxHtml=r.context?\`<div class="risk-ctx">\${esc(r.context)}</div>\`:'';
514
729
  return \`<div class="risk-rank">#\${r.rank}</div>
515
- <div class="risk-ep"><span class="v">\${esc(r.verb)}</span> \${esc(r.path)}</div>
730
+ <div class="risk-ep"><span class="v">\${esc(r.verb)}</span> \${esc(r.path)}</div>\${ctxHtml}
516
731
  <div class="risk-tags">\${tags}</div>
517
732
  <div class="todo">\${esc(r.todo)}</div>\${testsHtml}\${catHtml}\`;
518
733
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * G6 — the coverage-vs-proof reveal line for the start summary.
3
+ *
4
+ * Compares ONLY same-scope numbers: both sides are RTM counts over the SAME
5
+ * denominator (`summary.total`). The proven side is `coverage_confirmed` — the
6
+ * IN-DENOMINATOR proven count — never `summary.proven`, which also counts
7
+ * off-denominator proofs (relaxed hard-edge targets below the entry-point bar)
8
+ * and can exceed `total` (a 5/3 = 167% render). The signature omits `proven`
9
+ * entirely so the off-denominator number cannot be passed by mistake.
10
+ *
11
+ * `runtime_covered` is the DISJOINT runtime tier — behaviors a repo coverage
12
+ * report executed that are NOT dynamically proven. Runtime coverage only proves
13
+ * EXECUTION, and the unproven side may simply be unattempted or unrunnable —
14
+ * the line says so and never blames the tests outright. Source-report
15
+ * line-coverage totals (lcov LF/LH etc.) are NOT retained by ingestion and are
16
+ * never invented here (spec G6 guardrail). Never implies proven SHOULD equal
17
+ * coverage — they measure different things.
18
+ *
19
+ * Null when no runtime coverage was ingested (nothing to reveal) or the
20
+ * denominator is empty.
21
+ */
22
+ export function coverageRevealLine(summary) {
23
+ if (summary.runtime_covered <= 0 || summary.total <= 0)
24
+ return null;
25
+ const pct = (n) => Math.round((n / summary.total) * 100);
26
+ return (`Coverage vs proof: ${summary.runtime_covered}/${summary.total} behaviors are runtime-covered but not Dynamically Proven ` +
27
+ `(${pct(summary.runtime_covered)}%) vs ${summary.coverage_confirmed}/${summary.total} Dynamically Proven (${pct(summary.coverage_confirmed)}%) — ` +
28
+ `coverage only proves execution; the unproven side may be unattempted, blocked, or covered by tests that never assert these behaviors.`);
29
+ }
@@ -264,14 +264,19 @@ function codeAreaSummary(graph, ledger) {
264
264
  a.runtime_covered++;
265
265
  }
266
266
  else {
267
- const sample = { title: n.title || n.external_id, file, status: "none" };
268
267
  if (publicRow?.evidence_tier === "associated") {
269
268
  a.inferred++;
270
- a.associatedSamples.push({ ...sample, status: "associated" });
269
+ a.associatedSamples.push({ title: n.title || n.external_id, file, status: "associated" });
270
+ }
271
+ else if (publicRow?.evidence_tier === "candidate") {
272
+ // Lexical/Jaccard candidate: a lead, not evidence — surfaced distinctly,
273
+ // never folded into the associated (test-linked) column.
274
+ a.inferred++;
275
+ a.associatedSamples.push({ title: n.title || n.external_id, file, status: "candidate" });
271
276
  }
272
277
  else {
273
278
  a.none++;
274
- a.noLinkSamples.push(sample);
279
+ a.noLinkSamples.push({ title: n.title || n.external_id, file, status: "none" });
275
280
  }
276
281
  }
277
282
  }
@@ -301,7 +306,9 @@ function codeBehaviorRows(graph, ledger) {
301
306
  ? "runtime"
302
307
  : tier === "associated"
303
308
  ? "associated"
304
- : "none";
309
+ : tier === "candidate"
310
+ ? "candidate"
311
+ : "none";
305
312
  return {
306
313
  id: n.external_id,
307
314
  title: n.title || n.external_id,
@@ -312,7 +319,7 @@ function codeBehaviorRows(graph, ledger) {
312
319
  };
313
320
  })
314
321
  .sort((a, b) => {
315
- const rank = (x) => (x.evidence === "none" ? 0 : x.evidence === "associated" ? 1 : x.evidence === "runtime" ? 2 : 3);
322
+ const rank = (x) => (x.evidence === "none" ? 0 : x.evidence === "candidate" ? 1 : x.evidence === "associated" ? 2 : x.evidence === "runtime" ? 3 : 4);
316
323
  return rank(a) - rank(b) || a.area.localeCompare(b.area) || a.file.localeCompare(b.file) || a.title.localeCompare(b.title);
317
324
  });
318
325
  }
@@ -21,7 +21,7 @@ export function workspaceInitialized(root) {
21
21
  export function graphExists(root) {
22
22
  return existsSync(workspacePaths(root).graphPath);
23
23
  }
24
- const ORANGEPROIGNORE_TEMPLATE = `# .orangeproignore — paths the OrangePro local proof kit should never read.
24
+ const ORANGEPROIGNORE_PREAMBLE = `# .orangeproignore — paths the OrangePro local proof kit should never read.
25
25
  # Same spirit as .gitignore. Secrets and large assets are excluded by default.
26
26
  *.env
27
27
  *.pem
@@ -32,7 +32,8 @@ secrets/
32
32
 
33
33
  # Product-denominator defaults: example/demo apps are useful references, but
34
34
  # they usually should not count as product behavior coverage.
35
- examples/
35
+ `;
36
+ export const LEGACY_ORANGEPROIGNORE_TEMPLATE = `${ORANGEPROIGNORE_PREAMBLE}examples/
36
37
  example/
37
38
  demos/
38
39
  demo/
@@ -42,6 +43,16 @@ docs/examples/
42
43
  docs/demo/
43
44
  docs/demos/
44
45
  `;
46
+ export const ORANGEPROIGNORE_TEMPLATE = `${ORANGEPROIGNORE_PREAMBLE}/examples/
47
+ /example/
48
+ /demos/
49
+ /demo/
50
+ /samples/
51
+ /sample/
52
+ /docs/examples/
53
+ /docs/demo/
54
+ /docs/demos/
55
+ `;
45
56
  export function initWorkspace(root, now) {
46
57
  const paths = workspacePaths(root);
47
58
  mkdirSync(paths.dir, { recursive: true });
@@ -58,6 +69,18 @@ export function initWorkspace(root, now) {
58
69
  if (!existsSync(ignorePath)) {
59
70
  writeFileSync(ignorePath, ORANGEPROIGNORE_TEMPLATE, "utf8");
60
71
  }
72
+ else {
73
+ // Upgrade only the untouched generated template. A user-edited ignore file
74
+ // is configuration and must never be rewritten implicitly.
75
+ try {
76
+ if (readFileSync(ignorePath, "utf8") === LEGACY_ORANGEPROIGNORE_TEMPLATE) {
77
+ writeFileSync(ignorePath, ORANGEPROIGNORE_TEMPLATE, "utf8");
78
+ }
79
+ }
80
+ catch {
81
+ // A read-only or transiently unavailable ignore file must not fail init.
82
+ }
83
+ }
61
84
  return { paths, config: loadConfig(paths) };
62
85
  }
63
86
  export function loadConfig(paths) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangepro/orangepro-mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",
@@ -21,7 +21,12 @@
21
21
  "orangepro",
22
22
  "evidence-graph",
23
23
  "byok",
24
- "ai-testing"
24
+ "ai-testing",
25
+ "test-gaps",
26
+ "behavior-coverage",
27
+ "integration-tests",
28
+ "dynamic-proof",
29
+ "code-analysis"
25
30
  ],
26
31
  "files": [
27
32
  "dist/local",
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // go-dynamic-proof-spike.mjs — Go dynamic-proof MECHANISM (G-1).
3
3
  //
4
- // Proves ONE free function 0->1 on a single Go module by mutation: it byte-copies
4
+ // Proves ONE free function or receiver method 0->1 on a single Go module by
5
+ // mutation: it byte-copies
5
6
  // the module into a hermetic sandbox, runs `go test -json` baseline, replaces the
6
7
  // target function body with a signature-derived sentinel (via go-mutate.go), reruns
7
8
  // the SAME test, and classifies. It emits a JSON verdict mirroring the TS/JS spike's
@@ -21,12 +22,13 @@
21
22
  // assertion call (`t.Error`/`t.Errorf`, or testify `assert.`/`require.`), NOT a
22
23
  // `t.Fatal`/`t.Fatalf`/`t.FailNow`/`t.SkipNow` hard-stop or a helper call. A build
23
24
  // error, a panic, a t.Fatal precondition, a setup/helper failure, an unbindable
24
- // failure, and an ambiguous/method/no-return name all classify as `unrunnable`,
25
+ // failure, and an ambiguous or no-return name all classify as `unrunnable`,
25
26
  // never `proven`. An equivalent-value mutation survives -> `associated_survived`.
26
27
  //
27
- // This is a spike harness only: it writes no graph edges or product artifacts and is
28
- // NOT wired into autoProve / cert / RTM / the mint path. Use only on trusted checkouts
29
- // for local measurement.
28
+ // PRODUCT-WIRED: `opro` routes Go dynamic proof through this script (operations.ts
29
+ // dynamicProofSpikePathFor("go")). The script itself writes no graph edges or product
30
+ // artifacts it emits a JSON verdict; the orchestrator is the sole interpreter and
31
+ // the only place proof is minted.
30
32
  import { spawnSync } from "node:child_process";
31
33
  import { cpSync, existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync } from "node:fs";
32
34
  import { tmpdir } from "node:os";
@@ -39,11 +41,12 @@ function usage() {
39
41
  return [
40
42
  "Usage: node scripts/spikes/go-dynamic-proof-spike.mjs --root <module> --test-run <^TestName$> --target <rel.go> --func <name> [--json]",
41
43
  "",
42
- "Runs a Go baseline test, mutates the target free function body in an isolated byte-copy, reruns the same test, and classifies the result.",
44
+ "Runs a Go baseline test, mutates the target free function or method body in an isolated byte-copy, reruns the same test, and classifies the result.",
43
45
  "--test-run is passed verbatim to `go test -run` and should anchor a single test, e.g. '^TestCompute$'.",
46
+ "--recv <T> (optional): receiver base type — mutate only `func (x T) <name>` / `func (x *T) <name>`, so a same-named method on another receiver (or a free function) can never be the mutation target.",
44
47
  "--go-assertion-line <n> (optional): 1-based test-source line of the target's assertion. When set, the mutant's failure must bind to a frame at EXACTLY that line and subtest frames are considered — so a runtime-named subtest can prove while a sibling asserting elsewhere is refused.",
45
- "G-1 scope: FREE FUNCTIONS ONLY. Methods are refused (unrunnable). Equivalent-value mutations survive (associated_survived).",
46
- "This is a spike harness only; it does not write graph edges or product artifacts and is not wired into prove/RTM/mint."
48
+ "Scope: free functions and receiver methods. Without --recv the name must resolve to exactly ONE declaration in the target file; with --recv exactly one declaration on the selected base receiver must match. Ambiguity within the filtered receiver and generic receivers still fail closed. Equivalent-value mutations survive (associated_survived).",
49
+ "Product wiring: opro prove/auto-prove invokes this script for Go targets; it writes no graph edges or product artifacts itself the caller interprets the JSON verdict."
47
50
  ].join("\n");
48
51
  }
49
52
 
@@ -460,9 +463,9 @@ function redactSecrets(text) {
460
463
  // Run the AST mutator (go run go-mutate.go). Because `go run` collapses any
461
464
  // non-zero child status to 1, we classify on the MUTATE_ERROR:<code> marker the
462
465
  // helper prints to stderr, not the exit code.
463
- function mutateFunc({ targetAbs, func, mode, cacheRoot, timeoutMs }) {
466
+ function mutateFunc({ targetAbs, func, recv, mode, cacheRoot, timeoutMs }) {
464
467
  const helper = path.join(path.dirname(fileURLToPath(import.meta.url)), "go-mutate.go");
465
- const result = spawnSync(goBin(), ["run", helper, "--file", targetAbs, "--func", func, "--mode", mode], {
468
+ const result = spawnSync(goBin(), ["run", helper, "--file", targetAbs, "--func", func, ...(recv ? ["--recv", recv] : []), "--mode", mode], {
466
469
  encoding: "utf8",
467
470
  timeout: timeoutMs,
468
471
  env: hermeticEnv(cacheRoot),
@@ -481,9 +484,9 @@ function mutateFunc({ targetAbs, func, mode, cacheRoot, timeoutMs }) {
481
484
 
482
485
  function mutateErrorReason(code) {
483
486
  switch (code) {
484
- case 3: return "target function name is ambiguous (more than one free function)";
485
- case 4: return "target free function was not found";
486
- case 5: return "target is a method (out of scope for G-1)";
487
+ case 3: return "target name is ambiguous (more than one free function or method)";
488
+ case 4: return "target free function or method was not found";
489
+ // code 5 (method out of scope) is retired methods are mutable now, never emitted.
487
490
  case 6: return "target function has no return value (not mutable)";
488
491
  default: return "mutation could not be applied";
489
492
  }
@@ -565,6 +568,7 @@ function main() {
565
568
  const mutation = mutateFunc({
566
569
  targetAbs: path.join(mutantCopy.repoRoot, targetRel),
567
570
  func: args.func,
571
+ recv: args.recv,
568
572
  mode: args.mode,
569
573
  cacheRoot: mutantCopy.tmpRoot,
570
574
  timeoutMs