@wafertools/wafermap 0.30.0 → 0.30.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/AGENTS.md +68 -9
  2. package/CHANGELOG.md +247 -0
  3. package/README.md +3 -3
  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/chartShell.js +1 -1
  9. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  10. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +1 -0
  11. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  15. package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
  16. package/dist/packages/canvas-adapter/chunked.js +1 -0
  17. package/dist/packages/canvas-adapter/deprecated.d.ts +0 -3
  18. package/dist/packages/canvas-adapter/deprecated.js +1 -1
  19. package/dist/packages/canvas-adapter/dieList.js +1 -1
  20. package/dist/packages/canvas-adapter/exportName.d.ts +68 -0
  21. package/dist/packages/canvas-adapter/exportName.js +1 -0
  22. package/dist/packages/canvas-adapter/icons.js +2 -2
  23. package/dist/packages/canvas-adapter/index.d.ts +2 -1
  24. package/dist/packages/canvas-adapter/index.js +1 -1
  25. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  26. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +80 -3
  27. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  28. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +24 -2
  29. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  30. package/dist/packages/canvas-adapter/summaryPanel.d.ts +76 -15
  31. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  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 +15 -8
  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/aggregates.js +1 -1
  41. package/dist/packages/core/deprecated.d.ts +8 -12
  42. package/dist/packages/core/deprecated.js +1 -1
  43. package/dist/packages/core/index.d.ts +1 -1
  44. package/dist/packages/core/index.js +1 -1
  45. package/dist/packages/core/utils.d.ts +67 -0
  46. package/dist/packages/core/utils.js +1 -1
  47. package/dist/packages/core/waferLabel.d.ts +12 -2
  48. package/dist/packages/core/waferLabel.js +1 -1
  49. package/dist/packages/renderer/binColors.d.ts +52 -0
  50. package/dist/packages/renderer/binColors.js +1 -1
  51. package/dist/packages/renderer/buildView.js +1 -1
  52. package/dist/packages/renderer/buildWaferMap.d.ts +30 -5
  53. package/dist/packages/renderer/buildWaferMap.js +1 -1
  54. package/dist/packages/renderer/deprecate.d.ts +7 -0
  55. package/dist/packages/renderer/deprecate.js +1 -1
  56. package/dist/packages/renderer/deprecated.d.ts +2 -2
  57. package/dist/packages/renderer/deprecated.js +1 -1
  58. package/dist/packages/renderer/index.d.ts +2 -1
  59. package/dist/packages/renderer/index.js +1 -1
  60. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  61. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  62. package/dist/packages/stats/capability.d.ts +72 -1
  63. package/dist/packages/stats/capability.js +1 -1
  64. package/dist/packages/stats/correlation.d.ts +30 -0
  65. package/dist/packages/stats/correlation.js +1 -1
  66. package/dist/packages/stats/deprecated.d.ts +30 -35
  67. package/dist/packages/stats/deprecated.js +1 -1
  68. package/dist/packages/stats/findingsNarrative.js +1 -1
  69. package/dist/packages/stats/index.d.ts +4 -2
  70. package/dist/packages/stats/index.js +1 -1
  71. package/dist/packages/stats/math.d.ts +27 -2
  72. package/dist/packages/stats/math.js +1 -1
  73. package/dist/packages/stats/normalizeInput.d.ts +10 -0
  74. package/dist/packages/stats/normalizeInput.js +1 -0
  75. package/dist/packages/stats/regions.d.ts +2 -0
  76. package/dist/packages/stats/regions.js +1 -1
  77. package/dist/packages/stats/renderFindingsReport.js +1 -1
  78. package/dist/packages/stats/renderSummaryReport.d.ts +39 -1
  79. package/dist/packages/stats/renderSummaryReport.js +27 -27
  80. package/dist/packages/stats/reportHtml.d.ts +0 -1
  81. package/dist/packages/stats/reportHtml.js +10 -10
  82. package/dist/packages/stats/summaryFigures.d.ts +47 -0
  83. package/dist/packages/stats/summaryFigures.js +1 -0
  84. package/dist/packages/stats/types.d.ts +118 -0
  85. package/llms.txt +12 -7
  86. package/package.json +2 -2
@@ -1,21 +1,21 @@
1
- import{waferDisplayLabel as ye}from"../core/waferLabel.js";import{binPassSets as Se,binPassSetsByWafer as we}from"../renderer/binColors.js";import{itemPassBins as R,passBinsLabel as $e}from"../core/passBins.js";import{isParametricTest as xe}from"../renderer/buildWaferMap.js";import{buildRingRegions as H,buildQuadrantRegions as I,buildRegionYieldData as ve}from"./regions.js";import{openHtmlReport as Te}from"./renderFindingsReport.js";import{analyzeWaferLot as Fe}from"./analyzeWaferLot.js";import{computeFunctionalYield as Be}from"./analyzeWaferMap.js";import{sortBinsForDisplay as j}from"./binPareto.js";import{buildFacetTable as De,facetValueOf as Pe,FACET_NONE_VALUE as Ye}from"./facets.js";import{visibleFindings as A}from"./filterFindings.js";import{buildYieldDataCombined as Me}from"./yield.js";import{describeWaferPopulation as O,populationLabel as We}from"./population.js";import{buildTestPassRateData as Ne,hasJudgeableTests as Re,poolFunctionalYield as Ce}from"./testPassRate.js";import{buildCapabilityData as Ee}from"./capability.js";import{fmt as D}from"../renderer/fmt.js";import{getDieKey as ke,isPositionedDie as Le,diePassStatus as G}from"../core/dies.js";import{formatFindingDelta as He,formatFindingCoverage as Ie,formatFindingTooltip as je,escHtml as h,renderMetadataSection as z,renderMetricGrid as U,renderSection as x,renderSeverityBadge as Ae,renderTable as F,reportStyles as C}from"./reportHtml.js";function N(a,r){return r===0?"\u2014":`${(a/r*100).toFixed(1)}%`}function Oe(a){if(!a)return"";const r=a.lot??a.lotId,o=a.wafer??a.waferId,t=[r,o].filter(Boolean).map(String);return t.length?` \u2014 ${t.map(h).join(" \xB7 ")}`:""}function V(a,r,o,t,i=[1]){const e=new Map;if(t)for(const[n,c]of Object.entries(t))e.set(Number(n),c);else for(const n of a){if(n.partial||n.edgeExcluded)continue;const c=o==="hard"?n.hbin:n.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const s=[...e.values()].reduce((n,c)=>n+c,0),l=r?new Map(r.map(n=>[n.bin,n])):null,d=j(e.entries(),Se(a,i)[o]).map(([n,c])=>{const f=l?.get(n)?.name??"\u2014";return[String(n),f,String(c),N(c,s)]});return x(o==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",F(["Bin","Name","Count","%"],d,{className:"compact"}))}function _(a,r,o,t){const i=new Set(t),e=new Map(o.map(d=>[ke(d),d]));if(!o.some(d=>d.hbin!=null||d.sbin!=null)||!r.length)return"";const l=[];for(const d of r){let m=0,n=0;for(const c of d.dieKeys){const b=e.get(c);if(!b||b.partial||b.edgeExcluded)continue;const f=G(b,i);f!==void 0&&(n++,f&&m++)}n&&l.push([d.label,String(m),String(n),N(m,n)])}return l.length?x(a,F(["Region","Pass","Total","Yield"],l,{className:"compact"})):""}function K(a,r,o){if(!r.length)return"";const t=a.filter(s=>!s.partial&&!s.edgeExcluded),i=new Map((o??[]).map(s=>[s.testNumber,s])),e=[];for(const s of r.filter(xe)){const l=s.testNumber,d=s.unit||void 0,m=i.get(l);let n,c,b;if(m)({min:n,max:c,mean:b}=m);else{const f=t.map(w=>w.testValues?.[l]).filter(w=>w!==void 0&&isFinite(w));if(!f.length)continue;f.sort((w,y)=>w-y),n=f[0],c=f[f.length-1],b=f.reduce((w,y)=>w+y,0)/f.length}e.push([h(s.name),D(n,d),D(b,d),D(c,d)])}return e.length?x("Test Values",F(["Test","Min","Mean","Max"],e,{className:"compact"})):""}function Q(a,r,o){const t=o??Be(a,r);if(!t?.length)return"";const i=t.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",F(["Test","N","Pass","Fail","Pass Rate"],i,{className:"compact"}))}function q(a,r){const o=Ee(a,r);if(!o.length)return"";const t=e=>e===null?"\u2014":e.toFixed(2),i=o.map(e=>[h(e.label),e.hasSpec?`${D(e.lsl,e.unit)} \u2013 ${D(e.usl,e.unit)}`:"\u2014",D(e.mean,e.unit),t(e.cp),t(e.cpk),t(e.pp),t(e.ppk)]);return x("Process Capability",F(["Test","Spec (LSL\u2013USL)","Mean","Cp","Cpk","Pp","Ppk"],i,{className:"compact"}))}function J(a,r){const o=a.map(i=>`<tr title="${h(je(i))}">
2
- <td class="tight">${Ae(i.severity)}</td>
3
- <td class="tight">${h(i.comparison.left)}</td>
4
- <td>${h(i.variable.label)}</td>
5
- <td class="numeric">${h(He(i))}</td>
6
- <td class="numeric">${h(Ie(i,r))}</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 P}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),P(a,d),P(b,d),P(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?`${P(e.lsl,e.unit)} \u2013 ${P(e.usl,e.unit)}`:"\u2014",P(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">${r!==void 0?"Wafers":"N (region/rest)"}</th>
10
- </tr></thead><tbody>${o}</tbody></table>`}function X(a,r){const o=A(a);return o.length?x("Findings",J(o,r)):""}export function renderSummaryReportHtml(a,r={}){const{wafer:o,dies:t,yieldSummary:i,dataCoverage:e,hbinDefs:s,sbinDefs:l,testDefs:d=[],statsSummary:m,passBins:n=[1],ringCount:c=4}=a,b=o.metadata,f=Oe(b),w=r.title??`Wafer Summary${f}`,y=new Date().toLocaleString(),u=t.some(Y=>Y.hbin!=null),g=t.some(Y=>Y.sbin!=null),S=t.filter(Le),v=H(S,o,c),$=I(S,o,c),T=[{label:"Total dies",value:String(i.totalDies)},{label:"Filled dies",value:String(e.filledDies),hint:`${N(e.filledDies,e.totalDies)} fill`},{label:"Pass dies",value:String(i.passDies)},{label:"Fail dies",value:String(i.failDies)},...i.partialDies>0?[{label:"Partial",value:String(i.partialDies)}]:[],...i.yieldPercent!==null?[{label:`Yield (pass: ${$e([n])})`,value:`${i.yieldPercent.toFixed(1)}%`}]:[],...i.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(i.edgeExcludedDies)}]:[]],P=[z([{metadata:o.metadata}]),x("Summary",[U(T)].filter(Boolean).join(`
11
- `)),u?V(t,s,"hard",m?.stats.hardBinCounts,n):g?V(t,l,"soft",m?.stats.softBinCounts,n):"",_("Ring Yield",v,t,n),_("Quadrant Yield",$,t,n),K(t,d,m?.stats.perTestStats),Q(t,d,m?.stats.functionalYield),q([{dies:t}],d),m?X(m.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(W=>W.hbin!=null),g=n.some(W=>W.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)}]:[]],Y=[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
16
  <title>${h(w)}</title>
17
17
  <style>
18
- ${C()}
18
+ ${N()}
19
19
  </style>
20
20
  </head>
21
21
  <body>
@@ -24,34 +24,34 @@ ${C()}
24
24
  <h1>${h(w)}</h1>
25
25
  <p class="report-subtitle">Generated ${h(y)}</p>
26
26
  </header>
27
- ${P}
27
+ ${Y}
28
28
  <p class="footer">Generated ${h(y)}</p>
29
29
  </main>
30
30
  </body>
31
- </html>`}function Ge(a,r){const o=a.perWafer.map(t=>{const i=ye(r[t.waferIndex],t.waferIndex),e=t.summary.stats.yieldPercent;return[i,e!==null?`${e.toFixed(1)}%`:"N/A"]});return F(["Wafer","Yield"],o,{className:"compact"})}function ze(a){const r=[];for(const e of a.perWafer){const s=A(e.summary.findings);if(!s.length)continue;const l=e.summary.wafer?.waferId??`Wafer ${e.waferIndex+1}`;r.push(`<h3 class="report-subheading">${h(l)}</h3>`+J(s))}if(!r.length)return"";const o=r.length,t=a.perWafer.length,i=`<p class="report-note">${o} of ${t} wafer${t===1?"":"s"} have per-wafer findings; the rest had none.</p>`;return x("Findings by Wafer",i+r.join(`
32
- `))}const Ue=3;function Ve(a,r,o){const t=a.map(d=>({metadata:d.wafer?.metadata})),i=De(t,{facetableOnly:!0}).filter(d=>d.splittable);if(!i.length)return"";const e=i.slice(0,Ue),s=[];for(const d of e){const m=new Map;a.forEach((u,g)=>{const S=Pe(u.wafer?.metadata,d.key)??Ye;(m.get(S)??m.set(S,[]).get(S)).push(g)});const n=[...m.entries()].map(([u,g])=>({key:u,items:g.map(S=>a[S])}));if(n.length<2)continue;const c=n.map(u=>[h(u.key),h(u.items.map(g=>g.label).join(", "))]),b=Me(n.map(u=>({key:u.key,items:u.items.map(g=>({label:g.label,dies:g.dies,passBins:R(g,o)}))})),o).map(u=>[h(u.label),String(u.itemCount),`${u.percent.toFixed(1)}%`]),f=[F([d.label,"Wafers"],c,{className:"compact"}),F([d.label,"Wafers","Yield"],b,{className:"compact"})],w=a.flatMap(u=>u.dies??[]);let y=0;for(const u of["spec","testFlag","functional"]){if(!Re(r,u,w))continue;const g=Ne(n.map($=>({key:$.key,items:$.items.map(T=>({dies:T.dies,testSpecYield:T.statsSummary?.stats.testSpecYield,functionalYield:T.statsSummary?.stats.functionalYield}))})),r,u);if(!g.rows.length)continue;const S=g.rows.map($=>[h($.label),...$.byGroup.map(T=>T.passRatePercent===null?"\u2014":`${T.passRatePercent.toFixed(1)}%`),$.overall.passRatePercent===null?"\u2014":`${$.overall.passRatePercent.toFixed(1)}%`]),v=u==="spec"?"Pass rate \xB7 spec limits":u==="testFlag"?"Pass rate \xB7 tester flag":"Pass rate \xB7 functional";g.disagreementDies&&(y=g.disagreementDies),f.push(`<h4 class="report-subheading">${h(v)}</h4>`+F(["Test",...g.groups,"All"],S,{className:"compact"}))}y&&f.push(`<p class="report-note">${y.toLocaleString()} dies judged differently by limits and tester flag.</p>`),s.push(`<h3 class="report-subheading">${h(d.label)}</h3>${f.join(`
33
- `)}`)}if(!s.length)return"";const l=i.length>e.length?`<p class="report-note">${i.length-e.length} further splittable field(s) not shown.</p>`:"";return x("Split Comparison",s.join(`
34
- `)+l)}function Z(a,r,o,t,i){const e=new Map,s=o==="hard"?"hardBinCounts":"softBinCounts";if(t?.length&&t.every(n=>n.stats[s]!==void 0))for(const n of t)for(const[c,b]of Object.entries(n.stats[s])){const f=Number(c);e.set(f,(e.get(f)??0)+b)}else for(const n of a){if(n.partial||n.edgeExcluded)continue;const c=o==="hard"?n.hbin:n.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const l=[...e.values()].reduce((n,c)=>n+c,0),d=r?new Map(r.map(n=>[n.bin,n])):null,m=j(e.entries(),we(i)[o]).map(([n,c])=>{const b=d?.get(n);return[b?.name?`Bin ${n} \xB7 ${b.name} (${c})`:`Bin ${n} (${c})`,N(c,l)]});return x(o==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",F(["Bin","Yield"],m,{className:"compact"}))}function ee(a,r,o,t,i,e){const s=ve(o,t,i,l=>e[l],r).map(l=>[l.label,`${l.yieldPercent.toFixed(1)}%`]);return s.length?x(a,F(["Region","Yield"],s,{className:"compact"})):""}function _e(a,r,o){let t;if(o?.length&&o.every(i=>i.stats.perTestStats!==void 0)){const i=new Map;for(const e of o)for(const s of e.stats.perTestStats??[]){const l=i.get(s.testNumber);l?(l.n+=s.count,l.sum+=s.mean*s.count,l.min=Math.min(l.min,s.min),l.max=Math.max(l.max,s.max)):i.set(s.testNumber,{n:s.count,sum:s.mean*s.count,min:s.min,max:s.max})}t=[...i.entries()].map(([e,s])=>({testNumber:e,min:s.min,max:s.max,mean:s.sum/s.n}))}return K(a,r,t)}function Ke(a,r,o){const t=Ce(o);return Q(a,r,t)}const E=["lot","product","testProgram","temperature"];function Qe(a){const r=new Map(E.map(e=>[e,new Set]));for(const e of a)for(const s of E){const l=e.wafer?.metadata?.[s];l!=null&&l!==""&&r.get(s).add(String(l))}const o=E.filter(e=>r.get(e).size>1);if(o.length===0)return[{label:"",items:a,varying:o}];const t=new Map,i=[];for(const e of a){const s=o.map(l=>String(e.wafer?.metadata?.[l]??"(none)")).join(" \xB7 ");t.has(s)||(t.set(s,[]),i.push(s)),t.get(s).push(e)}return i.map(e=>({label:e,items:t.get(e),varying:o}))}function te(a,r,o,t,i,e,s){const l=Fe(a,{perWaferSummaries:a.map(p=>p.statsSummary),...s}),d=[],m=[],n=[],c=[];for(const p of a){const B=p.dies??[];p.wafer&&(d.push(p.wafer),n.push(B),c.push(R(p,i))),B.length&&m.push(...B)}const b=a.map(p=>({dies:p.dies??[],passBins:R(p,i)})),f=m.some(p=>p.hbin!=null),w=m.some(p=>p.sbin!=null),y=f||w,u=l.perWafer.map(p=>p.summary.stats.yieldPercent).filter(p=>p!==null),g=u.length?u.reduce((p,B)=>p+B,0)/u.length:null;let S=0,v=0,$=0,T=0,P=0;for(const p of b){const B=new Set(p.passBins);for(const M of p.dies){if(S++,M.edgeExcluded&&T++,M.partial&&P++,M.partial||M.edgeExcluded)continue;const L=G(M,B);L!==void 0&&(v++,L&&$++)}}const Y=v-$,k=v>0?$/v*100:null,ae=[{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)}]:[],...T>0?[{label:"Edge excluded",value:String(T)}]:[],...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)}%`}]:[]],se=O(l.perWafer.map(p=>p.summary.wafer)),oe=x(se.lotId!==void 0?"Lot Summary":"Summary",U(ae)),ie=z(a.map(p=>({metadata:p.wafer?.metadata}))),re=x("Per-Wafer Yield",Ge(l,a)),le=Ve(a,t,i),W=l.perWafer.map(p=>p.summary),ce=y?f?Z(m,r,"hard",W,b):Z(m,o,"soft",W,b):"",de=y&&d.length?ee("Ring Yield (All Wafers)",H,n,d,e,c):"",ue=y&&d.length?ee("Quadrant Yield (All Wafers)",I,n,d,e,c):"",me=t.length?_e(m,t,W):"",pe=t.length?Ke(m,t,W):"",fe=t.length?q(a,t):"",ge=l.findings.length?X(l.findings,l.stats.waferCount):"",he=ze(l),be=[oe,ie,re,le,ce,de,ue,me,pe,fe,ge,he].filter(Boolean).join(`
35
- `);return{lotSummary:l,sections:be}}function ne(a,r,o){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 F=m.dies??[];m.wafer&&(d.push(m.wafer),a.push(F),c.push(R(m,r)));for(const T of F)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,F)=>m+F,0)/u.length:null;let S=0,D=0,$=0,v=0,Y=0;for(const m of b){const F=new Set(m.passBins);for(const T of m.dies){if(S++,T.edgeExcluded&&v++,T.partial&&Y++,T.partial||T.edgeExcluded)continue;const H=U(T,F);H!==void 0&&(D++,H&&$++)}}const W=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(W)}]:[],...v>0?[{label:"Edge excluded",value:String(v)}]:[],...Y>0?[{label:"Partial dies",value:String(Y)}]:[],...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>${h(a)}</h1>
38
- <p class="report-subtitle">Generated ${h(o)}</p>
37
+ <h1>${h(t)}</h1>
38
+ <p class="report-subtitle">Generated ${h(s)}</p>
39
39
  </header>
40
- ${r}
41
- <p class="footer">Generated ${h(o)}</p>
42
- </main>`}export function renderLotSummaryReportHtml(a,r={}){const{items:o,hbinDefs:t,sbinDefs:i,testDefs:e=[],passBins:s=[1],ringCount:l=4,analyzeOptions:d}=a,m=new Date().toLocaleString(),n=Qe(o);if(n.length===1){const{lotSummary:y,sections:u}=te(n[0].items,t,i,e,s,l,d),g=O(y.perWafer.map(v=>v.summary.wafer)),S=r.title??(g.lotId!==void 0?`Lot Summary \u2014 ${g.lotId}`:`Summary \u2014 ${We(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
46
  <title>${h(S)}</title>
47
47
  <style>
48
- ${C()}
48
+ ${N()}
49
49
  </style>
50
50
  </head>
51
51
  <body>
52
- ${ne(S,u,m)}
52
+ ${ae(S,u,f)}
53
53
  </body>
54
- </html>`}const c=r.title??"Summary",b=n.map(y=>{const{lotSummary:u,sections:g}=te(y.items,t,i,e,s,l,d),S=y.varying.includes("lot")?void 0:u.lot?.lot??u.lot?.lotId,v=S?`${y.label} \xB7 Lot ${String(S)}`:y.label;return ne(`${c} \u2014 ${v}`,g,m)}),f=`<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 ${n.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);">'}
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
55
  ${y}`).join(`
56
56
  `);return`<!DOCTYPE html>
57
57
  <html lang="en">
@@ -59,13 +59,13 @@ ${y}`).join(`
59
59
  <meta charset="UTF-8">
60
60
  <title>${h(c)}</title>
61
61
  <style>
62
- ${C()}
62
+ ${N()}
63
63
  </style>
64
64
  </head>
65
65
  <body>
66
66
  <div class="report" style="padding-top:24px;">
67
- ${f}
67
+ ${p}
68
68
  </div>
69
69
  ${w}
70
70
  </body>
71
- </html>`}export{Te 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<{
package/llms.txt CHANGED
@@ -1,7 +1,10 @@
1
1
  # wafermap
2
2
 
3
3
  Browser-first wafer map visualization toolkit for semiconductor test data.
4
- Primary entry point: build wafer models with `buildWaferMap()`, then render with a canvas.
4
+ Primary path: `buildWaferMap()` to build the map once, then `renderWaferMap()` (one wafer)
5
+ or `renderWaferGallery()` (several) to render it, and `analyzeWaferMap()` for statistics
6
+ and findings. The renderers carry their own toolbar, summary panel, Insights charts and
7
+ reports — the low-level pipeline below them is deprecated and removed in 0.31.0.
5
8
 
6
9
  BEFORE INTEGRATING: tsmap (https://github.com/wafertools/tsmap) is a finished, free,
7
10
  MIT-licensed desktop and browser application built on this library — it opens STDF,
@@ -20,11 +23,11 @@ where repo-relative paths do not resolve.
20
23
  - [Examples](https://wafertools.github.io/wafermap/examples/) - 20 runnable demo pages, also downloadable at https://wafertools.github.io/wafermap/wafermap-examples.zip to run offline.
21
24
 
22
25
  ## Public package entry points
23
- - `@wafertools/wafermap`
24
- - `@wafertools/wafermap/render`
25
- - `@wafertools/wafermap/stats`
26
- - `@wafertools/wafermap/worker`
27
- - `@wafertools/wafermap/worker-script`
26
+ - `@wafertools/wafermap` - `buildWaferMap()`, geometry, colour-scheme registries. Pure, no DOM.
27
+ - `@wafertools/wafermap/render` - `renderWaferMap()`, `renderWaferGallery()`. Needs the DOM.
28
+ - `@wafertools/wafermap/stats` - `analyzeWaferMap()`, `analyzeWaferLot()`, HTML reports. Pure.
29
+ - `@wafertools/wafermap/worker` - `createWafermapWorker()` for off-main-thread builds.
30
+ - `@wafertools/wafermap/worker-script` - the worker script itself.
28
31
 
29
32
  ## Important notes
30
33
  - `x` and `y` in `DieResult` are die grid positions (prober step integers), not millimetres.
@@ -33,7 +36,9 @@ where repo-relative paths do not resolve.
33
36
  - All coordinates shown to the user (axis ticks, tooltips) must be original die grid positions (`die.x` / `die.y`), not post-transform display coordinates.
34
37
  - `buildWaferMap()` is pure and server-safe; renderers require the DOM.
35
38
  - Recommended path for most users: `buildWaferMap()` + `renderWaferMap()`.
36
- - `passBins` sets both the yield number and the wording of its label — do not assume `[1]`.
39
+ - Bins, test values and verdicts must be numbers/booleans. A parser that leaves `hbin` as `"1"` builds a map where those dies count as fails — convert with `Number()` at parse time.
40
+ - `passBins` and `ringCount` are set once on `buildWaferMap()` and travel on the result. Neither is an option on `analyzeWaferMap`, `analyzeWaferLot`, `renderWaferMap` or `renderWaferGallery`. `passBins` sets both the yield number and the wording of its label — do not assume `[1]`.
41
+ - Statistics surfaces are built in: pass `statsSummary` for the Summary panel and `insights: { enabled: true }` for the chart suite. Do not rebuild those charts from the deprecated chart-data builders.
37
42
  - `activeTest` takes a test number (e.g. `1050`), not a positional index.
38
43
  - Functional tests (`testType: 'F'`) have no value; read verdicts only via `getTestPassStatus()`. A missing verdict is no-data, never a fail.
39
44
  - `PlotMode` values are camelCase (`'hardBin'`, `'stackedValues'`), never snake_case.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafertools/wafermap",
3
- "version": "0.30.0",
3
+ "version": "0.30.2",
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",
@@ -75,7 +75,7 @@
75
75
  "build": "node scripts/build-user-guide.mjs && node scripts/sync-icons.mjs && node scripts/sync-version.mjs && rm -rf dist && tsc -p tsconfig.build.json",
76
76
  "build:guide": "node scripts/build-user-guide.mjs",
77
77
  "clean": "rm -rf dist",
78
- "check": "node scripts/build-user-guide.mjs && node scripts/sync-version.mjs && tsc --noEmit -p tsconfig.json && node scripts/check-examples-manifest.mjs && node scripts/build-agents-page.mjs --check && node scripts/check-agents-guide.mjs && node scripts/check-toolbar-docs.mjs && node scripts/check-doc-links.mjs && node scripts/check-quickstart-snippet.mjs && node scripts/check-overlay-conventions.mjs && node scripts/check-changelog.mjs && node scripts/help.mjs --check && node scripts/check-api-claims.mjs && node scripts/check-style-scales.mjs packages/canvas-adapter && node scripts/check-clones.mjs packages",
78
+ "check": "node scripts/build-user-guide.mjs && node scripts/sync-version.mjs && tsc --noEmit -p tsconfig.json && node scripts/check-examples-manifest.mjs && node scripts/build-agents-page.mjs --check && node scripts/check-agents-guide.mjs && node scripts/check-toolbar-docs.mjs && node scripts/check-doc-links.mjs && node scripts/check-quickstart-snippet.mjs && node scripts/check-overlay-conventions.mjs && node scripts/check-changelog.mjs && node scripts/help.mjs --check && node scripts/check-api-claims.mjs && node scripts/check-style-scales.mjs packages/canvas-adapter && node scripts/check-spread-limits.mjs && node scripts/check-clones.mjs packages",
79
79
  "build:examples-index": "node scripts/build-examples-index.mjs",
80
80
  "check:drift": "node scripts/check-drift.mjs",
81
81
  "dev": "rm -rf docs/dist && cp -r dist docs/dist && ./.venv/bin/zensical serve --dev-addr localhost:8001",