@wafertools/wafermap 0.30.2 → 0.30.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/AGENTS.md +16 -5
  2. package/CHANGELOG.md +432 -0
  3. package/README.md +6 -3
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +48 -0
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +4 -1
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +4 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +82 -8
  12. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  14. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  15. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/scatter.d.ts +5 -1
  18. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  19. package/dist/packages/canvas-adapter/charts/sweep.d.ts +31 -0
  20. package/dist/packages/canvas-adapter/charts/sweep.js +1 -0
  21. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  22. package/dist/packages/canvas-adapter/charts/trend.d.ts +3 -1
  23. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  24. package/dist/packages/canvas-adapter/dieList.js +2 -2
  25. package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
  26. package/dist/packages/canvas-adapter/drilldown.js +1 -0
  27. package/dist/packages/canvas-adapter/icons.js +1 -1
  28. package/dist/packages/canvas-adapter/insightsTab.d.ts +65 -3
  29. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  30. package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
  31. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  32. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  33. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  34. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  35. package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
  36. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  37. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  38. package/dist/packages/canvas-adapter/userGuideHtml.js +162 -37
  39. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  40. package/dist/packages/canvas-adapter/version.js +1 -1
  41. package/dist/packages/canvas-adapter/warnings.js +1 -1
  42. package/dist/packages/core/dies.d.ts +1 -1
  43. package/dist/packages/renderer/axisTicks.d.ts +32 -0
  44. package/dist/packages/renderer/axisTicks.js +1 -0
  45. package/dist/packages/renderer/binColors.js +1 -1
  46. package/dist/packages/renderer/buildView.d.ts +11 -37
  47. package/dist/packages/renderer/buildView.js +1 -1
  48. package/dist/packages/renderer/buildWaferMap.d.ts +97 -12
  49. package/dist/packages/renderer/buildWaferMap.js +1 -1
  50. package/dist/packages/renderer/deprecate.d.ts +13 -5
  51. package/dist/packages/renderer/deprecate.js +1 -1
  52. package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
  53. package/dist/packages/renderer/derivedTests/apply.js +1 -0
  54. package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
  55. package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
  56. package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
  57. package/dist/packages/renderer/derivedTests/parser.js +2 -0
  58. package/dist/packages/renderer/fmt.d.ts +24 -2
  59. package/dist/packages/renderer/fmt.js +1 -1
  60. package/dist/packages/renderer/index.d.ts +1 -0
  61. package/dist/packages/renderer/index.js +1 -1
  62. package/dist/packages/renderer/spec.d.ts +66 -0
  63. package/dist/packages/renderer/spec.js +1 -0
  64. package/dist/packages/renderer/testLabel.d.ts +154 -0
  65. package/dist/packages/renderer/testLabel.js +1 -0
  66. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  67. package/dist/packages/stats/capability.d.ts +8 -2
  68. package/dist/packages/stats/capability.js +1 -1
  69. package/dist/packages/stats/correlation.d.ts +4 -1
  70. package/dist/packages/stats/correlation.js +1 -1
  71. package/dist/packages/stats/deprecated.d.ts +4 -1
  72. package/dist/packages/stats/deprecated.js +1 -1
  73. package/dist/packages/stats/index.d.ts +2 -2
  74. package/dist/packages/stats/index.js +1 -1
  75. package/dist/packages/stats/mergeTestDefs.d.ts +1 -1
  76. package/dist/packages/stats/mergeTestDefs.js +1 -1
  77. package/dist/packages/stats/renderFindingsReport.js +8 -28
  78. package/dist/packages/stats/renderSummaryReport.js +19 -28
  79. package/dist/packages/stats/reportHtml.d.ts +27 -0
  80. package/dist/packages/stats/reportHtml.js +35 -14
  81. package/dist/packages/stats/sweep.d.ts +186 -0
  82. package/dist/packages/stats/sweep.js +1 -0
  83. package/dist/packages/stats/sweepXFromName.d.ts +27 -0
  84. package/dist/packages/stats/sweepXFromName.js +1 -0
  85. package/dist/packages/stats/testPassRate.js +1 -1
  86. package/dist/packages/stats/types.d.ts +37 -4
  87. package/llms.txt +3 -2
  88. package/package.json +1 -1
@@ -1,19 +1,10 @@
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
- </tr>`).join(`
8
- `);return`<table class="report-table findings-table compact"><thead><tr>
9
- <th>Severity</th><th>Region</th><th>Metric</th><th class="numeric">Delta</th><th class="numeric">${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(`
1
+ import{waferDisplayLabel as H}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 z,buildRegionYieldData as Be}from"./regions.js";import{openHtmlReport as ve}from"./renderFindingsReport.js";import{analyzeWaferLot as Te}from"./analyzeWaferLot.js";import{computeFunctionalYield as Pe,analyzeWaferMap as Fe}from"./analyzeWaferMap.js";import{mergeTestDefs as Ye}from"./mergeTestDefs.js";import{sortBinsForDisplay as A}from"./binPareto.js";import{buildFacetTable as We,facetValueOf as Me,FACET_NONE_VALUE as Ce}from"./facets.js";import{visibleFindings as O}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 Le,poolFunctionalYield as ke}from"./testPassRate.js";import{buildCapabilityData as He}from"./capability.js";import{fmt as T}from"../renderer/fmt.js";import{getDieKey as Ie,isPositionedDie as je,diePassStatus as U}from"../core/dies.js";import{findingsTableHtml as V,renderMetadataSection as _,renderMetricGrid as K,renderSection as x,renderTable as v,reportStyles as N}from"./reportHtml.js";import{escHtml as y}from"../core/utils.js";function C(t,o){return o===0?"\u2014":`${(t/o*100).toFixed(1)}%`}function ze(t){if(!t)return"";const o=t.lot??t.lotId,a=t.wafer??t.waferId,n=[o,a].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(y).join(" \xB7 ")}`:""}function Q(t,o,a,n,r=[1]){const e=new Map;if(n)for(const[s,c]of Object.entries(n))e.set(Number(s),c);else for(const s of t){if(s.partial||s.edgeExcluded)continue;const c=a==="hard"?s.hbin:s.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const i=[...e.values()].reduce((s,c)=>s+c,0),l=o?new Map(o.map(s=>[s.bin,s])):null,d=A(e.entries(),we(t,r)[a]).map(([s,c])=>{const p=l?.get(s)?.name??"\u2014";return[String(s),p,String(c),C(c,i)]});return x(a==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",v(["Bin","Name","Count","%"],d,{className:"compact"}))}function q(t,o,a,n){const r=new Set(n),e=new Map(a.map(d=>[Ie(d),d]));if(!a.some(d=>d.hbin!=null||d.sbin!=null)||!o.length)return"";const l=[];for(const d of o){let f=0,s=0;for(const c of d.dieKeys){const h=e.get(c);if(!h||h.partial||h.edgeExcluded)continue;const p=U(h,r);p!==void 0&&(s++,p&&f++)}s&&l.push([d.label,String(f),String(s),C(f,s)])}return l.length?x(t,v(["Region","Pass","Total","Yield"],l,{className:"compact"})):""}function J(t,o,a){if(!o.length)return"";const n=t.filter(i=>!i.partial&&!i.edgeExcluded),r=new Map((a??[]).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 s,c,h;if(f)({min:s,max:c,mean:h}=f);else{const p=n.map(w=>w.testValues?.[l]).filter(w=>w!==void 0&&isFinite(w));if(!p.length)continue;p.sort((w,b)=>w-b),s=p[0],c=p[p.length-1],h=p.reduce((w,b)=>w+b,0)/p.length}e.push([y(i.name),T(s,d),T(h,d),T(c,d)])}return e.length?x("Test Values",v(["Test","Min","Mean","Max"],e,{className:"compact"})):""}function X(t,o,a){const n=a??Pe(t,o);if(!n?.length)return"";const r=n.map(e=>[y(e.label),String(e.totalDies),String(e.passDies),String(e.failDies),e.passRatePercent!==null?`${e.passRatePercent.toFixed(1)}%`:"\u2014"]);return x("Functional Tests",v(["Test","N","Pass","Fail","Pass Rate"],r,{className:"compact"}))}function Z(t,o){const a=He(t,o);if(!a.length)return"";const n=e=>e===null?"\u2014":e.toFixed(2),r=a.map(e=>[y(e.label),e.hasSpec?e.limitBasis==="spec"?`LSL ${T(e.lsl,e.unit)} \u2013 USL ${T(e.usl,e.unit)}`:`Lo limit ${T(e.lsl,e.unit)} \u2013 Hi limit ${T(e.usl,e.unit)}`:"\u2014",T(e.mean,e.unit),n(e.cp),n(e.cpk),n(e.pp),n(e.ppk)]);return x("Process Capability",v(["Test","Limits","Mean","Cp","Cpk","Pp","Ppk"],r,{className:"compact"}))}function ee(t,o){const a=O(t);return a.length?x("Findings",V(a,o)):""}export function renderSummaryReportHtml(t,o={}){const{wafer:a,dies:n,yieldSummary:r,dataCoverage:e,hbinDefs:i,sbinDefs:l,testDefs:d=[],statsSummary:f,passBins:s=[1],ringCount:c=4}=t,h=a.metadata,p=ze(h),w=o.title??`Wafer Summary${p}`,b=new Date().toLocaleString(),u=n.some(W=>W.hbin!=null),g=n.some(W=>W.sbin!=null),S=n.filter(je),D=j(S,a,c),$=z(S,a,c),B=[{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([s])})`,value:`${r.yieldPercent.toFixed(1)}%`}]:[],...r.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(r.edgeExcludedDies)}]:[]],Y=[_([{metadata:a.metadata}]),x("Summary",[K(B)].filter(Boolean).join(`
2
+ `)),u?Q(n,i,"hard",f?.stats.hardBinCounts,s):g?Q(n,l,"soft",f?.stats.softBinCounts,s):"",q("Ring Yield",D,n,s),q("Quadrant Yield",$,n,s),J(n,d,f?.stats.perTestStats),X(n,d,f?.stats.functionalYield),Z([{dies:n}],d),f?ee(f.findings):""].filter(Boolean).join(`
12
3
  `);return`<!DOCTYPE html>
13
4
  <html lang="en">
14
5
  <head>
15
6
  <meta charset="UTF-8">
16
- <title>${h(w)}</title>
7
+ <title>${y(w)}</title>
17
8
  <style>
18
9
  ${N()}
19
10
  </style>
@@ -21,29 +12,29 @@ ${N()}
21
12
  <body>
22
13
  <main class="report">
23
14
  <header class="report-header">
24
- <h1>${h(w)}</h1>
25
- <p class="report-subtitle">Generated ${h(y)}</p>
15
+ <h1>${y(w)}</h1>
16
+ <p class="report-subtitle">Generated ${y(b)}</p>
26
17
  </header>
27
18
  ${Y}
28
- <p class="footer">Generated ${h(y)}</p>
19
+ <p class="footer">Generated ${y(b)}</p>
29
20
  </main>
30
21
  </body>
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(`
22
+ </html>`}function Ae(t,o){const a=t.perWafer.map(n=>{const r=H(o[n.waferIndex],n.waferIndex),e=n.summary.stats.yieldPercent;return[r,e!==null?`${e.toFixed(1)}%`:"N/A"]});return v(["Wafer","Yield"],a,{className:"compact"})}function Oe(t){const o=[];for(const e of t.perWafer){const i=O(e.summary.findings);if(!i.length)continue;const l=e.summary.wafer?.waferId??`Wafer ${e.waferIndex+1}`;o.push(`<h3 class="report-subheading">${y(l)}</h3>`+V(i))}if(!o.length)return"";const a=o.length,n=t.perWafer.length,r=`<p class="report-note">${a} of ${n} wafer${n===1?"":"s"} have per-wafer findings; the rest had none.</p>`;return x("Findings by Wafer",r+o.join(`
23
+ `))}const Ge=3;function Ue(t,o,a){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,Ge),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 s=[...f.entries()].map(([u,g])=>({key:u,items:g.map(S=>t[S])}));if(s.length<2)continue;const c=s.map(u=>[y(u.key),y(u.items.map(g=>g.label).join(", "))]),h=Re(s.map(u=>({key:u.key,items:u.items.map(g=>({label:g.label,dies:g.dies,passBins:R(g,a)}))})),a).map(u=>[y(u.label),String(u.itemCount),`${u.percent.toFixed(1)}%`]),p=[v([d.label,"Wafers"],c,{className:"compact"}),v([d.label,"Wafers","Yield"],h,{className:"compact"})],w=t.flatMap(u=>u.dies??[]);let b=0;for(const u of["spec","testFlag","functional"]){if(!Le(o,u,w))continue;const g=Ee(s.map($=>({key:$.key,items:$.items.map(B=>({dies:B.dies,testSpecYield:B.statsSummary?.stats.testSpecYield,functionalYield:B.statsSummary?.stats.functionalYield}))})),o,u);if(!g.rows.length)continue;const S=g.rows.map($=>[y($.label),...$.byGroup.map(B=>B.passRatePercent===null?"\u2014":`${B.passRatePercent.toFixed(1)}%`),$.overall.passRatePercent===null?"\u2014":`${$.overall.passRatePercent.toFixed(1)}%`]),D=u==="spec"?"Pass rate \xB7 test limits":u==="testFlag"?"Pass rate \xB7 tester flag":"Pass rate \xB7 functional";g.disagreementDies&&(b=g.disagreementDies),p.push(`<h4 class="report-subheading">${y(D)}</h4>`+v(["Test",...g.groups,"All"],S,{className:"compact"}))}b&&p.push(`<p class="report-note">${b.toLocaleString()} dies judged differently by limits and tester flag.</p>`),i.push(`<h3 class="report-subheading">${y(d.label)}</h3>${p.join(`
33
24
  `)}`)}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">
25
+ `)+l)}function te(t,o,a,n,r){const e=new Map,i=a==="hard"?"hardBinCounts":"softBinCounts";if(n?.length&&n.every(s=>s.stats[i]!==void 0))for(const s of n)for(const[c,h]of Object.entries(s.stats[i])){const p=Number(c);e.set(p,(e.get(p)??0)+h)}else for(const s of t){if(s.partial||s.edgeExcluded)continue;const c=a==="hard"?s.hbin:s.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const l=[...e.values()].reduce((s,c)=>s+c,0),d=o?new Map(o.map(s=>[s.bin,s])):null,f=A(e.entries(),$e(r)[a]).map(([s,c])=>{const h=d?.get(s);return[h?.name?`Bin ${s} \xB7 ${h.name} (${c})`:`Bin ${s} (${c})`,C(c,l)]});return x(a==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",v(["Bin","Yield"],f,{className:"compact"}))}function ne(t,o,a,n,r,e){const i=Be(a,n,r,l=>e[l],o).map(l=>[l.label,`${l.yieldPercent.toFixed(1)}%`]);return i.length?x(t,v(["Region","Yield"],i,{className:"compact"})):""}function Ve(t,o,a){let n;if(a?.length&&a.every(r=>r.stats.perTestStats!==void 0)){const r=new Map;for(const e of a)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 J(t,o,n)}function _e(t,o,a){const n=ke(a);return X(t,o,n)}const E=["lot","product","testProgram","temperature"];function Ke(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 a=E.filter(e=>o.get(e).size>1);if(a.length===0)return[{label:"",items:t,varying:a}];const n=new Map,r=[];for(const e of t){const i=a.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:a}))}function se(t,o,a,n,r,e,i){const l=Te(t.map(m=>m.source??m),{perWaferSummaries:t.map(m=>m.statsSummary),...i}),d=[],f=[],s=[],c=[];for(const m of t){const F=m.dies??[];m.wafer&&(d.push(m.wafer),s.push(F),c.push(R(m,r)));for(const P of F)f.push(P)}const h=t.map(m=>({dies:m.dies??[],passBins:R(m,r)})),p=f.some(m=>m.hbin!=null),w=f.some(m=>m.sbin!=null),b=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,B=0,Y=0;for(const m of h){const F=new Set(m.passBins);for(const P of m.dies){if(S++,P.edgeExcluded&&B++,P.partial&&Y++,P.partial||P.edgeExcluded)continue;const k=U(P,F);k!==void 0&&(D++,k&&$++)}}const W=D-$,L=D>0?$/D*100:null,ie=[{label:"Wafers",value:String(l.stats.waferCount)},{label:"Total dies",value:String(S)},...b?[{label:"Good dies",value:String($)},{label:"Bad dies",value:String(W)}]:[],...B>0?[{label:"Edge excluded",value:String(B)}]:[],...Y>0?[{label:"Partial dies",value:String(Y)}]:[],...g!==null?[{label:"Mean wafer yield",value:`${g.toFixed(1)}%`}]:[],...L!==null?[{label:"Total yield",value:`${L.toFixed(1)}%`}]:[]],oe=G(l.perWafer.map(m=>m.summary.wafer)),re=x(oe.lotId!==void 0?"Lot Summary":"Summary",K(ie)),le=_(t.map(m=>({metadata:m.wafer?.metadata}))),ce=x("Per-Wafer Yield",Ae(l,t)),de=Ue(t,n,r),M=l.perWafer.map(m=>m.summary),ue=b?p?te(f,o,"hard",M,h):te(f,a,"soft",M,h):"",fe=b&&d.length?ne("Ring Yield (All Wafers)",j,s,d,e,c):"",me=b&&d.length?ne("Quadrant Yield (All Wafers)",z,s,d,e,c):"",pe=n.length?Ve(f,n,M):"",ge=n.length?_e(f,n,M):"",he=n.length?Z(t,n):"",be=l.findings.length?ee(l.findings,l.stats.waferCount):"",ye=Oe(l),Se=[re,le,ce,de,ue,fe,me,pe,ge,he,be,ye].filter(Boolean).join(`
26
+ `);return{lotSummary:l,sections:Se}}function ae(t,o,a){return`<main class="report">
36
27
  <header class="report-header">
37
- <h1>${h(t)}</h1>
38
- <p class="report-subtitle">Generated ${h(s)}</p>
28
+ <h1>${y(t)}</h1>
29
+ <p class="report-subtitle">Generated ${y(a)}</p>
39
30
  </header>
40
31
  ${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>
32
+ <p class="footer">Generated ${y(a)}</p>
33
+ </main>`}export function renderLotSummaryReportHtml(t,o={}){const{items:a,hbinDefs:n,sbinDefs:r,testDefs:e=[],passBins:i=[1],ringCount:l=4,analyzeOptions:d}=t,f=new Date().toLocaleString(),s=Ke(a);if(s.length===1){const{lotSummary:b,sections:u}=se(s[0].items,n,r,e,i,l,d),g=G(b.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
34
  <html lang="en">
44
35
  <head>
45
36
  <meta charset="UTF-8">
46
- <title>${h(S)}</title>
37
+ <title>${y(S)}</title>
47
38
  <style>
48
39
  ${N()}
49
40
  </style>
@@ -51,13 +42,13 @@ ${N()}
51
42
  <body>
52
43
  ${ae(S,u,f)}
53
44
  </body>
54
- </html>`}const c=o.title??"Summary",b=a.map(y=>{const{lotSummary:u,sections:g}=se(y.items,n,r,e,i,l,d),S=y.varying.includes("lot")?void 0:u.lot?.lot??u.lot?.lotId,D=S?`${y.label} \xB7 Lot ${String(S)}`:y.label;return ae(`${c} \u2014 ${D}`,g,f)}),p=`<p style="max-width:1080px;margin:0 auto 8px;padding:8px 12px;background:var(--report-surface);border:1px solid var(--report-line);border-radius:4px;font-size:12px;color:var(--report-muted);">This load spans ${a.length} groups by identity \u2014 shown separately below so stats are never pooled across them.</p>`,w=b.map((y,u)=>`${u===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong);">'}
55
- ${y}`).join(`
45
+ </html>`}const c=o.title??"Summary",h=s.map(b=>{const{lotSummary:u,sections:g}=se(b.items,n,r,e,i,l,d),S=b.varying.includes("lot")?void 0:u.lot?.lot??u.lot?.lotId,D=S?`${b.label} \xB7 Lot ${String(S)}`:b.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 ${s.length} groups by identity \u2014 shown separately below so stats are never pooled across them.</p>`,w=h.map((b,u)=>`${u===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong);">'}
46
+ ${b}`).join(`
56
47
  `);return`<!DOCTYPE html>
57
48
  <html lang="en">
58
49
  <head>
59
50
  <meta charset="UTF-8">
60
- <title>${h(c)}</title>
51
+ <title>${y(c)}</title>
61
52
  <style>
62
53
  ${N()}
63
54
  </style>
@@ -68,4 +59,4 @@ ${p}
68
59
  </div>
69
60
  ${w}
70
61
  </body>
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})}
62
+ </html>`}export{ve as openHtmlReport};export function renderWaferReportHtml(t,o,a={}){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?Fe(t):void 0),passBins:[...t.passBins],ringCount:t.ringCount},a)}export function renderLotReportHtml(t,o={}){const a=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:H(e,i),wafer:e.wafer,dies:e.dies,passBins:[...e.passBins],statsSummary:e.statsSummary,source:"view"in e?e:void 0})),hbinDefs:a.length?a:void 0,sbinDefs:n.length?n:void 0,testDefs:r,ringCount:t[0]?.ringCount,analyzeOptions:o.analyzeOptions},{title:o.title})}
@@ -37,7 +37,34 @@ export declare function renderTable(headers: string[], rows: string[][], options
37
37
  }): string;
38
38
  export declare function renderSeverityBadge(severity: StatsSeverity): string;
39
39
  export declare function formatFindingDelta(finding: Pick<StatsFinding, 'effect' | 'variable'>): string;
40
+ /**
41
+ * A finding's hover text, for the Summary panel and both HTML reports alike —
42
+ * the one rule for it. The sentence already carries the `†` for a finding about
43
+ * a derived test; the tooltip adds the words for it and the expression, on a
44
+ * line of their own.
45
+ */
40
46
  export declare function formatFindingTooltip(finding: StatsFinding): string;
47
+ /**
48
+ * The `†` key under a findings table, as HTML — `''` when no finding in it is
49
+ * about a derived test. A printed report has no hover, so unlike the on-screen
50
+ * key this one names each derived test's expression: for a reader of the PDF
51
+ * it is the only place to learn what a value was computed from.
52
+ */
53
+ export declare function derivedFindingsKeyHtml(findings: StatsFinding[]): string;
54
+ /**
55
+ * THE findings table, for every HTML report — the wafer and lot reports' Findings
56
+ * sections and the findings-only report alike, with the derived-test key under
57
+ * it when any row is about a derived test.
58
+ *
59
+ * It existed twice until 0.30.3, once per report module, and the copies had
60
+ * already drifted: only the findings report translated the internal bin terms
61
+ * ("HBin 2" → "hard bin 2"), so the wafer and lot reports printed the jargon the
62
+ * design principles keep out of the UI.
63
+ *
64
+ * `totalWafers` switches the coverage column from "N (region/rest)" to a count
65
+ * of wafers, for lot-level findings.
66
+ */
67
+ export declare function findingsTableHtml(findings: StatsFinding[], totalWafers?: number): string;
41
68
  export declare function formatFindingCoverage(finding: Pick<StatsFinding, 'stats'>, total?: number): string;
42
69
  export declare function reportStyles(): string;
43
70
  //# sourceMappingURL=reportHtml.d.ts.map
@@ -1,21 +1,42 @@
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
- ${t}
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
- `);return`<dl class="${t}">
1
+ import{fmt as p,plainBinTerms as d}from"../renderer/fmt.js";import{buildFacetTable as c,prettyKey as m}from"./facets.js";import{escHtml as o}from"../core/utils.js";import{derivedTestNote as b,derivedKeyText as u,derivedFields as f}from"../renderer/testLabel.js";export function renderSection(t,e,r=""){return`<section class="${["report-section",r].filter(Boolean).join(" ")}">
2
+ <h2>${o(t)}</h2>
3
+ ${e}
4
+ </section>`}export function renderDefinitionList(t,e="meta-list"){if(!t.length)return"";const r=t.map(a=>`<div class="definition-item"><dt>${o(a.label)}</dt><dd>${o(a.value)}</dd></div>`).join(`
5
+ `);return`<dl class="${e}">
6
6
  ${r}
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
- ${e.map(r=>`<div class="metric">
9
- <dt>${a(r.label)}</dt>
10
- <dd>${a(r.value)}</dd>
11
- ${r.hint?`<span class="metric-hint">${a(r.hint)}</span>`:""}
7
+ </dl>`}export function buildMetadataRows(t){return c(t,{facetableOnly:t.length>1}).map(r=>({label:m(r.key),value:r.values.map(a=>a.value).join(", ")}))}export function renderMetadataSection(t){const e=buildMetadataRows(t);return e.length?renderSection("Wafer Info",renderDefinitionList(e)):""}export function renderMetricGrid(t){return t.length?`<dl class="metric-grid">
8
+ ${t.map(r=>`<div class="metric">
9
+ <dt>${o(r.label)}</dt>
10
+ <dd>${o(r.value)}</dd>
11
+ ${r.hint?`<span class="metric-hint">${o(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">${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
- `);return`<table class="${o}">
16
- <thead><tr>${p}</tr></thead>
14
+ </dl>`:""}export function renderTable(t,e,r={}){if(!e.length)return`<p class="no-data">${o(r.emptyMessage??"No data")}</p>`;const a=["report-table",r.className].filter(Boolean).join(" "),i=t.map(n=>`<th>${o(n)}</th>`).join(""),l=e.map(n=>`<tr>${n.map(s=>`<td>${s}</td>`).join("")}</tr>`).join(`
15
+ `);return`<table class="${a}">
16
+ <thead><tr>${i}</tr></thead>
17
17
  <tbody>${l}</tbody>
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`
18
+ </table>`}export function renderSeverityBadge(t){const e=t.charAt(0).toUpperCase()+t.slice(1);return`<span class="badge badge-${t}">${e}</span>`}export function formatFindingDelta(t){const e=t.effect.absoluteDelta??0,r=e>0?"+":e<0?"\u2212":"";if(t.variable.kind==="yield"||t.variable.kind==="hardBin"||t.variable.kind==="softBin")return`${r}${Math.abs(e*100).toFixed(1)} pp`;if(t.variable.kind==="test"){if(t.effect.relativeDelta!==void 0&&Number.isFinite(t.effect.relativeDelta)){const a=Math.abs(t.effect.relativeDelta*100).toFixed(1);return`${r}${a}%`}return`${r}${p(Math.abs(e),t.variable.unit)}`}return`${r}${p(Math.abs(e),t.variable.unit)}`}export function formatFindingTooltip(t){const e=b(t.variable);return e?`${t.summary}
19
+ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({label:r.variable.label,...f(r.variable)})));return e?`<p class="report-note">${o(e)}</p>`:""}export function findingsTableHtml(t,e){const r=t.length?t.map(i=>`<tr title="${o(formatFindingTooltip(i))}">
20
+ <td class="tight">${renderSeverityBadge(i.severity)}</td>
21
+ <td class="tight">${o(i.comparison.left)}</td>
22
+ <td>${o(d(i.variable.label))}</td>
23
+ <td class="numeric">${o(formatFindingDelta(i))}</td>
24
+ <td class="numeric">${o(formatFindingCoverage(i,e))}</td>
25
+ </tr>`).join(`
26
+ `):'<tr><td colspan="5" class="no-data">No significant findings</td></tr>';return`<table class="report-table findings-table compact">
27
+ <thead>
28
+ <tr>
29
+ <th>Severity</th>
30
+ <th>Region</th>
31
+ <th>Metric</th>
32
+ <th class="numeric">Delta</th>
33
+ <th class="numeric">${e!==void 0?"Wafers":"N (region/rest)"}</th>
34
+ </tr>
35
+ </thead>
36
+ <tbody>
37
+ ${r}
38
+ </tbody>
39
+ </table>${derivedFindingsKeyHtml(t)}`}export function formatFindingCoverage(t,e){return e!==void 0?`${t.stats.sampleSizeLeft}/${e}`:`${t.stats.sampleSizeLeft}/${t.stats.sampleSizeRight}`}export function reportStyles(){return`
19
40
  :root {
20
41
  --report-font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
42
  --report-text: #1f2328;
@@ -0,0 +1,186 @@
1
+ import type { Die } from '../core/dies.js';
2
+ import { type TestDef } from '../renderer/buildWaferMap.js';
3
+ /** One series of a sweep — an ordered run of tests measuring the same quantity. */
4
+ export interface SweepSeriesSpec {
5
+ /** Shown in the legend, e.g. `"Rising Response"`. */
6
+ label: string;
7
+ /**
8
+ * Test numbers in sweep order. Order is the x axis and is never sorted.
9
+ *
10
+ * An entry may also be a range string, `"1010..1030"` — exactly the syntax of
11
+ * a derived-test expression's `t[1010..1030]`, parsed by the same code
12
+ * (`parseTestReference`), so the same text always names the same tests. It
13
+ * expands to the DECLARED tests inside it, ascending: a program numbered in
14
+ * steps of 2 needs no step syntax. Entries are expanded in the order written,
15
+ * so `["1010..1020", 1035]` appends 1035 after the block.
16
+ */
17
+ tests: Array<number | string>;
18
+ /**
19
+ * Physical x value for each test — the actual swept quantity (power in dBm,
20
+ * voltage, temperature). Same length as `tests` AFTER ranges are expanded.
21
+ *
22
+ * A length mismatch is reported naming the tests the ranges matched, and the
23
+ * sweep is then drawn on the ordinal axis with no crossing or widths measured:
24
+ * a missing test inside a range would otherwise shift every later x onto the
25
+ * wrong test, which draws a plausible curve with a wrong crossing.
26
+ *
27
+ * Without it the x axis is the ORDINAL position in the sequence, labelled by
28
+ * test number, because test numbers are identifiers and are not guaranteed to
29
+ * be evenly spaced or even ascending — interpolating a crossing point along
30
+ * them would silently assume a scale the data never claimed.
31
+ */
32
+ xValues?: number[];
33
+ /**
34
+ * Read each test's x value from its NAME instead — for programs that record
35
+ * the swept quantity only in the test text. A placeholder pattern, not a
36
+ * regular expression: `{x}` reads a number, `*` matches any run of
37
+ * characters, everything else is literal. `"LRS_STATS_{x}"` reads 12,000 from
38
+ * a test named `"Normalized_LRS= LRS_STATS_12K / …"`.
39
+ *
40
+ * - The pattern is found ANYWHERE in the name; no leading or trailing `*`.
41
+ * - Literal text matches case-insensitively; the value's SI prefix does not
42
+ * (`m` is milli, `M` mega). `{x}` reads the prefix, and `K` is accepted as
43
+ * kilo. Put the prefix in the pattern instead (`"LRS_STATS_{x}K"`) to keep
44
+ * the numbers as the name shows them (12 rather than 12,000).
45
+ * - Each x stays attached to its own test, so a test missing from the data
46
+ * loses one point instead of shifting every later x the way a positional
47
+ * `xValues` list does.
48
+ *
49
+ * A test whose name does not match, or a series giving both this and
50
+ * `xValues`, is reported, and the crossing and widths are then not measured.
51
+ */
52
+ xFromName?: string;
53
+ /** Optional explicit colour. Omit to take one from the chart's palette. */
54
+ color?: string;
55
+ }
56
+ export interface SweepSpec {
57
+ /** Stable identity, so a host can persist which sweep was selected. */
58
+ id: string;
59
+ /** Panel title, e.g. `"Power Sweep"`. */
60
+ title: string;
61
+ /** Two or more series. Crossing and separation are measured between the
62
+ * FIRST TWO; further series are drawn but not measured. */
63
+ series: SweepSeriesSpec[];
64
+ xLabel?: string;
65
+ /**
66
+ * Unit of the x values — `"Ω"`, `"V"`, `"Hz"`. With it the ticks, crossing
67
+ * and widths print with SI prefixes (`47.3 kΩ`), the way the y axis does from
68
+ * the tests' own unit. Give the bare unit; the prefix comes from the values.
69
+ */
70
+ xUnit?: string;
71
+ /**
72
+ * `'log'` places the x values on a logarithmic axis — for a sweep whose steps
73
+ * grow by multiples (1k, 2k, 5k, 10k … 1M), which a linear axis squeezes into
74
+ * one edge. The crossing is then interpolated along log x, and a width is
75
+ * reported as a RATIO between the two series ("×1.8"), not a difference.
76
+ * Needs every x value to be positive; otherwise the axis stays linear and the
77
+ * card says why. Default `'linear'`.
78
+ */
79
+ xScale?: 'linear' | 'log';
80
+ yLabel?: string;
81
+ /** Report where the first two series cross. Default true when there are two. */
82
+ crossing?: boolean;
83
+ /** Y levels at which to report the horizontal distance between the first two
84
+ * series, in test-value units. */
85
+ separationAt?: number[];
86
+ }
87
+ export interface SweepPoint {
88
+ /** Test number this point came from. */
89
+ testNumber: number;
90
+ /** The point's test is derived (`TestDef.derived`); `label` then starts with `"† "`. A
91
+ * sweep over derived tests is how a curve on a log scale is drawn: sweep
92
+ * `log10(t[n])` per step. */
93
+ derived?: true;
94
+ /** The expression a `derived` point's test was computed from, for display. */
95
+ expression?: string;
96
+ /** Tick label — the test's name when defined, else `"Test 1010"`. */
97
+ label: string;
98
+ /** Position on the x axis: `xValues[i]` when supplied, else the ordinal index. */
99
+ x: number;
100
+ /** Median across the population. `NaN` when `count` is 0. */
101
+ median: number;
102
+ /** Spread band. Equal to `median` when `count` is 1. */
103
+ p10: number;
104
+ p90: number;
105
+ /** Dies contributing a finite value at this point. */
106
+ count: number;
107
+ }
108
+ export interface SweepSeriesData {
109
+ label: string;
110
+ color?: string;
111
+ points: SweepPoint[];
112
+ }
113
+ export interface SweepCrossing {
114
+ x: number;
115
+ y: number;
116
+ /** True when the curves cross more than once — the reported point is the
117
+ * first. A single number cannot describe a multiple crossing, so the panel
118
+ * says so rather than implying precision it does not have. */
119
+ multiple: boolean;
120
+ }
121
+ export interface SweepSeparation {
122
+ /** The y level asked about. */
123
+ y: number;
124
+ /** Horizontal distance between the two series at that level, or `null` when
125
+ * either series never reaches it within the swept range. */
126
+ distance: number | null;
127
+ /** Why it is null, for the panel to show instead of an empty cell. */
128
+ reason?: 'first-series-never-reaches' | 'second-series-never-reaches';
129
+ /** Where the first and second series reach the level, when both do. */
130
+ from?: number;
131
+ to?: number;
132
+ /** On a log axis, `to / from` or its inverse, whichever is ≥ 1 — the width
133
+ * in the form a log axis measures it. */
134
+ ratio?: number;
135
+ }
136
+ export interface SweepData {
137
+ title: string;
138
+ xLabel: string;
139
+ yLabel: string;
140
+ series: SweepSeriesData[];
141
+ crossing?: SweepCrossing | null;
142
+ separations: SweepSeparation[];
143
+ /**
144
+ * True when the caller supplied real `xValues`, so `SweepPoint.x` is a
145
+ * physical quantity and the axis should be labelled with NUMBERS. When false
146
+ * the x positions are ordinals and the ticks must be the test labels — an
147
+ * axis showing "0, 1, 2" for test identifiers would be a scale the data never
148
+ * claimed.
149
+ */
150
+ xIsPhysical: boolean;
151
+ /** `SweepSpec.xUnit`, when the axis is physical. */
152
+ xUnit?: string;
153
+ /** The axis actually drawn — `'log'` only when asked for and every x is positive. */
154
+ xScale: 'linear' | 'log';
155
+ /** Dies eligible for the sweep — the denominator behind every median. */
156
+ dieCount: number;
157
+ /** Problems with the spec itself, for the panel to show rather than silently
158
+ * drawing a partial sweep. */
159
+ warnings: string[];
160
+ /**
161
+ * Set when crossing and widths were deliberately NOT measured, saying why —
162
+ * `xValues` were supplied but cannot be paired with the tests. Measuring on
163
+ * the ordinal fallback would print an ordinal position under the caller's
164
+ * physical `xLabel`.
165
+ */
166
+ notMeasured?: string;
167
+ }
168
+ /**
169
+ * Whether `spec` names at least one parametric test in `testDefs` — i.e.
170
+ * whether it can draw anything for data with these tests. False means none of
171
+ * its tests exist here: typically a sweep from another test program, still
172
+ * defined after a different lot was loaded. Internal: the Sweeps tab and the
173
+ * sweep card say so themselves.
174
+ */
175
+ export declare function sweepAppliesTo(spec: SweepSpec, testDefs: readonly TestDef[] | undefined): boolean;
176
+ /**
177
+ * Build the plottable data for one sweep over a die population.
178
+ *
179
+ * Dies are filtered by `isYieldEligibleDie` — the same population convention as
180
+ * every other chart, so partial and edge-excluded dies do not quietly shift a
181
+ * median. A test that is missing, functional, or has no finite values anywhere
182
+ * still produces a point, with `count: 0`, so the gap stays visible in the
183
+ * sequence rather than closing up and misreporting the curve's shape.
184
+ */
185
+ export declare function buildSweepData(dies: Die[], testDefs: TestDef[] | undefined, spec: SweepSpec): SweepData;
186
+ //# sourceMappingURL=sweep.d.ts.map
@@ -0,0 +1 @@
1
+ import{isYieldEligibleDie as G}from"../core/dies.js";import{isParametricTest as M}from"../renderer/buildWaferMap.js";import{markedTestLabel as H,derivedFields as J}from"../renderer/testLabel.js";import{parseTestReference as K}from"../renderer/derivedTests/parser.js";import{quantile as V}from"./math.js";import{compileXNamePattern as Q,readXFromName as W}from"./sweepXFromName.js";function j(o,a,s){const e=[];let t=!1;for(const r of o.tests){if(typeof r=="number"){Number.isInteger(r)?e.push(r):s.push(`"${o.label}": ${r} is not a test number.`);continue}const i=K(String(r),a);if(!i.ok){s.push(`"${o.label}": "${r}" \u2014 ${i.message}.`);continue}i.range&&(t=!0),e.push(...i.tests)}return{spec:o,tests:e,usedRange:t}}export function sweepAppliesTo(o,a){const s=new Map((a??[]).map(e=>[e.testNumber,e]));return o.series.some(e=>j(e,s,[]).tests.some(t=>{const r=s.get(t);return r!==void 0&&M(r)}))}function B(o,a=16){return o.length<=a?o.join(", "):`${o.slice(0,a).join(", ")}, \u2026 (${o.length-a} more)`}function Z(o,a){const s=o?.[a];return s!==void 0&&Number.isFinite(s)?s:a}function _(o,a,s){const e=o.spec;if(e.xValues!==void 0&&e.xFromName!==void 0)return s.push(`"${e.label}": gives both xValues and xFromName \u2014 use one.`),{asked:!0};if(e.xValues!==void 0){const t=e.xValues.length;return t===o.tests.length?{xs:e.xValues,asked:!0}:(s.push(o.usedRange?`"${e.label}": ${t} x values for ${o.tests.length} tests \u2014 the ranges matched ${B(o.tests)}.`:`"${e.label}": ${t} x values for ${o.tests.length} tests.`),{asked:!0})}if(e.xFromName!==void 0){const t=Q(e.xFromName);if("error"in t)return s.push(`"${e.label}": xFromName "${e.xFromName}" \u2014 ${t.error}.`),{asked:!0};const r=[],i=[],u=[];for(const l of o.tests){const c=a.get(l)?.name,f=c===void 0?null:W(t,c);typeof f=="number"?r.push(f):(i.push(l),f!==null&&u.push(f.ambiguous))}return i.length>0?(s.push(`"${e.label}": xFromName "${e.xFromName}" found no x value in the name of ${i.length===1?"test":"tests"} ${B(i)}`+(u.length>0?` \u2014 "${u[0]}" is written in capitals, where M could be milli or mega; put the letter in the pattern ("\u2026{x}M\u2026") and the unit in xLabel.`:".")),{asked:!0}):{xs:r,asked:!0}}return{asked:!1}}function ee(o,a,s){const e=new Map;for(let t=0;t<s.length;t++){const r=e.get(s[t]);if(r!==void 0)return`"${o}": tests ${a[r]} and ${a[t]} have the same x value (${s[t]}) \u2014 a curve cannot be measured where it revisits an x.`;e.set(s[t],t)}return null}function D(o,a){const s=o.filter(e=>e.count>0);for(let e=1;e<s.length;e++){const t=s[e-1],r=s[e],i=Math.min(t.median,r.median),u=Math.max(t.median,r.median);if(a<i||a>u)continue;if(t.median===r.median)return t.x;const l=(a-t.median)/(r.median-t.median);return t.x+l*(r.x-t.x)}return null}function ne(o,a){const s=new Map(a.filter(i=>i.count>0).map(i=>[i.x,i])),e=[];for(const i of o){if(i.count===0)continue;const u=s.get(i.x);u!==void 0&&e.push({x:i.x,d:i.median-u.median,y:i.median})}if(e.length<2)return null;const t=[];for(let i=0;i<e.length;i++){const u=e[i];if(u.d===0){t.push({x:u.x,y:u.y});continue}if(i===0)continue;const l=e[i-1];if(l.d===0||l.d<0==u.d<0)continue;const c=l.d/(l.d-u.d);t.push({x:l.x+c*(u.x-l.x),y:l.y+c*(u.y-l.y)})}const r=t[0];return r===void 0?null:{x:r.x,y:r.y,multiple:t.length>1}}export function buildSweepData(o,a,s){const e=[],t=new Map((a??[]).map(n=>[n.testNumber,n])),r=o.filter(n=>G(n)),i=s.series.map(n=>j(n,t,e)),u=i.map(n=>_(n,t,e)),l=i.length>0&&u.every(n=>n.xs!==void 0),c=!l&&u.some(n=>n.asked);c&&u.some(n=>!n.asked)&&e.push("x values are given for some series but not all.");const f=l?i.map((n,d)=>ee(n.spec.label,n.tests,u[d].xs)).find(n=>n!==null)??null:null;f!==null&&e.push(f);const S=s.xScale==="log",T=l&&u.every(n=>n.xs.every(d=>d>0)),b=S&&T?"log":"linear";S&&!T&&e.push(l?"A log x axis needs every x value to be positive \u2014 drawn on a linear axis.":"A log x axis needs x values (xValues or xFromName) \u2014 drawn in test order.");const L=i.map(({spec:n,tests:d},p)=>{const v=l?u[p].xs:void 0,N=d.map((x,z)=>{const h=t.get(x);h===void 0?e.push(`"${n.label}": test ${x} is not in this program's test definitions.`):M(h)||e.push(`"${n.label}": test ${x} is functional and has no measured value.`);const m=[];if(h!==void 0&&M(h))for(const F of r){const g=F.testValues?.[x];g!==void 0&&Number.isFinite(g)&&m.push(g)}return m.sort((F,g)=>F-g),{testNumber:x,label:H(h,x),...J(h),x:Z(v,z),median:m.length?V(m,.5):NaN,p10:m.length?V(m,.1):NaN,p90:m.length?V(m,.9):NaN,count:m.length}});return{label:n.label,color:n.color,points:N}}),[X,C]=L,$=X!==void 0&&C!==void 0&&!c&&f===null,I=s.crossing??!0,Y=n=>b==="log"?Math.log10(n):n,k=n=>b==="log"?10**n:n,P=n=>n.map(d=>({...d,x:Y(d.x)})),q=$?P(X.points):[],R=$?P(C.points):[],E=(c||f!==null?[]:s.separationAt??[]).map(n=>{if(!$)return{y:n,distance:null};const d=D(q,n),p=D(R,n);if(d===null)return{y:n,distance:null,reason:"first-series-never-reaches"};if(p===null)return{y:n,distance:null,reason:"second-series-never-reaches"};const v=k(d),N=k(p);return{y:n,distance:Math.abs(N-v),from:v,to:N,...b==="log"?{ratio:10**Math.abs(d-p)}:{}}}),w=$&&I?ne(q,R):null,O=w&&{...w,x:k(w.x)},U=n=>n===void 0?void 0:t.get(n.tests[0]??-1)?.unit,y=U(i[0]),A=U(i[1]);return y!==void 0&&A!==void 0&&y!==A&&e.push(`Series measure different units (${y} and ${A}) \u2014 crossing and separation compare unlike quantities.`),{title:s.title,xLabel:c?"Test":s.xLabel??(l?"Sweep value":"Test"),yLabel:s.yLabel??"Value",series:L,xIsPhysical:l,...l&&s.xUnit?{xUnit:s.xUnit}:{},xScale:b,crossing:O,separations:E,dieCount:r.length,warnings:e,...c&&s.series.length>=2?{notMeasured:"x values do not match the tests, so the sweep is drawn in test order and the crossing and widths are not measured"}:f!==null&&s.series.length>=2?{notMeasured:"a series revisits an x value, so its points cannot be paired with the other series"}:{}}}
@@ -0,0 +1,27 @@
1
+ type Part = {
2
+ kind: 'lit';
3
+ text: string;
4
+ } | {
5
+ kind: 'star';
6
+ } | {
7
+ kind: 'x';
8
+ };
9
+ /** A compiled `xFromName` pattern. */
10
+ export interface XNamePattern {
11
+ source: string;
12
+ parts: Part[];
13
+ }
14
+ /** Compile a pattern, or return why it cannot be used. */
15
+ export declare function compileXNamePattern(source: string): XNamePattern | {
16
+ error: string;
17
+ };
18
+ /**
19
+ * The x value `pattern` reads from `name`; `null` when the pattern does not fit
20
+ * the name anywhere (or fits only where no number stands at `{x}`); or
21
+ * `{ ambiguous }` naming the text whose prefix cannot be read — `"2MV"`.
22
+ */
23
+ export declare function readXFromName(pattern: XNamePattern, name: string): number | null | {
24
+ ambiguous: string;
25
+ };
26
+ export {};
27
+ //# sourceMappingURL=sweepXFromName.d.ts.map
@@ -0,0 +1 @@
1
+ import{SI_PREFIX_SCALE as m,KNOWN_BASE_UNITS as N}from"../renderer/fmt.js";const a=Object.fromEntries(Object.entries({...m,K:1e3}).map(([t,e])=>[t,Math.round(Math.log10(e))])),x=t=>t!==void 0&&/\p{L}/u.test(t),w=[...new Set(N.map(t=>t.toLowerCase()))];function b(t,e){const n=t[e];if(n===void 0||!x(n))return null;const u=e+1;if(!x(t[u]))return a[n]??null;const i=t.slice(u),s=w.find(r=>i.toLowerCase().startsWith(r)&&!x(i[r.length]));if(s===void 0)return null;const c=i.slice(0,s.length);return!N.includes(c)||!/\p{Ll}/u.test(t)?n.toLowerCase()==="m"?"ambiguous":a[n]??a[n.toLowerCase()]??a[n.toUpperCase()]??null:a[n]??null}export function compileXNamePattern(t){const e=[];let n="",u=0;const i=()=>{n&&(e.push({kind:"lit",text:n.toLowerCase()}),n="")};for(let s=0;s<t.length;s++){if(t.startsWith("{x}",s)){i(),e.push({kind:"x"}),u++,s+=2;continue}if(t[s]==="*"){i(),e.at(-1)?.kind!=="star"&&e.push({kind:"star"});continue}n+=t[s]}return i(),u!==1?{error:`the pattern must contain {x} exactly once (it has ${u})`}:{source:t,parts:e}}const L=/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/;function p(t,e){const n=L.exec(t.slice(e));if(!n)return[];const u=n[0],i=e+u.length,s=b(t,i);if(s==="ambiguous"){const o=t.slice(e,i+1+(/^\p{L}+/u.exec(t.slice(i+1))?.[0].length??0));return[{value:Number(u),end:i,literalMustFollow:o}]}const c=[];if(s!==null){const[o,r="0"]=u.split(/[eE]/);c.push({value:+`${o}e${Number(r)+s}`,end:i+1})}return c.push({value:Number(u),end:i}),c}export function readXFromName(t,e){const{parts:n}=t;let u;const i=e.toLowerCase(),s=new Set,c=(r,l)=>r*(e.length+1)+l;function o(r,l){if(r===n.length)return NaN;if(s.has(c(r,l)))return;const h=n[r];let d;if(h.kind==="lit")i.startsWith(h.text,l)&&(d=o(r+1,l+h.text.length));else if(h.kind==="star")for(let f=l;f<=e.length&&d===void 0;f++)d=o(r+1,f);else for(const f of p(e,l)){if(f.literalMustFollow!==void 0){const g=n[r+1];if(g?.kind!=="lit"||!i.startsWith(g.text,f.end)){u??=f.literalMustFollow;continue}}if(o(r+1,f.end)!==void 0){d=f.value;break}}return d===void 0&&s.add(c(r,l)),d}for(let r=0;r<=e.length;r++){const l=o(0,r);if(l!==void 0)return Number.isFinite(l)?l:null}return u!==void 0?{ambiguous:u}:null}
@@ -1 +1 @@
1
- import{isYieldEligibleDie as Y}from"../core/dies.js";import{isParametricTest as L,getTestPassStatus as v}from"../renderer/buildWaferMap.js";const y=(n,e)=>e===0?null:n/e*100;export function buildTestPassRateData(n,e,o){const a=n.map(t=>t.key),i={groups:a,rows:[],disagreementDies:null};if(!e?.length)return i;const r=e.filter(t=>t.testNumber!==void 0&&P(t,o));if(!r.length)return i;const h=()=>({pass:0,fail:0,failLow:0,failHigh:0}),g=new Map;for(const t of r){const s=t.testNumber;g.set(s,{label:t.name??`Test ${s}`,overall:h(),perGroup:a.map(()=>h())})}const c=(t,s,f)=>{const l=g.get(t);if(l)for(const u of[l.overall,l.perGroup[s]])u.pass+=f.pass??0,u.fail+=f.fail??0,u.failLow+=f.failLow??0,u.failHigh+=f.failHigh??0},b=o==="spec"||o==="testFlag";let w=0,N=0;n.forEach((t,s)=>{for(const f of t.items){if(o==="spec"&&f.testSpecYield&&!R(f)){for(const l of f.testSpecYield)c(l.testNumber,s,{pass:l.passDies,fail:l.failLowDies+l.failHighDies,failLow:l.failLowDies,failHigh:l.failHighDies});continue}if(o==="functional"&&f.functionalYield){for(const l of f.functionalYield)c(l.testNumber,s,{pass:l.passDies,fail:l.failDies});continue}for(const l of f.dies??[])if(Y(l))for(const u of r){const p=u.testNumber,d=T(l,u),m=v(l,p,u);if(b&&d!==void 0&&m!==void 0&&(w++,d.pass!==m&&N++),o==="spec"){if(d===void 0)continue;d.pass?c(p,s,{pass:1}):d.low?c(p,s,{fail:1,failLow:1}):c(p,s,{fail:1,failHigh:1})}else{if(m===void 0)continue;c(p,s,m?{pass:1}:{fail:1})}}}});const H=t=>({passDies:t.pass,failDies:t.fail,totalDies:t.pass+t.fail,passRatePercent:y(t.pass,t.pass+t.fail)}),D=[];for(const[t,s]of g)s.overall.pass+s.overall.fail!==0&&D.push({testNumber:t,label:s.label,overall:H(s.overall),byGroup:s.perGroup.map(H),...o==="spec"?{failLowDies:s.overall.failLow,failHighDies:s.overall.failHigh}:{}});return D.sort((t,s)=>(t.overall.passRatePercent??101)-(s.overall.passRatePercent??101)||t.testNumber-s.testNumber),{groups:a,rows:D,disagreementDies:b&&w>0?N:null}}function T(n,e){if(e.limitLow===void 0&&e.limitHigh===void 0)return;const o=n.testValues?.[e.testNumber];if(o===void 0||!Number.isFinite(o))return;const a=e.limitLow!==void 0&&o<e.limitLow,i=e.limitHigh!==void 0&&o>e.limitHigh;return{pass:!a&&!i,low:a}}function R(n){return(n.dies??[]).some(e=>e.testPass!==void 0&&Object.keys(e.testPass).length>0)}function P(n,e){return e==="functional"?!L(n):L(n)?e==="spec"?n.limitLow!==void 0||n.limitHigh!==void 0:!0:!1}export function hasJudgeableTests(n,e,o){if(!n?.length)return!1;const a=n.filter(i=>i.testNumber!==void 0&&P(i,e));return a.length?e!=="testFlag"?!0:(o??[]).some(i=>a.some(r=>v(i,r.testNumber,r)!==void 0)):!1}export function poolFunctionalYield(n){if(!n?.length||!n.every(a=>a.stats.functionalYield!==void 0))return;const e=new Map;for(const a of n)for(const i of a.stats.functionalYield??[]){const r=e.get(i.testNumber)??{label:i.label,passDies:0,failDies:0,totalDies:0};r.passDies+=i.passDies,r.failDies+=i.failDies,r.totalDies+=i.totalDies,e.set(i.testNumber,r)}const o=[...e.entries()].map(([a,i])=>({testNumber:a,...i,passRatePercent:i.totalDies>0?i.passDies/i.totalDies*100:null}));return o.length?o:void 0}
1
+ import{isYieldEligibleDie as y}from"../core/dies.js";import{isParametricTest as L,getTestPassStatus as P}from"../renderer/buildWaferMap.js";import{classifySpec as Y}from"../renderer/spec.js";import{testLabel as T,derivedFields as R}from"../renderer/testLabel.js";const F=(i,l)=>l===0?null:i/l*100;export function buildTestPassRateData(i,l,o){const n=i.map(e=>e.key),t={groups:n,rows:[],disagreementDies:null};if(!l?.length)return t;const r=l.filter(e=>e.testNumber!==void 0&&H(e,o));if(!r.length)return t;const b=()=>({pass:0,fail:0,failLow:0,failHigh:0}),g=new Map;for(const e of r){const s=e.testNumber;g.set(s,{label:T(e,s),overall:b(),perGroup:n.map(()=>b())})}const c=(e,s,f)=>{const a=g.get(e);if(a)for(const u of[a.overall,a.perGroup[s]])u.pass+=f.pass??0,u.fail+=f.fail??0,u.failLow+=f.failLow??0,u.failHigh+=f.failHigh??0},h=o==="spec"||o==="testFlag";let w=0,N=0;i.forEach((e,s)=>{for(const f of e.items){if(o==="spec"&&f.testSpecYield&&!G(f)){for(const a of f.testSpecYield)c(a.testNumber,s,{pass:a.passDies,fail:a.failLowDies+a.failHighDies,failLow:a.failLowDies,failHigh:a.failHighDies});continue}if(o==="functional"&&f.functionalYield){for(const a of f.functionalYield)c(a.testNumber,s,{pass:a.passDies,fail:a.failDies});continue}for(const a of f.dies??[])if(y(a))for(const u of r){const p=u.testNumber,m=Y(a.testValues?.[p],u),d=P(a,p,u);if(h&&m!==null&&d!==void 0&&(w++,m==="pass"!==d&&N++),o==="spec"){if(m===null)continue;m==="pass"?c(p,s,{pass:1}):m==="failLow"?c(p,s,{fail:1,failLow:1}):c(p,s,{fail:1,failHigh:1})}else{if(d===void 0)continue;c(p,s,d?{pass:1}:{fail:1})}}}});const v=e=>({passDies:e.pass,failDies:e.fail,totalDies:e.pass+e.fail,passRatePercent:F(e.pass,e.pass+e.fail)}),D=[];for(const[e,s]of g)s.overall.pass+s.overall.fail!==0&&D.push({testNumber:e,label:s.label,overall:v(s.overall),byGroup:s.perGroup.map(v),...o==="spec"?{failLowDies:s.overall.failLow,failHighDies:s.overall.failHigh}:{}});return D.sort((e,s)=>(e.overall.passRatePercent??101)-(s.overall.passRatePercent??101)||e.testNumber-s.testNumber),{groups:n,rows:D,disagreementDies:h&&w>0?N:null}}function G(i){return(i.dies??[]).some(l=>l.testPass!==void 0&&Object.keys(l.testPass).length>0)}function H(i,l){return l==="functional"?!L(i):L(i)?l==="spec"?i.limitLow!==void 0||i.limitHigh!==void 0:!0:!1}export function hasJudgeableTests(i,l,o){if(!i?.length)return!1;const n=i.filter(t=>t.testNumber!==void 0&&H(t,l));return n.length?l!=="testFlag"?!0:(o??[]).some(t=>n.some(r=>P(t,r.testNumber,r)!==void 0)):!1}export function poolFunctionalYield(i){if(!i?.length||!i.every(n=>n.stats.functionalYield!==void 0))return;const l=new Map;for(const n of i)for(const t of n.stats.functionalYield??[]){const r=l.get(t.testNumber)??{label:t.label,...R(t),passDies:0,failDies:0,totalDies:0};r.passDies+=t.passDies,r.failDies+=t.failDies,r.totalDies+=t.totalDies,l.set(t.testNumber,r)}const o=[...l.entries()].map(([n,t])=>({testNumber:n,...t,passRatePercent:t.totalDies>0?t.passDies/t.totalDies*100:null}));return o.length?o:void 0}
@@ -72,10 +72,24 @@ export interface StatsFinding {
72
72
  index?: number;
73
73
  /** For bin kinds, the bin number this finding is about. */
74
74
  bin?: number;
75
- /** Display name, already resolved through any supplied bin/test definitions. */
75
+ /**
76
+ * Display name, already resolved through any supplied bin/test definitions.
77
+ * For a derived test (`derived`) it starts with `"† "`, as does the test's name
78
+ * inside `summary` — marked at the source, so no reader of a finding can
79
+ * present a derived value as a measured one. A surface showing either
80
+ * string should show the key ("† Derived, not measured") with it.
81
+ */
76
82
  label: string;
77
83
  /** Unit of `effect.absoluteDelta`, when the variable has one. */
78
84
  unit?: string;
85
+ /**
86
+ * True when the finding is about a test computed from other tests rather
87
+ * than measured (`TestDef.derived`). The structured form of the `†` in
88
+ * `label`, for a host that wants to mark it its own way.
89
+ */
90
+ derived?: true;
91
+ /** The expression a `derived` test was computed from, for display. */
92
+ expression?: string;
79
93
  };
80
94
  /** Which two populations were compared. */
81
95
  comparison: {
@@ -219,7 +233,7 @@ export interface StatsSummary {
219
233
  /** Number of wafers in the lot stack. Present only when isLotStack is true. */
220
234
  lotSize?: number;
221
235
  /**
222
- * Per-test spec yield for each test that has at least one limit defined.
236
+ * Per-test yield against the test limits, for each test that has at least one limit defined.
223
237
  * Only populated when testDefs with limitLow/limitHigh are provided.
224
238
  */
225
239
  testSpecYield?: Array<{
@@ -243,6 +257,10 @@ export interface StatsSummary {
243
257
  functionalYield?: Array<{
244
258
  testNumber: number;
245
259
  label: string;
260
+ /** Computed from other tests rather than measured; `label` then starts with `"† "`. See `TestDef.derived`. */
261
+ derived?: true;
262
+ /** The expression a `derived` test was computed from, for display. */
263
+ expression?: string;
246
264
  passDies: number;
247
265
  failDies: number;
248
266
  /** Dies with a recorded pass/fail verdict for this test. */
@@ -275,7 +293,7 @@ export interface StatsSummary {
275
293
  capability?: TestCapability[];
276
294
  /**
277
295
  * Per-test pass rate by the tester's recorded verdict, for parametric tests
278
- * with recorded verdicts — the counterpart of `testSpecYield` (spec limits)
296
+ * with recorded verdicts — the counterpart of `testSpecYield` (test limits)
279
297
  * and `functionalYield` (functional tests). Worst first.
280
298
  */
281
299
  testFlagYield?: TestVerdictYield[];
@@ -312,10 +330,25 @@ export interface TestCapability {
312
330
  testNumber: number;
313
331
  label: string;
314
332
  unit?: string;
315
- /** Both `limitLow` and `limitHigh` defined. When false, `lsl`/`usl` are absent and every index is null. */
333
+ /**
334
+ * True when this test was computed from other tests rather than measured
335
+ * (`TestDef.derived`). Carried onto the row so the panel can mark it: a Cpk
336
+ * printed against a derived quantity is a valid figure, but a reader who
337
+ * takes it for a measured one draws the wrong conclusion about the process.
338
+ */
339
+ derived?: true;
340
+ /** The expression a `derived` test was computed from, for display. */
341
+ expression?: string;
342
+ /**
343
+ * Limits to judge capability against exist: both spec limits (`specLow`/
344
+ * `specHigh`), or failing that both test limits (`limitLow`/`limitHigh`). When
345
+ * false, `lsl`/`usl` are absent and every index is null.
346
+ */
316
347
  hasSpec: boolean;
317
348
  lsl?: number;
318
349
  usl?: number;
350
+ /** `'spec'` when `lsl`/`usl` are the spec limits, `'test'` when they are the test limits. */
351
+ limitBasis?: 'spec' | 'test';
319
352
  mean: number;
320
353
  /** Sample stddev (ddof = 1) over every value. */
321
354
  stdOverall: number;