sparkforensics-cli 0.2.0 → 0.2.2
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/export-template/docs/404.html +1 -1
- package/export-template/docs/assets/{app.CndaAS6v.js → app.DQTZyGL1.js} +1 -1
- package/export-template/docs/assets/chunks/@localSearchIndexroot.DppnXnDE.js +1 -0
- package/export-template/docs/assets/chunks/{VPLocalSearchBox.yJbZbsEo.js → VPLocalSearchBox.BkBIPFs6.js} +1 -1
- package/export-template/docs/assets/chunks/theme.DP0u1AUq.js +2 -0
- package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.CWpj01WU.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.CWpj01WU.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.CgzUsQ6W.js +1 -0
- package/export-template/docs/assets/{contributor-guide_architecture_impact-estimation.md.DYCDPgkh.js → contributor-guide_architecture_impact-estimation.md.CooslVJt.js} +1 -1
- package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.C-xxn0q7.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.C-xxn0q7.lean.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.R27gQrgY.js +1 -0
- package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.IbnfNrV3.js +6 -0
- package/export-template/docs/assets/{style.DXOMCXxn.css → style.DSixAiZE.css} +1 -1
- package/export-template/docs/assets/{user-guide_alternative-log-retrieval.md.sU3KGarf.js → user-guide_alternative-log-retrieval.md.B4tPGIal.js} +1 -1
- package/export-template/docs/assets/{user-guide_alternative-log-retrieval.md.sU3KGarf.lean.js → user-guide_alternative-log-retrieval.md.B4tPGIal.lean.js} +1 -1
- package/export-template/docs/assets/user-guide_getting-started.md.BJvwLEIM.js +3 -0
- package/export-template/docs/assets/user-guide_getting-started.md.BJvwLEIM.lean.js +1 -0
- package/export-template/docs/assets/{user-guide_mcp-tools.md.C8MiIu7F.js → user-guide_mcp-tools.md.Vi3RoflJ.js} +3 -3
- package/export-template/docs/assets/{user-guide_mcp-tools.md.C8MiIu7F.lean.js → user-guide_mcp-tools.md.Vi3RoflJ.lean.js} +1 -1
- package/export-template/docs/assets/user-guide_run-comparison.md.CQc1aoU8.js +1 -0
- package/export-template/docs/assets/user-guide_run-comparison.md.CQc1aoU8.lean.js +1 -0
- package/export-template/docs/assets/user-guide_understanding-findings.md.DL1UDhvR.js +1 -0
- package/export-template/docs/assets/user-guide_understanding-findings.md.DL1UDhvR.lean.js +1 -0
- package/export-template/docs/contributor-guide/architecture/board-widgets.html +2 -2
- package/export-template/docs/contributor-guide/architecture/detector-contract.html +2 -2
- package/export-template/docs/contributor-guide/architecture/drill-down.html +1 -1
- package/export-template/docs/contributor-guide/architecture/impact-estimation.html +2 -2
- package/export-template/docs/contributor-guide/architecture/index.html +1 -1
- package/export-template/docs/contributor-guide/architecture/overview.html +1 -1
- package/export-template/docs/contributor-guide/architecture/state-and-history.html +2 -2
- package/export-template/docs/contributor-guide/architecture/widget-rendering.html +2 -2
- package/export-template/docs/contributor-guide/architecture/worker-protocol.html +2 -2
- package/export-template/docs/contributor-guide/contributing.html +1 -1
- package/export-template/docs/contributor-guide/development-setup.html +1 -1
- package/export-template/docs/contributor-guide/testing.html +1 -1
- package/export-template/docs/index.html +1 -1
- package/export-template/docs/tuning-reference/anti-patterns.html +1 -1
- package/export-template/docs/tuning-reference/aqe.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-broadcast-sizing.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-cold-start.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-failures.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-gc.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-job-failure-rate.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-memory-utilization.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-retry-waste.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-shuffle.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-skew.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-slow-host.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-small-files.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-spill.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-straggler.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-tiny-tasks.html +1 -1
- package/export-template/docs/tuning-reference/bottleneck-utilization.html +1 -1
- package/export-template/docs/tuning-reference/caching.html +1 -1
- package/export-template/docs/tuning-reference/cluster-config.html +1 -1
- package/export-template/docs/tuning-reference/config.html +1 -1
- package/export-template/docs/tuning-reference/data-formats.html +1 -1
- package/export-template/docs/tuning-reference/index.html +1 -1
- package/export-template/docs/tuning-reference/intro.html +1 -1
- package/export-template/docs/tuning-reference/joins.html +1 -1
- package/export-template/docs/tuning-reference/memory-model.html +1 -1
- package/export-template/docs/tuning-reference/metrics.html +1 -1
- package/export-template/docs/tuning-reference/partitioning.html +1 -1
- package/export-template/docs/tuning-reference/pyspark.html +1 -1
- package/export-template/docs/tuning-reference/shuffle.html +1 -1
- package/export-template/docs/tuning-reference/spark-architecture.html +1 -1
- package/export-template/docs/tuning-reference/table-formats.html +1 -1
- package/export-template/docs/user-guide/alternative-log-retrieval.html +2 -2
- package/export-template/docs/user-guide/getting-started.html +3 -3
- package/export-template/docs/user-guide/mcp-tools.html +4 -4
- package/export-template/docs/user-guide/run-comparison.html +2 -2
- package/export-template/docs/user-guide/understanding-findings.html +2 -2
- package/export-template/index.html +75 -79
- package/export-template/parser-worker-DyjiQvfP.js +112 -0
- package/export-template/sample-runs/sample-run.ndjson.gz +0 -0
- package/package.json +5 -4
- package/vendor-core/detectors.js +149 -24
- package/vendor-core/docs-content/detection/cache.md +4 -3
- package/vendor-core/docs-content/detection/chrn.md +4 -2
- package/vendor-core/docs-content/detection/local.md +2 -3
- package/vendor-core/docs-content/detection/mem.md +3 -3
- package/vendor-core/docs-content/detection/spec.md +4 -3
- package/vendor-core/evidence-report.js +1 -1
- package/vendor-core/parser-worker.js +2 -2
- package/vendor-core/run-comparison.js +21 -2
- package/export-template/docs/assets/chunks/@localSearchIndexroot.DNY8bVcl.js +0 -1
- package/export-template/docs/assets/chunks/theme.Df2VAG9w.js +0 -2
- package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.B-OsL91z.js +0 -1
- package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.B-OsL91z.lean.js +0 -1
- package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.BOeH4d1J.js +0 -1
- package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.m3S3UdMk.js +0 -1
- package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.m3S3UdMk.lean.js +0 -1
- package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.DbqPf2OT.js +0 -1
- package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.B93qJ_tT.js +0 -6
- package/export-template/docs/assets/user-guide_getting-started.md.DtEM37MK.js +0 -3
- package/export-template/docs/assets/user-guide_getting-started.md.DtEM37MK.lean.js +0 -1
- package/export-template/docs/assets/user-guide_run-comparison.md.S0TWWmLY.js +0 -1
- package/export-template/docs/assets/user-guide_run-comparison.md.S0TWWmLY.lean.js +0 -1
- package/export-template/docs/assets/user-guide_understanding-findings.md.D0R_Y-R2.js +0 -1
- package/export-template/docs/assets/user-guide_understanding-findings.md.D0R_Y-R2.lean.js +0 -1
- package/export-template/parser-worker-QqyEE4m9.js +0 -64
- /package/export-template/docs/assets/{contributor-guide_architecture_detector-contract.md.BOeH4d1J.lean.js → contributor-guide_architecture_detector-contract.md.CgzUsQ6W.lean.js} +0 -0
- /package/export-template/docs/assets/{contributor-guide_architecture_impact-estimation.md.DYCDPgkh.lean.js → contributor-guide_architecture_impact-estimation.md.CooslVJt.lean.js} +0 -0
- /package/export-template/docs/assets/{contributor-guide_architecture_widget-rendering.md.DbqPf2OT.lean.js → contributor-guide_architecture_widget-rendering.md.R27gQrgY.lean.js} +0 -0
- /package/export-template/docs/assets/{contributor-guide_architecture_worker-protocol.md.B93qJ_tT.lean.js → contributor-guide_architecture_worker-protocol.md.IbnfNrV3.lean.js} +0 -0
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/chunks/VPLocalSearchBox.yJbZbsEo.js","assets/chunks/framework.DSg0KOwT.js"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{d as p,c as d,r as u,n as T,o as r,a as C,t as x,b as g,w as m,T as de,e as h,_ as k,u as Pe,i as Fe,f as De,g as B,h as y,j as i,k as l,l as z,m as ie,p as S,q as D,s as X,v as q,x as ve,y as fe,z as je,A as Ue,F as N,B as A,C as K,D as Z,E as b,G as Le,H as O,I as Ve,J as ee,K as G,L as te,M as Ge,N as Se,O as re,P as we,Q as Te,R,S as qe,U as ze,V as Ke,W as xe,X as Ne,Y as We,Z as Re,$ as Je,a0 as Me,a1 as Qe,a2 as Ye,a3 as Xe,a4 as Ze,a5 as _e}from"./framework.DSg0KOwT.js";const et=p({__name:"VPBadge",props:{text:{},type:{default:"tip"}},setup(e){return(t,n)=>(r(),d("span",{class:T(["VPBadge",e.type])},[u(t.$slots,"default",{},()=>[C(x(e.text),1)])],2))}}),tt={key:0,class:"VPBackdrop"},nt=p({__name:"VPBackdrop",props:{show:{type:Boolean}},setup(e){return(t,n)=>(r(),g(de,{name:"fade"},{default:m(()=>[e.show?(r(),d("div",tt)):h("",!0)]),_:1}))}}),at=k(nt,[["__scopeId","data-v-c79a1216"]]),L=Pe;function st(e,t){let n,a=!1;return()=>{n&&clearTimeout(n),a?n=setTimeout(e,t):(e(),(a=!0)&&setTimeout(()=>a=!1,t))}}function le(e){return e.startsWith("/")?e:`/${e}`}function me(e){const{pathname:t,search:n,hash:a,protocol:s}=new URL(e,"http://a.com");if(Fe(e)||e.startsWith("#")||!s.startsWith("http")||!De(t))return e;const{site:c}=L(),o=t.endsWith("/")||t.endsWith(".html")?e:e.replace(/(?:(^\.+)\/)?.*$/,`$1${t.replace(/(\.md)?$/,c.value.cleanUrls?"":".html")}${n}${a}`);return B(o)}function J({correspondingLink:e=!1}={}){const{site:t,localeIndex:n,page:a,theme:s,hash:c}=L(),o=y(()=>{var f,$;return{label:(f=t.value.locales[n.value])==null?void 0:f.label,link:(($=t.value.locales[n.value])==null?void 0:$.link)||(n.value==="root"?"/":`/${n.value}/`)}});return{localeLinks:y(()=>Object.entries(t.value.locales).flatMap(([f,$])=>o.value.label===$.label?[]:{text:$.label,link:ot($.link||(f==="root"?"/":`/${f}/`),s.value.i18nRouting!==!1&&e,a.value.relativePath.slice(o.value.link.length-1),!t.value.cleanUrls)+c.value})),currentLang:o}}function ot(e,t,n,a){return t?e.replace(/\/$/,"")+le(n.replace(/(^|\/)index\.md$/,"$1").replace(/\.md$/,a?".html":"")):e}const it={class:"NotFound"},rt={class:"code"},lt={class:"title"},ct={class:"quote"},ut={class:"action"},dt=["href","aria-label"],vt=p({__name:"NotFound",setup(e){const{theme:t}=L(),{currentLang:n}=J();return(a,s)=>{var c,o,v,f,$;return r(),d("div",it,[i("p",rt,x(((c=l(t).notFound)==null?void 0:c.code)??"404"),1),i("h1",lt,x(((o=l(t).notFound)==null?void 0:o.title)??"PAGE NOT FOUND"),1),s[0]||(s[0]=i("div",{class:"divider"},null,-1)),i("blockquote",ct,x(((v=l(t).notFound)==null?void 0:v.quote)??"But if you don't change your direction, and if you keep looking, you may end up where you are heading."),1),i("div",ut,[i("a",{class:"link",href:l(B)(l(n).link),"aria-label":((f=l(t).notFound)==null?void 0:f.linkLabel)??"go to home"},x((($=l(t).notFound)==null?void 0:$.linkText)??"Take me home"),9,dt)])])}}}),ft=k(vt,[["__scopeId","data-v-d6be1790"]]);function Ie(e,t){if(Array.isArray(e))return Q(e);if(e==null)return[];t=le(t);const n=Object.keys(e).sort((s,c)=>c.split("/").length-s.split("/").length).find(s=>t.startsWith(le(s))),a=n?e[n]:[];return Array.isArray(a)?Q(a):Q(a.items,a.base)}function mt(e){const t=[];let n=0;for(const a in e){const s=e[a];if(s.items){n=t.push(s);continue}t[n]||t.push({items:[]}),t[n].items.push(s)}return t}function ht(e){const t=[];function n(a){for(const s of a)s.text&&s.link&&t.push({text:s.text,link:s.link,docFooterText:s.docFooterText}),s.items&&n(s.items)}return n(e),t}function ce(e,t){return Array.isArray(t)?t.some(n=>ce(e,n)):z(e,t.link)?!0:t.items?ce(e,t.items):!1}function Q(e,t){return[...e].map(n=>{const a={...n},s=a.base||t;return s&&a.link&&(a.link=s+a.link),a.items&&(a.items=Q(a.items,s)),a})}function j(){const{frontmatter:e,page:t,theme:n}=L(),a=ie("(min-width: 960px)"),s=S(!1),c=y(()=>{const I=n.value.sidebar,E=t.value.relativePath;return I?Ie(I,E):[]}),o=S(c.value);D(c,(I,E)=>{JSON.stringify(I)!==JSON.stringify(E)&&(o.value=c.value)});const v=y(()=>e.value.sidebar!==!1&&o.value.length>0&&e.value.layout!=="home"),f=y(()=>$?e.value.aside==null?n.value.aside==="left":e.value.aside==="left":!1),$=y(()=>e.value.layout==="home"?!1:e.value.aside!=null?!!e.value.aside:n.value.aside!==!1),V=y(()=>v.value&&a.value),_=y(()=>v.value?mt(o.value):[]);function P(){s.value=!0}function w(){s.value=!1}function M(){s.value?w():P()}return{isOpen:s,sidebar:o,sidebarGroups:_,hasSidebar:v,hasAside:$,leftAside:f,isSidebarEnabled:V,open:P,close:w,toggle:M}}function pt(e,t){let n;X(()=>{n=e.value?document.activeElement:void 0}),q(()=>{window.addEventListener("keyup",a)}),ve(()=>{window.removeEventListener("keyup",a)});function a(s){s.key==="Escape"&&e.value&&(t(),n==null||n.focus())}}function bt(e){const{page:t,hash:n}=L(),a=S(!1),s=y(()=>e.value.collapsed!=null),c=y(()=>!!e.value.link),o=S(!1),v=()=>{o.value=z(t.value.relativePath,e.value.link)};D([t,e,n],v),q(v);const f=y(()=>o.value?!0:e.value.items?ce(t.value.relativePath,e.value.items):!1),$=y(()=>!!(e.value.items&&e.value.items.length));X(()=>{a.value=!!(s.value&&e.value.collapsed)}),fe(()=>{(o.value||f.value)&&(a.value=!1)});function V(){s.value&&(a.value=!a.value)}return{collapsed:a,collapsible:s,isLink:c,isActiveLink:o,hasActiveLink:f,hasChildren:$,toggle:V}}function gt(){const{hasSidebar:e}=j(),t=ie("(min-width: 960px)"),n=ie("(min-width: 1280px)");return{isAsideEnabled:y(()=>!n.value&&!t.value?!1:e.value?n.value:t.value)}}const kt=/\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/,ue=[];function Ae(e){return typeof e.outline=="object"&&!Array.isArray(e.outline)&&e.outline.label||e.outlineTitle||"On this page"}function he(e){const t=[...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")].filter(n=>n.id&&n.hasChildNodes()).map(n=>{const a=Number(n.tagName[1]);return{element:n,title:_t(n),link:"#"+n.id,level:a}});return $t(t,e)}function _t(e){let t="";for(const n of e.childNodes)if(n.nodeType===1){if(kt.test(n.className))continue;t+=n.textContent}else n.nodeType===3&&(t+=n.textContent);return t.trim()}function $t(e,t){if(t===!1)return[];const n=(typeof t=="object"&&!Array.isArray(t)?t.level:t)||2,[a,s]=typeof n=="number"?[n,n]:n==="deep"?[2,6]:n;return Lt(e,a,s)}function yt(e,t){const{isAsideEnabled:n}=gt(),a=st(c,100);let s=null;q(()=>{requestAnimationFrame(c),window.addEventListener("scroll",a)}),je(()=>{o(location.hash)}),ve(()=>{window.removeEventListener("scroll",a)});function c(){if(!n.value)return;const v=window.scrollY,f=window.innerHeight,$=document.body.offsetHeight,V=Math.abs(v+f-$)<1,_=ue.map(({element:w,link:M})=>({link:M,top:Pt(w)})).filter(({top:w})=>!Number.isNaN(w)).sort((w,M)=>w.top-M.top);if(!_.length){o(null);return}if(v<1){o(null);return}if(V){o(_[_.length-1].link);return}let P=null;for(const{link:w,top:M}of _){if(M>v+Ue()+4)break;P=w}o(P)}function o(v){s&&s.classList.remove("active"),v==null?s=null:s=e.value.querySelector(`a[href="${decodeURIComponent(v)}"]`);const f=s;f?(f.classList.add("active"),t.value.style.top=f.offsetTop+39+"px",t.value.style.opacity="1"):(t.value.style.top="33px",t.value.style.opacity="0")}}function Pt(e){let t=0;for(;e!==document.body;){if(e===null)return NaN;t+=e.offsetTop,e=e.offsetParent}return t}function Lt(e,t,n){ue.length=0;const a=[],s=[];return e.forEach(c=>{const o={...c,children:[]};let v=s[s.length-1];for(;v&&v.level>=o.level;)s.pop(),v=s[s.length-1];if(o.element.classList.contains("ignore-header")||v&&"shouldIgnore"in v){s.push({level:o.level,shouldIgnore:!0});return}o.level>n||o.level<t||(ue.push({element:o.element,link:o.link}),v?v.children.push(o):a.push(o),s.push(o))}),a}const Vt=["href","title"],St=p({__name:"VPDocOutlineItem",props:{headers:{},root:{type:Boolean}},setup(e){function t({target:n}){const a=n.href.split("#")[1],s=document.getElementById(decodeURIComponent(a));s==null||s.focus({preventScroll:!0})}return(n,a)=>{const s=K("VPDocOutlineItem",!0);return r(),d("ul",{class:T(["VPDocOutlineItem",e.root?"root":"nested"])},[(r(!0),d(N,null,A(e.headers,({children:c,link:o,title:v})=>(r(),d("li",null,[i("a",{class:"outline-link",href:o,onClick:t,title:v},x(v),9,Vt),c!=null&&c.length?(r(),g(s,{key:0,headers:c},null,8,["headers"])):h("",!0)]))),256))],2)}}}),Ce=k(St,[["__scopeId","data-v-b933a997"]]),wt={class:"content"},Tt={"aria-level":"2",class:"outline-title",id:"doc-outline-aria-label",role:"heading"},xt=p({__name:"VPDocAsideOutline",setup(e){const{frontmatter:t,theme:n}=L(),a=Le([]);Z(()=>{a.value=he(t.value.outline??n.value.outline)});const s=S(),c=S();return yt(s,c),(o,v)=>(r(),d("nav",{"aria-labelledby":"doc-outline-aria-label",class:T(["VPDocAsideOutline",{"has-outline":a.value.length>0}]),ref_key:"container",ref:s},[i("div",wt,[i("div",{class:"outline-marker",ref_key:"marker",ref:c},null,512),i("div",Tt,x(l(Ae)(l(n))),1),b(Ce,{headers:a.value,root:!0},null,8,["headers"])])],2))}}),Nt=k(xt,[["__scopeId","data-v-a5bbad30"]]),Mt={class:"VPDocAsideCarbonAds"},It=p({__name:"VPDocAsideCarbonAds",props:{carbonAds:{}},setup(e){const t=()=>null;return(n,a)=>(r(),d("div",Mt,[b(l(t),{"carbon-ads":e.carbonAds},null,8,["carbon-ads"])]))}}),At={class:"VPDocAside"},Ct=p({__name:"VPDocAside",setup(e){const{theme:t}=L();return(n,a)=>(r(),d("div",At,[u(n.$slots,"aside-top",{},void 0,!0),u(n.$slots,"aside-outline-before",{},void 0,!0),b(Nt),u(n.$slots,"aside-outline-after",{},void 0,!0),a[0]||(a[0]=i("div",{class:"spacer"},null,-1)),u(n.$slots,"aside-ads-before",{},void 0,!0),l(t).carbonAds?(r(),g(It,{key:0,"carbon-ads":l(t).carbonAds},null,8,["carbon-ads"])):h("",!0),u(n.$slots,"aside-ads-after",{},void 0,!0),u(n.$slots,"aside-bottom",{},void 0,!0)]))}}),Et=k(Ct,[["__scopeId","data-v-3f215769"]]);function Bt(){const{theme:e,page:t}=L();return y(()=>{const{text:n="Edit this page",pattern:a=""}=e.value.editLink||{};let s;return typeof a=="function"?s=a(t.value):s=a.replace(/:path/g,t.value.filePath),{url:s,text:n}})}function Ht(){const{page:e,theme:t,frontmatter:n}=L();return y(()=>{var $,V,_,P,w,M,I,E;const a=Ie(t.value.sidebar,e.value.relativePath),s=ht(a),c=Ot(s,H=>H.link.replace(/[?#].*$/,"")),o=c.findIndex(H=>z(e.value.relativePath,H.link)),v=(($=t.value.docFooter)==null?void 0:$.prev)===!1&&!n.value.prev||n.value.prev===!1,f=((V=t.value.docFooter)==null?void 0:V.next)===!1&&!n.value.next||n.value.next===!1;return{prev:v?void 0:{text:(typeof n.value.prev=="string"?n.value.prev:typeof n.value.prev=="object"?n.value.prev.text:void 0)??((_=c[o-1])==null?void 0:_.docFooterText)??((P=c[o-1])==null?void 0:P.text),link:(typeof n.value.prev=="object"?n.value.prev.link:void 0)??((w=c[o-1])==null?void 0:w.link)},next:f?void 0:{text:(typeof n.value.next=="string"?n.value.next:typeof n.value.next=="object"?n.value.next.text:void 0)??((M=c[o+1])==null?void 0:M.docFooterText)??((I=c[o+1])==null?void 0:I.text),link:(typeof n.value.next=="object"?n.value.next.link:void 0)??((E=c[o+1])==null?void 0:E.link)}}})}function Ot(e,t){const n=new Set;return e.filter(a=>{const s=t(a);return n.has(s)?!1:n.add(s)})}const F=p({__name:"VPLink",props:{tag:{},href:{},noIcon:{type:Boolean},target:{},rel:{}},setup(e){const t=e,n=y(()=>t.tag??(t.href?"a":"span")),a=y(()=>t.href&&Ve.test(t.href)||t.target==="_blank");return(s,c)=>(r(),g(O(n.value),{class:T(["VPLink",{link:e.href,"vp-external-link-icon":a.value,"no-icon":e.noIcon}]),href:e.href?l(me)(e.href):void 0,target:e.target??(a.value?"_blank":void 0),rel:e.rel??(a.value?"noreferrer":void 0)},{default:m(()=>[u(s.$slots,"default")]),_:3},8,["class","href","target","rel"]))}}),Ft={class:"VPLastUpdated"},Dt=["datetime"],jt=p({__name:"VPDocFooterLastUpdated",setup(e){const{theme:t,page:n,lang:a}=L(),s=y(()=>new Date(n.value.lastUpdated)),c=y(()=>s.value.toISOString()),o=S("");return q(()=>{X(()=>{var v,f,$;o.value=new Intl.DateTimeFormat((f=(v=t.value.lastUpdated)==null?void 0:v.formatOptions)!=null&&f.forceLocale?a.value:void 0,(($=t.value.lastUpdated)==null?void 0:$.formatOptions)??{dateStyle:"short",timeStyle:"short"}).format(s.value)})}),(v,f)=>{var $;return r(),d("p",Ft,[C(x((($=l(t).lastUpdated)==null?void 0:$.text)||l(t).lastUpdatedText||"Last updated")+": ",1),i("time",{datetime:c.value},x(o.value),9,Dt)])}}}),Ut=k(jt,[["__scopeId","data-v-e98dd255"]]),Gt={key:0,class:"VPDocFooter"},qt={key:0,class:"edit-info"},zt={key:0,class:"edit-link"},Kt={key:1,class:"last-updated"},Wt={key:1,class:"prev-next","aria-labelledby":"doc-footer-aria-label"},Rt={class:"pager"},Jt=["innerHTML"],Qt=["innerHTML"],Yt={class:"pager"},Xt=["innerHTML"],Zt=["innerHTML"],en=p({__name:"VPDocFooter",setup(e){const{theme:t,page:n,frontmatter:a}=L(),s=Bt(),c=Ht(),o=y(()=>t.value.editLink&&a.value.editLink!==!1),v=y(()=>n.value.lastUpdated),f=y(()=>o.value||v.value||c.value.prev||c.value.next);return($,V)=>{var _,P,w,M;return f.value?(r(),d("footer",Gt,[u($.$slots,"doc-footer-before",{},void 0,!0),o.value||v.value?(r(),d("div",qt,[o.value?(r(),d("div",zt,[b(F,{class:"edit-link-button",href:l(s).url,"no-icon":!0},{default:m(()=>[V[0]||(V[0]=i("span",{class:"vpi-square-pen edit-link-icon"},null,-1)),C(" "+x(l(s).text),1)]),_:1},8,["href"])])):h("",!0),v.value?(r(),d("div",Kt,[b(Ut)])):h("",!0)])):h("",!0),(_=l(c).prev)!=null&&_.link||(P=l(c).next)!=null&&P.link?(r(),d("nav",Wt,[V[1]||(V[1]=i("span",{class:"visually-hidden",id:"doc-footer-aria-label"},"Pager",-1)),i("div",Rt,[(w=l(c).prev)!=null&&w.link?(r(),g(F,{key:0,class:"pager-link prev",href:l(c).prev.link},{default:m(()=>{var I;return[i("span",{class:"desc",innerHTML:((I=l(t).docFooter)==null?void 0:I.prev)||"Previous page"},null,8,Jt),i("span",{class:"title",innerHTML:l(c).prev.text},null,8,Qt)]}),_:1},8,["href"])):h("",!0)]),i("div",Yt,[(M=l(c).next)!=null&&M.link?(r(),g(F,{key:0,class:"pager-link next",href:l(c).next.link},{default:m(()=>{var I;return[i("span",{class:"desc",innerHTML:((I=l(t).docFooter)==null?void 0:I.next)||"Next page"},null,8,Xt),i("span",{class:"title",innerHTML:l(c).next.text},null,8,Zt)]}),_:1},8,["href"])):h("",!0)])])):h("",!0)])):h("",!0)}}}),tn=k(en,[["__scopeId","data-v-e257564d"]]),nn={class:"container"},an={class:"aside-container"},sn={class:"aside-content"},on={class:"content"},rn={class:"content-container"},ln={class:"main"},cn=p({__name:"VPDoc",setup(e){const{theme:t}=L(),n=ee(),{hasSidebar:a,hasAside:s,leftAside:c}=j(),o=y(()=>n.path.replace(/[./]+/g,"_").replace(/_html$/,""));return(v,f)=>{const $=K("Content");return r(),d("div",{class:T(["VPDoc",{"has-sidebar":l(a),"has-aside":l(s)}])},[u(v.$slots,"doc-top",{},void 0,!0),i("div",nn,[l(s)?(r(),d("div",{key:0,class:T(["aside",{"left-aside":l(c)}])},[f[0]||(f[0]=i("div",{class:"aside-curtain"},null,-1)),i("div",an,[i("div",sn,[b(Et,null,{"aside-top":m(()=>[u(v.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":m(()=>[u(v.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":m(()=>[u(v.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":m(()=>[u(v.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":m(()=>[u(v.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":m(()=>[u(v.$slots,"aside-ads-after",{},void 0,!0)]),_:3})])])],2)):h("",!0),i("div",on,[i("div",rn,[u(v.$slots,"doc-before",{},void 0,!0),i("main",ln,[b($,{class:T(["vp-doc",[o.value,l(t).externalLinkIcon&&"external-link-icon-enabled"]])},null,8,["class"])]),b(tn,null,{"doc-footer-before":m(()=>[u(v.$slots,"doc-footer-before",{},void 0,!0)]),_:3}),u(v.$slots,"doc-after",{},void 0,!0)])])]),u(v.$slots,"doc-bottom",{},void 0,!0)],2)}}}),un=k(cn,[["__scopeId","data-v-39a288b8"]]),dn=p({__name:"VPButton",props:{tag:{},size:{default:"medium"},theme:{default:"brand"},text:{},href:{},target:{},rel:{}},setup(e){const t=e,n=y(()=>t.href&&Ve.test(t.href)),a=y(()=>t.tag||(t.href?"a":"button"));return(s,c)=>(r(),g(O(a.value),{class:T(["VPButton",[e.size,e.theme]]),href:e.href?l(me)(e.href):void 0,target:t.target??(n.value?"_blank":void 0),rel:t.rel??(n.value?"noreferrer":void 0)},{default:m(()=>[C(x(e.text),1)]),_:1},8,["class","href","target","rel"]))}}),vn=k(dn,[["__scopeId","data-v-fa7799d5"]]),fn=["src","alt"],mn=p({inheritAttrs:!1,__name:"VPImage",props:{image:{},alt:{}},setup(e){return(t,n)=>{const a=K("VPImage",!0);return e.image?(r(),d(N,{key:0},[typeof e.image=="string"||"src"in e.image?(r(),d("img",G({key:0,class:"VPImage"},typeof e.image=="string"?t.$attrs:{...e.image,...t.$attrs},{src:l(B)(typeof e.image=="string"?e.image:e.image.src),alt:e.alt??(typeof e.image=="string"?"":e.image.alt||"")}),null,16,fn)):(r(),d(N,{key:1},[b(a,G({class:"dark",image:e.image.dark,alt:e.image.alt},t.$attrs),null,16,["image","alt"]),b(a,G({class:"light",image:e.image.light,alt:e.image.alt},t.$attrs),null,16,["image","alt"])],64))],64)):h("",!0)}}}),Y=k(mn,[["__scopeId","data-v-8426fc1a"]]),hn={class:"container"},pn={class:"main"},bn={class:"heading"},gn=["innerHTML"],kn=["innerHTML"],_n=["innerHTML"],$n={key:0,class:"actions"},yn={key:0,class:"image"},Pn={class:"image-container"},Ln=p({__name:"VPHero",props:{name:{},text:{},tagline:{},image:{},actions:{}},setup(e){const t=te("hero-image-slot-exists");return(n,a)=>(r(),d("div",{class:T(["VPHero",{"has-image":e.image||l(t)}])},[i("div",hn,[i("div",pn,[u(n.$slots,"home-hero-info-before",{},void 0,!0),u(n.$slots,"home-hero-info",{},()=>[i("h1",bn,[e.name?(r(),d("span",{key:0,innerHTML:e.name,class:"name clip"},null,8,gn)):h("",!0),e.text?(r(),d("span",{key:1,innerHTML:e.text,class:"text"},null,8,kn)):h("",!0)]),e.tagline?(r(),d("p",{key:0,innerHTML:e.tagline,class:"tagline"},null,8,_n)):h("",!0)],!0),u(n.$slots,"home-hero-info-after",{},void 0,!0),e.actions?(r(),d("div",$n,[(r(!0),d(N,null,A(e.actions,s=>(r(),d("div",{key:s.link,class:"action"},[b(vn,{tag:"a",size:"medium",theme:s.theme,text:s.text,href:s.link,target:s.target,rel:s.rel},null,8,["theme","text","href","target","rel"])]))),128))])):h("",!0),u(n.$slots,"home-hero-actions-after",{},void 0,!0)]),e.image||l(t)?(r(),d("div",yn,[i("div",Pn,[a[0]||(a[0]=i("div",{class:"image-bg"},null,-1)),u(n.$slots,"home-hero-image",{},()=>[e.image?(r(),g(Y,{key:0,class:"image-src",image:e.image},null,8,["image"])):h("",!0)],!0)])])):h("",!0)])],2))}}),Vn=k(Ln,[["__scopeId","data-v-4f9c455b"]]),Sn=p({__name:"VPHomeHero",setup(e){const{frontmatter:t}=L();return(n,a)=>l(t).hero?(r(),g(Vn,{key:0,class:"VPHomeHero",name:l(t).hero.name,text:l(t).hero.text,tagline:l(t).hero.tagline,image:l(t).hero.image,actions:l(t).hero.actions},{"home-hero-info-before":m(()=>[u(n.$slots,"home-hero-info-before")]),"home-hero-info":m(()=>[u(n.$slots,"home-hero-info")]),"home-hero-info-after":m(()=>[u(n.$slots,"home-hero-info-after")]),"home-hero-actions-after":m(()=>[u(n.$slots,"home-hero-actions-after")]),"home-hero-image":m(()=>[u(n.$slots,"home-hero-image")]),_:3},8,["name","text","tagline","image","actions"])):h("",!0)}}),wn={class:"box"},Tn={key:0,class:"icon"},xn=["innerHTML"],Nn=["innerHTML"],Mn=["innerHTML"],In={key:4,class:"link-text"},An={class:"link-text-value"},Cn=p({__name:"VPFeature",props:{icon:{},title:{},details:{},link:{},linkText:{},rel:{},target:{}},setup(e){return(t,n)=>(r(),g(F,{class:"VPFeature",href:e.link,rel:e.rel,target:e.target,"no-icon":!0,tag:e.link?"a":"div"},{default:m(()=>[i("article",wn,[typeof e.icon=="object"&&e.icon.wrap?(r(),d("div",Tn,[b(Y,{image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])])):typeof e.icon=="object"?(r(),g(Y,{key:1,image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])):e.icon?(r(),d("div",{key:2,class:"icon",innerHTML:e.icon},null,8,xn)):h("",!0),i("h2",{class:"title",innerHTML:e.title},null,8,Nn),e.details?(r(),d("p",{key:3,class:"details",innerHTML:e.details},null,8,Mn)):h("",!0),e.linkText?(r(),d("div",In,[i("p",An,[C(x(e.linkText)+" ",1),n[0]||(n[0]=i("span",{class:"vpi-arrow-right link-text-icon"},null,-1))])])):h("",!0)])]),_:1},8,["href","rel","target","tag"]))}}),En=k(Cn,[["__scopeId","data-v-a3976bdc"]]),Bn={key:0,class:"VPFeatures"},Hn={class:"container"},On={class:"items"},Fn=p({__name:"VPFeatures",props:{features:{}},setup(e){const t=e,n=y(()=>{const a=t.features.length;if(a){if(a===2)return"grid-2";if(a===3)return"grid-3";if(a%3===0)return"grid-6";if(a>3)return"grid-4"}else return});return(a,s)=>e.features?(r(),d("div",Bn,[i("div",Hn,[i("div",On,[(r(!0),d(N,null,A(e.features,c=>(r(),d("div",{key:c.title,class:T(["item",[n.value]])},[b(En,{icon:c.icon,title:c.title,details:c.details,link:c.link,"link-text":c.linkText,rel:c.rel,target:c.target},null,8,["icon","title","details","link","link-text","rel","target"])],2))),128))])])])):h("",!0)}}),Dn=k(Fn,[["__scopeId","data-v-a6181336"]]),jn=p({__name:"VPHomeFeatures",setup(e){const{frontmatter:t}=L();return(n,a)=>l(t).features?(r(),g(Dn,{key:0,class:"VPHomeFeatures",features:l(t).features},null,8,["features"])):h("",!0)}}),Un=p({__name:"VPHomeContent",setup(e){const{width:t}=Ge({initialWidth:0,includeScrollbar:!1});return(n,a)=>(r(),d("div",{class:"vp-doc container",style:Se(l(t)?{"--vp-offset":`calc(50% - ${l(t)/2}px)`}:{})},[u(n.$slots,"default",{},void 0,!0)],4))}}),Gn=k(Un,[["__scopeId","data-v-8e2d4988"]]),qn=p({__name:"VPHome",setup(e){const{frontmatter:t,theme:n}=L();return(a,s)=>{const c=K("Content");return r(),d("div",{class:T(["VPHome",{"external-link-icon-enabled":l(n).externalLinkIcon}])},[u(a.$slots,"home-hero-before",{},void 0,!0),b(Sn,null,{"home-hero-info-before":m(()=>[u(a.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":m(()=>[u(a.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":m(()=>[u(a.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":m(()=>[u(a.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":m(()=>[u(a.$slots,"home-hero-image",{},void 0,!0)]),_:3}),u(a.$slots,"home-hero-after",{},void 0,!0),u(a.$slots,"home-features-before",{},void 0,!0),b(jn),u(a.$slots,"home-features-after",{},void 0,!0),l(t).markdownStyles!==!1?(r(),g(Gn,{key:0},{default:m(()=>[b(c)]),_:1})):(r(),g(c,{key:1}))],2)}}}),zn=k(qn,[["__scopeId","data-v-8b561e3d"]]),Kn={},Wn={class:"VPPage"};function Rn(e,t){const n=K("Content");return r(),d("div",Wn,[u(e.$slots,"page-top"),b(n),u(e.$slots,"page-bottom")])}const Jn=k(Kn,[["render",Rn]]),Qn=p({__name:"VPContent",setup(e){const{page:t,frontmatter:n}=L(),{hasSidebar:a}=j();return(s,c)=>(r(),d("div",{class:T(["VPContent",{"has-sidebar":l(a),"is-home":l(n).layout==="home"}]),id:"VPContent"},[l(t).isNotFound?u(s.$slots,"not-found",{},()=>[b(ft)],!0,0):l(n).layout==="page"?(r(),g(Jn,{key:1},{"page-top":m(()=>[u(s.$slots,"page-top",{},void 0,!0)]),"page-bottom":m(()=>[u(s.$slots,"page-bottom",{},void 0,!0)]),_:3})):l(n).layout==="home"?(r(),g(zn,{key:2},{"home-hero-before":m(()=>[u(s.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":m(()=>[u(s.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":m(()=>[u(s.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":m(()=>[u(s.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":m(()=>[u(s.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":m(()=>[u(s.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":m(()=>[u(s.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":m(()=>[u(s.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":m(()=>[u(s.$slots,"home-features-after",{},void 0,!0)]),_:3})):l(n).layout&&l(n).layout!=="doc"?(r(),g(O(l(n).layout),{key:3})):(r(),g(un,{key:4},{"doc-top":m(()=>[u(s.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":m(()=>[u(s.$slots,"doc-bottom",{},void 0,!0)]),"doc-footer-before":m(()=>[u(s.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":m(()=>[u(s.$slots,"doc-before",{},void 0,!0)]),"doc-after":m(()=>[u(s.$slots,"doc-after",{},void 0,!0)]),"aside-top":m(()=>[u(s.$slots,"aside-top",{},void 0,!0)]),"aside-outline-before":m(()=>[u(s.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":m(()=>[u(s.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":m(()=>[u(s.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":m(()=>[u(s.$slots,"aside-ads-after",{},void 0,!0)]),"aside-bottom":m(()=>[u(s.$slots,"aside-bottom",{},void 0,!0)]),_:3}))],2))}}),Yn=k(Qn,[["__scopeId","data-v-1428d186"]]),Xn={class:"container"},Zn=["innerHTML"],ea=["innerHTML"],ta=p({__name:"VPFooter",setup(e){const{theme:t,frontmatter:n}=L(),{hasSidebar:a}=j();return(s,c)=>l(t).footer&&l(n).footer!==!1?(r(),d("footer",{key:0,class:T(["VPFooter",{"has-sidebar":l(a)}])},[i("div",Xn,[l(t).footer.message?(r(),d("p",{key:0,class:"message",innerHTML:l(t).footer.message},null,8,Zn)):h("",!0),l(t).footer.copyright?(r(),d("p",{key:1,class:"copyright",innerHTML:l(t).footer.copyright},null,8,ea)):h("",!0)])],2)):h("",!0)}}),na=k(ta,[["__scopeId","data-v-e315a0ad"]]);function aa(){const{theme:e,frontmatter:t}=L(),n=Le([]),a=y(()=>n.value.length>0);return Z(()=>{n.value=he(t.value.outline??e.value.outline)}),{headers:n,hasLocalNav:a}}const sa={class:"menu-text"},oa={class:"header"},ia={class:"outline"},ra=p({__name:"VPLocalNavOutlineDropdown",props:{headers:{},navHeight:{}},setup(e){const t=e,{theme:n}=L(),a=S(!1),s=S(0),c=S(),o=S();function v(_){var P;(P=c.value)!=null&&P.contains(_.target)||(a.value=!1)}D(a,_=>{if(_){document.addEventListener("click",v);return}document.removeEventListener("click",v)}),re("Escape",()=>{a.value=!1}),Z(()=>{a.value=!1});function f(){a.value=!a.value,s.value=window.innerHeight+Math.min(window.scrollY-t.navHeight,0)}function $(_){_.target.classList.contains("outline-link")&&(o.value&&(o.value.style.transition="none"),we(()=>{a.value=!1}))}function V(){a.value=!1,window.scrollTo({top:0,left:0,behavior:"smooth"})}return(_,P)=>(r(),d("div",{class:"VPLocalNavOutlineDropdown",style:Se({"--vp-vh":s.value+"px"}),ref_key:"main",ref:c},[e.headers.length>0?(r(),d("button",{key:0,onClick:f,class:T({open:a.value})},[i("span",sa,x(l(Ae)(l(n))),1),P[0]||(P[0]=i("span",{class:"vpi-chevron-right icon"},null,-1))],2)):(r(),d("button",{key:1,onClick:V},x(l(n).returnToTopLabel||"Return to top"),1)),b(de,{name:"flyout"},{default:m(()=>[a.value?(r(),d("div",{key:0,ref_key:"items",ref:o,class:"items",onClick:$},[i("div",oa,[i("a",{class:"top-link",href:"#",onClick:V},x(l(n).returnToTopLabel||"Return to top"),1)]),i("div",ia,[b(Ce,{headers:e.headers},null,8,["headers"])])],512)):h("",!0)]),_:1})],4))}}),la=k(ra,[["__scopeId","data-v-8a42e2b4"]]),ca={class:"container"},ua=["aria-expanded"],da={class:"menu-text"},va=p({__name:"VPLocalNav",props:{open:{type:Boolean}},emits:["open-menu"],setup(e){const{theme:t,frontmatter:n}=L(),{hasSidebar:a}=j(),{headers:s}=aa(),{y:c}=Te(),o=S(0);q(()=>{o.value=parseInt(getComputedStyle(document.documentElement).getPropertyValue("--vp-nav-height"))}),Z(()=>{s.value=he(n.value.outline??t.value.outline)});const v=y(()=>s.value.length===0),f=y(()=>v.value&&!a.value),$=y(()=>({VPLocalNav:!0,"has-sidebar":a.value,empty:v.value,fixed:f.value}));return(V,_)=>l(n).layout!=="home"&&(!f.value||l(c)>=o.value)?(r(),d("div",{key:0,class:T($.value)},[i("div",ca,[l(a)?(r(),d("button",{key:0,class:"menu","aria-expanded":e.open,"aria-controls":"VPSidebarNav",onClick:_[0]||(_[0]=P=>V.$emit("open-menu"))},[_[1]||(_[1]=i("span",{class:"vpi-align-left menu-icon"},null,-1)),i("span",da,x(l(t).sidebarMenuLabel||"Menu"),1)],8,ua)):h("",!0),b(la,{headers:l(s),navHeight:o.value},null,8,["headers","navHeight"])])],2)):h("",!0)}}),fa=k(va,[["__scopeId","data-v-a6f0e41e"]]);function ma(){const e=S(!1);function t(){e.value=!0,window.addEventListener("resize",s)}function n(){e.value=!1,window.removeEventListener("resize",s)}function a(){e.value?n():t()}function s(){window.outerWidth>=768&&n()}const c=ee();return D(()=>c.path,n),{isScreenOpen:e,openScreen:t,closeScreen:n,toggleScreen:a}}const ha={},pa={class:"VPSwitch",type:"button",role:"switch"},ba={class:"check"},ga={key:0,class:"icon"};function ka(e,t){return r(),d("button",pa,[i("span",ba,[e.$slots.default?(r(),d("span",ga,[u(e.$slots,"default",{},void 0,!0)])):h("",!0)])])}const _a=k(ha,[["render",ka],["__scopeId","data-v-1d5665e3"]]),$a=p({__name:"VPSwitchAppearance",setup(e){const{isDark:t,theme:n}=L(),a=te("toggle-appearance",()=>{t.value=!t.value}),s=S("");return fe(()=>{s.value=t.value?n.value.lightModeSwitchTitle||"Switch to light theme":n.value.darkModeSwitchTitle||"Switch to dark theme"}),(c,o)=>(r(),g(_a,{title:s.value,class:"VPSwitchAppearance","aria-checked":l(t),onClick:l(a)},{default:m(()=>[...o[0]||(o[0]=[i("span",{class:"vpi-sun sun"},null,-1),i("span",{class:"vpi-moon moon"},null,-1)])]),_:1},8,["title","aria-checked","onClick"]))}}),pe=k($a,[["__scopeId","data-v-5337faa4"]]),ya={key:0,class:"VPNavBarAppearance"},Pa=p({__name:"VPNavBarAppearance",setup(e){const{site:t}=L();return(n,a)=>l(t).appearance&&l(t).appearance!=="force-dark"&&l(t).appearance!=="force-auto"?(r(),d("div",ya,[b(pe)])):h("",!0)}}),La=k(Pa,[["__scopeId","data-v-6c893767"]]),be=S();let Ee=!1,oe=0;function Va(e){const t=S(!1);if(R){!Ee&&Sa(),oe++;const n=D(be,a=>{var s,c,o;a===e.el.value||(s=e.el.value)!=null&&s.contains(a)?(t.value=!0,(c=e.onFocus)==null||c.call(e)):(t.value=!1,(o=e.onBlur)==null||o.call(e))});ve(()=>{n(),oe--,oe||wa()})}return qe(t)}function Sa(){document.addEventListener("focusin",Be),Ee=!0,be.value=document.activeElement}function wa(){document.removeEventListener("focusin",Be)}function Be(){be.value=document.activeElement}const Ta={class:"VPMenuLink"},xa=["innerHTML"],Na=p({__name:"VPMenuLink",props:{item:{}},setup(e){const{page:t}=L();return(n,a)=>(r(),d("div",Ta,[b(F,{class:T({active:l(z)(l(t).relativePath,e.item.activeMatch||e.item.link,!!e.item.activeMatch)}),href:e.item.link,target:e.item.target,rel:e.item.rel,"no-icon":e.item.noIcon},{default:m(()=>[i("span",{innerHTML:e.item.text},null,8,xa)]),_:1},8,["class","href","target","rel","no-icon"])]))}}),ne=k(Na,[["__scopeId","data-v-35975db6"]]),Ma={class:"VPMenuGroup"},Ia={key:0,class:"title"},Aa=p({__name:"VPMenuGroup",props:{text:{},items:{}},setup(e){return(t,n)=>(r(),d("div",Ma,[e.text?(r(),d("p",Ia,x(e.text),1)):h("",!0),(r(!0),d(N,null,A(e.items,a=>(r(),d(N,null,["link"in a?(r(),g(ne,{key:0,item:a},null,8,["item"])):h("",!0)],64))),256))]))}}),Ca=k(Aa,[["__scopeId","data-v-69e747b5"]]),Ea={class:"VPMenu"},Ba={key:0,class:"items"},Ha=p({__name:"VPMenu",props:{items:{}},setup(e){return(t,n)=>(r(),d("div",Ea,[e.items?(r(),d("div",Ba,[(r(!0),d(N,null,A(e.items,a=>(r(),d(N,{key:JSON.stringify(a)},["link"in a?(r(),g(ne,{key:0,item:a},null,8,["item"])):"component"in a?(r(),g(O(a.component),G({key:1,ref_for:!0},a.props),null,16)):(r(),g(Ca,{key:2,text:a.text,items:a.items},null,8,["text","items"]))],64))),128))])):h("",!0),u(t.$slots,"default",{},void 0,!0)]))}}),Oa=k(Ha,[["__scopeId","data-v-b98bc113"]]),Fa=["aria-expanded","aria-label"],Da={key:0,class:"text"},ja=["innerHTML"],Ua={key:1,class:"vpi-more-horizontal icon"},Ga={class:"menu"},qa=p({__name:"VPFlyout",props:{icon:{},button:{},label:{},items:{}},setup(e){const t=S(!1),n=S();Va({el:n,onBlur:a});function a(){t.value=!1}return(s,c)=>(r(),d("div",{class:"VPFlyout",ref_key:"el",ref:n,onMouseenter:c[1]||(c[1]=o=>t.value=!0),onMouseleave:c[2]||(c[2]=o=>t.value=!1)},[i("button",{type:"button",class:"button","aria-haspopup":"true","aria-expanded":t.value,"aria-label":e.label,onClick:c[0]||(c[0]=o=>t.value=!t.value)},[e.button||e.icon?(r(),d("span",Da,[e.icon?(r(),d("span",{key:0,class:T([e.icon,"option-icon"])},null,2)):h("",!0),e.button?(r(),d("span",{key:1,innerHTML:e.button},null,8,ja)):h("",!0),c[3]||(c[3]=i("span",{class:"vpi-chevron-down text-icon"},null,-1))])):(r(),d("span",Ua))],8,Fa),i("div",Ga,[b(Oa,{items:e.items},{default:m(()=>[u(s.$slots,"default",{},void 0,!0)]),_:3},8,["items"])])],544))}}),ge=k(qa,[["__scopeId","data-v-cf11d7a2"]]),za=["href","aria-label","innerHTML"],Ka=p({__name:"VPSocialLink",props:{icon:{},link:{},ariaLabel:{}},setup(e){const t=e,n=S();q(async()=>{var c;await we();const s=(c=n.value)==null?void 0:c.children[0];s instanceof HTMLElement&&s.className.startsWith("vpi-social-")&&(getComputedStyle(s).maskImage||getComputedStyle(s).webkitMaskImage)==="none"&&s.style.setProperty("--icon",`url('https://api.iconify.design/simple-icons/${t.icon}.svg')`)});const a=y(()=>typeof t.icon=="object"?t.icon.svg:`<span class="vpi-social-${t.icon}"></span>`);return(s,c)=>(r(),d("a",{ref_key:"el",ref:n,class:"VPSocialLink no-icon",href:e.link,"aria-label":e.ariaLabel??(typeof e.icon=="string"?e.icon:""),target:"_blank",rel:"noopener",innerHTML:a.value},null,8,za))}}),Wa=k(Ka,[["__scopeId","data-v-bd121fe5"]]),Ra={class:"VPSocialLinks"},Ja=p({__name:"VPSocialLinks",props:{links:{}},setup(e){return(t,n)=>(r(),d("div",Ra,[(r(!0),d(N,null,A(e.links,({link:a,icon:s,ariaLabel:c})=>(r(),g(Wa,{key:a,icon:s,link:a,ariaLabel:c},null,8,["icon","link","ariaLabel"]))),128))]))}}),ke=k(Ja,[["__scopeId","data-v-7bc22406"]]),Qa={key:0,class:"group translations"},Ya={class:"trans-title"},Xa={key:1,class:"group"},Za={class:"item appearance"},es={class:"label"},ts={class:"appearance-action"},ns={key:2,class:"group"},as={class:"item social-links"},ss=p({__name:"VPNavBarExtra",setup(e){const{site:t,theme:n}=L(),{localeLinks:a,currentLang:s}=J({correspondingLink:!0}),c=y(()=>a.value.length&&s.value.label||t.value.appearance||n.value.socialLinks);return(o,v)=>c.value?(r(),g(ge,{key:0,class:"VPNavBarExtra",label:"extra navigation"},{default:m(()=>[l(a).length&&l(s).label?(r(),d("div",Qa,[i("p",Ya,x(l(s).label),1),(r(!0),d(N,null,A(l(a),f=>(r(),g(ne,{key:f.link,item:f},null,8,["item"]))),128))])):h("",!0),l(t).appearance&&l(t).appearance!=="force-dark"&&l(t).appearance!=="force-auto"?(r(),d("div",Xa,[i("div",Za,[i("p",es,x(l(n).darkModeSwitchLabel||"Appearance"),1),i("div",ts,[b(pe)])])])):h("",!0),l(n).socialLinks?(r(),d("div",ns,[i("div",as,[b(ke,{class:"social-links-list",links:l(n).socialLinks},null,8,["links"])])])):h("",!0)]),_:1})):h("",!0)}}),os=k(ss,[["__scopeId","data-v-bb2aa2f0"]]),is=["aria-expanded"],rs=p({__name:"VPNavBarHamburger",props:{active:{type:Boolean}},emits:["click"],setup(e){return(t,n)=>(r(),d("button",{type:"button",class:T(["VPNavBarHamburger",{active:e.active}]),"aria-label":"mobile navigation","aria-expanded":e.active,"aria-controls":"VPNavScreen",onClick:n[0]||(n[0]=a=>t.$emit("click"))},[...n[1]||(n[1]=[i("span",{class:"container"},[i("span",{class:"top"}),i("span",{class:"middle"}),i("span",{class:"bottom"})],-1)])],10,is))}}),ls=k(rs,[["__scopeId","data-v-e5dd9c1c"]]),cs=["innerHTML"],us=p({__name:"VPNavBarMenuLink",props:{item:{}},setup(e){const{page:t}=L();return(n,a)=>(r(),g(F,{class:T({VPNavBarMenuLink:!0,active:l(z)(l(t).relativePath,e.item.activeMatch||e.item.link,!!e.item.activeMatch)}),href:e.item.link,target:e.item.target,rel:e.item.rel,"no-icon":e.item.noIcon,tabindex:"0"},{default:m(()=>[i("span",{innerHTML:e.item.text},null,8,cs)]),_:1},8,["class","href","target","rel","no-icon"]))}}),ds=k(us,[["__scopeId","data-v-e56f3d57"]]),vs=p({__name:"VPNavBarMenuGroup",props:{item:{}},setup(e){const t=e,{page:n}=L(),a=c=>"component"in c?!1:"link"in c?z(n.value.relativePath,c.link,!!t.item.activeMatch):c.items.some(a),s=y(()=>a(t.item));return(c,o)=>(r(),g(ge,{class:T({VPNavBarMenuGroup:!0,active:l(z)(l(n).relativePath,e.item.activeMatch,!!e.item.activeMatch)||s.value}),button:e.item.text,items:e.item.items},null,8,["class","button","items"]))}}),fs={key:0,"aria-labelledby":"main-nav-aria-label",class:"VPNavBarMenu"},ms=p({__name:"VPNavBarMenu",setup(e){const{theme:t}=L();return(n,a)=>l(t).nav?(r(),d("nav",fs,[a[0]||(a[0]=i("span",{id:"main-nav-aria-label",class:"visually-hidden"}," Main Navigation ",-1)),(r(!0),d(N,null,A(l(t).nav,s=>(r(),d(N,{key:JSON.stringify(s)},["link"in s?(r(),g(ds,{key:0,item:s},null,8,["item"])):"component"in s?(r(),g(O(s.component),G({key:1,ref_for:!0},s.props),null,16)):(r(),g(vs,{key:2,item:s},null,8,["item"]))],64))),128))])):h("",!0)}}),hs=k(ms,[["__scopeId","data-v-dc692963"]]);function ps(e){const{localeIndex:t,theme:n}=L();function a(s){var M,I,E;const c=s.split("."),o=(M=n.value.search)==null?void 0:M.options,v=o&&typeof o=="object",f=v&&((E=(I=o.locales)==null?void 0:I[t.value])==null?void 0:E.translations)||null,$=v&&o.translations||null;let V=f,_=$,P=e;const w=c.pop();for(const H of c){let U=null;const W=P==null?void 0:P[H];W&&(U=P=W);const ae=_==null?void 0:_[H];ae&&(U=_=ae);const se=V==null?void 0:V[H];se&&(U=V=se),W||(P=U),ae||(_=U),se||(V=U)}return(V==null?void 0:V[w])??(_==null?void 0:_[w])??(P==null?void 0:P[w])??""}return a}const bs=["aria-label"],gs={class:"DocSearch-Button-Container"},ks={class:"DocSearch-Button-Placeholder"},$e=p({__name:"VPNavBarSearchButton",setup(e){const n=ps({button:{buttonText:"Search",buttonAriaLabel:"Search"}});return(a,s)=>(r(),d("button",{type:"button",class:"DocSearch DocSearch-Button","aria-label":l(n)("button.buttonAriaLabel")},[i("span",gs,[s[0]||(s[0]=i("span",{class:"vp-icon DocSearch-Search-Icon"},null,-1)),i("span",ks,x(l(n)("button.buttonText")),1)]),s[1]||(s[1]=i("span",{class:"DocSearch-Button-Keys"},[i("kbd",{class:"DocSearch-Button-Key"}),i("kbd",{class:"DocSearch-Button-Key"},"K")],-1))],8,bs))}}),_s={class:"VPNavBarSearch"},$s={id:"local-search"},ys={key:1,id:"docsearch"},Ps=p({__name:"VPNavBarSearch",setup(e){const t=ze(()=>Ke(()=>import("./VPLocalSearchBox.yJbZbsEo.js"),__vite__mapDeps([0,1]))),n=()=>null,{theme:a}=L(),s=S(!1),c=S(!1);q(()=>{});function o(){s.value||(s.value=!0,setTimeout(v,16))}function v(){const _=new Event("keydown");_.key="k",_.metaKey=!0,window.dispatchEvent(_),setTimeout(()=>{document.querySelector(".DocSearch-Modal")||v()},16)}function f(_){const P=_.target,w=P.tagName;return P.isContentEditable||w==="INPUT"||w==="SELECT"||w==="TEXTAREA"}const $=S(!1);re("k",_=>{(_.ctrlKey||_.metaKey)&&(_.preventDefault(),$.value=!0)}),re("/",_=>{f(_)||(_.preventDefault(),$.value=!0)});const V="local";return(_,P)=>{var w;return r(),d("div",_s,[l(V)==="local"?(r(),d(N,{key:0},[$.value?(r(),g(l(t),{key:0,onClose:P[0]||(P[0]=M=>$.value=!1)})):h("",!0),i("div",$s,[b($e,{onClick:P[1]||(P[1]=M=>$.value=!0)})])],64)):l(V)==="algolia"?(r(),d(N,{key:1},[s.value?(r(),g(l(n),{key:0,algolia:((w=l(a).search)==null?void 0:w.options)??l(a).algolia,onVnodeBeforeMount:P[2]||(P[2]=M=>c.value=!0)},null,8,["algolia"])):h("",!0),c.value?h("",!0):(r(),d("div",ys,[b($e,{onClick:o})]))],64)):h("",!0)])}}}),Ls=p({__name:"VPNavBarSocialLinks",setup(e){const{theme:t}=L();return(n,a)=>l(t).socialLinks?(r(),g(ke,{key:0,class:"VPNavBarSocialLinks",links:l(t).socialLinks},null,8,["links"])):h("",!0)}}),Vs=k(Ls,[["__scopeId","data-v-0394ad82"]]),Ss=["href","rel","target"],ws=["innerHTML"],Ts={key:2},xs=p({__name:"VPNavBarTitle",setup(e){const{site:t,theme:n}=L(),{hasSidebar:a}=j(),{currentLang:s}=J(),c=y(()=>{var f;return typeof n.value.logoLink=="string"?n.value.logoLink:(f=n.value.logoLink)==null?void 0:f.link}),o=y(()=>{var f;return typeof n.value.logoLink=="string"||(f=n.value.logoLink)==null?void 0:f.rel}),v=y(()=>{var f;return typeof n.value.logoLink=="string"||(f=n.value.logoLink)==null?void 0:f.target});return(f,$)=>(r(),d("div",{class:T(["VPNavBarTitle",{"has-sidebar":l(a)}])},[i("a",{class:"title",href:c.value??l(me)(l(s).link),rel:o.value,target:v.value},[u(f.$slots,"nav-bar-title-before",{},void 0,!0),l(n).logo?(r(),g(Y,{key:0,class:"logo",image:l(n).logo},null,8,["image"])):h("",!0),l(n).siteTitle?(r(),d("span",{key:1,innerHTML:l(n).siteTitle},null,8,ws)):l(n).siteTitle===void 0?(r(),d("span",Ts,x(l(t).title),1)):h("",!0),u(f.$slots,"nav-bar-title-after",{},void 0,!0)],8,Ss)],2))}}),Ns=k(xs,[["__scopeId","data-v-1168a8e4"]]),Ms={class:"items"},Is={class:"title"},As=p({__name:"VPNavBarTranslations",setup(e){const{theme:t}=L(),{localeLinks:n,currentLang:a}=J({correspondingLink:!0});return(s,c)=>l(n).length&&l(a).label?(r(),g(ge,{key:0,class:"VPNavBarTranslations",icon:"vpi-languages",label:l(t).langMenuLabel||"Change language"},{default:m(()=>[i("div",Ms,[i("p",Is,x(l(a).label),1),(r(!0),d(N,null,A(l(n),o=>(r(),g(ne,{key:o.link,item:o},null,8,["item"]))),128))])]),_:1},8,["label"])):h("",!0)}}),Cs=k(As,[["__scopeId","data-v-88af2de4"]]),Es={class:"wrapper"},Bs={class:"container"},Hs={class:"title"},Os={class:"content"},Fs={class:"content-body"},Ds=p({__name:"VPNavBar",props:{isScreenOpen:{type:Boolean}},emits:["toggle-screen"],setup(e){const t=e,{y:n}=Te(),{hasSidebar:a}=j(),{frontmatter:s}=L(),c=S({});return fe(()=>{c.value={"has-sidebar":a.value,home:s.value.layout==="home",top:n.value===0,"screen-open":t.isScreenOpen}}),(o,v)=>(r(),d("div",{class:T(["VPNavBar",c.value])},[i("div",Es,[i("div",Bs,[i("div",Hs,[b(Ns,null,{"nav-bar-title-before":m(()=>[u(o.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":m(()=>[u(o.$slots,"nav-bar-title-after",{},void 0,!0)]),_:3})]),i("div",Os,[i("div",Fs,[u(o.$slots,"nav-bar-content-before",{},void 0,!0),b(Ps,{class:"search"}),b(hs,{class:"menu"}),b(Cs,{class:"translations"}),b(La,{class:"appearance"}),b(Vs,{class:"social-links"}),b(os,{class:"extra"}),u(o.$slots,"nav-bar-content-after",{},void 0,!0),b(ls,{class:"hamburger",active:e.isScreenOpen,onClick:v[0]||(v[0]=f=>o.$emit("toggle-screen"))},null,8,["active"])])])])]),v[1]||(v[1]=i("div",{class:"divider"},[i("div",{class:"divider-line"})],-1))],2))}}),js=k(Ds,[["__scopeId","data-v-6aa21345"]]),Us={key:0,class:"VPNavScreenAppearance"},Gs={class:"text"},qs=p({__name:"VPNavScreenAppearance",setup(e){const{site:t,theme:n}=L();return(a,s)=>l(t).appearance&&l(t).appearance!=="force-dark"&&l(t).appearance!=="force-auto"?(r(),d("div",Us,[i("p",Gs,x(l(n).darkModeSwitchLabel||"Appearance"),1),b(pe)])):h("",!0)}}),zs=k(qs,[["__scopeId","data-v-b44890b2"]]),Ks=["innerHTML"],Ws=p({__name:"VPNavScreenMenuLink",props:{item:{}},setup(e){const t=te("close-screen");return(n,a)=>(r(),g(F,{class:"VPNavScreenMenuLink",href:e.item.link,target:e.item.target,rel:e.item.rel,"no-icon":e.item.noIcon,onClick:l(t)},{default:m(()=>[i("span",{innerHTML:e.item.text},null,8,Ks)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),Rs=k(Ws,[["__scopeId","data-v-df37e6dd"]]),Js=["innerHTML"],Qs=p({__name:"VPNavScreenMenuGroupLink",props:{item:{}},setup(e){const t=te("close-screen");return(n,a)=>(r(),g(F,{class:"VPNavScreenMenuGroupLink",href:e.item.link,target:e.item.target,rel:e.item.rel,"no-icon":e.item.noIcon,onClick:l(t)},{default:m(()=>[i("span",{innerHTML:e.item.text},null,8,Js)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),He=k(Qs,[["__scopeId","data-v-3e9c20e4"]]),Ys={class:"VPNavScreenMenuGroupSection"},Xs={key:0,class:"title"},Zs=p({__name:"VPNavScreenMenuGroupSection",props:{text:{},items:{}},setup(e){return(t,n)=>(r(),d("div",Ys,[e.text?(r(),d("p",Xs,x(e.text),1)):h("",!0),(r(!0),d(N,null,A(e.items,a=>(r(),g(He,{key:a.text,item:a},null,8,["item"]))),128))]))}}),eo=k(Zs,[["__scopeId","data-v-8133b170"]]),to=["aria-controls","aria-expanded"],no=["innerHTML"],ao=["id"],so={key:0,class:"item"},oo={key:1,class:"item"},io={key:2,class:"group"},ro=p({__name:"VPNavScreenMenuGroup",props:{text:{},items:{}},setup(e){const t=e,n=S(!1),a=y(()=>`NavScreenGroup-${t.text.replace(" ","-").toLowerCase()}`);function s(){n.value=!n.value}return(c,o)=>(r(),d("div",{class:T(["VPNavScreenMenuGroup",{open:n.value}])},[i("button",{class:"button","aria-controls":a.value,"aria-expanded":n.value,onClick:s},[i("span",{class:"button-text",innerHTML:e.text},null,8,no),o[0]||(o[0]=i("span",{class:"vpi-plus button-icon"},null,-1))],8,to),i("div",{id:a.value,class:"items"},[(r(!0),d(N,null,A(e.items,v=>(r(),d(N,{key:JSON.stringify(v)},["link"in v?(r(),d("div",so,[b(He,{item:v},null,8,["item"])])):"component"in v?(r(),d("div",oo,[(r(),g(O(v.component),G({ref_for:!0},v.props,{"screen-menu":""}),null,16))])):(r(),d("div",io,[b(eo,{text:v.text,items:v.items},null,8,["text","items"])]))],64))),128))],8,ao)],2))}}),lo=k(ro,[["__scopeId","data-v-b9ab8c58"]]),co={key:0,class:"VPNavScreenMenu"},uo=p({__name:"VPNavScreenMenu",setup(e){const{theme:t}=L();return(n,a)=>l(t).nav?(r(),d("nav",co,[(r(!0),d(N,null,A(l(t).nav,s=>(r(),d(N,{key:JSON.stringify(s)},["link"in s?(r(),g(Rs,{key:0,item:s},null,8,["item"])):"component"in s?(r(),g(O(s.component),G({key:1,ref_for:!0},s.props,{"screen-menu":""}),null,16)):(r(),g(lo,{key:2,text:s.text||"",items:s.items},null,8,["text","items"]))],64))),128))])):h("",!0)}}),vo=p({__name:"VPNavScreenSocialLinks",setup(e){const{theme:t}=L();return(n,a)=>l(t).socialLinks?(r(),g(ke,{key:0,class:"VPNavScreenSocialLinks",links:l(t).socialLinks},null,8,["links"])):h("",!0)}}),fo={class:"list"},mo=p({__name:"VPNavScreenTranslations",setup(e){const{localeLinks:t,currentLang:n}=J({correspondingLink:!0}),a=S(!1);function s(){a.value=!a.value}return(c,o)=>l(t).length&&l(n).label?(r(),d("div",{key:0,class:T(["VPNavScreenTranslations",{open:a.value}])},[i("button",{class:"title",onClick:s},[o[0]||(o[0]=i("span",{class:"vpi-languages icon lang"},null,-1)),C(" "+x(l(n).label)+" ",1),o[1]||(o[1]=i("span",{class:"vpi-chevron-down icon chevron"},null,-1))]),i("ul",fo,[(r(!0),d(N,null,A(l(t),v=>(r(),d("li",{key:v.link,class:"item"},[b(F,{class:"link",href:v.link},{default:m(()=>[C(x(v.text),1)]),_:2},1032,["href"])]))),128))])],2)):h("",!0)}}),ho=k(mo,[["__scopeId","data-v-858fe1a4"]]),po={class:"container"},bo=p({__name:"VPNavScreen",props:{open:{type:Boolean}},setup(e){const t=S(null),n=xe(R?document.body:null);return(a,s)=>(r(),g(de,{name:"fade",onEnter:s[0]||(s[0]=c=>n.value=!0),onAfterLeave:s[1]||(s[1]=c=>n.value=!1)},{default:m(()=>[e.open?(r(),d("div",{key:0,class:"VPNavScreen",ref_key:"screen",ref:t,id:"VPNavScreen"},[i("div",po,[u(a.$slots,"nav-screen-content-before",{},void 0,!0),b(uo,{class:"menu"}),b(ho,{class:"translations"}),b(zs,{class:"appearance"}),b(vo,{class:"social-links"}),u(a.$slots,"nav-screen-content-after",{},void 0,!0)])],512)):h("",!0)]),_:3}))}}),go=k(bo,[["__scopeId","data-v-f2779853"]]),ko={key:0,class:"VPNav"},_o=p({__name:"VPNav",setup(e){const{isScreenOpen:t,closeScreen:n,toggleScreen:a}=ma(),{frontmatter:s}=L(),c=y(()=>s.value.navbar!==!1);return Ne("close-screen",n),X(()=>{R&&document.documentElement.classList.toggle("hide-nav",!c.value)}),(o,v)=>c.value?(r(),d("header",ko,[b(js,{"is-screen-open":l(t),onToggleScreen:l(a)},{"nav-bar-title-before":m(()=>[u(o.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":m(()=>[u(o.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":m(()=>[u(o.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":m(()=>[u(o.$slots,"nav-bar-content-after",{},void 0,!0)]),_:3},8,["is-screen-open","onToggleScreen"]),b(go,{open:l(t)},{"nav-screen-content-before":m(()=>[u(o.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":m(()=>[u(o.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3},8,["open"])])):h("",!0)}}),$o=k(_o,[["__scopeId","data-v-ae24b3ad"]]),yo=["role","tabindex"],Po={key:1,class:"items"},Lo=p({__name:"VPSidebarItem",props:{item:{},depth:{}},setup(e){const t=e,{collapsed:n,collapsible:a,isLink:s,isActiveLink:c,hasActiveLink:o,hasChildren:v,toggle:f}=bt(y(()=>t.item)),$=y(()=>v.value?"section":"div"),V=y(()=>s.value?"a":"div"),_=y(()=>v.value?t.depth+2===7?"p":`h${t.depth+2}`:"p"),P=y(()=>s.value?void 0:"button"),w=y(()=>[[`level-${t.depth}`],{collapsible:a.value},{collapsed:n.value},{"is-link":s.value},{"is-active":c.value},{"has-active":o.value}]);function M(E){"key"in E&&E.key!=="Enter"||!t.item.link&&f()}function I(){t.item.link&&f()}return(E,H)=>{const U=K("VPSidebarItem",!0);return r(),g(O($.value),{class:T(["VPSidebarItem",w.value])},{default:m(()=>[e.item.text?(r(),d("div",G({key:0,class:"item",role:P.value},We(e.item.items?{click:M,keydown:M}:{},!0),{tabindex:e.item.items&&0}),[H[1]||(H[1]=i("div",{class:"indicator"},null,-1)),e.item.link?(r(),g(F,{key:0,tag:V.value,class:"link",href:e.item.link,rel:e.item.rel,target:e.item.target},{default:m(()=>[(r(),g(O(_.value),{class:"text",innerHTML:e.item.text},null,8,["innerHTML"]))]),_:1},8,["tag","href","rel","target"])):(r(),g(O(_.value),{key:1,class:"text",innerHTML:e.item.text},null,8,["innerHTML"])),e.item.collapsed!=null&&e.item.items&&e.item.items.length?(r(),d("div",{key:2,class:"caret",role:"button","aria-label":"toggle section",onClick:I,onKeydown:Re(I,["enter"]),tabindex:"0"},[...H[0]||(H[0]=[i("span",{class:"vpi-chevron-right caret-icon"},null,-1)])],32)):h("",!0)],16,yo)):h("",!0),e.item.items&&e.item.items.length?(r(),d("div",Po,[e.depth<5?(r(!0),d(N,{key:0},A(e.item.items,W=>(r(),g(U,{key:W.text,item:W,depth:e.depth+1},null,8,["item","depth"]))),128)):h("",!0)])):h("",!0)]),_:1},8,["class"])}}}),Vo=k(Lo,[["__scopeId","data-v-b3fd67f8"]]),So=p({__name:"VPSidebarGroup",props:{items:{}},setup(e){const t=S(!0);let n=null;return q(()=>{n=setTimeout(()=>{n=null,t.value=!1},300)}),Je(()=>{n!=null&&(clearTimeout(n),n=null)}),(a,s)=>(r(!0),d(N,null,A(e.items,c=>(r(),d("div",{key:c.text,class:T(["group",{"no-transition":t.value}])},[b(Vo,{item:c,depth:0},null,8,["item"])],2))),128))}}),wo=k(So,[["__scopeId","data-v-c40bc020"]]),To={class:"nav",id:"VPSidebarNav","aria-labelledby":"sidebar-aria-label",tabindex:"-1"},xo=p({__name:"VPSidebar",props:{open:{type:Boolean}},setup(e){const{sidebarGroups:t,hasSidebar:n}=j(),a=e,s=S(null),c=xe(R?document.body:null);D([a,s],()=>{var v;a.open?(c.value=!0,(v=s.value)==null||v.focus()):c.value=!1},{immediate:!0,flush:"post"});const o=S(0);return D(t,()=>{o.value+=1},{deep:!0}),(v,f)=>l(n)?(r(),d("aside",{key:0,class:T(["VPSidebar",{open:e.open}]),ref_key:"navEl",ref:s,onClick:f[0]||(f[0]=Me(()=>{},["stop"]))},[f[2]||(f[2]=i("div",{class:"curtain"},null,-1)),i("nav",To,[f[1]||(f[1]=i("span",{class:"visually-hidden",id:"sidebar-aria-label"}," Sidebar Navigation ",-1)),u(v.$slots,"sidebar-nav-before",{},void 0,!0),(r(),g(wo,{items:l(t),key:o.value},null,8,["items"])),u(v.$slots,"sidebar-nav-after",{},void 0,!0)])],2)):h("",!0)}}),No=k(xo,[["__scopeId","data-v-319d5ca6"]]),Mo=p({__name:"VPSkipLink",setup(e){const{theme:t}=L(),n=ee(),a=S();D(()=>n.path,()=>a.value.focus());function s({target:c}){const o=document.getElementById(decodeURIComponent(c.hash).slice(1));if(o){const v=()=>{o.removeAttribute("tabindex"),o.removeEventListener("blur",v)};o.setAttribute("tabindex","-1"),o.addEventListener("blur",v),o.focus(),window.scrollTo(0,0)}}return(c,o)=>(r(),d(N,null,[i("span",{ref_key:"backToTop",ref:a,tabindex:"-1"},null,512),i("a",{href:"#VPContent",class:"VPSkipLink visually-hidden",onClick:s},x(l(t).skipToContentLabel||"Skip to content"),1)],64))}}),Io=k(Mo,[["__scopeId","data-v-0b0ada53"]]),Ao=p({__name:"Layout",setup(e){const{isOpen:t,open:n,close:a}=j(),s=ee();D(()=>s.path,a),pt(t,a);const{frontmatter:c}=L(),o=Qe(),v=y(()=>!!o["home-hero-image"]);return Ne("hero-image-slot-exists",v),(f,$)=>{const V=K("Content");return l(c).layout!==!1?(r(),d("div",{key:0,class:T(["Layout",l(c).pageClass])},[u(f.$slots,"layout-top",{},void 0,!0),b(Io),b(at,{class:"backdrop",show:l(t),onClick:l(a)},null,8,["show","onClick"]),b($o,null,{"nav-bar-title-before":m(()=>[u(f.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":m(()=>[u(f.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":m(()=>[u(f.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":m(()=>[u(f.$slots,"nav-bar-content-after",{},void 0,!0)]),"nav-screen-content-before":m(()=>[u(f.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":m(()=>[u(f.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3}),b(fa,{open:l(t),onOpenMenu:l(n)},null,8,["open","onOpenMenu"]),b(No,{open:l(t)},{"sidebar-nav-before":m(()=>[u(f.$slots,"sidebar-nav-before",{},void 0,!0)]),"sidebar-nav-after":m(()=>[u(f.$slots,"sidebar-nav-after",{},void 0,!0)]),_:3},8,["open"]),b(Yn,null,{"page-top":m(()=>[u(f.$slots,"page-top",{},void 0,!0)]),"page-bottom":m(()=>[u(f.$slots,"page-bottom",{},void 0,!0)]),"not-found":m(()=>[u(f.$slots,"not-found",{},void 0,!0)]),"home-hero-before":m(()=>[u(f.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":m(()=>[u(f.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":m(()=>[u(f.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":m(()=>[u(f.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":m(()=>[u(f.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":m(()=>[u(f.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":m(()=>[u(f.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":m(()=>[u(f.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":m(()=>[u(f.$slots,"home-features-after",{},void 0,!0)]),"doc-footer-before":m(()=>[u(f.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":m(()=>[u(f.$slots,"doc-before",{},void 0,!0)]),"doc-after":m(()=>[u(f.$slots,"doc-after",{},void 0,!0)]),"doc-top":m(()=>[u(f.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":m(()=>[u(f.$slots,"doc-bottom",{},void 0,!0)]),"aside-top":m(()=>[u(f.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":m(()=>[u(f.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":m(()=>[u(f.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":m(()=>[u(f.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":m(()=>[u(f.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":m(()=>[u(f.$slots,"aside-ads-after",{},void 0,!0)]),_:3}),b(na),u(f.$slots,"layout-bottom",{},void 0,!0)],2)):(r(),g(V,{key:1}))}}}),Co=k(Ao,[["__scopeId","data-v-5d98c3a5"]]),Eo={Layout:Co,enhanceApp:({app:e})=>{e.component("Badge",et)}},Bo={id:"main-content",class:"tuning-landing"},Ho={class:"hero","aria-labelledby":"page-title"},Oo={class:"container hero-grid"},Fo={class:"hero-copy"},Do={class:"hero-actions"},jo=["href"],Uo={class:"known-term-row"},Go={class:"signal-board","aria-label":"Example symptom paths"},qo={class:"signal-list"},zo=["href"],Ko=["href"],Wo=["href"],Ro={class:"section",id:"symptoms","aria-labelledby":"symptoms-title"},Jo={class:"container"},Qo={class:"symptom-grid"},Yo={class:"symptom"},Xo=["href"],Zo={class:"symptom"},ei=["href"],ti={class:"symptom"},ni=["href"],ai={class:"symptom"},si=["href"],oi={class:"symptom"},ii=["href"],ri=p({__name:"TuningLanding",setup(e){const t=Ye(),n=S(""),a={memory:"memory-model","memory-model":"memory-model",spill:"memory-model","spark.memory.fraction":"memory-model",shuffle:"shuffle","spark.sql.shuffle.partitions":"shuffle",joins:"joins",join:"joins","spark.sql.autobroadcastjointhreshold":"joins",aqe:"aqe","spark.sql.adaptive.enabled":"aqe",config:"config"};function s(){const c=n.value.trim().toLowerCase(),o=a[c]||"config";t.go(B(`/tuning-reference/${o}`))}return(c,o)=>(r(),d("main",Bo,[i("section",Ho,[i("div",Oo,[i("div",Fo,[o[6]||(o[6]=i("p",{class:"eyebrow"},"Practical mechanics, stable references",-1)),o[7]||(o[7]=i("h1",{id:"page-title"},"Spark tuning reference",-1)),o[8]||(o[8]=i("p",{class:"hero-summary"}," Move from a job symptom to the Spark mechanics and settings that can change its outcome without treating a tuning guess as evidence. ",-1)),i("div",Do,[i("a",{class:"button button-primary",href:l(B)("/tuning-reference/intro")},[...o[1]||(o[1]=[C("Browse the full reference ",-1),i("span",{class:"arrow","aria-hidden":"true"},"→",-1)])],8,jo),o[5]||(o[5]=i("a",{class:"button button-secondary",href:"#symptoms"},"Choose a symptom",-1)),i("form",{class:"known-term",onSubmit:Me(s,["prevent"])},[o[3]||(o[3]=i("label",{for:"known-term-input"},"Know the term or config key?",-1)),i("div",Uo,[Xe(i("input",{id:"known-term-input","onUpdate:modelValue":o[0]||(o[0]=v=>n.value=v),name:"term",type:"search",list:"known-term-options",placeholder:"e.g. spark.sql.shuffle.partitions",autocomplete:"off"},null,512),[[Ze,n.value]]),o[2]||(o[2]=i("button",{class:"button button-secondary",type:"submit"},"Jump to topic",-1))]),o[4]||(o[4]=_e('<small data-v-5d65fdb9>Known routes: memory, shuffle, joins, AQE, and config. Other terms open the configuration reference.</small><datalist id="known-term-options" data-v-5d65fdb9><option value="memory" data-v-5d65fdb9></option><option value="shuffle" data-v-5d65fdb9></option><option value="joins" data-v-5d65fdb9></option><option value="AQE" data-v-5d65fdb9></option><option value="config" data-v-5d65fdb9></option><option value="spark.memory.fraction" data-v-5d65fdb9></option><option value="spark.sql.shuffle.partitions" data-v-5d65fdb9></option><option value="spark.sql.adaptive.enabled" data-v-5d65fdb9></option><option value="spark.sql.autoBroadcastJoinThreshold" data-v-5d65fdb9></option></datalist>',2))],32)])]),i("aside",Go,[o[12]||(o[12]=i("div",{class:"board-label"},"evidence → mechanic → lever",-1)),i("ul",qo,[i("li",null,[i("a",{class:"signal-link",href:l(B)("/tuning-reference/memory-model")},[...o[9]||(o[9]=[i("span",{class:"signal-marker","aria-hidden":"true"},null,-1),i("span",{class:"signal-title"},"Executor memory pressure",-1),i("span",{class:"signal-destination"},"#memory-model",-1)])],8,zo)]),i("li",null,[i("a",{class:"signal-link",href:l(B)("/tuning-reference/shuffle")},[...o[10]||(o[10]=[i("span",{class:"signal-marker","aria-hidden":"true"},null,-1),i("span",{class:"signal-title"},"Slow, wide stages",-1),i("span",{class:"signal-destination"},"#shuffle",-1)])],8,Ko)]),i("li",null,[i("a",{class:"signal-link",href:l(B)("/tuning-reference/joins")},[...o[11]||(o[11]=[i("span",{class:"signal-marker","aria-hidden":"true"},null,-1),i("span",{class:"signal-title"},"Join strategy drift",-1),i("span",{class:"signal-destination"},"#joins",-1)])],8,Wo)])])])])]),i("section",Ro,[i("div",Jo,[o[23]||(o[23]=i("div",{class:"section-heading"},[i("h2",{id:"symptoms-title"},"Symptoms, mapped to the right lever"),i("p",null,"Each path stays on the canonical reference document, so existing links and saved anchors continue to work."),i("p",{class:"decision-note"},[i("strong",null,"How to use this:"),C(" SparkForensics reads your Spark event log and surfaces evidence; this reference explains the mechanics and settings behind it. Diagnose the symptom, change one informed lever, then validate against the next run's evidence. Confirm behavior and defaults against your deployed Spark version and distribution. ")])],-1)),i("div",Qo,[i("article",Yo,[o[14]||(o[14]=i("div",null,[i("h3",null,"Spill or GC pressure"),i("p",null,"Trace executor memory, object lifetime, and the trade-offs behind changing memory settings.")],-1)),i("a",{href:l(B)("/tuning-reference/memory-model")},[...o[13]||(o[13]=[C("Open memory model ",-1),i("span",{"aria-hidden":"true"},"→",-1)])],8,Xo)]),i("article",Zo,[o[16]||(o[16]=i("div",null,[i("h3",null,"Shuffle-heavy stages"),i("p",null,"Understand write/read costs, partition size, and why moving data becomes the bottleneck.")],-1)),i("a",{href:l(B)("/tuning-reference/shuffle")},[...o[15]||(o[15]=[C("Open shuffle guide ",-1),i("span",{"aria-hidden":"true"},"→",-1)])],8,ei)]),i("article",ti,[o[18]||(o[18]=i("div",null,[i("h3",null,"Slow or skewed joins"),i("p",null,"Compare join mechanics before changing broadcast thresholds or reshaping a plan.")],-1)),i("a",{href:l(B)("/tuning-reference/joins")},[...o[17]||(o[17]=[C("Open join optimization ",-1),i("span",{"aria-hidden":"true"},"→",-1)])],8,ni)]),i("article",ai,[o[20]||(o[20]=i("div",null,[i("h3",null,"Plan adapts poorly"),i("p",null,"Use AQE's runtime choices as an inspectable mechanism, not a checkbox to enable blindly.")],-1)),i("a",{href:l(B)("/tuning-reference/aqe")},[...o[19]||(o[19]=[C("Open AQE guide ",-1),i("span",{"aria-hidden":"true"},"→",-1)])],8,si)]),i("article",oi,[o[22]||(o[22]=i("div",null,[i("h3",null,"Need the exact setting"),i("p",null,"Check a setting's scope, default, and interaction before applying it to a cluster.")],-1)),i("a",{href:l(B)("/tuning-reference/config")},[...o[21]||(o[21]=[C("Open config reference ",-1),i("span",{"aria-hidden":"true"},"→",-1)])],8,ii)])])])]),o[24]||(o[24]=_e('<section class="section workflow" aria-labelledby="workflow-title" data-v-5d65fdb9><div class="container workflow-grid" data-v-5d65fdb9><div class="workflow-copy" data-v-5d65fdb9><h2 id="workflow-title" data-v-5d65fdb9>Let SparkForensics surface the evidence. Use this reference to reason about it.</h2><p data-v-5d65fdb9>The two tools stay deliberately separate: SparkForensics reads the event log; this static reference explains the mechanics and settings behind what you find.</p></div><ol class="steps" data-v-5d65fdb9><li data-v-5d65fdb9><span class="step-label" data-v-5d65fdb9>Observe</span><div data-v-5d65fdb9><strong data-v-5d65fdb9>Read the stage or executor behavior</strong><span data-v-5d65fdb9>Start with the event log, metrics, and plan details Spark already produces.</span></div></li><li data-v-5d65fdb9><span class="step-label" data-v-5d65fdb9>Explain</span><div data-v-5d65fdb9><strong data-v-5d65fdb9>Name the mechanism at work</strong><span data-v-5d65fdb9>Use the stable reference anchors to connect symptoms to memory, shuffle, joins, AQE, or configuration.</span></div></li><li data-v-5d65fdb9><span class="step-label" data-v-5d65fdb9>Test</span><div data-v-5d65fdb9><strong data-v-5d65fdb9>Change one informed lever</strong><span data-v-5d65fdb9>Make a measured change, then compare the evidence again rather than carrying a tuning superstition forward.</span></div></li></ol></div></section>',1))]))}}),li=k(ri,[["__scopeId","data-v-5d65fdb9"]]),Oe="shuffle-works-theme";function ci(){try{const e=window.localStorage.getItem(Oe);return e==="light"||e==="dark"?e:null}catch{return null}}function ye(e){const t=e?"dark":"light";try{window.localStorage.setItem(Oe,t)}catch{}document.documentElement.dataset.theme=t}const di={extends:Eo,enhanceApp({app:e}){e.component("TuningLanding",li),R&&e.runWithContext(()=>{const{isDark:t}=Pe(),n=ci();n&&(t.value=n==="dark"),ye(t.value),D(t,o=>ye(o)),new MutationObserver(()=>{const o=document.documentElement.dataset.theme;if(o==="light"||o==="dark"){const v=o==="dark";t.value!==v&&(t.value=v)}}).observe(document.documentElement,{attributes:!0,attributeFilter:["data-theme"]});const s=()=>{document.querySelectorAll(".VPNavBarAppearance, .VPNavScreenAppearance, .VPNavBarExtra .appearance-action").forEach(o=>{o.hasAttribute("data-shuffle-page-controls")||o.setAttribute("data-shuffle-page-controls","")})};s(),new MutationObserver(s).observe(document.documentElement,{childList:!0,subtree:!0})})}};export{ps as c,di as t,L as u};
|
package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.B-OsL91z.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{_ as o,o as t,c as d,a5 as a}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"Board widgets","description":"","frontmatter":{},"headers":[],"relativePath":"contributor-guide/architecture/board-widgets.md","filePath":"contributor-guide/architecture/board-widgets.md"}'),i={name:"contributor-guide/architecture/board-widgets.md"};function c(n,e,r,s,l,g){return t(),d("div",null,[...e[0]||(e[0]=[a('<h1 id="board-widgets" tabindex="-1">Board widgets <a class="header-anchor" href="#board-widgets" aria-label="Permalink to "Board widgets""></a></h1><h2 id="board-widgets-beyond-the-fixed-six" tabindex="-1">Beyond the fixed six <a class="header-anchor" href="#board-widgets-beyond-the-fixed-six" aria-label="Permalink to "Beyond the fixed six {#board-widgets-beyond-the-fixed-six}""></a></h2><p>(Components below live in <code>src/view/widgets/</code>, one file per widget name, e.g. <code>JobFailures.tsx</code>, <code>MemoryUtilization.tsx</code>.)</p><p>Six extra cards render in the Findings tab's active grid alongside the fixed spec §5 six: Incomplete Run, Job Failures, Caching Opportunities, Config Audit, Plan Advisor, and Autoscaling Churn, all <code>region: 'action'</code> in <code>detector-registry.tsx</code>:</p><ul><li><p><strong>Incomplete Run</strong> (tag <code>INCMP</code>, <code>IncompleteRun.tsx</code>): app-level "the capture never finished" caveat (DETECTORS entry <code>incompleteRun</code>). Fires whenever <code>app.startTime</code> was observed but <code>app.endTime</code> was not, i.e. no <code>SparkListenerApplicationEnd</code> in the log: an in-flight job, a rotated-away log, or a capture cut short. Order 5, ahead of every other detector, so its card is first among the Findings tab's <code>action</code>-region active widgets when present. The Findings tab's recommendation rollup (rendered by <code>FixTheseFirst.tsx</code>) excludes <code>incompleteRun</code> from that list outright (see <a href="./widget-rendering.html#widget-rendering-order-fixed-spec-§5">Widget rendering order</a>): it's a pipeline-completeness caveat, not an addressable fix, so it never competes with other findings for a ranked slot there. Self-gates to <code>null</code> (no card in the DOM) once the run completed normally. Unrelated to <code>evidence-availability.ts</code>'s own <code>trustworthy</code> gate (see <a href="./worker-protocol.html#evidence-availability-contract-v1">Evidence-availability contract</a>): that ledger only downgrades <em>absence</em> conclusions for individual evidence categories, never becomes a <code>DETECTORS</code> finding itself, and this card does not read it. No <code>docAnchor</code> is set, the same deviation as Autoscaling Churn below: this is a tool-specific signal with no upstream <code>spark-tuning-reference</code> section.</p></li><li><p><strong>Job Failures</strong> (tag <code>JOBS</code>): app-level job-failure-rate rollup (DETECTORS entry <code>jobFailureRate</code> in <code>packages/core/src/detectors.ts</code>), computed from <code>app.jobs</code> (<code>SparkListenerJobEnd</code> results): ≥10% info, ≥30% warning, ≥50% critical. Complements the per-stage, task-level Failed Tasks card (tag <code>FAIL</code>).</p></li><li><p><strong>Caching Opportunities</strong> (tag <code>CACHE</code>, <code>CachingOpportunity.tsx</code>): app-level SQL relation-reuse detector (DETECTORS entry <code>cachingOpportunity</code>), computed from <code>ctx.sql</code>. It walks each execution's <code>planTree</code> and keys every scan by its stable pre-AQE identity via <code>scanRelationId</code> (<code>plan-summary.ts</code>): <code>parquet:<db.table></code>, <code>delta:<db.table></code>, <code>jdbc:<schema.table></code>. That dedupes relations within one execution (self-joins count once) and flags any relation scanned by <code>>= minExecutions</code> (2) distinct executions. Relation identity comes from the catalog-qualified scan name (nodeName, e.g. <code>Scan parquet spark_catalog.db.t</code>), not the <code>Location:</code> path, since Spark truncates that path at ~100 chars and points it at <code>_delta_log</code> for Delta tables; internal Delta-log metadata scans are dropped. Read bytes come from the scan's <code>size of files read</code> metric (<code>0</code>/unknown for JDBC). Findings carry <code>executionReuse</code> (<code>value</code> = distinct-execution count), <code>relation</code>, <code>format</code>, <code>executionIds</code>, <code>totalReadBytes</code>, and a size-aware <code>recommendation</code> (<code>confidence:'low'</code>, <code>validationRequired</code>). Renders nothing when clean (no card in the DOM). One row per relation: name + format badge, reuse count, <code>Data read</code> (<code>formatBytes</code>, em-dash when unknown), recommendation, sorted by <code>totalReadBytes</code> then reuse count descending. Rows reveal 6 initially, then up to 30 more per click. Pure-RDD-API apps (no SQL executions) produce no finding: a deliberate trade-off replacing the former RDD-lineage heuristic, which surfaced only internal query-engine RDDs on DataFrame/SQL workloads.</p><p>It also detects composite reuse. When the same join/union subtree (not just a leaf scan) recurs across <code>>= minExecutions</code> distinct SQL executions, the detector emits one <code>variant:'composite'</code> finding recommending caching the derived join/union result instead of two independent leaf-relation rows, and suppresses the leaf findings for relations fully covered by it. A relation reused beyond the composite's executions keeps a residual leaf finding for just the uncovered executions. Composite identity is structural: an anchor plan-shape fingerprint (<code>findCompositeCandidates</code>/<code>computePlanShapes</code>'s <code>opts.includeDetail</code> path, <code>detectors.ts</code>) folds the join/union node's own normalized <code>detail</code> (join type, columns, literals, with expr ids, <code>plan_id=</code>, codegen-stage numbers, and AQE's BuildLeft/BuildRight stripped, and commutative equality operands canonicalized) plus its children's detail-free shapes. Descendant nodes never contribute detail text, only structural shape, so a shared join reused across differently-filtered pre-join scans still matches (a deliberate trade-off) while a different join <em>condition</em> on the same tables does not collide. Nested composites (an inner join reused both standalone and inside an outer join) dedupe one level at a time: an inner composite fully covered by a qualifying outer composite's execution set is suppressed entirely, and a superset recomputes a residual finding over just its extra executions. Reuse via pure projection/aggregation with no join/union underneath stays leaf-level, not modeled as composite. <code>variant:'composite'</code> findings carry <code>operator</code> (<code>'join'</code>|<code>'union'</code>), <code>relations</code> (leaf relations under the composite, for display), and <code>format:'derived'</code> (a sentinel: composites have no scan storage format). They render a <code>JOIN</code>/<code>UNION</code> operator badge (styled distinct from the format badges) plus a low-confidence marker (<code>confidence:'low'</code>, <code>validationRequired</code>) in <code>CachingOpportunity.tsx</code>.</p></li><li><p><strong>Autoscaling Churn</strong> (tag <code>CHRN</code>, <code>AutoscalingChurn.tsx</code>): app-level short-lived-executor detector (DETECTORS entry <code>autoscalingChurn</code> in <code>packages/core/src/detectors.ts</code>; reuses the same <code>executorsAdded</code>/<code>executorsRemoved</code> matching logic as <code>utilization</code>). For each added executor, finds its matching removal event (falling back to <code>app.endTime</code> for an executor still alive when the log ends) and flags it short-lived if its lifetime is under 2 minutes (<code>thresholds.shortLivedMs</code>). Warns above 30% short-lived, escalates to critical above 60% (<code>confidence: 'low'</code>: these percentages are an unvalidated design-spike estimate, not yet checked against real autoscaling-heavy logs). Returns no finding below 5 total executors (noise floor) or when <code>app.endTime</code> is missing (truncated/still-running log). The widget itself is unchanged apart from a finding-driven verdict banner (impact dot + <code>CHRN</code> tag + recommendation) above its existing add/remove chart. The chart's muted scale-down bar coloring is untouched. No <code>docAnchor</code> is set yet: the upstream <code>shuffle-works/spark-tuning-reference</code> docs repo has no <code>bottleneck-autoscaling-churn</code> section (verified 2026-08-19), so the <code>CHRN</code> tag currently renders with no docs-panel link (a tracked follow-up).</p></li><li><p><strong>Config Audit</strong> (tag <code>CFG</code>): static Spark-config sanity findings derived from <code>app.config</code>/<code>app.resources</code> (parsed from <code>SparkListenerEnvironmentUpdate</code>): dynamic-allocation vs. shuffle-service mismatch, inverted/missing autoscaling bounds, non-Kryo serializer, low executor <code>memoryOverhead</code>. Computed outside the runtime bottleneck catalog: its findings live in the separate <code>configFindings</code> stream, not <code>catalog</code>. But <code>FixTheseFirst</code>/<code>Alerts</code> both merge <code>catalog</code> and <code>configFindings</code> (neither reads <code>region</code> any more), so a Config Audit finding is still eligible for a row in the Findings tab's recommendation rollup and still gets its own card in the active grid, alongside genuine bottleneck-catalog findings.</p></li><li><p><strong>Plan Advisor</strong> (tag <code>PLAN</code>): SQL-plan-level findings computed from <code>appModel.sql</code>'s resolved <code>planTree</code> (DETECTORS entries <code>duplicatePlanSubtree</code>, <code>smallFiles</code>, <code>broadcastSizing</code> in <code>packages/core/src/detectors.ts</code>): repeated plan subtrees (≥3 nodes, ≥2 occurrences, critical if the repeated root is an <code>Exchange</code>), small-files read/write (>100 files averaging ❤️ MiB), and broadcast-join sizing in both directions (missed- broadcast info finding, over-broadcast warning at >1 GB). Each of the four emitted types now renders as its own card (<code>DuplicatePlanSubtree.tsx</code>, <code>SmallFiles.tsx</code>, <code>UnderBroadcast.tsx</code>, <code>OverBroadcast.tsx</code>; the 2026-09 widget/finding-type 1:1 mapping redesign split what used to be one shared <code>PlanFindings.tsx</code> card): all four still share the <code>PLAN</code> tag and the same <code>--plan-aggregate</code> badge tint (<code>src/view/plan-finding-shared.ts</code>). Their <code>docAnchor</code>s (<code>#bottleneck-duplicate-plan-subtree</code>, <code>#bottleneck-small-files</code>, <code>#bottleneck-broadcast-sizing</code>) are not yet live in the vendored docs site, so <code>npm run update-docs</code> will abort until upstream <code>spark-tuning-reference</code> adds them.</p></li></ul><p><code>detector-registry.tsx</code>'s <code>REGISTRY</code> still carries <code>region: 'reference'</code> on four entries, each its own component now: <code>memoryUtilization</code> (<code>MemoryUtilization</code>), <code>utilization</code> (<code>ExecutorUtilization</code>), <code>cacheUtilization</code> (<code>CacheUtilization</code>), and <code>coreLocality</code> (<code>CoreUsageArea</code>). <code>broadcastSizing</code> is gone from <code>REGISTRY</code> entirely (the 2026-09 widget/finding-type 1:1 mapping redesign dropped it): it never backed a real <code>Finding</code> (the <code>broadcastSizing</code> <code>DETECTORS</code> entry only ever emits <code>underBroadcast</code>/<code>overBroadcast</code>, both <code>region: 'action'</code>, each now its own Plan Advisor card), so there is no key left for it to occupy or a clean-check line for it to render. Autoscaling Churn, the other executor-provisioning-lifecycle detector alongside <code>utilization</code>/<code>memoryUtilization</code>, was deliberately given <code>action</code> rather than <code>reference</code> (see "Beyond the fixed six" above).</p><p><code>region</code> decides one thing (see <a href="./widget-rendering.html#widget-rendering-order-fixed-spec-§5">Widget rendering order</a>): whether a widget always mounts. <code>isAlwaysMountedType()</code> flags exactly one of the four <code>reference</code>-region types: Core Usage by Locality. That one mounts unconditionally from <code>appModel</code> in its own small grid inside the Findings tab, regardless of finding state. Memory Utilization, Executor Utilization, and Cache Storage are <code>reference</code> too, but all three are excluded by product decision, not a component-sharing constraint (<code>ALWAYS_MOUNTED_EXCEPTIONS</code> in <code>src/view/detector-registry.tsx</code> carries <code>cacheUtilization</code>, <code>memoryUtilization</code>, and <code>utilization</code>): a clean run on any of them isn't evidence worth surfacing unconditionally, so each collapses to an ordinary <code>CleanCheckRow</code> like any other action-region type on a clean run. Every other <code>REGISTRY</code> widget still renders unconditionally as either an active card or a clean-check line, and <code>region</code> still sets <code>orderedWidgets()</code>'s sort order within the active grid (<code>action</code> components first, <code>reference</code> ones after). The Full app report tab is structural-only and reads no <code>REGISTRY</code> entry. So Core Usage by Locality, the one widget still tagged <code>region: 'reference'</code> and exempt from <code>ALWAYS_MOUNTED_EXCEPTIONS</code>, renders in the Findings tab's always-visible grid rather than inside a separate Reference region (Memory Utilization, Executor Utilization, and Cache Storage all render through the ordinary active/clean paths instead):</p><ul><li><strong>Memory Utilization</strong> (tag <code>MEM</code>): app-level card combining three sub-findings from the <code>memoryUtilization</code> DETECTORS entry (<code>packages/core/src/detectors.ts</code>): idle-cores rate (busy-core-time from the worker's run-aggregates sweep vs. peak-cores × wall-clock), per-executor memory bands (peak heap vs. allocated, gated on <code>spark.eventLog.logStageExecutorMetrics</code>: a distinct <code>dataUnavailable</code> finding renders when that config was off), and an unverified memory-waste model (<code>confidence:'low'</code>, a 1.5× buffer). The driver-memory half of the original spec is dropped: the worker only extracts <em>allocated</em> <code>spark.driver.memory</code>, never a driver actual-usage metric, so there is nothing to band against. The separate <code>utilization</code> DETECTORS entry (an <code>avgUtilization</code> info finding: active-executor-time fraction below 60%) has its own card, <strong>Executor Utilization</strong> (tag <code>UTIL</code>, <code>ExecutorUtilization.tsx</code>): a <code>reference</code>-region widget in its own right that renders only with an active finding, split out of this same combined widget in the 2026-09 widget/finding-type 1:1 mapping redesign (it used to render inline here; the former <code>ExecutorTimeline.tsx</code> never owned this type, despite the tag's letters).</li><li><strong>Cache Storage</strong> (tag <code>CSTOR</code>): app-level card driven by the <code>cacheUtilization</code> DETECTORS entry (<code>packages/core/src/detectors.ts</code>), evaluating two per-RDD proxies over <code>ctx.app.rddInfo</code> storage snapshots since Spark event logs carry no runtime block-access/read-count data: partial caching (<code>numCachedPartitions / numPartitions < 0.90</code>, <code>< 0.50</code> for the warning tier) and disk spillover for <code>MEMORY_AND_DISK*</code> RDDs (<code>diskSize / (memorySize + diskSize) > 0.15</code>, <code>> 0.40</code> for the warning tier; <code>DISK_ONLY</code> RDDs are never flagged). Every finding carries <code>confidence: 'medium'</code>, because the ratio is a point-in-time storage snapshot from stage-submission events, not a runtime read-count. The existing RDD table (ported from the legacy <code>src/widgets/cache-utilization.js</code> canvas widget) still renders unconditionally; flagged rows get an inline <code>CSTOR</code> tag next to the RDD name, and every flagged RDD's recommendation renders below the table, worst-first.</li><li><strong>Core Usage by Locality</strong> (tag <code>LOCAL</code>, <code>CoreUsageArea.tsx</code>): app-level non-local-task-ratio threshold (DETECTORS entry <code>coreLocality</code>; the other half of a wasted-cores-ratio check, the idle-core half already covered by Memory Utilization's <code>idleCores</code> variant above). Sums <code>RACK_LOCAL</code> + <code>ANY</code> task counts against total task count across every stage's <code>stage.localityStats</code> (pure reducer <code>computeCoreLocalityRatio</code>, <code>packages/core/src/core-locality-ratio.ts</code>). <code>NO_PREF</code> stays in the denominator only, since it's what shuffle-read stages legitimately report with no locality problem. Below 50 total tasks or below a 15% non-local ratio: no finding; 15%-35%: warning; >= 35%: critical (<code>confidence:'low'</code>, unvalidated design-spike thresholds, same convention as the memory-waste model above). The widget's always-rendered stacked-area chart (<code>packages/core/src/core-usage-locality.ts</code>) is unaffected. The finding only adds a threshold/impact-band section above it, a per-stage non-local breakdown below it (shown whenever any non-local tasks exist at all, independent of whether the aggregate crossed threshold), and a one-line cross-reference to Memory Utilization when its <code>idleCores</code> finding also fired (idle-core ratio itself is not duplicated here).</li></ul><p>Documented ALL-CAPS tag vocabulary: <code>SKEW</code>, <code>SHFL</code>, <code>SPILL</code>, <code>GC</code>, <code>COLD</code>, <code>UTIL</code>, <code>MEM</code> (Memory Utilization), <code>CSTOR</code> (Cache Storage), <code>LOCAL</code> (Core Usage by Locality), plus <code>FAIL</code> (Failed Tasks), <code>JOBS</code> (Job Failures), <code>CFG</code> (Config Audit), <code>PLAN</code> (Plan Advisor), <code>SFAIL</code> (stage failed outright), <code>PART</code> (partition sizing), <code>SLOW</code> (stage overall slowness), <code>SHAPE</code> (stage shape smells), <code>CACHE</code> (caching opportunity) and <code>CHRN</code> (autoscaling churn).</p><p>ETL Phase Attribution (<code>packages/core/src/etl-phases.ts</code> + <code>EtlPhases.tsx</code>), What-If Executor Scaling (<code>packages/core/src/scaling-sim.ts</code> + <code>ScalingSim.tsx</code>, design spike: makespan predictions are unvalidated and carry a Model Error indicator), and Compute Efficiency (<code>packages/core/src/efficiency-model.ts</code> + <code>EfficiencyModel.tsx</code>, design spike: driver-vs-executor waste split, two theoretical floors, and the §6 right-sizing copy) are main-thread report modules, not <code>DETECTORS</code> entries: descriptive lenses with no impact-band threshold, rendered unconditionally into the Full app report tab rather than participating in the bottleneck catalog. <code>packages/core/src/job-groups.ts</code> (<code>checkConcurrentJobGroups</code>) is likewise a report helper: it flags when concurrent SQL-execution job groups make wall-clock-based estimates unreliable, and both the scaling simulator and the efficiency model gate their precision on it, showing a caveat banner rather than suppressing the estimate.</p><h2 id="confidence-metadata" tabindex="-1">Confidence metadata <a class="header-anchor" href="#confidence-metadata" aria-label="Permalink to "Confidence metadata""></a></h2><p>Best-effort (non-deterministic) findings carry a <code>confidence</code> + <code>validationRequired</code> marker, rendered inline by each consuming widget (e.g. <code>Spill.tsx</code>, <code>DuplicatePlanSubtree.tsx</code>, <code>SmallFiles.tsx</code>, <code>UnderBroadcast.tsx</code>, <code>OverBroadcast.tsx</code>, <code>EfficiencyModel.tsx</code>) when <code>confidence</code> is present and not <code>'high'</code>. There is no shared helper: each widget renders its own muted "N confidence: verify" text, with the <code>title</code> attribute carrying <code>validationRequired</code> as a tooltip. Spill classification is <code>medium</code> when classified (skew/volume) and <code>low</code> when unclassified; plan-summary warnings are <code>low</code>. Deterministic detectors stay unmarked, treated as high confidence.</p><h2 id="visual-system" tabindex="-1">Visual system <a class="header-anchor" href="#visual-system" aria-label="Permalink to "Visual system""></a></h2><p>Tailwind CSS v4 + shadcn/ui (Base UI primitives). The design tokens (colors, including the telemetry-console dark palette) are defined as CSS variables in an <code>@theme inline</code> block and consumed via Tailwind utility classes; there is no more hand-authored BEM CSS. Theme polarity is unchanged from the legacy app: dark = bare <code>:root</code> (no attribute), light = <code>:root[data-theme="light"]</code>, toggled by <code>src/theme/ThemeProvider.tsx</code> and persisted to <code>localStorage</code>. <code>index.html</code>'s inline FOUC-prevention script still runs before React mounts. Mono is still the typeface for numerics (<code>--font-mono</code>), applied via a <code>.num</code>-equivalent Tailwind utility per call site rather than one global class. Charts are Recharts (<code>src/view/charts/ChartTheme.tsx</code>'s <code>CHART_COLORS</code>, read from the same CSS tokens) instead of Chart.js. React re-renders on theme toggle, so chart colors update live with the theme, unlike the old Chart.js canvases, which were built once per parse. <code>ChartFrame</code> can also expose the underlying rows through a toggleable accessible table and copy them to the clipboard as TSV.</p><h2 id="templating-xss-safe" tabindex="-1">Templating (XSS-safe) <a class="header-anchor" href="#templating-xss-safe" aria-label="Permalink to "Templating (XSS-safe)""></a></h2><p>JSX auto-escapes every interpolated value by default. The hand-rolled auto-escaping <code> html</code> `` tagged template (<code>src/widgets/utils.js</code>) and its <code>no-raw-innerhtml</code> guard test are gone, and no <code>.innerHTML</code> assignment is left anywhere in the view layer.</p><p>One Base UI-specific gotcha carried no equivalent in the legacy app: <code>WidgetCard.tsx</code> passes <code>aria-expanded={String(open) as 'true' | 'false'}</code> rather than the raw boolean, because Base UI's <code>Collapsible.Trigger</code> otherwise overrides a boolean <code>aria-expanded</code> prop with its own internal state via prop merging. The explicit <code>String()</code> cast keeps the rendered attribute in sync with this app's own <code>open</code> state.</p>',17)])])}const u=o(i,[["render",c]]);export{p as __pageData,u as default};
|
package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.B-OsL91z.lean.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{_ as o,o as t,c as d,a5 as a}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"Board widgets","description":"","frontmatter":{},"headers":[],"relativePath":"contributor-guide/architecture/board-widgets.md","filePath":"contributor-guide/architecture/board-widgets.md"}'),i={name:"contributor-guide/architecture/board-widgets.md"};function c(n,e,r,s,l,g){return t(),d("div",null,[...e[0]||(e[0]=[a("",17)])])}const u=o(i,[["render",c]]);export{p as __pageData,u as default};
|
package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.BOeH4d1J.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{_ as t,o,c as d,a5 as a}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"Detector contract","description":"","frontmatter":{},"headers":[],"relativePath":"contributor-guide/architecture/detector-contract.md","filePath":"contributor-guide/architecture/detector-contract.md"}'),c={name:"contributor-guide/architecture/detector-contract.md"};function s(i,e,r,n,l,h){return o(),d("div",null,[...e[0]||(e[0]=[a('<h1 id="detector-contract" tabindex="-1">Detector contract <a class="header-anchor" href="#detector-contract" aria-label="Permalink to "Detector contract""></a></h1><p><code>packages/core/src/detectors.ts</code> is the single source of Spark-optimization logic: one declarative <code>DETECTORS</code> entry per pattern, each carrying <code>type</code>, <code>scope</code> (<code>stage</code> / <code>app</code> / <code>config</code> / <code>sql</code>), <code>order</code>, <code>fixEffort</code>, a <code>thresholds</code> object, impactBand/copy, a <code>docAnchor</code>, and a co-located <code>detect()</code> method. Both consumers are thin loops over that array:</p><ul><li><p><code>packages/core/src/analyzer.ts</code>: <code>analyze()</code> runs every entry regardless of scope, skipping only <code>inScorecard:false</code> ones; <code>auditConfig()</code> separately runs the <code>scope:'config'</code> entries. The four <code>configAudit</code> entries stay out of the bottleneck catalog because each sets <code>inScorecard:false</code>, not because of <code>scope:'config'</code>: a future config-scope detector without that flag would run through <code>analyze()</code> too. Each finding is stamped with its entry's <code>docAnchor</code>.</p></li><li><p><code>src/view/detector-registry.tsx</code>: a <code>REGISTRY: Record<findingType, {component, region}></code> replaces <code>dashboard-renderer.js</code>'s <code>render:</code> bindings, one entry per emitted finding type. <code>orderedWidgets()</code> walks <code>DETECTORS</code> ascending by <code>order</code>, then sorts <code>action</code>-region components before <code>reference</code>-region ones. Every <code>finding.type</code> maps to its own component now (the 2026-09 widget/finding-type 1:1 mapping redesign split six components that used to multiplex several types each: <code>TaskSkew</code> into <code>Skew</code>/<code>StageShape</code>/<code>TinyTask</code>; <code>ShuffleIO</code> narrowed to <code>shuffle</code> only, plus a new <code>PartitionSizing</code>; <code>Failures</code> into <code>StageFailed</code>/ <code>TaskFailures</code>/<code>RetryWaste</code>; <code>ExecutorTimeline</code> into <code>SlowHost</code>/ <code>StageSlowness</code>/<code>Straggler</code>/<code>SpeculationWaste</code>/<code>ColdStart</code> (its non-finding-driven executor-count chart moved to <code>ExecutorCountChart</code>, a <code>ReferenceSection</code> tile, not a <code>REGISTRY</code> entry); <code>MemoryUtilization</code> narrowed to <code>memoryUtilization</code> only, plus a new <code>ExecutorUtilization</code> for <code>utilization</code>; <code>PlanFindings</code> into <code>DuplicatePlanSubtree</code>/<code>SmallFiles</code>/ <code>UnderBroadcast</code>/<code>OverBroadcast</code>, dropping the dead <code>broadcastSizing</code> key entirely). No two <code>REGISTRY</code> entries share a <code>component</code> value any more.</p><p>Each widget component receives the full catalog and self-gates when it has nothing to show, rendering <code>null</code> or a muted "no issue" card for the always-visible ones. <code>orderedWidgets()</code> itself has no empty/non-empty branching, since it iterates the static <code>DETECTORS</code> import, not the runtime <code>catalog</code>.</p></li></ul><p>Thresholds live only in each entry's <code>thresholds</code>; see <a href="#bottleneck-thresholds-spec-§4">Bottleneck thresholds</a>.</p><h2 id="confidence-disclosure" tabindex="-1">Confidence disclosure <a class="header-anchor" href="#confidence-disclosure" aria-label="Permalink to "Confidence disclosure""></a></h2><p>A <code>Detector</code> entry (or the <code>Finding</code> it returns) may carry <code>confidence: 'low' | 'medium' | 'high'</code> plus a <code>validationRequired</code> string. <code>RowStatusCluster</code> (<code>src/view/RowStatusCluster.tsx</code>) is the one place that renders it, gated to Advanced density: a plain "<confidence> confidence" badge whose tooltip carries the full <code>validationRequired</code> text. A finding with no <code>confidence</code> field renders identically to a fully-validated one, so every detector whose thresholds are our own unvalidated noise floor (marked <code>NOT SOURCED</code> in a code comment) should set both fields, not just the ones that happen to already have <code>RowStatusCluster</code> wired into their widget. <code>skew</code>, <code>straggler</code>, and <code>gc</code> set <code>confidence: 'low'</code> for exactly this reason: their runtime-floor thresholds carry the same kind of unvalidated-noise-floor caveat <code>coreLocality</code>, <code>autoscalingChurn</code>, and <code>memoryUtilization</code>'s <code>wasteModel</code> variant already disclose.</p><h2 id="the-fixeffort-field" tabindex="-1">The <code>fixEffort</code> field <a class="header-anchor" href="#the-fixeffort-field" aria-label="Permalink to "The `fixEffort` field""></a></h2><p>Each <code>Detector</code> entry also carries <code>fixEffort: 'config' | 'code' | 'rearchitect'</code>, alongside <code>order</code> and <code>thresholds</code>: a rough estimate of how much work resolving the finding takes.</p><p>No view currently reads it. The quadrant impact/effort bucketing this field was meant to feed (<code>bucketFinding</code>/<code>effortTier</code>/<code>computeImpactMagnitude</code> in a since-deleted <code>src/quadrant-bucket.ts</code>, gated behind a <code>FIX_EFFORT_MAPPING_REVIEWED</code> flag that never flipped to <code>true</code>) was removed as dead code in the recommendations-consolidation redesign: <code>FixTheseFirst</code> (<code>src/view/widgets/FixTheseFirst.tsx</code>) ranks purely by impact magnitude. See <a href="./widget-rendering.html#widget-rendering-order-fixed-spec-§5">Widget rendering order</a> for how it ranks findings today.</p><p>Two shared helpers back multiple detectors and reports. <code>packages/core/src/plan-tree-walk.ts</code>'s <code>walkPlanTree(root, visit, {dedupe})</code> is the iterative pre-order plan-tree traversal used by <code>detectors.ts</code> and every <code>plan-*.ts</code> module (<code>plan-summary.ts</code>, <code>plan-duration-attribution.ts</code>, <code>plan-node-detail.ts</code>, <code>plan-dot.ts</code>). <code>packages/core/src/core-count.ts</code>'s <code>computeTotalCores(app, executorsAdded)</code> is the shared core-count logic used by <code>efficiency-model.ts</code>, <code>scaling-sim.ts</code>, and <code>wasted-core-hours.ts</code>. <code>detectors.ts</code>'s own <code>utilization</code> and <code>memoryUtilization</code> entries use the same file's <code>computePeakConcurrentCores</code>/<code>computePeakConcurrentExecutorCount</code> instead: <code>computeTotalCores</code> sums every <code>ExecutorAdded</code> event with no regard for overlap, so under executor churn (spot preemption, <code>dynamicAllocation</code> replacement) it double-counts a churned executor's capacity against its replacement's; the peak-concurrent sweeps don't.</p><h2 id="cross-detector-suppression" tabindex="-1">Cross-detector suppression <a class="header-anchor" href="#cross-detector-suppression" aria-label="Permalink to "Cross-detector suppression""></a></h2><p>An entry may declare an optional <code>suppressWhen(finding, out)</code> method. <code>analyzer.ts</code>'s <code>push()</code>, the single choke point every finding passes through, calls it per-finding, after the null guard and before the push, and drops the finding silently when it returns <code>true</code>. <code>out</code> is the findings accumulated so far. Since <code>analyze()</code>'s loop is detector-outer / stage-inner, every finding from a detector declared earlier in <code>DETECTORS</code> is already in <code>out</code> by the time a later detector runs, for every stage. That makes the pattern purely declaration-order-driven: the suppressing detector must be declared earlier in the <code>DETECTORS</code> array than the suppressed one.</p><p><code>stageSlowness</code> uses this to defer to <code>slowHost</code>. It is spliced immediately after the <code>slowHost</code> entry regardless of its <code>order</code> field (<code>order</code> only controls render sequencing, not evaluation order), and <code>tests/analyzer.test.js</code>'s "detector contract" suite asserts the array-index ordering so a future reorder can't silently break the suppression. The mechanism is deliberately minimal: a same-array, predicate-in-<code>push()</code> filter, not a general dependency graph. <code>auditConfig()</code>'s own <code>push()</code> call is unaffected, since <code>scope:'config'</code> entries declare no <code>suppressWhen</code>.</p><h2 id="per-operator-duration-attribution" tabindex="-1">Per-operator duration attribution <a class="header-anchor" href="#per-operator-duration-attribution" aria-label="Permalink to "Per-operator duration attribution""></a></h2><p><code>packages/core/src/plan-duration-attribution.ts</code> (entry <code>attributeStageDurationToPlan(planTree, stagesById, sqlExec)</code>) approximates how a SQL execution's stage wall-time splits across plan operators, returning a <code>Map<planNode, milliseconds></code>. It cuts the plan tree at Exchange boundaries into connected components: since the Exchange write/read split (<code>resolvePlanTree</code> in <code>event-handlers.ts</code> always synthesizes a <code>read</code> node wrapping a <code>write</code> node for every raw <code>Exchange</code>/<code>BroadcastExchange</code>), the cut is keyed off <code>PlanNode.exchangeRole === 'read'</code> on the parent, not a name regex: the write half starts the new component, the read half stays in its parent's. A node with no <code>exchangeRole</code> at all (for example <code>ReusedExchange</code>, which is never split) never starts a new component on its own, unlike the old name-based regex, which matched any Exchange-family name regardless of split state. Each component receives a stable pre-order identity and separate parent/depth/traversal metadata; the identity itself does not encode its count of Exchange ancestors. It zips components deepest-first by that explicit depth against submission-ordered stage IDs, then apportions each matched stage's wall-time across that component's nodes by timing-metric weight, falling back to an even split when no node carries a timing metric.</p><p>This is best-effort inference, not measurement. Spark's event model exposes no ground truth for per-operator time within a stage; the Exchange-boundary segmentation and deepest-component-to-earliest-stage zip are heuristics. Treat the per-operator numbers as directional hints, never as authoritative timings, and do not build hard thresholds or findings on top of them.</p><h2 id="stage-id-attribution-for-plan-advisor-findings" tabindex="-1">Stage-ID attribution for Plan Advisor findings <a class="header-anchor" href="#stage-id-attribution-for-plan-advisor-findings" aria-label="Permalink to "Stage-ID attribution for Plan Advisor findings""></a></h2><p>The Plan Advisor detectors (<code>duplicatePlanSubtree</code>, <code>smallFiles</code>, <code>broadcastSizing</code> in <code>packages/core/src/detectors.ts</code>) each attribute their finding to a narrowed <code>stageIds</code> set rather than the whole SQL execution: <code>PlanNode.stageIds</code> is resolved once per plan tree at parse time by unioning, per node, every metric's accumulator ID against a <code>taskAccumStages: Map<accumulatorId, Set<stageId>></code> built while parsing <code>TaskEnd</code> events, then clipping the result to the execution's own stage set. An accumulator ID occasionally points to a <em>different</em> execution's stages, e.g. a <code>ReusedSubquery</code> computed once and reused verbatim, and the clip prevents misattributing that other execution's work. Each detector unions its implicated node(s)' <code>stageIds</code> and falls back to the execution-wide set only when no implicated node has any coverage; a finding never partially blends a narrowed set with the execution-wide one. When an execution has no stage universe at all (no jobs ever recorded against it, which is true for 42% of real-log SQL executions with a plan tree, typically job-less/driver-only executions), the clip drops every candidate stage ID instead of passing them through: every node in that execution's tree ends up with no <code>stageIds</code> anywhere, same "coverage is partial" framing as below. (An earlier version of this clip treated "no stage universe" as "no clip," which let a foreign accumulator ID collision, e.g. the <code>ReusedSubquery</code> case above, leak another execution's stages into a job-less execution's nodes; the clip is now unconditional on <code>executionStageIds</code> being present.)</p><p>Coverage is partial by Spark's own design: whole-stage-codegen wrapper nodes (<code>InputAdapter</code>, and other purely structural passthrough markers) carry no accumulators at all, and <code>BroadcastExchangeExec</code>'s own metrics are computed entirely on the driver and never appear on any <code>TaskEnd</code> (real Spark behavior). Since the Exchange write/read split, those driver-computed metrics live specifically on the synthesized <em>write</em> half (<code>exchangeRole: 'write'</code>); the <em>read</em> half always carries <code>metrics: []</code>. The write half's immediate child, which does carry executor-side metrics, is unioned in instead, see <code>overBroadcast</code>'s wiring. A <code>TaskEnd</code> arriving after its stage has already been finalized is also silently excluded from <code>taskAccumStages</code>, consistent with the parser's existing out-of-order tolerance elsewhere.</p><p><code>planTree</code> itself is kept current against Spark's adaptive query execution (AQE) re-plans: <code>SparkListenerSQLAdaptiveExecutionUpdate</code> events overwrite the execution's <code>sparkPlanInfo</code>/<code>physicalPlanDescription</code> last-write-wins, so accumulator-ID evidence is matched against the plan that actually ran rather than a stale pre-AQE snapshot.</p><p>No eviction/pruning is added to <code>taskAccumStages</code>, a deliberate choice, not an oversight: measured on real logs, it holds roughly 1,050 keys per compressed MB (9,850 keys on an 11.6 MB fixture, about 29,000 keys on a 28.1 MB fixture). Extrapolated to a 240MB+ log, the scale this tool targets (see <code>CLAUDE.md</code>), that is roughly 250,000 keys, around 45 MB of heap for an equivalent synthetic <code>Map<number, Set<number>></code>. This heap estimate is still small relative to this tool's other in-memory state. It is higher, though, than the fixture-only measurements taken when this mechanism was built suggested.</p><p>Per-execution pruning (e.g. dropping a <code>taskAccumStages</code> entry once its stage finalizes or its owning SQL execution resolves, mirroring how <code>accumState</code> is cleared in <code>endSqlExecution</code>) is deliberately not done either: unlike <code>accumState</code>, <code>taskAccumStages</code> is one global, un-scoped map read by every execution's <code>resolvePlanTree</code> call, and the <code>ReusedSubquery</code> case above depends on a stage recorded under one execution still being visible when a later execution resolves. Pruning on any single execution's lifecycle would break that cross-execution lookup. What is bounded is the growth from a single pathological event: <code>TaskEndEventSchema</code>'s <code>Accumulables</code> array is capped at <code>MAX_ACCUMULABLES_PER_TASK</code> (10,000, <code>event-schemas.ts</code>), well above any real plan's per-task metric count, so a single crafted <code>TaskEnd</code> can't grow the map past that per-event bound; a <code>TaskEnd</code> exceeding it fails schema validation and the line is skipped (counted in <code>skippedLines</code>) like any other malformed event.</p><h2 id="bottleneck-thresholds-spec-§4" tabindex="-1">Bottleneck thresholds (spec §4) <a class="header-anchor" href="#bottleneck-thresholds-spec-§4" aria-label="Permalink to "Bottleneck thresholds (spec §4)""></a></h2><p>Every change to <code>packages/core/src/detectors.ts</code> should reference this table.</p><p>Every finding's <code>impactBand</code> comes from one of two places. For any finding whose <code>impactEstimate</code> carries a <code>wallClock</code> estimate (the common case for most rules below), <code>analyzer.ts</code> calls <code>deriveImpactBand()</code> (<code>packages/core/src/impact-band.ts</code>) immediately after <code>estimateImpact()</code>, which sets <code>.impactBand</code> purely from <code>wallClock.high</code> as a fraction of the app's total duration (<code>>= 2%</code> critical, <code>>= 0.5%</code> warning, else info: the same <code>floorPctWarn</code>/<code>floorPctCrit</code> values <code>skew</code>/<code>straggler</code> use for their own thresholds below). For those rules, the table below documents their firing gate plus their fixed fallback constant, which surfaces only when this run's finding of that type didn't get a wallClock estimate (a stage excluded from the occupancy sweep). For rules whose finding type never gets a wallClock estimate (<code>resourceOnly</code>/<code>informational</code> basis, e.g. <code>configAudit</code>, or a rule that keeps its own ratio-tiered classification per the design's Decision 2, e.g. <code>failures</code>), the full threshold table below is the real, displayed classification: <code>detectors.ts</code> sets <code>impactBand</code> directly and nothing overwrites it. <code>partitionSizing</code>'s <code>maxPartitionTooBig</code> rule is a third case: it does carry a <code>wallClock</code> estimate but is explicitly exempted in <code>deriveImpactBand()</code> because it's a hardcoded-critical OOM/crash-risk safety signal, not a time-recovery one, so <code>detectors.ts</code>'s own classification stands regardless of how small that estimate is relative to the run.</p><h3 id="fixed-fallback-only-usually-wallclock-derived-instead" tabindex="-1">Fixed fallback only (usually wallClock-derived instead) <a class="header-anchor" href="#fixed-fallback-only-usually-wallclock-derived-instead" aria-label="Permalink to "Fixed fallback only (usually wallClock-derived instead)""></a></h3><p>These rules' <em>band</em> tiers were deleted from <code>packages/core/src/detectors.ts</code> (they were always overwritten by <code>deriveImpactBand</code> whenever a wallClock estimate was available); the constant in the last column is only a floor-case fallback. Their <em>firing</em> gate is untouched and still lives in each entry's <code>thresholds</code> object: it decides whether the rule reports anything, so it stays documented here in full.</p><table tabindex="0"><thead><tr><th>Rule</th><th>Fires when</th><th>Fallback</th></tr></thead><tbody><tr><td>Task skew</td><td><code>taskDurationP95 / taskDurationP50 > 3×</code> (<code>taskDurationMax / P50</code> for stages under <code>minTasksForP95</code> = 20 tasks), <strong>and</strong> the occupancy-clipped P95−P50 (or max−P50) delta is ≥ <code>floorPctWarn</code> = 0.5% of app runtime</td><td><code>warning</code></td></tr><tr><td>Shuffle read</td><td><code>shuffleReadBytes > minBytes</code> = 50 MiB</td><td><code>info</code></td></tr><tr><td>Partition sizing: skew</td><td><code>shuffleReadMax > 5×</code> <code>shuffleReadP50</code> <strong>and</strong> <code>shuffleReadMax > 256 MiB</code></td><td><code>warning</code></td></tr><tr><td>Partition sizing: low parallelism</td><td><code>shuffleReadBytes ≥ 1 GiB</code> <strong>and</strong> <code>taskCount ≤ 7</code></td><td><code>warning</code></td></tr><tr><td>Partition sizing: oversized partition</td><td><code>shuffleReadMax ≥ 5 GiB</code></td><td><code>critical</code></td></tr><tr><td>GC</td><td><code>executorRunTime ≥ minRunTimeMs</code> = 10 s <strong>and</strong> <code>gcPct > 10%</code></td><td><code>warning</code></td></tr><tr><td>GC (low / cost)</td><td><code>executorRunTime ≥ 10 s</code> <strong>and</strong> <code>gcPct < lowInfoPct100</code> = 5% (checked only when the GC row above did not fire)</td><td><code>info</code></td></tr><tr><td>Spill (magnitude v2)</td><td>any non-zero <code>memoryBytesSpilled</code>. The magnitude sub-table below classifies <em>how much</em>, but does not gate firing</td><td><code>warning</code></td></tr><tr><td>Cold start</td><td><code>firstStageSubmittedAt − app.startTime > gapSeconds</code> = 30 s</td><td><code>warning</code></td></tr><tr><td>Slow host: mean-duration ratio</td><td>stage has ≥ <code>minHosts</code> = 3 hosts (or executors) and ≥ <code>minTasks</code> = 15 tasks; then per host: mean task duration / overall median ≥ <code>ratioWarn</code> = 2.0× <strong>and</strong> host task-share ≥ <code>minShare</code> = 20% <strong>and</strong> host mean ≥ <code>floorMs</code> = 1000 ms (absolute-magnitude floor, rules out sub-second noise)</td><td><code>warning</code></td></tr><tr><td>Slow host: duration-share</td><td>same stage gate as the row above; then per host: ≥ <code>shareWarn</code> = 75% of the stage's total task-duration <strong>and</strong> ≥ <code>taskShareWarn</code> = 50% of its task count</td><td><code>warning</code></td></tr><tr><td>Stage slowness: absolute fallback, suppressed when <code>slowHost</code> already fired</td><td>stage wall-clock duration ≥ <code>infoMin</code> = 15 min</td><td><code>info</code></td></tr><tr><td>Straggler / speculative-execution</td><td><code>taskCount ≥ minTasks</code> = 10, <strong>and</strong> either any speculative task ran <strong>or</strong> straggler share > <code>shareWarn</code> = 5%. <code>warnPct</code>/<code>critPct</code> (10%/20% speculative share) and <code>floorPctWarn</code>/<code>floorPctCrit</code> (0.5%/2% of app runtime) no longer set the band; they rank the straggler-vs-speculative tiers that pick which <em>metric</em> the finding reports</td><td><code>info</code></td></tr><tr><td>Speculation waste (new)</td><td><code>speculationWastedAttempts ≥ minWasted</code> = 5 <strong>and</strong> <code>speculationWasteMs ≥ minWasteMs</code> = 60 s</td><td><code>warning</code></td></tr><tr><td>Retry waste</td><td><code>wastedAttempts ≥ minWasted</code> = 3 <strong>and</strong> <code>retryWasteMs ≥ minWasteMs</code> = 30 s, on a stage that still completed</td><td><code>warning</code></td></tr><tr><td>Tiny tasks</td><td><code>taskCount ≥ minTasks</code> = 100 <strong>and</strong> <code>taskDurationP50 ≤ maxP50</code> = 500 ms <strong>and</strong> <code>taskDurationP95 ≤ maxP95</code> = 1000 ms</td><td><code>info</code></td></tr><tr><td>Duplicate plan subtree</td><td>a subtree of ≥ <code>minSubtreeSize</code> = 3 nodes whose shape fingerprint repeats ≥ <code>minOccurrences</code> = 2× in the plan</td><td><code>warning</code></td></tr><tr><td>Small files read/write</td><td>per read/write side: file count > <code>minFiles</code> = 100 <strong>and</strong> average file size < <code>maxAvgFileSizeMB</code> = 3 MiB</td><td><code>warning</code></td></tr><tr><td>Broadcast sizing: missed</td><td>a 2-child <code>SortMergeJoin</code> whose smaller side is < 10 MiB (unconditional), or < 100 MiB with the larger side > 10 GiB, or < 1 GiB with larger > 300 GiB, or < 5 GiB with larger > 1 TiB (<code>broadcastTiers</code> × <code>comparisonTiers</code>)</td><td><code>info</code></td></tr><tr><td>Broadcast sizing: oversized</td><td>a <code>BroadcastExchange</code> node whose <code>data size</code> metric > <code>overBroadcastBytes</code> = 1 GiB</td><td><code>warning</code></td></tr></tbody></table><h4 id="spill-magnitude-tiers" tabindex="-1">Spill magnitude tiers <a class="header-anchor" href="#spill-magnitude-tiers" aria-label="Permalink to "Spill magnitude tiers""></a></h4><p>The spill row's band is a fixed <code>warning</code> fallback, but <code>computeSpillMagnitude</code> (<code>packages/core/src/detectors.ts</code>) still runs on every spill finding and sets its <code>spillMagnitude</code> field, which the Spill widget displays. Its tiers, in evaluation order (first match wins, <code>null</code> when nothing matches):</p><table tabindex="0"><thead><tr><th>Condition</th><th>Magnitude</th></tr></thead><tbody><tr><td>Single-task stage: <code>spillDiskMax ≥ singleTaskDiskGiB</code> = 1 GiB <strong>or</strong> <code>spillMemMax ≥ singleTaskMemGiB</code> = 4 GiB</td><td><code>severe</code></td></tr><tr><td>Multi-task stage: <code>spillDiskMax ≥ highDiskGiB</code> = 1 GiB, <strong>or</strong> <code>spillDiskMax / taskCount ≥ highTaskDiskMB</code> = 512 MiB (per-task proxy), <strong>or</strong> <code>spillMemMax ≥ highMemGiB</code> = 4 GiB</td><td><code>high</code></td></tr><tr><td>Multi-task stage: <code>spillDiskMax ≥ medDiskMB</code> = 256 MiB <strong>or</strong> <code>spillMemMax ≥ medMemGiB</code> = 1 GiB</td><td><code>medium</code></td></tr><tr><td>Skew (<code>taskCount ≥ skewMinTasks</code> = 10): <code>spillDiskMax / spillDiskP50 > skewRatio</code> = 5× <strong>and</strong> <code>spillDiskMax ≥ skewDiskFloorMB</code> = 128 MiB</td><td><code>high</code></td></tr><tr><td>Skew (<code>taskCount ≥ 10</code>): <code>spillMemMax / spillMemP50 > 5×</code> <strong>and</strong> <code>spillMemMax ≥ skewMemFloorMB</code> = 256 MiB</td><td><code>medium</code></td></tr></tbody></table><p>Disk spill is weighted worse than memory spill by design: the memory thresholds sit well above their disk counterparts at every tier.</p><h3 id="full-threshold-table-never-wallclock-derived" tabindex="-1">Full threshold table (never wallClock-derived) <a class="header-anchor" href="#full-threshold-table-never-wallclock-derived" aria-label="Permalink to "Full threshold table (never wallClock-derived)""></a></h3><table tabindex="0"><thead><tr><th>Rule</th><th>Warning</th><th>Critical</th></tr></thead><tbody><tr><td>Stage shape: PRatio</td><td><code>taskCount / totalCores < 0.5</code> (info, under-parallelized)</td><td>none</td></tr><tr><td>Stage shape: OIRatio</td><td><code>outputBytes / inputBytes > 10×</code> (info, data explosion)</td><td>none</td></tr><tr><td>Stage shape: TaskStageSkew</td><td><code>taskDurationMax / stageDuration > 3×</code> (info)</td><td>none</td></tr><tr><td>Failed tasks</td><td>failure rate > 5% (min 10 tasks)</td><td>> 20%</td></tr><tr><td>Stage failed outright</td><td>none</td><td>any <code>stageFailureReason</code> present</td></tr><tr><td>Slow host: multi-dimensional</td><td>max/median ratio across taskTime/inputBytes/shuffleBytes/storageMemory ≥ 1.33× (info); each dimension's sample must also clear an absolute floor (1000 ms for taskTime, 64 MiB for the byte dimensions)</td><td>≥ 3.16× warning, ≥ 10× critical</td></tr><tr><td>Utilization</td><td>avg active executors / peak < 60% (info)</td><td>none</td></tr><tr><td>Autoscaling churn: short-lived executors (design spike, unvalidated thresholds)</td><td>> 30% of executors alive under 2 min (min 5 executors)</td><td>> 60%</td></tr><tr><td>Job failure rate</td><td>≥ 30% (≥ 10% info)</td><td>≥ 50%</td></tr><tr><td>Idle cores</td><td>busy-core-time / (peak cores × wall-clock) idle > 50% (warning)</td><td>none</td></tr><tr><td>Memory band</td><td>peak heap / allocated > 95% too-small (warning); < 70% over-provisioned (info)</td><td>none</td></tr><tr><td>Caching opportunity</td><td>RDD read across ≥3 stages without <code>.persist()</code></td><td>none (single tier, info)</td></tr><tr><td>Cache utilization: partial caching (this repo)</td><td><code>numCachedPartitions / numPartitions < 0.90</code> (info)</td><td><code>< 0.50</code> (warning)</td></tr><tr><td>Cache utilization: disk spillover (this repo)</td><td><code>diskSize / (memorySize + diskSize) > 0.15</code> (info), <code>MEMORY_AND_DISK*</code> only</td><td><code>> 0.40</code> (warning)</td></tr></tbody></table><p>Spill classification: ≥80% tasks with zero spill → <code>skew</code>; <20% zero → <code>volume</code>; else <code>unclassified</code>. The classification badge is always shown in both compact and expanded spill widget states. This is independent of the magnitude tiers above: classification says <em>what kind</em> of spill, magnitude says <em>how much</em>.</p><h4 id="evidence-fields-stagefailed-retrywaste" tabindex="-1">Evidence fields (<code>stageFailed</code> / <code>retryWaste</code>) <a class="header-anchor" href="#evidence-fields-stagefailed-retrywaste" aria-label="Permalink to "Evidence fields (`stageFailed` / `retryWaste`)""></a></h4><p>Neither entry's <code>detect()</code> used to put anything beyond a scalar <code>metric</code>/ <code>value</code> on its finding. Both now also attach:</p><ul><li><code>numTasks</code>: <code>stage.taskCount</code> at detection time.</li><li><code>memoryBytesSpilled</code>: <code>stage.memoryBytesSpilled</code> at detection time.</li><li><code>stageFailed</code> only: <code>failedTaskDetails</code>: up to 20 <code>FailedTaskSample</code> records (<code>taskId</code>, <code>attemptNumber</code>, <code>host</code>, <code>executorId</code>, <code>reason</code>, <code>peakExecMem</code>, <code>memSpilled</code>, <code>shuffleWrite</code>) for tasks still marked failed when the stage was finalized (<code>finalizeStage</code>, <code>stage-quantiles.ts</code>).</li><li><code>retryWaste</code> only: <code>retriedTaskDetails</code>: up to 20 <code>FailedTaskSample</code> records for attempts discarded by the retry-dedup logic in <code>accumulateTask</code> (<code>event-handlers.ts</code>): captured at the moment they'd otherwise be thrown away, since by finalize time only the winning attempt survives.</li></ul><p>Both sample arrays are capped at 20 entries, filled in first-encountered order (finalize order for <code>failedTaskDetails</code>, discard order for <code>retriedTaskDetails</code>), not spread across distinct hosts/executors: a stage with failures clustered on one bad host could fill the cap before a more informative failure elsewhere in the stage is ever sampled. This is the first evidence-shape documentation in this file; no other finding type has one yet.</p>',39)])])}const g=t(c,[["render",s]]);export{p as __pageData,g as default};
|
package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.m3S3UdMk.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{_ as t,o,c as a,a5 as s}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"State and History Server intake","description":"","frontmatter":{},"headers":[],"relativePath":"contributor-guide/architecture/state-and-history.md","filePath":"contributor-guide/architecture/state-and-history.md"}'),d={name:"contributor-guide/architecture/state-and-history.md"};function r(n,e,i,c,l,h){return o(),a("div",null,[...e[0]||(e[0]=[s('<h1 id="state-and-history-server-intake" tabindex="-1">State and History Server intake <a class="header-anchor" href="#state-and-history-server-intake" aria-label="Permalink to "State and History Server intake""></a></h1><h2 id="state-model" tabindex="-1">State model <a class="header-anchor" href="#state-model" aria-label="Permalink to "State model""></a></h2><p><code>src/store/store.ts</code> is a single Zustand store (<code>createStore</code> from <code>zustand/vanilla</code>, wrapped by a <code>useStore</code> hook). It holds all shared state; no component keeps a <code>useState</code> of its own for anything shared: <code>appModel: AppModel</code>, <code>catalog: Finding[]</code>, <code>activeFileId</code>, <code>sessionCache</code> (in-memory snapshot cache for instant file-switching, see <code>session-snapshot.ts</code>), <code>taskDataCache</code>, <code>parse: {pct, lines, etaMs}</code>, <code>status: 'idle'|'parsing'|'ready'|'error'</code>, <code>errorMessage</code>, <code>theme</code>, <code>skippedLines</code> (malformed-JSON-line count from the parser's <code>done</code> payload).</p><p><code>src/store/useIngest.ts</code> is the only writer during a parse. It builds <code>createModelCallbacks</code>' <code>onProgress</code>/<code>onDone</code>/<code>onError</code> handlers to call the store's setters directly (<code>setParse</code>, <code>setCatalog</code>, <code>setStatus</code>, <code>setSkippedLines</code>, ...). Components never talk to the worker: they use <code>useIngest()</code>'s returned actions (<code>startLoad</code>/<code>startLoadFolder</code>/<code>startLoadFromUrl</code>/<code>pickRecent</code>/<code>getTaskData</code>/ <code>resetToDropZone</code>) and the store's read state.</p><p><code>resetModel()</code> empties <code>appModel</code>/<code>catalog</code>/<code>taskDataCache</code>/<code>skippedLines</code> on every new parse or reset-to-drop-zone, and bumps <code>modelResetCount</code>. That counter has no setter of its own; only <code>PlanGraphRoute.tsx</code>'s <code>store.subscribe</code> reads it, to evict the plan-graph model memo cache (see <a href="./drill-down.html#plan-graph-view">Plan graph view</a>).</p><h3 id="finding-filter-state" tabindex="-1">Finding filter state <a class="header-anchor" href="#finding-filter-state" aria-label="Permalink to "Finding filter state""></a></h3><p>The board-wide finding filter (impact band, raw <code>finding.type</code>, stage) lives outside the Zustand store, in <code>FindingFilterContext</code> (<code>src/view/FindingFilterContext.tsx</code>): a <code>createContext</code>+<code>useState</code><code>FilterSelection</code> (<code>src/view/finding-filter.ts</code>, three <code>Set</code>s) that every widget filters <code>catalog</code> through via <code>filterFindings</code>. It is seeded from the URL's <code>impact</code>/<code>type</code>/<code>stage</code> query params on mount and nowhere else, so a reload with no params gives the unfiltered board. Every change writes those params back with <code>history.replaceState</code>, never <code>push</code>, so filtering doesn't grow Back history. A <code>popstate</code> listener re-seeds the selection from the URL, so Back and forward re-apply filters.</p><p>Switching files resets the selection to empty, keyed on the stable file id rather than <code>catalog</code>, so a same-file catalog refresh keeps the active filter. A page reload re-reads the address bar, so deep-linked filtered URLs still restore. Filters are seeded only from the URL and never persisted anywhere else (no localStorage, no restore-as-default): a filtered view silently becoming the default on reload would risk hiding findings from a user who didn't realize a filter was still active, so a plain reload with no filter params is always the unfiltered board.</p><h3 id="recent-files-vs-session-cache" tabindex="-1">Recent files vs. session cache <a class="header-anchor" href="#recent-files-vs-session-cache" aria-label="Permalink to "Recent files vs. session cache""></a></h3><p>Two mechanisms cover reopening a file, at different lifetimes.</p><p><code>sessionCache</code> (in the Zustand store, see <a href="#state-model">State model</a>) is in-memory and per-session: it makes switching between files already loaded in the current tab instant, and it is gone on reload.</p><p>Recent files (<code>src/recent-files.ts</code>, consumed by <code>src/view/useRecentFiles.ts</code>) is IndexedDB-backed and cross-session. It persists each file's <code>FileSystemFileHandle</code> plus light metadata (name, size, <code>lastModified</code>, app name, issue count, <code>lastOpenedAt</code>), capped at 10 entries with the oldest evicted past the cap, so a file can be reopened after a full browser restart, pending the browser re-granting permission on the handle. Picking a recent entry re-parses from the handle; no parsed model is ever persisted.</p><h2 id="run-comparison" tabindex="-1">Run comparison <a class="header-anchor" href="#run-comparison" aria-label="Permalink to "Run comparison""></a></h2><p><code>src/run-comparison.ts</code> is the whole A/B engine. The entry point <code>compareRuns(baseline, candidate)</code> takes two <code>{ label, snapshot }</code> run records (each <code>snapshot</code> a normalized model: <code>app</code>, <code>stages</code>, <code>sql</code>, <code>catalog</code>, <code>executors</code>) and returns one plain object the view renders. It runs on already-parsed snapshots, with no worker involved.</p><ul><li><code>stageIdentity(stage, snapshot)</code> is a run-independent key: <code>normalizeStageName</code> (lowercased, digit-runs and long hex ids collapsed to <code>#</code>) joined with the stage's SQL-execution plan identity. That identity is scoped to only the plan nodes this stage's tasks were attributed to (<code>node.stageIds</code>), not the whole tree, so two stages sharing one SQL execution (e.g. a self-join's two Exchange stages) don't collapse onto one identity; it falls back to a bottom-up structural fingerprint of the whole resolved <code>planTree</code> (<code>planTreeIdentity</code>, <code>normalizeDetail</code>-normalized: the same normalizer <code>cachingOpportunity</code> uses in <code>src/detectors.ts</code>) when a stage has no such attribution. <code>matchStages(baseSnap, candSnap)</code> indexes each run by that identity and pairs identities that map to exactly one stage on both sides. An identity colliding equally on both sides (same count) is also paired, positionally by sorted stage id: exact when comparing a run against itself (every stage matches itself), a best-effort guess otherwise (two unrelated same-named stages with no SQL/attribution could get cross-paired). Collisions are still recorded in <code>collisionIdentities</code> even when resolved this way; a differing count leaves them there unpaired. <code>coverage</code> reports the matched fraction.</li><li><code>metricDeltas</code> computes whole-run aggregate deltas (wall-clock, spill, task skew p95, failed-task rate, GC, I/O bytes, executor count, ...) as plain sums over all stages, deliberately not gated on stage matching, since matching is unreliable on real logs. Each metric carries a <code>direction</code> (improvement/regression/unchanged) and an <code>unavailableReason</code> when a side lacks the field.</li><li><code>findingsDelta</code> tallies each run's <code>catalog</code> by <code>(rule × impact band)</code> and reports <code>introduced</code> vs <code>resolved</code> categories: a count diff, also matching-free.</li><li>Only the per-stage skew deltas (<code>stageSkewDeltas</code>) and the pinned-stage panel consume the <code>matchStages</code> pairs, so low match coverage degrades those two surfaces without invalidating the aggregate deltas.</li></ul><p><code>compareRuns</code> also flags <code>confidence: 'low'</code> when the two app names differ (a weak signal, not a hard gate). The view lives in <code>src/view/RunComparison.tsx</code> (the comparison page), <code>CompareLanding.tsx</code> (the two-slot Run A / Run B intake off the landing), and <code>PinnedStageDeltas.tsx</code> (the manual per-stage pinning panel fed by <code>baseStages</code>/<code>candStages</code>).</p><h2 id="history-server-intake-and-recovery" tabindex="-1">History Server intake and recovery <a class="header-anchor" href="#history-server-intake-and-recovery" aria-label="Permalink to "History Server intake and recovery""></a></h2><p><code>DropZone</code> keeps the History Server disclosure, Base URL, Application ID, optional Attempt ID, validation/touched state, and recoverable SHS error in mounted React state rather than Zustand. The local browser-first path is still the default: <strong>Choose file</strong> loads a single event log, while <strong>Choose rolling-log folder</strong> accepts only an <code>eventlog_v2_*</code> directory and directs a rejected folder back to the file picker.</p><p>The collapsed <strong>Fetch from Spark History Server</strong> disclosure requires local-server mode, a reachable History Server, and a supported base application ID: <code>application_<timestamp>_<id></code>, <code>local-<timestamp></code>, or <code>app-<identifier></code>. <code>server/lib/shs-request.js</code> trims and validates the three request fields, accepts only absolute credential-free <code>http:</code>/<code>https:</code> base URLs without a query or fragment, preserves a reverse-proxy path prefix, and canonicalizes the base URL to one trailing slash. The optional attempt is a separate path-safe identifier; neither identifier can contain a path separator. The shared helper builds the encoded <code>/shs-proxy</code> request and the encoded <code>api/v1/applications/<app>[/<attempt>]/logs</code> upstream path from that normalized object only.</p><p>The optional Node server is loopback-only: a narrow CORS proxy, not a general or hosted proxy. It validates the same request contract, sends no credentials, follows no upstream redirects, and returns only stable safe error codes. It never forwards upstream response text, status details, locations, or credentials to the browser.</p><p>Routing preserves the recovery boundary: local file and folder failures use the existing page-level error route. A typed SHS failure instead resets the model to idle and returns to the still-mounted, expanded History Server disclosure, which retains its values and shows safe recovery guidance along with local file intake. During an SHS parse, the mounted intake shows progress in place; successful completion follows the normal dashboard route.</p>',21)])])}const g=t(d,[["render",r]]);export{u as __pageData,g as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{_ as t,o,c as a,a5 as s}from"./chunks/framework.DSg0KOwT.js";const u=JSON.parse('{"title":"State and History Server intake","description":"","frontmatter":{},"headers":[],"relativePath":"contributor-guide/architecture/state-and-history.md","filePath":"contributor-guide/architecture/state-and-history.md"}'),d={name:"contributor-guide/architecture/state-and-history.md"};function r(n,e,i,c,l,h){return o(),a("div",null,[...e[0]||(e[0]=[s("",21)])])}const g=t(d,[["render",r]]);export{u as __pageData,g as default};
|
package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.DbqPf2OT.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{_ as o,o as t,c as d,a5 as a}from"./chunks/framework.DSg0KOwT.js";const p=JSON.parse('{"title":"Widget rendering","description":"","frontmatter":{},"headers":[],"relativePath":"contributor-guide/architecture/widget-rendering.md","filePath":"contributor-guide/architecture/widget-rendering.md"}'),i={name:"contributor-guide/architecture/widget-rendering.md"};function n(c,e,r,s,l,g){return t(),d("div",null,[...e[0]||(e[0]=[a('<h1 id="widget-rendering" tabindex="-1">Widget rendering <a class="header-anchor" href="#widget-rendering" aria-label="Permalink to "Widget rendering""></a></h1><h2 id="widget-rendering-order-fixed-spec-§5" tabindex="-1">Render order (fixed, spec §5) <a class="header-anchor" href="#widget-rendering-order-fixed-spec-§5" aria-label="Permalink to "Render order (fixed, spec §5) {#widget-rendering-order-fixed-spec-§5}""></a></h2><p><code>src/view/Dashboard.tsx</code>'s <code>FilteredBoard</code> renders inside a <code><main></code> that opens with <code>FindingFilterBar</code> and (only when an active filter empties both finding streams) <code>NoMatchBanner</code>, then a single <code>Scorecard</code> strip, then (when the active filter doesn't empty the board) a two-tab <code>Tabs</code> (<code>src/components/ui/tabs.tsx</code>, a base-ui primitive): <strong>Findings</strong> and <strong>Full app report</strong> (2026-09-03 tabbed-impact-band-board redesign, replacing the prior three stacked sections: All recommendations, Suggested Improvements, Full app report, with a merged, impact-grouped Findings tab and an always-reachable Full app report tab). Base-ui <code>Tabs</code> fully unmount the inactive <code>TabsContent</code> panel rather than hiding it: a widget mounted only in Findings (every routeable <code>REGISTRY</code> widget; see "First investigation routing" below) is not in the DOM at all while Full app report is active, and remounts fresh, with its own state reset, when the user switches back.</p><p><code>region</code> on <code>RegistryEntry</code> (<code>src/view/detector-registry.tsx</code>) is read again, but only to decide whether a widget always mounts: <code>isAlwaysMountedType()</code> flags the one <code>reference</code>-region type still carved out as an always-mounted exception, <code>coreLocality</code> → <code>CoreUsageArea</code>. The other <code>reference</code>-region types stay ordinary finding-gated instead: <code>cacheUtilization</code>, <code>memoryUtilization</code>, and <code>utilization</code> are a product decision: a clean run on any of them isn't evidence worth surfacing unconditionally, so each collapses to a plain clean-check line like any other detector. The Findings tab (<code>src/view/widgets/ImpactBoard.tsx</code>) groups its content into three tiers, same as the retired Suggested Improvements section did, just re-sliced by impact band instead of living as one flat active grid. The three tiers: impact-banded rows and cards for every <code>REGISTRY</code> component and every recommendation row with at least one finding; a small always-visible grid holding just the one exception above (mounted unconditionally from <code>appModel</code> regardless of finding state, below the impact bands); and a collapsed "Clean checks" disclosure covering every remaining type with zero findings, built per detector <em>type</em> (<code>Object.keys(REGISTRY)</code>). Full app report stays structural-only (see "ReferenceSection" below).</p><p>Tags carry their own docs links; there is no separate legend widget. <code>TagBadge</code> (<code>src/view/ImpactBadge.tsx</code>) resolves its own tooltip, and when its type resolves a single known documentation anchor (<code>docAnchorForType</code>, <code>src/view/finding-tag-help.ts</code>) it renders as a link into the docs panel. It also always renders a second icon link (<code>findingGuideUrl</code>, <code>packages/core/src/docs-site-config.ts</code>) to that tag's entry in <code>docs-site/user-guide/understanding-findings.md</code>, opened as a plain new-tab navigation rather than the in-app panel. <code>TagBadge</code>'s <code>plainBadge</code> prop suppresses both links together; a Findings-tab recommendation row is the one place that never sets it (see below), since its badge cell isn't nested inside a button.</p><h3 id="findings-tab" tabindex="-1">Findings tab <a class="header-anchor" href="#findings-tab" aria-label="Permalink to "Findings tab""></a></h3><p>Rendered by <code>FilteredBoard</code>'s <code>TabsContent value="findings"</code> (<code>src/view/widgets/ImpactBoard.tsx</code>). Merges what used to be two stacked sections, All recommendations and Suggested Improvements, into one impact-ranked board. The recommendation-rollup logic lives in <code>src/view/widgets/FixTheseFirst.tsx</code> (kept as its own file and its own directly-testable exports, but no longer rendered as a standalone page section by <code>Dashboard.tsx</code>) and the active-widget logic lives in <code>src/view/widgets/Alerts.tsx</code> (same: kept, no longer rendered standalone). <code>ImpactBoard</code> calls <code>FixTheseFirst.tsx</code>'s exported <code>useFixTheseFirstData</code> (eligible findings, rollup groups, the top triage target) and <code>Alerts.tsx</code>'s exported <code>computeActiveWidgets</code> (the ranked active-<code>REGISTRY</code> list) to get the exact same data these two retired sections used to render independently, then regroups both by impact band via <code>groupImpactBand</code> (<code>FixTheseFirst.tsx</code>: a rollup group's representative member's impact band, the same finding whose impact band its own badge already shows) and each active widget's own <code>worstImpactBand</code>.</p><p>Eligible findings for the rollup are <code>catalog</code> ∪ <code>configFindings</code>, filtered to a <code>REGISTRY</code>-mapped type, with <code>incompleteRun</code> (a pipeline-completeness caveat, not an addressable fix; see the spec's <code>fixEffort</code> table) and <code>memoryUtilization</code>'s <code>memoryBand</code>/<code>dataUnavailable</code> variant (a missing-evidence caveat already covered by Evidence availability's own <code>executorMetrics</code> entry, <code>packages/core/src/evidence-availability.ts</code>) both explicitly excluded. Findings are grouped strictly by <code>finding.type</code> via <code>buildRecommendationRollup</code> (<code>packages/core/src/recommendation-rollup.ts</code>, further split within a type by impact kind and, for <code>resource</code>, unit: never merged across types); each resulting group becomes one row: a type with exactly one finding renders that finding directly, a type with more than one collapses into a summary row. Within an impact band, group order comes from <code>buildRecommendationRollup</code>'s own sort: <code>time</code> groups (a real <code>wallClock</code> claim) first, ranked among themselves by their union-capped <code>recoverableMsHigh</code> descending; then <code>resource</code> groups (<code>rawWaste</code> but no <code>wallClock</code>); then <code>count</code> groups (neither), both of the latter two ranked by worst impact band, never by their incomparable raw magnitudes. A summary row's tag and dot, plus its text, all come from the group's own highest-impact member (via the same three-tier comparator, computed locally in <code>FixTheseFirst.tsx</code>); the trailing stat depends on the group's kind (<code>×N · <time> recoverable</code>, <code>×N · resource-cost projection</code>, or a plain per-impact-band tally). Clicking it expands straight to the group's full, impact-ranked list, with no intermediate "worst-K" step, paginated at 10 rows per page (<code>data-testid="fix-these-first-group-row"</code>; no pager renders for a group of 10 or fewer findings; it appears once a group exceeds 10). A band's rollup rows render as a headerless three-column <code>Table</code> (<code>src/components/ui/table.tsx</code>): every individual row, whether shown directly or inside an expanded group, is a <code>TableRow</code> (<code>data-testid="fix-these-first-row"</code>, <code>data-finding-type</code>) with three <code>TableCell</code>s: the impact dot + ALL-CAPS tag as a real <code>TagBadge</code> (not <code>plainBadge</code>: nothing wraps it, so its own docs links stay real <code><a></code>s, same as everywhere else on the board); a text block inside its own nested <code><button></code> (a short imperative action label, e.g. "Reduce shuffle size", from <code>findingActionLabel</code> (<code>src/view/finding-action-label.ts</code>), over the finding's own full <code>recommendation</code> sentence in smaller muted text, both wrapping rather than truncating); and a right-aligned monospace stage reference + impact figure (e.g. <code>St.49 · 20.1s</code>, via <code>ImpactEstimate.tsx</code>'s shared <code>formatWallClockRange</code>/<code>formatRawWaste</code>). That inner button, not the row, is the click target: it routes via <code>selectTriageTargetForFinding</code> (<code>src/view/triage-target.ts</code>), the same per-finding resolver Stage Summary Table's own control uses (see "First investigation routing" below); a <code>TypeGroupRow</code>'s own inner button toggles its expand state instead (<code>aria-expanded</code>) and its expanded findings render as further <code>TableRow</code>s indented one badge-cell notch to read as the group's sub-list.</p><p>Each impact band (<code>ImpactBoard.tsx</code>'s own <code>ImpactGroup</code>, one call per entry of <code>IMPACT_BAND_ORDER_LIST = ['critical', 'warning', 'info']</code>) is a <code><section aria-label="Critical" | "Warning" | "Info"></code> with an <code><h3></code> heading, and renders nothing (not even the heading) when it has neither a rollup row nor an active widget: a run with no critical findings has no "Critical" heading or section at all. Inside a band, rollup rows render first as the headerless <code>Table</code> described above, followed by that band's active <code>REGISTRY</code> widget cards (<code>computeActiveWidgets</code>'s ranked list, filtered to this impact band) in their own <code>WidgetGrid</code>: every one of <code>orderedWidgets()</code>'s deduped <code>REGISTRY</code> components <em>except</em> the one always-mounted one below, with at least one finding in <code>catalog</code> ∪ <code>configFindings</code>. Within a band, active widgets keep <code>orderedWidgets()</code>'s own <code>action</code>-region-first, ascending-<code>DETECTORS</code>-order tiebreak. <code>cacheUtilization</code>, <code>memoryUtilization</code>, and <code>utilization</code> are <code>reference</code>-region types but aren't always-mounted exceptions, so a Cache Storage, Memory Utilization, or Executor Utilization card with an active finding surfaces in its own impact band like any other active widget.</p><p>Below the impact bands, <code>Alerts.tsx</code>'s exported <code>AlwaysVisibleAndCleanChecks</code> (shared verbatim with the retired standalone <code>Alerts</code> component) renders the same two tiers it always did, now living outside the impact-band grouping entirely rather than as this section's second and third tier: a small always-visible grid holding just Core Usage by Locality (<code>coreLocality</code>, resolving to <code>CoreUsageArea</code>), mounted unconditionally from <code>appModel</code> regardless of finding state (<code>alwaysMountedWidgets()</code>/<code>isAlwaysMountedType()</code>), carrying its own impact-band indicator when a finding is active instead of collapsing to a clean-check line on a clean run; and a collapsed "Clean checks" disclosure of <code>CleanCheckRow</code> lines (<code>src/view/widgets/CleanCheckRow.tsx</code>: label, the threshold it was measured against via <code>getThresholdSummary</code>, and "No fix needed.") built per detector <em>type</em> (every <code>REGISTRY</code> key except that one always-mounted key): a clean run lands <code>cacheUtilization</code>, <code>memoryUtilization</code>, and <code>utilization</code> here too, same as any ordinary action-region type. Caching Opportunities, Config Audit, and the four split Plan Advisor widgets (Redundant Plan Subtree, Excessive Small Files, Missed Broadcast Join, Oversized Broadcast Join) render through the ordinary active/clean paths above (see <a href="./board-widgets.html#board-widgets-beyond-the-fixed-six">Board widgets beyond the fixed six</a>). One consequence of this always-visible grid sitting below every impact band: a <code>critical</code>-band <code>coreLocality</code> finding still renders in that lower grid, below the <code>info</code>-band widgets above it, a deliberate tradeoff the spec accepted in exchange for never losing the widget on a clean run, not a ranking bug.</p><h3 id="per-widget-list-sort-mode" tabindex="-1">Per-widget list sort mode <a class="header-anchor" href="#per-widget-list-sort-mode" aria-label="Permalink to "Per-widget list sort mode""></a></h3><p><code>src/view/impact-sort.ts</code> (<code>sumWallClockLow</code>, <code>hasSortableImpact</code>, <code>byImpactDesc</code>, <code>stageIdOf</code>, <code>minStageId</code>, <code>byStageAsc</code>) and <code>src/view/SortModeToggle.tsx</code> are a shared, opt-in pair a Findings-tab active widget can use to let its own expanded, multi-row list default to potential-savings order (<code>wallClock.low</code> descending, the guaranteed-floor bound, not the optimistic <code>high</code>) instead of stage number. Each of the widgets below holds local <code>sortMode</code> state (<code>SortMode</code>, <code>'impact' | 'stage'</code>, default <code>'impact'</code>) and renders a <code>SortModeToggle</code> in the <code>WidgetCard</code> <code>badges</code> slot, so it sits on the title row itself rather than floating in the body: a button alongside the widget's tag badges (a <code>WidgetCard</code> header renders <code>badges</code> beside the title heading, never inside another control). Every one of the former combined widgets' split single-type widgets carries the same <code>canToggleSort</code> check its parent did, so the toggle only actually renders for a type whose finding can carry a wall-clock estimate: <code>Skew.tsx</code>, <code>TinyTask.tsx</code> (<code>stageShape</code>'s own rules never produce one, so <code>StageShape.tsx</code> carries the same check but it never fires), <code>ShuffleIO.tsx</code> (narrowed to <code>shuffle</code>), <code>PartitionSizing.tsx</code>, <code>Spill.tsx</code>, <code>GcPressure.tsx</code> (both its high-GC and low-GC sections, one shared toggle), <code>RetryWaste.tsx</code> (its siblings <code>StageFailed.tsx</code>/<code>TaskFailures.tsx</code> carry the same check, but <code>stageFailed</code>/<code>failures</code> are <code>estimateMethod: 'none'</code>, so it never fires there either), <code>SlowHost.tsx</code>, <code>StageSlowness.tsx</code>, <code>Straggler.tsx</code>, <code>SpeculationWaste.tsx</code>, <code>ColdStart.tsx</code> (five widgets now, one per type, each sorting only its own flat issue list; the old cross-type "coldStart sinks to the bottom under By stage" note no longer applies now that each type has its own single-type list), and <code>DuplicatePlanSubtree.tsx</code>/ <code>SmallFiles.tsx</code>/<code>UnderBroadcast.tsx</code>/<code>OverBroadcast.tsx</code> (each reorders its own list by <code>stageIdOf</code>, the lowest stage id its finding touches; the old cross-type <code>minStageId</code> group-ordering tiebreak no longer applies, since each type is its own widget now). Its <code>alternateOrderLabel</code> prop is required and every caller passes "By stage": stage number is the one axis every one of these widgets' items can be compared on, unlike the impact/raw-metric order this pattern replaced (dropped: comparing findings by impact band ranks them by how bad they are, not by how much fixing them would save, which is a worse default now that a real potential-savings figure exists to sort by instead). A per-stage detector's own <code>finding.stageId</code> is the sort key directly; a sql-scope finding that spans several stages (<code>duplicatePlanSubtree</code>, <code>smallFiles</code>, <code>underBroadcast</code>, <code>overBroadcast</code>, via <code>stageIds</code>) sorts by the lowest stage id it touches (<code>stageIdOf</code>). The toggle only renders when <code>hasSortableImpact</code> finds at least one wall-clock claim in the list; re-sorting a list with none would be a silent no-op. Both comparators return <code>0</code> when neither side has a comparable value, so those items keep their prior relative order (<code>Array.prototype.sort</code>'s stability) rather than being shuffled. <code>FixTheseFirst</code>'s own expanded group list (see "Findings tab" above) already sorted by impact before this pattern existed and does not use it; it has no stage-order toggle.</p><h3 id="gold-standard-row-expand-contract" tabindex="-1">Gold Standard row/expand contract <a class="header-anchor" href="#gold-standard-row-expand-contract" aria-label="Permalink to "Gold Standard row/expand contract""></a></h3><p>Every <code>REGISTRY</code> widget renders its content as N≥1 rows, using <code>Skew.tsx</code> as the reference implementation. A widget's data shape (chart, table, single app-scoped scalar) is never by itself a reason to skip this: the three named exceptions below are the only ones. Any further exception must get its own entry here, justified in the same PR that introduces it.</p><ul><li>Tier A (universal): <code>WidgetCard</code> chrome, an impact-band left-border + dot, ALL-CAPS <code>TagBadge</code>s, a <code>SortModeToggle</code> when <code>hasSortableImpact</code> is true, a <code>finding-anchor</code> ref on any focusable/deep-linkable unit, and an early <code>null</code> return on an empty catalog filter (except Core Usage by Locality, the one always-mounted widget that renders unconditionally from <code>appModel</code> via <code>alwaysMountedWidgets()</code>/<code>isAlwaysMountedType()</code> rather than early-returning on an empty catalog filter).</li><li>Tier B (the row/expand pattern): a row's collapsed state shows its core metric(s), <code>ImpactEstimate</code>, its recommendation text, any config-hint code snippet/list, and docs links, all unconditionally. Confidence and evidence are unconditional too, since the 2026-09 redesign: <code>RowStatusCluster</code> (<code>src/view/RowStatusCluster.tsx</code>) is a single, fixed-position pill combining both, replacing the old per-row <code>ExpandToggleButton</code> + <code>ConfidenceMarker</code> + <code>EvidenceLink</code> trio entirely. Every widget that carries one wraps it in <code>AdvancedOnly</code> (<code>src/view/AdvancedOnly.tsx</code>), so it only renders at the Advanced density tier: this is a content-visibility gate (Basic vs. Advanced), not a per-row collapse, and it's applied consistently across every adopting widget. There is nothing left to gate behind a per-row click for confidence or evidence in any widget. A widget-header <code>RowStatusCluster</code> (passed as <code>WidgetCard</code>'s <code>statusBadge</code> prop) carries one further, unrelated gate on top of <code>AdvancedOnly</code>: <code>open && statusBadge</code> (<code>WidgetCard.tsx:138,141</code>) keeps it out of the collapsed header, matching the comment on <code>statusBadge</code> itself ("kept separate so tag/impact badges stay visible in the collapsed summary while a <code>RowStatusCluster</code>-style ... marker doesn't"). This is the card's own expand/collapse, not a separate reveal-on-click built for the cluster, and it composes with density as an AND: a header cluster needs both Advanced tier and an expanded card. A row with neither renders no cluster at all (<code>RowStatusCluster</code> returns <code>null</code>). Confidence renders as plain, non-interactive text ("<code>{confidence} confidence</code>", no further suffix) with a <code>title</code> + <code>aria-describedby</code> tooltip carrying the full <code>validationRequired</code> detail: never itself a click target. Evidence renders as the cluster's one real click target: a button whose visible text is just the evidence label (e.g. "SQL plan") but whose accessible name always carries the "Evidence: ..." prefix via an explicit <code>aria-label</code>, regardless of what else sits nearby (the old <code>bare</code> prop/prefix distinction is gone: there's only one form now). Clicking it calls the same <code>revealEvidence</code> navigation the retired <code>EvidenceLink</code> used. <code>RowStatusCluster</code>'s fixed slot is the row's own stage-pill/title line (<code>justify-between</code>, cluster right-aligned), the same position in every adopting widget, not floating with the recommendation text below. Lists longer than <code>VISIBLE_LIMIT</code> (6, <code>packages/core/src/format-utils.ts</code>) still get page-based navigation (Previous/Next, <code>usePagedRows</code>/<code>RowPagination</code>, <code>src/view/usePagedRows.ts</code>/<code>src/view/RowPagination.tsx</code>) instead of rendering unconditionally: the one sanctioned cap mechanism across every Tier B widget. A Tier B widget whose rows are also triage-routable (wires <code>useFindingAnchor</code>) must pass its <code>routeIndex</code> (the routed finding's position in the paginated list) into <code>usePagedRows</code>, or a deep-linked route to a finding beyond the first page silently degrades to focusing the widget's disclosure title instead of the row.</li></ul><p>The following widgets carry a <code>RowStatusCluster</code>, all Advanced-only: <code>Spill.tsx</code> (confidence only), <code>ConfigAudit.tsx</code>'s widget-header control (evidence only, a widget-wide constant rather than per-finding data, since the evidence key doesn't vary per row; also the control shown in the widget's empty-findings/no-data states), <code>MemoryUtilization.tsx</code>'s rows (confidence + evidence together; <code>ExecutorUtilization.tsx</code>'s rows, split out of the same former combined widget, carry neither), each of the four split Plan Advisor widgets' (<code>DuplicatePlanSubtree.tsx</code>, <code>SmallFiles.tsx</code>, <code>UnderBroadcast.tsx</code>, <code>OverBroadcast.tsx</code>) per-row control (evidence only) plus their own widget-header marker (confidence only, one marker per widget now that each is its own finding-type, replacing the old <code>PlanFindings.tsx</code> per-group heading), <code>CoreUsageArea.tsx</code>, <code>EfficiencyModel.tsx</code>, <code>PlanView.tsx</code> (three call sites), <code>ScalingSim.tsx</code> (two call sites), and <code>WastedCoreHours.tsx</code> (all confidence-only, standalone rather than per-finding-row: a card-header badge, a widget-level single marker, a plan-tree node/summary-row marker, or an "unavailable data" message: the same component, same visual language, regardless of where it sits); and, since <code>skew</code>/<code>straggler</code>/<code>gc</code> started disclosing their own unvalidated noise-floor thresholds (confidence only, per-row, no <code>evidenceKey</code> passed), <code>GcPressure.tsx</code>'s rows, <code>Straggler.tsx</code>'s rows, and the shared <code>StageFindingGroup.tsx</code> row (adopted by <code>Skew.tsx</code>/<code>StageShape.tsx</code>/<code>TinyTask.tsx</code>, though today only <code>skew</code> findings actually carry a <code>confidence</code> field).</p><p>Two named exceptions:</p><ul><li><code>CachingOpportunity.tsx</code>, kept exactly as it was before the 2026-09 redesign, since it isn't a per-row confidence/evidence signal: the <code>cachingOpportunity</code> detector always emits <code>confidence: 'low'</code>, a constant across every row, not a per-finding signal, so a single caveat sentence below the table covers it instead of a <code>RowStatusCluster</code> repeated identically on every row.</li><li><code>Skew.tsx</code>/<code>StageShape.tsx</code>/<code>TinyTask.tsx</code>: their per-row histogram toggle (<code>ExpandToggleButton</code> + <code>useExpandableRow</code>) gates a lazily-fetched duration histogram, unrelated to confidence/evidence, so it was untouched by the 2026-09 redesign and untouched again when <code>RowStatusCluster</code> was later added alongside it in the shared <code>StageFindingGroup.tsx</code> row. The two controls coexist per row. <code>ExpandToggleButton</code> is now single-purpose (always the task-detail toggle) since these three (split out of the former combined <code>TaskSkew.tsx</code>) are its only remaining adopters.</li></ul><p>No toggle at all, unconditional recommendation/content, same as before this redesign (unaffected either way, since these widgets never carried confidence/evidence display in the first place): <code>IncompleteRun.tsx</code>, <code>ShuffleIO.tsx</code>, <code>PartitionSizing.tsx</code>, <code>StageFailed.tsx</code>, <code>TaskFailures.tsx</code>, <code>RetryWaste.tsx</code>, <code>ColdStart.tsx</code>, <code>SlowHost.tsx</code>, <code>StageSlowness.tsx</code>, <code>SpeculationWaste.tsx</code>, <code>ExecutorCountChart.tsx</code>, <code>ExecutorUtilization.tsx</code>, <code>JobFailures.tsx</code>, <code>CacheUtilization.tsx</code>, <code>AutoscalingChurn.tsx</code>, and the four split Plan Advisor widgets (<code>DuplicatePlanSubtree.tsx</code>, <code>SmallFiles.tsx</code>, <code>UnderBroadcast.tsx</code>, <code>OverBroadcast.tsx</code>), split out of, respectively, the former combined <code>ExecutorTimeline.tsx</code>, <code>Failures.tsx</code>, and <code>PlanFindings.tsx</code>, none of which carried a per-row toggle either.</p><p>Three named exceptions to specific pieces of the contract, not to the row wrapper or pagination, which still apply to all three:</p><ul><li><code>CoreUsageArea.tsx</code>'s non-local-stage list is a derived stat breakdown (<code>computeCoreLocalityRatio</code>'s <code>topStages</code>), not a <code>Finding[]</code>: there is no per-row finding or recommendation to reveal. Paginated like every other Tier B list, but with no per-row expand.</li><li><code>CacheUtilization.tsx</code>'s RDD <code><Table></code> holds reference columns (name, storage level, partitions, memory, disk bytes) with no recommendation attached to any row. Paginated like every other Tier B list, but with no per-row expand. Its separate recommendation list below the table is full Tier B (unconditional, no toggle, per the paragraph above).</li><li><code>IncompleteRun.tsx</code> leads with no metric of its own: this app-scoped, single-finding widget has no natural lead figure distinct from its title/badge, unlike every other Gold Standard widget, which leads with a percentage, duration, or count.</li></ul><h3 id="referencesection" tabindex="-1">ReferenceSection <a class="header-anchor" href="#referencesection" aria-label="Permalink to "ReferenceSection""></a></h3><p>Rendered by <code>FilteredBoard</code>'s <code>TabsContent value="full-report"</code>, tab label "Full app report". A plain <code><div></code>, structural-only, reading none of <code>REGISTRY</code>/<code>orderedWidgets()</code> at all. Its heading is a visually-hidden (<code>sr-only</code>) <code><h2>Full app report</h2></code>, matching the tab's own label for the accessibility-tree heading outline without visually duplicating the tab text. No longer an <code>Accordion</code>: base-ui <code>Tabs</code> unmount this panel entirely while Findings is active (see "Render order" above), so there is no "collapsed but present" state left to model, and this content is simply not in the DOM until the user clicks the Full app report tab. Its exact order is WallClock → Timeline → Executor Count Over Time (<code>ExecutorCountChart.tsx</code>, the executor add/remove count chart extracted out of the former combined <code>ExecutorTimeline.tsx</code>; not driven by any finding, so it isn't a <code>REGISTRY</code> entry) → StageTable → a <code>WidgetGrid</code> holding Evidence availability → ETL Phase Attribution → What-If Executor Scaling → Compute Efficiency → Wasted Core-Hours → Core-Usage Distribution. Scorecard used to lead this section; it now renders once, above the tabs themselves, in <code>FilteredBoard</code> (<code>src/view/Dashboard.tsx</code>), so it stays visible regardless of which tab is active rather than living inside either one (a three-tile run-info row: Wall-clock, Efficiency, Wastage; see <a href="./board-widgets.html#board-widgets-beyond-the-fixed-six">Board widgets beyond the fixed six</a>). WallClock, Timeline, StageTable and every tile in the grid beside them (Evidence availability included) all render immediately and fully expanded. No detector-driven <code>REGISTRY</code> card renders in this section any more: Memory Utilization, Executor Utilization, Core Usage by Locality, and Cache Storage all moved to the Findings tab above (Cache Storage, Memory Utilization, and Executor Utilization only surface there when they have an active finding; Core Usage by Locality alone is always-mounted).</p><p>The Evidence availability card is the persistent, non-impact-band ledger <a href="./worker-protocol.html#evidence-availability-contract-v1">defined in the worker protocol</a>, not an alert or detector widget. An <code>Evidence: …</code> control appears only where a conclusion or unavailable report lens declares a relevant ledger dependency. <code>revealEvidence</code> (<code>src/view/EvidenceAvailabilityContext.tsx</code>) sets <code>referenceOpen</code> true, which <code>DashboardContent</code> (<code>src/view/Dashboard.tsx</code>) watches in a <code>useEffect</code> and translates into <code>setActiveTab('full-report')</code>, replacing what used to be "expand the Reference accordion": mouse and keyboard activation switches to the Full app report tab, opens the ledger card, then focuses the referenced stable entry id (<code>evidence-availability-<key></code>). The control explains evidence availability; it does not promise an unavailable signal would have produced a finding.</p><p><code>WidgetGrid</code> (both inside a Findings impact band and inside <code>ReferenceSection</code>): collapsed cards occupy one responsive column; opening a card expands it across the full row. Nested <code>WidgetCard</code>s are isolated from the parent grid state. A Findings-tab <code>FixTheseFirst</code> rollup row is a table row, not a <code>WidgetGrid</code> card, and carries no collapse/expand state of its own (a <code>TypeGroupRow</code> summary row does own its own local expand/pagination state). Full app report's non-<code>REGISTRY</code> tiles (ETL Phase Attribution, What-If Executor Scaling, Compute Efficiency, Wasted Core-Hours, Core-Usage Distribution) sit in their own <code>WidgetGrid</code> alongside Evidence availability, but that grid has no <code>widgetId</code> wired to any card (see "First investigation routing" below), so nothing there is ever a route destination.</p><p>Firm constraint: <code>orderedWidgets()</code> (in <code>src/view/detector-registry.tsx</code>) still runs the single <code>DETECTORS</code>-ascending sort exactly as <code>dashboard-renderer.js</code> used to, and still sorts <code>action</code> components ahead of <code>reference</code> ones within that one list; the component-identity dedup it used to need is gone now that every <code>REGISTRY</code> entry maps to its own unique component (2026-09 widget/finding-type 1:1 mapping redesign). <code>cacheUtilization</code>, <code>memoryUtilization</code>, and <code>utilization</code> (<code>reference</code>-region) can still reach the active grid alongside <code>duplicatePlanSubtree</code> (<code>action</code>-region) and any other active finding, since none of them is the one always-mounted exception filtered out before that grid. <code>computeActiveWidgets</code> (<code>Alerts.tsx</code>) is still its main consumer, now filtered through <code>isAlwaysMountedType()</code> to exclude that one always-mounted component; the clean-check list bypasses <code>orderedWidgets()</code> entirely, iterating <code>Object.keys(REGISTRY)</code> per type instead (see "Findings tab" above). <code>DETECTORS</code>' own array order and iteration, plus its cross-detector <code>suppressWhen</code> logic (e.g. <code>stageSlowness</code> deferring to <code>slowHost</code>, see <a href="./detector-contract.html#detector-contract">Detector contract</a>), live entirely in <code>packages/core/src/detectors.ts</code>/<code>packages/core/src/analyzer.ts</code>, untouched by this redesign. Which React component each finding type resolves to is still registry-owned.</p><h3 id="first-investigation-routing" tabindex="-1">First investigation routing <a class="header-anchor" href="#first-investigation-routing" aria-label="Permalink to "First investigation routing""></a></h3><p>The dashboard has a view-only per-finding route, not a single first-investigation pick any more: every Findings-tab recommendation row (rendered by <code>FixTheseFirst.tsx</code>'s row components inside <code>ImpactBoard</code>) and the visible-finding control in Stage Summary (<code>StageTable.tsx</code>) each independently resolve and request their own target. <code>detector-registry.tsx</code> owns each routeable finding type's stable <code>widgetId</code>, widget title, and finding label; identity and copy come from there, not from runtime position or raw detector type. Eligible targets have a mapped, routeable registry entry and a non-empty trimmed recommendation (<code>targetForFinding</code>, <code>src/view/triage-target.ts</code>). The target's widget still renders every affected stage in its local order.</p><p><code>src/view/triage-target.ts</code> also still exports <code>selectTriageTarget</code>, the whole-catalog "single highest-potential-savings finding" picker that used to back a start-here callout and each per-tag headline tile in the retired <code>ProblemHeadlines.tsx</code> (potential savings first via <code>impactEstimate.wallClock.high</code>, then <code>orderedWidgets()</code> widget order, then catalog order; impact band plays no part). Ranking by potential savings replaced the earlier severity-first routing once the occupancy-weighted impact estimator gave every finding a real, comparable <code>impactEstimate.wallClock</code> figure: severity-first could point the single "start here" callout at a <code>skew</code>/<code>straggler</code> finding ranked <code>critical</code> on a ratio basis while its occupancy-clipped recoverable time was near zero, passing over a lower-severity finding with an order-of-magnitude larger real recoverable-time estimate right next to it. Nothing in the app calls <code>selectTriageTarget</code> any more: <code>FixTheseFirst</code> already ranks every eligible finding by impact magnitude, so a separate single-winner pick has no call site left. <code>tests/view/triage-target.test.ts</code> still covers it directly.</p><p>The route is re-derived from the current catalog before each asynchronous step. Its identity is object-reference equality against the current catalog (<code>catalog.includes(finding)</code> in <code>selectTriageTargetForFinding</code>, <code>src/view/triage-target.ts</code>) rather than a derived key: a new parse yields new finding object references, so reference presence alone detects staleness. It is not persisted and does not extend the core <code>Finding</code> contract; cross-session consumers (exports, future URL-restored state) use the core <code>Finding.id</code> instead (see <a href="./worker-protocol.html#finding-identity">Finding identity</a>).</p><p><code>Dashboard</code> owns disclosure and navigation. Every routeable <code>REGISTRY</code> widget now lives in the Findings tab (no <code>REGISTRY</code> widget renders inside Full app report any more; see "Render order" above), so <code>requestRoute</code> (<code>DashboardContent</code>, <code>src/view/Dashboard.tsx</code>) starts with an unconditional <code>setActiveTab('findings')</code>, whether the request came from a control already on that tab or from Full app report's own Stage Summary route link. That tab switch can unmount and remount the whole Findings subtree in the same commit as the route landing (base-ui <code>Tabs</code> fully unmounts the inactive panel; see "Render order" above), which two routing paths have to account for: <code>reportWidgetOpen</code> no longer clears a route just because the target <code>WidgetGridItem</code> hasn't registered yet in this commit (a child <code>WidgetCard</code> reports its own open state before its parent <code>WidgetGridItem</code>'s registration effect runs on a fresh mount, so treating "not registered yet" as "stale" would drop the route before the parent even gets a chance to register it); and a paginated widget that jumps its own page to reveal a routed row (<code>GcPressure.tsx</code>, <code>StageFailed.tsx</code>, <code>TaskFailures.tsx</code>, <code>RetryWaste.tsx</code>) computes that jump during render, not only in a post-commit effect, so the row exists in the DOM in time for this same commit's anchor lookup instead of one commit later.</p><p>After the target card commits open, its wrapper scrolls with a top margin below the fixed header and its disclosure button receives visible, temporary route focus (using instant scrolling for reduced motion). Requests use a latest-request-wins token (<code>requestTokenRef</code>) that guards every step and revalidates; a stale, missing, unmounted, changed-catalog, or changed-active-file target cancels without redirecting, scrolling, or moving focus. Widget registration probes survive StrictMode double-mounts via an <code>unregistrationCheck</code> that re-validates before clearing.</p><p>This route deliberately does not include Config Audit, detector/parser or threshold changes, Stage Detail routing, or generic badges, tags, chips, or StagePills as controls.</p><h3 id="widget-placement" tabindex="-1">Widget placement <a class="header-anchor" href="#widget-placement" aria-label="Permalink to "Widget placement""></a></h3><p>The Plan Explorer (<code>src/view/widgets/PlanExplorer.tsx</code>) is embedded inside a flagged stage's own row, not as a standalone widget. Neither Task Skew nor Spill embed it any more: both dropped their own <code>PlanExplorer</code> trigger entirely (not merely re-gated behind a toggle), since the same stage's plan is already reachable, unconditionally, from <code>StageDetailDialog</code>. Shuffle I/O is <code>PlanExplorer</code>'s only remaining call site: it has no row-expansion toggle of its own, so its trigger renders as a direct sibling in the row body.</p><p>Plan detection logic belongs in a <code>packages/core/src/detectors.ts</code> entry (<code>scope:'sql'</code>) consuming <code>planTree</code>. <code>packages/core/src/plan-summary.ts</code> is display-only summarization; do not add detection heuristics there.</p><p><code>StageHeader.tsx</code> (<code>StagePill</code> plus a <code><span></code> naming the stage) vs. bare <code>StagePill</code>/<code>StagePillGroup</code> is a deliberate row-density choice, not an inconsistency: <code>StageHeader</code> is for single-stage-per-row detail widgets where naming the stage adds context (<code>SlowHost.tsx</code>, <code>StageSlowness.tsx</code>, <code>Straggler.tsx</code>, <code>SpeculationWaste.tsx</code>, <code>GcPressure.tsx</code>, <code>Skew.tsx</code>, <code>StageShape.tsx</code>, <code>TinyTask.tsx</code>, <code>PartitionSizing.tsx</code>), while bare <code>StagePill</code>/<code>StagePillGroup</code> is for compact, multi-row lists where many stages appear per widget (<code>StageFailed.tsx</code>, <code>TaskFailures.tsx</code>, <code>RetryWaste.tsx</code>, the four split Plan Advisor widgets, <code>Spill.tsx</code>, and <code>StageTable.tsx</code>). <code>ShuffleIO.tsx</code> uses both in different parts of its own row, which is fine: it isn't a violation of the convention above.</p><p>Stage Summary Table (<code>src/view/widgets/StageTable.tsx</code>) defaults to <strong>problem stages</strong>, with a header toggle to <strong>top 10 by duration</strong>. It renders inside the Full app report tab, not as a standalone board section.</p><h3 id="full-render-sequence" tabindex="-1">Full render sequence <a class="header-anchor" href="#full-render-sequence" aria-label="Permalink to "Full render sequence""></a></h3><p>Top to bottom, in <code>Dashboard.tsx</code>'s <code>FilteredBoard</code>:</p><ol><li><code>FindingFilterBar</code> (plus <code>NoMatchBanner</code> when the active filter empties both finding streams).</li><li><code>Scorecard</code>: a three-tile run-info row (Wall-clock, Efficiency, Wastage), rendered once regardless of which tab is active.</li><li>A two-tab <code>Tabs</code> (skipped entirely when the active filter empties both finding streams; <code>NoMatchBanner</code> above already covers that case), tab labels <strong>Findings</strong> and <strong>Full app report</strong>: <ul><li><strong>Findings</strong> (<code>ImpactBoard</code>): a <code>HighestImpactBar</code> callout for the single highest-impact eligible finding, if any; then one <code><section></code> per impact band in <code>Critical</code> → <code>Warning</code> → <code>Info</code> order, each rendering nothing when it has neither a recommendation row nor an active widget: a recommendation-rollup <code>Table</code> (one row per eligible-finding type, <code>REGISTRY</code>-mapped, <code>incompleteRun</code> excluded: a direct row for a type with one finding, an expandable, paginated summary row for a type with more than one) followed by a <code>WidgetGrid</code> of that band's active <code>REGISTRY</code> widgets (ranked by widget order: ascending <code>DETECTORS</code> order, action-region components ahead of reference-region ones); then, below every impact band, a small always-visible grid of just Core Usage by Locality, mounted unconditionally regardless of finding state, and a collapsed "Clean checks" disclosure of <code>CleanCheckRow</code> lines built per detector type (every remaining <code>REGISTRY</code> key with zero findings); or inline "No findings to fix right now." text when nothing is eligible at all.</li><li><strong>Full app report</strong> (<code>ReferenceSection</code>): WallClock → Timeline → Executor Count Over Time → StageTable → Evidence availability → fixed report-lens tail (ETL Phase Attribution → What-If Executor Scaling → Compute Efficiency → Wasted Core-Hours → Core-Usage Distribution). Structural-only, as above. Fully unmounted while Findings is active (see "Render order" above).</li></ul></li></ol><p>Report lenses carry no impact-band chip and self-hide when their input data is absent. Every widget self-wraps in <code>WidgetCard</code> (<code>src/view/WidgetCard.tsx</code>: an <code><h3></code> title, one level below the board's <code><h2></code> section headers) for the document outline, except Scorecard, which renders its own header band.</p><p>Every card is collapsible: the title sits in a <code>CollapsibleTrigger</code> button with a chevron (<code>WidgetCard.tsx</code>), and each widget sets its own <code>defaultCollapsed</code> (most default to <code>true</code>; <code>ImpactBoard</code>'s action-region grid forces <code>defaultCollapsed</code> on every <code>REGISTRY</code> widget instance it mounts, so a marginal finding doesn't default open). Route navigation ("jump to this finding") goes through the grid coordinator instead of a fixed <code>tabIndex</code>: <code>WidgetGrid.tsx</code> tracks each card's disclosure button and bumps <code>openRequestGeneration</code> to force a collapsed card open and focus its trigger when the target finding has no anchored row of its own. How much a card shows beyond that is a board-wide choice, set by the Basic/Advanced density tier in the topbar.</p>',43)])])}const u=o(i,[["render",n]]);export{p as __pageData,u as default};
|