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.
@@ -0,0 +1,101 @@
1
+ const escape = (value) => String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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;
@@ -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
- for (const shot of screenshots) {
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 image = await readFile(fullPath);
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 verifiedLabel = `${name}: ${width} x ${height}`;
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 recorded as invalid and skipped.
591
- if (strictPaths && error instanceof Error && error.message.includes("must resolve inside projectPath.")) throw error;
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 = JSON.parse(await readFile(reportPath, "utf8"));
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 = Array.isArray(report.tests) ? report.tests : [];
816
- const freshnessWarnings = await collectFreshnessWarnings(projectPath, report, [appPath, cssPath], screenshots, locale);
817
- const taskStatus = taskStatusModel(report.task?.status, locale);
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, 5).map((file) => ({
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, 3).map((test) => ({
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}: ${statusLabel(report.task?.status, locale) || text.notRecorded}`,
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: Number(report.git?.summary?.changedFiles) || files.length,
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 }] : []),