@wafertools/wafermap 0.29.0 → 0.30.1

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/AGENTS.md +26 -17
  2. package/CHANGELOG.md +410 -0
  3. package/README.md +6 -6
  4. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  7. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  9. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +1 -0
  10. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  13. package/dist/packages/canvas-adapter/deprecated.d.ts +7 -0
  14. package/dist/packages/canvas-adapter/deprecated.js +1 -0
  15. package/dist/packages/canvas-adapter/dieList.d.ts +1 -3
  16. package/dist/packages/canvas-adapter/dieList.js +7 -7
  17. package/dist/packages/canvas-adapter/exportName.d.ts +68 -0
  18. package/dist/packages/canvas-adapter/exportName.js +1 -0
  19. package/dist/packages/canvas-adapter/icons.js +2 -2
  20. package/dist/packages/canvas-adapter/index.d.ts +2 -3
  21. package/dist/packages/canvas-adapter/index.js +1 -1
  22. package/dist/packages/canvas-adapter/insightsTab.d.ts +0 -1
  23. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  24. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  25. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +23 -40
  26. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  27. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +96 -104
  28. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  29. package/dist/packages/canvas-adapter/summaryPanel.d.ts +31 -13
  30. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  31. package/dist/packages/canvas-adapter/toCanvas.d.ts +20 -12
  32. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  33. package/dist/packages/canvas-adapter/toolbar.d.ts +6 -0
  34. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  35. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  36. package/dist/packages/canvas-adapter/userGuideHtml.js +26 -15
  37. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  38. package/dist/packages/canvas-adapter/version.js +1 -1
  39. package/dist/packages/canvas-adapter/warnings.js +1 -1
  40. package/dist/packages/core/deprecated.d.ts +60 -0
  41. package/dist/packages/core/deprecated.js +1 -0
  42. package/dist/packages/core/index.d.ts +1 -3
  43. package/dist/packages/core/index.js +1 -1
  44. package/dist/packages/core/passBins.d.ts +33 -0
  45. package/dist/packages/core/passBins.js +1 -0
  46. package/dist/packages/core/utils.d.ts +12 -0
  47. package/dist/packages/core/utils.js +1 -1
  48. package/dist/packages/core/waferLabel.d.ts +35 -0
  49. package/dist/packages/core/waferLabel.js +1 -0
  50. package/dist/packages/renderer/binColors.d.ts +105 -13
  51. package/dist/packages/renderer/binColors.js +1 -1
  52. package/dist/packages/renderer/buildView.d.ts +8 -9
  53. package/dist/packages/renderer/buildView.js +1 -1
  54. package/dist/packages/renderer/buildWaferMap.d.ts +76 -17
  55. package/dist/packages/renderer/buildWaferMap.js +1 -1
  56. package/dist/packages/renderer/colorMap.d.ts +4 -3
  57. package/dist/packages/renderer/colorSchemes.d.ts +8 -6
  58. package/dist/packages/renderer/deprecate.d.ts +31 -0
  59. package/dist/packages/renderer/deprecate.js +1 -0
  60. package/dist/packages/renderer/deprecated.d.ts +45 -0
  61. package/dist/packages/renderer/deprecated.js +1 -0
  62. package/dist/packages/renderer/index.d.ts +4 -4
  63. package/dist/packages/renderer/index.js +1 -1
  64. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  65. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  66. package/dist/packages/stats/binPareto.d.ts +6 -1
  67. package/dist/packages/stats/capability.d.ts +16 -1
  68. package/dist/packages/stats/capability.js +1 -1
  69. package/dist/packages/stats/clusterDetection.d.ts +0 -2
  70. package/dist/packages/stats/deprecated.d.ts +82 -0
  71. package/dist/packages/stats/deprecated.js +1 -0
  72. package/dist/packages/stats/index.d.ts +5 -14
  73. package/dist/packages/stats/index.js +1 -1
  74. package/dist/packages/stats/normalizeInput.d.ts +10 -0
  75. package/dist/packages/stats/normalizeInput.js +1 -0
  76. package/dist/packages/stats/regions.d.ts +9 -2
  77. package/dist/packages/stats/regions.js +1 -1
  78. package/dist/packages/stats/renderFindingsReport.js +1 -1
  79. package/dist/packages/stats/renderSummaryReport.d.ts +42 -1
  80. package/dist/packages/stats/renderSummaryReport.js +34 -34
  81. package/dist/packages/stats/reportHtml.d.ts +0 -1
  82. package/dist/packages/stats/reportHtml.js +10 -10
  83. package/dist/packages/stats/summaryFigures.d.ts +47 -0
  84. package/dist/packages/stats/summaryFigures.js +1 -0
  85. package/dist/packages/stats/types.d.ts +118 -43
  86. package/dist/packages/stats/yield.d.ts +12 -6
  87. package/dist/packages/stats/yield.js +1 -1
  88. package/package.json +1 -1
@@ -1,71 +1,71 @@
1
- import{isParametricTest as ge}from"../renderer/buildWaferMap.js";import{buildRingRegions as E,buildQuadrantRegions as k}from"./regions.js";import{openHtmlReport as he}from"./renderFindingsReport.js";import{analyzeWaferLot as be}from"./analyzeWaferLot.js";import{computeFunctionalYield as ye}from"./analyzeWaferMap.js";import{sortBinsForDisplay as C}from"./binPareto.js";import{buildFacetTable as Se,facetValueOf as we,FACET_NONE_VALUE as $e}from"./facets.js";import{visibleFindings as H}from"./filterFindings.js";import{buildYieldDataCombined as xe}from"./yield.js";import{describeWaferPopulation as I,populationLabel as ve}from"./population.js";import{buildTestPassRateData as Te,hasJudgeableTests as Fe,poolFunctionalYield as De}from"./testPassRate.js";import{buildCapabilityData as Pe}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 Be,formatFindingCoverage as Ye,formatFindingTooltip as Me,escHtml as S,renderMetadataSection as A,renderMetricGrid as O,renderSection as v,renderSeverityBadge as Ne,renderTable as F,reportStyles as W}from"./reportHtml.js";function Y(s,l){return l===0?"\u2014":`${(s/l*100).toFixed(1)}%`}function We(s){if(!s)return"";const l=s.lot??s.lotId,i=s.wafer??s.waferId,n=[l,i].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(S).join(" \xB7 ")}`:""}function z(s,l,i,n,a=[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 s){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(),a).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 G(s,l,i,n){const a=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 b=e.get(c);if(!b||b.partial||b.edgeExcluded)continue;const m=b.hbin??b.sbin;m!=null&&(t++,a.has(m)&&u++)}t&&r.push([d.label,String(u),String(t),Y(u,t)])}return r.length?v(s,F(["Region","Pass","Total","Yield"],r,{className:"compact"})):""}function K(s,l,i){if(!l.length)return"";const n=s.filter(o=>!o.partial&&!o.edgeExcluded),a=new Map((i??[]).map(o=>[o.testNumber,o])),e=[];for(const o of l.filter(ge)){const r=o.testNumber,d=o.unit||void 0,u=a.get(r);let t,c,b;if(u)({min:t,max:c,mean:b}=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],b=m.reduce((w,p)=>w+p,0)/m.length}e.push([S(o.name),B(t,d),B(b,d),B(c,d)])}return e.length?v("Test Values",F(["Test","Min","Mean","Max"],e,{className:"compact"})):""}function U(s,l,i){const n=i??ye(s,l);if(!n?.length)return"";const a=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"],a,{className:"compact"}))}function V(s,l){const i=Pe(s,l);if(!i.length)return"";const n=e=>e===null?"\u2014":e.toFixed(2),a=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"],a,{className:"compact"}))}function _(s,l){const i=s.map(a=>`<tr title="${S(Me(a))}">
2
- <td class="tight">${Ne(a.severity)}</td>
3
- <td class="tight">${S(a.comparison.left)}</td>
4
- <td>${S(a.variable.label)}</td>
5
- <td class="numeric">${S(Be(a))}</td>
6
- <td class="numeric">${S(Ye(a,l))}</td>
1
+ import{waferDisplayLabel as L}from"../core/waferLabel.js";import{binPassSets as we,binPassSetsByWafer as $e,mergeBinDefs as I}from"../renderer/binColors.js";import{itemPassBins as R,passBinsLabel as xe}from"../core/passBins.js";import{isParametricTest as De}from"../renderer/buildWaferMap.js";import{buildRingRegions as j,buildQuadrantRegions as O,buildRegionYieldData as ve}from"./regions.js";import{openHtmlReport as Be}from"./renderFindingsReport.js";import{analyzeWaferLot as Te}from"./analyzeWaferLot.js";import{computeFunctionalYield as Fe,analyzeWaferMap as Pe}from"./analyzeWaferMap.js";import{mergeTestDefs as Ye}from"./mergeTestDefs.js";import{sortBinsForDisplay as z}from"./binPareto.js";import{buildFacetTable as We,facetValueOf as Me,FACET_NONE_VALUE as Ce}from"./facets.js";import{visibleFindings as A}from"./filterFindings.js";import{buildYieldDataCombined as Re}from"./yield.js";import{describeWaferPopulation as G,populationLabel as Ne}from"./population.js";import{buildTestPassRateData as Ee,hasJudgeableTests as ke,poolFunctionalYield as He}from"./testPassRate.js";import{buildCapabilityData as Le}from"./capability.js";import{fmt as F}from"../renderer/fmt.js";import{getDieKey as Ie,isPositionedDie as je,diePassStatus as U}from"../core/dies.js";import{formatFindingDelta as Oe,formatFindingCoverage as ze,formatFindingTooltip as Ae,renderMetadataSection as V,renderMetricGrid as _,renderSection as x,renderSeverityBadge as Ge,renderTable as B,reportStyles as N}from"./reportHtml.js";import{escHtml as h}from"../core/utils.js";function C(t,o){return o===0?"\u2014":`${(t/o*100).toFixed(1)}%`}function Ue(t){if(!t)return"";const o=t.lot??t.lotId,s=t.wafer??t.waferId,n=[o,s].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(h).join(" \xB7 ")}`:""}function K(t,o,s,n,r=[1]){const e=new Map;if(n)for(const[a,c]of Object.entries(n))e.set(Number(a),c);else for(const a of t){if(a.partial||a.edgeExcluded)continue;const c=s==="hard"?a.hbin:a.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const i=[...e.values()].reduce((a,c)=>a+c,0),l=o?new Map(o.map(a=>[a.bin,a])):null,d=z(e.entries(),we(t,r)[s]).map(([a,c])=>{const p=l?.get(a)?.name??"\u2014";return[String(a),p,String(c),C(c,i)]});return x(s==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",B(["Bin","Name","Count","%"],d,{className:"compact"}))}function Q(t,o,s,n){const r=new Set(n),e=new Map(s.map(d=>[Ie(d),d]));if(!s.some(d=>d.hbin!=null||d.sbin!=null)||!o.length)return"";const l=[];for(const d of o){let f=0,a=0;for(const c of d.dieKeys){const b=e.get(c);if(!b||b.partial||b.edgeExcluded)continue;const p=U(b,r);p!==void 0&&(a++,p&&f++)}a&&l.push([d.label,String(f),String(a),C(f,a)])}return l.length?x(t,B(["Region","Pass","Total","Yield"],l,{className:"compact"})):""}function q(t,o,s){if(!o.length)return"";const n=t.filter(i=>!i.partial&&!i.edgeExcluded),r=new Map((s??[]).map(i=>[i.testNumber,i])),e=[];for(const i of o.filter(De)){const l=i.testNumber,d=i.unit||void 0,f=r.get(l);let a,c,b;if(f)({min:a,max:c,mean:b}=f);else{const p=n.map(w=>w.testValues?.[l]).filter(w=>w!==void 0&&isFinite(w));if(!p.length)continue;p.sort((w,y)=>w-y),a=p[0],c=p[p.length-1],b=p.reduce((w,y)=>w+y,0)/p.length}e.push([h(i.name),F(a,d),F(b,d),F(c,d)])}return e.length?x("Test Values",B(["Test","Min","Mean","Max"],e,{className:"compact"})):""}function J(t,o,s){const n=s??Fe(t,o);if(!n?.length)return"";const r=n.map(e=>[h(e.label),String(e.totalDies),String(e.passDies),String(e.failDies),e.passRatePercent!==null?`${e.passRatePercent.toFixed(1)}%`:"\u2014"]);return x("Functional Tests",B(["Test","N","Pass","Fail","Pass Rate"],r,{className:"compact"}))}function X(t,o){const s=Le(t,o);if(!s.length)return"";const n=e=>e===null?"\u2014":e.toFixed(2),r=s.map(e=>[h(e.label),e.hasSpec?`${F(e.lsl,e.unit)} \u2013 ${F(e.usl,e.unit)}`:"\u2014",F(e.mean,e.unit),n(e.cp),n(e.cpk),n(e.pp),n(e.ppk)]);return x("Process Capability",B(["Test","Spec (LSL\u2013USL)","Mean","Cp","Cpk","Pp","Ppk"],r,{className:"compact"}))}function Z(t,o){const s=t.map(r=>`<tr title="${h(Ae(r))}">
2
+ <td class="tight">${Ge(r.severity)}</td>
3
+ <td class="tight">${h(r.comparison.left)}</td>
4
+ <td>${h(r.variable.label)}</td>
5
+ <td class="numeric">${h(Oe(r))}</td>
6
+ <td class="numeric">${h(ze(r,o))}</td>
7
7
  </tr>`).join(`
8
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 Q(s,l){const i=H(s);return i.length?v("Findings",_(i,l)):""}export function renderSummaryReportHtml(s,l={}){const{wafer:i,dies:n,yieldSummary:a,dataCoverage:e,hbinDefs:o,sbinDefs:r,testDefs:d=[],statsSummary:u,passBins:t=[1],ringCount:c=4}=s,b=i.metadata,m=We(b),w=l.title??`Wafer Summary${m}`,p=new Date().toLocaleString(),f=n.some(P=>P.hbin!=null),g=n.some(P=>P.sbin!=null),y=n.filter(j),$=E(y,i,c),x=k(y,i,c),T=[{label:"Total dies",value:String(a.totalDies)},{label:"Filled dies",value:String(e.filledDies),hint:`${Y(e.filledDies,e.totalDies)} fill`},{label:"Pass dies",value:String(a.passDies)},{label:"Fail dies",value:String(a.failDies)},...a.partialDies>0?[{label:"Partial",value:String(a.partialDies)}]:[],...a.yieldPercent!==null?[{label:`Yield (pass: ${t.length===1?`bin ${t[0]}`:`bins ${t.join(", ")}`})`,value:`${a.yieldPercent.toFixed(1)}%`}]:[],...a.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(a.edgeExcludedDies)}]:[]],N=[A([{metadata:i.metadata}]),v("Summary",[O(T)].filter(Boolean).join(`
11
- `)),f?z(n,o,"hard",u?.stats.hardBinCounts,t):g?z(n,r,"soft",u?.stats.softBinCounts,t):"",G("Ring Yield",$,n,t),G("Quadrant Yield",x,n,t),K(n,d,u?.stats.perTestStats),U(n,d,u?.stats.functionalYield),V([{dies:n}],d),u?Q(u.findings):""].filter(Boolean).join(`
9
+ <th>Severity</th><th>Region</th><th>Metric</th><th class="numeric">Delta</th><th class="numeric">${o!==void 0?"Wafers":"N (region/rest)"}</th>
10
+ </tr></thead><tbody>${s}</tbody></table>`}function ee(t,o){const s=A(t);return s.length?x("Findings",Z(s,o)):""}export function renderSummaryReportHtml(t,o={}){const{wafer:s,dies:n,yieldSummary:r,dataCoverage:e,hbinDefs:i,sbinDefs:l,testDefs:d=[],statsSummary:f,passBins:a=[1],ringCount:c=4}=t,b=s.metadata,p=Ue(b),w=o.title??`Wafer Summary${p}`,y=new Date().toLocaleString(),u=n.some(Y=>Y.hbin!=null),g=n.some(Y=>Y.sbin!=null),S=n.filter(je),D=j(S,s,c),$=O(S,s,c),v=[{label:"Total dies",value:String(r.totalDies)},{label:"Filled dies",value:String(e.filledDies),hint:`${C(e.filledDies,e.totalDies)} fill`},{label:"Pass dies",value:String(r.passDies)},{label:"Fail dies",value:String(r.failDies)},...r.partialDies>0?[{label:"Partial",value:String(r.partialDies)}]:[],...r.yieldPercent!==null?[{label:`Yield (pass: ${xe([a])})`,value:`${r.yieldPercent.toFixed(1)}%`}]:[],...r.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(r.edgeExcludedDies)}]:[]],P=[V([{metadata:s.metadata}]),x("Summary",[_(v)].filter(Boolean).join(`
11
+ `)),u?K(n,i,"hard",f?.stats.hardBinCounts,a):g?K(n,l,"soft",f?.stats.softBinCounts,a):"",Q("Ring Yield",D,n,a),Q("Quadrant Yield",$,n,a),q(n,d,f?.stats.perTestStats),J(n,d,f?.stats.functionalYield),X([{dies:n}],d),f?ee(f.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>${S(w)}</title>
16
+ <title>${h(w)}</title>
17
17
  <style>
18
- ${W()}
18
+ ${N()}
19
19
  </style>
20
20
  </head>
21
21
  <body>
22
22
  <main class="report">
23
23
  <header class="report-header">
24
- <h1>${S(w)}</h1>
25
- <p class="report-subtitle">Generated ${S(p)}</p>
24
+ <h1>${h(w)}</h1>
25
+ <p class="report-subtitle">Generated ${h(y)}</p>
26
26
  </header>
27
- ${N}
28
- <p class="footer">Generated ${S(p)}</p>
27
+ ${P}
28
+ <p class="footer">Generated ${h(y)}</p>
29
29
  </main>
30
30
  </body>
31
- </html>`}function Re(s,l){const i=s.perWafer.map(n=>{const a=l[n.waferIndex]?.label??`W${n.waferIndex+1}`,e=n.summary.stats.yieldPercent;return[a,e!==null?`${e.toFixed(1)}%`:"N/A"]});return F(["Wafer","Yield"],i,{className:"compact"})}function Ee(s){const l=[];for(const e of s.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>`+_(o))}if(!l.length)return"";const i=l.length,n=s.perWafer.length,a=`<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",a+l.join(`
32
- `))}const ke=3;function Ce(s,l,i){const n=s.map(d=>({metadata:d.wafer?.metadata})),a=Se(n,{facetableOnly:!0}).filter(d=>d.splittable);if(!a.length)return"";const e=a.slice(0,ke),o=[];for(const d of e){const u=new Map;s.forEach((f,g)=>{const y=we(f.wafer?.metadata,d.key)??$e;(u.get(y)??u.set(y,[]).get(y)).push(g)});const t=[...u.entries()].map(([f,g])=>({key:f,items:g.map(y=>s[y])}));if(t.length<2)continue;const c=t.map(f=>[S(f.key),S(f.items.map(g=>g.label).join(", "))]),b=xe(t.map(f=>({key:f.key,items:f.items.map(g=>({label:g.label,dies:g.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"],b,{className:"compact"})],w=s.flatMap(f=>f.dies??[]);let p=0;for(const f of["spec","testFlag","functional"]){if(!Fe(l,f,w))continue;const g=Te(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(!g.rows.length)continue;const y=g.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";g.disagreementDies&&(p=g.disagreementDies),m.push(`<h4 class="report-subheading">${S($)}</h4>`+F(["Test",...g.groups,"All"],y,{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=a.length>e.length?`<p class="report-note">${a.length-e.length} further splittable field(s) not shown.</p>`:"";return v("Split Comparison",o.join(`
34
- `)+r)}function q(s,l,i,n,a=[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,b]of Object.entries(t.stats[o])){const m=Number(c);e.set(m,(e.get(m)??0)+b)}else for(const t of s){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(),a).map(([t,c])=>{const b=d?.get(t);return[b?.name?`Bin ${t} \xB7 ${b.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 J(s,l,i,n,a,e){const o=new Set(e),r=new Map,d=[];for(let t=0;t<n.length;t++){const c=n[t],b=i[t]??[];if(!b.length)continue;const m=l(b.filter(j),c,a),w=new Map(b.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 g of p.dieKeys){const y=w.get(g);if(!y||y.partial||y.edgeExcluded)continue;const $=y.hbin??y.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(s,F(["Region","Yield"],u,{className:"compact"})):""}function He(s,l,i){let n;if(i?.length&&i.every(a=>a.stats.perTestStats!==void 0)){const a=new Map;for(const e of i)for(const o of e.stats.perTestStats??[]){const r=a.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)):a.set(o.testNumber,{n:o.count,sum:o.mean*o.count,min:o.min,max:o.max})}n=[...a.entries()].map(([e,o])=>({testNumber:e,min:o.min,max:o.max,mean:o.sum/o.n}))}return K(s,l,n)}function Ie(s,l,i){const n=De(i);return U(s,l,n)}const R=["lot","product","testProgram","temperature"];function Le(s){const l=new Map(R.map(e=>[e,new Set]));for(const e of s)for(const o of R){const r=e.wafer?.metadata?.[o];r!=null&&r!==""&&l.get(o).add(String(r))}const i=R.filter(e=>l.get(e).size>1);if(i.length===0)return[{label:"",items:s,varying:i}];const n=new Map,a=[];for(const e of s){const o=i.map(r=>String(e.wafer?.metadata?.[r]??"(none)")).join(" \xB7 ");n.has(o)||(n.set(o,[]),a.push(o)),n.get(o).push(e)}return a.map(e=>({label:e,items:n.get(e),varying:i}))}function X(s,l,i,n,a,e,o){const r=be(s,{perWaferSummaries:s.map(h=>h.statsSummary),...o}),d=[],u=[],t=[];for(const h of s){const D=h.dies??[];h.wafer&&(d.push(h.wafer),t.push(D)),D.length&&u.push(...D)}const c=u.some(h=>h.hbin!=null),b=u.some(h=>h.sbin!=null),m=c||b,w=r.perWafer.map(h=>h.summary.stats.yieldPercent).filter(h=>h!==null),p=w.length?w.reduce((h,D)=>h+D,0)/w.length:null,f=new Set(a);let g=0,y=0,$=0,x=0,T=0;for(const h of u){if(g++,h.edgeExcluded&&x++,h.partial&&T++,h.partial||h.edgeExcluded)continue;const D=h.hbin??h.sbin;D!=null&&(y++,f.has(D)&&$++)}const N=y-$,P=y>0?$/y*100:null,ee=[{label:"Wafers",value:String(r.stats.waferCount)},{label:"Total dies",value:String(g)},...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)}%`}]:[]],te=I(r.perWafer.map(h=>h.summary.wafer)),ne=v(te.lotId!==void 0?"Lot Summary":"Summary",O(ee)),ae=A(s.map(h=>({metadata:h.wafer?.metadata}))),se=v("Per-Wafer Yield",Re(r,s)),oe=Ce(s,n,a),M=r.perWafer.map(h=>h.summary),ie=m?c?q(u,l,"hard",M,a):q(u,i,"soft",M,a):"",le=m&&d.length?J("Ring Yield (All Wafers)",E,t,d,e,a):"",re=m&&d.length?J("Quadrant Yield (All Wafers)",k,t,d,e,a):"",ce=n.length?He(u,n,M):"",de=n.length?Ie(u,n,M):"",ue=n.length?V(s,n):"",fe=r.findings.length?Q(r.findings,r.stats.waferCount):"",me=Ee(r),pe=[ne,ae,se,oe,ie,le,re,ce,de,ue,fe,me].filter(Boolean).join(`
35
- `);return{lotSummary:r,sections:pe}}function Z(s,l,i){return`<main class="report">
31
+ </html>`}function Ve(t,o){const s=t.perWafer.map(n=>{const r=L(o[n.waferIndex],n.waferIndex),e=n.summary.stats.yieldPercent;return[r,e!==null?`${e.toFixed(1)}%`:"N/A"]});return B(["Wafer","Yield"],s,{className:"compact"})}function _e(t){const o=[];for(const e of t.perWafer){const i=A(e.summary.findings);if(!i.length)continue;const l=e.summary.wafer?.waferId??`Wafer ${e.waferIndex+1}`;o.push(`<h3 class="report-subheading">${h(l)}</h3>`+Z(i))}if(!o.length)return"";const s=o.length,n=t.perWafer.length,r=`<p class="report-note">${s} of ${n} wafer${n===1?"":"s"} have per-wafer findings; the rest had none.</p>`;return x("Findings by Wafer",r+o.join(`
32
+ `))}const Ke=3;function Qe(t,o,s){const n=t.map(d=>({metadata:d.wafer?.metadata})),r=We(n,{facetableOnly:!0}).filter(d=>d.splittable);if(!r.length)return"";const e=r.slice(0,Ke),i=[];for(const d of e){const f=new Map;t.forEach((u,g)=>{const S=Me(u.wafer?.metadata,d.key)??Ce;(f.get(S)??f.set(S,[]).get(S)).push(g)});const a=[...f.entries()].map(([u,g])=>({key:u,items:g.map(S=>t[S])}));if(a.length<2)continue;const c=a.map(u=>[h(u.key),h(u.items.map(g=>g.label).join(", "))]),b=Re(a.map(u=>({key:u.key,items:u.items.map(g=>({label:g.label,dies:g.dies,passBins:R(g,s)}))})),s).map(u=>[h(u.label),String(u.itemCount),`${u.percent.toFixed(1)}%`]),p=[B([d.label,"Wafers"],c,{className:"compact"}),B([d.label,"Wafers","Yield"],b,{className:"compact"})],w=t.flatMap(u=>u.dies??[]);let y=0;for(const u of["spec","testFlag","functional"]){if(!ke(o,u,w))continue;const g=Ee(a.map($=>({key:$.key,items:$.items.map(v=>({dies:v.dies,testSpecYield:v.statsSummary?.stats.testSpecYield,functionalYield:v.statsSummary?.stats.functionalYield}))})),o,u);if(!g.rows.length)continue;const S=g.rows.map($=>[h($.label),...$.byGroup.map(v=>v.passRatePercent===null?"\u2014":`${v.passRatePercent.toFixed(1)}%`),$.overall.passRatePercent===null?"\u2014":`${$.overall.passRatePercent.toFixed(1)}%`]),D=u==="spec"?"Pass rate \xB7 spec limits":u==="testFlag"?"Pass rate \xB7 tester flag":"Pass rate \xB7 functional";g.disagreementDies&&(y=g.disagreementDies),p.push(`<h4 class="report-subheading">${h(D)}</h4>`+B(["Test",...g.groups,"All"],S,{className:"compact"}))}y&&p.push(`<p class="report-note">${y.toLocaleString()} dies judged differently by limits and tester flag.</p>`),i.push(`<h3 class="report-subheading">${h(d.label)}</h3>${p.join(`
33
+ `)}`)}if(!i.length)return"";const l=r.length>e.length?`<p class="report-note">${r.length-e.length} further splittable field(s) not shown.</p>`:"";return x("Split Comparison",i.join(`
34
+ `)+l)}function te(t,o,s,n,r){const e=new Map,i=s==="hard"?"hardBinCounts":"softBinCounts";if(n?.length&&n.every(a=>a.stats[i]!==void 0))for(const a of n)for(const[c,b]of Object.entries(a.stats[i])){const p=Number(c);e.set(p,(e.get(p)??0)+b)}else for(const a of t){if(a.partial||a.edgeExcluded)continue;const c=s==="hard"?a.hbin:a.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const l=[...e.values()].reduce((a,c)=>a+c,0),d=o?new Map(o.map(a=>[a.bin,a])):null,f=z(e.entries(),$e(r)[s]).map(([a,c])=>{const b=d?.get(a);return[b?.name?`Bin ${a} \xB7 ${b.name} (${c})`:`Bin ${a} (${c})`,C(c,l)]});return x(s==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",B(["Bin","Yield"],f,{className:"compact"}))}function ne(t,o,s,n,r,e){const i=ve(s,n,r,l=>e[l],o).map(l=>[l.label,`${l.yieldPercent.toFixed(1)}%`]);return i.length?x(t,B(["Region","Yield"],i,{className:"compact"})):""}function qe(t,o,s){let n;if(s?.length&&s.every(r=>r.stats.perTestStats!==void 0)){const r=new Map;for(const e of s)for(const i of e.stats.perTestStats??[]){const l=r.get(i.testNumber);l?(l.n+=i.count,l.sum+=i.mean*i.count,l.min=Math.min(l.min,i.min),l.max=Math.max(l.max,i.max)):r.set(i.testNumber,{n:i.count,sum:i.mean*i.count,min:i.min,max:i.max})}n=[...r.entries()].map(([e,i])=>({testNumber:e,min:i.min,max:i.max,mean:i.sum/i.n}))}return q(t,o,n)}function Je(t,o,s){const n=He(s);return J(t,o,n)}const E=["lot","product","testProgram","temperature"];function Xe(t){const o=new Map(E.map(e=>[e,new Set]));for(const e of t)for(const i of E){const l=e.wafer?.metadata?.[i];l!=null&&l!==""&&o.get(i).add(String(l))}const s=E.filter(e=>o.get(e).size>1);if(s.length===0)return[{label:"",items:t,varying:s}];const n=new Map,r=[];for(const e of t){const i=s.map(l=>String(e.wafer?.metadata?.[l]??"(none)")).join(" \xB7 ");n.has(i)||(n.set(i,[]),r.push(i)),n.get(i).push(e)}return r.map(e=>({label:e,items:n.get(e),varying:s}))}function se(t,o,s,n,r,e,i){const l=Te(t.map(m=>m.source??m),{perWaferSummaries:t.map(m=>m.statsSummary),...i}),d=[],f=[],a=[],c=[];for(const m of t){const T=m.dies??[];m.wafer&&(d.push(m.wafer),a.push(T),c.push(R(m,r))),T.length&&f.push(...T)}const b=t.map(m=>({dies:m.dies??[],passBins:R(m,r)})),p=f.some(m=>m.hbin!=null),w=f.some(m=>m.sbin!=null),y=p||w,u=l.perWafer.map(m=>m.summary.stats.yieldPercent).filter(m=>m!==null),g=u.length?u.reduce((m,T)=>m+T,0)/u.length:null;let S=0,D=0,$=0,v=0,P=0;for(const m of b){const T=new Set(m.passBins);for(const W of m.dies){if(S++,W.edgeExcluded&&v++,W.partial&&P++,W.partial||W.edgeExcluded)continue;const H=U(W,T);H!==void 0&&(D++,H&&$++)}}const Y=D-$,k=D>0?$/D*100:null,ie=[{label:"Wafers",value:String(l.stats.waferCount)},{label:"Total dies",value:String(S)},...y?[{label:"Good dies",value:String($)},{label:"Bad dies",value:String(Y)}]:[],...v>0?[{label:"Edge excluded",value:String(v)}]:[],...P>0?[{label:"Partial dies",value:String(P)}]:[],...g!==null?[{label:"Mean wafer yield",value:`${g.toFixed(1)}%`}]:[],...k!==null?[{label:"Total yield",value:`${k.toFixed(1)}%`}]:[]],oe=G(l.perWafer.map(m=>m.summary.wafer)),re=x(oe.lotId!==void 0?"Lot Summary":"Summary",_(ie)),le=V(t.map(m=>({metadata:m.wafer?.metadata}))),ce=x("Per-Wafer Yield",Ve(l,t)),de=Qe(t,n,r),M=l.perWafer.map(m=>m.summary),ue=y?p?te(f,o,"hard",M,b):te(f,s,"soft",M,b):"",fe=y&&d.length?ne("Ring Yield (All Wafers)",j,a,d,e,c):"",me=y&&d.length?ne("Quadrant Yield (All Wafers)",O,a,d,e,c):"",pe=n.length?qe(f,n,M):"",ge=n.length?Je(f,n,M):"",he=n.length?X(t,n):"",be=l.findings.length?ee(l.findings,l.stats.waferCount):"",ye=_e(l),Se=[re,le,ce,de,ue,fe,me,pe,ge,he,be,ye].filter(Boolean).join(`
35
+ `);return{lotSummary:l,sections:Se}}function ae(t,o,s){return`<main class="report">
36
36
  <header class="report-header">
37
- <h1>${S(s)}</h1>
38
- <p class="report-subtitle">Generated ${S(i)}</p>
37
+ <h1>${h(t)}</h1>
38
+ <p class="report-subtitle">Generated ${h(s)}</p>
39
39
  </header>
40
- ${l}
41
- <p class="footer">Generated ${S(i)}</p>
42
- </main>`}export function renderLotSummaryReportHtml(s,l={}){const{items:i,hbinDefs:n,sbinDefs:a,testDefs:e=[],passBins:o=[1],ringCount:r=4,analyzeOptions:d}=s,u=new Date().toLocaleString(),t=Le(i);if(t.length===1){const{lotSummary:p,sections:f}=X(t[0].items,n,a,e,o,r,d),g=I(p.perWafer.map($=>$.summary.wafer)),y=l.title??(g.lotId!==void 0?`Lot Summary \u2014 ${g.lotId}`:`Summary \u2014 ${ve(g)}`);return`<!DOCTYPE html>
40
+ ${o}
41
+ <p class="footer">Generated ${h(s)}</p>
42
+ </main>`}export function renderLotSummaryReportHtml(t,o={}){const{items:s,hbinDefs:n,sbinDefs:r,testDefs:e=[],passBins:i=[1],ringCount:l=4,analyzeOptions:d}=t,f=new Date().toLocaleString(),a=Xe(s);if(a.length===1){const{lotSummary:y,sections:u}=se(a[0].items,n,r,e,i,l,d),g=G(y.perWafer.map(D=>D.summary.wafer)),S=o.title??(g.lotId!==void 0?`Lot Summary \u2014 ${g.lotId}`:`Summary \u2014 ${Ne(g)}`);return`<!DOCTYPE html>
43
43
  <html lang="en">
44
44
  <head>
45
45
  <meta charset="UTF-8">
46
- <title>${S(y)}</title>
46
+ <title>${h(S)}</title>
47
47
  <style>
48
- ${W()}
48
+ ${N()}
49
49
  </style>
50
50
  </head>
51
51
  <body>
52
- ${Z(y,f,u)}
52
+ ${ae(S,u,f)}
53
53
  </body>
54
- </html>`}const c=l.title??"Summary",b=t.map(p=>{const{lotSummary:f,sections:g}=X(p.items,n,a,e,o,r,d),y=p.varying.includes("lot")?void 0:f.lot?.lot??f.lot?.lotId,$=y?`${p.label} \xB7 Lot ${String(y)}`:p.label;return Z(`${c} \u2014 ${$}`,g,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=b.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(`
54
+ </html>`}const c=o.title??"Summary",b=a.map(y=>{const{lotSummary:u,sections:g}=se(y.items,n,r,e,i,l,d),S=y.varying.includes("lot")?void 0:u.lot?.lot??u.lot?.lotId,D=S?`${y.label} \xB7 Lot ${String(S)}`:y.label;return ae(`${c} \u2014 ${D}`,g,f)}),p=`<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 ${a.length} groups by identity \u2014 shown separately below so stats are never pooled across them.</p>`,w=b.map((y,u)=>`${u===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong);">'}
55
+ ${y}`).join(`
56
56
  `);return`<!DOCTYPE html>
57
57
  <html lang="en">
58
58
  <head>
59
59
  <meta charset="UTF-8">
60
- <title>${S(c)}</title>
60
+ <title>${h(c)}</title>
61
61
  <style>
62
- ${W()}
62
+ ${N()}
63
63
  </style>
64
64
  </head>
65
65
  <body>
66
66
  <div class="report" style="padding-top:24px;">
67
- ${m}
67
+ ${p}
68
68
  </div>
69
69
  ${w}
70
70
  </body>
71
- </html>`}export{he as openHtmlReport};
71
+ </html>`}export{Be as openHtmlReport};export function renderWaferReportHtml(t,o,s={}){return renderSummaryReportHtml({wafer:t.wafer,dies:t.dies,yieldSummary:t.yield,dataCoverage:t.dataCoverage,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,testDefs:t.testDefs,statsSummary:o??t.statsSummary??("view"in t?Pe(t):void 0),passBins:[...t.passBins],ringCount:t.ringCount},s)}export function renderLotReportHtml(t,o={}){const s=I(t.map(e=>e.hbinDefs)),n=I(t.map(e=>e.sbinDefs)),r=t.some(e=>e.testDefs?.length)?Ye([...t]).defs:void 0;return renderLotSummaryReportHtml({items:t.map((e,i)=>({label:L(e,i),wafer:e.wafer,dies:e.dies,passBins:[...e.passBins],statsSummary:e.statsSummary,source:"view"in e?e:void 0})),hbinDefs:s.length?s:void 0,sbinDefs:n.length?n:void 0,testDefs:r,ringCount:t[0]?.ringCount,analyzeOptions:o.analyzeOptions},{title:o.title})}
@@ -5,7 +5,6 @@ export interface MetricItem {
5
5
  value: string;
6
6
  hint?: string;
7
7
  }
8
- export declare function escHtml(value: string): string;
9
8
  export declare function renderSection(title: string, body: string, className?: string): string;
10
9
  export declare function renderDefinitionList(entries: Array<{
11
10
  label: string;
@@ -1,20 +1,20 @@
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>
1
+ import{fmt as n}from"../renderer/fmt.js";import{buildFacetTable as d,prettyKey as c}from"./facets.js";import{escHtml as a}from"../core/utils.js";export function renderSection(e,t,r=""){return`<section class="${["report-section",r].filter(Boolean).join(" ")}">
2
+ <h2>${a(e)}</h2>
3
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(`
4
+ </section>`}export function renderDefinitionList(e,t="meta-list"){if(!e.length)return"";const r=e.map(o=>`<div class="definition-item"><dt>${a(o.label)}</dt><dd>${a(o.value)}</dd></div>`).join(`
5
5
  `);return`<dl class="${t}">
6
6
  ${r}
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">
7
+ </dl>`}export function buildMetadataRows(e){return d(e,{facetableOnly:e.length>1}).map(r=>({label:c(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
8
  ${e.map(r=>`<div class="metric">
9
- <dt>${escHtml(r.label)}</dt>
10
- <dd>${escHtml(r.value)}</dd>
11
- ${r.hint?`<span class="metric-hint">${escHtml(r.hint)}</span>`:""}
9
+ <dt>${a(r.label)}</dt>
10
+ <dd>${a(r.value)}</dd>
11
+ ${r.hint?`<span class="metric-hint">${a(r.hint)}</span>`:""}
12
12
  </div>`).join(`
13
13
  `)}
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(`
14
+ </dl>`:""}export function renderTable(e,t,r={}){if(!t.length)return`<p class="no-data">${a(r.emptyMessage??"No data")}</p>`;const o=["report-table",r.className].filter(Boolean).join(" "),p=e.map(i=>`<th>${a(i)}</th>`).join(""),l=t.map(i=>`<tr>${i.map(s=>`<td>${s}</td>`).join("")}</tr>`).join(`
15
15
  `);return`<table class="${o}">
16
- <thead><tr>${i}</tr></thead>
17
- <tbody>${p}</tbody>
16
+ <thead><tr>${p}</tr></thead>
17
+ <tbody>${l}</tbody>
18
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;
@@ -0,0 +1,47 @@
1
+ import type { Die, Wafer } from '../core/index.js';
2
+ import type { TestDef } from '../renderer/buildWaferMap.js';
3
+ import type { TestCapability, TestVerdictYield, RegionYieldFigures, StatsSummary } from './types.js';
4
+ /** Capability over `items` (each one wafer — the subgroup), restricted to `testNumbers`. */
5
+ export declare function computeCapability(items: {
6
+ dies: Die[];
7
+ }[], testDefs: TestDef[] | undefined, testNumbers: readonly number[]): TestCapability[] | undefined;
8
+ /**
9
+ * Lot capability pooled from each wafer's own `stats.capability` — exact, and
10
+ * without revisiting a die. Absent unless every wafer has capability, like the
11
+ * other pooled lot figures.
12
+ */
13
+ export declare function poolCapability(summaries: StatsSummary[], testDefs: TestDef[]): TestCapability[] | undefined;
14
+ /** Pass rates by the tester's recorded verdict, plus the spec/verdict disagreement count. */
15
+ export declare function computeTestFlagYield(dies: Die[], testDefs: TestDef[] | undefined): {
16
+ testFlagYield?: TestVerdictYield[];
17
+ specVerdictDisagreementDies?: number;
18
+ };
19
+ /**
20
+ * Ring and quadrant yield over one or more wafers, each die judged by its own
21
+ * wafer's pass bins and rings by its own ring count. Rings are pooled only when
22
+ * every wafer has the same ring count — otherwise there are no common rings
23
+ * (the gallery raises `ring-count-mixed` for the same case) and `ring` is absent.
24
+ */
25
+ export declare function computeRegionYield(maps: {
26
+ dies: Die[];
27
+ wafer: Wafer;
28
+ passBins?: readonly number[];
29
+ ringCount: number;
30
+ }[]): RegionYieldFigures | undefined;
31
+ /**
32
+ * Lot region yield pooled from each wafer's own `regionYield`: pass and die
33
+ * counts summed per region key, first-appearance order. Pooled from the
34
+ * summaries rather than recomputed over every die, so a lot analysis given
35
+ * `perWaferSummaries` stays cheap. Same rule as the pass rates: absent unless
36
+ * every wafer has it, and rings pool only when every wafer has the same ring
37
+ * count (the gallery raises `ring-count-mixed` for the same case).
38
+ */
39
+ export declare function poolRegionYield(summaries: StatsSummary[], ringCounts: readonly number[]): RegionYieldFigures | undefined;
40
+ /** The lot-level pass-rate figures: each wafer's counts, summed per test, only when every wafer has them. */
41
+ export declare function poolPassRates(summaries: StatsSummary[]): {
42
+ testSpecYield?: NonNullable<StatsSummary['stats']['testSpecYield']>;
43
+ functionalYield?: NonNullable<StatsSummary['stats']['functionalYield']>;
44
+ testFlagYield?: TestVerdictYield[];
45
+ specVerdictDisagreementDies?: number;
46
+ };
47
+ //# sourceMappingURL=summaryFigures.d.ts.map
@@ -0,0 +1 @@
1
+ import{itemPassBins as D}from"../core/passBins.js";import{buildCapabilityFigures as m,poolCapabilityFigures as h}from"./capability.js";import{buildTestPassRateData as y,hasJudgeableTests as w,poolFunctionalYield as b}from"./testPassRate.js";import{buildRegionYieldData as c,buildRingRegions as Y,buildQuadrantRegions as R}from"./regions.js";export function computeCapability(t,i,n){if(!i?.length||!n.length)return;const o=new Set(n),r=m(t,i.filter(d=>o.has(d.testNumber)));return r.length?r:void 0}export function poolCapability(t,i){const n=t.map(r=>r.stats.capability);if(!n.length||n.some(r=>r===void 0))return;const o=h(n,i);return o.length?o:void 0}export function computeTestFlagYield(t,i){if(!w(i,"testFlag",t))return{};const n=y([{key:"",items:[{dies:t}]}],i,"testFlag"),o={};return n.rows.length&&(o.testFlagYield=n.rows.map(r=>({testNumber:r.testNumber,label:r.label,...r.overall}))),n.disagreementDies!==null&&(o.specVerdictDisagreementDies=n.disagreementDies),o}export function computeRegionYield(t){if(!t.length)return;const i=t.map(l=>l.dies),n=t.map(l=>l.wafer),o=l=>D(t[l]),r=new Set(t.map(l=>l.ringCount)),d=c(i,n,t[0].ringCount,o,R),s=r.size===1?c(i,n,t[0].ringCount,o,Y):void 0;if(!(!d.length&&!s?.length))return{...s?.length?{ring:s}:{},quadrant:d}}export function poolRegionYield(t,i){const n=t.map(s=>s.stats.regionYield);if(!n.length||n.some(s=>s===void 0))return;const o=s=>{if(s.some(a=>a===void 0))return;const l=new Map;for(const a of s)for(const u of a){const f=l.get(u.key);f?(f.n+=u.n,f.passDies+=u.passDies):l.set(u.key,{...u})}const e=[...l.values()];for(const a of e)a.yieldPercent=a.passDies/a.n*100;return e.length?e:void 0},r=o(n.map(s=>s.quadrant)),d=new Set(i).size===1?o(n.map(s=>s.ring)):void 0;if(!(!r&&!d))return{...d?{ring:d}:{},quadrant:r??[]}}function p(t,i,n){if(!t.length||t.some(d=>d===void 0))return;const o=new Map;for(const d of t)for(const s of d??[]){const l=o.get(s.testNumber);l?i(l,s):o.set(s.testNumber,{...s})}if(!o.size)return;const r=[...o.values()];return r.forEach(n),r}const g=(t,i)=>i===0?null:t/i*100;export function poolPassRates(t){const i={},n=p(t.map(e=>e.stats.testSpecYield),(e,a)=>{e.passDies+=a.passDies,e.failLowDies+=a.failLowDies,e.failHighDies+=a.failHighDies,e.totalDies+=a.totalDies},e=>{e.yieldPercent=g(e.passDies,e.totalDies)});n&&(i.testSpecYield=n);const o=(e,a)=>{e.passDies+=a.passDies,e.failDies+=a.failDies,e.totalDies+=a.totalDies},r=e=>{e.passRatePercent=g(e.passDies,e.totalDies)},d=b(t);d&&(i.functionalYield=d);const s=p(t.map(e=>e.stats.testFlagYield),o,r);s&&(i.testFlagYield=s.sort((e,a)=>(e.passRatePercent??1/0)-(a.passRatePercent??1/0)));const l=t.map(e=>e.stats.specVerdictDisagreementDies);return l.length&&l.every(e=>e!==void 0)&&(i.specVerdictDisagreementDies=l.reduce((e,a)=>e+a,0)),i}
@@ -1,3 +1,4 @@
1
+ import type { PatternClassification } from './patternClassification.js';
1
2
  import type { WaferMapInput, WaferMapResult, WaferWarning } from '../renderer/buildWaferMap.js';
2
3
  export type { WaferWarning };
3
4
  export type StatsSeverity = 'info' | 'notable' | 'unusual';
@@ -266,8 +267,108 @@ export interface StatsSummary {
266
267
  q1: number;
267
268
  q3: number;
268
269
  }>;
270
+ /**
271
+ * Cp/Cpk/Pp/Ppk for each parametric test with values, worst first: tests with
272
+ * both limits by Ppk, then tests without by variability. Populated under the
273
+ * same conditions as `perTestStats` and over the same tests.
274
+ */
275
+ capability?: TestCapability[];
276
+ /**
277
+ * Per-test pass rate by the tester's recorded verdict, for parametric tests
278
+ * with recorded verdicts — the counterpart of `testSpecYield` (spec limits)
279
+ * and `functionalYield` (functional tests). Worst first.
280
+ */
281
+ testFlagYield?: TestVerdictYield[];
282
+ /**
283
+ * Dies whose spec-limit judgement and recorded tester verdict disagree, across
284
+ * every parametric test that has both. Absent when no test has both sources;
285
+ * `0` means both are present and agree everywhere. Not an error — guard bands
286
+ * make disagreement expected — but worth a look.
287
+ */
288
+ specVerdictDisagreementDies?: number;
289
+ /** Yield by ring and by quadrant (positioned dies only). */
290
+ regionYield?: RegionYieldFigures;
291
+ /**
292
+ * The spatial pattern classifier's result for this wafer — its label, confidence
293
+ * and the geometry features it measured (failure density overall and at the edge,
294
+ * the failing cluster's radial position, eccentricity, linearity, …). Present for
295
+ * every wafer the classifier could measure, including `'random'` and `'none'`,
296
+ * which raise no finding — so the features can train or feed a model of your own,
297
+ * negatives included. Absent when the map has no bin data, or too few failing dies
298
+ * to have a shape (fewer than 5, or 0.3% of the wafer).
299
+ */
300
+ spatialPattern?: PatternClassification;
269
301
  };
270
302
  }
303
+ /**
304
+ * Process capability for one parametric test.
305
+ *
306
+ * Cp/Cpk use `stdWithin` — the pooled within-wafer sample stddev, each wafer
307
+ * being the short-term subgroup — and Pp/Ppk use `stdOverall`. For a single
308
+ * wafer there is one subgroup, so `stdWithin` equals `stdOverall` and Cp equals
309
+ * Pp. Partial and edge-excluded dies are excluded, as for yield.
310
+ */
311
+ export interface TestCapability {
312
+ testNumber: number;
313
+ label: string;
314
+ unit?: string;
315
+ /** Both `limitLow` and `limitHigh` defined. When false, `lsl`/`usl` are absent and every index is null. */
316
+ hasSpec: boolean;
317
+ lsl?: number;
318
+ usl?: number;
319
+ mean: number;
320
+ /** Sample stddev (ddof = 1) over every value. */
321
+ stdOverall: number;
322
+ /** Pooled within-wafer sample stddev. NaN when no wafer contributed two or more values. */
323
+ stdWithin: number;
324
+ /** Values counted. */
325
+ n: number;
326
+ /** Null when `hasSpec` is false or `stdWithin` is NaN or 0. */
327
+ cp: number | null;
328
+ cpk: number | null;
329
+ /** Null when `hasSpec` is false or `stdOverall` is 0. */
330
+ pp: number | null;
331
+ ppk: number | null;
332
+ }
333
+ /**
334
+ * Pass rate for one test, judged by the tester's own recorded verdict
335
+ * (`die.testPass`, STDF PTR `TEST_FLG`) — which exists whether or not spec
336
+ * limits do, and can legitimately disagree with a spec-limit judgement (guard
337
+ * bands, dynamic or per-site limits). Read through `getTestPassStatus`; dies
338
+ * with no recorded verdict are not counted as fails.
339
+ */
340
+ export interface TestVerdictYield {
341
+ testNumber: number;
342
+ label: string;
343
+ passDies: number;
344
+ failDies: number;
345
+ /** Dies with a recorded verdict for this test. */
346
+ totalDies: number;
347
+ /** `(passDies / totalDies) × 100`, or `null` when no die had a verdict. */
348
+ passRatePercent: number | null;
349
+ }
350
+ /** Yield of one wafer region, each die judged by its own wafer's pass bins. */
351
+ export interface RegionYield {
352
+ /** Stable identity, e.g. `"ring:2"` or `"quadrant:NE"`. Use it as a key; do not parse it. */
353
+ key: string;
354
+ /** Display name, e.g. `"Ring 4 (edge)"` or `"NE"`. */
355
+ label: string;
356
+ /** `(pass / n) × 100`, in [0, 100]. */
357
+ yieldPercent: number;
358
+ /** Yield-eligible dies with a bin in this region. */
359
+ n: number;
360
+ /** Of those, the dies that pass — so regions can be pooled exactly. */
361
+ passDies: number;
362
+ }
363
+ /**
364
+ * Yield by ring and by quadrant — the Summary panel's region yield. Regions with
365
+ * no eligible dies are omitted. `ring` is absent from a lot summary whose wafers
366
+ * were built with different ring counts, which have no common rings to pool.
367
+ */
368
+ export interface RegionYieldFigures {
369
+ ring?: RegionYield[];
370
+ quadrant: RegionYield[];
371
+ }
271
372
  export interface LotStatsSummary {
272
373
  /** Discriminant — always `'lot'`. Narrow on this to tell a lot summary from a {@link StatsSummary}. */
273
374
  level: 'lot';
@@ -302,6 +403,23 @@ export interface LotStatsSummary {
302
403
  /** Engine-computed analysis stats for this lot. */
303
404
  stats: {
304
405
  waferCount: number;
406
+ /**
407
+ * Cp/Cpk/Pp/Ppk over the whole lot, each wafer as a subgroup — so `stdWithin`
408
+ * is the pooled within-wafer stddev and Cp and Pp genuinely differ. Present
409
+ * when per-test statistics were computed (`computePerTestStats` or
410
+ * `enableTestValueAnalysis`). Worst first.
411
+ */
412
+ capability?: TestCapability[];
413
+ /** `testSpecYield` pooled over the lot: counts summed per test. Absent unless every wafer has it. */
414
+ testSpecYield?: NonNullable<StatsSummary['stats']['testSpecYield']>;
415
+ /** `functionalYield` pooled over the lot: counts summed per test. Absent unless every wafer has it. */
416
+ functionalYield?: NonNullable<StatsSummary['stats']['functionalYield']>;
417
+ /** `testFlagYield` pooled over the lot: counts summed per test. Absent unless every wafer has it. */
418
+ testFlagYield?: TestVerdictYield[];
419
+ /** `specVerdictDisagreementDies` summed over the lot. Absent unless every wafer has it. */
420
+ specVerdictDisagreementDies?: number;
421
+ /** Ring and quadrant yield pooled over the lot: counts summed per region, each wafer judged by its own pass bins. Absent unless every wafer has it; `ring` also absent when ring counts differ. */
422
+ regionYield?: RegionYieldFigures;
305
423
  };
306
424
  /** Per-wafer yield as a flat series, ordered by waferIndex. `yieldPercent` is in [0, 100]; null when a wafer had no bin data. */
307
425
  lotYieldSeries: Array<{
@@ -342,35 +460,6 @@ export interface LotStatsSummary {
342
460
  }>;
343
461
  }
344
462
  export interface AnalyzeWaferMapOptions {
345
- /**
346
- * Should mirror the ring count used by the renderer for consistent semantics.
347
- * Defaults to 4, which matches the current render default.
348
- */
349
- ringCount?: number;
350
- passBins?: number[];
351
- /**
352
- * REMOVED in 0.27.0 — `significanceLevel`, `minimumEffectSize` and
353
- * `minimumRelativeEffect` are now internal constants, not options.
354
- *
355
- * They decide what counts as a finding, so a wrong value does not make the
356
- * output *look* different, it makes it wrong — and silently. A negative
357
- * `significanceLevel` returned zero findings across the board, which reads as
358
- * "nothing wrong with this wafer": the worst failure an analysis tool has.
359
- * `minimumSampleSize` was already internal for exactly this reason, and
360
- * `adaptOptions()` declines to adapt `significanceLevel` even internally
361
- * because it perturbs the multiple-comparison correction unpredictably. The
362
- * library was being more careful with itself than with its callers.
363
- *
364
- * The gates are documented in docs/api.md §7.3.2, so a reader can still learn
365
- * *why* a pattern did or did not produce a finding — which is what callers
366
- * actually wanted. Values passed by untyped (plain-JS) callers are now
367
- * validated and clamped rather than honoured; see `resolveOptions`.
368
- */
369
- includePartial?: boolean;
370
- includeEdgeExcluded?: boolean;
371
- enableYieldAnalysis?: boolean;
372
- enableHardBinAnalysis?: boolean;
373
- enableSoftBinAnalysis?: boolean;
374
463
  /**
375
464
  * Full parametric **spatial significance** analysis: Welch comparisons of each
376
465
  * test's values between every region (ring/quadrant/reticle/site/sector) and
@@ -393,22 +482,8 @@ export interface AnalyzeWaferMapOptions {
393
482
  * Off by default. Implied by `enableTestValueAnalysis`.
394
483
  */
395
484
  computePerTestStats?: boolean;
396
- enableReticlePositionAnalysis?: boolean;
397
- /**
398
- * Analyse yield and bin distributions by test site (`siteNum`).
399
- * Enabled automatically when the wafer contains meaningful site duplication
400
- * (at least 2 distinct site numbers each appearing on 3 or more dies).
401
- * Set to `false` to suppress, or `true` to force-enable regardless of the guard.
402
- */
403
- enableTestSiteAnalysis?: boolean;
404
- /** Detect contiguous failure clusters and edge arc damage. Default true. */
405
- enableClusterAnalysis?: boolean;
406
- /** Angular sector analysis (finer-grained than quadrants). Default true. */
407
- enableAngularAnalysis?: boolean;
408
485
  /** Number of angular sectors for sector analysis. Must be 4, 8, 16, or 32. Default 8. */
409
486
  sectorCount?: number;
410
- /** Classify the spatial failure pattern (center, edge-ring, scratch, etc.). Default true. */
411
- enablePatternClassification?: boolean;
412
487
  /**
413
488
  * Restrict test value analysis to a specific subset of test numbers.
414
489
  * When omitted and more than 250 tests are present in the data, test value
@@ -13,10 +13,10 @@ export interface ChartDatum {
13
13
  * `label` — a caller with an `onOpen` callback should resolve back to its
14
14
  * own item by `key`, not by re-searching for `label`. Labels are only
15
15
  * guaranteed unique when the caller supplies one per item; an item with no
16
- * `label` falls back to a shared default elsewhere (e.g. `analysisTab.ts`
17
- * falls back to `''` when both `label` and `wafer.metadata.waferId` are
18
- * absent), so two rows can carry an identical, ambiguous label — matching
19
- * on it would silently resolve to the wrong item. Absent on group rows
16
+ * `label` is named by its wafer ID (`waferDisplayLabel`, core/waferLabel.ts),
17
+ * and two wafers from different lots can share an ID — so two rows can carry
18
+ * an identical, ambiguous label, and matching on it would silently resolve
19
+ * to the wrong item. Absent on group rows
20
20
  * (`itemCount > 1`), which are drilled into rather than opened directly.
21
21
  */
22
22
  key?: string | number;
@@ -24,6 +24,12 @@ export interface ChartDatum {
24
24
  export interface YieldItem {
25
25
  label?: string;
26
26
  dies?: Die[];
27
+ /**
28
+ * This item's own pass bins (`WaferMapResult.passBins`). Wins over the
29
+ * function's `passBins` argument, which only fills in for items without —
30
+ * a lot can mix wafers built with different pass bins.
31
+ */
32
+ passBins?: readonly number[];
27
33
  /**
28
34
  * Precomputed yield percent for this item (e.g. from `analyzeWaferLot`'s
29
35
  * `lotYieldSeries[waferIndex].yieldPercent`) — used directly when present.
@@ -35,7 +41,7 @@ export interface YieldItem {
35
41
  }
36
42
  export type YieldSortBy = 'yield' | 'label';
37
43
  /** One bar per item. */
38
- export declare function buildYieldData(items: YieldItem[], passBins?: number[], sortBy?: YieldSortBy): ChartDatum[];
44
+ export declare function buildYieldData(items: YieldItem[], passBins?: readonly number[], sortBy?: YieldSortBy): ChartDatum[];
39
45
  /**
40
46
  * Combined yield: one bar per group, pooling the group's items. The pooled
41
47
  * yield is the yield-eligible-die-count-weighted mean of per-item yields —
@@ -52,5 +58,5 @@ export declare function buildYieldData(items: YieldItem[], passBins?: number[],
52
58
  export declare function buildYieldDataCombined(groups: {
53
59
  key: string;
54
60
  items: YieldItem[];
55
- }[], passBins?: number[], sortBy?: YieldSortBy): ChartDatum[];
61
+ }[], passBins?: readonly number[], sortBy?: YieldSortBy): ChartDatum[];
56
62
  //# sourceMappingURL=yield.d.ts.map
@@ -1 +1 @@
1
- import{isYieldEligibleDie as c,diePassStatus as p}from"../core/dies.js";import{compareNatural as b}from"../core/utils.js";function m(e,n){let i=0,t=0;const l=new Set(n);for(const r of e){if(!c(r))continue;const o=p(r,l);o!==void 0&&(t++,o&&i++)}return t>0?i/t*100:0}function d(e,n){return e.yieldPercent!==void 0?e.yieldPercent??0:m(e.dies??[],n)}function y(e){let n=0;for(const i of e)c(i)&&n++;return n}function f(e,n){n==="yield"?e.sort((i,t)=>t.value-i.value):e.sort((i,t)=>b(i.label,t.label))}export function buildYieldData(e,n=[1],i="label"){const t=e.map((l,r)=>{const o=d(l,n);return{label:l.label??`#${r}`,value:o,percent:o,itemCount:1,key:l.key}});return f(t,i),t}export function buildYieldDataCombined(e,n=[1],i="label"){const t=e.map(l=>{let r=0,o=0;for(const a of l.items){const s=y(a.dies??[]);r+=d(a,n)*s,o+=s}const u=o>0?r/o:0;return{label:l.key,value:u,percent:u,itemCount:l.items.length}});return f(t,i),t}
1
+ import{isYieldEligibleDie as c,diePassStatus as p}from"../core/dies.js";import{compareNatural as b}from"../core/utils.js";function m(e,n){let i=0,t=0;const l=new Set(n);for(const r of e){if(!c(r))continue;const o=p(r,l);o!==void 0&&(t++,o&&i++)}return t>0?i/t*100:0}function d(e,n){return e.yieldPercent!==void 0?e.yieldPercent??0:m(e.dies??[],e.passBins??n)}function y(e){let n=0;for(const i of e)c(i)&&n++;return n}function f(e,n){n==="yield"?e.sort((i,t)=>t.value-i.value):e.sort((i,t)=>b(i.label,t.label))}export function buildYieldData(e,n=[1],i="label"){const t=e.map((l,r)=>{const o=d(l,n);return{label:l.label??`#${r}`,value:o,percent:o,itemCount:1,key:l.key}});return f(t,i),t}export function buildYieldDataCombined(e,n=[1],i="label"){const t=e.map(l=>{let r=0,o=0;for(const a of l.items){const u=y(a.dies??[]);r+=d(a,n)*u,o+=u}const s=o>0?r/o:0;return{label:l.key,value:s,percent:s,itemCount:l.items.length}});return f(t,i),t}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafertools/wafermap",
3
- "version": "0.29.0",
3
+ "version": "0.30.1",
4
4
  "description": "Interactive wafer map visualization and yield analysis for semiconductor test data. Hard bins, soft bins, test values, retests, edge exclusion, reticle overlays, spatial statistics, failure clustering, and lot-level trend analysis — pure ES modules, no runtime dependencies, no server required.",
5
5
  "type": "module",
6
6
  "license": "MIT",