dsh-showcase 0.1.1 → 0.1.3
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/CHANGELOG.md +32 -0
- package/README.md +58 -13
- package/package.json +10 -4
- package/plugin/client.js +12 -3
- package/plugin/dijiang-layout.js +161 -0
- package/plugin/dijiang-motion.js +101 -0
- package/plugin/dijiang-style.js +37 -0
- package/plugin/index.js +8 -2
- package/plugin/layout-summary.d.ts +1 -0
- package/plugin/layout-summary.js +111 -23
- package/plugin/poster-html.js +117 -24
- package/plugin/project-io.d.ts +5 -0
- package/plugin/project-io.js +82 -0
- package/plugin/redaction.js +26 -0
- package/plugin/report-content.js +67 -0
- package/plugin/report-schema.js +18 -8
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const escape = (value) => String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]);
|
|
2
|
+
|
|
3
|
+
export function renderDijiangLoader(projectName, en, instrument) {
|
|
4
|
+
return `<div class="loader loader--field" role="status" aria-label="${en ? "Opening local report" : "打开本地报告"}">
|
|
5
|
+
<section class="field-loader-sheet">
|
|
6
|
+
<canvas class="ed-contours" aria-hidden="true"></canvas>
|
|
7
|
+
<header class="field-loader-mast"><span class="field-loader-id">DSH / DIJIANG</span><span class="field-loader-project">${escape(projectName)}</span></header>
|
|
8
|
+
<div class="field-loader-center"><div class="field-loader-copy"><span class="field-loader-kicker">${en ? "LOCAL EVIDENCE / REPORT ACCESS" : "本地凭据 / 报告接入"}</span><strong>${en ? "EVIDENCE\nONLINE" : "证据接入"}</strong><p>${en ? "Dijiang / Opening the retained delivery record" : "终末地帝江号 / 正在打开交付档案"}</p></div>${instrument.replace("D-0017", "DIJIANG").replace("LINK / 04", "LOCAL")}</div>
|
|
9
|
+
<footer class="field-loader-statusbar"><div class="field-loader-readout"><span class="field-loader-status" data-loader-status>${en ? "Opening local report" : "打开本地报告"}</span><b data-loader-percent>00%</b></div><i class="field-loader-progress"><em data-loader-fill></em></i><div class="field-loader-stages" aria-hidden="true"><b>01 / LOCAL</b><b>02 / RECORDS</b><b>03 / DISPLAY</b></div></footer>
|
|
10
|
+
</section><div class="field-loader-topband" aria-hidden="true"></div>
|
|
11
|
+
</div>`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readDijiangPreferences(embedded, readStored) {
|
|
15
|
+
const prefs = { palette: "light", accent: "yellow", background: true, motion: true, fps: 24, speed: 1 };
|
|
16
|
+
const sources = [embedded];
|
|
17
|
+
try { sources.push(readStored()); } catch { /* Offline files may not have access to storage. */ }
|
|
18
|
+
for (const source of sources) {
|
|
19
|
+
try {
|
|
20
|
+
const saved = JSON.parse(source || "{}");
|
|
21
|
+
if (!saved || typeof saved !== "object" || Array.isArray(saved)) continue;
|
|
22
|
+
if (["light", "dark"].includes(saved.palette)) prefs.palette = saved.palette;
|
|
23
|
+
if (["yellow", "cyan"].includes(saved.accent)) prefs.accent = saved.accent;
|
|
24
|
+
if (typeof saved.background === "boolean") prefs.background = saved.background;
|
|
25
|
+
if (typeof saved.motion === "boolean") prefs.motion = saved.motion;
|
|
26
|
+
if ([24,60,120].includes(saved.fps)) prefs.fps = saved.fps;
|
|
27
|
+
if ([.5,1,1.5,2].includes(saved.speed)) prefs.speed = saved.speed;
|
|
28
|
+
} catch { /* A corrupt preference does not invalidate the report. */ }
|
|
29
|
+
}
|
|
30
|
+
return prefs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Embedded in the exported HTML: no runtime, CDN or network access is required.
|
|
34
|
+
export const dijiangMotionScript = `(()=>{
|
|
35
|
+
const root=document.documentElement;
|
|
36
|
+
const key='dijiang-presentation-v1';
|
|
37
|
+
const prefs=(${readDijiangPreferences.toString()})(root.dataset.edPreferences,()=>localStorage.getItem(key));
|
|
38
|
+
if(new URLSearchParams(location.search).get('motion')==='accessible')prefs.motion=false;
|
|
39
|
+
let redraw=()=>{};
|
|
40
|
+
function apply(persist=false){
|
|
41
|
+
root.dataset.edPalette=prefs.palette;root.dataset.edAccent=prefs.accent;root.dataset.dijiangMotion=prefs.motion?'full':'accessible';root.dataset.edPreferences=JSON.stringify(prefs);
|
|
42
|
+
document.querySelectorAll('[data-ed-pref]').forEach(input=>{const name=input.dataset.edPref;if(input.type==='radio')input.checked=input.value===prefs[name];else if(input.type==='checkbox')input.checked=prefs[name];else input.value=String(prefs[name]);if(name==='fps'||name==='speed')input.disabled=!prefs.background||!prefs.motion});
|
|
43
|
+
const speed=document.querySelector('[data-ed-speed-output]');if(speed)speed.value=prefs.speed+'x';
|
|
44
|
+
if(persist)try{localStorage.setItem(key,JSON.stringify(prefs))}catch{}
|
|
45
|
+
redraw();
|
|
46
|
+
}
|
|
47
|
+
apply();
|
|
48
|
+
document.querySelectorAll('[data-ed-pref]').forEach(input=>input.addEventListener('input',()=>{const name=input.dataset.edPref;prefs[name]=input.type==='checkbox'?input.checked:['fps','speed'].includes(name)?Number(input.value):input.value;apply(true)}));
|
|
49
|
+
const settings=document.querySelector('.ed-settings');
|
|
50
|
+
document.addEventListener('click',event=>{if(settings&&!settings.contains(event.target))settings.open=false});
|
|
51
|
+
document.addEventListener('keydown',event=>{if(event.key==='Escape'&&settings?.open){settings.open=false;settings.querySelector('summary').focus()}});
|
|
52
|
+
|
|
53
|
+
// Trace terrain isolines once per size or palette; animate only the raster offset.
|
|
54
|
+
const fields=[...document.querySelectorAll('canvas.ed-contours')].map(canvas=>({canvas,buffer:document.createElement('canvas'),visible:true,width:0,height:0}));
|
|
55
|
+
function prepare(field){
|
|
56
|
+
const {canvas,buffer}=field,box=canvas.getBoundingClientRect();
|
|
57
|
+
const scale=Math.min(devicePixelRatio||1,1.5);field.width=Math.max(1,Math.round(box.width*scale));field.height=Math.max(1,Math.round(box.height*scale));
|
|
58
|
+
canvas.width=field.width;canvas.height=field.height;buffer.width=field.width+96;buffer.height=field.height+96;
|
|
59
|
+
const ctx=buffer.getContext('2d');if(!ctx)return;
|
|
60
|
+
ctx.strokeStyle=getComputedStyle(canvas).getPropertyValue('--contour').trim();
|
|
61
|
+
const step=6*scale,cols=Math.ceil(buffer.width/step),rows=Math.ceil(buffer.height/step);
|
|
62
|
+
const values=Array.from({length:rows+1},(_,j)=>Array.from({length:cols+1},(_,i)=>{
|
|
63
|
+
const x=i/cols*2,y=j/rows;
|
|
64
|
+
return .58*x-.72*y+.36*Math.sin(x*3.8+y*2.6)+.12*Math.cos(x*7.3-y*3.5)+.06*Math.sin(x*10.5+y*7.4);
|
|
65
|
+
}));
|
|
66
|
+
const paths=Array.from({length:97},()=>new Path2D());
|
|
67
|
+
for(let j=0;j<rows;j++)for(let i=0;i<cols;i++){
|
|
68
|
+
const corners=[[i*step,j*step,values[j][i]],[(i+1)*step,j*step,values[j][i+1]],[(i+1)*step,(j+1)*step,values[j+1][i+1]],[i*step,(j+1)*step,values[j+1][i]]];
|
|
69
|
+
const low=Math.max(-32,Math.ceil(Math.min(...corners.map(c=>c[2]))/.026)),high=Math.min(64,Math.floor(Math.max(...corners.map(c=>c[2]))/.026));
|
|
70
|
+
for(let level=low;level<=high;level++){
|
|
71
|
+
const height=level*.026,hits=[],path=paths[level+32];
|
|
72
|
+
for(let edge=0;edge<4;edge++){
|
|
73
|
+
const a=corners[edge],b=corners[(edge+1)%4];
|
|
74
|
+
if((a[2]<height)===(b[2]<height))continue;
|
|
75
|
+
const t=(height-a[2])/(b[2]-a[2]);hits.push([a[0]+(b[0]-a[0])*t,a[1]+(b[1]-a[1])*t]);
|
|
76
|
+
}
|
|
77
|
+
for(let n=0;n+1<hits.length;n+=2){path.moveTo(...hits[n]);path.lineTo(...hits[n+1])}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
paths.forEach((path,index)=>{ctx.lineWidth=((index-32)%5===0?1:.5)*scale;ctx.stroke(path)});
|
|
81
|
+
paint(field,0);
|
|
82
|
+
}
|
|
83
|
+
function paint(field,phase){const ctx=field.canvas.getContext('2d');if(!ctx)return;ctx.clearRect(0,0,field.width,field.height);ctx.drawImage(field.buffer,-48+Math.sin(phase)*16,-48+Math.cos(phase*.7)*12)}
|
|
84
|
+
let frame=0,last=0,phase=0;
|
|
85
|
+
function tick(now){frame=0;if(document.hidden||!prefs.background||!prefs.motion)return;const interval=1000/prefs.fps;if(now-last>=interval){phase+=Math.min(100,now-(last||now))*.00004*prefs.speed;last=now;fields.forEach(field=>{if(field.visible&&field.canvas.isConnected)paint(field,phase)})}if(fields.some(field=>field.visible&&field.canvas.isConnected))frame=requestAnimationFrame(tick)}
|
|
86
|
+
function start(){if(frame)cancelAnimationFrame(frame);frame=0;last=0;if(!document.hidden&&prefs.background&&prefs.motion&&fields.some(field=>field.visible&&field.canvas.isConnected))frame=requestAnimationFrame(tick)}
|
|
87
|
+
redraw=()=>{fields.forEach(field=>{if(field.canvas.isConnected)prepare(field)});start()};
|
|
88
|
+
const observer=typeof IntersectionObserver==='function'?new IntersectionObserver(entries=>{for(const entry of entries){const field=fields.find(item=>item.canvas===entry.target);if(field)field.visible=entry.isIntersecting}start()}):null;
|
|
89
|
+
fields.forEach(field=>observer?.observe(field.canvas));
|
|
90
|
+
const resize=typeof ResizeObserver==='function'?new ResizeObserver(()=>redraw()):null;fields.forEach(field=>resize?.observe(field.canvas));if(!resize)addEventListener('resize',redraw);
|
|
91
|
+
document.addEventListener('visibilitychange',start);addEventListener('pagehide',()=>{cancelAnimationFrame(frame);observer?.disconnect();resize?.disconnect()});addEventListener('pageshow',event=>{if(event.persisted){fields.forEach(field=>{if(field.canvas.isConnected){observer?.observe(field.canvas);resize?.observe(field.canvas)}});redraw()}else start()});redraw();
|
|
92
|
+
|
|
93
|
+
const loader=document.querySelector('.loader');
|
|
94
|
+
if(!loader){root.dataset.loaderReady='true';return}
|
|
95
|
+
root.dataset.loaderReady='false';loader.dataset.dijiangMotion=root.dataset.dijiangMotion;loader.setAttribute('aria-busy','true');
|
|
96
|
+
const en=root.lang==='en',began=performance.now();let finished=false,progressFrame=0;
|
|
97
|
+
function progress(now){if(finished)return;const value=Math.min(1,(now-began)/1550);loader.querySelector('[data-loader-fill]').style.transform='scaleX('+value+')';loader.querySelector('[data-loader-percent]').textContent=String(Math.round(value*100)).padStart(2,'0')+'%';loader.querySelector('[data-loader-status]').textContent=(en?['Opening local report','Preparing retained records','Preparing the display','Report available']:['打开本地报告','整理已保存记录','准备显示界面','报告可供查看'])[Math.min(3,Math.floor(value*4))];if(value<1)progressFrame=requestAnimationFrame(progress)}
|
|
98
|
+
function remove(){if(finished)return;finished=true;cancelAnimationFrame(progressFrame);loader.setAttribute('aria-busy','false');root.dataset.loaderReady='true';fields.filter(field=>loader.contains(field.canvas)).forEach(field=>{observer?.unobserve(field.canvas);resize?.unobserve(field.canvas);field.visible=false});loader.remove();start()}
|
|
99
|
+
function onExit(event){if(event.target===loader){loader.removeEventListener('animationend',onExit);remove()}}
|
|
100
|
+
loader.addEventListener('animationend',onExit);progressFrame=requestAnimationFrame(progress);setTimeout(remove,prefs.motion?2900:180);
|
|
101
|
+
})()`;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const dijiangConsoleCss = `
|
|
2
|
+
:root{color-scheme:light;--paper:#fff;--ink:#191919;--muted:#666;--line:#d9d9d9;--surface:#f2f2f2;--inset:#e6e6e6;--signal:#fffa00;--contour:#191919;--positive:#191919;--negative:#666;--mono:'Cascadia Mono',Consolas,'PingFang SC','Microsoft YaHei',monospace;--space-xs:4px;--space-sm:8px;--space-md:16px;--space-lg:24px;--space-xl:32px;--topbar:64px;--rail:80px}
|
|
3
|
+
:root[data-ed-palette=dark]{color-scheme:dark;--paper:#191919;--ink:#fff;--muted:#b3b3b3;--line:#424242;--surface:#252525;--inset:#353535;--contour:#fff;--positive:#fff;--negative:#b3b3b3}
|
|
4
|
+
:root[data-ed-accent=cyan]{--signal:#14d0d0}
|
|
5
|
+
*{box-sizing:border-box;min-width:0;letter-spacing:0}html{font-size:16px;scroll-behavior:smooth;scroll-padding-top:88px}body{margin:0;background:var(--paper);color:var(--ink);font-family:Arial,'Helvetica Neue','PingFang SC','Microsoft YaHei',sans-serif;font-variant-numeric:tabular-nums;line-height:1.5}button,input,select{font:inherit}button,a,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer}button,a,summary,input,select{border-radius:0}button:focus-visible,a:focus-visible,summary:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--ink);outline-offset:4px}a{color:inherit}button{color:inherit}svg{display:block}.lucide{flex:0 0 auto}button{display:inline-flex;align-items:center;justify-content:center;gap:8px}button:active{transform:translateX(1px)}button:hover{background:var(--inset)}[hidden]{display:none!important}
|
|
6
|
+
.poster.dijiang-console{width:100%;height:auto;min-height:100svh;padding:var(--topbar) 0 0 var(--rail);background:var(--paper);color:var(--ink);overflow:visible}.ed-shell{width:100%;max-width:1440px;padding:0 56px;margin:auto}.ed-skip{position:fixed;top:-80px;left:16px;z-index:60;background:var(--signal);color:#191919;padding:12px 16px}.ed-skip:focus{top:8px}
|
|
7
|
+
.ed-mast{position:fixed;z-index:30;inset:0 0 auto;height:var(--topbar);display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 24px;background:var(--paper);border-bottom:1px solid var(--line)}.ed-brand{display:flex;align-items:center;gap:16px;text-decoration:none}.ed-brand>b{display:grid;place-items:center;width:40px;height:40px;background:var(--ink);color:var(--paper);font-size:20px;font-weight:900}.ed-brand>span{font-size:14px;font-weight:700}.ed-brand small{display:block;font:10px/1.6 var(--mono);color:var(--muted)}.ed-mast-meta{display:flex;align-items:center;gap:24px;font:11px var(--mono)}.ed-mast-meta>span:last-of-type{border:1px solid var(--line);padding:4px 8px}.ed-settings{position:relative}.ed-settings>summary{width:40px;height:40px;display:grid;place-items:center;cursor:pointer;list-style:none;border:1px solid var(--line)}.ed-settings summary::-webkit-details-marker{display:none}.ed-settings[open]>summary{background:var(--signal);color:#191919;border-color:#191919}.ed-preferences{position:absolute;z-index:40;right:0;top:48px;width:288px;max-width:calc(100vw - 32px);padding:20px;background:var(--paper);border:1px solid var(--ink);box-shadow:6px 6px 0 color-mix(in srgb,var(--ink) 12%,transparent)}.ed-preferences h2{font-size:16px;margin:0 0 16px}.ed-preferences fieldset{border:0;margin:0 0 16px;padding:0}.ed-preferences legend{font-size:12px;margin-bottom:8px;color:var(--muted)}.ed-options{display:flex;gap:0}.ed-options label{flex:1;position:relative}.ed-options input{position:absolute;opacity:0;width:1px;height:1px}.ed-options span{min-height:40px;display:flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--line);font-size:12px;cursor:pointer}.ed-options input:checked+span{background:var(--ink);color:var(--paper);border-color:var(--ink)}.ed-options input:focus-visible+span{outline:2px solid var(--ink);outline-offset:3px}.ed-swatch{display:block;width:12px;height:12px;border:1px solid #191919}.ed-swatch--yellow{background:#fffa00}.ed-swatch--cyan{background:#14d0d0}.ed-option-row{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px;margin-bottom:12px}.ed-option-row input[type=checkbox]{width:18px;height:18px;accent-color:var(--ink)}.ed-option-row select{background:var(--surface);color:var(--ink);border:1px solid var(--line);min-height:36px;padding:4px 8px}.ed-option-row input[type=range]{width:128px;accent-color:var(--ink)}.ed-option-row output{width:3ch;text-align:right}.ed-preferences :disabled{opacity:.45;cursor:not-allowed}
|
|
8
|
+
.ed-preferences{max-height:calc(100svh - var(--topbar) - 88px);overflow:auto;overscroll-behavior:contain}
|
|
9
|
+
.ed-stages.current-route{position:fixed;z-index:20;inset:var(--topbar) auto 0 0;width:var(--rail);display:flex;flex-direction:column;gap:0;padding:16px 0;background:var(--paper);border-right:1px solid var(--line);overflow-y:auto}.ed-stages a{min-height:72px;flex:0 0 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;text-decoration:none;border-left:4px solid transparent;border-bottom:0;transition:background .15s,color .15s}.ed-stages b{font:18px var(--mono)}.ed-stages span{font-size:12px;font-weight:700;overflow-wrap:anywhere}.ed-stages i{display:none}.ed-stages a[aria-current]{background:var(--inset);color:var(--ink);border-left-color:var(--ink)}.ed-stages a:hover{background:var(--surface);color:var(--ink)}.ed-stages:after{content:'DSH / DIJIANG';writing-mode:vertical-rl;margin:24px auto 0;font:10px var(--mono);color:var(--muted)}
|
|
10
|
+
.ed-bridge{position:relative;isolation:isolate;background:var(--surface);border-bottom:1px solid var(--line);overflow:hidden}.ed-bridge:before{content:'';position:absolute;z-index:-1;inset:0 0 0 45%;background-image:radial-gradient(var(--muted) .65px,transparent .65px);background-size:12px 12px;opacity:.14;pointer-events:none}.ed-contours{position:absolute;inset:0 0 0 auto;width:58%;height:100%;z-index:-1;pointer-events:none;opacity:.14;mask-image:linear-gradient(90deg,transparent,#000 30%)}.ed-heading{display:grid;grid-template-columns:minmax(0,1fr) 288px;gap:64px;align-items:center;padding:56px 0 64px}.ed-heading-code{display:flex;align-items:center;flex-wrap:wrap;gap:12px;margin:0 0 28px;font-size:12px;font-weight:700}.ed-heading-code>span{font-size:11px;font-weight:400;color:var(--muted)}.ed-heading-code:before{content:'';width:24px;height:12px;background:var(--signal);border-left:4px solid var(--ink);transform:skewX(-18deg)}.ed-heading h1{max-width:26ch;font-size:64px;line-height:1.04;font-weight:900;overflow-wrap:anywhere;margin:0 0 24px;text-wrap:balance}.ed-task{font-size:15px;line-height:1.8;margin:0;max-width:70ch;overflow-wrap:anywhere}.ed-station{--ink:#fff;--paper:#191919;--muted:#b3b3b3;--line:#424242;border:1px solid var(--line);border-top:3px solid var(--signal);padding:24px;background:var(--paper);color:var(--ink)}.ed-station>header{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:12px;font-weight:700}.ed-station small{font:9px var(--mono);color:var(--muted)}.ed-station-code,.ed-outcome{display:none}.ed-verdict{display:flex;gap:12px;align-items:center;margin:24px 0}.ed-verdict>span{display:grid;place-items:center;min-width:24px;height:32px;background:var(--signal);color:#191919;font-size:20px;font-weight:700}.ed-verdict strong{font-size:30px;line-height:1.2;overflow-wrap:anywhere}.ed-metrics{display:grid;grid-template-columns:1fr 1fr;gap:24px}.ed-metrics>div{display:flex;flex-direction:column;align-items:start;border-top:1px solid var(--line);padding:16px 0;gap:6px}.ed-metrics small{font:11px Arial,'Microsoft YaHei',sans-serif;color:var(--muted)}.ed-metrics b{font:24px var(--mono);order:2;overflow-wrap:anywhere}.ed-metrics>div:last-child{display:none}.ed-station footer{display:flex;justify-content:space-between;gap:12px;border-top:1px solid var(--line);padding-top:12px;font-size:10px}.ed-station footer b{font-weight:400}.dijiang-console[data-status=failed] .ed-verdict>span{background:var(--signal);color:#191919;border:1px solid var(--ink)}
|
|
11
|
+
.ed-workspace{padding:0 0 24px;display:flex;flex-direction:column}.ed-side{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr) minmax(0,.8fr);gap:24px;padding:20px 0;border-bottom:1px solid var(--line);margin-bottom:24px}.ed-review header{display:flex;align-items:center;justify-content:space-between;gap:8px}.ed-review h2{font-size:12px;margin:0}.ed-review header>span{font:11px var(--mono)}.ed-review ul{margin:8px 0 0;padding-left:16px}.ed-review p,.ed-review li{font-size:11px;line-height:1.7;color:var(--muted);overflow-wrap:anywhere}.ed-review p{margin:8px 0 0}.ed-meta{display:grid;gap:8px;margin:0;padding-left:24px;border-left:1px solid var(--line);font-size:10px}.ed-meta dt{color:var(--muted)}.ed-meta dd{margin:4px 0 0;font:11px/1.5 var(--mono);overflow-wrap:anywhere}.ed-scope{border-left:1px solid var(--line);padding-left:24px;font-size:11px;color:var(--muted)}.ed-scope p{line-height:1.7;margin:8px 0 0}.ed-main{width:100%}.ed-section{margin:0 0 32px;scroll-margin-top:24px}.ed-section-head{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;border-bottom:1px solid var(--ink);padding:0 0 12px;margin:0}.ed-section-head h2{display:flex;align-items:center;gap:10px;font-size:16px;line-height:1.3;margin:0;font-weight:700}.ed-section-head h2:before{content:'';width:4px;height:14px;flex:0 0 4px;background:var(--ink)}.ed-section-head>span{font:11px var(--mono);color:var(--muted);text-align:right}.ed-overview dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));margin:0;border-bottom:1px solid var(--line)}.ed-overview dl>div{padding:16px 24px;border-right:1px solid var(--line)}.ed-overview dl>div:first-child{padding-left:0}.ed-overview dl>div:last-child{border-right:0}.ed-overview dt{font-size:11px;color:var(--muted)}.ed-overview dd{font:22px/1.4 var(--mono);margin:8px 0 0;overflow-wrap:anywhere}.ed-overview .ed-delta{font-size:22px}.ed-delta{display:flex;flex-wrap:wrap;gap:8px;font:12px var(--mono)}.ed-delta b{font-weight:600;color:var(--positive)}.ed-delta i{font-style:normal;color:var(--negative)}
|
|
12
|
+
.ed-ledger.delivery-manifest{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:32px;position:static;background:none;border:0;padding:0;margin:0;overflow:visible}.ed-files,.ed-receipts{list-style:none;padding:0;margin:0}.ed-files li{border-bottom:1px solid var(--line)}.ed-files button{display:grid;grid-template-columns:24px minmax(0,1fr) auto;align-items:baseline;gap:8px;width:100%;padding:12px 8px;text-align:left;border:0;background:none}.ed-files button[aria-pressed=true]{background:var(--inset);box-shadow:inset 3px 0 var(--signal)}.ed-files button:hover{background:var(--surface)}.ed-index{font:10px var(--mono);color:var(--muted)}.ed-files code,.ed-receipts code{font:12px/1.6 var(--mono);overflow-wrap:anywhere}.ed-diff-panel{margin-top:16px}.ed-diff-panel header{display:flex;align-items:baseline;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:12px;background:var(--ink);color:var(--paper)}.ed-diff-panel h3{font-size:12px;margin:0}.ed-diff-panel header code{font:10px var(--mono);overflow-wrap:anywhere}.ed-diff-code{margin:0;padding:12px;background:var(--surface);border:1px solid var(--line);border-top:0;color:var(--ink);max-height:256px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.7 var(--mono)}.ed-diff-line{font:inherit}.ed-diff-line.addition{color:var(--positive)}.ed-diff-line.deletion{color:var(--negative)}.ed-diff-line.hunk{color:var(--ink);background:color-mix(in srgb,var(--signal) 32%,var(--paper))}.ed-diff-missing{margin:0;padding:16px;background:var(--inset);color:var(--muted);font-size:12px}.ed-more{font-size:11px;color:var(--muted);margin:8px 0;overflow-wrap:anywhere}.ed-receipts>li{border-bottom:1px solid var(--line)}.ed-receipts summary{display:grid;grid-template-columns:16px minmax(0,1fr) auto 12px;gap:8px;align-items:center;padding:12px 8px;cursor:pointer;list-style:none}.ed-receipts summary::-webkit-details-marker{display:none}.ed-receipts summary:after{content:'+';font:16px var(--mono)}.ed-receipts details[open]>summary:after{content:'−'}.ed-receipts summary:hover,.ed-receipts details[open]>summary{background:var(--inset)}.ed-receipt-mark{font-size:16px}.ed-receipt-result{font-size:11px;text-align:right}.ed-receipt-result small{display:block;font:9px/1.7 var(--mono);color:var(--muted)}.ed-receipts [data-status=failed] .ed-receipt-result,.ed-receipts [data-status=failed] .ed-receipt-mark{color:var(--negative)}.ed-receipts pre{background:var(--surface);color:var(--ink);margin:0;padding:16px;max-height:240px;overflow:auto;font:12px/1.8 var(--mono);white-space:pre-wrap;overflow-wrap:anywhere}.ed-empty{padding:24px 0}.ed-empty strong{font-size:13px}.ed-empty p{font-size:12px;line-height:1.7;color:var(--muted);max-width:65ch;margin:8px 0 0}
|
|
13
|
+
.ed-visual.evidence-page{position:relative;background:none;min-height:0;overflow:visible;padding:0}.ed-captures{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px}.ed-captures details{align-self:start;border-bottom:1px solid var(--line)}.ed-captures summary{display:flex;align-items:baseline;gap:12px;padding:12px 0;font-size:12px;cursor:pointer;list-style:none;overflow-wrap:anywhere}.ed-captures summary::-webkit-details-marker{display:none}.ed-captures summary>span{font:10px var(--mono);color:var(--muted)}.ed-captures summary:after{content:'+';margin-left:auto}.ed-captures details[open]>summary:after{content:'−'}.ed-captures figure{margin:0}.ed-captures .ed-shot{display:block;height:240px;background:var(--inset);overflow:auto;border:1px solid var(--line)}.ed-captures img{display:block;width:100%;height:100%;object-fit:contain}.ed-captures figcaption{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin:8px 0;font-size:10px;color:var(--muted)}.ed-captures button,.ed-outputs button{min-height:36px;padding:8px 12px;border:1px solid var(--line);background:none;font-size:11px}.ed-captures button:hover,.ed-outputs button:hover{background:var(--inset);border-color:var(--ink)}.ed-visual .evidence-page__guard{display:block;min-height:0;padding:24px 0;border:0;background:none;color:var(--ink)}.ed-visual .evidence-page__guard:before,.ed-visual .evidence-page__guard:after,.ed-visual .evidence-page__guard-meta,.ed-visual .evidence-page__guard-copy>small,.ed-visual .evidence-page__guard-empty,.ed-visual .evidence-page__guard-action{display:none}.ed-visual .evidence-page__guard-copy b{font-size:13px}.ed-visual .evidence-page__guard-copy p{font-size:12px;color:var(--muted);line-height:1.7;margin:8px 0}.ed-visual .evidence-page__guard-list{list-style:none;padding:0;margin:12px 0}.ed-visual .evidence-page__guard-list li{display:flex;flex-wrap:wrap;gap:8px;border-top:1px solid var(--line);padding:8px 0;font-size:11px}.ed-visual .evidence-page__guard-list small{margin-left:auto}
|
|
14
|
+
.ed-comparison{margin-top:24px}.ed-comparison h3{font-size:13px;margin:0 0 12px}.ed-comparison-stage{position:relative;width:100%;aspect-ratio:16/7;max-height:420px;overflow:hidden;background:var(--inset);border:1px solid var(--line)}.ed-compare-before,.ed-compare-after{position:absolute;inset:0}.ed-compare-after{clip-path:inset(0 0 0 50%)}.ed-comparison-stage img{display:block;width:100%;height:100%;object-fit:contain}.ed-compare-label{position:absolute;z-index:2;top:12px;left:12px;background:var(--ink);color:var(--paper);padding:4px 8px;font-size:10px}.ed-compare-label--after{left:auto;right:12px;background:var(--signal);color:#191919}.ed-compare-handle{position:absolute;top:0;bottom:0;left:50%;width:2px;background:var(--signal);box-shadow:0 0 0 1px #191919;pointer-events:none}.ed-compare-control{display:flex;align-items:center;gap:12px;font-size:11px;margin-top:12px}.ed-compare-control input{flex:1;accent-color:var(--ink)}.ed-compare-control output{min-width:3ch;text-align:right}
|
|
15
|
+
.ed-privacy dl{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 24px;margin:0}.ed-privacy dl>div{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid var(--line);font-size:12px}.ed-privacy dt{flex:1;overflow-wrap:anywhere}.ed-privacy dd{flex:0 0 auto;font:16px var(--mono);margin:0}.ed-export-actions{display:flex;flex-wrap:wrap;gap:8px}.ed-outputs dl{margin:0}.ed-outputs dl>div{display:grid;grid-template-columns:112px minmax(0,1fr);gap:16px;border-bottom:1px solid var(--line);padding:12px 0;font-size:12px}.ed-outputs dd{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0}.ed-outputs code{font:12px/1.5 var(--mono);overflow-wrap:anywhere;user-select:all}.ed-outputs dd button{border:0;flex:0 0 40px}.ed-notice{min-height:20px;font-size:12px;margin:8px 0 0}.ed-footer{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;border-top:1px solid var(--ink);padding:16px 0 24px;font:10px/1.6 var(--mono);color:var(--muted)}
|
|
16
|
+
html:not([data-loader-ready=true]) .dijiang-console{visibility:hidden}html[data-loader-ready=true] .dijiang-console{visibility:visible}html[data-loader-ready=true] .ed-heading,html[data-loader-ready=true] .ed-main{animation:ed-lateral-reveal .5s cubic-bezier(.16,1,.3,1) both}@keyframes ed-lateral-reveal{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0)}}
|
|
17
|
+
.loader.loader--field{--paper:#141414;--ink:#fff;--muted:#b3b3b3;--line:#424242;--inset:#353535;--contour:#fff;position:fixed;z-index:50;inset:0;background:transparent;color:var(--ink);overflow:hidden;animation:dijiang-reference-exit 2.4s step-end both}.field-loader-sheet{position:absolute;inset:0;display:grid;grid-template-rows:auto minmax(0,1fr) auto;padding:24px 48px;background:var(--paper);overflow:hidden;animation:ed-loader-sheet-exit 2.4s step-end both}.field-loader-sheet:before{content:'';position:absolute;inset:0;background-image:radial-gradient(var(--muted) .65px,transparent .65px);background-size:16px 16px;opacity:.15;pointer-events:none}.field-loader-sheet>.ed-contours{width:75%;opacity:.12;z-index:0}.field-loader-mast,.field-loader-center,.field-loader-statusbar{position:relative;z-index:1}.field-loader-mast{display:flex;align-items:center;justify-content:space-between;gap:24px;border-bottom:1px solid var(--line);padding-bottom:16px;font:11px var(--mono)}.field-loader-id{font-weight:700}.field-loader-project{max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.field-loader-center{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,400px);gap:48px;align-items:center;width:min(100%,1200px);margin:auto}.field-loader-copy{min-width:0}.field-loader-kicker{display:block;color:var(--signal);font:12px var(--mono);margin-bottom:24px}.field-loader-copy strong{display:block;font-size:96px;font-weight:900;line-height:.98;overflow-wrap:anywhere;max-width:10ch;animation:ed-lateral-reveal .65s cubic-bezier(.16,1,.3,1) both}.field-loader-copy p{color:var(--muted);font-size:13px;margin:24px 0 0;max-width:60ch;overflow-wrap:anywhere}.field-loader-instrument{width:100%;height:auto;max-height:400px;overflow:visible;fill:none;stroke:var(--ink);stroke-width:1}.field-loader-instrument .instrument-halo,.field-loader-instrument .instrument-ring,.field-loader-instrument .instrument-ticks,.field-loader-instrument .instrument-sweep,.field-loader-instrument .instrument-cross{transform-box:view-box;transform-origin:160px 160px}.instrument-halo{stroke-dasharray:1 8;animation:dijiang-instrument-spin 36s linear infinite}.instrument-ring--outer{stroke-dasharray:220 50 40 40;stroke-width:2;animation:dijiang-instrument-spin 12s linear infinite}.instrument-ring--inner{stroke-dasharray:120 35;animation:dijiang-instrument-spin-reverse 8s linear infinite}.instrument-ring--core{stroke-width:8;stroke-dasharray:50 35;stroke:var(--signal);animation:dijiang-instrument-core-spin 3s linear infinite}.instrument-ticks{stroke-width:1}.instrument-tick--major{stroke-width:3}.instrument-cross{stroke:var(--line);stroke-dasharray:2 5}.instrument-sweep{stroke:var(--signal);stroke-width:2;animation:dijiang-loader-sweep-rotate 3s linear infinite}.instrument-node{fill:var(--signal);stroke:var(--ink)}.instrument-label{fill:var(--ink);stroke:none;text-anchor:middle;font:8px Arial,sans-serif}.field-loader-statusbar{border-top:1px solid var(--line);padding-top:16px}.field-loader-readout{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:16px}.field-loader-status{font-size:12px}.field-loader-readout>b{font:48px var(--mono);color:var(--signal)}.field-loader-progress{position:relative;display:block;width:100%;height:20px;background:var(--inset);border:1px solid var(--line);overflow:hidden}.field-loader-progress em{position:absolute;inset:0;background:var(--signal);transform:scaleX(0);transform-origin:left}.field-loader-progress:after{content:'';position:absolute;inset:0;background:repeating-linear-gradient(90deg,transparent 0 39px,#141414 39px 40px);opacity:.2}.field-loader-stages{display:flex;justify-content:space-between;gap:16px;margin-top:12px;font:10px var(--mono);color:var(--muted)}.field-loader-stages b{font-weight:400}.field-loader-topband{position:absolute;z-index:3;top:-8%;left:-25%;width:150%;height:116%;bottom:auto;border:0;background:var(--signal);transform:translateX(-120%) skewX(-14deg);animation:dijiang-yellow-transfer 2.4s cubic-bezier(.65,0,.25,1) both;pointer-events:none}@keyframes dijiang-yellow-transfer{0%,64%{transform:translateX(-120%) skewX(-14deg)}79%{transform:translateX(0) skewX(-14deg)}100%{transform:translateX(120%) skewX(-14deg)}}@keyframes ed-loader-sheet-exit{0%,79%{visibility:visible}79.1%,100%{visibility:hidden}}@keyframes dijiang-reference-exit{0%,99.9%{visibility:visible}100%{visibility:hidden;pointer-events:none}}@keyframes dijiang-instrument-spin{to{transform:rotate(360deg)}}@keyframes dijiang-instrument-spin-reverse{to{transform:rotate(-360deg)}}@keyframes dijiang-instrument-core-spin{to{transform:rotate(360deg)}}@keyframes dijiang-loader-sweep-rotate{to{transform:rotate(360deg)}}
|
|
18
|
+
html[data-dijiang-motion=accessible]{scroll-behavior:auto}html[data-dijiang-motion=accessible] .loader.loader--field{animation:dijiang-reference-reduced-exit .15s linear both}html[data-dijiang-motion=accessible] .loader.loader--field *{animation:none!important}html[data-dijiang-motion=accessible] .field-loader-topband{display:none}html[data-dijiang-motion=accessible] .dijiang-console *{animation:none!important;transition:none!important}@keyframes dijiang-reference-reduced-exit{to{opacity:0;visibility:hidden}}
|
|
19
|
+
@media(min-width:761px){.ed-captures:has(>details:only-child){grid-template-columns:1fr;max-width:960px}.ed-captures:has(>details:only-child) .ed-shot{height:auto;aspect-ratio:16/10;max-height:480px}.ed-captures:has(>details:nth-child(2):last-child){grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
20
|
+
@media(min-width:1700px){.ed-heading h1{font-size:72px}.ed-heading{padding:40px 0}.ed-ledger.delivery-manifest{gap:48px}}
|
|
21
|
+
@media(max-width:1100px){.ed-heading h1{font-size:48px}.ed-heading{grid-template-columns:minmax(0,1fr) 240px;gap:24px}.ed-shell{padding:0 24px}.ed-ledger.delivery-manifest{gap:24px}.ed-receipts summary{grid-template-columns:16px minmax(0,1fr) 12px}.ed-receipt-result{grid-column:2;text-align:left;display:flex;align-items:baseline;gap:8px}.ed-receipts summary:after{grid-column:3;grid-row:1}.ed-side{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr)}.ed-scope{display:none}.field-loader-copy strong{font-size:72px}.field-loader-center{gap:24px}.ed-files .ed-delta{font-size:11px}}
|
|
22
|
+
@media(max-width:760px){:root{--topbar:56px;--rail:0px}html{scroll-padding-top:76px}.poster.dijiang-console{padding-bottom:72px}.ed-mast{padding:0 16px}.ed-brand{gap:8px}.ed-brand>b{width:32px;height:32px;font-size:16px}.ed-brand>span{font-size:12px}.ed-brand small{font-size:8px}.ed-mast-meta{gap:8px}.ed-mast-meta>span:first-child{display:none}.ed-mast-meta>span:last-of-type{font-size:9px}.ed-stages.current-route{inset:auto 0 0;width:100%;height:64px;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));padding:0;border-right:0;border-top:1px solid var(--ink);background:var(--paper);overflow:visible}.ed-stages a{min-height:0;border-left:0;border-top:3px solid transparent;border-bottom:0;border-right:1px solid var(--line);gap:2px}.ed-stages a[aria-current]{border-top-color:var(--ink)}.ed-stages a:last-child{border-right:0}.ed-stages b{font-size:12px}.ed-stages span{font-size:11px}.ed-stages:after{display:none}.ed-shell{padding:0 16px}.ed-heading{grid-template-columns:1fr;gap:24px;padding:24px 0}.ed-heading h1{font-size:36px;margin-bottom:16px}.ed-heading-code{margin-bottom:16px;gap:8px;font-size:11px}.ed-heading-code>span{font-size:10px}.ed-task{font-size:13px}.ed-contours{width:100%;opacity:.1}.ed-station{padding:16px;border-top:3px solid var(--signal)}.ed-station>header{font-size:11px}.ed-verdict{margin:12px 0}.ed-verdict strong{font-size:26px}.ed-metrics>div{flex-direction:row;align-items:baseline;justify-content:space-between;padding:8px 0;gap:12px}.ed-metrics b{font-size:20px}.ed-station footer{padding-top:8px}.ed-side{gap:16px;padding:16px 0;margin-bottom:24px;grid-template-columns:1fr}.ed-meta{grid-template-columns:1fr 1fr;padding:12px 0 0;border-left:0;border-top:1px solid var(--line)}.ed-review ul{margin-top:4px}.ed-overview dd,.ed-overview .ed-delta{font-size:16px}.ed-overview dl>div{padding:12px 8px 12px 0}.ed-section{margin-bottom:24px}.ed-section-head h2{font-size:15px}.ed-ledger.delivery-manifest{display:block}.ed-captures{grid-template-columns:1fr;gap:16px}.ed-captures .ed-shot{height:256px}.ed-comparison-stage{aspect-ratio:1.2}.ed-captures button,.ed-outputs button{min-height:44px}.ed-privacy dl{grid-template-columns:1fr}.ed-outputs dl>div{grid-template-columns:1fr;gap:4px}.ed-outputs code{font-size:11px}.ed-section-head>span{font-size:10px}.ed-receipts summary{grid-template-columns:16px minmax(0,1fr) 12px}.ed-preferences{right:0}.field-loader-sheet{padding:16px}.field-loader-mast{font-size:9px;gap:12px;padding-bottom:12px}.field-loader-center{grid-template-columns:1fr;gap:16px;padding:24px 0}.field-loader-copy strong{font-size:56px}.field-loader-kicker{font-size:10px;margin-bottom:16px}.field-loader-copy p{font-size:11px;margin-top:16px}.field-loader-instrument{max-height:240px}.field-loader-readout>b{font-size:28px}.field-loader-stages{font-size:9px;gap:8px}.field-loader-status{font-size:11px}.field-loader-project{max-width:45%}}
|
|
23
|
+
@media(max-width:760px){.ed-option-row{min-height:44px}.ed-options span{min-height:44px}.ed-settings>summary{width:44px;height:44px}}
|
|
24
|
+
@media(max-width:760px) and (max-height:700px){.field-loader-center{grid-template-columns:minmax(0,1fr) 112px;gap:16px}.field-loader-copy strong{font-size:40px}.field-loader-instrument{max-height:112px}.field-loader-copy p{max-height:48px;overflow:hidden}}
|
|
25
|
+
@media(min-width:761px) and (max-height:750px){.ed-heading{padding:20px 0}.ed-heading h1{margin-bottom:12px}.ed-station{padding:16px}.ed-verdict{margin:12px 0}.ed-verdict strong{font-size:26px}.ed-metrics>div{padding:8px 0}.ed-metrics b{font-size:20px}.ed-side{padding:12px 0;margin-bottom:20px}.ed-overview dl>div{padding-top:12px;padding-bottom:12px}.ed-overview{margin-bottom:24px}}
|
|
26
|
+
@media(min-width:761px) and (max-height:650px){.field-loader-sheet{padding:16px 32px}.field-loader-instrument{max-height:260px}.field-loader-copy strong{font-size:64px}.field-loader-center{grid-template-columns:minmax(0,1fr) 260px}.ed-heading{padding:20px 0}}
|
|
27
|
+
@media(prefers-reduced-motion:reduce){html:not([data-dijiang-motion=full]){scroll-behavior:auto}html:not([data-dijiang-motion=full]) *{animation:none!important;transition:none!important}}
|
|
28
|
+
/* Industrial editorial override: keep status semantics monochrome and give the report a quieter reading rhythm. */
|
|
29
|
+
.ed-receipt-mark{display:grid;place-items:center;width:16px;height:16px;color:#191919;background:var(--signal);font-size:11px;font-weight:900}.ed-receipts [data-status=failed] .ed-receipt-result{color:var(--ink);text-decoration:underline;text-decoration-color:var(--signal);text-decoration-thickness:2px;text-underline-offset:3px}.ed-receipts [data-status=failed] .ed-receipt-mark{color:var(--ink);background:var(--surface);border:1px solid var(--ink)}
|
|
30
|
+
@media(min-width:1101px){.ed-shell{max-width:1440px;padding-inline:56px}.ed-heading{gap:64px;padding:56px 0 64px}.ed-heading-code{margin-bottom:28px}.ed-heading h1{margin-bottom:24px}.ed-task{line-height:1.8}.ed-station{padding:24px}.ed-verdict{margin:24px 0}.ed-metrics{gap:24px}.ed-metrics>div{padding:16px 0;gap:6px}.ed-workspace{padding-bottom:48px}.ed-side{gap:40px;padding:32px 0;margin-bottom:48px}.ed-review ul{margin-top:12px}.ed-review p,.ed-review li{line-height:1.8}.ed-review p{margin-top:12px}.ed-meta{gap:12px;padding-left:32px}.ed-meta dd{line-height:1.6}.ed-scope{padding-left:32px}.ed-scope p{line-height:1.8;margin-top:12px}.ed-section{margin-bottom:48px}.ed-section-head{padding-bottom:16px}.ed-overview dl>div{padding:24px 32px}.ed-overview dd{margin-top:12px}}
|
|
31
|
+
@media(max-width:760px){.poster.dijiang-console{padding-bottom:0}.ed-stages.current-route{position:sticky;top:var(--topbar);bottom:auto;z-index:25}}
|
|
32
|
+
/* Bold industrial hero: a single structural peak carries the extra energy. */
|
|
33
|
+
.ed-bridge>.ed-shell{position:relative;z-index:1}.ed-bridge:after{content:'';position:absolute;z-index:0;top:0;right:0;width:34%;height:100%;background:var(--ink);opacity:.055;clip-path:polygon(18% 0,100% 0,100% 100%,0 100%,12% 54%);pointer-events:none}.ed-heading{position:relative}.ed-heading:after{content:'';position:absolute;z-index:0;top:0;right:31%;width:12px;height:100%;background:var(--signal);clip-path:polygon(28% 0,100% 0,72% 100%,0 100%);opacity:.92;pointer-events:none}.ed-heading>div,.ed-heading>.ed-station{position:relative;z-index:1}.ed-heading h1{font-size:clamp(4rem,5.7vw,6rem);letter-spacing:-.025em}.ed-contours{width:68%;opacity:.2}.ed-station{width:100%;box-shadow:inset 0 0 0 1px rgba(255,255,255,.08)}.ed-overview{position:relative;padding-top:16px;border-top:4px solid var(--signal)}
|
|
34
|
+
@media(max-width:1100px){.ed-heading:after{right:29%;width:8px}.ed-heading h1{font-size:48px}.ed-contours{width:64%;opacity:.17}}
|
|
35
|
+
@media(max-width:760px){.ed-bridge:after{width:30%;opacity:.045}.ed-heading:after{right:10%;top:0;width:7px;height:100%;opacity:.72}.ed-heading h1{font-size:40px;letter-spacing:-.015em}.ed-contours{width:100%;opacity:.14}.ed-overview{padding-top:10px;border-top-width:3px}}
|
|
36
|
+
@media print{.loader,.ed-mast,.ed-stages,.ed-settings,.ed-skip,.ed-export-actions,.ed-outputs button,.ed-captures button,.ed-contours{display:none!important}body{background:#fff;color:#191919}.poster.dijiang-console{padding:0;min-height:0}.ed-shell{padding:0}.ed-ledger.delivery-manifest{display:block}.ed-diff-panel[hidden]{display:block!important}.ed-diff-code,.ed-receipts pre{max-height:none}.ed-side{display:block}.ed-meta{border:0;padding:0}.ed-comparison{display:none}.ed-captures{grid-template-columns:1fr}*{animation:none!important;transition:none!important}}
|
|
37
|
+
`;
|
package/plugin/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
2
2
|
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
3
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
4
|
import { generateLayoutSummary } from "./layout-summary.js";
|
|
5
|
+
import { isAbsolute, resolve } from "node:path";
|
|
5
6
|
|
|
6
7
|
export const name = "showcase-layout-summary";
|
|
7
8
|
export const inject = ["tools", "settings", "connection"];
|
|
@@ -118,13 +119,18 @@ export function apply(ctx, config = {}) {
|
|
|
118
119
|
return [{ type: "text", text: `${labels.heading}${punctuation}\n${labels.markdown}: ${value.outputPath}\n${labels.poster}: ${poster}\n${labels.freshness}: ${value.freshnessWarnings.length}${punctuation}\n${labels.sections}: ${value.sections.join(", ")}\n${labels.theme}: ${value.theme}\n${labels.stages}: ${value.stages.join(", ")}\n${labels.breakpoints}: ${value.breakpoints.join(", ")}\n${labels.tests}: ${value.testCount}; ${labels.redactions}: ${value.redactionCount}${punctuation}` }];
|
|
119
120
|
},
|
|
120
121
|
},
|
|
121
|
-
async execute({ projectPath, reportPath, outputPath, posterPath, appPath, cssPath, locale, generatePoster: requestedGeneratePoster }) {
|
|
122
|
+
async execute({ projectPath, reportPath, outputPath, posterPath, appPath, cssPath, locale, generatePoster: requestedGeneratePoster }, exec) {
|
|
122
123
|
if (requestedGeneratePoster !== undefined && typeof requestedGeneratePoster !== "boolean") {
|
|
123
124
|
throw new Error("generatePoster must be a boolean.");
|
|
124
125
|
}
|
|
125
126
|
const current = readSettings();
|
|
127
|
+
const sessionRoot = exec?.agent?.session?.meta?.cwd;
|
|
128
|
+
if (!isAbsolute(projectPath)) {
|
|
129
|
+
if (!sessionRoot) throw new Error("Relative projectPath requires a session workspace. Provide an absolute projectPath.");
|
|
130
|
+
projectPath = resolve(sessionRoot, projectPath);
|
|
131
|
+
}
|
|
126
132
|
const shouldGeneratePoster = typeof requestedGeneratePoster === "boolean" ? requestedGeneratePoster : current.generatePoster;
|
|
127
|
-
return generateLayoutSummary({ projectPath, reportPath, outputPath, posterPath, appPath, cssPath, locale, theme: current.theme, generatePoster: shouldGeneratePoster });
|
|
133
|
+
return generateLayoutSummary({ projectPath, reportPath, outputPath, posterPath, appPath, cssPath, locale, theme: current.theme, generatePoster: shouldGeneratePoster, signal: exec?.signal });
|
|
128
134
|
},
|
|
129
135
|
}));
|
|
130
136
|
}
|
|
@@ -15,6 +15,7 @@ export type LayoutSummaryResult = {
|
|
|
15
15
|
|
|
16
16
|
export function generateLayoutSummary(options: {
|
|
17
17
|
projectPath: string;
|
|
18
|
+
signal?: AbortSignal;
|
|
18
19
|
reportPath?: string;
|
|
19
20
|
/** Optional Markdown output under .showcase; must end in .md or .markdown. */
|
|
20
21
|
outputPath?: string;
|
package/plugin/layout-summary.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { mkdir, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { createStyledPosterHtml } from "./poster-html.js";
|
|
4
|
+
import { findComparisonPair, sanitizeReportExport } from "./report-content.js";
|
|
4
5
|
import { formatReportIssues, reportSchema } from "./report-schema.js";
|
|
6
|
+
import { withProjectLock, readBoundedFile, parseJson } from "./project-io.js";
|
|
7
|
+
import { redact } from "./redaction.js";
|
|
5
8
|
|
|
6
9
|
const DEFAULT_REPORT_PATH = ".showcase/report.json";
|
|
7
10
|
const DEFAULT_OUTPUT_PATH = ".showcase/layout-summary.md";
|
|
@@ -380,7 +383,7 @@ function truncateText(value, maxLength = MAX_TEXT_LENGTH) {
|
|
|
380
383
|
}
|
|
381
384
|
|
|
382
385
|
function safeText(value, maxLength = MAX_TEXT_LENGTH) {
|
|
383
|
-
const redacted = String(value ?? "")
|
|
386
|
+
const redacted = redact(String(value ?? "")).text
|
|
384
387
|
.replace(/\b(?:authorization|api\s*[_-]?key|access\s*[_-]?token|token)\s*[:=]\s*(?:bearer\s+)?[^\s,;]+/gi, "[REDACTED]")
|
|
385
388
|
.replace(/\b(?:sk-(?:proj-|ant-|or-v1-)?[A-Za-z0-9_-]{16,}|AIza[0-9A-Za-z_-]{20,}|xai-[A-Za-z0-9_-]{16,}|pplx-[A-Za-z0-9_-]{16,}|hf_[A-Za-z0-9_-]{16,})\b/g, "[REDACTED]")
|
|
386
389
|
.replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[REDACTED]")
|
|
@@ -391,6 +394,10 @@ function safeText(value, maxLength = MAX_TEXT_LENGTH) {
|
|
|
391
394
|
return truncateText(redacted, maxLength);
|
|
392
395
|
}
|
|
393
396
|
|
|
397
|
+
function safeBlock(value, maxLength = 16_000) {
|
|
398
|
+
return truncateText(redact(String(value ?? "")).text, maxLength);
|
|
399
|
+
}
|
|
400
|
+
|
|
394
401
|
function safeRelativePath(value, maxLength = 240) {
|
|
395
402
|
const path = String(value ?? "").replace(/\\/g, "/").replace(/[\x60\r\n]+/g, "").trim();
|
|
396
403
|
if (!path || path.startsWith("/") || /^[a-z]:/i.test(path) || path === ".." || path.startsWith("../") || path.includes("/../")) return "[path omitted]";
|
|
@@ -547,12 +554,17 @@ function imageInfo(image) {
|
|
|
547
554
|
|
|
548
555
|
// Keep one bounded, verified set for both Markdown and poster output. The
|
|
549
556
|
// Markdown-only path validates bytes without retaining a base64 copy.
|
|
550
|
-
async function collectVerifiedScreenshots(projectPath, screenshots, locale, { embed = false, strictPaths = embed } = {}) {
|
|
557
|
+
async function collectVerifiedScreenshots(projectPath, screenshots, locale, { embed = false, strictPaths = embed, generatedAt = Infinity, sourceTime = 0, fingerprint, signal } = {}) {
|
|
551
558
|
const mimeTypes = new Map([[".png", "image/png"], [".webp", "image/webp"], [".jpg", "image/jpeg"], [".jpeg", "image/jpeg"]]);
|
|
552
559
|
const posterScreenshots = [];
|
|
553
560
|
const verifiedEvidenceViewports = [];
|
|
554
561
|
const invalidEvidenceViewports = [];
|
|
555
|
-
|
|
562
|
+
let currentCaptureCount = 0;
|
|
563
|
+
const pair = findComparisonPair(screenshots);
|
|
564
|
+
const ordered = [...screenshots].sort((a, b) => (a.kind === "before" ? 1 : 0) - (b.kind === "before" ? 1 : 0));
|
|
565
|
+
if (pair) ordered.splice(0, ordered.length, pair.after, pair.before, ...ordered.filter((shot) => shot !== pair.after && shot !== pair.before));
|
|
566
|
+
for (const shot of ordered) {
|
|
567
|
+
signal?.throwIfAborted();
|
|
556
568
|
if (verifiedEvidenceViewports.length >= 3) break;
|
|
557
569
|
const label = screenshotLabel(shot, locale);
|
|
558
570
|
if (!shot?.imagePath || typeof shot.imagePath !== "string") {
|
|
@@ -566,7 +578,16 @@ async function collectVerifiedScreenshots(projectPath, screenshots, locale, { em
|
|
|
566
578
|
invalidEvidenceViewports.push(label);
|
|
567
579
|
continue;
|
|
568
580
|
}
|
|
569
|
-
const
|
|
581
|
+
const details = await stat(fullPath);
|
|
582
|
+
const capturedAt = Date.parse(shot.capturedAt);
|
|
583
|
+
if (!Number.isFinite(capturedAt) || capturedAt <= 0 || capturedAt > generatedAt + FRESHNESS_CLOCK_SKEW_MS
|
|
584
|
+
|| details.mtimeMs > capturedAt + FRESHNESS_CLOCK_SKEW_MS
|
|
585
|
+
|| (shot.kind !== "before" && (sourceTime > capturedAt + FRESHNESS_CLOCK_SKEW_MS
|
|
586
|
+
|| (shot.sourceFingerprint && fingerprint && shot.sourceFingerprint !== fingerprint)))) {
|
|
587
|
+
invalidEvidenceViewports.push(`${label} (${locale === "zh-CN" ? "过期或时间未验证" : "stale or unverified timestamp"})`);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const image = await readBoundedFile(fullPath, MAX_IMAGE_BYTES, signal);
|
|
570
591
|
const info = imageInfo(image);
|
|
571
592
|
if (!info) {
|
|
572
593
|
invalidEvidenceViewports.push(label);
|
|
@@ -575,24 +596,48 @@ async function collectVerifiedScreenshots(projectPath, screenshots, locale, { em
|
|
|
575
596
|
const name = viewportLabel(safeText(shot.viewport?.name), locale);
|
|
576
597
|
const width = Number(shot.viewport?.width) || 0;
|
|
577
598
|
const height = Number(shot.viewport?.height) || 0;
|
|
578
|
-
const
|
|
599
|
+
const scale = shot.deviceScaleFactor ?? info.width / width;
|
|
600
|
+
const region = shot.captureMode === "region";
|
|
601
|
+
if (!region && (!(scale >= (shot.deviceScaleFactor ? 0 : 1) && scale <= 8)
|
|
602
|
+
|| Math.abs(info.width - width * scale) > 1
|
|
603
|
+
|| info.height < height * scale - 1
|
|
604
|
+
|| (shot.captureMode === "viewport" && Math.abs(info.height - height * scale) > 1))) {
|
|
605
|
+
invalidEvidenceViewports.push(`${label} (${locale === "zh-CN" ? "图片尺寸不匹配" : "pixel dimensions mismatch"})`);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const kind = shot.kind === "before" ? (locale === "zh-CN" ? "改版前" : "Before") : (locale === "zh-CN" ? "改版后" : "After");
|
|
609
|
+
const verifiedLabel = `${kind} / ${name}: ${width} x ${height}${region ? ` / region ${info.width} x ${info.height}` : ""}`;
|
|
579
610
|
verifiedEvidenceViewports.push(verifiedLabel);
|
|
611
|
+
if (shot.kind !== "before" && !region) currentCaptureCount += 1;
|
|
580
612
|
if (embed) {
|
|
581
613
|
posterScreenshots.push({
|
|
582
614
|
image: image.toString("base64"),
|
|
583
615
|
mimeType: info.mimeType,
|
|
584
616
|
label: verifiedLabel,
|
|
617
|
+
kind: shot.kind,
|
|
618
|
+
viewport: { name: shot.viewport.name, width, height },
|
|
619
|
+
captureMode: shot.captureMode,
|
|
620
|
+
imageWidth: info.width,
|
|
621
|
+
imageHeight: info.height,
|
|
622
|
+
url: safeText(shot.url),
|
|
623
|
+
comparisonId: shot.comparisonId ? safeText(shot.comparisonId) : undefined,
|
|
585
624
|
});
|
|
586
625
|
}
|
|
587
626
|
} catch (error) {
|
|
627
|
+
signal?.throwIfAborted();
|
|
628
|
+
const outsideProject = error instanceof Error && error.message.includes("must resolve inside projectPath.");
|
|
588
629
|
// Embedded poster bytes must never cross the project boundary. A
|
|
589
630
|
// Markdown-only summary does not read or expose the outside file, so an
|
|
590
|
-
// obsolete or cross-theme symlink is
|
|
591
|
-
|
|
631
|
+
// obsolete or cross-theme symlink is skipped without echoing its
|
|
632
|
+
// viewport label into the generated document.
|
|
633
|
+
if (outsideProject) {
|
|
634
|
+
if (strictPaths) throw error;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
592
637
|
invalidEvidenceViewports.push(label);
|
|
593
638
|
}
|
|
594
639
|
}
|
|
595
|
-
return { posterScreenshots, verifiedEvidenceViewports, invalidEvidenceViewports };
|
|
640
|
+
return { posterScreenshots, verifiedEvidenceViewports, invalidEvidenceViewports, currentCaptureCount };
|
|
596
641
|
}
|
|
597
642
|
|
|
598
643
|
function formatList(items, emptyLabel) {
|
|
@@ -662,6 +707,19 @@ async function collectFreshnessWarnings(projectPath, report, sourcePaths, screen
|
|
|
662
707
|
return [...new Set(warnings)].slice(0, MAX_FRESHNESS_WARNINGS);
|
|
663
708
|
}
|
|
664
709
|
|
|
710
|
+
async function latestSourceTime(projectPath, sourcePaths, files) {
|
|
711
|
+
let latest = 0;
|
|
712
|
+
for (const path of new Set([...sourcePaths.filter(Boolean), ...files.filter((file) => file.status !== "deleted").map((file) => file.path)])) {
|
|
713
|
+
try {
|
|
714
|
+
const details = await stat(await insideProjectRealpath(projectPath, path, "Source path"));
|
|
715
|
+
if (details.isFile()) latest = Math.max(latest, details.mtimeMs);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (!isMissingSourceError(error) && !error.message?.includes("must resolve inside projectPath")) throw error;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return latest;
|
|
721
|
+
}
|
|
722
|
+
|
|
665
723
|
function localeText(locale) {
|
|
666
724
|
return locale === "zh-CN" ? {
|
|
667
725
|
title: "布局摘要", projectTask: "项目与任务", task: "任务", status: "状态", version: "报告版本", poster: "海报产物", posterNotGenerated: "本次未生成", freshness: "时效审查", freshnessOk: "报告、源文件和截图的时间顺序未发现明显冲突", theme: "主题系统", rail: "交付轨道", main: "主页面区段", breakpoints: "响应式断点", screenshots: "截图视口", git: "Git 改动", range: "范围", receipts: "测试回执", privacy: "隐私审查", redactions: "已脱敏值", interactions: "交互控件", notRecorded: "未记录", untitledProject: "未命名项目", unknown: "未知", notCaptured: "未捕获", exit: "退出码", conclusion: "根据报告和布局源文件在本地生成。未上传任何内容。", empty: "未记录", posterTitle: "布局证据海报", kicker: "交付证据 / 本地报告", verified: "已验证", files: "改动文件", tests: "通过测试", redaction: "已脱敏", footer: "仅在本地生成 / 不会上传",
|
|
@@ -715,6 +773,11 @@ function durationLabel(value) {
|
|
|
715
773
|
}
|
|
716
774
|
|
|
717
775
|
export async function generateLayoutSummary(options = {}) {
|
|
776
|
+
if (!options.projectPath || typeof options.projectPath !== "string") throw new Error("projectPath is required.");
|
|
777
|
+
return withProjectLock(options.projectPath, () => generateLockedSummary(options), options.signal);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function generateLockedSummary(options) {
|
|
718
781
|
if (!options.projectPath || typeof options.projectPath !== "string") {
|
|
719
782
|
throw new Error("projectPath is required.");
|
|
720
783
|
}
|
|
@@ -759,7 +822,7 @@ export async function generateLayoutSummary(options = {}) {
|
|
|
759
822
|
if (reportDetails.size > MAX_REPORT_BYTES) {
|
|
760
823
|
throw new Error(`reportPath exceeds the ${MAX_REPORT_BYTES} byte safety limit.`);
|
|
761
824
|
}
|
|
762
|
-
const rawReport =
|
|
825
|
+
const rawReport = parseJson(await readBoundedFile(reportPath, MAX_REPORT_BYTES, options.signal));
|
|
763
826
|
report = reportSchema.parse(rawReport);
|
|
764
827
|
} catch (error) {
|
|
765
828
|
if (Array.isArray(error?.issues)) {
|
|
@@ -793,12 +856,13 @@ export async function generateLayoutSummary(options = {}) {
|
|
|
793
856
|
const stages = collectStages(app, locale);
|
|
794
857
|
const breakpoints = collectBreakpoints(css);
|
|
795
858
|
const screenshots = Array.isArray(report.screenshots) ? report.screenshots : [];
|
|
859
|
+
const sourceTime = await latestSourceTime(projectPath, [appPath, cssPath], report.git.files);
|
|
796
860
|
const matchingScreenshots = [];
|
|
797
861
|
const isolatedScreenshots = [];
|
|
798
862
|
const unclassifiedScreenshots = [];
|
|
799
863
|
for (const screenshot of screenshots) {
|
|
800
864
|
const captureTheme = screenshotTheme(screenshot);
|
|
801
|
-
if (captureTheme === theme) matchingScreenshots.push(screenshot);
|
|
865
|
+
if (captureTheme === theme || !captureTheme) matchingScreenshots.push(screenshot);
|
|
802
866
|
else if (captureTheme) isolatedScreenshots.push(screenshot);
|
|
803
867
|
else unclassifiedScreenshots.push(screenshot);
|
|
804
868
|
}
|
|
@@ -806,15 +870,28 @@ export async function generateLayoutSummary(options = {}) {
|
|
|
806
870
|
projectPath,
|
|
807
871
|
generatePoster ? matchingScreenshots : screenshots,
|
|
808
872
|
locale,
|
|
809
|
-
{ embed: generatePoster },
|
|
873
|
+
{ embed: generatePoster, generatedAt: Date.parse(report.generatedAt), sourceTime, fingerprint: report.git.fingerprint, signal: options.signal },
|
|
810
874
|
);
|
|
811
875
|
const posterScreenshots = collectedEvidence.posterScreenshots;
|
|
812
876
|
const invalidEvidenceViewports = collectedEvidence.invalidEvidenceViewports;
|
|
813
877
|
const evidenceViewports = collectedEvidence.verifiedEvidenceViewports;
|
|
814
878
|
const files = Array.isArray(report.git?.files) ? report.git.files : [];
|
|
815
|
-
const tests =
|
|
816
|
-
const
|
|
817
|
-
|
|
879
|
+
const tests = report.tests.map((test) => ({ ...test, status: test.exitCode !== 0 ? "failed" : test.status }));
|
|
880
|
+
const visualProject = Boolean(
|
|
881
|
+
cssPath
|
|
882
|
+
|| (appPath && VISUAL_SOURCE_EXTENSIONS.has(sourceExtension(appPath)))
|
|
883
|
+
|| /<(?:html|body|main|section|article|div|button|form|canvas|svg)\b|React\.createElement\s*\(|document\.(?:querySelector|getElementById|createElement)\s*\(/i.test(app),
|
|
884
|
+
);
|
|
885
|
+
const freshnessWarnings = [
|
|
886
|
+
...(report.warnings ?? []).map((warning) => safeText(warning)),
|
|
887
|
+
...await collectFreshnessWarnings(projectPath, report, [appPath, cssPath], screenshots, locale),
|
|
888
|
+
...invalidEvidenceViewports.map((label) => locale === "zh-CN" ? `截图证据不可用: ${label}` : `Capture evidence unavailable: ${label}`),
|
|
889
|
+
...(visualProject && !collectedEvidence.currentCaptureCount ? [locale === "zh-CN" ? "尚无有效的改版后整屏截图,视觉验收未完成。" : "No valid after viewport capture; visual acceptance is incomplete."] : []),
|
|
890
|
+
].slice(0, MAX_FRESHNESS_WARNINGS);
|
|
891
|
+
const failed = tests.some((test) => test.status === "failed");
|
|
892
|
+
const incomplete = !tests.length || tests.some((test) => test.status !== "passed") || freshnessWarnings.length > 0
|
|
893
|
+
|| (report.git.baseRef === "NO_GIT" && report.git.headRef === "NO_GIT");
|
|
894
|
+
const taskStatus = taskStatusModel(failed || report.task.status === "failed" ? "failed" : incomplete ? "partial" : report.task.status, locale);
|
|
818
895
|
const redaction = report.redaction ?? {};
|
|
819
896
|
const sections = locale === "zh-CN" ? ["项目与任务", "主题系统", "交付轨道", "证据带", "响应式采集", "变更清单", "验证回执", "时效审查", "隐私审查", "本地导出控件"] : ["Project and task", "Theme system", "Operations rail", "Evidence bands", "Responsive capture", "Change set", "Verification receipts", "Freshness review", "Privacy review", "Local export controls"];
|
|
820
897
|
// A poster is scoped to one visual theme. Keep its textual viewport claims
|
|
@@ -827,23 +904,25 @@ export async function generateLayoutSummary(options = {}) {
|
|
|
827
904
|
const gitUnavailable = report.git?.baseRef === "NO_GIT" && report.git?.headRef === "NO_GIT";
|
|
828
905
|
const gitState = gitUnavailable ? "unavailable" : files.length ? "changed" : "clean";
|
|
829
906
|
const testState = tests.length ? (tests.some((test) => test.status === "failed") ? "failed" : "configured") : "unconfigured";
|
|
830
|
-
const visualProject = Boolean(
|
|
831
|
-
cssPath
|
|
832
|
-
|| (appPath && VISUAL_SOURCE_EXTENSIONS.has(sourceExtension(appPath)))
|
|
833
|
-
|| /<(?:html|body|main|section|article|div|button|form|canvas|svg)\b|React\.createElement\s*\(|document\.(?:querySelector|getElementById|createElement)\s*\(/i.test(app),
|
|
834
|
-
);
|
|
835
907
|
const gitRange = truncateText(`${safeText(report.git?.baseRef) || text.unknown}..${safeText(report.git?.headRef) || text.unknown}`, 180);
|
|
836
|
-
const gitFiles = files.slice(0,
|
|
908
|
+
const gitFiles = files.slice(0, 20).map((file) => ({
|
|
837
909
|
path: safeRelativePath(file.path),
|
|
838
910
|
additions: Number(file.additions) || 0,
|
|
839
911
|
deletions: Number(file.deletions) || 0,
|
|
912
|
+
status: file.status,
|
|
913
|
+
previousPath: file.previousPath ? safeRelativePath(file.previousPath) : undefined,
|
|
914
|
+
diff: file.diff === undefined ? undefined : safeBlock(file.diff),
|
|
915
|
+
diffTruncated: Boolean(file.diffTruncated || (file.diff?.length ?? 0) > 16_000),
|
|
916
|
+
diffUnavailable: file.diffUnavailable,
|
|
840
917
|
}));
|
|
841
|
-
const testReceipts = tests.slice(0,
|
|
918
|
+
const testReceipts = tests.slice(0, 20).map((test) => ({
|
|
842
919
|
command: truncateText(safeText(test.command), 240),
|
|
843
920
|
duration: durationLabel(test.durationMs),
|
|
844
921
|
exitCode: Number(test.exitCode) || 0,
|
|
845
922
|
status: safeText(test.status),
|
|
846
923
|
statusLabel: statusLabel(test.status, locale),
|
|
924
|
+
output: safeBlock(test.output, 4000),
|
|
925
|
+
outputTruncated: Boolean(test.outputTruncated || test.output.length > 4000),
|
|
847
926
|
}));
|
|
848
927
|
const changes = files.map((file) => `\`${safeRelativePath(file.path)}\` (${statusLabel(file.status, locale)}, +${Number(file.additions) || 0} / -${Number(file.deletions) || 0})`);
|
|
849
928
|
const receipts = tests.map((test) => `\`${truncateText(safeText(test.command), 240)}\` (${statusLabel(test.status, locale)}, ${text.exit} ${Number(test.exitCode) || 0})`);
|
|
@@ -855,7 +934,7 @@ export async function generateLayoutSummary(options = {}) {
|
|
|
855
934
|
"",
|
|
856
935
|
`## ${text.projectTask}`,
|
|
857
936
|
`- ${text.task}: ${taskText}`,
|
|
858
|
-
`- ${text.status}: ${
|
|
937
|
+
`- ${text.status}: ${taskStatus.label}`,
|
|
859
938
|
`- ${text.version}: ${safeText(report.version) || text.notRecorded}`,
|
|
860
939
|
`- ${text.poster}: ${posterReference}`,
|
|
861
940
|
"",
|
|
@@ -902,12 +981,20 @@ export async function generateLayoutSummary(options = {}) {
|
|
|
902
981
|
image: posterImage.toString("base64"),
|
|
903
982
|
surfaceImage: surfaceAssetPath ? `./${DIJIANG_SURFACE_FILENAME}` : "",
|
|
904
983
|
projectName,
|
|
984
|
+
generatedAt: safeText(report.generatedAt),
|
|
905
985
|
task: taskText,
|
|
906
986
|
stages,
|
|
907
987
|
fileCount: files.length,
|
|
908
988
|
passedTests,
|
|
909
989
|
testCount: tests.length,
|
|
910
990
|
redactionCount: Number(redaction.totalReplacements) || 0,
|
|
991
|
+
redactionDetails: Object.entries(redaction.replacements ?? {}).map(([name, count]) => ({ name: redactionLabel(name, locale), count })),
|
|
992
|
+
durationMs: report.task.durationMs,
|
|
993
|
+
additions: report.git.summary.additions,
|
|
994
|
+
deletions: report.git.summary.deletions,
|
|
995
|
+
reportData: sanitizeReportExport({ ...report, task: { ...report.task, status: taskStatus.key }, tests, warnings: freshnessWarnings,
|
|
996
|
+
evidenceReview: { theme, matched: evidenceViewports, excluded: invalidEvidenceViewports } }),
|
|
997
|
+
summaryMarkdown: sanitizedMarkdown,
|
|
911
998
|
// Keep the filtered poster set available to the renderer as the source of
|
|
912
999
|
// truth; legacy fields remain populated for callers that already consume
|
|
913
1000
|
// them.
|
|
@@ -925,7 +1012,7 @@ export async function generateLayoutSummary(options = {}) {
|
|
|
925
1012
|
visualProject,
|
|
926
1013
|
taskStatus: taskStatus.key,
|
|
927
1014
|
taskStatusLabel: taskStatus.label,
|
|
928
|
-
gitFileCount:
|
|
1015
|
+
gitFileCount: files.length,
|
|
929
1016
|
gitFiles,
|
|
930
1017
|
testReceipts,
|
|
931
1018
|
freshnessWarnings,
|
|
@@ -937,6 +1024,7 @@ export async function generateLayoutSummary(options = {}) {
|
|
|
937
1024
|
});
|
|
938
1025
|
sanitizedPoster = posterHtml.replaceAll(projectPath, "[PATH OMITTED]");
|
|
939
1026
|
}
|
|
1027
|
+
options.signal?.throwIfAborted();
|
|
940
1028
|
await writeArtifactFiles([
|
|
941
1029
|
...(surfaceAssetPath ? [{ path: surfaceAssetPath, content: posterImage }] : []),
|
|
942
1030
|
...(generatePoster ? [{ path: posterPath, content: sanitizedPoster }] : []),
|