@evomap/evolver-webui 2.0.0-beta.2 → 2.0.0-beta.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/dist/console.js CHANGED
@@ -1,83 +1,126 @@
1
1
  /** 3 栏主控台 HTML(自包含, 无构建步; 轮询 /api/* 渲染). 军杰 §9.5: Triggers|Timeline|Inspector. */
2
2
  export const CONSOLE_HTML = `<!doctype html><html lang="zh"><head><meta charset="utf-8"><title>Evolver 控台</title>
3
3
  <style>
4
- *{box-sizing:border-box}html{height:100%}body{min-height:100%;margin:0;font:13px/1.5 ui-monospace,Menlo,monospace;background:#0d1117;color:#c9d1d9;display:grid;grid-template-rows:auto auto auto auto minmax(320px,1fr);overflow:auto}
4
+ *{box-sizing:border-box}html{height:100%}body{min-height:100%;margin:0;font:13px/1.5 ui-monospace,Menlo,monospace;background:#0d1117;color:#c9d1d9;display:grid;grid-template-rows:auto auto auto auto auto minmax(320px,1fr);overflow:auto}
5
5
  header{padding:8px 12px;background:#161b22;border-bottom:1px solid #30363d;display:flex;gap:16px;align-items:center}
6
6
  header b{color:#58a6ff}.muted{color:#8b949e}
7
7
  .cols{display:grid;grid-template-columns:1fr 1fr 1.2fr;min-height:320px}
8
8
  .col{overflow:auto;border-right:1px solid #30363d;padding:8px}
9
9
  h2{font-size:12px;text-transform:uppercase;color:#8b949e;margin:4px 0 8px;letter-spacing:.05em}
10
- .row{padding:6px 8px;border:1px solid #30363d;border-radius:6px;margin-bottom:6px;cursor:pointer}
11
- .row:hover{border-color:#58a6ff}.tag{display:inline-block;padding:0 6px;border-radius:10px;font-size:11px}
10
+ .row{padding:6px 8px;border:1px solid #30363d;border-radius:6px;margin-bottom:6px}
11
+ .clickable{cursor:pointer}.clickable:hover,.row:hover{border-color:#58a6ff}.tag{display:inline-block;padding:0 6px;border-radius:10px;font-size:11px}
12
12
  .ok{background:#1a3d1a;color:#6fdd6f}.fail{background:#3d1a1a;color:#ff7b72}.warn{background:#3d3a1a;color:#e3b341}
13
- .act{display:flex;gap:6px;margin-top:8px}button{background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:4px 8px;cursor:pointer}
14
- button:hover{border-color:#58a6ff}pre{white-space:pre-wrap;word-break:break-all;background:#161b22;padding:8px;border-radius:6px}
15
- .valuecard{padding:10px 14px;background:#11161d;border-bottom:1px solid #30363d;display:flex;gap:24px;align-items:center;flex-wrap:wrap}
13
+ .act{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}button{background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:4px 8px;cursor:pointer}
14
+ button:hover{border-color:#58a6ff}button:disabled{cursor:default;opacity:.5}pre{white-space:pre-wrap;word-break:break-word;background:#161b22;padding:8px;border-radius:6px}
15
+ a{color:#58a6ff;overflow-wrap:anywhere}.valuecard{padding:10px 14px;background:#11161d;border-bottom:1px solid #30363d;display:flex;gap:24px;align-items:center;flex-wrap:wrap}
16
16
  .valuecard .big{font-size:18px;color:#6fdd6f;font-weight:600}.valuecard .est{color:#e3b341}.valuecard .lbl{color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}
17
17
  #reviewqueue{max-height:156px;overflow:auto;align-items:flex-start}
18
- @media(max-width:760px){body{display:block;overflow:auto}.cols{display:block}.col{border-right:0;border-bottom:1px solid #30363d;min-height:240px}#reviewqueue{max-height:none}}
18
+ #assetbar{max-height:190px;overflow:auto;align-items:flex-start}.assetgrid{display:flex;gap:6px;flex-wrap:wrap}.assetbutton{text-align:left;max-width:360px}.pager{display:flex;gap:6px;align-items:center}.lineage-section{margin-top:12px;scroll-margin-top:8px}.lineage-section h3{font-size:11px;color:#8b949e;text-transform:uppercase}.lineage-empty{color:#8b949e}
19
+ .observability{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));background:#0d1117;border-bottom:1px solid #30363d}.surface{padding:10px 14px;min-height:150px;border-right:1px solid #30363d;border-top:1px solid #30363d;overflow:auto}.surface-head{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:8px}.surface-body{max-height:260px;overflow:auto}.surface h3{margin:0;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}.metricgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px}.metric{padding:6px;background:#161b22;border-radius:6px}.metric b{display:block;color:#8b949e;font-size:11px}.logview{max-height:220px;overflow:auto;margin:0}.relation-list{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.relation-card{padding:6px 8px;border:1px solid #30363d;border-radius:6px;min-width:180px}.section-nav{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}
20
+ @media(max-width:760px){body{display:block;overflow:auto}.cols,.observability{display:block}.col{border-right:0;border-bottom:1px solid #30363d;min-height:240px}#reviewqueue{max-height:none}.surface{border-right:0}}
19
21
  </style></head><body>
20
22
  <header><b>EVOLVER</b> <span id="stat" class="muted">…</span><span id="daily" class="muted"></span></header>
21
23
  <div class="valuecard" id="valuecard"><span class="muted">价值 / value …</span></div>
22
24
  <div class="valuecard" id="reviewqueue"><span class="muted">review queue …</span></div>
23
25
  <div class="valuecard" id="retentionbar"><span class="muted">retention health …</span></div>
26
+ <div class="diagnostics">
27
+ <div class="valuecard" id="assetbar"><span class="muted">asset lineage …</span></div>
28
+ <div class="observability" aria-label="deep observability">
29
+ <section class="surface"><div class="surface-head"><h3>Personality</h3><span id="personality-status" class="muted">…</span></div><div id="personality" class="surface-body"></div></section>
30
+ <section class="surface"><div class="surface-head"><h3>MemoryGraph</h3><span id="memorygraph-status" class="muted">…</span></div><div id="memorygraph" class="surface-body"></div></section>
31
+ <section class="surface"><div class="surface-head"><h3>Logs</h3><span id="logs-status" class="muted">…</span></div><div id="logs" class="surface-body"></div></section>
32
+ <section class="surface"><div class="surface-head"><h3>GitHub PR status</h3><span id="githubprs-status" class="muted">…</span></div><div id="githubprs" class="surface-body"></div></section>
33
+ </div>
34
+ </div>
24
35
  <div class="cols">
25
36
  <div class="col"><h2>Triggers</h2><div id="triggers"></div></div>
26
37
  <div class="col"><h2>Now &amp; Timeline</h2><div id="cycles"></div></div>
27
38
  <div class="col"><h2>Inspector</h2><div id="inspector" class="muted">选一个 cycle 或事件</div>
28
- <div class="act"><button onclick="act('observe')">Observe</button><button onclick="act('nudge')">Nudge</button><button onclick="act('intervene')">Intervene</button><button onclick="act('teach')">Teach</button></div>
39
+ <div class="act"><button id="act-observe">Observe</button><button id="act-nudge">Nudge</button><button id="act-intervene">Intervene</button><button id="act-teach">Teach</button></div>
29
40
  </div>
30
41
  </div>
31
42
  <script>
32
- let sel=null;
43
+ let sel=null,assetPage=1,lineageCapsulePage=1,lineageEventPage=1,lineageId='';
33
44
  const FRAG=new URLSearchParams(location.hash.slice(1));
34
45
  const QUERY=new URLSearchParams(location.search);
35
46
  let STORED='';try{STORED=sessionStorage.getItem('evolver.dashboard.token')||''}catch{}
36
47
  const TOK=FRAG.get('token')||QUERY.get('token')||STORED;
37
48
  if(TOK)try{sessionStorage.setItem('evolver.dashboard.token',TOK)}catch{}
38
- if(FRAG.has('token')||QUERY.has('token')){QUERY.delete('token');const q=QUERY.toString();history.replaceState(null,'',location.pathname+(q?'?'+q:''))}
39
- async function j(u,o){o=o||{};o.headers=Object.assign({},o.headers||{});if(TOK)o.headers.authorization='Bearer '+TOK;o.credentials='same-origin';const r=await fetch(u,o);return r.json()}
40
- // XSS guards (#200): server data (gene summaries / event titles / payloads) is built from untrusted material —
41
- // never put it into innerHTML raw. esc() = HTML-text escape (numeric entities) for text positions; q() strips a
42
- // value to the content-addressed id charset for inline-handler string args so a malicious id cannot break the
43
- // attribute/JS string. Every dynamic value below goes through one of these before reaching innerHTML.
44
- const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>'&#'+c.charCodeAt(0)+';');
45
- const q=s=>String(s==null?'':s).replace(/[^a-zA-Z0-9:_.-]/g,'');
46
- const nf=n=>Math.round(Number(n)||0).toLocaleString('en-US');
47
- async function refresh(){
48
- const s=await j('/api/status');document.getElementById('stat').textContent='events='+s.totalEvents+' cycles='+s.cycles;
49
- const d=await j('/api/daily-summary');document.getElementById('daily').textContent='今日 '+d.solidified+'✓/'+d.failed+'✗ 触发'+d.triggered;
50
- const v=await j('/api/value?window=7d');
51
- const top=(v.topGenes||[]).slice(0,3).map(g=>esc(g.assetId)+' ×'+esc(g.reuses)).join(' · ')||'—';
52
- document.getElementById('valuecard').innerHTML=
53
- '<div><div class="lbl">measured saved (7d)</div><span class="big">'+nf(v.totalTokensSaved)+' tokens</span> <span class="muted">$'+(v.totalCostUsd||0).toFixed(4)+'</span></div>'
54
- +'<div><div class="lbl">estimated</div><span class="est">'+nf(v.estimated&&v.estimated.totalTokensSaved)+' tokens</span> <span class="muted">(tracked separately)</span></div>'
55
- +'<div><div class="lbl">top reused</div><span class="muted">'+top+'</span></div>';
56
- const rv=await j('/api/review');const stag=s=>s==='quarantined'?'warn':s==='approved'?'ok':s==='rejected'?'fail':'muted';
57
- const pend=rv.filter(g=>g.state==='quarantined').length;
58
- const btns=g=>g.state==='quarantined'?' <button onclick="reviewAct(\\''+q(g.assetId)+'\\',\\'approve\\')">Approve</button><button onclick="reviewAct(\\''+q(g.assetId)+'\\',\\'reject\\')">Reject</button>':'';
59
- document.getElementById('reviewqueue').innerHTML='<div><div class="lbl">review queue</div><span class="big">'+pend+'</span> <span class="muted">待审 pending</span></div>'
60
- +rv.slice(0,8).map(g=>'<div><span class="tag '+stag(g.state)+'">'+esc(g.state)+'</span> '+esc(g.geneId)+(g.autoDrafted?' <span class="muted">auto</span>':'')+btns(g)+'</div>').join('');
61
- const tr=await j('/api/triggers');document.getElementById('triggers').innerHTML=tr.map(t=>'<div class="row"><span class="tag '+(t.triggered?'ok':'warn')+'">'+(t.triggered?'触发':'抑制')+'</span> '+esc(t.patternId)+' <span class="muted">v='+((t.value||0).toFixed?t.value.toFixed(2):esc(t.value))+'</span></div>').join('')||'<div class="muted">无</div>';
62
- const cs=await j('/api/cycles');document.getElementById('cycles').innerHTML=cs.slice(-30).reverse().map(c=>'<div class="row" onclick="showCycle(\\''+q(c.cycleId)+'\\')"><span class="tag '+(c.finalStage==='solidified'?'ok':c.finalStage==='failed'?'fail':'warn')+'">'+esc(c.finalStage)+'</span> '+esc(c.cycleId)+' <span class="muted">('+esc(c.events)+')</span></div>').join('')||'<div class="muted">无 cycle</div>';
49
+ if(FRAG.has('token')||QUERY.has('token')){QUERY.delete('token');const query=QUERY.toString();history.replaceState(null,'',location.pathname+(query?'?'+query:''))}
50
+ async function j(url,options){options=options||{};options.headers=Object.assign({},options.headers||{});if(TOK)options.headers.authorization='Bearer '+TOK;options.credentials='same-origin';const response=await fetch(url,options);return response.json()}
51
+ // Kept for compatibility with older embedded-console extensions. New observability surfaces use DOM text nodes only.
52
+ const esc=value=>String(value==null?'':value).replace(/[&<>"']/g,char=>'&#'+char.charCodeAt(0)+';');
53
+ const q=value=>String(value==null?'':value).replace(/[^a-zA-Z0-9:_.-]/g,'');
54
+ const nf=value=>Math.round(Number(value)||0).toLocaleString('en-US');
55
+ const el=(tag,text,cls)=>{const node=document.createElement(tag);if(text!=null)node.textContent=String(text);if(cls)node.className=cls;return node};
56
+ const clear=node=>{while(node.firstChild)node.removeChild(node.firstChild)};
57
+ const appendLine=(parent,label,value)=>{const row=el('div');row.append(el('b',label+': '),document.createTextNode(value==null||value===''?'—':String(value)));parent.append(row)};
58
+ const asArray=value=>Array.isArray(value)?value:[];
59
+ function safeJson(value){try{return JSON.stringify(value,null,1)}catch{return '[unavailable]'}}
60
+ function sourceSection(parent,title,source,render,id){const section=el('section',null,'lineage-section');if(id)section.id=id;section.append(el('h3',title));
61
+ if(!source||source.available!==true){section.append(el('div',(source&&source.error)||'unavailable','tag warn'));parent.append(section);return;}
62
+ render(section,source.data);parent.append(section);
63
63
  }
64
- async function refreshRetention(){const el=document.getElementById('retentionbar');
65
- try{const r=await j('/api/retention');if(!r||r.available!==true){el.innerHTML='<span class="muted">retention unavailable</span>';return;}
66
- const safe=!!(r.rootEvents&&r.rootEvents.archiveRotationSafe&&r.material&&r.material.archiveRotationSafe);
67
- const gate=safe?'ok':'warn';const warnings=Array.isArray(r.warnings)?r.warnings.length:0;
68
- el.innerHTML='<div><div class="lbl">retention health</div><span class="tag '+gate+'">'+(safe?'safe':'attention')+'</span> <span class="muted">warnings '+esc(warnings)+'</span></div>'
69
- +'<div><div class="lbl">root events · '+esc(r.rootEvents.state)+'</div><span class="big">'+esc(nf(r.rootEvents.records))+'</span> <span class="muted">active · '+esc(nf(r.rootEvents.archiveRecords))+' archive · '+esc(nf(r.rootEvents.historyRecords))+' history</span></div>'
70
- +'<div><div class="lbl">material · '+esc(r.material.state)+'</div><span class="big">'+esc(nf(r.material.records))+'</span> <span class="muted">active · '+esc(nf(r.material.archiveRecords))+' archive · '+esc(nf(r.material.historyRecords))+' history · '+esc(nf(r.material.pending))+' pending · cursor '+esc(r.material.cursor)+'</span></div>';
71
- }catch{el.innerHTML='<span class="tag warn">retention unavailable</span>'}}
72
- async function showCycle(id){sel=id;const t=await j('/api/cycle?id='+encodeURIComponent(id));
73
- document.getElementById('inspector').innerHTML='<b>'+esc(id)+'</b>'+t.timeline.map(e=>'<div class="row">#'+esc(e.seq)+' <b>'+esc(e.type)+'</b><br>'+esc(e.title)+(e.why?'<br><span class="muted">why: '+esc(e.why)+'</span>':'')+(e.payload?'<pre>'+esc(JSON.stringify(e.payload,null,1))+'</pre>':'')+'</div>').join('');
64
+ function setSurfaceStatus(name,text,cls){const status=document.getElementById(name+'-status');status.textContent=text;status.className=cls||'muted'}
65
+ function surfaceFailure(name,message){setSurfaceStatus(name,'unavailable','tag warn');const root=document.getElementById(name);clear(root);root.append(el('div',message||'unavailable','muted'))}
66
+ function githubUrl(value){try{const url=new URL(String(value));const host=url.hostname.toLowerCase();if(url.protocol!=='https:'||(host!=='github.com'&&!host.endsWith('.github.com'))||url.port)return null;url.username='';url.password='';url.search='';url.hash='';return url.href}catch{return null}}
67
+ function externalLink(url,label){const valid=githubUrl(url);if(!valid)return null;const link=el('a',label||valid);link.href=valid;link.target='_blank';link.rel='noopener noreferrer';return link}
68
+ function relationItems(value){if(Array.isArray(value))return value;if(value&&value.available===true){if(Array.isArray(value.data))return value.data;if(value.data&&Array.isArray(value.data.items))return value.data.items}if(value&&Array.isArray(value.items))return value.items;return []}
69
+ function relationLabel(value){if(value==null)return 'relation';if(typeof value!=='object')return String(value);const fields=['title','type','id','trajectoryId','traceId','sessionId','number','state'];const parts=[];for(const field of fields){if(value[field]!=null&&value[field]!=='')parts.push(field+' '+String(value[field]))}return parts.join(' · ')||'relation'}
70
+ function renderRelationCards(parent,items,kind){const list=el('div',null,'relation-list');for(const item of items){const card=el('div',null,'relation-card');if(kind==='asset'){
71
+ const assetId=String(item&&item.assetId||'');if(!assetId)continue;const button=el('button',relationLabel(item));button.addEventListener('click',()=>void showAsset(assetId,1,1));card.append(button);
72
+ }else if(kind==='pr'){
73
+ const link=externalLink(item&&item.url,relationLabel(item));card.append(link||el('span',relationLabel(item)));
74
+ }else card.append(el('span',relationLabel(item)));
75
+ list.append(card)}if(list.childNodes.length)parent.append(list)}
76
+ function renderOptionalRelations(parent,relations,scope){if(!relations)return;const assets=relationItems(relations.assets);const trajectories=relationItems(relations.trajectories);const prs=relationItems(relations.pullRequests);
77
+ if(assets.length)sourceSection(parent,'related assets',{available:true,data:assets},(section,data)=>renderRelationCards(section,data,'asset'));
78
+ if(trajectories.length)sourceSection(parent,'trajectories',{available:true,data:trajectories},(section,data)=>renderRelationCards(section,data,'trajectory'),scope?scope+'-trajectories':undefined);
79
+ if(prs.length)sourceSection(parent,'related pull requests',{available:true,data:prs},(section,data)=>renderRelationCards(section,data,'pr'));
74
80
  }
75
- async function reviewAct(gene,action){const reason=prompt(action+' 理由 reason:');if(reason===null)return;
76
- try{const r=await j('/api/review',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({gene,action,reason})});
77
- if(!r||r.ok!==true){alert('review 失败 failed: '+((r&&r.error)||'unknown'));return;}}
78
- catch(e){alert('review 失败 failed: '+e);return;}
79
- refresh();}
80
- async function act(kind){const note=prompt(kind+' 备注:');if(note===null)return;
81
- await j('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:kind,title:kind+(sel?(' @'+sel):''),note,cycleId:sel})});refresh();}
82
- refresh();refreshRetention();setInterval(refresh,4000);setInterval(refreshRetention,60000);
81
+ async function refreshStatus(){try{const status=await j('/api/status');document.getElementById('stat').textContent='events='+status.totalEvents+' cycles='+status.cycles}catch{document.getElementById('stat').textContent='status unavailable'}}
82
+ async function refreshDaily(){try{const daily=await j('/api/daily-summary');document.getElementById('daily').textContent='今日 '+daily.solidified+'✓/'+daily.failed+'✗ 触发'+daily.triggered}catch{document.getElementById('daily').textContent='daily unavailable'}}
83
+ async function refreshValue(){const root=document.getElementById('valuecard');try{const value=await j('/api/value?window=7d');clear(root);
84
+ const measured=el('div');measured.append(el('div','measured saved (7d)','lbl'),el('span',nf(value.totalTokensSaved)+' tokens','big'),document.createTextNode(' '),el('span','$'+Number(value.totalCostUsd||0).toFixed(4),'muted'));
85
+ const estimated=el('div');estimated.append(el('div','estimated','lbl'),el('span',nf(value.estimated&&value.estimated.totalTokensSaved)+' tokens','est'),document.createTextNode(' '),el('span','(tracked separately)','muted'));
86
+ const top=el('div');top.append(el('div','top reused','lbl'));const topText=asArray(value.topGenes).slice(0,3).map(g=>String(g.assetId)+' ×'+String(g.reuses)).join(' · ')||'—';top.append(el('span',topText,'muted'));root.append(measured,estimated,top);
87
+ }catch{clear(root);root.append(el('span','value unavailable','tag warn'))}}
88
+ async function refreshReview(){const root=document.getElementById('reviewqueue');try{const review=await j('/api/review');clear(root);const pending=asArray(review).filter(item=>item.state==='quarantined').length;const summary=el('div');summary.append(el('div','review queue','lbl'),el('span',pending,'big'),document.createTextNode(' '),el('span','待审 pending','muted'));root.append(summary);
89
+ const stateClass=state=>state==='quarantined'?'warn':state==='approved'?'ok':state==='rejected'?'fail':'muted';for(const item of asArray(review).slice(0,8)){const row=el('div');row.append(el('span',item.state,'tag '+stateClass(item.state)),document.createTextNode(' '),el('span',item.geneId));if(item.autoDrafted)row.append(document.createTextNode(' '),el('span','auto','muted'));
90
+ if(item.state==='quarantined'){const assetId=String(item.assetId||'');const approve=el('button','Approve');const reject=el('button','Reject');approve.addEventListener('click',()=>void reviewAct(assetId,'approve'));reject.addEventListener('click',()=>void reviewAct(assetId,'reject'));row.append(document.createTextNode(' '),approve,reject)}root.append(row)}
91
+ }catch{clear(root);root.append(el('span','review unavailable','tag warn'))}}
92
+ async function refreshTriggers(){const root=document.getElementById('triggers');try{const triggers=await j('/api/triggers');clear(root);for(const trigger of asArray(triggers)){const row=el('div',null,'row');row.append(el('span',trigger.triggered?'触发':'抑制','tag '+(trigger.triggered?'ok':'warn')),document.createTextNode(' '+String(trigger.patternId)+' '));const value=Number(trigger.value);row.append(el('span','v='+(Number.isFinite(value)?value.toFixed(2):String(trigger.value)),'muted'));root.append(row)}if(!root.firstChild)root.append(el('div','无','muted'))}catch{clear(root);root.append(el('div','triggers unavailable','muted'))}}
93
+ async function refreshCycles(){const root=document.getElementById('cycles');try{const cycles=await j('/api/cycles');clear(root);for(const cycle of asArray(cycles).slice(-30).reverse()){const cycleId=String(cycle.cycleId||'');const row=el('div',null,'row clickable');const stageClass=cycle.finalStage==='solidified'?'ok':cycle.finalStage==='failed'?'fail':'warn';row.append(el('span',cycle.finalStage,'tag '+stageClass),document.createTextNode(' '+String(cycle.cycleId)+' '),el('span','('+String(cycle.events)+')','muted'));row.addEventListener('click',()=>void showCycle(cycleId));root.append(row)}if(!root.firstChild)root.append(el('div','无 cycle','muted'))}catch{clear(root);root.append(el('div','cycles unavailable','muted'))}}
94
+ function refresh(){void refreshStatus();void refreshDaily();void refreshValue();void refreshReview();void refreshTriggers();void refreshCycles()}
95
+ async function refreshRetention(){const root=document.getElementById('retentionbar');try{const retention=await j('/api/retention');clear(root);if(!retention||retention.available!==true){root.append(el('span','retention unavailable','muted'));return;}
96
+ const safe=!!(retention.rootEvents&&retention.rootEvents.archiveRotationSafe&&retention.material&&retention.material.archiveRotationSafe);const summary=el('div');summary.append(el('div','retention health','lbl'),el('span',safe?'safe':'attention','tag '+(safe?'ok':'warn')),document.createTextNode(' '),el('span','warnings '+asArray(retention.warnings).length,'muted'));root.append(summary);
97
+ const rootEvents=el('div');rootEvents.append(el('div','root events · '+String(retention.rootEvents.state),'lbl'),el('span',nf(retention.rootEvents.records),'big'),document.createTextNode(' '),el('span','active · '+nf(retention.rootEvents.archiveRecords)+' archive · '+nf(retention.rootEvents.historyRecords)+' history','muted'));root.append(rootEvents);
98
+ const material=el('div');material.append(el('div','material · '+String(retention.material.state),'lbl'),el('span',nf(retention.material.records),'big'),document.createTextNode(' '),el('span','active · '+nf(retention.material.archiveRecords)+' archive · '+nf(retention.material.historyRecords)+' history · '+nf(retention.material.pending)+' pending · cursor '+String(retention.material.cursor),'muted'));root.append(material);
99
+ }catch{clear(root);root.append(el('span','retention unavailable','tag warn'))}}
100
+ async function refreshAssets(){const root=document.getElementById('assetbar');try{const source=await j('/api/asset-lineage/assets?page='+assetPage+'&pageSize=12');clear(root);const title=el('div');title.append(el('div','asset lineage','lbl'));root.append(title);
101
+ if(!source||source.available!==true){root.append(el('span',(source&&source.error)||'asset store unavailable','tag warn'));return;}const grid=el('div',null,'assetgrid');for(const asset of source.data.items){const assetId=String(asset.assetId||'');const button=el('button',asset.type+' · '+asset.id+(asset.summary?' · '+asset.summary:''),'assetbutton');button.addEventListener('click',()=>void showAsset(assetId,1,1));grid.append(button)}if(source.data.items.length===0)grid.append(el('span','no assets','muted'));root.append(grid);
102
+ const pager=el('div',null,'pager');const previous=el('button','Previous');previous.disabled=source.data.page<=1;previous.addEventListener('click',()=>{assetPage=Math.max(1,assetPage-1);void refreshAssets()});const next=el('button','Next');next.disabled=!source.data.hasMore;next.addEventListener('click',()=>{assetPage+=1;void refreshAssets()});pager.append(previous,el('span','page '+source.data.page+(source.data.truncated?' · bounded':''),'muted'),next);root.append(pager);
103
+ }catch{clear(root);root.append(el('span','asset store unavailable','tag warn'))}}
104
+ function renderPersonalityRecords(parent,title,records){if(!Array.isArray(records)||records.length===0)return;const section=el('section',null,'lineage-section');section.append(el('h3',title));for(const record of records.slice(-8).reverse()){const row=el('div',null,'row');if(record&&typeof record==='object'){for(const key of ['at','updatedAt','key','success','fail','avgScore','n','outcome','score'])if(record[key]!=null)appendLine(row,key,record[key]);if(!row.firstChild)row.append(el('span',safeJson(record)))}else row.append(el('span',record));section.append(row)}parent.append(section)}
105
+ async function refreshPersonality(){const root=document.getElementById('personality');try{const response=await j('/api/personality');if(!response||response.available!==true){surfaceFailure('personality',(response&&response.error)||'personality unavailable');return;}clear(root);setSurfaceStatus('personality','live','tag ok');const data=response.data||{};const current=data.current&&typeof data.current==='object'?data.current:{};const grid=el('div',null,'metricgrid');for(const key of Object.keys(current).slice(0,5)){const metric=el('div',null,'metric');metric.append(el('b',key),el('span',current[key]));grid.append(metric)}if(!grid.firstChild)grid.append(el('span','no current profile','muted'));root.append(grid);appendLine(root,'updated',data.updatedAt);renderPersonalityRecords(root,'stats',data.stats);renderPersonalityRecords(root,'history',data.history)}catch{surfaceFailure('personality','personality unavailable')}}
106
+ async function refreshMemoryGraph(){const root=document.getElementById('memorygraph');try{const response=await j('/api/memory-graph');if(!response||response.available!==true){surfaceFailure('memorygraph',(response&&response.error)||'memory graph unavailable');return;}clear(root);const recovery=String(response.recovery||'degraded');const gate=recovery==='healthy'?'ok':recovery==='degraded'?'fail':'warn';setSurfaceStatus('memorygraph',recovery,'tag '+gate);appendLine(root,'selection',response.selectionReason||'no scoped selection yet');appendLine(root,'compacted records',nf(response.compactedRecords));appendLine(root,'active records',nf(response.activeRecords));appendLine(root,'archives',nf(response.archives));appendLine(root,'corrupt lines',nf(response.corruptLines));appendLine(root,'oversized lines',nf(response.oversizedLines));appendLine(root,'oversized files',nf(response.oversizedFiles))}catch{surfaceFailure('memorygraph','memory graph unavailable')}}
107
+ async function refreshLogs(){const root=document.getElementById('logs');try{const response=await j('/api/logs');if(!response||response.available!==true){surfaceFailure('logs',(response&&response.error)||'logs unavailable');return;}clear(root);setSurfaceStatus('logs',response.data&&response.data.truncated?'bounded':'live',response.data&&response.data.truncated?'tag warn':'tag ok');const pre=el('pre',null,'logview');pre.append(document.createTextNode(asArray(response.data&&response.data.lines).join('\\n')));root.append(pre)}catch{surfaceFailure('logs','logs unavailable')}}
108
+ function checksText(checks){checks=checks||{};return 'checks '+Number(checks.passed||0)+' passing · '+Number(checks.failed||0)+' failing · '+Number(checks.pending||0)+' pending / '+Number(checks.total||0)}
109
+ async function refreshGitHubPrs(){const root=document.getElementById('githubprs');try{const response=await j('/api/github-prs');if(!response||response.available!==true){surfaceFailure('githubprs',(response&&response.error)||'GitHub PR status unavailable');return;}clear(root);setSurfaceStatus('githubprs',response.data&&response.data.truncated?'bounded':'live',response.data&&response.data.truncated?'tag warn':'tag ok');const items=asArray(response.data&&response.data.prs);for(const item of items){const row=el('div',null,'row');const title='#'+String(item.number)+' '+String(item.title||'');const link=externalLink(item.url,title);row.append(link||el('span',title));const state=String(item.state||'unknown')+(item.isDraft?' · draft':'');appendLine(row,'state',state);appendLine(row,'branch',String(item.head||'—')+' → '+String(item.base||'—'));appendLine(row,'review',item.reviewDecision);appendLine(row,'updated',item.updatedAt);row.append(el('div',checksText(item.checks),'muted'));root.append(row)}if(!root.firstChild)root.append(el('div','no pull requests','muted'))}catch{surfaceFailure('githubprs','GitHub PR status unavailable')}}
110
+ function renderRows(section,page,fields){if(!page||!Array.isArray(page.items)||page.items.length===0){section.append(el('div','none','lineage-empty'));return;}for(const item of page.items){const row=el('div',null,'row');for(const field of fields)appendLine(row,field[0],item[field[1]]);section.append(row)}if(page.truncated)section.append(el('div','source window bounded for dashboard safety','tag warn'))}
111
+ function renderEventRows(section,page){if(!page||!Array.isArray(page.items)||page.items.length===0){section.append(el('div','none','lineage-empty'));return;}for(const item of page.items){const row=el('div',null,'row');for(const field of [['seq','seq'],['type','type'],['time','ts'],['title','title'],['why','why']])appendLine(row,field[0],item[field[1]]);const cycleId=String(item.cycleId||'');if(cycleId){const button=el('button','Open cycle '+String(item.cycleId));button.addEventListener('click',()=>void showCycle(cycleId));row.append(button)}section.append(row)}if(page.truncated)section.append(el('div','source window bounded for dashboard safety','tag warn'))}
112
+ function lineagePager(parent,label,page,data,onPage){const pager=el('div',null,'pager');const previous=el('button','Previous');previous.disabled=page<=1;previous.addEventListener('click',()=>onPage(page-1));const next=el('button','Next');next.disabled=!data||!data.hasMore;next.addEventListener('click',()=>onPage(page+1));pager.append(el('span',label,'muted'),previous,el('span','page '+page,'muted'),next);parent.append(pager)}
113
+ function inspectorNav(parent,targetId,label){const button=el('button',label);button.addEventListener('click',()=>{const target=document.getElementById(targetId);if(target)target.scrollIntoView({block:'start'})});parent.append(button)}
114
+ async function showAsset(id,capsulePage,eventPage){lineageId=id;lineageCapsulePage=capsulePage||1;lineageEventPage=eventPage||1;sel=null;const root=document.getElementById('inspector');clear(root);root.append(el('span','loading asset lineage …','muted'));try{const response=await j('/api/asset-lineage?id='+encodeURIComponent(id)+'&capsulePage='+lineageCapsulePage+'&eventPage='+lineageEventPage+'&pageSize=20');const relations=response.relations||{trajectories:response.trajectories,pullRequests:response.pullRequests};clear(root);root.append(el('h2','Asset lineage'));const nav=el('div',null,'section-nav');inspectorNav(nav,'asset-source','Source');inspectorNav(nav,'asset-review','Review');if(relationItems(relations.trajectories).length)inspectorNav(nav,'asset-trajectories','Trajectory');root.append(nav);
115
+ sourceSection(root,'asset',response.asset,(section,data)=>{if(!data){section.append(el('div','asset not found','tag warn'));return;}appendLine(section,'type',data.type);appendLine(section,'id',data.id);appendLine(section,'asset',data.assetId);appendLine(section,'category',data.category);appendLine(section,'summary',data.summary)});
116
+ sourceSection(root,'review',response.review,(section,data)=>{if(!data){section.append(el('div','default eligible / no record','lineage-empty'));return;}appendLine(section,'state',data.state);appendLine(section,'at',data.at);appendLine(section,'by',data.by);appendLine(section,'reason',data.reason)},'asset-review');
117
+ sourceSection(root,'provenance / source',response.provenance,(section,data)=>{if(!data){section.append(el('div','local default / no record','lineage-empty'));return;}appendLine(section,'source',data.source);appendLine(section,'trusted',data.trusted);appendLine(section,'at',data.at);appendLine(section,'promoted by',data.promotedBy);appendLine(section,'reason',data.reason)},'asset-source');
118
+ sourceSection(root,'capsules',response.capsules,(section,data)=>renderRows(section,data,[['type','type'],['id','id'],['asset','assetId'],['summary','summary']]));sourceSection(root,'events',response.events,(section,data)=>renderEventRows(section,data));renderOptionalRelations(root,relations,'asset');lineagePager(root,'capsules',lineageCapsulePage,response.capsules&&response.capsules.data,page=>showAsset(lineageId,page,lineageEventPage));lineagePager(root,'events',lineageEventPage,response.events&&response.events.data,page=>showAsset(lineageId,lineageCapsulePage,page));
119
+ }catch{clear(root);root.append(el('span','asset lineage unavailable','tag warn'))}}
120
+ async function showCycle(id){sel=id;const root=document.getElementById('inspector');clear(root);root.append(el('span','loading cycle …','muted'));try{const response=await j('/api/cycle?id='+encodeURIComponent(id));const relations=response.relations;clear(root);root.append(el('h2','Cycle '+String(id)));if(relationItems(relations&&relations.trajectories).length){const nav=el('div',null,'section-nav');inspectorNav(nav,'cycle-trajectories','Trajectory');root.append(nav)}renderOptionalRelations(root,relations,'cycle');for(const event of asArray(response.timeline)){const row=el('div',null,'row');const heading=el('div');heading.append(document.createTextNode('#'+String(event.seq)+' '),el('b',event.type));row.append(heading);if(event.title)row.append(el('div',event.title));if(event.why)appendLine(row,'why',event.why);if(event.payload!=null){const pre=el('pre');pre.append(document.createTextNode(safeJson(event.payload)));row.append(pre)}root.append(row)}if(!asArray(response.timeline).length)root.append(el('div','no timeline events','muted'))}catch{clear(root);root.append(el('span','cycle unavailable','tag warn'))}}
121
+ async function reviewAct(gene,action){const reason=prompt(action+' 理由 reason:');if(reason===null)return;try{const response=await j('/api/review',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({gene,action,reason})});if(!response||response.ok!==true){alert('review 失败 failed: '+((response&&response.error)||'unknown'));return;}}catch(error){alert('review 失败 failed: '+error);return;}refresh()}
122
+ async function act(kind){const note=prompt(kind+' 备注:');if(note===null)return;await j('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:kind,title:kind+(sel?(' @'+sel):''),note,cycleId:sel})});refresh()}
123
+ for(const kind of ['observe','nudge','intervene','teach'])document.getElementById('act-'+kind).addEventListener('click',()=>void act(kind));
124
+ refresh();void refreshRetention();void refreshAssets();void refreshPersonality();void refreshMemoryGraph();void refreshLogs();void refreshGitHubPrs();
125
+ setInterval(refresh,4000);setInterval(refreshRetention,60000);setInterval(refreshAssets,30000);setInterval(refreshPersonality,30000);setInterval(refreshMemoryGraph,30000);setInterval(refreshLogs,10000);setInterval(refreshGitHubPrs,60000);
83
126
  </script></body></html>`;
@@ -0,0 +1,2 @@
1
+ export declare function redactDiagnosticText(value: unknown, maxChars?: number): string;
2
+ export declare function sanitizeDiagnosticValue(value: unknown, depth?: number): unknown;
@@ -0,0 +1,49 @@
1
+ const MAX_TEXT = 500;
2
+ const MAX_ARRAY = 50;
3
+ const MAX_KEYS = 50;
4
+ const MAX_DEPTH = 5;
5
+ const SECRET_KEY = /(?:authorization|proxy[_-]?authorization|cookie|set[_-]?cookie|password|passwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|private[_-]?key)/i;
6
+ function stripUnsafeControls(value) {
7
+ let out = '';
8
+ for (const char of value) {
9
+ const code = char.codePointAt(0) ?? 0;
10
+ if (code >= 0x20 || code === 0x09 || code === 0x0a || code === 0x0d)
11
+ out += char;
12
+ }
13
+ return out;
14
+ }
15
+ export function redactDiagnosticText(value, maxChars = MAX_TEXT) {
16
+ const raw = typeof value === 'string' ? value : value == null ? '' : String(value);
17
+ const redacted = stripUnsafeControls(raw)
18
+ .replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, '[redacted private key]')
19
+ .replace(/\b(https?:\/\/)[^/\s@]+@/gi, '$1')
20
+ .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1[redacted]')
21
+ .replace(/\b(authorization|proxy-authorization|cookie|set-cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|passwd|secret)\b\s*[:=]\s*([^\s,;]+)/gi, '$1=[redacted]')
22
+ .replace(/\b[A-Za-z]:\\[^\s"']+/g, '[path]')
23
+ .replace(/\\\\[^\\\s"']+\\[^\\\s"']+(?:\\[^\\\s"']+)*/g, '[path]')
24
+ .replace(/(?:\/(?:Users|home|var|tmp|private|opt)\/[^\s"']+)+/g, '[path]')
25
+ .trim();
26
+ return redacted.length > maxChars ? `${redacted.slice(0, maxChars)}…` : redacted;
27
+ }
28
+ export function sanitizeDiagnosticValue(value, depth = 0) {
29
+ if (depth >= MAX_DEPTH)
30
+ return '[truncated]';
31
+ if (value === null || typeof value === 'boolean')
32
+ return value;
33
+ if (typeof value === 'number')
34
+ return Number.isFinite(value) ? value : null;
35
+ if (typeof value === 'string' || typeof value === 'bigint')
36
+ return redactDiagnosticText(value);
37
+ if (Array.isArray(value))
38
+ return value.slice(0, MAX_ARRAY).map((entry) => sanitizeDiagnosticValue(entry, depth + 1));
39
+ if (typeof value !== 'object')
40
+ return redactDiagnosticText(value);
41
+ const out = {};
42
+ for (const [rawKey, entry] of Object.entries(value).slice(0, MAX_KEYS)) {
43
+ const key = redactDiagnosticText(rawKey, 120);
44
+ if (!key)
45
+ continue;
46
+ out[key] = SECRET_KEY.test(key) ? '[redacted]' : sanitizeDiagnosticValue(entry, depth + 1);
47
+ }
48
+ return out;
49
+ }
@@ -1,13 +1,18 @@
1
1
  import { events as ev } from '@evomap/evolver-core';
2
+ type Awaitable<T> = T | PromiseLike<T>;
3
+ export type EventSnapshotReader = (eventsPath: string) => Awaitable<readonly ev.ReportEvent[]>;
2
4
  export interface EventSnapshotSource {
3
- version(): string;
4
- read(): readonly ev.ReportEvent[];
5
+ version(): Awaitable<string>;
6
+ read(): Awaitable<readonly ev.ReportEvent[]>;
5
7
  }
6
- export declare function fileEventSnapshotSource(eventsPath: string): EventSnapshotSource;
7
- /** Reuses a parsed append-only event snapshot only when the file identity is stable across the read. */
8
+ export declare function fileEventSnapshotSource(eventsPath: string, readEvents?: EventSnapshotReader): EventSnapshotSource;
9
+ /** Reuses a parsed event history only when the active file and archive segments stay stable across the read. */
8
10
  export declare class EventSnapshotCache {
9
11
  private readonly source;
10
12
  private cached;
13
+ private inFlight;
11
14
  constructor(source: EventSnapshotSource);
12
- read(): readonly ev.ReportEvent[];
13
- }
15
+ read(): Promise<readonly ev.ReportEvent[]>;
16
+ private readOnce;
17
+ }
18
+ export {};
@@ -1,45 +1,143 @@
1
- import { statSync } from 'node:fs';
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { Worker } from 'node:worker_threads';
2
4
  import { events as ev } from '@evomap/evolver-core';
5
+ const ARCHIVE_SEGMENT_PATTERN = /^root-events-\d{16}-\d{16}\.jsonl$/;
6
+ const EVENT_SNAPSHOT_WORKER_SOURCE = `
7
+ import { parentPort, workerData } from 'node:worker_threads';
8
+
9
+ try {
10
+ const core = await import(workerData.coreModuleUrl);
11
+ const events = core.events.readEvents(workerData.eventsPath);
12
+ parentPort.postMessage({ ok: true, events });
13
+ } catch (error) {
14
+ parentPort.postMessage({
15
+ ok: false,
16
+ error: {
17
+ name: error instanceof Error ? error.name : 'Error',
18
+ message: error instanceof Error ? error.message : String(error),
19
+ code: error && typeof error === 'object' && 'code' in error ? error.code : undefined,
20
+ },
21
+ });
22
+ }
23
+ `;
24
+ const EVENT_SNAPSHOT_WORKER_URL = new URL(`data:text/javascript,${encodeURIComponent(EVENT_SNAPSHOT_WORKER_SOURCE)}`);
3
25
  function isMissing(error) {
4
26
  return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
5
27
  }
6
- export function fileEventSnapshotSource(eventsPath) {
28
+ function statFingerprint(value) {
29
+ return `${value.dev}:${value.ino}:${value.size}:${value.mtimeNs}:${value.ctimeNs}`;
30
+ }
31
+ async function pathFingerprint(path) {
32
+ try {
33
+ return statFingerprint(await stat(path, { bigint: true }));
34
+ }
35
+ catch (error) {
36
+ if (isMissing(error))
37
+ return 'missing';
38
+ throw error;
39
+ }
40
+ }
41
+ async function archiveFingerprint(eventsPath) {
42
+ const archiveDir = ev.rootEventArchiveDir(eventsPath);
43
+ let entries;
44
+ try {
45
+ entries = (await readdir(archiveDir)).filter((entry) => ARCHIVE_SEGMENT_PATTERN.test(entry)).sort();
46
+ }
47
+ catch (error) {
48
+ if (isMissing(error))
49
+ return 'missing';
50
+ throw error;
51
+ }
52
+ const fingerprints = [];
53
+ for (const entry of entries) {
54
+ fingerprints.push(`${entry}:${await pathFingerprint(join(archiveDir, entry))}`);
55
+ }
56
+ return `${await pathFingerprint(archiveDir)}:[${fingerprints.join(',')}]`;
57
+ }
58
+ async function fileSnapshotVersion(eventsPath) {
59
+ const [active, archive] = await Promise.all([
60
+ pathFingerprint(eventsPath),
61
+ archiveFingerprint(eventsPath),
62
+ ]);
63
+ return `active=${active};archive=${archive}`;
64
+ }
65
+ function workerFailure(error) {
66
+ const restored = new Error(error.message);
67
+ restored.name = error.name;
68
+ if (error.code !== undefined)
69
+ Object.assign(restored, { code: error.code });
70
+ return restored;
71
+ }
72
+ function readEventsInWorker(eventsPath) {
73
+ return new Promise((resolve, reject) => {
74
+ const worker = new Worker(EVENT_SNAPSHOT_WORKER_URL, {
75
+ workerData: {
76
+ eventsPath,
77
+ coreModuleUrl: new URL('../../evolver-core/dist/index.js', import.meta.url).href,
78
+ },
79
+ });
80
+ let settled = false;
81
+ const settle = (action) => {
82
+ if (settled)
83
+ return;
84
+ settled = true;
85
+ action();
86
+ };
87
+ worker.once('message', (response) => {
88
+ settle(() => {
89
+ if (response.ok)
90
+ resolve(response.events);
91
+ else
92
+ reject(workerFailure(response.error));
93
+ });
94
+ });
95
+ worker.once('error', (error) => settle(() => reject(error)));
96
+ worker.once('exit', (code) => {
97
+ settle(() => reject(new Error(code === 0
98
+ ? 'event snapshot worker exited without a response'
99
+ : `event snapshot worker exited with code ${code}`)));
100
+ });
101
+ });
102
+ }
103
+ export function fileEventSnapshotSource(eventsPath, readEvents = readEventsInWorker) {
7
104
  return {
8
- version: () => {
9
- try {
10
- const stat = statSync(eventsPath, { bigint: true });
11
- return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
12
- }
13
- catch (error) {
14
- if (isMissing(error))
15
- return 'missing';
16
- throw error;
17
- }
18
- },
19
- read: () => ev.readEvents(eventsPath),
105
+ version: () => fileSnapshotVersion(eventsPath),
106
+ read: () => readEvents(eventsPath),
20
107
  };
21
108
  }
22
- /** Reuses a parsed append-only event snapshot only when the file identity is stable across the read. */
109
+ /** Reuses a parsed event history only when the active file and archive segments stay stable across the read. */
23
110
  export class EventSnapshotCache {
24
111
  source;
25
112
  cached;
113
+ inFlight;
26
114
  constructor(source) {
27
115
  this.source = source;
28
116
  }
29
117
  read() {
118
+ if (this.inFlight !== undefined)
119
+ return this.inFlight;
120
+ const pending = this.readOnce().finally(() => {
121
+ if (this.inFlight === pending)
122
+ this.inFlight = undefined;
123
+ });
124
+ this.inFlight = pending;
125
+ return pending;
126
+ }
127
+ async readOnce() {
30
128
  let before;
31
129
  try {
32
- before = this.source.version();
130
+ before = await this.source.version();
33
131
  }
34
132
  catch {
35
- return this.source.read();
133
+ return await this.source.read();
36
134
  }
37
135
  if (this.cached?.version === before)
38
136
  return this.cached.events;
39
- const events = this.source.read();
137
+ const events = await this.source.read();
40
138
  let after;
41
139
  try {
42
- after = this.source.version();
140
+ after = await this.source.version();
43
141
  }
44
142
  catch {
45
143
  return events;
@@ -0,0 +1,57 @@
1
+ export declare const GITHUB_PR_TIMEOUT_MS = 15000;
2
+ export declare const GITHUB_PR_MAX_BUFFER_BYTES: number;
3
+ export declare const GITHUB_PR_CACHE_TTL_MS = 45000;
4
+ export declare const GITHUB_PR_MAX_ITEMS = 50;
5
+ export interface GithubPrCheckCounts {
6
+ total: number;
7
+ passed: number;
8
+ failed: number;
9
+ pending: number;
10
+ }
11
+ export interface GithubPrDiagnosticRow {
12
+ number: number;
13
+ title: string;
14
+ url: string;
15
+ state: 'OPEN' | 'CLOSED' | 'MERGED' | 'UNKNOWN';
16
+ isDraft: boolean;
17
+ head: string;
18
+ base: string;
19
+ updatedAt: string | null;
20
+ reviewDecision: 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | 'UNKNOWN' | null;
21
+ checks: GithubPrCheckCounts;
22
+ }
23
+ export interface GithubPrDiagnosticData {
24
+ prs: GithubPrDiagnosticRow[];
25
+ truncated: boolean;
26
+ refreshedAt: string;
27
+ }
28
+ export type GithubPrDiagnosticsResult = {
29
+ available: true;
30
+ data: GithubPrDiagnosticData;
31
+ } | {
32
+ available: false;
33
+ error: 'github_pr_unavailable' | 'github_pr_invalid_response';
34
+ };
35
+ export interface GithubPrRunnerOptions {
36
+ cwd?: string;
37
+ timeoutMs: number;
38
+ maxBufferBytes: number;
39
+ shell: false;
40
+ }
41
+ export interface GithubPrRunnerResult {
42
+ code: number;
43
+ stdout: string;
44
+ }
45
+ export type GithubPrRunner = (command: string, args: readonly string[], options: GithubPrRunnerOptions) => Promise<GithubPrRunnerResult>;
46
+ export interface GithubPrDiagnosticsProvider {
47
+ read(): Promise<GithubPrDiagnosticsResult>;
48
+ }
49
+ export interface GithubPrDiagnosticsProviderOptions {
50
+ cwd?: string;
51
+ runner?: GithubPrRunner;
52
+ now?: () => number;
53
+ ttlMs?: number;
54
+ maxItems?: number;
55
+ }
56
+ export declare const defaultGithubPrRunner: GithubPrRunner;
57
+ export declare function createGithubPrDiagnosticsProvider(options?: GithubPrDiagnosticsProviderOptions): GithubPrDiagnosticsProvider;