@wafertools/wafermap 0.26.0 → 0.27.0

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +367 -0
  2. package/README.md +46 -3
  3. package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
  4. package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +13 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -10
  11. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
  13. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -9
  15. package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
  16. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
  17. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
  18. package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -1
  19. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  20. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  21. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  22. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
  23. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
  24. package/dist/packages/canvas-adapter/charts/trend.d.ts +30 -0
  25. package/dist/packages/canvas-adapter/charts/trend.js +1 -0
  26. package/dist/packages/canvas-adapter/dieList.js +7 -7
  27. package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
  28. package/dist/packages/canvas-adapter/identityHeader.js +1 -0
  29. package/dist/packages/canvas-adapter/index.d.ts +1 -1
  30. package/dist/packages/canvas-adapter/insightsTab.d.ts +27 -3
  31. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  32. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  33. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
  34. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
  35. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +72 -17
  36. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  37. package/dist/packages/canvas-adapter/summaryPanel.d.ts +219 -33
  38. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  39. package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
  40. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  41. package/dist/packages/canvas-adapter/toolbar.d.ts +310 -6
  42. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  43. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  44. package/dist/packages/canvas-adapter/userGuideHtml.js +98 -35
  45. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  46. package/dist/packages/canvas-adapter/version.js +1 -1
  47. package/dist/packages/canvas-adapter/warnings.d.ts +0 -4
  48. package/dist/packages/canvas-adapter/warnings.js +1 -1
  49. package/dist/packages/core/utils.d.ts +11 -0
  50. package/dist/packages/core/utils.js +1 -1
  51. package/dist/packages/renderer/buildView.d.ts +34 -1
  52. package/dist/packages/renderer/buildView.js +1 -1
  53. package/dist/packages/renderer/buildWaferMap.d.ts +104 -5
  54. package/dist/packages/renderer/buildWaferMap.js +1 -1
  55. package/dist/packages/renderer/colorSchemes.js +1 -1
  56. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  57. package/dist/packages/stats/binPareto.d.ts +15 -0
  58. package/dist/packages/stats/binPareto.js +1 -1
  59. package/dist/packages/stats/clusterDetection.js +1 -1
  60. package/dist/packages/stats/connectedComponents.d.ts +14 -0
  61. package/dist/packages/stats/connectedComponents.js +1 -0
  62. package/dist/packages/stats/correlation.d.ts +26 -0
  63. package/dist/packages/stats/correlation.js +1 -1
  64. package/dist/packages/stats/facets.js +1 -1
  65. package/dist/packages/stats/filterFindings.d.ts +18 -0
  66. package/dist/packages/stats/filterFindings.js +1 -1
  67. package/dist/packages/stats/findingsNarrative.js +1 -1
  68. package/dist/packages/stats/histogram.d.ts +19 -1
  69. package/dist/packages/stats/histogram.js +1 -1
  70. package/dist/packages/stats/index.d.ts +6 -0
  71. package/dist/packages/stats/index.js +1 -1
  72. package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
  73. package/dist/packages/stats/mergeTestDefs.js +1 -0
  74. package/dist/packages/stats/metadataColumns.d.ts +20 -4
  75. package/dist/packages/stats/metadataColumns.js +1 -1
  76. package/dist/packages/stats/patternClassification.js +1 -1
  77. package/dist/packages/stats/renderFindingsReport.js +14 -14
  78. package/dist/packages/stats/renderSummaryReport.js +32 -29
  79. package/dist/packages/stats/reportHtml.js +38 -13
  80. package/dist/packages/stats/testPassRate.d.ts +85 -0
  81. package/dist/packages/stats/testPassRate.js +1 -0
  82. package/dist/packages/stats/trend.d.ts +40 -0
  83. package/dist/packages/stats/trend.js +1 -0
  84. package/dist/packages/stats/types.d.ts +117 -6
  85. package/llms.txt +6 -0
  86. package/package.json +7 -5
  87. package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
  88. package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
@@ -1 +1 @@
1
- const _=["spatial-pattern","ring","sector","quadrant","cluster","edge-arc","reticle-position"],y={unusual:0,notable:1,info:2},S={E:0,ENE:22.5,NE:45,NNE:67.5,N:90,NNW:112.5,NW:135,WNW:157.5,W:180,WSW:202.5,SW:225,SSW:247.5,S:270,SSE:292.5,SE:315,ESE:337.5};function m(e){return e.stats.adjustedPValue??e.stats.pValue??1}function b(e){return[...e].sort((n,t)=>{const o=y[n.severity]-y[t.severity];return o!==0?o:m(n)-m(t)})[0]}function w(e){return e.some(t=>t.variable.kind==="yield")?e.filter(t=>t.variable.kind!=="hardBin"&&t.variable.kind!=="softBin"):e}function $(e){return e.replace(/^(Rings?|Quadrants?|Sectors?)\s+/i,"").split(/[,&]|–|\band\b/).map(n=>n.trim()).filter(Boolean)}function W(e,n){const t=new Set($(e));return $(n).some(o=>t.has(o))}function v(e){return e.variable.kind==="yield"?"yield":e.variable.kind==="hardBin"?`HBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.kind==="softBin"?`SBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.label}function h(e){let n=0,t=0;for(const o of e)o.effect.direction==="higher"?n++:o.effect.direction==="lower"&&t++;return n>t?"higher":t>n?"lower":"mixed"}function d(e){return e==="higher"?"elevated":e==="lower"?"reduced":"shifted"}function p(e){const n=new Set,t=[];for(const o of e){const r=v(o);n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:t.length===3?`${t[0]}, ${t[1]}, and ${t[2]}`:`${t[0]}, ${t[1]}, and ${t.length-2} more`}function E(e){const n=new Set,t=[];for(const o of e){const r=o.comparison.left;n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`}function j(e){return e.length!==1?!1:!/^(Rings|Quadrants|Sectors)\b/.test(e[0].comparison.left)}function A(e){const n=h(e),t=e.every(c=>c.comparison.left.includes("(edge)")),o=e.every(c=>c.comparison.left.includes("(core)"));let r,a;return o?(r="The core ring",a=!0):t&&e.length>1?(r="The edge rings",a=!1):(r=E(e),a=j(e)),`${r} show${a?"s":""} ${d(n)} ${p(e)}.`}function B(e){const n=h(e),t=s=>s.highlight.kind==="region"&&s.highlight.regionKeys.length>1||s.comparison.left.startsWith("Quadrants"),o=e.filter(s=>t(s)),r=e.filter(s=>!t(s)),a=[];r.length===1?a.push(`the ${r[0].comparison.left} quadrant`):r.length>1&&a.push(`the ${E(r)} quadrants`);for(const s of o)a.push(s.comparison.left.replace(/^Quadrants/,"quadrants"));const c=a.join(" and "),u=r.length===1&&o.length===0;return`${c.charAt(0).toUpperCase()+c.slice(1)} show${u?"s":""} ${d(n)} ${p(e)}.`}function P(e){const n=h(e),t=[],o=new Set;for(const u of e){const i=u.comparison.left.replace(/^Sectors? /,"");o.has(i)||(o.add(i),t.push(i))}let r,a;return t.length===1?(r=`the ${t[0]} ${e[0].comparison.left.startsWith("Sectors")?"sectors":"region"}`,a=!e[0].comparison.left.startsWith("Sectors")):(r=`the ${t.length===2?t.join(" and "):`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`} sectors`,a=!1),`${r.charAt(0).toUpperCase()+r.slice(1)} show${a?"s":""} ${d(n)} ${p(e)}.`}function q(e){if(e.length===1)return`A failure cluster at ${e[0].comparison.left.replace(/^Cluster at /,"")}.`;const t=e.reduce((o,r)=>(o.stats.sampleSizeLeft??0)>=(r.stats.sampleSizeLeft??0)?o:r).comparison.left.replace(/^Cluster at /,"");return`${e.length} failure clusters identified; the largest at ${t}.`}function x(e){const n=e[0].comparison.left.replace(/^Edge arc ~/,"");return e.length===1?`An edge arc near ${n} shows localised failures.`:`${e.length} edge arcs detected, including near ${n}.`}function C(e){const n=h(e);return e.length===1?`Reticle-position variation: ${e[0].comparison.left} shows ${d(n)} ${p(e)}.`:`Reticle-position variation across ${e.length} cells (${d(n)} ${p(e)}).`}function Y(e,n){const t=e.stats.method==="geometry"?` (${e.severity==="unusual"?"high":e.severity==="notable"?"medium":"low"} confidence)`:"",o=new Set(e.relatedIds??[]),r=n.find(c=>o.has(c.id)&&(c.comparison.family==="edge-arc"||c.comparison.family==="cluster"));let a="";return r&&(r.comparison.family==="edge-arc"?a=`; failures concentrated at the wafer edge near ${r.comparison.left.replace(/^Edge arc ~/,"")}`:a=`; failures concentrated near ${r.comparison.left.replace(/^Cluster at /,"")}`),`${e.comparison.left} failure pattern detected${t}${a}.`}function F(e){const n=e.replace(/\.$/,"");return n.charAt(0).toLowerCase()+n.slice(1)}function M(e){const n=e.filter(t=>t.variable.kind==="yield");return n.length>0?h(n):h(e)}function k(e,n){const t=new Map;for(const i of n){const s=t.get(i.comparison.left)??[];s.push(i),t.set(i.comparison.left,s)}const o=[],r=[];for(const i of t.values()){const s=M(i);s==="higher"?o.push(...i.filter(l=>l.effect.direction==="higher")):s==="lower"?r.push(...i.filter(l=>l.effect.direction==="lower")):o.push(...i)}const a=o.map(i=>i.comparison.left),c=r.filter(i=>!a.some(s=>W(s,i.comparison.left)));if(o.length>0&&c.length>0){const i=n.some(f=>f.variable.kind==="yield"),[s,l]=i?[c,o]:[o,c];return`${e(s).replace(/\.$/,"")} while ${F(e(l))}.`}if(o.length>0&&r.length>0&&c.length===0)return e(o);const u=M(n);if(u!=="mixed"&&n.some(i=>i.variable.kind==="yield")){const i=n.filter(s=>s.effect.direction===u);if(i.length>0)return e(i)}return e(n)}function N(e){let n=0,t=0,o=0;for(const r of e)for(const a of $(r.comparison.left)){const c=S[a];if(c===void 0)continue;const u=c*Math.PI/180;n+=Math.cos(u),t+=Math.sin(u),o++}return o===0||n===0&&t===0?null:(Math.atan2(t,n)*180/Math.PI+360)%360}function I(e){let n="E",t=360;for(const[o,r]of Object.entries(S)){const a=Math.abs((e-r+540)%360-180);a<t&&(t=a,n=o)}return n}function O(e){const n=e.filter(u=>u.effect.direction==="higher"),t=e.filter(u=>u.effect.direction==="lower");if(n.length===0||t.length===0)return null;const o=new Set(e.map(v));if(o.size!==1)return null;const r=N(n),a=N(t);return r===null||a===null||Math.abs((r-a+540)%360-180)<120?null:`${[...o][0]} increases from ${I(a)} toward ${I(r)} across the wafer.`}function T(e){const n=w(e);if(n.length===0)return"";const t=["sector","quadrant","ring"],o=new Map;for(const i of n){const s=o.get(i.comparison.family)??[];s.push(i),o.set(i.comparison.family,s)}const r=[...o.keys()].sort((i,s)=>{const l=Math.min(...o.get(i).map(m)),f=Math.min(...o.get(s).map(m));return l!==f?l-f:t.indexOf(i)-t.indexOf(s)})[0],a=o.get(r),c=O(a);return c||k(r==="ring"?A:r==="quadrant"?B:P,a)}function L(e,n){switch(e){case"cluster":return q(n);case"edge-arc":return x(n);case"reticle-position":return k(C,n);default:return""}}const R=new Set(["ring","quadrant","sector"]);export function buildFindingsNarrative(e){if(e.length===0)return"";const n=e.filter(s=>s.severity!=="info");if(n.length===0)return"Minor spatial variation detected; no strongly significant patterns.";const t=[],o=n.filter(s=>s.comparison.family==="spatial-pattern"),r=o.length?b(o):null,a=new Set(r?.relatedIds??[]);r&&t.push(Y(r,n));const c=n.filter(s=>s!==r&&s.comparison.family!=="spatial-pattern"&&!a.has(s.id));let u=c.filter(s=>R.has(s.comparison.family));if(r&&u.length){const s=w(u);s.length>0&&s.every(f=>f.variable.kind==="yield"&&f.effect.direction==="higher")&&(u=[])}const i=new Set;if(u.length){const s=T(u);if(s){t.push(s);for(const l of R)i.add(l)}}else if(!r){const s=b(c);if(s){const l=s.comparison.family,f=L(l,c.filter(g=>g.comparison.family===l));f&&(t.push(f),i.add(l))}}if(t.length<3){const s=c.filter(l=>(l.comparison.family==="cluster"||l.comparison.family==="edge-arc")&&!i.has(l.comparison.family));if(s.length){const l=s[0].comparison.family,f=L(l,s.filter(g=>g.comparison.family===l));f&&t.push(f)}}return t.slice(0,3).join(" ")}
1
+ const y={unusual:0,notable:1,info:2},S={E:0,ENE:22.5,NE:45,NNE:67.5,N:90,NNW:112.5,NW:135,WNW:157.5,W:180,WSW:202.5,SW:225,SSW:247.5,S:270,SSE:292.5,SE:315,ESE:337.5};function m(e){return e.stats.adjustedPValue??e.stats.pValue??1}function b(e){return[...e].sort((n,t)=>{const o=y[n.severity]-y[t.severity];return o!==0?o:m(n)-m(t)})[0]}function w(e){return e.some(t=>t.variable.kind==="yield")?e.filter(t=>t.variable.kind!=="hardBin"&&t.variable.kind!=="softBin"):e}function $(e){return e.replace(/^(Rings?|Quadrants?|Sectors?)\s+/i,"").split(/[,&]|–|\band\b/).map(n=>n.trim()).filter(Boolean)}function A(e,n){const t=new Set($(e));return $(n).some(o=>t.has(o))}function v(e){return e.variable.kind==="yield"?"yield":e.variable.kind==="hardBin"?`HBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.kind==="softBin"?`SBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.label}function h(e){let n=0,t=0;for(const o of e)o.effect.direction==="higher"?n++:o.effect.direction==="lower"&&t++;return n>t?"higher":t>n?"lower":"mixed"}function d(e){return e==="higher"?"elevated":e==="lower"?"reduced":"shifted"}function p(e){const n=new Set,t=[];for(const o of e){const r=v(o);n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:t.length===3?`${t[0]}, ${t[1]}, and ${t[2]}`:`${t[0]}, ${t[1]}, and ${t.length-2} more`}function E(e){const n=new Set,t=[];for(const o of e){const r=o.comparison.left;n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`}function R(e){return e.length!==1?!1:!/^(Rings|Quadrants|Sectors)\b/.test(e[0].comparison.left)}function B(e){const n=h(e),t=e.every(c=>c.comparison.left.includes("(edge)")),o=e.every(c=>c.comparison.left.includes("(core)"));let r,a;return o?(r="The core ring",a=!0):t&&e.length>1?(r="The edge rings",a=!1):(r=E(e),a=R(e)),`${r} show${a?"s":""} ${d(n)} ${p(e)}.`}function I(e){const n=h(e),t=s=>s.highlight.kind==="region"&&s.highlight.regionKeys.length>1||s.comparison.left.startsWith("Quadrants"),o=e.filter(s=>t(s)),r=e.filter(s=>!t(s)),a=[];r.length===1?a.push(`the ${r[0].comparison.left} quadrant`):r.length>1&&a.push(`the ${E(r)} quadrants`);for(const s of o)a.push(s.comparison.left.replace(/^Quadrants/,"quadrants"));const c=a.join(" and "),u=r.length===1&&o.length===0;return`${c.charAt(0).toUpperCase()+c.slice(1)} show${u?"s":""} ${d(n)} ${p(e)}.`}function P(e){const n=h(e),t=[],o=new Set;for(const u of e){const i=u.comparison.left.replace(/^Sectors? /,"");o.has(i)||(o.add(i),t.push(i))}let r,a;return t.length===1?(r=`the ${t[0]} ${e[0].comparison.left.startsWith("Sectors")?"sectors":"region"}`,a=!e[0].comparison.left.startsWith("Sectors")):(r=`the ${t.length===2?t.join(" and "):`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`} sectors`,a=!1),`${r.charAt(0).toUpperCase()+r.slice(1)} show${a?"s":""} ${d(n)} ${p(e)}.`}function x(e){if(e.length===1)return`A failure cluster at ${e[0].comparison.left.replace(/^Cluster at /,"")}.`;const t=e.reduce((o,r)=>(o.stats.sampleSizeLeft??0)>=(r.stats.sampleSizeLeft??0)?o:r).comparison.left.replace(/^Cluster at /,"");return`${e.length} failure clusters identified; the largest at ${t}.`}function C(e){const n=e[0].comparison.left.replace(/^Edge arc ~/,"");return e.length===1?`An edge arc near ${n} shows localised failures.`:`${e.length} edge arcs detected, including near ${n}.`}function q(e){const n=h(e);return e.length===1?`Reticle-position variation: ${e[0].comparison.left} shows ${d(n)} ${p(e)}.`:`Reticle-position variation across ${e.length} cells (${d(n)} ${p(e)}).`}function F(e,n){const t=e.stats.method==="geometry"?` (${e.severity==="unusual"?"high":e.severity==="notable"?"medium":"low"} confidence)`:"",o=new Set(e.relatedIds??[]),r=n.find(c=>o.has(c.id)&&(c.comparison.family==="edge-arc"||c.comparison.family==="cluster"));let a="";return r&&(r.comparison.family==="edge-arc"?a=`; failures concentrated at the wafer edge near ${r.comparison.left.replace(/^Edge arc ~/,"")}`:a=`; failures concentrated near ${r.comparison.left.replace(/^Cluster at /,"")}`),`${e.comparison.left} failure pattern detected${t}${a}.`}function Y(e){const n=e.replace(/\.$/,"");return n.charAt(0).toLowerCase()+n.slice(1)}function M(e){const n=e.filter(t=>t.variable.kind==="yield");return n.length>0?h(n):h(e)}function k(e,n){const t=new Map;for(const i of n){const s=t.get(i.comparison.left)??[];s.push(i),t.set(i.comparison.left,s)}const o=[],r=[];for(const i of t.values()){const s=M(i);s==="higher"?o.push(...i.filter(l=>l.effect.direction==="higher")):s==="lower"?r.push(...i.filter(l=>l.effect.direction==="lower")):o.push(...i)}const a=o.map(i=>i.comparison.left),c=r.filter(i=>!a.some(s=>A(s,i.comparison.left)));if(o.length>0&&c.length>0){const i=n.some(f=>f.variable.kind==="yield"),[s,l]=i?[c,o]:[o,c];return`${e(s).replace(/\.$/,"")} while ${Y(e(l))}.`}if(o.length>0&&r.length>0&&c.length===0)return e(o);const u=M(n);if(u!=="mixed"&&n.some(i=>i.variable.kind==="yield")){const i=n.filter(s=>s.effect.direction===u);if(i.length>0)return e(i)}return e(n)}function N(e){let n=0,t=0,o=0;for(const r of e)for(const a of $(r.comparison.left)){const c=S[a];if(c===void 0)continue;const u=c*Math.PI/180;n+=Math.cos(u),t+=Math.sin(u),o++}return o===0||n===0&&t===0?null:(Math.atan2(t,n)*180/Math.PI+360)%360}function W(e){let n="E",t=360;for(const[o,r]of Object.entries(S)){const a=Math.abs((e-r+540)%360-180);a<t&&(t=a,n=o)}return n}function D(e){const n=e.filter(u=>u.effect.direction==="higher"),t=e.filter(u=>u.effect.direction==="lower");if(n.length===0||t.length===0)return null;const o=new Set(e.map(v));if(o.size!==1)return null;const r=N(n),a=N(t);return r===null||a===null||Math.abs((r-a+540)%360-180)<120?null:`${[...o][0]} increases from ${W(a)} toward ${W(r)} across the wafer.`}function O(e){const n=w(e);if(n.length===0)return"";const t=["sector","quadrant","ring"],o=new Map;for(const i of n){const s=o.get(i.comparison.family)??[];s.push(i),o.set(i.comparison.family,s)}const r=[...o.keys()].sort((i,s)=>{const l=Math.min(...o.get(i).map(m)),f=Math.min(...o.get(s).map(m));return l!==f?l-f:t.indexOf(i)-t.indexOf(s)})[0],a=o.get(r),c=D(a);return c||k(r==="ring"?B:r==="quadrant"?I:P,a)}function j(e,n){switch(e){case"cluster":return x(n);case"edge-arc":return C(n);case"reticle-position":return k(q,n);default:return""}}const L=new Set(["ring","quadrant","sector"]);export function buildFindingsNarrative(e){if(e.length===0)return"";const n=e.filter(s=>s.severity!=="info");if(n.length===0)return"Minor spatial variation detected; no strongly significant patterns.";const t=[],o=n.filter(s=>s.comparison.family==="spatial-pattern"),r=o.length?b(o):null,a=new Set(r?.relatedIds??[]);r&&t.push(F(r,n));const c=n.filter(s=>s!==r&&s.comparison.family!=="spatial-pattern"&&!a.has(s.id));let u=c.filter(s=>L.has(s.comparison.family));if(r&&u.length){const s=w(u);s.length>0&&s.every(f=>f.variable.kind==="yield"&&f.effect.direction==="higher")&&(u=[])}const i=new Set;if(u.length){const s=O(u);if(s){t.push(s);for(const l of L)i.add(l)}}else if(!r){const s=b(c);if(s){const l=s.comparison.family,f=j(l,c.filter(g=>g.comparison.family===l));f&&(t.push(f),i.add(l))}}if(t.length<3){const s=c.filter(l=>(l.comparison.family==="cluster"||l.comparison.family==="edge-arc")&&!i.has(l.comparison.family));if(s.length){const l=s[0].comparison.family,f=j(l,s.filter(g=>g.comparison.family===l));f&&t.push(f)}}return t.slice(0,3).join(" ")}
@@ -27,7 +27,25 @@ export interface HistogramSeriesData {
27
27
  * equal-width buckets. If `limitLow`/`limitHigh` are given, the axis range
28
28
  * is expanded to include them so limit lines always draw.
29
29
  */
30
- export declare function buildTestHistogramData(items: HistogramItem[], testNumber: number, bucketCount?: number, limitLow?: number, limitHigh?: number): HistogramBucket[];
30
+ /** Every finite recorded value for one test across `items`. Exported because a
31
+ * panel that wants to derive a robust fence needs the raw population, and
32
+ * re-walking dies in the chart layer would be a second copy of this loop. */
33
+ export declare function collectTestValues(items: HistogramItem[], testNumber: number): number[];
34
+ export declare function buildTestHistogramData(items: HistogramItem[], testNumber: number, bucketCount?: number, limitLow?: number, limitHigh?: number,
35
+ /**
36
+ * Bound the bucket range and DROP values outside it. Unlike `limitLow`/
37
+ * `limitHigh`, which only ever widen the range, this narrows it — for the
38
+ * panel's "Clip outliers" axis control, where one wild reading otherwise
39
+ * compresses every real bucket into the first column.
40
+ *
41
+ * Affects this chart only. No statistic anywhere is computed from a clipped
42
+ * population: an out-of-spec die is a distribution outlier by construction, so
43
+ * excluding it from yield or capability would delete real failures.
44
+ */
45
+ clip?: {
46
+ lo: number;
47
+ hi: number;
48
+ }): HistogramBucket[];
31
49
  /**
32
50
  * Faceted histogram: one count-series per group over a *shared* set of
33
51
  * buckets, so the series overlay and compare directly. The bucket range
@@ -1 +1 @@
1
- export function buildTestHistogramData(g,M,a=16,m,h){const n=[];for(const e of g)for(const i of e.dies??[]){const t=i.testValues?.[M];t!==void 0&&Number.isFinite(t)&&n.push(t)}if(n.length===0)return[];let s=n[0],o=n[0];for(let e=1;e<n.length;e++)n[e]<s&&(s=n[e]),n[e]>o&&(o=n[e]);const f=m!==void 0?Math.min(s,m):s,u=((h!==void 0?Math.max(o,h):o)-f||1)/a,d=Array.from({length:a},(e,i)=>({rangeLow:f+i*u,rangeHigh:f+(i+1)*u,count:0}));for(const e of n){const i=Math.min(a-1,Math.floor((e-f)/u));d[i].count++}return d}export function buildTestHistogramSeries(g,M,a=16,m,h){const n=new Map;let s=1/0,o=-1/0;for(const t of g){const r=[];for(const x of t.items)for(const p of x.dies??[]){const c=p.testValues?.[M];c!==void 0&&Number.isFinite(c)&&(r.push(c),c<s&&(s=c),c>o&&(o=c))}n.set(t.key,r)}const f=g.map(t=>t.key).filter(t=>n.get(t).length>0);if(f.length===0)return{ranges:[],series:[]};const l=m!==void 0?Math.min(s,m):s,d=((h!==void 0?Math.max(o,h):o)-l||1)/a,e=Array.from({length:a},(t,r)=>({rangeLow:l+r*d,rangeHigh:l+(r+1)*d})),i=f.map(t=>{const r=new Array(a).fill(0);for(const x of n.get(t)){const p=Math.min(a-1,Math.floor((x-l)/d));r[p]++}return{groupKey:t,counts:r}});return{ranges:e,series:i}}
1
+ export function collectTestValues(h,u){const s=[];for(const f of h)for(const c of f.dies??[]){const o=c.testValues?.[u];o!==void 0&&Number.isFinite(o)&&s.push(o)}return s}export function buildTestHistogramData(h,u,s=16,f,c,o){let n=collectTestValues(h,u);if(o&&(n=n.filter(e=>e>=o.lo&&e<=o.hi)),n.length===0)return[];let a=n[0],d=n[0];for(let e=1;e<n.length;e++)n[e]<a&&(a=n[e]),n[e]>d&&(d=n[e]);const i=f!==void 0?Math.min(a,f):a,l=((c!==void 0?Math.max(d,c):d)-i||1)/s,g=Array.from({length:s},(e,t)=>({rangeLow:i+t*l,rangeHigh:i+(t+1)*l,count:0}));for(const e of n){const t=Math.min(s-1,Math.floor((e-i)/l));g[t].count++}return g}export function buildTestHistogramSeries(h,u,s=16,f,c){const o=new Map;let n=1/0,a=-1/0;for(const t of h){const r=[];for(const x of t.items)for(const M of x.dies??[]){const m=M.testValues?.[u];m!==void 0&&Number.isFinite(m)&&(r.push(m),m<n&&(n=m),m>a&&(a=m))}o.set(t.key,r)}const d=h.map(t=>t.key).filter(t=>o.get(t).length>0);if(d.length===0)return{ranges:[],series:[]};const i=f!==void 0?Math.min(n,f):n,l=((c!==void 0?Math.max(a,c):a)-i||1)/s,g=Array.from({length:s},(t,r)=>({rangeLow:i+r*l,rangeHigh:i+(r+1)*l})),e=d.map(t=>{const r=new Array(s).fill(0);for(const x of o.get(t)){const M=Math.min(s-1,Math.floor((x-i)/l));r[M]++}return{groupKey:t,counts:r}});return{ranges:g,series:e}}
@@ -12,12 +12,18 @@ export { buildFacetTable, facetValueOf, FACET_NONE_VALUE, DEFAULT_FACET_CURATION
12
12
  export type { FacetField, FacetValue, FacetCuration, FacetItem, BuildFacetTableOptions } from './facets.js';
13
13
  export { resolveMetadataColumns, discoverDieMetadataKeys } from './metadataColumns.js';
14
14
  export type { MetadataColumn, MetadataColumnScope, MetadataColumnSet, MetadataKeySelection, ResolveMetadataColumnsOptions, } from './metadataColumns.js';
15
+ export { mergeTestDefs } from './mergeTestDefs.js';
16
+ export type { MergedTestDefs, TestDefConflict, TestDefConflictKind } from './mergeTestDefs.js';
15
17
  export { buildCapabilityData } from './capability.js';
16
18
  export type { CapabilityDatum, CapabilityItem } from './capability.js';
17
19
  export { buildCorrelationMatrix, filterCorrelationMatrix } from './correlation.js';
18
20
  export type { CorrelationTestInfo, CorrelationCell, CorrelationMatrix, CorrelationSummary } from './correlation.js';
19
21
  export { buildTestBoxplotData } from './boxplot.js';
20
22
  export type { BoxplotDatum, BoxplotItem } from './boxplot.js';
23
+ export { buildTestTrendData, trendCentre } from './trend.js';
24
+ export type { TrendDatum, TrendItem } from './trend.js';
25
+ export { buildTestPassRateData, hasJudgeableTests } from './testPassRate.js';
26
+ export type { TestPassKind, TestPassRateData, TestPassRateItem, TestPassRateRow, TestPassRateValue } from './testPassRate.js';
21
27
  export { buildTestHistogramData, buildTestHistogramSeries } from './histogram.js';
22
28
  export type { HistogramBucket, HistogramItem, HistogramSeries, HistogramSeriesData } from './histogram.js';
23
29
  export { buildScatterData, buildScatterDataGrouped } from './scatter.js';
@@ -1 +1 @@
1
- export*from"./types.js";export*from"./regions.js";export*from"./analyzeWaferMap.js";export*from"./analyzeWaferLot.js";export*from"./filterFindings.js";export{classifyPattern}from"./patternClassification.js";export{renderFindingsReportHtml,openHtmlReport,setReportOpener}from"./renderFindingsReport.js";export{renderSummaryReportHtml,renderLotSummaryReportHtml}from"./renderSummaryReport.js";export{buildFacetTable,facetValueOf,FACET_NONE_VALUE,DEFAULT_FACET_CURATION}from"./facets.js";export{resolveMetadataColumns,discoverDieMetadataKeys}from"./metadataColumns.js";export{buildCapabilityData}from"./capability.js";export{buildCorrelationMatrix,filterCorrelationMatrix}from"./correlation.js";export{buildTestBoxplotData}from"./boxplot.js";export{buildTestHistogramData,buildTestHistogramSeries}from"./histogram.js";export{buildScatterData,buildScatterDataGrouped}from"./scatter.js";export{buildYieldData,buildYieldDataCombined}from"./yield.js";export{buildBinParetoData,buildBinClusterData}from"./binPareto.js";
1
+ export*from"./types.js";export*from"./regions.js";export*from"./analyzeWaferMap.js";export*from"./analyzeWaferLot.js";export*from"./filterFindings.js";export{classifyPattern}from"./patternClassification.js";export{renderFindingsReportHtml,openHtmlReport,setReportOpener}from"./renderFindingsReport.js";export{renderSummaryReportHtml,renderLotSummaryReportHtml}from"./renderSummaryReport.js";export{buildFacetTable,facetValueOf,FACET_NONE_VALUE,DEFAULT_FACET_CURATION}from"./facets.js";export{resolveMetadataColumns,discoverDieMetadataKeys}from"./metadataColumns.js";export{mergeTestDefs}from"./mergeTestDefs.js";export{buildCapabilityData}from"./capability.js";export{buildCorrelationMatrix,filterCorrelationMatrix}from"./correlation.js";export{buildTestBoxplotData}from"./boxplot.js";export{buildTestTrendData,trendCentre}from"./trend.js";export{buildTestPassRateData,hasJudgeableTests}from"./testPassRate.js";export{buildTestHistogramData,buildTestHistogramSeries}from"./histogram.js";export{buildScatterData,buildScatterDataGrouped}from"./scatter.js";export{buildYieldData,buildYieldDataCombined}from"./yield.js";export{buildBinParetoData,buildBinClusterData}from"./binPareto.js";
@@ -0,0 +1,44 @@
1
+ import type { TestDef, WaferWarning } from '../renderer/buildWaferMap.js';
2
+ /** Which field disagreed. `'limits'` is the soft tier; the rest are hard. */
3
+ export type TestDefConflictKind = 'name' | 'unit' | 'testType' | 'limits';
4
+ /** One test number whose defs could not be reconciled across the population. */
5
+ export interface TestDefConflict {
6
+ testNumber: number;
7
+ kind: TestDefConflictKind;
8
+ /** True when the test was withheld from `defs` (the hard tier). */
9
+ excluded: boolean;
10
+ /**
11
+ * The distinct STATED values that disagreed, in first-seen order, already
12
+ * stringified for display. Absent values are not represented — they were
13
+ * never in disagreement with anything.
14
+ */
15
+ values: string[];
16
+ }
17
+ export interface MergedTestDefs {
18
+ /**
19
+ * The union of every test number across every item, each carrying a def
20
+ * reconciled from all items that described it. Hard-conflicting tests are
21
+ * absent. Ordered by first appearance, so a single-program population gets
22
+ * back exactly the order it supplied.
23
+ */
24
+ defs: TestDef[];
25
+ /** Every unreconcilable test, both tiers. Empty for a clean population. */
26
+ conflicts: TestDefConflict[];
27
+ /**
28
+ * `conflicts` rendered as structured advisories, ready to hand to
29
+ * `collectWarnings` so the toolbar indicator and Summary banner report them
30
+ * with no host work. At most one warning per severity — a program mismatch
31
+ * hits many tests at once, and one line per test would bury everything else.
32
+ */
33
+ warnings: WaferWarning[];
34
+ }
35
+ /**
36
+ * Reconcile every item's `testDefs` into one list for the whole population.
37
+ *
38
+ * Items with no defs are skipped, not treated as disagreeing — a wafer that
39
+ * describes nothing contradicts nothing.
40
+ */
41
+ export declare function mergeTestDefs(items: Array<{
42
+ testDefs?: TestDef[];
43
+ } | null | undefined>): MergedTestDefs;
44
+ //# sourceMappingURL=mergeTestDefs.d.ts.map
@@ -0,0 +1 @@
1
+ const k=1e-6;function w(t,e){if(t===e)return!0;if(!Number.isFinite(t)||!Number.isFinite(e))return!1;const i=Math.max(Math.abs(t),Math.abs(e));return Math.abs(t-e)<=1e-6*i}function v(t){const e=t.name?.trim();return e||void 0}function y(t){const e=t.unit?.trim();return e||void 0}function L(t){return t.toLowerCase()}function h(t,e){const i=new Set,s=[];for(const o of t){const r=e(o);i.has(r)||(i.add(r),s.push(o))}return s}function g(t){const e=[];for(const i of t)e.some(s=>w(s,i))||e.push(i);return e}function T(t){return`${t.testNumber} (${t.values.join(" vs ")})`}function b(t,e=5){const i=t.slice(0,e).map(T).join(", "),s=t.length-Math.min(e,t.length);return s>0?`${i}, and ${s} more`:i}export function mergeTestDefs(t){const e=new Map,i=[];for(const r of t)for(const u of r?.testDefs??[]){const f=u.testNumber;let d=e.get(f);d||(d=[],e.set(f,d),i.push(f)),d.push(u)}const s=[],o=[];for(const r of i){const u=e.get(r),f=h(u.map(v).filter(n=>n!==void 0),L),d=h(u.map(y).filter(n=>n!==void 0),n=>n),l=h(u.map(n=>n.testType).filter(n=>n!==void 0),n=>n),p=f.length>1?{testNumber:r,kind:"name",excluded:!0,values:f}:d.length>1?{testNumber:r,kind:"unit",excluded:!0,values:d}:l.length>1?{testNumber:r,kind:"testType",excluded:!0,values:l}:void 0;if(p){o.push(p);continue}const a=g(u.map(n=>n.limitLow).filter(n=>n!==void 0)),c=g(u.map(n=>n.limitHigh).filter(n=>n!==void 0)),m=a.length>1||c.length>1;m&&o.push({testNumber:r,kind:"limits",excluded:!1,values:(a.length>1?a:c).map(n=>String(n))}),s.push({testNumber:r,name:f[0]??"",...d[0]!==void 0?{unit:d[0]}:{},...l[0]!==void 0?{testType:l[0]}:{},...u.find(n=>n.logScale!==void 0)?{logScale:u.find(n=>n.logScale!==void 0).logScale}:{},...!m&&a[0]!==void 0?{limitLow:a[0]}:{},...!m&&c[0]!==void 0?{limitHigh:c[0]}:{}})}return{defs:s,conflicts:o,warnings:N(o)}}function N(t){const e=[],i=t.filter(o=>o.excluded),s=t.filter(o=>o.kind==="limits");return i.length>0&&e.push({code:"test-def-collision",severity:"error",message:`${i.length} test ${i.length===1?"number describes":"numbers describe"} a different measurement in different wafers, so the same number cannot identify one test across this population. Affected: `+b(i)+". These tests are withheld from every cross-wafer chart, report and map mode \u2014 pooling them would mix unrelated measurements into one distribution. Each wafer's own view is unaffected. Load one test program at a time to see them."}),s.length>0&&e.push({code:"test-limit-conflict",severity:"warning",message:`${s.length} ${s.length===1?"test is":"tests are"} held to different spec limits in different wafers. Affected: `+b(s)+". The measured values are still comparable, so distributions include them, but capability (Cp/Cpk/Pp/Ppk), spec yield and the limit lines are withheld for these tests \u2014 there is no single spec to judge the merged population against."}),e}
@@ -5,7 +5,21 @@ import type { MetadataFieldDef } from '../renderer/buildWaferMap.js';
5
5
  * every row; a `'die'` column genuinely varies die to die. */
6
6
  export type MetadataColumnScope = 'die' | 'wafer';
7
7
  /** How metadata keys are chosen. `'auto'` = every key present in the data. */
8
- export type MetadataKeySelection = 'auto' | 'none' | string[];
8
+ /**
9
+ * `'auto'` — every displayable key.
10
+ * `'identity'` — only the curated keys that identify a population (lot, wafer,
11
+ * product, program, split, operator, date…), i.e. `DEFAULT_FACET_CURATION`.
12
+ * `'none'` — no columns.
13
+ * `string[]` — exactly these keys, when present.
14
+ *
15
+ * `'identity'` exists because `'auto'` is the wrong default for a per-test
16
+ * export: a host that maps an STDF header into wafer metadata carries WCR
17
+ * geometry (`Center X`, `Die Ht`, `Wf Flat`, `Wafr Siz`, `Pos X`…) alongside the
18
+ * identity fields, and `'auto'` stamped all fifteen of them as constant leading
19
+ * columns on every row — pushing the actual statistics off the right of the
20
+ * screen while answering none of "which wafer is this?".
21
+ */
22
+ export type MetadataKeySelection = 'auto' | 'identity' | 'none' | string[];
9
23
  export interface MetadataColumn {
10
24
  /** The metadata key this column reads. */
11
25
  key: string;
@@ -62,9 +76,11 @@ export declare function discoverDieMetadataKeys(dies: Die[], metadataFields?: Me
62
76
  * Build the metadata column set for a population.
63
77
  *
64
78
  * A key present on both a die and its wafer yields exactly ONE column, scope
65
- * `'die'`, carrying the die value — the same shadowing rule the hover tooltip
66
- * applies with `{ ...waferMeta, ...die.metadata }`. Two columns for one key
67
- * could otherwise disagree in the same row.
79
+ * `'die'`, carrying the die value. Two columns for one key could otherwise
80
+ * disagree in the same row. (This used to cite the hover tooltip as applying the
81
+ * same `{ ...waferMeta, ...die.metadata }` shadowing; the tooltip no longer shows
82
+ * wafer metadata at all — that identity moved to the identity header — so the
83
+ * rule now stands on its own reasoning rather than a parallel that has gone.)
68
84
  */
69
85
  export declare function resolveMetadataColumns(o: ResolveMetadataColumnsOptions): MetadataColumnSet;
70
86
  //# sourceMappingURL=metadataColumns.d.ts.map
@@ -1 +1 @@
1
- import{metadataDisplayValue as k}from"../core/metadata.js";import{compareNatural as w,prettyKey as h}from"../core/utils.js";import{DEFAULT_FACET_CURATION as v}from"./facets.js";const K=64;export function discoverDieMetadataKeys(e,n,c=K){const d=new Set;for(const a of e)if(a.metadata)for(const i of Object.keys(a.metadata))d.add(i);const t=[];for(const a of n??[])d.has(a.key)&&!t.includes(a.key)&&t.push(a.key);const y=[...d].filter(a=>!t.includes(a)).sort(w),r=[...t,...y];return{keys:r.slice(0,c),truncated:r.slice(c)}}function A(e){const n=Object.keys(e).filter(t=>k(e[t])!==void 0),c=Object.keys(v).filter(t=>n.includes(t)),d=n.filter(t=>!c.includes(t)).sort(w);return[...c,...d]}const b=(e,n)=>e==="none"?[]:Array.isArray(e)?e.filter(c=>n.includes(c)):n;export function resolveMetadataColumns(e){const n=e.waferMetadata??{},c=e.waferPlacement??"csv",d=discoverDieMetadataKeys(e.dies??[],e.metadataFields,e.maxDieKeys??K),t=b(e.dieKeys,d.keys),y=c==="none"?[]:b(e.waferKeys,A(n)).filter(s=>!t.includes(s)),r=new Set(e.reservedLabels??[]),a=(s,o)=>{const f=e.metadataFields?.find(u=>u.key===s)?.label??h(s);if(!r.has(f))return r.add(f),f;const m=o==="die"?`${f} (metadata)`:`${f} (wafer metadata)`;if(!r.has(m))return r.add(m),m;const l=`${f} (${s})`;if(!r.has(l))return r.add(l),l;for(let u=2;;u++){const p=`${l} (${u})`;if(!r.has(p))return r.add(p),p}},i=[];for(const s of t)i.push({key:s,label:a(s,"die"),scope:"die",get:o=>k(o.metadata?.[s])??"",csvOnly:!1});for(const s of y){const o=k(n[s])??"";i.push({key:s,label:a(s,"wafer"),scope:"wafer",get:()=>o,constant:o,csvOnly:c==="csv"})}return{columns:i,truncatedKeys:d.truncated}}
1
+ import{metadataDisplayValue as k}from"../core/metadata.js";import{compareNatural as w,prettyKey as A}from"../core/utils.js";import{DEFAULT_FACET_CURATION as K}from"./facets.js";const b=64;export function discoverDieMetadataKeys(e,n,s=b){const d=new Set;for(const c of e)if(c.metadata)for(const i of Object.keys(c.metadata))d.add(i);const t=[];for(const c of n??[])d.has(c.key)&&!t.includes(c.key)&&t.push(c.key);const y=[...d].filter(c=>!t.includes(c)).sort(w),a=[...t,...y];return{keys:a.slice(0,s),truncated:a.slice(s)}}function v(e){const n=Object.keys(e).filter(t=>k(e[t])!==void 0),s=Object.keys(K).filter(t=>n.includes(t)),d=n.filter(t=>!s.includes(t)).sort(w);return[...s,...d]}const h=(e,n)=>e==="none"?[]:Array.isArray(e)?e.filter(s=>n.includes(s)):e==="identity"?n.filter(s=>s in K):n;export function resolveMetadataColumns(e){const n=e.waferMetadata??{},s=e.waferPlacement??"csv",d=discoverDieMetadataKeys(e.dies??[],e.metadataFields,e.maxDieKeys??b),t=h(e.dieKeys,d.keys),y=s==="none"?[]:h(e.waferKeys,v(n)).filter(r=>!t.includes(r)),a=new Set(e.reservedLabels??[]),c=(r,o)=>{const f=e.metadataFields?.find(u=>u.key===r)?.label??A(r);if(!a.has(f))return a.add(f),f;const m=o==="die"?`${f} (metadata)`:`${f} (wafer metadata)`;if(!a.has(m))return a.add(m),m;const l=`${f} (${r})`;if(!a.has(l))return a.add(l),l;for(let u=2;;u++){const p=`${l} (${u})`;if(!a.has(p))return a.add(p),p}},i=[];for(const r of t)i.push({key:r,label:c(r,"die"),scope:"die",get:o=>k(o.metadata?.[r])??"",csvOnly:!1});for(const r of y){const o=k(n[r])??"";i.push({key:r,label:c(r,"wafer"),scope:"wafer",get:()=>o,constant:o,csvOnly:s==="csv"})}return{columns:i,truncatedKeys:d.truncated}}
@@ -1 +1 @@
1
- import{getDieKey as F}from"../core/dies.js";import{clamp01 as j}from"../core/utils.js";export const DEFAULT_PATTERN_THRESHOLDS={nearFullGlobalRdd:.6,edgeRingEdgeRdd:.18,edgeRingEdgeRddHigh:.3,edgeRingRadialSpread:.45,edgeRingAngularSpread:.6,edgeLocalEdgeRdd:.1,edgeLocalCentroidDist:.76,donutP25Dist:.4,centerCentroidDist:.22,centerCentroidDistHigh:.1,centerEdgeRdd:.35,donutEdgeRdd:.22,scratchLinearScore:.25,scratchLinearScoreHigh:.45,scratchEccentricity:.75,minimumFailingDies:5};function J(t){if(t.length===0)return[];const e=new Map;for(const s of t)e.set(F(s),s);const c=new Set,i=[];for(const s of t){const d=F(s);if(c.has(d))continue;const r=[],n=[s];for(c.add(d);n.length>0;){const a=n.pop();r.push(a);for(let g=-1;g<=1;g++)for(let m=-1;m<=1;m++){if(g===0&&m===0)continue;const u=`${a.x+g},${a.y+m}`;!c.has(u)&&e.has(u)&&(c.add(u),n.push(e.get(u)))}}i.push(r)}return i}function O(t,e,c){const i=t.physX-e.center.x,s=t.physY-e.center.y,d=Math.sqrt(i*i+s*s)/e.radius;return Math.min(c,Math.max(1,Math.floor(d*c)+1))===c}function Q(t,e,c){if(t.length<3)return 0;let i=0,s=0,d=0;for(const p of t){const l=p.physX-e,D=p.physY-c;i+=l*l,s+=D*D,d+=l*D}i/=t.length,s/=t.length,d/=t.length;const r=i+s,n=i*s-d*d,a=Math.max(0,(r/2)**2-n),g=r/2+Math.sqrt(a),m=r/2-Math.sqrt(a);if(g<=0)return 0;const u=j(m/g);return Math.sqrt(1-u)}function V(t,e){if(e===0)return 0;const c=new Map,i=new Map,s=new Map,d=new Map;for(const n of t)c.set(n.y,(c.get(n.y)??0)+1),i.set(n.x,(i.get(n.x)??0)+1),s.set(n.x-n.y,(s.get(n.x-n.y)??0)+1),d.set(n.x+n.y,(d.get(n.x+n.y)??0)+1);let r=0;for(const n of c.values())n>r&&(r=n);for(const n of i.values())n>r&&(r=n);for(const n of s.values())n>r&&(r=n);for(const n of d.values())n>r&&(r=n);return r/e}function W(t,e,c,i){const s=e.length,d=t.length,r=s>0?d/s:0,n=e.filter(o=>O(o,c,i)),a=t.filter(o=>O(o,c,i)),g=n.length>0?a.length/n.length:0,m=J(t),u=m.length>0?[...m].sort((o,h)=>h.length-o.length):[t],p=u[0],l=u.slice(0,5).flat(),D=p.length,H=d>0?D/d:0,f=c.center.x,y=c.center.y,R=c.radius;let E=0,N=0,M=1/0,x=0;for(const o of p){E+=o.physX,N+=o.physY;const h=Math.sqrt((o.physX-f)**2+(o.physY-y)**2);h<M&&(M=h),h>x&&(x=h)}const P=E/p.length,T=N/p.length,w=Math.sqrt((P-f)**2+(T-y)**2)/R,I=M===1/0?0:M/R,v=Math.min(x/R,1),C=t.map(o=>Math.sqrt((o.physX-f)**2+(o.physY-y)**2)/R).sort((o,h)=>o-h),k=C[Math.floor(C.length*.25)]??0,b=e.filter(o=>Math.sqrt((o.physX-f)**2+(o.physY-y)**2)/R<.5).length,X=e.filter(o=>Math.sqrt((o.physX-f)**2+(o.physY-y)**2)/R>=.5).length,z=t.filter(o=>Math.sqrt((o.physX-f)**2+(o.physY-y)**2)/R<.5).length,A=t.filter(o=>Math.sqrt((o.physX-f)**2+(o.physY-y)**2)/R>=.5).length,Y=b>0?z/b:0,q=X>0?A/X:0,_=q>0?Y/q:Y>0?2:1,G=l.reduce((o,h)=>o+h.physX,0)/l.length,K=l.reduce((o,h)=>o+h.physY,0)/l.length,U=Q(l,G,K),$=V(l,l.length),S=16,L=new Set;for(const o of a){const h=(Math.atan2(o.physY-y,o.physX-f)+2*Math.PI)%(2*Math.PI);L.add(Math.floor(h/(2*Math.PI)*S)%S)}const B=a.length>0?L.size/S:0;return{globalRdd:r,edgeRdd:g,centroidDistNorm:w,minDistNorm:I,maxDistNorm:v,p25DistNorm:k,eccentricity:U,linearScore:$,salienceSize:D,salienceFraction:H,edgeAngularSpread:B,innerOuterRatio:_}}function Z(t,e,c){const i=Math.max(5,Math.round(c*.003)),s=t.salienceSize>=i;if(t.globalRdd<e.nearFullGlobalRdd&&t.salienceFraction<.1)return{pattern:"random",confidence:"low"};if(t.globalRdd>=e.nearFullGlobalRdd)return{pattern:"near-full",confidence:t.globalRdd>=.8?"high":"medium"};if(s&&t.linearScore>=e.scratchLinearScore&&t.eccentricity>=e.scratchEccentricity){const a=t.linearScore>=e.scratchLinearScoreHigh?"high":"medium";return{pattern:"scratch",confidence:a,note:a==="medium"?"Scratch detection is less reliable for fragmented or diagonal patterns":void 0}}const d=s&&t.edgeRdd>=e.edgeLocalEdgeRdd&&t.maxDistNorm>=.7;if(d&&t.p25DistNorm>=e.edgeLocalCentroidDist&&t.maxDistNorm-t.minDistNorm<=e.edgeRingRadialSpread)return{pattern:"edge-ring",confidence:t.edgeRdd>=e.edgeRingEdgeRddHigh?"high":"medium"};if(d&&t.p25DistNorm>=.55&&t.p25DistNorm<e.edgeLocalCentroidDist)return{pattern:"edge-local",confidence:t.edgeRdd>=e.edgeRingEdgeRdd?"medium":"low"};const r=s&&t.centroidDistNorm<=e.centerCentroidDist&&t.edgeRdd<=e.centerEdgeRdd,n="Center and donut patterns have similar geometry; classification may be imprecise";if(r&&t.p25DistNorm<e.donutP25Dist){const a=t.centroidDistNorm<=e.centerCentroidDistHigh?"high":"medium";return{pattern:"center",confidence:a,note:a!=="high"?n:void 0}}return r&&t.p25DistNorm>=e.donutP25Dist&&t.p25DistNorm<.55?{pattern:"donut",confidence:"medium",note:n}:{pattern:"random",confidence:"low"}}export function classifyPattern(t,e,c){const i={...DEFAULT_PATTERN_THRESHOLDS},s=c.ringCount??4,d=new Set(c.passBins),r=t.filter(p=>{const l=p.hbin??p.sbin;return l!==void 0&&!d.has(l)}),n=Math.max(i.minimumFailingDies,Math.round(t.length*.003));if(r.length<n)return null;const a=W(r,t,e,s),{pattern:g,confidence:m,note:u}=Z(a,i,t.length);return{pattern:g,confidence:m,features:a,...u?{note:u}:{}}}
1
+ import{findConnectedComponents as J}from"./connectedComponents.js";import{clamp01 as K}from"../core/utils.js";export const DEFAULT_PATTERN_THRESHOLDS={nearFullGlobalRdd:.6,edgeRingEdgeRdd:.18,edgeRingEdgeRddHigh:.3,edgeRingRadialSpread:.45,edgeRingAngularSpread:.6,edgeLocalEdgeRdd:.1,edgeLocalCentroidDist:.76,donutP25Dist:.4,centerCentroidDist:.22,centerCentroidDistHigh:.1,centerEdgeRdd:.35,donutEdgeRdd:.22,scratchLinearScore:.25,scratchLinearScoreHigh:.45,scratchEccentricity:.75,minimumFailingDies:5};function F(t,o,s){const i=t.physX-o.center.x,r=t.physY-o.center.y,d=Math.sqrt(i*i+r*r)/o.radius;return Math.min(s,Math.max(1,Math.floor(d*s)+1))===s}function Q(t,o,s){if(t.length<3)return 0;let i=0,r=0,d=0;for(const g of t){const l=g.physX-o,D=g.physY-s;i+=l*l,r+=D*D,d+=l*D}i/=t.length,r/=t.length,d/=t.length;const c=i+r,n=i*r-d*d,a=Math.max(0,(c/2)**2-n),m=c/2+Math.sqrt(a),R=c/2-Math.sqrt(a);if(m<=0)return 0;const f=K(R/m);return Math.sqrt(1-f)}function V(t,o){if(o===0)return 0;const s=new Map,i=new Map,r=new Map,d=new Map;for(const n of t)s.set(n.y,(s.get(n.y)??0)+1),i.set(n.x,(i.get(n.x)??0)+1),r.set(n.x-n.y,(r.get(n.x-n.y)??0)+1),d.set(n.x+n.y,(d.get(n.x+n.y)??0)+1);let c=0;for(const n of s.values())n>c&&(c=n);for(const n of i.values())n>c&&(c=n);for(const n of r.values())n>c&&(c=n);for(const n of d.values())n>c&&(c=n);return c/o}function W(t,o,s,i){const r=o.length,d=t.length,c=r>0?d/r:0,n=o.filter(e=>F(e,s,i)),a=t.filter(e=>F(e,s,i)),m=n.length>0?a.length/n.length:0,R=J(t),f=R.length>0?[...R].sort((e,h)=>h.length-e.length):[t],g=f[0],l=f.slice(0,5).flat(),D=g.length,O=d>0?D/d:0,u=s.center.x,p=s.center.y,y=s.radius;let E=0,N=0,M=1/0,x=0;for(const e of g){E+=e.physX,N+=e.physY;const h=Math.sqrt((e.physX-u)**2+(e.physY-p)**2);h<M&&(M=h),h>x&&(x=h)}const H=E/g.length,P=N/g.length,T=Math.sqrt((H-u)**2+(P-p)**2)/y,I=M===1/0?0:M/y,v=Math.min(x/y,1),C=t.map(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y).sort((e,h)=>e-h),w=C[Math.floor(C.length*.25)]??0,b=o.filter(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y<.5).length,X=o.filter(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y>=.5).length,z=t.filter(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y<.5).length,A=t.filter(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y>=.5).length,Y=b>0?z/b:0,L=X>0?A/X:0,_=L>0?Y/L:Y>0?2:1,G=l.reduce((e,h)=>e+h.physX,0)/l.length,k=l.reduce((e,h)=>e+h.physY,0)/l.length,U=Q(l,G,k),B=V(l,l.length),S=16,q=new Set;for(const e of a){const h=(Math.atan2(e.physY-p,e.physX-u)+2*Math.PI)%(2*Math.PI);q.add(Math.floor(h/(2*Math.PI)*S)%S)}const j=a.length>0?q.size/S:0;return{globalRdd:c,edgeRdd:m,centroidDistNorm:T,minDistNorm:I,maxDistNorm:v,p25DistNorm:w,eccentricity:U,linearScore:B,salienceSize:D,salienceFraction:O,edgeAngularSpread:j,innerOuterRatio:_}}function Z(t,o,s){const i=Math.max(5,Math.round(s*.003)),r=t.salienceSize>=i;if(t.globalRdd<o.nearFullGlobalRdd&&t.salienceFraction<.1)return{pattern:"random",confidence:"low"};if(t.globalRdd>=o.nearFullGlobalRdd)return{pattern:"near-full",confidence:t.globalRdd>=.8?"high":"medium"};if(r&&t.linearScore>=o.scratchLinearScore&&t.eccentricity>=o.scratchEccentricity){const a=t.linearScore>=o.scratchLinearScoreHigh?"high":"medium";return{pattern:"scratch",confidence:a,note:a==="medium"?"Scratch detection is less reliable for fragmented or diagonal patterns":void 0}}const d=r&&t.edgeRdd>=o.edgeLocalEdgeRdd&&t.maxDistNorm>=.7;if(d&&t.p25DistNorm>=o.edgeLocalCentroidDist&&t.maxDistNorm-t.minDistNorm<=o.edgeRingRadialSpread)return{pattern:"edge-ring",confidence:t.edgeRdd>=o.edgeRingEdgeRddHigh?"high":"medium"};if(d&&t.p25DistNorm>=.55&&t.p25DistNorm<o.edgeLocalCentroidDist)return{pattern:"edge-local",confidence:t.edgeRdd>=o.edgeRingEdgeRdd?"medium":"low"};const c=r&&t.centroidDistNorm<=o.centerCentroidDist&&t.edgeRdd<=o.centerEdgeRdd,n="Center and donut patterns have similar geometry; classification may be imprecise";if(c&&t.p25DistNorm<o.donutP25Dist){const a=t.centroidDistNorm<=o.centerCentroidDistHigh?"high":"medium";return{pattern:"center",confidence:a,note:a!=="high"?n:void 0}}return c&&t.p25DistNorm>=o.donutP25Dist&&t.p25DistNorm<.55?{pattern:"donut",confidence:"medium",note:n}:{pattern:"random",confidence:"low"}}export function classifyPattern(t,o,s){const i={...DEFAULT_PATTERN_THRESHOLDS},r=s.ringCount??4,d=new Set(s.passBins),c=t.filter(g=>{const l=g.hbin??g.sbin;return l!==void 0&&!d.has(l)}),n=Math.max(i.minimumFailingDies,Math.round(t.length*.003));if(c.length<n)return null;const a=W(c,t,o,r),{pattern:m,confidence:R,note:f}=Z(a,i,t.length);return{pattern:m,confidence:R,features:a,...f?{note:f}:{}}}
@@ -1,11 +1,11 @@
1
- import{formatFindingDelta as b,formatFindingCoverage as g,formatFindingTooltip as v,escHtml as a,buildMetadataRows as c,renderDefinitionList as p,renderSection as f,renderSeverityBadge as y,reportStyles as R}from"./reportHtml.js";import{buildFindingsNarrative as $}from"./findingsNarrative.js";import{plainBinTerms as u}from"../renderer/fmt.js";function S(e,n){if(e.level==="lot"){const o=e,l=o.perWafer.map(r=>r.summary.wafer?.wafer??r.summary.wafer?.waferId).filter(r=>r!=null).map(String).join(", ");return p([...c(o.perWafer.map(r=>({metadata:r.summary.wafer}))),...l?[{label:"Wafers",value:l}]:[],{label:"Wafer count",value:String(o.stats.waferCount)},{label:"Generated",value:n}])}const t=e,i=t.stats.yieldPercent!==null?`${t.stats.yieldPercent.toFixed(1)}%`:"N/A";return p([...c([{metadata:t.wafer}]),{label:"Total dies",value:String(t.stats.totalDies)},{label:"Analysed dies",value:String(t.stats.analyzedDies)},{label:"Yield",value:i},{label:"Generated",value:n}])}function F(e,n){return e.length?e.map(t=>`<tr title="${a(v(t))}">
2
- <td class="tight">${y(t.severity)}</td>
3
- <td class="tight">${a(t.comparison.left)}</td>
4
- <td>${a(u(t.variable.label))}</td>
5
- <td class="numeric">${a(b(t))}</td>
6
- <td class="numeric">${a(g(t,n))}</td>
1
+ import{formatFindingDelta as h,formatFindingCoverage as w,formatFindingTooltip as b,escHtml as o,buildMetadataRows as d,renderDefinitionList as c,renderSection as p,renderSeverityBadge as g,reportStyles as v}from"./reportHtml.js";import{buildFindingsNarrative as y}from"./findingsNarrative.js";import{plainBinTerms as u}from"../renderer/fmt.js";import{visibleFindings as R}from"./filterFindings.js";function $(e,n){if(e.level==="lot"){const a=e,l=a.perWafer.map(r=>r.summary.wafer?.wafer??r.summary.wafer?.waferId).filter(r=>r!=null).map(String).join(", ");return c([...d(a.perWafer.map(r=>({metadata:r.summary.wafer}))),...l?[{label:"Wafers",value:l}]:[],{label:"Wafer count",value:String(a.stats.waferCount)},{label:"Generated",value:n}])}const t=e,i=t.stats.yieldPercent!==null?`${t.stats.yieldPercent.toFixed(1)}%`:"N/A";return c([...d([{metadata:t.wafer}]),{label:"Total dies",value:String(t.stats.totalDies)},{label:"Analysed dies",value:String(t.stats.analyzedDies)},{label:"Yield",value:i},{label:"Generated",value:n}])}function F(e,n){return e.length?e.map(t=>`<tr title="${o(b(t))}">
2
+ <td class="tight">${g(t.severity)}</td>
3
+ <td class="tight">${o(t.comparison.left)}</td>
4
+ <td>${o(u(t.variable.label))}</td>
5
+ <td class="numeric">${o(h(t))}</td>
6
+ <td class="numeric">${o(w(t,n))}</td>
7
7
  </tr>`).join(`
8
- `):'<tr><td colspan="5" class="no-data">No significant findings</td></tr>'}function T(e,n){return`<table class="report-table findings-table compact">
8
+ `):'<tr><td colspan="5" class="no-data">No significant findings</td></tr>'}function S(e,n){return`<table class="report-table findings-table compact">
9
9
  <thead>
10
10
  <tr>
11
11
  <th>Severity</th>
@@ -18,24 +18,24 @@ import{formatFindingDelta as b,formatFindingCoverage as g,formatFindingTooltip a
18
18
  <tbody>
19
19
  ${F(e,n)}
20
20
  </tbody>
21
- </table>`}export function renderFindingsReportHtml(e,n={}){const t=e.level==="lot",i=n.title??(t?"Lot Findings Report":"Wafer Findings Report"),o=new Date().toLocaleString(),l=new Set(e.findings.flatMap(s=>s.absorbedIds??[])),r=e.findings.filter(s=>!l.has(s.id)),m=e.level==="lot"?e.stats.waferCount:void 0,d=u($(r)??""),h=d?`<p class="findings-narrative">${a(d)}</p>
22
- `:"",w=[f("Summary",S(e,o)),f("Findings",h+T(r,m))].join(`
21
+ </table>`}export function renderFindingsReportHtml(e,n={}){const t=e.level==="lot",i=n.title??(t?"Lot Findings Report":"Wafer Findings Report"),a=new Date().toLocaleString(),l=R(e.findings),r=e.level==="lot"?e.stats.waferCount:void 0,s=u(y(l)??""),f=s?`<p class="findings-narrative">${o(s)}</p>
22
+ `:"",m=[p("Summary",$(e,a)),p("Findings",f+S(l,r))].join(`
23
23
  `);return`<!DOCTYPE html>
24
24
  <html lang="en">
25
25
  <head>
26
26
  <meta charset="UTF-8">
27
- <title>${a(i)}</title>
27
+ <title>${o(i)}</title>
28
28
  <style>
29
- ${R()}
29
+ ${v()}
30
30
  </style>
31
31
  </head>
32
32
  <body>
33
33
  <main class="report">
34
34
  <header class="report-header">
35
- <h1>${a(i)}</h1>
36
- <p class="report-subtitle">Generated ${a(o)}</p>
35
+ <h1>${o(i)}</h1>
36
+ <p class="report-subtitle">Generated ${o(a)}</p>
37
37
  </header>
38
- ${w}
38
+ ${m}
39
39
  </main>
40
40
  </body>
41
41
  </html>`}export function openHtmlReport(e){if(typeof window.__openHtmlReport=="function"){window.__openHtmlReport(e);return}const n=URL.createObjectURL(new Blob([e],{type:"text/html"})),t=window.open(n,"_blank");setTimeout(()=>URL.revokeObjectURL(n),3e4),t||console.warn("wmap: window.open() returned null, so the report could not be shown. If this host cannot use window.open (e.g. Tauri, or popups are blocked), call setReportOpener() to supply your own opener.")}export function setReportOpener(e){window.__openHtmlReport=e}
@@ -1,19 +1,19 @@
1
- import{isParametricTest as ct}from"../renderer/buildWaferMap.js";import{buildRingRegions as H,buildQuadrantRegions as L}from"./regions.js";import{openHtmlReport as dt}from"./renderFindingsReport.js";import{analyzeWaferLot as ut}from"./analyzeWaferLot.js";import{computeFunctionalYield as ft}from"./analyzeWaferMap.js";import{buildCapabilityData as mt}from"./capability.js";import{fmt as M}from"../renderer/fmt.js";import{getDieKey as W,isPositionedDie as k}from"../core/dies.js";import{formatFindingDelta as pt,formatFindingCoverage as gt,formatFindingTooltip as ht,escHtml as h,renderMetadataSection as I,renderMetricGrid as E,renderSection as $,renderSeverityBadge as bt,renderTable as D,reportStyles as R}from"./reportHtml.js";function Y(a,r){return r===0?"\u2014":`${(a/r*100).toFixed(1)}%`}function St(a){if(!a)return"";const r=a.lot??a.lotId,i=a.wafer??a.waferId,n=[r,i].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(h).join(" \xB7 ")}`:""}function Rt(a,r){const i=[{label:"Total dies",value:String(a.totalDies)},{label:"Filled dies",value:String(r.filledDies),hint:`${Y(r.filledDies,r.totalDies)} fill`},{label:"Pass dies",value:String(a.passDies)},{label:"Fail dies",value:String(a.failDies)},...a.edgeExcludedDies>0?[{label:"Edge excluded",value:String(a.edgeExcludedDies)}]:[],...a.partialDies>0?[{label:"Partial dies",value:String(a.partialDies)}]:[],{label:"Yield",value:a.yieldPercent!==null?`${a.yieldPercent.toFixed(1)}%`:"N/A"}];return $("Yield",E(i))}function j(a,r,i,n){const l=new Map;if(n)for(const[e,o]of Object.entries(n))l.set(Number(e),o);else for(const e of a){if(e.partial||e.edgeExcluded)continue;const o=i==="hard"?e.hbin:e.sbin;o!=null&&l.set(o,(l.get(o)??0)+1)}if(!l.size)return"";const t=[...l.values()].reduce((e,o)=>e+o,0),s=r?new Map(r.map(e=>[e.bin,e])):null,c=[...l.entries()].sort((e,o)=>e[0]-o[0]).map(([e,o])=>{const p=s?.get(e)?.name??"\u2014";return[String(e),p,String(o),Y(o,t)]});return $(i==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",D(["Bin","Name","Count","%"],c,{className:"compact"}))}function z(a,r,i,n){const l=new Set(n),t=new Map(i.map(d=>[W(d),d]));if(!i.some(d=>d.hbin!=null||d.sbin!=null)||!r.length)return"";const c=[];for(const d of r){let e=0,o=0;for(const m of d.dieKeys){const p=t.get(m);if(!p||p.partial||p.edgeExcluded)continue;const g=p.hbin??p.sbin;g!=null&&(o++,l.has(g)&&e++)}o&&c.push([d.label,String(e),String(o),Y(e,o)])}return c.length?$(a,D(["Region","Pass","Total","Yield"],c,{className:"compact"})):""}function A(a,r,i){if(!r.length)return"";const n=a.filter(s=>!s.partial&&!s.edgeExcluded),l=new Map((i??[]).map(s=>[s.testNumber,s])),t=[];for(const s of r.filter(ct)){const c=s.testNumber,d=s.unit||void 0,e=l.get(c);let o,m,p;if(e)({min:o,max:m,mean:p}=e);else{const g=n.map(b=>b.testValues?.[c]).filter(b=>b!==void 0&&isFinite(b));if(!g.length)continue;g.sort((b,u)=>b-u),o=g[0],m=g[g.length-1],p=g.reduce((b,u)=>b+u,0)/g.length}t.push([h(s.name),M(o,d),M(p,d),M(m,d)])}return t.length?$("Test Values",D(["Test","Min","Mean","Max"],t,{className:"compact"})):""}function G(a,r,i){const n=i??ft(a,r);if(!n?.length)return"";const l=n.map(t=>[h(t.label),String(t.totalDies),String(t.passDies),String(t.failDies),t.passRatePercent!==null?`${t.passRatePercent.toFixed(1)}%`:"\u2014"]);return $("Functional Tests",D(["Test","N","Pass","Fail","Pass Rate"],l,{className:"compact"}))}function O(a,r){const i=mt(a,r);if(!i.length)return"";const n=t=>t===null?"\u2014":t.toFixed(2),l=i.map(t=>[h(t.label),t.hasSpec?`${M(t.lsl,t.unit)} \u2013 ${M(t.usl,t.unit)}`:"\u2014",M(t.mean,t.unit),n(t.cp),n(t.cpk),n(t.pp),n(t.ppk)]);return $("Process Capability",D(["Test","Spec (LSL\u2013USL)","Mean","Cp","Cpk","Pp","Ppk"],l,{className:"compact"}))}function K(a,r){if(!a.length)return"";const i=a.map(t=>`<tr title="${h(ht(t))}">
2
- <td class="tight">${bt(t.severity)}</td>
3
- <td class="tight">${h(t.comparison.left)}</td>
4
- <td>${h(t.variable.label)}</td>
5
- <td class="numeric">${h(pt(t))}</td>
6
- <td class="numeric">${h(gt(t,r))}</td>
1
+ import{isParametricTest as me}from"../renderer/buildWaferMap.js";import{buildRingRegions as W,buildQuadrantRegions as k}from"./regions.js";import{openHtmlReport as pe}from"./renderFindingsReport.js";import{analyzeWaferLot as ge}from"./analyzeWaferLot.js";import{computeFunctionalYield as he}from"./analyzeWaferMap.js";import{sortBinsForDisplay as C}from"./binPareto.js";import{buildFacetTable as be,facetValueOf as ye,FACET_NONE_VALUE as Se}from"./facets.js";import{visibleFindings as H}from"./filterFindings.js";import{buildYieldDataCombined as we}from"./yield.js";import{buildTestPassRateData as $e,hasJudgeableTests as xe,poolFunctionalYield as ve}from"./testPassRate.js";import{buildCapabilityData as Te}from"./capability.js";import{fmt as B}from"../renderer/fmt.js";import{getDieKey as L,isPositionedDie as j}from"../core/dies.js";import{formatFindingDelta as Fe,formatFindingCoverage as De,formatFindingTooltip as Pe,escHtml as S,renderMetadataSection as I,renderMetricGrid as A,renderSection as v,renderSeverityBadge as Be,renderTable as F,reportStyles as R}from"./reportHtml.js";function Y(a,l){return l===0?"\u2014":`${(a/l*100).toFixed(1)}%`}function Ye(a){if(!a)return"";const l=a.lot??a.lotId,i=a.wafer??a.waferId,n=[l,i].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(S).join(" \xB7 ")}`:""}function O(a,l,i,n,s=[1]){const e=new Map;if(n)for(const[t,c]of Object.entries(n))e.set(Number(t),c);else for(const t of a){if(t.partial||t.edgeExcluded)continue;const c=i==="hard"?t.hbin:t.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const o=[...e.values()].reduce((t,c)=>t+c,0),r=l?new Map(l.map(t=>[t.bin,t])):null,d=C(e.entries(),s).map(([t,c])=>{const m=r?.get(t)?.name??"\u2014";return[String(t),m,String(c),Y(c,o)]});return v(i==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",F(["Bin","Name","Count","%"],d,{className:"compact"}))}function z(a,l,i,n){const s=new Set(n),e=new Map(i.map(d=>[L(d),d]));if(!i.some(d=>d.hbin!=null||d.sbin!=null)||!l.length)return"";const r=[];for(const d of l){let u=0,t=0;for(const c of d.dieKeys){const g=e.get(c);if(!g||g.partial||g.edgeExcluded)continue;const m=g.hbin??g.sbin;m!=null&&(t++,s.has(m)&&u++)}t&&r.push([d.label,String(u),String(t),Y(u,t)])}return r.length?v(a,F(["Region","Pass","Total","Yield"],r,{className:"compact"})):""}function G(a,l,i){if(!l.length)return"";const n=a.filter(o=>!o.partial&&!o.edgeExcluded),s=new Map((i??[]).map(o=>[o.testNumber,o])),e=[];for(const o of l.filter(me)){const r=o.testNumber,d=o.unit||void 0,u=s.get(r);let t,c,g;if(u)({min:t,max:c,mean:g}=u);else{const m=n.map(w=>w.testValues?.[r]).filter(w=>w!==void 0&&isFinite(w));if(!m.length)continue;m.sort((w,p)=>w-p),t=m[0],c=m[m.length-1],g=m.reduce((w,p)=>w+p,0)/m.length}e.push([S(o.name),B(t,d),B(g,d),B(c,d)])}return e.length?v("Test Values",F(["Test","Min","Mean","Max"],e,{className:"compact"})):""}function K(a,l,i){const n=i??he(a,l);if(!n?.length)return"";const s=n.map(e=>[S(e.label),String(e.totalDies),String(e.passDies),String(e.failDies),e.passRatePercent!==null?`${e.passRatePercent.toFixed(1)}%`:"\u2014"]);return v("Functional Tests",F(["Test","N","Pass","Fail","Pass Rate"],s,{className:"compact"}))}function U(a,l){const i=Te(a,l);if(!i.length)return"";const n=e=>e===null?"\u2014":e.toFixed(2),s=i.map(e=>[S(e.label),e.hasSpec?`${B(e.lsl,e.unit)} \u2013 ${B(e.usl,e.unit)}`:"\u2014",B(e.mean,e.unit),n(e.cp),n(e.cpk),n(e.pp),n(e.ppk)]);return v("Process Capability",F(["Test","Spec (LSL\u2013USL)","Mean","Cp","Cpk","Pp","Ppk"],s,{className:"compact"}))}function V(a,l){const i=a.map(s=>`<tr title="${S(Pe(s))}">
2
+ <td class="tight">${Be(s.severity)}</td>
3
+ <td class="tight">${S(s.comparison.left)}</td>
4
+ <td>${S(s.variable.label)}</td>
5
+ <td class="numeric">${S(Fe(s))}</td>
6
+ <td class="numeric">${S(De(s,l))}</td>
7
7
  </tr>`).join(`
8
- `),l=`<table class="report-table findings-table compact"><thead><tr>
9
- <th>Severity</th><th>Region</th><th>Metric</th><th class="numeric">Delta</th><th class="numeric">${r!==void 0?"Wafers":"N (region/rest)"}</th>
10
- </tr></thead><tbody>${i}</tbody></table>`;return $("Findings",l)}export function renderSummaryReportHtml(a,r={}){const{wafer:i,dies:n,yieldSummary:l,dataCoverage:t,hbinDefs:s,sbinDefs:c,testDefs:d=[],statsSummary:e,passBins:o=[1],ringCount:m=4}=a,p=i.metadata,g=St(p),b=r.title??`Wafer Summary${g}`,u=new Date().toLocaleString(),w=n.some(B=>B.hbin!=null),x=n.some(B=>B.sbin!=null),S=n.filter(k),y=H(S,i,m),v=L(S,i,m),P=[{label:"Total dies",value:String(l.totalDies)},...l.partialDies>0?[{label:"Partial",value:String(l.partialDies)}]:[],...l.yieldPercent!==null?[{label:`Yield (pass: ${o.length===1?`bin ${o[0]}`:`bins ${o.join(", ")}`})`,value:`${l.yieldPercent.toFixed(1)}%`}]:[],...l.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(l.edgeExcludedDies)}]:[]],N=[I([{metadata:i.metadata}]),$("Summary",[E(P)].filter(Boolean).join(`
11
- `)),w?j(n,s,"hard",e?.stats.hardBinCounts):x?j(n,c,"soft",e?.stats.softBinCounts):"",z("Ring Yield",y,n,o),z("Quadrant Yield",v,n,o),A(n,d,e?.stats.perTestStats),G(n,d,e?.stats.functionalYield),O([{dies:n}],d),e?K(e.findings):""].filter(Boolean).join(`
8
+ `);return`<table class="report-table findings-table compact"><thead><tr>
9
+ <th>Severity</th><th>Region</th><th>Metric</th><th class="numeric">Delta</th><th class="numeric">${l!==void 0?"Wafers":"N (region/rest)"}</th>
10
+ </tr></thead><tbody>${i}</tbody></table>`}function _(a,l){const i=H(a);return i.length?v("Findings",V(i,l)):""}export function renderSummaryReportHtml(a,l={}){const{wafer:i,dies:n,yieldSummary:s,dataCoverage:e,hbinDefs:o,sbinDefs:r,testDefs:d=[],statsSummary:u,passBins:t=[1],ringCount:c=4}=a,g=i.metadata,m=Ye(g),w=l.title??`Wafer Summary${m}`,p=new Date().toLocaleString(),f=n.some(P=>P.hbin!=null),b=n.some(P=>P.sbin!=null),h=n.filter(j),$=W(h,i,c),x=k(h,i,c),T=[{label:"Total dies",value:String(s.totalDies)},{label:"Filled dies",value:String(e.filledDies),hint:`${Y(e.filledDies,e.totalDies)} fill`},{label:"Pass dies",value:String(s.passDies)},{label:"Fail dies",value:String(s.failDies)},...s.partialDies>0?[{label:"Partial",value:String(s.partialDies)}]:[],...s.yieldPercent!==null?[{label:`Yield (pass: ${t.length===1?`bin ${t[0]}`:`bins ${t.join(", ")}`})`,value:`${s.yieldPercent.toFixed(1)}%`}]:[],...s.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(s.edgeExcludedDies)}]:[]],N=[I([{metadata:i.metadata}]),v("Summary",[A(T)].filter(Boolean).join(`
11
+ `)),f?O(n,o,"hard",u?.stats.hardBinCounts,t):b?O(n,r,"soft",u?.stats.softBinCounts,t):"",z("Ring Yield",$,n,t),z("Quadrant Yield",x,n,t),G(n,d,u?.stats.perTestStats),K(n,d,u?.stats.functionalYield),U([{dies:n}],d),u?_(u.findings):""].filter(Boolean).join(`
12
12
  `);return`<!DOCTYPE html>
13
13
  <html lang="en">
14
14
  <head>
15
15
  <meta charset="UTF-8">
16
- <title>${h(b)}</title>
16
+ <title>${S(w)}</title>
17
17
  <style>
18
18
  ${R()}
19
19
  </style>
@@ -21,48 +21,51 @@ ${R()}
21
21
  <body>
22
22
  <main class="report">
23
23
  <header class="report-header">
24
- <h1>${h(b)}</h1>
25
- <p class="report-subtitle">Generated ${h(u)}</p>
24
+ <h1>${S(w)}</h1>
25
+ <p class="report-subtitle">Generated ${S(p)}</p>
26
26
  </header>
27
27
  ${N}
28
- <p class="footer">Generated ${h(u)}</p>
28
+ <p class="footer">Generated ${S(p)}</p>
29
29
  </main>
30
30
  </body>
31
- </html>`}function yt(a,r){const i=a.perWafer.map(n=>{const l=r[n.waferIndex]?.label??`W${n.waferIndex+1}`,t=n.summary.stats.yieldPercent;return[l,t!==null?`${t.toFixed(1)}%`:"N/A"]});return D(["Wafer","Yield"],i,{className:"compact"})}function wt(a){const r=a.map(i=>[i.label,i.wafer?.metadata?.split]).filter(i=>!!i[1]).map(([i,n])=>[h(i),h(n)]);return r.length?$("Splits",D(["Wafer","Split"],r,{className:"compact"})):""}function U(a,r,i,n){const l=new Map,t=i==="hard"?"hardBinCounts":"softBinCounts";if(n?.length&&n.every(e=>e.stats[t]!==void 0))for(const e of n)for(const[o,m]of Object.entries(e.stats[t])){const p=Number(o);l.set(p,(l.get(p)??0)+m)}else for(const e of a){if(e.partial||e.edgeExcluded)continue;const o=i==="hard"?e.hbin:e.sbin;o!=null&&l.set(o,(l.get(o)??0)+1)}if(!l.size)return"";const s=[...l.values()].reduce((e,o)=>e+o,0),c=r?new Map(r.map(e=>[e.bin,e])):null,d=[...l.entries()].sort((e,o)=>e[0]-o[0]).map(([e,o])=>{const m=c?.get(e);return[m?.name?`Bin ${e} \xB7 ${m.name} (${o})`:`Bin ${e} (${o})`,Y(o,s)]});return $(i==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",D(["Bin","Yield"],d,{className:"compact"}))}function Q(a,r,i,n,l,t){const s=new Set(t),c=new Map,d=[];for(let o=0;o<n.length;o++){const m=n[o],p=i[o]??[];if(!p.length)continue;const g=r(p.filter(k),m,l),b=new Map(p.map(u=>[W(u),u]));for(const u of g){d.includes(u.label)||d.push(u.label);const w=c.get(u.label)??{pass:0,total:0};for(const x of u.dieKeys){const S=b.get(x);if(!S||S.partial||S.edgeExcluded)continue;const y=S.hbin??S.sbin;y!=null&&(w.total++,s.has(y)&&w.pass++)}c.set(u.label,w)}}if(!c.size)return"";const e=d.filter(o=>c.get(o)?.total).map(o=>{const m=c.get(o);return[o,Y(m.pass,m.total)]});return e.length?$(a,D(["Region","Yield"],e,{className:"compact"})):""}function $t(a,r,i){let n;if(i?.length&&i.every(l=>l.stats.perTestStats!==void 0)){const l=new Map;for(const t of i)for(const s of t.stats.perTestStats??[]){const c=l.get(s.testNumber);c?(c.n+=s.count,c.sum+=s.mean*s.count,c.min=Math.min(c.min,s.min),c.max=Math.max(c.max,s.max)):l.set(s.testNumber,{n:s.count,sum:s.mean*s.count,min:s.min,max:s.max})}n=[...l.entries()].map(([t,s])=>({testNumber:t,min:s.min,max:s.max,mean:s.sum/s.n}))}return A(a,r,n)}function xt(a,r,i){let n;if(i?.length&&i.every(l=>l.stats.functionalYield!==void 0)){const l=new Map;for(const t of i)for(const s of t.stats.functionalYield??[]){const c=l.get(s.testNumber)??{label:s.label,passDies:0,failDies:0,totalDies:0};c.passDies+=s.passDies,c.failDies+=s.failDies,c.totalDies+=s.totalDies,l.set(s.testNumber,c)}n=[...l.entries()].map(([t,s])=>({testNumber:t,...s,passRatePercent:s.totalDies>0?s.passDies/s.totalDies*100:null}))}return G(a,r,n?.length?n:void 0)}const C=["lot","product","testProgram","temperature"];function Dt(a){const r=new Map(C.map(t=>[t,new Set]));for(const t of a)for(const s of C){const c=t.wafer?.metadata?.[s];c!=null&&c!==""&&r.get(s).add(String(c))}const i=C.filter(t=>r.get(t).size>1);if(i.length===0)return[{label:"",items:a,varying:i}];const n=new Map,l=[];for(const t of a){const s=i.map(c=>String(t.wafer?.metadata?.[c]??"(none)")).join(" \xB7 ");n.has(s)||(n.set(s,[]),l.push(s)),n.get(s).push(t)}return l.map(t=>({label:t,items:n.get(t),varying:i}))}function q(a,r,i,n,l,t,s){const c=ut(a,{perWaferSummaries:a.map(f=>f.statsSummary),...s}),d=[],e=[],o=[];for(const f of a){const T=f.dies??[];f.wafer&&(d.push(f.wafer),o.push(T)),T.length&&e.push(...T)}const m=e.some(f=>f.hbin!=null),p=e.some(f=>f.sbin!=null),g=m||p,b=c.perWafer.map(f=>f.summary.stats.yieldPercent).filter(f=>f!==null),u=b.length?b.reduce((f,T)=>f+T,0)/b.length:null,w=new Set(l);let x=0,S=0,y=0,v=0,P=0;for(const f of e){if(x++,f.edgeExcluded&&v++,f.partial&&P++,f.partial||f.edgeExcluded)continue;const T=f.hbin??f.sbin;T!=null&&(S++,w.has(T)&&y++)}const N=S-y,B=S>0?y/S*100:null,_=[{label:"Wafers",value:String(c.stats.waferCount)},{label:"Total dies",value:String(x)},...g?[{label:"Good dies",value:String(y)},{label:"Bad dies",value:String(N)}]:[],...v>0?[{label:"Edge excluded",value:String(v)}]:[],...P>0?[{label:"Partial dies",value:String(P)}]:[],...u!==null?[{label:"Mean wafer yield",value:`${u.toFixed(1)}%`}]:[],...B!==null?[{label:"Total yield",value:`${B.toFixed(1)}%`}]:[]],J=$("Lot Summary",E(_)),X=I(a.map(f=>({metadata:f.wafer?.metadata}))),Z=$("Per-Wafer Yield",yt(c,a)),tt=wt(a),F=c.perWafer.map(f=>f.summary),et=g?m?U(e,r,"hard",F):U(e,i,"soft",F):"",nt=g&&d.length?Q("Ring Yield (All Wafers)",H,o,d,t,l):"",st=g&&d.length?Q("Quadrant Yield (All Wafers)",L,o,d,t,l):"",at=n.length?$t(e,n,F):"",ot=n.length?xt(e,n,F):"",it=n.length?O(a,n):"",lt=c.findings.length?K(c.findings,c.stats.waferCount):"",rt=[J,X,Z,tt,et,nt,st,at,ot,it,lt].filter(Boolean).join(`
32
- `);return{lotSummary:c,sections:rt}}function V(a,r,i){return`<main class="report">
31
+ </html>`}function Me(a,l){const i=a.perWafer.map(n=>{const s=l[n.waferIndex]?.label??`W${n.waferIndex+1}`,e=n.summary.stats.yieldPercent;return[s,e!==null?`${e.toFixed(1)}%`:"N/A"]});return F(["Wafer","Yield"],i,{className:"compact"})}function Ne(a){const l=[];for(const e of a.perWafer){const o=H(e.summary.findings);if(!o.length)continue;const r=e.summary.wafer?.waferId??`Wafer ${e.waferIndex+1}`;l.push(`<h3 class="report-subheading">${S(r)}</h3>`+V(o))}if(!l.length)return"";const i=l.length,n=a.perWafer.length,s=`<p class="report-note">${i} of ${n} wafer${n===1?"":"s"} have per-wafer findings; the rest had none.</p>`;return v("Findings by Wafer",s+l.join(`
32
+ `))}const Re=3;function Ee(a,l,i){const n=a.map(d=>({metadata:d.wafer?.metadata})),s=be(n,{facetableOnly:!0}).filter(d=>d.splittable);if(!s.length)return"";const e=s.slice(0,Re),o=[];for(const d of e){const u=new Map;a.forEach((f,b)=>{const h=ye(f.wafer?.metadata,d.key)??Se;(u.get(h)??u.set(h,[]).get(h)).push(b)});const t=[...u.entries()].map(([f,b])=>({key:f,items:b.map(h=>a[h])}));if(t.length<2)continue;const c=t.map(f=>[S(f.key),S(f.items.map(b=>b.label).join(", "))]),g=we(t.map(f=>({key:f.key,items:f.items.map(b=>({label:b.label,dies:b.dies}))})),i).map(f=>[S(f.label),String(f.itemCount),`${f.percent.toFixed(1)}%`]),m=[F([d.label,"Wafers"],c,{className:"compact"}),F([d.label,"Wafers","Yield"],g,{className:"compact"})],w=a.flatMap(f=>f.dies??[]);let p=0;for(const f of["spec","testFlag","functional"]){if(!xe(l,f,w))continue;const b=$e(t.map(x=>({key:x.key,items:x.items.map(T=>({dies:T.dies,testSpecYield:T.statsSummary?.stats.testSpecYield,functionalYield:T.statsSummary?.stats.functionalYield}))})),l,f);if(!b.rows.length)continue;const h=b.rows.map(x=>[S(x.label),...x.byGroup.map(T=>T.passRatePercent===null?"\u2014":`${T.passRatePercent.toFixed(1)}%`),x.overall.passRatePercent===null?"\u2014":`${x.overall.passRatePercent.toFixed(1)}%`]),$=f==="spec"?"Pass rate \xB7 spec limits":f==="testFlag"?"Pass rate \xB7 tester flag":"Pass rate \xB7 functional";b.disagreementDies&&(p=b.disagreementDies),m.push(`<h4 class="report-subheading">${S($)}</h4>`+F(["Test",...b.groups,"All"],h,{className:"compact"}))}p&&m.push(`<p class="report-note">${p.toLocaleString()} dies judged differently by limits and tester flag.</p>`),o.push(`<h3 class="report-subheading">${S(d.label)}</h3>${m.join(`
33
+ `)}`)}if(!o.length)return"";const r=s.length>e.length?`<p class="report-note">${s.length-e.length} further splittable field(s) not shown.</p>`:"";return v("Split Comparison",o.join(`
34
+ `)+r)}function Q(a,l,i,n,s=[1]){const e=new Map,o=i==="hard"?"hardBinCounts":"softBinCounts";if(n?.length&&n.every(t=>t.stats[o]!==void 0))for(const t of n)for(const[c,g]of Object.entries(t.stats[o])){const m=Number(c);e.set(m,(e.get(m)??0)+g)}else for(const t of a){if(t.partial||t.edgeExcluded)continue;const c=i==="hard"?t.hbin:t.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const r=[...e.values()].reduce((t,c)=>t+c,0),d=l?new Map(l.map(t=>[t.bin,t])):null,u=C(e.entries(),s).map(([t,c])=>{const g=d?.get(t);return[g?.name?`Bin ${t} \xB7 ${g.name} (${c})`:`Bin ${t} (${c})`,Y(c,r)]});return v(i==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",F(["Bin","Yield"],u,{className:"compact"}))}function q(a,l,i,n,s,e){const o=new Set(e),r=new Map,d=[];for(let t=0;t<n.length;t++){const c=n[t],g=i[t]??[];if(!g.length)continue;const m=l(g.filter(j),c,s),w=new Map(g.map(p=>[L(p),p]));for(const p of m){d.includes(p.label)||d.push(p.label);const f=r.get(p.label)??{pass:0,total:0};for(const b of p.dieKeys){const h=w.get(b);if(!h||h.partial||h.edgeExcluded)continue;const $=h.hbin??h.sbin;$!=null&&(f.total++,o.has($)&&f.pass++)}r.set(p.label,f)}}if(!r.size)return"";const u=d.filter(t=>r.get(t)?.total).map(t=>{const c=r.get(t);return[t,Y(c.pass,c.total)]});return u.length?v(a,F(["Region","Yield"],u,{className:"compact"})):""}function We(a,l,i){let n;if(i?.length&&i.every(s=>s.stats.perTestStats!==void 0)){const s=new Map;for(const e of i)for(const o of e.stats.perTestStats??[]){const r=s.get(o.testNumber);r?(r.n+=o.count,r.sum+=o.mean*o.count,r.min=Math.min(r.min,o.min),r.max=Math.max(r.max,o.max)):s.set(o.testNumber,{n:o.count,sum:o.mean*o.count,min:o.min,max:o.max})}n=[...s.entries()].map(([e,o])=>({testNumber:e,min:o.min,max:o.max,mean:o.sum/o.n}))}return G(a,l,n)}function ke(a,l,i){const n=ve(i);return K(a,l,n)}const E=["lot","product","testProgram","temperature"];function Ce(a){const l=new Map(E.map(e=>[e,new Set]));for(const e of a)for(const o of E){const r=e.wafer?.metadata?.[o];r!=null&&r!==""&&l.get(o).add(String(r))}const i=E.filter(e=>l.get(e).size>1);if(i.length===0)return[{label:"",items:a,varying:i}];const n=new Map,s=[];for(const e of a){const o=i.map(r=>String(e.wafer?.metadata?.[r]??"(none)")).join(" \xB7 ");n.has(o)||(n.set(o,[]),s.push(o)),n.get(o).push(e)}return s.map(e=>({label:e,items:n.get(e),varying:i}))}function J(a,l,i,n,s,e,o){const r=ge(a,{perWaferSummaries:a.map(y=>y.statsSummary),...o}),d=[],u=[],t=[];for(const y of a){const D=y.dies??[];y.wafer&&(d.push(y.wafer),t.push(D)),D.length&&u.push(...D)}const c=u.some(y=>y.hbin!=null),g=u.some(y=>y.sbin!=null),m=c||g,w=r.perWafer.map(y=>y.summary.stats.yieldPercent).filter(y=>y!==null),p=w.length?w.reduce((y,D)=>y+D,0)/w.length:null,f=new Set(s);let b=0,h=0,$=0,x=0,T=0;for(const y of u){if(b++,y.edgeExcluded&&x++,y.partial&&T++,y.partial||y.edgeExcluded)continue;const D=y.hbin??y.sbin;D!=null&&(h++,f.has(D)&&$++)}const N=h-$,P=h>0?$/h*100:null,Z=[{label:"Wafers",value:String(r.stats.waferCount)},{label:"Total dies",value:String(b)},...m?[{label:"Good dies",value:String($)},{label:"Bad dies",value:String(N)}]:[],...x>0?[{label:"Edge excluded",value:String(x)}]:[],...T>0?[{label:"Partial dies",value:String(T)}]:[],...p!==null?[{label:"Mean wafer yield",value:`${p.toFixed(1)}%`}]:[],...P!==null?[{label:"Total yield",value:`${P.toFixed(1)}%`}]:[]],ee=v("Lot Summary",A(Z)),te=I(a.map(y=>({metadata:y.wafer?.metadata}))),ne=v("Per-Wafer Yield",Me(r,a)),se=Ee(a,n,s),M=r.perWafer.map(y=>y.summary),ae=m?c?Q(u,l,"hard",M,s):Q(u,i,"soft",M,s):"",oe=m&&d.length?q("Ring Yield (All Wafers)",W,t,d,e,s):"",ie=m&&d.length?q("Quadrant Yield (All Wafers)",k,t,d,e,s):"",le=n.length?We(u,n,M):"",re=n.length?ke(u,n,M):"",ce=n.length?U(a,n):"",de=r.findings.length?_(r.findings,r.stats.waferCount):"",ue=Ne(r),fe=[ee,te,ne,se,ae,oe,ie,le,re,ce,de,ue].filter(Boolean).join(`
35
+ `);return{lotSummary:r,sections:fe}}function X(a,l,i){return`<main class="report">
33
36
  <header class="report-header">
34
- <h1>${h(a)}</h1>
35
- <p class="report-subtitle">Generated ${h(i)}</p>
37
+ <h1>${S(a)}</h1>
38
+ <p class="report-subtitle">Generated ${S(i)}</p>
36
39
  </header>
37
- ${r}
38
- <p class="footer">Generated ${h(i)}</p>
39
- </main>`}export function renderLotSummaryReportHtml(a,r={}){const{items:i,hbinDefs:n,sbinDefs:l,testDefs:t=[],passBins:s=[1],ringCount:c=4,analyzeOptions:d}=a,e=new Date().toLocaleString(),o=Dt(i);if(o.length===1){const{lotSummary:u,sections:w}=q(o[0].items,n,l,t,s,c,d),x=(()=>{const y=u.lot;if(!y)return"";const v=y.lot??y.lotId;return v?` \u2014 ${h(String(v))}`:""})(),S=r.title??`Lot Summary${x}`;return`<!DOCTYPE html>
40
+ ${l}
41
+ <p class="footer">Generated ${S(i)}</p>
42
+ </main>`}export function renderLotSummaryReportHtml(a,l={}){const{items:i,hbinDefs:n,sbinDefs:s,testDefs:e=[],passBins:o=[1],ringCount:r=4,analyzeOptions:d}=a,u=new Date().toLocaleString(),t=Ce(i);if(t.length===1){const{lotSummary:p,sections:f}=J(t[0].items,n,s,e,o,r,d),b=(()=>{const $=p.lot;if(!$)return"";const x=$.lot??$.lotId;return x?` \u2014 ${S(String(x))}`:""})(),h=l.title??`Lot Summary${b}`;return`<!DOCTYPE html>
40
43
  <html lang="en">
41
44
  <head>
42
45
  <meta charset="UTF-8">
43
- <title>${h(S)}</title>
46
+ <title>${S(h)}</title>
44
47
  <style>
45
48
  ${R()}
46
49
  </style>
47
50
  </head>
48
51
  <body>
49
- ${V(S,w,e)}
52
+ ${X(h,f,u)}
50
53
  </body>
51
- </html>`}const m=r.title??"Lot Summary",p=o.map(u=>{const{lotSummary:w,sections:x}=q(u.items,n,l,t,s,c,d),S=u.varying.includes("lot")?void 0:w.lot?.lot??w.lot?.lotId,y=S?`${u.label} \xB7 Lot ${String(S)}`:u.label;return V(`${m} \u2014 ${y}`,x,e)}),g=`<p style="max-width:1080px;margin:0 auto 8px;padding:8px 12px;background:var(--report-surface);border:1px solid var(--report-line);border-radius:4px;font-size:12px;color:var(--report-muted);">This load spans ${o.length} groups by identity \u2014 shown separately below so stats are never pooled across them.</p>`,b=p.map((u,w)=>`${w===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong);">'}
52
- ${u}`).join(`
54
+ </html>`}const c=l.title??"Lot Summary",g=t.map(p=>{const{lotSummary:f,sections:b}=J(p.items,n,s,e,o,r,d),h=p.varying.includes("lot")?void 0:f.lot?.lot??f.lot?.lotId,$=h?`${p.label} \xB7 Lot ${String(h)}`:p.label;return X(`${c} \u2014 ${$}`,b,u)}),m=`<p style="max-width:1080px;margin:0 auto 8px;padding:8px 12px;background:var(--report-surface);border:1px solid var(--report-line);border-radius:4px;font-size:12px;color:var(--report-muted);">This load spans ${t.length} groups by identity \u2014 shown separately below so stats are never pooled across them.</p>`,w=g.map((p,f)=>`${f===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong);">'}
55
+ ${p}`).join(`
53
56
  `);return`<!DOCTYPE html>
54
57
  <html lang="en">
55
58
  <head>
56
59
  <meta charset="UTF-8">
57
- <title>${h(m)}</title>
60
+ <title>${S(c)}</title>
58
61
  <style>
59
62
  ${R()}
60
63
  </style>
61
64
  </head>
62
65
  <body>
63
66
  <div class="report" style="padding-top:24px;">
64
- ${g}
67
+ ${m}
65
68
  </div>
66
- ${b}
69
+ ${w}
67
70
  </body>
68
- </html>`}export{dt as openHtmlReport};
71
+ </html>`}export{pe as openHtmlReport};
@@ -1,21 +1,21 @@
1
- import{fmt as i}from"../renderer/fmt.js";import{buildFacetTable as d,prettyKey as s}from"./facets.js";export function escHtml(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export function renderSection(t,e,r=""){return`<section class="${["report-section",r].filter(Boolean).join(" ")}">
2
- <h2>${escHtml(t)}</h2>
3
- ${e}
4
- </section>`}export function renderDefinitionList(t,e="meta-list"){if(!t.length)return"";const r=t.map(a=>`<div class="definition-item"><dt>${escHtml(a.label)}</dt><dd>${escHtml(a.value)}</dd></div>`).join(`
5
- `);return`<dl class="${e}">
1
+ import{fmt as n}from"../renderer/fmt.js";import{buildFacetTable as s,prettyKey as d}from"./facets.js";export function escHtml(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}export function renderSection(e,t,r=""){return`<section class="${["report-section",r].filter(Boolean).join(" ")}">
2
+ <h2>${escHtml(e)}</h2>
3
+ ${t}
4
+ </section>`}export function renderDefinitionList(e,t="meta-list"){if(!e.length)return"";const r=e.map(o=>`<div class="definition-item"><dt>${escHtml(o.label)}</dt><dd>${escHtml(o.value)}</dd></div>`).join(`
5
+ `);return`<dl class="${t}">
6
6
  ${r}
7
- </dl>`}export function buildMetadataRows(t){return d(t,{facetableOnly:t.length>1}).map(r=>({label:s(r.key),value:r.values.map(a=>a.value).join(", ")}))}export function renderMetadataSection(t){const e=buildMetadataRows(t);return e.length?renderSection("Wafer Info",renderDefinitionList(e)):""}export function renderMetricGrid(t){return t.length?`<dl class="metric-grid">
8
- ${t.map(r=>`<div class="metric">
7
+ </dl>`}export function buildMetadataRows(e){return s(e,{facetableOnly:e.length>1}).map(r=>({label:d(r.key),value:r.values.map(o=>o.value).join(", ")}))}export function renderMetadataSection(e){const t=buildMetadataRows(e);return t.length?renderSection("Wafer Info",renderDefinitionList(t)):""}export function renderMetricGrid(e){return e.length?`<dl class="metric-grid">
8
+ ${e.map(r=>`<div class="metric">
9
9
  <dt>${escHtml(r.label)}</dt>
10
10
  <dd>${escHtml(r.value)}</dd>
11
11
  ${r.hint?`<span class="metric-hint">${escHtml(r.hint)}</span>`:""}
12
12
  </div>`).join(`
13
13
  `)}
14
- </dl>`:""}export function renderTable(t,e,r={}){if(!e.length)return`<p class="no-data">${escHtml(r.emptyMessage??"No data")}</p>`;const a=["report-table",r.className].filter(Boolean).join(" "),n=t.map(o=>`<th>${escHtml(o)}</th>`).join(""),p=e.map(o=>`<tr>${o.map(l=>`<td>${l}</td>`).join("")}</tr>`).join(`
15
- `);return`<table class="${a}">
16
- <thead><tr>${n}</tr></thead>
14
+ </dl>`:""}export function renderTable(e,t,r={}){if(!t.length)return`<p class="no-data">${escHtml(r.emptyMessage??"No data")}</p>`;const o=["report-table",r.className].filter(Boolean).join(" "),i=e.map(a=>`<th>${escHtml(a)}</th>`).join(""),p=t.map(a=>`<tr>${a.map(l=>`<td>${l}</td>`).join("")}</tr>`).join(`
15
+ `);return`<table class="${o}">
16
+ <thead><tr>${i}</tr></thead>
17
17
  <tbody>${p}</tbody>
18
- </table>`}export function renderSeverityBadge(t){const e=t.charAt(0).toUpperCase()+t.slice(1);return`<span class="badge badge-${t}">${e}</span>`}export function formatFindingDelta(t){const e=t.effect.absoluteDelta??0,r=e>0?"+":e<0?"\u2212":"";if(t.variable.kind==="yield"||t.variable.kind==="hardBin"||t.variable.kind==="softBin")return`${r}${Math.abs(e*100).toFixed(1)} pp`;if(t.variable.kind==="test"){if(t.effect.relativeDelta!==void 0&&Number.isFinite(t.effect.relativeDelta)){const a=Math.abs(t.effect.relativeDelta*100).toFixed(1);return`${r}${a}%`}return`${r}${i(Math.abs(e),t.variable.unit)}`}return`${r}${i(Math.abs(e),t.variable.unit)}`}export function formatFindingTooltip(t){return t.summary}export function formatFindingCoverage(t,e){return e!==void 0?`${t.stats.sampleSizeLeft}/${e}`:`${t.stats.sampleSizeLeft}/${t.stats.sampleSizeRight}`}export function reportStyles(){return`
18
+ </table>`}export function renderSeverityBadge(e){const t=e.charAt(0).toUpperCase()+e.slice(1);return`<span class="badge badge-${e}">${t}</span>`}export function formatFindingDelta(e){const t=e.effect.absoluteDelta??0,r=t>0?"+":t<0?"\u2212":"";if(e.variable.kind==="yield"||e.variable.kind==="hardBin"||e.variable.kind==="softBin")return`${r}${Math.abs(t*100).toFixed(1)} pp`;if(e.variable.kind==="test"){if(e.effect.relativeDelta!==void 0&&Number.isFinite(e.effect.relativeDelta)){const o=Math.abs(e.effect.relativeDelta*100).toFixed(1);return`${r}${o}%`}return`${r}${n(Math.abs(t),e.variable.unit)}`}return`${r}${n(Math.abs(t),e.variable.unit)}`}export function formatFindingTooltip(e){return e.summary}export function formatFindingCoverage(e,t){return t!==void 0?`${e.stats.sampleSizeLeft}/${t}`:`${e.stats.sampleSizeLeft}/${e.stats.sampleSizeRight}`}export function reportStyles(){return`
19
19
  :root {
20
20
  --report-font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
21
  --report-text: #1f2328;
@@ -72,6 +72,26 @@ ${t.map(r=>`<div class="metric">
72
72
  font-size: 12px;
73
73
  }
74
74
 
75
+ /* Sub-headings and notes inside a section \u2014 used by the split comparison,
76
+ which nests one block per facet under a single section heading. */
77
+ .report-subheading {
78
+ margin: 14px 0 6px;
79
+ font-size: 13px;
80
+ font-weight: 600;
81
+ color: var(--report-text);
82
+ }
83
+ h4.report-subheading {
84
+ margin: 10px 0 4px;
85
+ font-size: 12px;
86
+ font-weight: 600;
87
+ color: var(--report-muted);
88
+ }
89
+ .report-note {
90
+ margin: 4px 0 0;
91
+ font-size: 11px;
92
+ color: var(--report-muted);
93
+ }
94
+
75
95
  section.report-section {
76
96
  margin-top: 18px;
77
97
  break-inside: avoid;
@@ -186,7 +206,12 @@ ${t.map(r=>`<div class="metric">
186
206
  padding-top: 3px;
187
207
  padding-bottom: 5px;
188
208
  color: var(--report-muted);
189
- font-size: 10px;
209
+ /* 11px, not 10px. This document prints (see @page below), where 10px is
210
+ about 7.5pt \u2014 too small to read on paper. The report keeps its own
211
+ document scale (20 title / 18 stat / 13 / 12 body / 11 label) rather than
212
+ the app's denser tiers; this is a print-legibility floor, not an
213
+ alignment to those. */
214
+ font-size: 11px;
190
215
  font-weight: 600;
191
216
  text-transform: uppercase;
192
217
  letter-spacing: 0.08em;
@@ -230,7 +255,7 @@ ${t.map(r=>`<div class="metric">
230
255
  padding: 1px 6px;
231
256
  border: 1px solid transparent;
232
257
  border-radius: 999px;
233
- font-size: 10px;
258
+ font-size: 11px; /* print floor \u2014 see the table-header note above */
234
259
  font-weight: 600;
235
260
  line-height: 1.35;
236
261
  letter-spacing: 0.04em;