software-dashboard-ui.ts 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. /** Self-contained read-only view. No credentials, CDN, or project-specific data. */
  2. export function softwareDashboardHTML(token: string) {
  3. return `<!doctype html>
  4. <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>重构观察台 · LambdaAgentPaaS</title>
  5. <style>
  6. :root{color-scheme:light;--ink:#1d1d1f;--muted:#737379;--blue:#0071e3;--line:#e8e8ed;--green:#167650;--amber:#a16108;--red:#b93838}*{box-sizing:border-box}body{margin:0;background:#f5f5f7;color:var(--ink);font:14px/1.6 -apple-system,BlinkMacSystemFont,"SF Pro Display","PingFang SC",sans-serif}button,input,select{font:inherit}button{cursor:pointer;border:0;background:#f0f0f3;color:var(--ink);border-radius:10px;padding:9px 15px}button:hover{background:#e6e6eb}button:focus-visible,a:focus-visible,select:focus-visible,input:focus-visible{outline:3px solid #0071e366;outline-offset:3px}button:disabled{opacity:.5;cursor:default}header{height:64px;background:#ffffffed;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 max(24px,calc((100vw - 1240px)/2));gap:20px;position:sticky;top:0;z-index:3;backdrop-filter:blur(18px)}.brand{font-size:16px;font-weight:650;letter-spacing:-.3px}.brand b{color:var(--blue);margin-right:9px}.status{font-size:12px;color:var(--muted)}main{max-width:1240px;margin:auto;padding:40px 24px 65px}.eyebrow{font-size:11px;letter-spacing:2px;color:var(--muted);font-weight:650}.hero{display:flex;align-items:flex-start;justify-content:space-between;gap:28px;margin-bottom:24px}h1{font-size:34px;line-height:1.3;letter-spacing:-1px;margin:10px 0 12px}h2{font-size:22px;letter-spacing:-.5px;margin:0 0 6px}h3{font-size:15px;margin:0 0 10px}p{margin:6px 0 12px}.muted{color:var(--muted)}.small{font-size:12px}.pill{display:inline-flex;padding:5px 12px;border-radius:20px;background:#eaf3ff;color:var(--blue);font-size:12px;white-space:nowrap}.good{background:#eaf6ef;color:var(--green)}.warn{background:#fff3e0;color:var(--amber)}.bad{background:#fcecec;color:var(--red)}.banner{padding:12px 18px;border-radius:12px;background:#fff3e0;color:#81520f;margin:0 0 20px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.metric,.card{background:white;border:1px solid #ebebef;border-radius:20px;padding:24px}.metric strong{display:block;font-size:32px;font-weight:600;letter-spacing:-1px;margin:8px 0 2px}.metric span,.metric small{color:var(--muted);font-size:12px}.progress{height:5px;background:#ededf2;border-radius:8px;overflow:hidden;margin-top:12px}.progress i{display:block;height:100%;background:var(--blue);border-radius:8px}.steps{display:flex;margin:22px 0;gap:8px}.step{flex:1;padding:12px 4px;border-top:2px solid #dddde3;color:var(--muted);font-size:12px}.step.active{border-color:var(--blue);color:var(--blue);font-weight:650}.step small{display:block;font-weight:400}.tabs{display:flex;gap:6px;border-bottom:1px solid var(--line);margin:8px 0 26px;overflow-x:auto}.tabs button{border-radius:0;background:none;color:var(--muted);padding:14px 18px;white-space:nowrap;border-bottom:2px solid transparent}.tabs button.active{border-color:var(--ink);color:var(--ink);font-weight:650}.grid{display:grid;grid-template-columns:1.2fr 1fr;gap:18px}.stack>*+*{margin-top:18px}.line{display:flex;justify-content:space-between;align-items:center;gap:14px}.sectionhead{margin-bottom:20px}.directory{display:grid;grid-template-columns:minmax(120px,1fr) 1.2fr 40px;align-items:center;gap:12px;margin:14px 0;font-size:12px}.directory .progress{margin:0;height:7px}.directory span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty{padding:28px 16px;text-align:center;color:var(--muted);border:1px dashed #dcdce2;border-radius:14px}.flow{display:flex;align-items:stretch;gap:10px;margin:22px 0}.node{border:1px solid #dce8f6;border-radius:15px;background:#f5f9ff;padding:18px;flex:1;min-width:0}.node small{display:block;color:var(--muted);margin-bottom:8px}.node b{display:block;overflow-wrap:anywhere}.arrow{align-self:center;color:#9daab8;font-size:24px}.path{font:12px/1.6 ui-monospace,SFMono-Regular,monospace;overflow-wrap:anywhere;color:#5b6776}.paths{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.pathcard{padding:16px;background:#f8f8fa;border-radius:13px;min-width:0}.pathcard summary{cursor:pointer}.pathcard ul{padding-left:18px}.candidatebar{display:flex;gap:12px;margin-bottom:22px}.candidatebar select,.candidatebar input{border:1px solid #dcdce2;border-radius:10px;background:white;padding:10px;min-width:0}.candidatebar select{flex:1}table{width:100%;border-collapse:collapse;font-size:13px}td,th{text-align:left;padding:13px 8px;border-bottom:1px solid #efeff3;vertical-align:top;overflow-wrap:anywhere}th{color:var(--muted);font-weight:500}.tablewrap{overflow:auto}.diff{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:12px 0 20px}.diff>div{min-width:0}.diff h3{padding:9px 14px;margin:0;border-radius:10px 10px 0 0;font-size:12px}.before h3{background:#fcefee;color:#a44c46}.after h3{background:#edf7ef;color:#307349}pre{font:12px/1.7 ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;overflow-wrap:anywhere;max-height:420px;overflow:auto;background:#f8f8fa;padding:16px;border-radius:10px;margin:0}.before pre,.after pre{border-radius:0 0 10px 10px}.minimetrics{display:flex;gap:28px;padding:16px 0}.minimetrics strong{display:block;font-size:24px;font-weight:600}.minimetrics span{font-size:12px;color:var(--muted)}.issue{padding:16px 0;border-bottom:1px solid var(--line)}.issue:last-child{border:0}.issue b{font-size:14px}.issue p{margin:6px 0 0;font-size:13px;color:var(--muted)}.link{background:transparent;color:var(--blue);padding:4px 0;text-align:left}.candidate-row{border:1px solid var(--line);border-radius:12px;margin:10px 0;padding:16px}.candidate-row b{overflow-wrap:anywhere}.files{display:flex;flex-direction:column;gap:7px;max-height:260px;overflow:auto}.files button{background:#f7f7f9;text-align:left;font:12px ui-monospace,monospace;overflow-wrap:anywhere}.count{font-variant-numeric:tabular-nums}footer{font-size:12px;color:var(--muted);margin-top:26px;display:flex;justify-content:space-between;gap:20px}dialog{border:1px solid var(--line);border-radius:20px;width:min(920px,94vw);max-height:85vh;padding:24px;color:var(--ink);box-shadow:0 25px 100px #0002}dialog::backdrop{background:#17172355;backdrop-filter:blur(4px)}dialog .line{margin-bottom:16px}dialog h3{overflow-wrap:anywhere;margin:0}dialog pre{max-height:55vh}.badge-list{display:flex;gap:6px;flex-wrap:wrap}.badge-list span{background:#f4f4f6;border-radius:7px;padding:5px 9px;font-size:12px}.loading{padding:80px;text-align:center;color:var(--muted)}@media(max-width:800px){main{padding:24px 16px}.metrics{grid-template-columns:repeat(2,1fr)}.grid,.paths,.diff{grid-template-columns:1fr}.hero{display:block}h1{font-size:27px}.hero>.pill{margin-top:8px}.flow{flex-direction:column}.arrow{transform:rotate(90deg)}.steps{overflow-x:auto}.step{min-width:110px}.candidatebar{flex-direction:column}header{padding:0 16px}.status{max-width:170px;text-align:right}footer{display:block}}
  7. </style></head><body>
  8. <header><div class="brand"><b>λ</b>重构观察台 <span class="muted small">/ LambdaAgentPaaS</span></div><div class="line"><span id="sync" class="status" role="status">正在连接任务</span><button id="refresh" aria-label="刷新任务">刷新</button></div></header>
  9. <main><div id="error" role="alert"></div><section id="summary"><div class="loading">正在读取真实任务与验收证据…</div></section><nav id="tabs" class="tabs" aria-label="结果视图"></nav><section id="content"></section><footer><span>只读看板 · 在 OpenCode 的 /refactor 中选择、确认与审查</span><span id="identity"></span></footer></main>
  10. <dialog id="artifact"><div class="line"><h3 id="artifact-title"></h3><button id="close">关闭</button></div><pre id="artifact-text"></pre><div class="line"><span id="artifact-page" class="small muted"></span><button id="more">下一页</button></div></dialog>
  11. <script>
  12. const token=${JSON.stringify(token)};
  13. let state=null,index=null,tab='overview',chosen='',filter='all',search='',candidatePage=0,busy=false,lastRevision=-1,artifactOffset=0,artifactName='',artifactNext=null,artifactEpoch=0,artifactEvidence=null;
  14. const $=s=>document.querySelector(s);
  15. const esc=x=>String(x??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
  16. const arr=x=>Array.isArray(x)?x:[];
  17. const n=x=>typeof x==='number'?x.toLocaleString():'—';
  18. const short=p=>String(p??'').split('/').pop();
  19. const candidateName=c=>String(c.title||c.symbol||c.node_id||c.path||c.kind||'未命名候选');
  20. const groupCount=g=>g.files??g.java_files;
  21. const scope=()=>state?.analysis_scope||{};
  22. const phaseNames={analyzing:'本体分析中',analysis_failed:'分析失败',awaiting_selection:'等待选择候选',selected:'等待生成计划',awaiting_confirmation:'等待确认计划',running:'执行与验证中',awaiting_review:'等待质量审查',accepted_for_delivery:'已审定 · 待交付',blocked:'执行阻塞',verification_failed:'改后验证失败',execution_failed:'执行失败',changes_requested:'审查退回',cancelled:'任务已停止',interrupted:'执行中断',evidence_failed:'证据待恢复'};
  23. const nextSteps={changes_requested:'在 CLI 查看退回意见,返回规划后针对意见修订。',evidence_failed:'在 CLI 重新整理已执行的证据,无需重复运行补丁。',blocked:'在 CLI 查看阻塞原因,处理环境或调整计划。',verification_failed:'先看失败测试,再回到 CLI 调整计划。',cancelled:'任务已经停止。已有证据保留,可在 CLI 决定是否继续。',interrupted:'在 CLI 核对原任务的执行记录,再决定恢复方式。',analysis_failed:'查看分析错误与已扫描范围,修复原因后再发起分析。',analyzing:'分析完成后,在 CLI 查看候选并选择关注项。',awaiting_selection:'在 CLI 选择本轮关注项,交给智能体制定计划。',selected:'请智能体读取本体依据与源码,生成具体计划和补丁。',awaiting_confirmation:'在 CLI 审阅计划与补丁,确认后才会执行。',running:'正在执行原任务,可在下方查看已生成证据。',awaiting_review:'在 CLI 对照改动、测试与风险,完成人工质量审查。',accepted_for_delivery:'本轮已经人工审定;尚未自动合并或部署。'};
  24. const checkStatus=x=>({passed:'通过',failed:'失败',blocked:'受阻',not_run:'未运行',skipped:'跳过',timeout:'超时'})[x]||x||'未运行';
  25. const ruleStatus=()=>state.ontology?.rules_status==='source_bound_declared'?'已提供有出处的规则':state.ontology?.rules_status==='not_supplied'?'未提供业务规则':'规则状态未记录';
  26. const kinds={large_method:'方法职责与体量',assignment_in_condition:'条件内状态更新'};
  27. const decisions={selected:'本轮已选',deferred:'暂缓',retained:'保留现状'};
  28. const empty=s=>'<div class="empty">'+esc(s)+'</div>';
  29. const pill=(s,t='')=>'<span class="pill '+t+'">'+esc(s)+'</span>';
  30. const stats=()=>state?.ontology?.statistics||{};
  31. const result=()=>state?.execution?.result||{};
  32. const candidates=()=>arr(state?.candidates);
  33. const active=()=>candidates().find(c=>c.id===chosen)||candidates().find(c=>state.decisions?.[c.id]==='selected')||candidates()[0];
  34. async function request(path){const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),20000);try{const r=await fetch('/api/'+path,{headers:{'X-Review-Token':token},signal:controller.signal});const d=await r.json();if(!r.ok)throw Error(d.error||d.detail||'读取失败');return d}finally{clearTimeout(timer)}}
  35. function renderSummary(){
  36. const coverage=state.coverage||{}, repo=state.repository_summary||{}, count=candidates().length,selected=Object.values(state.decisions||{}).filter(x=>x==='selected').length;
  37. const failed=['analysis_failed','blocked','verification_failed','execution_failed','evidence_failed','interrupted'].includes(state.phase);
  38. const total=scope().total_files??coverage.total_files??coverage.java_files, analyzed=coverage.analyzed_files;
  39. const percent=typeof total==='number'&&total>0&&typeof analyzed==='number'?Math.min(100,100*analyzed/total):0;
  40. const stoppedAnalysis=!state.proposal&&['cancelled','interrupted','analysis_failed'].includes(state.phase);
  41. const stage=state.phase==='analyzing'||stoppedAnalysis?0:state.phase==='awaiting_selection'?1:['selected','awaiting_confirmation'].includes(state.phase)?2:['awaiting_review','accepted_for_delivery','changes_requested'].includes(state.phase)?4:3;
  42. $('#summary').innerHTML='<div class="hero"><div><div class="eyebrow">SOFTWARE / ONTOLOGY / EVIDENCE</div><h1>'+esc(state.project||'仓库重构')+'</h1><p class="muted">'+esc(stoppedAnalysis?(state.next_review?'已有后续分析任务,在 CLI 打开该任务继续查看。':'分析未完成。在 CLI 查看原因,选择保留记录并重新分析。'):nextSteps[state.phase]||'在 CLI 查看阻塞原因与证据,处理后再决定下一步。')+'</p></div>'+pill(phaseNames[state.phase]||state.phase,failed?'bad':state.phase==='accepted_for_delivery'?'good':'')+'</div><div class="metrics">'+
  43. '<div class="metric"><span>'+esc(scope().label||'分析范围内文件')+' · 已分析 / 总数</span><strong>'+n(analyzed)+' <span>/ '+n(total)+'</span></strong><div class="progress"><i style="width:'+percent+'%"></i></div></div>'+
  44. '<div class="metric"><span>初筛候选</span><strong>'+n(state.phase==='analyzing'?undefined:count)+'</strong><small>本轮已选 '+selected+' 项 · '+(state.phase==='analyzing'?'建模完成后公布':'候选不等于缺陷')+'</small></div>'+
  45. '<div class="metric"><span>本体关系</span><strong>'+n(stats().unique_edges)+'</strong><small>'+n(stats().unique_nodes)+' 个节点 · 静态结构</small></div>'+
  46. '<div class="metric"><span>实际改动文件</span><strong>'+n(Array.isArray(result().changed_files)?result().changed_files.length:undefined)+'</strong><small>'+(state.execution?.source_unchanged===true?'原仓库保持不变':'执行前显示 —,不推定为 0')+'</small></div></div>'+
  47. '<div class="steps">'+['构建本体','选择候选','审阅计划','执行与验证','质量审查'].map((s,i)=>'<div class="step '+(i===stage?'active':'')+'">0'+(i+1)+' '+s+(i===stage?'<small>'+(state.phase==='accepted_for_delivery'?'已完成审查':'当前阶段')+'</small>':'')+'</div>').join('')+'</div>';
  48. if(state.phase==='analyzing')$('#summary').insertAdjacentHTML('beforeend','<p class="small muted" role="status">'+(state.progress?.stage==='link'?'源码扫描完成,正在关联跨文件本体与约束。':'已完成 '+n(state.progress?.completed_batches)+' / '+n(state.progress?.total_batches)+' 个分析批次;进度自动更新。')+'</p>');
  49. $('#identity').textContent='审阅 '+state.id+' · 版本 '+state.revision;
  50. }
  51. function render(){if(!state)return;const focused=document.activeElement,focusID=focused?.id,selection=focusID==='search'?[focused.selectionStart,focused.selectionEnd]:null;const opened=[...document.querySelectorAll('#content details[open]')].map(d=>d.querySelector('summary')?.textContent);renderSummary();$('#tabs').innerHTML=[['overview','项目总览'],['impact','影响路径'],['changes','前后变化'],['tests','测试结果'],['risks','剩余问题']].map(([id,label])=>'<button data-tab="'+id+'" class="'+(tab===id?'active':'')+'" aria-current="'+(tab===id?'page':'false')+'">'+label+'</button>').join('');$('#content').innerHTML=({overview,impact,changes,tests,risks}[tab])();document.querySelectorAll('#content details').forEach(d=>{if(opened.includes(d.querySelector('summary')?.textContent))d.open=true});if(focusID){const restored=document.getElementById(focusID);restored?.focus({preventScroll:true});if(selection&&restored)restored.setSelectionRange(...selection)}}
  52. function overview(){
  53. const repo=state.repository_summary||{}, groups=arr(repo.directory_groups),max=Math.max(1,...groups.map(g=>groupCount(g)??0));
  54. return '<div class="grid"><article class="card"><div class="sectionhead"><h2>看清仓库,再确定改动。</h2><p class="muted small">'+esc(repo.directory_group_basis==='snapshot_files'?'冻结源码清单中的全文件目录分布':'目录分布以服务端返回的统计口径为准')+';目录分组不代表业务模块。</p></div>'+(groups.length?groups.map(g=>'<div class="directory"><span title="'+esc(g.name)+'">'+esc(g.name)+'</span><div class="progress"><i style="width:'+100*(groupCount(g)??0)/max+'%"></i></div><span class="count">'+n(groupCount(g))+'</span></div>').join(''):empty('当前服务未提供全仓库目录统计;不以候选文件代替整体规模。'))+'<div class="minimetrics">'+(Array.isArray(repo.facts)?repo.facts:[{label:'纳入快照的文件',value:repo.files}]).map(f=>'<div><strong>'+n(f.value)+'</strong><span>'+esc(f.label)+'</span></div>').join('')+'</div><details><summary class="small muted">文件类型分布 · 按扩展名统计</summary><div class="badge-list">'+arr(repo.file_types).map(f=>'<span>'+esc(f.extension)+' · '+n(f.files)+'</span>').join('')+'</div></details><p class="small muted">分析适配器:'+esc(scope().adapter||'服务端未说明')+'</p></article><div class="stack"><article class="card"><h2>本体如何参与重构</h2><div class="flow"><div class="node"><small>事实</small><b>源码与约束</b></div><span class="arrow">→</span><div class="node"><small>关联</small><b>本体影响路径</b></div><span class="arrow">→</span><div class="node"><small>决策</small><b>候选与验证</b></div></div><p class="small muted">'+n(stats().resolved_references)+' 条已解析引用 · '+n(stats().unresolved_references)+' 条未解析引用</p><button class="link" data-tab="impact">查看候选对应的真实依据 →</button></article><article class="card"><h3>本轮目标</h3><p>'+esc(state.proposal?.plan?.goal||'尚未确认重构计划。先审阅候选与影响范围。')+'</p><div class="badge-list">'+arr(state.proposal?.plan?.allowed_files).map(p=>'<span title="'+esc(p)+'">'+esc(short(p))+'</span>').join('')+'</div><p class="small muted">业务规则:'+ruleStatus()+'</p></article></div></div>';
  55. }
  56. function relationMap(c,witnesses){
  57. const shown=witnesses.slice(0,6);if(!shown.length)return '';
  58. const height=Math.max(180,shown.length*64),middle=height/2;
  59. return '<svg viewBox="0 0 900 '+height+'" role="img" aria-label="候选的静态可达关联图" style="width:100%;margin:18px 0">'+shown.map((w,i)=>'<path d="M 290 '+middle+' C 430 '+middle+',430 '+(32+i*64)+',550 '+(32+i*64)+'" fill="none" stroke="#b7c9df" stroke-width="2"/>').join('')+'<rect x="20" y="'+(middle-40)+'" width="270" height="80" rx="15" fill="#edf5ff" stroke="#9fc6f2"/><text x="38" y="'+(middle-7)+'" fill="#0071e3" font-size="12">当前候选</text><text x="38" y="'+(middle+18)+'" fill="#1d1d1f" font-size="15">'+esc(short(c.path).slice(0,27))+'</text>'+shown.map((w,i)=>'<g><title>'+esc(w.path)+'</title><rect x="550" y="'+(8+i*64)+'" width="330" height="48" rx="10" fill="#f7f8fa" stroke="#e2e6ec"/><text x="565" y="'+(37+i*64)+'" font-size="13" fill="#405269">'+esc(short(w.path).slice(0,38))+'</text></g>').join('')+'</svg><p class="small muted">显示前 '+shown.length+' 条静态可达关联;连线不表示直接调用,具体关系见下方证据。</p>';
  60. }
  61. function unresolvedEvidence(context){
  62. const refs=arr(context.unresolved_references),reasons={external_or_unindexed_symbol:'外部库或未索引类型',ambiguous_project_symbol:'存在同名声明',wildcard_import_requires_type_resolution:'通配导入待解析',production_to_test_source:'生产代码指向测试类型'};
  63. if(!refs.length)return '';
  64. return '<details><summary>待查证引用 · '+n(context.unresolved_references_total??refs.length)+' 条</summary>'+refs.slice(0,8).map(r=>'<div class="issue"><b>'+esc(r.target)+'</b><div class="path">'+esc(r.path)+' · L'+esc(r.line??'—')+'</div><p>'+esc(reasons[r.reason]||r.reason||'解析依据不足')+'</p></div>').join('')+(refs.length>8||context.unresolved_references_truncated?'<p class="small muted">仅显示前 '+Math.min(8,refs.length)+' 条;完整关系见本体证据,需结合源码核查。</p>':'')+'</details>';
  65. }
  66. function dependencyEvidence(context){
  67. const deps=arr(context.dependency_paths);
  68. return '<h3 style="margin-top:24px">下游依赖 · 需要保持哪些组件契约</h3>'+(Array.isArray(context.dependency_paths)?'<p class="small muted">'+n(context.dependency_paths_total??deps.length)+' 个直接依赖;静态类型关联不等于方法调用。</p><div class="paths">'+deps.slice(0,8).map(d=>'<div class="pathcard"><b>'+esc(short(d.path))+'</b><div class="path">'+esc(d.path)+'</div><details><summary class="small muted">引用依据</summary>'+arr(d.via).map(e=>'<div class="path">'+esc(e.origin?.path||'')+' · L'+esc(e.origin?.line??'—')+'</div>').join('')+'</details></div>').join('')+'</div>'+(deps.length>8||context.dependency_paths_truncated?'<p class="small muted">摘要有省略;规划时可分页读取完整关联源码。</p>':'')+(deps.length?'':'<p class="muted">本次未解析到直接依赖;不表示没有依赖。</p>'):empty('旧记录未采集下游依赖摘要。'))+(typeof context.unresolved_references_total==='number'?'<p class="small muted">本候选邻域有 '+n(context.unresolved_references_total)+' 条未解析引用,可能包含外部库,需要按具体关系查证。</p>':'')+unresolvedEvidence(context);
  69. }
  70. function impact(){
  71. const c=active();if(!c)return empty('候选尚未产生。分析完成后在这里查看源码与本体影响路径。');
  72. const context=c.ontology_context||{}, witnesses=arr(context.impact_paths), shown=witnesses.slice(0,8), rules=arr(context.rules);
  73. return '<div class="candidatebar"><select id="candidate" aria-label="查看候选">'+candidates().map(x=>'<option value="'+esc(x.id)+'" '+(x.id===c.id?'selected':'')+'>'+esc(short(x.path)+' · '+candidateName(x))+'</option>').join('')+'</select>'+pill(decisions[state.decisions?.[c.id]]||'待确认')+'</div><article class="card"><div class="sectionhead"><h2>'+esc(c.title||kinds[c.kind]||c.kind||'待审候选')+'</h2><p>'+esc(c.suggestion)+'</p></div><div class="flow"><div class="node"><small>源文件 · 第 '+esc((c.source_range||c.method_range)?.start??'—')+'–'+esc((c.source_range||c.method_range)?.end??'—')+' 行</small><b>'+esc(short(c.path))+'</b><span class="path">'+esc(c.path)+'</span></div><span class="arrow">→</span><div class="node"><small>本体节点</small><b>'+esc(candidateName(c))+'</b><span class="small muted">'+n(c.metrics?.method_lines)+' 行 · '+n(c.metrics?.branch_points)+' 个分支点</span></div><span class="arrow">→</span><div class="node"><small>静态可达范围</small><b>'+n(context.impact_paths_total??witnesses.length)+' 条路径</b><span class="small muted">'+arr(c.test_hints).length+' 个测试提示,覆盖未证明</span></div></div><details><summary>查看源码依据</summary><pre>'+esc(Array.isArray(c.source_excerpt)?c.source_excerpt.map(e=>'L'+e.line+' '+e.text).join('\\n'):c.source_excerpt||arr(c.evidence).map(e=>'L'+e.line+' '+e.quote).join('\\n'))+'</pre></details><h3 style="margin-top:24px">上游影响 · 哪些文件可能受影响</h3>'+relationMap(c,witnesses)+'<div class="paths">'+shown.map(w=>'<div class="pathcard"><b>'+esc(short(w.path))+'</b><div class="path">'+esc(w.path)+'</div><details><summary class="small muted">展开关系依据 · '+n(w.via_total??arr(w.via).length)+' 条</summary>'+arr(w.via).map(e=>'<div class="issue"><b>'+esc(e.relation)+'</b><div class="path">'+esc(e.origin?.path||e.origin?.source||'')+' '+esc(e.origin?.line?'L'+e.origin.line:'')+'</div><p>'+esc(e.origin?.quote||e.origin?.excerpt||'')+'</p></div>').join('')+(w.via_truncated?'<p class="small muted">部分关系已截断。</p>':'')+'</details></div>').join('')+'</div>'+(witnesses.length?'':'<p class="muted">暂无可展示的关联路径;不表示不存在依赖。</p>')+'<p class="small muted">显示 '+shown.length+' / '+n(context.impact_paths_total??witnesses.length)+' 条。'+(context.impact_paths_truncated?'服务端返回也有截断;完整证据见 analysis/linked-ontology.json。':'')+'引用和可达关系不等于实际运行调用链。</p>'+dependencyEvidence(context)+'<h3>必须保持的业务约束</h3>'+(rules.length?'<div class="badge-list">'+arr(context.required_invariants).map(r=>'<span>'+esc(r)+'</span>').join('')+'</div>':empty('未提供与本候选绑定的业务规则。'))+'</article>';
  74. }
  75. function changes(){
  76. const p=state.proposal,r=result();if(!p)return empty('尚未生成计划。确认候选后,由智能体提交具体补丁。');
  77. const changed=arr(r.changed_files),edits=arr(p.edits);
  78. return '<article class="card"><div class="sectionhead line"><div><h2>从计划到实际改动</h2><p class="muted small">提案片段用于解释意图;实际变化以执行记录与 changes.patch 为准。</p></div>'+pill(state.execution?'已有执行记录':'尚未执行','')+'</div><div class="flow"><div class="node"><small>计划范围</small><b>'+arr(p.plan.allowed_files).length+' 个文件</b><span class="small muted">'+edits.length+' 处替换片段</span></div><span class="arrow">→</span><div class="node"><small>已记录的实际变化</small><b>'+(Array.isArray(r.changed_files)?changed.length+' 个文件':'尚无结果')+'</b><span class="small muted">'+(state.execution?.source_unchanged===true?'在工作副本内执行':'原仓库状态尚未确认')+'</span></div><span class="arrow">→</span><div class="node"><small>验证与审查</small><b>'+(r.verification_passed===true?'冻结范围验证通过':state.execution?'查看验证记录':'尚未验证')+'</b><span class="small muted">'+(state.phase==='accepted_for_delivery'?'已人工审定,未自动发布':'人工质量审查独立进行')+'</span></div></div><div class="badge-list">'+changed.map(p=>'<span>'+esc(p)+'</span>').join('')+'</div><p>'+esc(p.plan.goal)+'</p><h3>计划中的前后片段</h3>'+edits.map(e=>'<div class="path">'+esc(e.path)+'</div><div class="diff"><div class="before"><h3>修改前 · 提案</h3><pre>'+esc(e.old)+'</pre></div><div class="after"><h3>修改后 · 提案</h3><pre>'+esc(e.new)+'</pre></div></div>').join('')+'<h3>保持不变的行为</h3><ul>'+arr(p.plan.invariants).map(x=>'<li>'+esc(x)+'</li>').join('')+'</ul><h3>业务验收计划</h3>'+businessAcceptance(p.plan.business_acceptance,true)+'<div class="line"><button data-evidence="changes.patch">查看实际补丁</button><button data-comparison>查看本体变化统计</button></div><div id="comparison" style="margin-top:16px"></div></article>';
  79. }
  80. function verdict(value){return value===true?pill('通过','good'):value===false?pill('未通过','bad'):pill('未确认','warn')}
  81. function businessAcceptance(spec,planned=false){
  82. if(!spec&&!planned)return empty('尚未取得业务验收结果。');
  83. if(!spec||spec.status==='not_declared')return empty('未声明业务验收;构建与回归通过不能代替业务验证。');
  84. const names={input_validation:'输入校验',write_scope:'写入范围',resource_ownership:'资源归属',workflow_integrity:'业务流程完整性',query_context:'查询上下文',units_ranges:'单位与取值范围',variant_revision:'车型与版本',protocol_compatibility:'协议兼容',retry_idempotency:'重试与幂等',audit_traceability:'审计可追溯'};
  85. const label=c=>esc(names[c]||c);
  86. return '<p>'+pill(planned?'待执行':spec.passed===true?'声明范围通过':'未通过或未验证',!planned&&spec.passed===true?'good':'warn')+'</p>'+arr(spec.contracts).map(c=>'<div class="issue"><div class="line"><b>'+label(c.category)+'</b>'+pill(planned?'待执行':c.status==='verified'?'已绑定通过的测试':'未验证',!planned&&c.status==='verified'?'good':'warn')+'</div><p>'+esc(c.invariant)+'</p><div class="path">'+esc(c.test_path)+' → '+esc(c.test_case)+'</div>'+(planned?'<details><summary>断言依据</summary><pre>'+esc(c.quote)+'</pre></details>':'')+(Array.isArray(c.executed_test_cases)?'<p class="small muted">已绑定 '+c.executed_test_cases.length+' 个实际执行用例</p>':'')+(c.reason?'<p>'+esc(c.reason)+'</p>':'')+'</div>').join('')+arr(spec.exclusions).map(e=>'<p class="small muted">排除 · '+label(e.category)+':'+esc(e.reason)+'</p>').join('')+arr(spec.missing_categories).map(c=>'<p class="small">缺项 · '+label(c)+'</p>').join('')+'<p class="small muted">仅核验声明与已执行测试的绑定;排除项不计通过。</p>';
  87. }
  88. function tests(){
  89. const rows=Object.entries(index?.checks||{}).filter(([k])=>k==='baseline/checks.json'||(typeof result().generation==='number'?k==='round-'+result().generation+'/checks.json':k.startsWith('round-')&&k.endsWith('/checks.json')));
  90. return '<article class="card"><div class="sectionhead"><h2>同一范围,前后对照。</h2><p class="muted small">数字来自测试报告。构建、行为测试和业务验收分别判断。</p></div>'+(rows.length?'<div class="tablewrap"><table><thead><tr><th>检查阶段</th><th>综合结果</th><th>构建 / 测试</th><th>通过</th><th>失败</th><th>跳过</th><th>未知</th><th>证据</th></tr></thead><tbody>'+rows.map(([name,v])=>'<tr><td>'+ (name.startsWith('baseline')?'重构前 · 基线':'重构后 · 第 '+name.split('/')[0].slice(6)+' 轮')+'</td><td>'+verdict(v.passed)+'</td><td>'+esc(checkStatus(v.build_status))+' / '+esc(checkStatus(v.test_status))+'</td>'+['passed','failed','skipped','unknown'].map(k=>'<td class="count">'+n(v.case_counts?.[k])+'</td>').join('')+'<td><button class="link" data-evidence="'+esc(name)+'">查看</button></td></tr>').join('')+'</tbody></table></div>':empty(index?'本轮尚无测试检查报告;未执行不计通过。':'测试证据尚未载入。'))+rows.filter(([,v])=>v.build_reason||v.test_reason||v.ontology_error).map(([name,v])=>'<div class="issue"><b>'+esc(name.startsWith('baseline')?'基线检查说明':'改后检查说明')+'</b><p>'+esc([v.build_reason,v.test_reason,v.ontology_error?'本体重建失败:'+v.ontology_error:''].filter(Boolean).join(';'))+'</p></div>').join('')+'<div class="grid" style="margin-top:24px"><div class="pathcard"><h3>行为保持</h3><p>通过仅表示已执行测试和冻结验证范围内未发现回归,不代表所有行为等价。</p></div><div class="pathcard"><h3>业务范围</h3>'+pill(result().business_scope_verified===true?'声明的业务范围已验证':'业务范围尚未验证',result().business_scope_verified===true?'good':'warn')+'<p class="small muted">业务规则与实际运行效果不能用构建通过代替。</p></div></div>'+'<h3>业务验收 · 重构前</h3>'+businessAcceptance(result().business_acceptance?.baseline)+'<h3>业务验收 · 重构后</h3>'+businessAcceptance(result().business_acceptance?.current)+rows.map(([name,v])=>arr(v.failed_cases).length?'<div class="issue"><h3>'+esc(name)+' · 失败测试</h3>'+arr(v.failed_cases).map(c=>'<p class="path">'+esc(c.id||c.class)+'</p>').join('')+(v.failed_cases_truncated?'<p>仅显示前 20 项。</p>':'')+'</div>':'').join('')+'</article><article class="card" style="margin-top:18px"><h3>证据文件</h3><p class="small muted">按需读取本任务文件,默认展示文字分页,不自动执行其中内容。</p><div class="files">'+arr(index?.files).map(f=>'<button data-evidence="'+esc(f.name)+'">'+esc(f.name)+' <span class="muted">· '+n(f.bytes)+' B</span></button>').join('')+'</div></article>';
  91. }
  92. function risks(){
  93. const list=candidates().filter(c=>(filter==='all'||(state.decisions?.[c.id]||'pending')===filter)&&(!search||(String(c.path||'')+' '+candidateName(c)).toLowerCase().includes(search.toLowerCase()))),unresolved=stats().unresolved_references;candidatePage=Math.min(candidatePage,Math.max(0,Math.ceil(list.length/20)-1));const offset=candidatePage*20;
  94. return '<div class="grid"><article class="card"><h2>边界与待处理事项</h2>'+(state.error?'<div class="issue"><b>当前阻塞</b><p>'+esc(state.error)+'</p></div>':'')+'<div class="issue"><b>'+n(unresolved)+' 条引用尚未解析</b><p>静态分析存在边界,影响范围仍需结合源码与测试审阅。</p></div><div class="issue"><b>'+ruleStatus()+'</b><p>未提供的行业规则不会由模型自动编造。</p></div><div class="issue"><b>已选不等于已解决</b><p>候选是否消失可查看本体变化统计;业务效果需独立证据。</p></div><div class="issue"><b>覆盖范围</b><p>分析适配器:'+esc(scope().adapter||'服务端未说明')+'。测试提示不是覆盖证明。'+(arr(state.coverage?.batches).some(b=>b.truncated)?'部分批次候选达到上限,返回结果有截断。':'')+'</p></div></article><article class="card"><h2>人工审查记录</h2>'+(state.quality_review?'<p>'+pill(state.quality_review.decision==='accept'?'人工已通过':'人工退回',state.quality_review.decision==='accept'?'good':'warn')+'</p>'+(state.quality_review.decision==='revise'?[['reason','需要修改的原因']]:[['benefits','重构收益'],['invariants','行为保持依据'],['risks','剩余风险']]).map(([k,label])=>'<div class="issue"><b>'+label+'</b><p>'+esc(state.quality_review.notes?.[k]||'未填写')+'</p></div>').join(''):empty('尚无人工质量结论。验证通过后,在 CLI 完成审查。'))+'</article></div><article class="card" style="margin-top:18px"><div class="line sectionhead"><h2>候选清单</h2><span class="small muted">查看不会改变 CLI 中的选择</span></div><div class="candidatebar"><select id="filter" aria-label="候选状态">'+[['all','全部'],['pending','待确认'],['selected','本轮已选'],['deferred','暂缓'],['retained','保留现状']].map(([v,l])=>'<option value="'+v+'" '+(v===filter?'selected':'')+'>'+l+'</option>').join('')+'</select><input id="search" placeholder="搜索文件或方法" aria-label="搜索候选" value="'+esc(search)+'"></div><div class="line" style="margin-bottom:12px"><span class="small muted">显示 '+(list.length?offset+1:0)+'–'+Math.min(offset+20,list.length)+' / '+list.length+' 项</span><div><button data-candidate-page="-1" '+(candidatePage===0?'disabled':'')+'>上一页</button> <button data-candidate-page="1" '+(offset+20>=list.length?'disabled':'')+'>下一页</button></div></div>'+list.slice(offset,offset+20).map(c=>'<div class="candidate-row"><div class="line"><b>'+esc(candidateName(c))+'</b>'+pill(decisions[state.decisions?.[c.id]]||'待确认')+'</div><div class="path">'+esc(c.path)+'</div><button class="link" data-candidate="'+esc(c.id)+'">查看影响依据 →</button></div>').join('')+(list.length?'':empty('当前筛选无候选。'))+'</article>';
  95. }
  96. async function comparison(){
  97. const el=$('#comparison');el.textContent='正在读取本轮本体变化…';
  98. try{const name='after-'+result().generation+'/comparison.json',d=await readDocument(name);const cc=d.candidate_changes||{};el.innerHTML='<div class="minimetrics">'+[['新增文件',(Array.isArray(d.added)?d.added.length:undefined)],['移除文件',(Array.isArray(d.removed)?d.removed.length:undefined)],['变化文件',(Array.isArray(d.changed)?d.changed.length:undefined)],['消失线索',(Array.isArray(cc.resolved)?cc.resolved.length:undefined)],['新增线索',(Array.isArray(cc.introduced)?cc.introduced.length:undefined)]].map(([label,count])=>'<div><strong>'+n(count)+'</strong><span>'+label+'</span></div>').join('')+'</div><p class="small muted">仅本轮重提取范围内的结构与规则初筛变化;线索消失不等于业务缺陷已解决。'+(cc.complete===false?'本次比较不完整。':'')+'</p>'}catch(e){el.textContent=e.message}
  99. }
  100. async function readDocument(name){
  101. const evidence=index;if(!evidence?.files?.some(f=>f.name===name))throw Error('本轮尚未生成该证据。');
  102. let text='',offset=0;for(let i=0;i<32;i++){const d=await request('evidence-text?name='+encodeURIComponent(name)+'&token='+encodeURIComponent(evidence.token)+'&offset='+offset+'&limit=16000');text+=d.text;if(d.next_offset===null)return JSON.parse(text);offset=d.next_offset}throw Error('证据较大,请在测试结果中分页查看原文件。')
  103. }
  104. async function showArtifact(name,offset=0){
  105. if(offset===0||name!==artifactName)artifactEvidence=index;const evidence=artifactEvidence;
  106. artifactName=name;artifactOffset=offset;const epoch=++artifactEpoch;$('#artifact-title').textContent=name;$('#artifact-text').textContent='正在读取…';$('#more').disabled=true;if(!$('#artifact').open)$('#artifact').showModal();
  107. try{if(!evidence?.files?.some(f=>f.name===name))throw Error('本轮尚未生成该证据。');const d=await request('evidence-text?name='+encodeURIComponent(name)+'&token='+encodeURIComponent(evidence.token)+'&offset='+offset+'&limit=16000');if(epoch!==artifactEpoch)return;$('#artifact-text').textContent=d.text;artifactNext=d.next_offset;$('#more').disabled=d.next_offset===null;$('#artifact-page').textContent='字符 '+offset+'–'+(offset+d.text.length)+' / '+d.total_chars+' · 证据版本 '+evidence.revision}catch(e){if(epoch===artifactEpoch){$('#artifact-text').textContent=e.message;$('#artifact-page').textContent=''}}
  108. }
  109. async function poll(){
  110. if(busy)return;busy=true;$('#refresh').disabled=true;
  111. try{const next=await request('review');const changed=!state||next.revision!==lastRevision;state=next;if(changed){index=null;lastRevision=next.revision;render()}$('#error').textContent='';$('#sync').textContent='已同步 '+new Date().toLocaleTimeString('zh-CN',{hour12:false});
  112. if(changed||!index||state.phase==='running'){try{const evidence=await request('evidence');if(evidence.revision!==state.revision){lastRevision=-1;throw Error('任务版本正在变化,下一次同步将读取一致证据。')}const updated=!index||index.token!==evidence.token;index=evidence;if(updated&&tab==='tests')render();}catch(e){$('#error').innerHTML='<div class="banner">证据暂不可用:'+esc(e.message)+'。状态已显示,未将缺失证据计为通过。</div>'}}
  113. }catch(e){$('#sync').textContent='连接中断 · 展示上次快照';$('#error').innerHTML='<div class="banner">同步失败:'+esc(e.message)+'。当前画面不是最新状态,可点击刷新。</div>'}finally{busy=false;$('#refresh').disabled=false}
  114. }
  115. document.addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;if(b.hasAttribute('data-candidate-page')){candidatePage+=Number(b.dataset.candidatePage);render()}if(b.dataset.tab){tab=b.dataset.tab;render()}if(b.dataset.candidate){chosen=b.dataset.candidate;tab='impact';render()}if(b.dataset.evidence)showArtifact(b.dataset.evidence);if(b.hasAttribute('data-comparison'))comparison()});
  116. document.addEventListener('change',e=>{if(e.target.id==='candidate'){chosen=e.target.value;render()}if(e.target.id==='filter'){filter=e.target.value;candidatePage=0;render()}});
  117. document.addEventListener('input',e=>{if(e.target.id==='search'){search=e.target.value;candidatePage=0;const pos=e.target.selectionStart;$('#content').innerHTML=risks();$('#search').focus();$('#search').setSelectionRange(pos,pos)}});
  118. $('#close').onclick=()=>{artifactEpoch++;$('#artifact').close()};$('#more').onclick=()=>{if(artifactNext!==null)showArtifact(artifactName,artifactNext)};$('#refresh').onclick=poll;
  119. setInterval(()=>{if(!document.hidden)poll()},3000);document.addEventListener('visibilitychange',()=>{if(!document.hidden)poll()});poll();
  120. </script></body></html>`
  121. }