@wafertools/wafermap 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +181 -0
  2. package/CHANGELOG.md +261 -1
  3. package/README.md +9 -1
  4. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  5. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  6. package/dist/packages/canvas-adapter/dieList.d.ts +41 -0
  7. package/dist/packages/canvas-adapter/dieList.js +2 -0
  8. package/dist/packages/canvas-adapter/icons.js +1 -1
  9. package/dist/packages/canvas-adapter/index.d.ts +4 -0
  10. package/dist/packages/canvas-adapter/index.js +1 -1
  11. package/dist/packages/canvas-adapter/maplessSummary.d.ts +34 -0
  12. package/dist/packages/canvas-adapter/maplessSummary.js +1 -0
  13. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +9 -11
  14. package/dist/packages/canvas-adapter/renderWaferGallery.js +8 -8
  15. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +31 -1
  16. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  17. package/dist/packages/canvas-adapter/summaryPanel.d.ts +13 -1
  18. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  19. package/dist/packages/canvas-adapter/toolbar.d.ts +4 -1
  20. package/dist/packages/canvas-adapter/toolbar.js +1 -1
  21. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  22. package/dist/packages/canvas-adapter/userGuideHtml.js +59 -22
  23. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  24. package/dist/packages/canvas-adapter/version.js +1 -1
  25. package/dist/packages/canvas-adapter/warnings.d.ts +57 -0
  26. package/dist/packages/canvas-adapter/warnings.js +1 -0
  27. package/dist/packages/core/classify.d.ts +6 -2
  28. package/dist/packages/core/dies.d.ts +67 -7
  29. package/dist/packages/core/dies.js +1 -1
  30. package/dist/packages/core/probe.d.ts +5 -0
  31. package/dist/packages/core/probe.js +1 -1
  32. package/dist/packages/core/transforms.d.ts +4 -4
  33. package/dist/packages/core/transforms.js +1 -1
  34. package/dist/packages/renderer/buildView.d.ts +3 -3
  35. package/dist/packages/renderer/buildView.js +1 -1
  36. package/dist/packages/renderer/buildWaferMap.d.ts +40 -9
  37. package/dist/packages/renderer/buildWaferMap.js +1 -1
  38. package/dist/packages/renderer/index.d.ts +1 -1
  39. package/dist/packages/renderer/index.js +1 -1
  40. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  41. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  42. package/dist/packages/stats/clusterDetection.d.ts +2 -2
  43. package/dist/packages/stats/patternClassification.d.ts +2 -2
  44. package/dist/packages/stats/regions.d.ts +6 -6
  45. package/dist/packages/stats/regions.js +1 -1
  46. package/dist/packages/stats/renderFindingsReport.js +15 -15
  47. package/dist/packages/stats/renderSummaryReport.js +17 -17
  48. package/dist/packages/stats/types.d.ts +34 -4
  49. package/llms.txt +39 -0
  50. package/package.json +12 -8
@@ -1,21 +1,21 @@
1
- import{isParametricTest as rt}from"../renderer/buildWaferMap.js";import{buildRingRegions as H,buildQuadrantRegions as L}from"./regions.js";import{openHtmlReport as ct}from"./renderFindingsReport.js";import{analyzeWaferLot as dt}from"./analyzeWaferLot.js";import{computeFunctionalYield as ut}from"./analyzeWaferMap.js";import{buildCapabilityData as ft}from"./capability.js";import{fmt as B}from"../renderer/fmt.js";import{getDieKey as W}from"../core/dies.js";import{formatFindingDelta as mt,formatFindingCoverage as pt,formatFindingTooltip as gt,escHtml as h,renderMetadataSection as k,renderMetricGrid as N,renderSection as $,renderSeverityBadge as ht,renderTable as D,reportStyles as E}from"./reportHtml.js";function P(o,r){return r===0?"\u2014":`${(o/r*100).toFixed(1)}%`}function bt(o){if(!o)return"";const r=o.lot??o.lotId,i=o.wafer??o.waferId,e=[r,i].filter(Boolean).map(String);return e.length?` \u2014 ${e.map(h).join(" \xB7 ")}`:""}function Et(o,r){const i=[{label:"Total dies",value:String(r.totalDies)},{label:"Filled dies",value:String(r.filledDies),hint:`${P(r.filledDies,r.totalDies)} fill`},{label:"Pass dies",value:String(o.passDies)},{label:"Fail dies",value:String(o.failDies)},...o.edgeExcludedDies>0?[{label:"Edge excluded",value:String(o.edgeExcludedDies)}]:[],...o.partialDies>0?[{label:"Partial dies",value:String(o.partialDies)}]:[],{label:"Yield",value:o.yieldPercent!==null?`${o.yieldPercent.toFixed(1)}%`:"N/A"}];return $("Yield",N(i))}function I(o,r,i,e){const l=new Map;if(e)for(const[n,s]of Object.entries(e))l.set(Number(n),s);else for(const n of o){if(n.partial||n.edgeExcluded)continue;const s=i==="hard"?n.hbin:n.sbin;s!=null&&l.set(s,(l.get(s)??0)+1)}if(!l.size)return"";const t=[...l.values()].reduce((n,s)=>n+s,0),a=r?new Map(r.map(n=>[n.bin,n])):null,c=[...l.entries()].sort((n,s)=>n[0]-s[0]).map(([n,s])=>{const p=a?.get(n)?.name??"\u2014";return[String(n),p,String(s),P(s,t)]});return $(i==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",D(["Bin","Name","Count","%"],c,{className:"compact"}))}function j(o,r,i,e){const l=new Set(e),t=new Map(i.map(d=>[W(d),d]));if(!i.some(d=>d.hbin!=null||d.sbin!=null)||!r.length)return"";const c=[];for(const d of r){let n=0,s=0;for(const m of d.dieKeys){const p=t.get(m);if(!p||p.partial||p.edgeExcluded)continue;const g=p.hbin??p.sbin;g!=null&&(s++,l.has(g)&&n++)}s&&c.push([d.label,String(n),String(s),P(n,s)])}return c.length?$(o,D(["Region","Pass","Total","Yield"],c,{className:"compact"})):""}function z(o,r,i){if(!r.length)return"";const e=o.filter(a=>!a.partial&&!a.edgeExcluded),l=new Map((i??[]).map(a=>[a.testNumber,a])),t=[];for(const a of r.filter(rt)){const c=a.testNumber,d=a.unit||void 0,n=l.get(c);let s,m,p;if(n)({min:s,max:m,mean:p}=n);else{const g=e.map(b=>b.testValues?.[c]).filter(b=>b!==void 0&&isFinite(b));if(!g.length)continue;g.sort((b,u)=>b-u),s=g[0],m=g[g.length-1],p=g.reduce((b,u)=>b+u,0)/g.length}t.push([h(a.name),B(s,d),B(p,d),B(m,d)])}return t.length?$("Test Values",D(["Test","Min","Mean","Max"],t,{className:"compact"})):""}function A(o,r,i){const e=i??ut(o,r);if(!e?.length)return"";const l=e.map(t=>[h(t.label),String(t.totalDies),String(t.passDies),String(t.failDies),t.passRatePercent!==null?`${t.passRatePercent.toFixed(1)}%`:"\u2014"]);return $("Functional Tests",D(["Test","N","Pass","Fail","Pass Rate"],l,{className:"compact"}))}function G(o,r){const i=ft(o,r);if(!i.length)return"";const e=t=>t===null?"\u2014":t.toFixed(2),l=i.map(t=>[h(t.label),t.hasSpec?`${B(t.lsl,t.unit)} \u2013 ${B(t.usl,t.unit)}`:"\u2014",B(t.mean,t.unit),e(t.cp),e(t.cpk),e(t.pp),e(t.ppk)]);return $("Process Capability",D(["Test","Spec (LSL\u2013USL)","Mean","Cp","Cpk","Pp","Ppk"],l,{className:"compact"}))}function O(o,r){if(!o.length)return"";const i=o.map(t=>`<tr title="${h(gt(t))}">
2
- <td class="tight">${ht(t.severity)}</td>
1
+ import{isParametricTest as ct}from"../renderer/buildWaferMap.js";import{buildRingRegions as H,buildQuadrantRegions as L}from"./regions.js";import{openHtmlReport as dt}from"./renderFindingsReport.js";import{analyzeWaferLot as ut}from"./analyzeWaferLot.js";import{computeFunctionalYield as ft}from"./analyzeWaferMap.js";import{buildCapabilityData as mt}from"./capability.js";import{fmt as M}from"../renderer/fmt.js";import{getDieKey as W,isPositionedDie as k}from"../core/dies.js";import{formatFindingDelta as pt,formatFindingCoverage as gt,formatFindingTooltip as ht,escHtml as h,renderMetadataSection as I,renderMetricGrid as E,renderSection as $,renderSeverityBadge as bt,renderTable as D,reportStyles as R}from"./reportHtml.js";function Y(a,r){return r===0?"\u2014":`${(a/r*100).toFixed(1)}%`}function St(a){if(!a)return"";const r=a.lot??a.lotId,i=a.wafer??a.waferId,n=[r,i].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(h).join(" \xB7 ")}`:""}function Rt(a,r){const i=[{label:"Total dies",value:String(a.totalDies)},{label:"Filled dies",value:String(r.filledDies),hint:`${Y(r.filledDies,r.totalDies)} fill`},{label:"Pass dies",value:String(a.passDies)},{label:"Fail dies",value:String(a.failDies)},...a.edgeExcludedDies>0?[{label:"Edge excluded",value:String(a.edgeExcludedDies)}]:[],...a.partialDies>0?[{label:"Partial dies",value:String(a.partialDies)}]:[],{label:"Yield",value:a.yieldPercent!==null?`${a.yieldPercent.toFixed(1)}%`:"N/A"}];return $("Yield",E(i))}function j(a,r,i,n){const l=new Map;if(n)for(const[e,o]of Object.entries(n))l.set(Number(e),o);else for(const e of a){if(e.partial||e.edgeExcluded)continue;const o=i==="hard"?e.hbin:e.sbin;o!=null&&l.set(o,(l.get(o)??0)+1)}if(!l.size)return"";const t=[...l.values()].reduce((e,o)=>e+o,0),s=r?new Map(r.map(e=>[e.bin,e])):null,c=[...l.entries()].sort((e,o)=>e[0]-o[0]).map(([e,o])=>{const p=s?.get(e)?.name??"\u2014";return[String(e),p,String(o),Y(o,t)]});return $(i==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",D(["Bin","Name","Count","%"],c,{className:"compact"}))}function z(a,r,i,n){const l=new Set(n),t=new Map(i.map(d=>[W(d),d]));if(!i.some(d=>d.hbin!=null||d.sbin!=null)||!r.length)return"";const c=[];for(const d of r){let e=0,o=0;for(const m of d.dieKeys){const p=t.get(m);if(!p||p.partial||p.edgeExcluded)continue;const g=p.hbin??p.sbin;g!=null&&(o++,l.has(g)&&e++)}o&&c.push([d.label,String(e),String(o),Y(e,o)])}return c.length?$(a,D(["Region","Pass","Total","Yield"],c,{className:"compact"})):""}function A(a,r,i){if(!r.length)return"";const n=a.filter(s=>!s.partial&&!s.edgeExcluded),l=new Map((i??[]).map(s=>[s.testNumber,s])),t=[];for(const s of r.filter(ct)){const c=s.testNumber,d=s.unit||void 0,e=l.get(c);let o,m,p;if(e)({min:o,max:m,mean:p}=e);else{const g=n.map(b=>b.testValues?.[c]).filter(b=>b!==void 0&&isFinite(b));if(!g.length)continue;g.sort((b,u)=>b-u),o=g[0],m=g[g.length-1],p=g.reduce((b,u)=>b+u,0)/g.length}t.push([h(s.name),M(o,d),M(p,d),M(m,d)])}return t.length?$("Test Values",D(["Test","Min","Mean","Max"],t,{className:"compact"})):""}function G(a,r,i){const n=i??ft(a,r);if(!n?.length)return"";const l=n.map(t=>[h(t.label),String(t.totalDies),String(t.passDies),String(t.failDies),t.passRatePercent!==null?`${t.passRatePercent.toFixed(1)}%`:"\u2014"]);return $("Functional Tests",D(["Test","N","Pass","Fail","Pass Rate"],l,{className:"compact"}))}function O(a,r){const i=mt(a,r);if(!i.length)return"";const n=t=>t===null?"\u2014":t.toFixed(2),l=i.map(t=>[h(t.label),t.hasSpec?`${M(t.lsl,t.unit)} \u2013 ${M(t.usl,t.unit)}`:"\u2014",M(t.mean,t.unit),n(t.cp),n(t.cpk),n(t.pp),n(t.ppk)]);return $("Process Capability",D(["Test","Spec (LSL\u2013USL)","Mean","Cp","Cpk","Pp","Ppk"],l,{className:"compact"}))}function K(a,r){if(!a.length)return"";const i=a.map(t=>`<tr title="${h(ht(t))}">
2
+ <td class="tight">${bt(t.severity)}</td>
3
3
  <td class="tight">${h(t.comparison.left)}</td>
4
4
  <td>${h(t.variable.label)}</td>
5
- <td class="numeric">${h(mt(t))}</td>
6
- <td class="numeric">${h(pt(t,r))}</td>
5
+ <td class="numeric">${h(pt(t))}</td>
6
+ <td class="numeric">${h(gt(t,r))}</td>
7
7
  </tr>`).join(`
8
8
  `),l=`<table class="report-table findings-table compact"><thead><tr>
9
9
  <th>Severity</th><th>Region</th><th>Metric</th><th class="numeric">Delta</th><th class="numeric">${r!==void 0?"Wafers":"N (region/rest)"}</th>
10
- </tr></thead><tbody>${i}</tbody></table>`;return $("Findings",l)}export function renderSummaryReportHtml(o,r={}){const{wafer:i,dies:e,yieldSummary:l,dataCoverage:t,hbinDefs:a,sbinDefs:c,testDefs:d=[],statsSummary:n,passBins:s=[1],ringCount:m=4}=o,p=i.metadata,g=bt(p),b=r.title??`Wafer Summary${g}`,u=new Date().toLocaleString(),w=e.some(Y=>Y.hbin!=null),x=e.some(Y=>Y.sbin!=null),S=H(e,i,m),y=L(e,i,m),v=[{label:"Total dies",value:String(t.totalDies)},...l.partialDies>0?[{label:"Partial",value:String(l.partialDies)}]:[],...l.yieldPercent!==null?[{label:`Yield (pass: ${s.length===1?`bin ${s[0]}`:`bins ${s.join(", ")}`})`,value:`${l.yieldPercent.toFixed(1)}%`}]:[],...l.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(l.edgeExcludedDies)}]:[]],M=[k([{metadata:i.metadata}]),$("Summary",[N(v)].filter(Boolean).join(`
11
- `)),w?I(e,a,"hard",n?.stats.hardBinCounts):x?I(e,c,"soft",n?.stats.softBinCounts):"",j("Ring Yield",S,e,s),j("Quadrant Yield",y,e,s),z(e,d,n?.stats.perTestStats),A(e,d,n?.stats.functionalYield),G([{dies:e}],d),n?O(n.findings):""].filter(Boolean).join(`
10
+ </tr></thead><tbody>${i}</tbody></table>`;return $("Findings",l)}export function renderSummaryReportHtml(a,r={}){const{wafer:i,dies:n,yieldSummary:l,dataCoverage:t,hbinDefs:s,sbinDefs:c,testDefs:d=[],statsSummary:e,passBins:o=[1],ringCount:m=4}=a,p=i.metadata,g=St(p),b=r.title??`Wafer Summary${g}`,u=new Date().toLocaleString(),w=n.some(B=>B.hbin!=null),x=n.some(B=>B.sbin!=null),S=n.filter(k),y=H(S,i,m),v=L(S,i,m),P=[{label:"Total dies",value:String(l.totalDies)},...l.partialDies>0?[{label:"Partial",value:String(l.partialDies)}]:[],...l.yieldPercent!==null?[{label:`Yield (pass: ${o.length===1?`bin ${o[0]}`:`bins ${o.join(", ")}`})`,value:`${l.yieldPercent.toFixed(1)}%`}]:[],...l.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(l.edgeExcludedDies)}]:[]],N=[I([{metadata:i.metadata}]),$("Summary",[E(P)].filter(Boolean).join(`
11
+ `)),w?j(n,s,"hard",e?.stats.hardBinCounts):x?j(n,c,"soft",e?.stats.softBinCounts):"",z("Ring Yield",y,n,o),z("Quadrant Yield",v,n,o),A(n,d,e?.stats.perTestStats),G(n,d,e?.stats.functionalYield),O([{dies:n}],d),e?K(e.findings):""].filter(Boolean).join(`
12
12
  `);return`<!DOCTYPE html>
13
13
  <html lang="en">
14
14
  <head>
15
15
  <meta charset="UTF-8">
16
16
  <title>${h(b)}</title>
17
17
  <style>
18
- ${E()}
18
+ ${R()}
19
19
  </style>
20
20
  </head>
21
21
  <body>
@@ -24,31 +24,31 @@ ${E()}
24
24
  <h1>${h(b)}</h1>
25
25
  <p class="report-subtitle">Generated ${h(u)}</p>
26
26
  </header>
27
- ${M}
27
+ ${N}
28
28
  <p class="footer">Generated ${h(u)}</p>
29
29
  </main>
30
30
  </body>
31
- </html>`}function St(o,r){const i=o.perWafer.map(e=>{const l=r[e.waferIndex]?.label??`W${e.waferIndex+1}`,t=e.summary.stats.yieldPercent;return[l,t!==null?`${t.toFixed(1)}%`:"N/A"]});return D(["Wafer","Yield"],i,{className:"compact"})}function yt(o){const r=o.map(i=>[i.label,i.wafer?.metadata?.split]).filter(i=>!!i[1]).map(([i,e])=>[h(i),h(e)]);return r.length?$("Splits",D(["Wafer","Split"],r,{className:"compact"})):""}function K(o,r,i,e){const l=new Map,t=i==="hard"?"hardBinCounts":"softBinCounts";if(e?.length&&e.every(n=>n.stats[t]!==void 0))for(const n of e)for(const[s,m]of Object.entries(n.stats[t])){const p=Number(s);l.set(p,(l.get(p)??0)+m)}else for(const n of o){if(n.partial||n.edgeExcluded)continue;const s=i==="hard"?n.hbin:n.sbin;s!=null&&l.set(s,(l.get(s)??0)+1)}if(!l.size)return"";const a=[...l.values()].reduce((n,s)=>n+s,0),c=r?new Map(r.map(n=>[n.bin,n])):null,d=[...l.entries()].sort((n,s)=>n[0]-s[0]).map(([n,s])=>{const m=c?.get(n);return[m?.name?`Bin ${n} \xB7 ${m.name} (${s})`:`Bin ${n} (${s})`,P(s,a)]});return $(i==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",D(["Bin","Yield"],d,{className:"compact"}))}function U(o,r,i,e,l,t){const a=new Set(t),c=new Map,d=[];for(let s=0;s<e.length;s++){const m=e[s],p=i[s]??[];if(!p.length)continue;const g=r(p,m,l),b=new Map(p.map(u=>[W(u),u]));for(const u of g){d.includes(u.label)||d.push(u.label);const w=c.get(u.label)??{pass:0,total:0};for(const x of u.dieKeys){const S=b.get(x);if(!S||S.partial||S.edgeExcluded)continue;const y=S.hbin??S.sbin;y!=null&&(w.total++,a.has(y)&&w.pass++)}c.set(u.label,w)}}if(!c.size)return"";const n=d.filter(s=>c.get(s)?.total).map(s=>{const m=c.get(s);return[s,P(m.pass,m.total)]});return n.length?$(o,D(["Region","Yield"],n,{className:"compact"})):""}function wt(o,r,i){let e;if(i?.length&&i.every(l=>l.stats.perTestStats!==void 0)){const l=new Map;for(const t of i)for(const a of t.stats.perTestStats??[]){const c=l.get(a.testNumber);c?(c.n+=a.count,c.sum+=a.mean*a.count,c.min=Math.min(c.min,a.min),c.max=Math.max(c.max,a.max)):l.set(a.testNumber,{n:a.count,sum:a.mean*a.count,min:a.min,max:a.max})}e=[...l.entries()].map(([t,a])=>({testNumber:t,min:a.min,max:a.max,mean:a.sum/a.n}))}return z(o,r,e)}function $t(o,r,i){let e;if(i?.length&&i.every(l=>l.stats.functionalYield!==void 0)){const l=new Map;for(const t of i)for(const a of t.stats.functionalYield??[]){const c=l.get(a.testNumber)??{label:a.label,passDies:0,failDies:0,totalDies:0};c.passDies+=a.passDies,c.failDies+=a.failDies,c.totalDies+=a.totalDies,l.set(a.testNumber,c)}e=[...l.entries()].map(([t,a])=>({testNumber:t,...a,passRatePercent:a.totalDies>0?a.passDies/a.totalDies*100:null}))}return A(o,r,e?.length?e:void 0)}const R=["lot","product","testProgram","temperature"];function xt(o){const r=new Map(R.map(t=>[t,new Set]));for(const t of o)for(const a of R){const c=t.wafer?.metadata?.[a];c!=null&&c!==""&&r.get(a).add(String(c))}const i=R.filter(t=>r.get(t).size>1);if(i.length===0)return[{label:"",items:o,varying:i}];const e=new Map,l=[];for(const t of o){const a=i.map(c=>String(t.wafer?.metadata?.[c]??"(none)")).join(" \xB7 ");e.has(a)||(e.set(a,[]),l.push(a)),e.get(a).push(t)}return l.map(t=>({label:t,items:e.get(t),varying:i}))}function Q(o,r,i,e,l,t,a){const c=dt(o,{perWaferSummaries:o.map(f=>f.statsSummary),...a}),d=[],n=[],s=[];for(const f of o){const T=f.dies??[];f.wafer&&(d.push(f.wafer),s.push(T)),T.length&&n.push(...T)}const m=n.some(f=>f.hbin!=null),p=n.some(f=>f.sbin!=null),g=m||p,b=c.perWafer.map(f=>f.summary.stats.yieldPercent).filter(f=>f!==null),u=b.length?b.reduce((f,T)=>f+T,0)/b.length:null,w=new Set(l);let x=0,S=0,y=0,v=0,M=0;for(const f of n){if(x++,f.edgeExcluded&&v++,f.partial&&M++,f.partial||f.edgeExcluded)continue;const T=f.hbin??f.sbin;T!=null&&(S++,w.has(T)&&y++)}const Y=S-y,C=S>0?y/S*100:null,V=[{label:"Wafers",value:String(c.stats.waferCount)},{label:"Total dies",value:String(x)},...g?[{label:"Good dies",value:String(y)},{label:"Bad dies",value:String(Y)}]:[],...v>0?[{label:"Edge excluded",value:String(v)}]:[],...M>0?[{label:"Partial dies",value:String(M)}]:[],...u!==null?[{label:"Mean wafer yield",value:`${u.toFixed(1)}%`}]:[],...C!==null?[{label:"Total yield",value:`${C.toFixed(1)}%`}]:[]],_=$("Lot Summary",N(V)),J=k(o.map(f=>({metadata:f.wafer?.metadata}))),X=$("Per-Wafer Yield",St(c,o)),Z=yt(o),F=c.perWafer.map(f=>f.summary),tt=g?m?K(n,r,"hard",F):K(n,i,"soft",F):"",et=g&&d.length?U("Ring Yield (All Wafers)",H,s,d,t,l):"",nt=g&&d.length?U("Quadrant Yield (All Wafers)",L,s,d,t,l):"",at=e.length?wt(n,e,F):"",st=e.length?$t(n,e,F):"",ot=e.length?G(o,e):"",it=c.findings.length?O(c.findings,c.stats.waferCount):"",lt=[_,J,X,Z,tt,et,nt,at,st,ot,it].filter(Boolean).join(`
32
- `);return{lotSummary:c,sections:lt}}function q(o,r,i){return`<main class="report">
31
+ </html>`}function yt(a,r){const i=a.perWafer.map(n=>{const l=r[n.waferIndex]?.label??`W${n.waferIndex+1}`,t=n.summary.stats.yieldPercent;return[l,t!==null?`${t.toFixed(1)}%`:"N/A"]});return D(["Wafer","Yield"],i,{className:"compact"})}function wt(a){const r=a.map(i=>[i.label,i.wafer?.metadata?.split]).filter(i=>!!i[1]).map(([i,n])=>[h(i),h(n)]);return r.length?$("Splits",D(["Wafer","Split"],r,{className:"compact"})):""}function U(a,r,i,n){const l=new Map,t=i==="hard"?"hardBinCounts":"softBinCounts";if(n?.length&&n.every(e=>e.stats[t]!==void 0))for(const e of n)for(const[o,m]of Object.entries(e.stats[t])){const p=Number(o);l.set(p,(l.get(p)??0)+m)}else for(const e of a){if(e.partial||e.edgeExcluded)continue;const o=i==="hard"?e.hbin:e.sbin;o!=null&&l.set(o,(l.get(o)??0)+1)}if(!l.size)return"";const s=[...l.values()].reduce((e,o)=>e+o,0),c=r?new Map(r.map(e=>[e.bin,e])):null,d=[...l.entries()].sort((e,o)=>e[0]-o[0]).map(([e,o])=>{const m=c?.get(e);return[m?.name?`Bin ${e} \xB7 ${m.name} (${o})`:`Bin ${e} (${o})`,Y(o,s)]});return $(i==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",D(["Bin","Yield"],d,{className:"compact"}))}function Q(a,r,i,n,l,t){const s=new Set(t),c=new Map,d=[];for(let o=0;o<n.length;o++){const m=n[o],p=i[o]??[];if(!p.length)continue;const g=r(p.filter(k),m,l),b=new Map(p.map(u=>[W(u),u]));for(const u of g){d.includes(u.label)||d.push(u.label);const w=c.get(u.label)??{pass:0,total:0};for(const x of u.dieKeys){const S=b.get(x);if(!S||S.partial||S.edgeExcluded)continue;const y=S.hbin??S.sbin;y!=null&&(w.total++,s.has(y)&&w.pass++)}c.set(u.label,w)}}if(!c.size)return"";const e=d.filter(o=>c.get(o)?.total).map(o=>{const m=c.get(o);return[o,Y(m.pass,m.total)]});return e.length?$(a,D(["Region","Yield"],e,{className:"compact"})):""}function $t(a,r,i){let n;if(i?.length&&i.every(l=>l.stats.perTestStats!==void 0)){const l=new Map;for(const t of i)for(const s of t.stats.perTestStats??[]){const c=l.get(s.testNumber);c?(c.n+=s.count,c.sum+=s.mean*s.count,c.min=Math.min(c.min,s.min),c.max=Math.max(c.max,s.max)):l.set(s.testNumber,{n:s.count,sum:s.mean*s.count,min:s.min,max:s.max})}n=[...l.entries()].map(([t,s])=>({testNumber:t,min:s.min,max:s.max,mean:s.sum/s.n}))}return A(a,r,n)}function xt(a,r,i){let n;if(i?.length&&i.every(l=>l.stats.functionalYield!==void 0)){const l=new Map;for(const t of i)for(const s of t.stats.functionalYield??[]){const c=l.get(s.testNumber)??{label:s.label,passDies:0,failDies:0,totalDies:0};c.passDies+=s.passDies,c.failDies+=s.failDies,c.totalDies+=s.totalDies,l.set(s.testNumber,c)}n=[...l.entries()].map(([t,s])=>({testNumber:t,...s,passRatePercent:s.totalDies>0?s.passDies/s.totalDies*100:null}))}return G(a,r,n?.length?n:void 0)}const C=["lot","product","testProgram","temperature"];function Dt(a){const r=new Map(C.map(t=>[t,new Set]));for(const t of a)for(const s of C){const c=t.wafer?.metadata?.[s];c!=null&&c!==""&&r.get(s).add(String(c))}const i=C.filter(t=>r.get(t).size>1);if(i.length===0)return[{label:"",items:a,varying:i}];const n=new Map,l=[];for(const t of a){const s=i.map(c=>String(t.wafer?.metadata?.[c]??"(none)")).join(" \xB7 ");n.has(s)||(n.set(s,[]),l.push(s)),n.get(s).push(t)}return l.map(t=>({label:t,items:n.get(t),varying:i}))}function q(a,r,i,n,l,t,s){const c=ut(a,{perWaferSummaries:a.map(f=>f.statsSummary),...s}),d=[],e=[],o=[];for(const f of a){const T=f.dies??[];f.wafer&&(d.push(f.wafer),o.push(T)),T.length&&e.push(...T)}const m=e.some(f=>f.hbin!=null),p=e.some(f=>f.sbin!=null),g=m||p,b=c.perWafer.map(f=>f.summary.stats.yieldPercent).filter(f=>f!==null),u=b.length?b.reduce((f,T)=>f+T,0)/b.length:null,w=new Set(l);let x=0,S=0,y=0,v=0,P=0;for(const f of e){if(x++,f.edgeExcluded&&v++,f.partial&&P++,f.partial||f.edgeExcluded)continue;const T=f.hbin??f.sbin;T!=null&&(S++,w.has(T)&&y++)}const N=S-y,B=S>0?y/S*100:null,_=[{label:"Wafers",value:String(c.stats.waferCount)},{label:"Total dies",value:String(x)},...g?[{label:"Good dies",value:String(y)},{label:"Bad dies",value:String(N)}]:[],...v>0?[{label:"Edge excluded",value:String(v)}]:[],...P>0?[{label:"Partial dies",value:String(P)}]:[],...u!==null?[{label:"Mean wafer yield",value:`${u.toFixed(1)}%`}]:[],...B!==null?[{label:"Total yield",value:`${B.toFixed(1)}%`}]:[]],J=$("Lot Summary",E(_)),X=I(a.map(f=>({metadata:f.wafer?.metadata}))),Z=$("Per-Wafer Yield",yt(c,a)),tt=wt(a),F=c.perWafer.map(f=>f.summary),et=g?m?U(e,r,"hard",F):U(e,i,"soft",F):"",nt=g&&d.length?Q("Ring Yield (All Wafers)",H,o,d,t,l):"",st=g&&d.length?Q("Quadrant Yield (All Wafers)",L,o,d,t,l):"",at=n.length?$t(e,n,F):"",ot=n.length?xt(e,n,F):"",it=n.length?O(a,n):"",lt=c.findings.length?K(c.findings,c.stats.waferCount):"",rt=[J,X,Z,tt,et,nt,st,at,ot,it,lt].filter(Boolean).join(`
32
+ `);return{lotSummary:c,sections:rt}}function V(a,r,i){return`<main class="report">
33
33
  <header class="report-header">
34
- <h1>${h(o)}</h1>
34
+ <h1>${h(a)}</h1>
35
35
  <p class="report-subtitle">Generated ${h(i)}</p>
36
36
  </header>
37
37
  ${r}
38
38
  <p class="footer">Generated ${h(i)}</p>
39
- </main>`}export function renderLotSummaryReportHtml(o,r={}){const{items:i,hbinDefs:e,sbinDefs:l,testDefs:t=[],passBins:a=[1],ringCount:c=4,analyzeOptions:d}=o,n=new Date().toLocaleString(),s=xt(i);if(s.length===1){const{lotSummary:u,sections:w}=Q(s[0].items,e,l,t,a,c,d),x=(()=>{const y=u.lot;if(!y)return"";const v=y.lot??y.lotId;return v?` \u2014 ${h(String(v))}`:""})(),S=r.title??`Lot Summary${x}`;return`<!DOCTYPE html>
39
+ </main>`}export function renderLotSummaryReportHtml(a,r={}){const{items:i,hbinDefs:n,sbinDefs:l,testDefs:t=[],passBins:s=[1],ringCount:c=4,analyzeOptions:d}=a,e=new Date().toLocaleString(),o=Dt(i);if(o.length===1){const{lotSummary:u,sections:w}=q(o[0].items,n,l,t,s,c,d),x=(()=>{const y=u.lot;if(!y)return"";const v=y.lot??y.lotId;return v?` \u2014 ${h(String(v))}`:""})(),S=r.title??`Lot Summary${x}`;return`<!DOCTYPE html>
40
40
  <html lang="en">
41
41
  <head>
42
42
  <meta charset="UTF-8">
43
43
  <title>${h(S)}</title>
44
44
  <style>
45
- ${E()}
45
+ ${R()}
46
46
  </style>
47
47
  </head>
48
48
  <body>
49
- ${q(S,w,n)}
49
+ ${V(S,w,e)}
50
50
  </body>
51
- </html>`}const m=r.title??"Lot Summary",p=s.map(u=>{const{lotSummary:w,sections:x}=Q(u.items,e,l,t,a,c,d),S=u.varying.includes("lot")?void 0:w.lot?.lot??w.lot?.lotId,y=S?`${u.label} \xB7 Lot ${String(S)}`:u.label;return q(`${m} \u2014 ${y}`,x,n)}),g=`<p style="max-width:1080px;margin:0 auto 8px;padding:8px 12px;background:var(--report-surface,#f7f8fa);border:1px solid var(--report-line,#d8dee6);border-radius:4px;font-size:12px;color:var(--report-muted,#5c6570);">This load spans ${s.length} groups by identity \u2014 shown separately below so stats are never pooled across them.</p>`,b=p.map((u,w)=>`${w===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong,#c7ced8);">'}
51
+ </html>`}const m=r.title??"Lot Summary",p=o.map(u=>{const{lotSummary:w,sections:x}=q(u.items,n,l,t,s,c,d),S=u.varying.includes("lot")?void 0:w.lot?.lot??w.lot?.lotId,y=S?`${u.label} \xB7 Lot ${String(S)}`:u.label;return V(`${m} \u2014 ${y}`,x,e)}),g=`<p style="max-width:1080px;margin:0 auto 8px;padding:8px 12px;background:var(--report-surface,#f7f8fa);border:1px solid var(--report-line,#d8dee6);border-radius:4px;font-size:12px;color:var(--report-muted,#5c6570);">This load spans ${o.length} groups by identity \u2014 shown separately below so stats are never pooled across them.</p>`,b=p.map((u,w)=>`${w===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong,#c7ced8);">'}
52
52
  ${u}`).join(`
53
53
  `);return`<!DOCTYPE html>
54
54
  <html lang="en">
@@ -56,7 +56,7 @@ ${u}`).join(`
56
56
  <meta charset="UTF-8">
57
57
  <title>${h(m)}</title>
58
58
  <style>
59
- ${E()}
59
+ ${R()}
60
60
  </style>
61
61
  </head>
62
62
  <body>
@@ -65,4 +65,4 @@ ${g}
65
65
  </div>
66
66
  ${b}
67
67
  </body>
68
- </html>`}export{ct as openHtmlReport};
68
+ </html>`}export{dt as openHtmlReport};
@@ -1,4 +1,5 @@
1
- import type { WaferMapInput, WaferMapResult } from '../renderer/buildWaferMap.js';
1
+ import type { WaferMapInput, WaferMapResult, WaferWarning } from '../renderer/buildWaferMap.js';
2
+ export type { WaferWarning };
2
3
  export type StatsSeverity = 'info' | 'notable' | 'unusual';
3
4
  export type StatsLevel = 'wafer' | 'lot' | 'inter-wafer';
4
5
  export type StatsVariableKind = 'yield' | 'hardBin' | 'softBin' | 'test' | 'functionalTest' | 'spatialPattern';
@@ -65,8 +66,25 @@ export interface StatsFinding {
65
66
  };
66
67
  summary: string;
67
68
  highlight: HighlightTarget;
68
- /** IDs of other findings that describe the same signal at a finer level of detail. */
69
+ /**
70
+ * IDs of other findings that describe the same signal at a finer level of
71
+ * detail. Note these do NOT all resolve to entries in `findings`: when a run
72
+ * of per-region findings is merged into one (e.g. `Rings 3–4`), this is the
73
+ * audit trail of the constituents it REPLACED, and those no longer exist.
74
+ */
69
75
  relatedIds?: string[];
76
+ /**
77
+ * IDs of findings that state exactly the same fact as this one and are
78
+ * therefore hidden from reading surfaces (the Summary panel, the findings
79
+ * report) in favour of it.
80
+ *
81
+ * Distinct from `relatedIds` deliberately: everything named here IS still
82
+ * present in `findings` and can be read programmatically — nothing is
83
+ * discarded, it is only de-duplicated for display. Two cases are collapsed:
84
+ * a soft-bin finding whose hard-bin twin covers provably the same dies, and
85
+ * the single pass bin's row against the yield row that restates it.
86
+ */
87
+ absorbedIds?: string[];
70
88
  }
71
89
  export interface StatsSummary {
72
90
  level: 'wafer';
@@ -96,8 +114,20 @@ export interface StatsSummary {
96
114
  */
97
115
  hardBinCounts?: Record<number, number>;
98
116
  softBinCounts?: Record<number, number>;
99
- /** Structured warnings emitted during analysis (e.g. test-count cap exceeded). */
100
- warnings?: string[];
117
+ /**
118
+ * Structured advisories raised during analysis — the same `WaferWarning`
119
+ * shape used by `WaferMapResult.warnings`, so a host has one warning
120
+ * vocabulary to handle rather than two. Branch on `warning.code`.
121
+ *
122
+ * The one raised today is `'test-count-capped'`: more tests were found than
123
+ * the analysis cap allows, so test-value analysis was skipped and no test
124
+ * findings exist. That is a silent absence — nothing throws — so check this
125
+ * rather than assuming an empty findings list means "nothing to report".
126
+ *
127
+ * `renderWaferMap`/`renderWaferGallery` surface these automatically in the
128
+ * toolbar's warning indicator; a host does not have to render them itself.
129
+ */
130
+ warnings?: WaferWarning[];
101
131
  /** True when this summary was produced from lot-aggregated data (lotStack). */
102
132
  isLotStack?: boolean;
103
133
  /** Aggregation method used to produce the lot-stack (e.g. 'mean', 'countBin'). Present only when isLotStack is true. */
package/llms.txt ADDED
@@ -0,0 +1,39 @@
1
+ # wafermap
2
+
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.
5
+
6
+ ## Core docs
7
+ Absolute URLs: this file ships inside the npm package and the examples archive,
8
+ where repo-relative paths do not resolve.
9
+ - [Agent rules](https://wafertools.github.io/wafermap/agents/) - the traps that produce silently wrong maps. Read this first.
10
+ - [Developer Guide](https://wafertools.github.io/wafermap/guide/) - end-to-end usage, CSV loading, display controls, stats, galleries, and worker usage.
11
+ - [API Reference](https://wafertools.github.io/wafermap/api/) - public API, input types, coordinate system rules, and configuration details.
12
+ - [Quick start](https://wafertools.github.io/wafermap/quickstart/) - install and first map.
13
+ - [Troubleshooting](https://wafertools.github.io/wafermap/troubleshooting/) - common failure modes.
14
+ - [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.
15
+
16
+ ## Public package entry points
17
+ - `@wafertools/wafermap`
18
+ - `@wafertools/wafermap/render`
19
+ - `@wafertools/wafermap/stats`
20
+ - `@wafertools/wafermap/worker`
21
+ - `@wafertools/wafermap/worker-script`
22
+
23
+ ## Important notes
24
+ - `x` and `y` in `DieResult` are die grid positions (prober step integers), not millimetres.
25
+ - Use `testValues: Record<number, number>` (keyed by stable test number). Example: `testValues: { 1050: 0.95 }`. The positional `values: number[]` array was **removed** in 0.21.0 — passing it is a type error and is ignored at runtime; do not add a compatibility shim.
26
+ - Hard bins go in `hbin`; soft bins go in `sbin`. Their number spaces are independent. Never merge them, and never fall back to `?? 0` — a missing bin is not bin 0.
27
+ - All coordinates shown to the user (axis ticks, tooltips) must be original die grid positions (`die.x` / `die.y`), not post-transform display coordinates.
28
+ - `buildWaferMap()` is pure and server-safe; renderers require the DOM.
29
+ - Recommended path for most users: `buildWaferMap()` + `renderWaferMap()`.
30
+ - `passBins` sets both the yield number and the wording of its label — do not assume `[1]`.
31
+ - `activeTest` takes a test number (e.g. `1050`), not a positional index.
32
+ - Functional tests (`testType: 'F'`) have no value; read verdicts only via `getTestPassStatus()`. A missing verdict is no-data, never a fail.
33
+ - `PlotMode` values are camelCase (`'hardBin'`, `'stackedValues'`), never snake_case.
34
+ - Full rules, including the removed-API table: see AGENTS.md, shipped alongside this file.
35
+
36
+ ## Development
37
+ - Build: `npm run build`
38
+ - Test: `npm test`
39
+ - Typecheck: `npm run check`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafertools/wafermap",
3
- "version": "0.21.1",
3
+ "version": "0.23.0",
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",
@@ -25,7 +25,9 @@
25
25
  "!dist/**/*.map",
26
26
  "README.md",
27
27
  "LICENSE",
28
- "CHANGELOG.md"
28
+ "CHANGELOG.md",
29
+ "AGENTS.md",
30
+ "llms.txt"
29
31
  ],
30
32
  "main": "./dist/index.js",
31
33
  "module": "./dist/index.js",
@@ -72,10 +74,12 @@
72
74
  "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",
73
75
  "build:guide": "node scripts/build-user-guide.mjs",
74
76
  "clean": "rm -rf dist",
75
- "check": "node scripts/sync-version.mjs && tsc --noEmit -p tsconfig.json",
77
+ "check": "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",
78
+ "build:examples-index": "node scripts/build-examples-index.mjs",
79
+ "check:drift": "node scripts/check-drift.mjs",
76
80
  "dev": "rm -rf docs/dist && cp -r dist docs/dist && ./.venv/bin/zensical serve --dev-addr localhost:8001",
77
81
  "test": "npm run build && node --test tests/*.test.mjs",
78
- "build:site": "rm -rf docs/dist && cp -r dist docs/ && ${ZENSICAL:-.venv/bin/zensical} build --clean && cp -r docs/data site/data && rm -rf docs/dist && node scripts/bundle-docs.mjs",
82
+ "build:site": "rm -rf docs/dist && cp -r dist docs/ && ${ZENSICAL:-.venv/bin/zensical} build --clean && cp -r docs/data site/data && rm -rf docs/dist && node scripts/bundle-docs.mjs && node scripts/build-examples-archive.mjs",
79
83
  "preview:site": "npm run build:site && echo \"\\nServing bundled site at http://localhost:8002/examples/ — Ctrl-C to stop\\n\" && cd site && python3 -m http.server 8002",
80
84
  "prepack": "npm run build && node scripts/minify-dist.mjs",
81
85
  "pack:check": "npm pack --dry-run",
@@ -83,7 +87,8 @@
83
87
  "postpublish": "echo \"\\ndist/ was minified for publish — run 'npm run build' to restore the readable dev build.\\n\"",
84
88
  "screenshots": "node scripts/capture-screenshots.mjs",
85
89
  "screenshots:list": "node scripts/capture-screenshots.mjs --list",
86
- "build:images": "npm run build && node scripts/capture-screenshots.mjs"
90
+ "build:images": "npm run build && node scripts/capture-screenshots.mjs",
91
+ "build:agents-page": "node scripts/build-agents-page.mjs"
87
92
  },
88
93
  "keywords": [
89
94
  "wafer-map",
@@ -110,11 +115,10 @@
110
115
  "devDependencies": {
111
116
  "esbuild": "^0.28.1",
112
117
  "jsdom": "^29.1.1",
113
- "marked": "^18.0.3",
118
+ "marked": "^18.0.7",
114
119
  "playwright": "^1.60.0",
115
- "semble": "^0.1.1",
116
120
  "serve": "^14.2.0",
117
- "typescript": "^5.6.3"
121
+ "typescript": "^5.9.3"
118
122
  },
119
123
  "allowScripts": {
120
124
  "esbuild@0.28.1": true