@orangepro/orangepro-mcp 0.2.4 → 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.
- package/README.md +10 -3
- package/dist/local/analyze/behaviorContracts.js +78 -5
- package/dist/local/analyze/parseCache.js +16 -2
- package/dist/local/cli.js +7 -3
- package/dist/local/enrich/markdown.js +11 -0
- package/dist/local/flows/flowWalker.js +11 -6
- package/dist/local/generate/generator.js +54 -3
- package/dist/local/generate/promptV5.js +12 -1
- package/dist/local/operations.js +25 -5
- package/dist/local/proofDoctor.js +28 -3
- package/dist/local/rtm.js +30 -26
- package/dist/local/score/risk.js +128 -37
- package/dist/local/viz/behaviorReportData.js +393 -23
- package/dist/local/viz/behaviorReportHtml.js +225 -10
- package/dist/local/viz/payload.js +12 -5
- package/package.json +7 -2
|
@@ -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
|
|
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,""")+'">'+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=>({"&":"&","<":"<",">":">"}[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
|
|
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(
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
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">▼</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
|
}
|
|
@@ -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({
|
|
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(
|
|
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
|
-
: "
|
|
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 === "
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
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",
|