@wafertools/wafermap 0.31.0 → 0.33.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 (100) hide show
  1. package/AGENTS.md +13 -13
  2. package/CHANGELOG.md +334 -1
  3. package/README.md +3 -3
  4. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +2 -12
  5. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  6. package/dist/packages/canvas-adapter/dieList.js +7 -7
  7. package/dist/packages/canvas-adapter/drilldown.js +1 -1
  8. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  9. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  10. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +47 -4
  11. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  12. package/dist/packages/canvas-adapter/summaryPanel.d.ts +22 -3
  13. package/dist/packages/canvas-adapter/summaryPanel.js +4 -3
  14. package/dist/packages/canvas-adapter/toCanvas.d.ts +23 -2
  15. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  16. package/dist/packages/canvas-adapter/toolbar.d.ts +54 -1
  17. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  18. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  19. package/dist/packages/canvas-adapter/userGuideHtml.js +100 -27
  20. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  21. package/dist/packages/canvas-adapter/version.js +1 -1
  22. package/dist/packages/core/aggregates.d.ts +6 -0
  23. package/dist/packages/core/aggregates.js +1 -1
  24. package/dist/packages/core/compact.d.ts +205 -0
  25. package/dist/packages/core/compact.js +3 -0
  26. package/dist/packages/core/dieTable.d.ts +137 -0
  27. package/dist/packages/core/dieTable.js +1 -0
  28. package/dist/packages/core/dies.d.ts +22 -0
  29. package/dist/packages/core/dies.js +1 -1
  30. package/dist/packages/core/passBins.d.ts +6 -0
  31. package/dist/packages/core/passBins.js +1 -1
  32. package/dist/packages/core/utils.d.ts +14 -0
  33. package/dist/packages/core/utils.js +1 -1
  34. package/dist/packages/renderer/axisTicks.d.ts +14 -0
  35. package/dist/packages/renderer/axisTicks.js +1 -1
  36. package/dist/packages/renderer/buildView.d.ts +29 -4
  37. package/dist/packages/renderer/buildView.js +1 -1
  38. package/dist/packages/renderer/buildWaferMap.d.ts +29 -11
  39. package/dist/packages/renderer/buildWaferMap.js +1 -1
  40. package/dist/packages/renderer/columnarInput.d.ts +87 -0
  41. package/dist/packages/renderer/columnarInput.js +1 -0
  42. package/dist/packages/renderer/compactView.d.ts +22 -0
  43. package/dist/packages/renderer/compactView.js +1 -0
  44. package/dist/packages/renderer/derivedTests/apply.d.ts +8 -6
  45. package/dist/packages/renderer/derivedTests/apply.js +1 -1
  46. package/dist/packages/renderer/index.d.ts +1 -0
  47. package/dist/packages/renderer/spec.d.ts +3 -7
  48. package/dist/packages/renderer/spec.js +1 -1
  49. package/dist/packages/renderer/xyIndicator.d.ts +27 -0
  50. package/dist/packages/renderer/xyIndicator.js +1 -0
  51. package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
  52. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  53. package/dist/packages/stats/analyzeWaferMap.d.ts +169 -2
  54. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  55. package/dist/packages/stats/binRows.d.ts +38 -0
  56. package/dist/packages/stats/binRows.js +1 -0
  57. package/dist/packages/stats/boxplot.js +1 -1
  58. package/dist/packages/stats/capability.d.ts +7 -7
  59. package/dist/packages/stats/capability.js +1 -1
  60. package/dist/packages/stats/clusterDetection.js +1 -1
  61. package/dist/packages/stats/connectedComponents.js +1 -1
  62. package/dist/packages/stats/correlation.js +1 -1
  63. package/dist/packages/stats/filterFindings.d.ts +30 -0
  64. package/dist/packages/stats/filterFindings.js +1 -1
  65. package/dist/packages/stats/findingText.d.ts +9 -0
  66. package/dist/packages/stats/findingText.js +2 -0
  67. package/dist/packages/stats/histogram.js +1 -1
  68. package/dist/packages/stats/lotDrift.d.ts +16 -0
  69. package/dist/packages/stats/lotDrift.js +1 -0
  70. package/dist/packages/stats/lotPattern.d.ts +33 -0
  71. package/dist/packages/stats/lotPattern.js +1 -0
  72. package/dist/packages/stats/math.d.ts +73 -6
  73. package/dist/packages/stats/math.js +1 -1
  74. package/dist/packages/stats/normalizeInput.js +1 -1
  75. package/dist/packages/stats/patternClassification.d.ts +34 -3
  76. package/dist/packages/stats/patternClassification.js +1 -1
  77. package/dist/packages/stats/presentation.d.ts +42 -0
  78. package/dist/packages/stats/presentation.js +1 -0
  79. package/dist/packages/stats/regions.d.ts +16 -0
  80. package/dist/packages/stats/regions.js +1 -1
  81. package/dist/packages/stats/renderSummaryReport.d.ts +15 -2
  82. package/dist/packages/stats/renderSummaryReport.js +32 -27
  83. package/dist/packages/stats/reportHtml.d.ts +54 -9
  84. package/dist/packages/stats/reportHtml.js +130 -47
  85. package/dist/packages/stats/scatter.js +1 -1
  86. package/dist/packages/stats/sweep.js +1 -1
  87. package/dist/packages/stats/synthesis.d.ts +66 -0
  88. package/dist/packages/stats/synthesis.js +2 -0
  89. package/dist/packages/stats/testPassRate.js +1 -1
  90. package/dist/packages/stats/trend.js +1 -1
  91. package/dist/packages/stats/types.d.ts +6 -1
  92. package/dist/packages/stats/yieldRows.d.ts +31 -0
  93. package/dist/packages/stats/yieldRows.js +1 -0
  94. package/dist/packages/worker/index.js +1 -1
  95. package/dist/packages/worker/wafermap.worker.d.ts +3 -0
  96. package/dist/packages/worker/wafermap.worker.js +1 -1
  97. package/llms.txt +1 -1
  98. package/package.json +1 -1
  99. package/dist/packages/stats/findingsNarrative.d.ts +0 -3
  100. package/dist/packages/stats/findingsNarrative.js +0 -1
@@ -0,0 +1,66 @@
1
+ import type { LotStatsSummary, StatsSeverity, StatsSummary } from './types.js';
2
+ /** What a sentence fragment points at, so a surface can link it to its evidence. */
3
+ export interface SynthesisTarget {
4
+ kind: 'finding';
5
+ id: string;
6
+ }
7
+ /** A run of text, optionally linked. Structured, not a string, so the panel can make
8
+ * links and the report can make anchors from the same sentence. */
9
+ export interface SynthesisPart {
10
+ text: string;
11
+ target?: SynthesisTarget;
12
+ }
13
+ /** How much of the lot an item costs, in three steps. Drawn as a meter, never as colour alone. */
14
+ export type ImpactTier = 'high' | 'medium' | 'low';
15
+ export interface SynthesisItem {
16
+ /** From the share of analysed dies lost, the same quantity the items are ranked by. */
17
+ impact: ImpactTier;
18
+ /** Estimated dies lost to this item (pass-rate shortfall × dies in the region). */
19
+ diesLost: number;
20
+ severity: StatsSeverity;
21
+ parts: SynthesisPart[];
22
+ /** The leading finding, then the others that stand on the same dies. */
23
+ findingIds: string[];
24
+ /** The item in a few words, for the "also" line: `Ring 2 (hard bin 4 (Short), 62 dies)`. */
25
+ brief: SynthesisPart;
26
+ }
27
+ /**
28
+ * A signal that costs no dies of its own yet but predicts loss: a trend across the wafers, or a test whose
29
+ * spread leaves little margin to its limits. Kept apart from the items, which are ranked by dies lost: a
30
+ * Ppk of 0.9 with no failures does not belong on the same axis as 400 failing dies.
31
+ */
32
+ export interface SynthesisWatch {
33
+ parts: SynthesisPart[];
34
+ findingIds: string[];
35
+ }
36
+ export interface Synthesis {
37
+ headline: SynthesisPart[];
38
+ items: SynthesisItem[];
39
+ /**
40
+ * Items that clear the floor but not the cap, in a few words each, so nothing material goes
41
+ * unmentioned. Absent when the cap shows them all.
42
+ */
43
+ also?: SynthesisPart[];
44
+ /** At most two things to keep an eye on, after the items. Absent when there are none. */
45
+ watch?: SynthesisWatch[];
46
+ /** Said instead of the items when none is material, so a good lot reads as one rather than as an empty list. */
47
+ nothing?: string;
48
+ /** What was compared and not repeated above. Required: a short synthesis must not read as "not analysed". */
49
+ checked: string;
50
+ }
51
+ export interface SynthesisContext {
52
+ /**
53
+ * The pass bins every wafer was judged by: names them in the headline, and says which bins are
54
+ * failures. Left out when the wafers were judged differently — the synthesis then stays with
55
+ * yield and does not call any bin a failure, rather than assume `[1]`.
56
+ */
57
+ passBins?: readonly number[];
58
+ }
59
+ /**
60
+ * The synthesis for a wafer or a lot: a headline, up to three loss items ranked by dies
61
+ * lost, and a line saying what was compared.
62
+ */
63
+ export declare function buildSynthesis(summary: StatsSummary | LotStatsSummary, ctx?: SynthesisContext): Synthesis;
64
+ /** A synthesis as plain text, one line per part, for tests and for any surface that is not HTML. */
65
+ export declare function synthesisText(s: Synthesis): string;
66
+ //# sourceMappingURL=synthesis.d.ts.map
@@ -0,0 +1,2 @@
1
+ import{maxOf as C,minOf as P}from"../core/utils.js";import{passBinsLabel as T}from"../core/passBins.js";import{DRIFT_MIN_WAFERS as q}from"./lotDrift.js";import{plainBinTerms as Y}from"../renderer/fmt.js";const G=2,W=1,N=3,L=5,F=.01,X=.02,U=.04,V=.15,J=.4,Q=.5,Z=.1,ee=2,te=new Set(["ring","quadrant","sector","reticle-position","test-site","cluster","edge-arc"]);function ie(e){const n=e.highlight;return"dieKeysByWafer"in n&&n.dieKeysByWafer?Object.values(n.dieKeysByWafer).reduce((i,a)=>i+a.length,0):"dieKeys"in n&&n.dieKeys&&n.dieKeys.length>0?n.dieKeys.length:e.level==="lot"?void 0:e.stats.sampleSizeLeft}function ne(e){const n=e.highlight,i=new Set;if("dieKeysByWafer"in n&&n.dieKeysByWafer)for(const[a,l]of Object.entries(n.dieKeysByWafer))for(const t of l)i.add(`${a}:${t}`);else if("dieKeys"in n&&n.dieKeys)for(const a of n.dieKeys)i.add(a);return i}const h=e=>Math.abs(e.effect.absoluteDelta??0),S=e=>h(e)*(ie(e)??0),I=e=>(e*100).toFixed(1),H=e=>{const n=e.variable.label.replace(/ \(same dies\)$/,"").replace(/^HBin and SBin (\d+)/,"HBin $1").replace(/^(HBin \d+(?: \([^)]*\))?) and SBin .*$/,"$1");return Y(n)},g=e=>Math.round(e).toLocaleString("en-GB"),m=(e,n,i=`${n}s`)=>`${e} ${e===1?n:i}`;function M(e){const n=e.comparison.left;return e.comparison.family==="quadrant"&&!/quadrant/i.test(n)?`${n} quadrant`:n}function se(e){const n=e.map((t,s)=>s),i=t=>n[t]===t?t:n[t]=i(n[t]),a=e.map(ne);for(let t=0;t<e.length;t++)for(let s=t+1;s<e.length;s++){const o=a[t].size<=a[s].size?a[t]:a[s],w=o===a[t]?a[s]:a[t];if(o.size===0)continue;let f=0;for(const c of o)w.has(c)&&f++;f>=Q*o.size&&(n[i(t)]=i(s))}const l=new Map;return e.forEach((t,s)=>{const o=i(s);(l.get(o)??l.set(o,[]).get(o)).push(s)}),[...l.values()]}const oe=e=>e.variable.kind==="hardBin"||e.variable.kind==="softBin",K=(e,n)=>e.level===n.level&&e.comparison.family===n.comparison.family&&e.comparison.left===n.comparison.left;function ae(e,n,i){if(!i)return[];const a=t=>n.filter(s=>s.variable.kind===t&&K(s,e)&&s.effect.direction==="higher"&&!(t==="hardBin"&&i.includes(s.variable.bin??NaN)));return(a("hardBin").length>0?a("hardBin"):a("softBin")).filter(t=>t!==e).sort((t,s)=>h(s)-h(t))}function re(e,n,i,a){const l=e.filter(d=>d.variable.kind==="yield"),t=[...l.length>0?l:e].sort((d,p)=>S(p)-S(d))[0],s=e.filter(d=>d!==t),o=[],w=t.level==="lot"&&"waferIndices"in t.highlight?t.highlight.waferIndices?.length:void 0,f=w!==void 0&&i?` on ${w}/${i} wafers`:"",c=oe(t),$=(d,p)=>({text:d,target:{kind:"finding",id:p.id}});o.push($(M(t),t)),o.push({text:c?"":`: pass rate ${I(h(t))} points below the rest of the wafer${f}`}),c&&o.push({text:": "},$(H(t),t),{text:` ${I(h(t))} points higher than the rest of the wafer${f}`});const y=ae(t,n,a).map(d=>({f:d,share:h(d)/(h(t)||1)})).filter(d=>c||d.share>=Z).slice(0,ee);if(c)for(const{f:d}of y)o.push({text:", "},$(H(d),d),{text:` ${I(h(d))} points higher`});else if(y.length>0){const d=y[0].share>.5&&y[0].share<=1?"mostly ":"";y.forEach(({f:p,share:v},b)=>{const x=v<=1;b===0?o.push({text:`; ${x?d:""}`}):o.push({text:", "}),o.push($(H(p),p)),o.push({text:b===0?x?` accounts for ${I(h(p))} of those points`:` is ${I(h(p))} points higher`:x?` for ${I(h(p))}`:` ${I(h(p))} points higher`})})}o.push({text:` \u2014 about ${g(S(t))} dies lost`});const B=[...new Set(s.filter(d=>!K(d,t)).map(M))];B.length>0&&o.push({text:` (also seen as ${B.join(", ")})`});const _=c?H(t):"pass rate";return{impact:"low",diesLost:S(t),severity:t.severity,parts:o,findingIds:[t.id,...s.map(d=>d.id)],brief:{text:`${M(t)}: ${_}, ${g(S(t))} dies`,target:{kind:"finding",id:t.id}}}}function le(e,n){return e>=U||n>=J?"high":e>=X||n>=V?"medium":"low"}function de(e){const n=i=>i.stats.yieldPercent==null?0:i.stats.analyzedDies*(1-i.stats.yieldPercent/100);return e.level==="wafer"?n(e):e.perWafer.reduce((i,a)=>i+n(a.summary),0)}function fe(e){return e.level==="wafer"?e.stats.analyzedDies:e.perWafer.reduce((n,i)=>n+i.summary.stats.analyzedDies,0)}const O=e=>e.id.startsWith("inter-wafer:yield:");function ce(e){const n=[];for(const i of e.findings){if(!O(i)||i.effect.direction!=="lower")continue;const a="waferIndices"in i.highlight?i.highlight.waferIndices?.[0]:void 0,l=a===void 0?void 0:e.perWafer.find(t=>t.waferIndex===a)?.summary.stats.analyzedDies;l!==void 0&&n.push({impact:"low",diesLost:h(i)*l,severity:i.severity,parts:[{text:i.summary.replace(/ percentage points lower than the lot median$/," points below the lot median"),target:{kind:"finding",id:i.id}},{text:` \u2014 about ${g(h(i)*l)} dies lost`}],findingIds:[i.id],brief:{text:`${i.comparison.left}: yield, ${g(h(i)*l)} dies`,target:{kind:"finding",id:i.id}}})}return n}function he(e,n){const i=e.stats,a=[],l=(t,s,o,w,f,c)=>{if(o<=0||w<=0)return;n.add(t);const $=o/w*100;a.push({impact:"low",diesLost:o,severity:"info",parts:[{text:s},{text:` ${f} on ${g(o)} of ${g(w)} dies (${$.toFixed(1)}%)`}],findingIds:[],brief:{text:`${s}: ${g(o)} dies ${c}`}})};for(const t of i.testSpecYield??[]){const s=t.failLowDies+t.failHighDies,o=t.failHighDies===0?"is below its low limit":t.failLowDies===0?"is above its high limit":`is outside its limits (${g(t.failLowDies)} low, ${g(t.failHighDies)} high)`;l(t.testNumber,t.label,s,t.totalDies,o,"outside limits")}for(const t of i.functionalYield??[])l(t.testNumber,t.label,t.failDies,t.totalDies,"fails","fail");return a}function pe(e,n){const i=[],a={unusual:0,notable:1,info:2},l=e.findings.filter(s=>s.id.startsWith("drift:")).sort((s,o)=>a[s.severity]-a[o.severity]||(s.stats.adjustedPValue??1)-(o.stats.adjustedPValue??1));for(const s of l){const o=s.variable.label;i.push({parts:[{text:o,target:{kind:"finding",id:s.id}},{text:s.summary.slice(o.length)}],findingIds:[s.id]})}const t=(e.stats.capability??[]).filter(s=>s.hasSpec&&s.ppk!==null&&s.ppk!==void 0&&s.ppk<W&&!n.has(s.testNumber)).sort((s,o)=>(s.ppk??0)-(o.ppk??0));for(const s of t)i.push({parts:[{text:`${s.label} has Ppk ${s.ppk.toFixed(2)} against its limits (below ${W.toFixed(1)})`}],findingIds:[]});return i.slice(0,G)}function ue(e,n){const i=n.passBins&&n.passBins.length>0?`; pass ${T([n.passBins])}`:"";if(e.level==="wafer"){const f=e.stats.yieldPercent;return f==null?[{text:`${g(e.stats.totalDies)} dies, no bin data to judge yield by.`}]:[{text:`Yield ${f.toFixed(1)}% (${g(e.stats.totalDies)} dies${i}).`}]}const a=e.lotYieldSeries.map(f=>f.yieldPercent).filter(f=>typeof f=="number");if(a.length===0)return[{text:`${m(e.stats.waferCount,"wafer")}, no bin data to judge yield by.`}];const l=a.reduce((f,c)=>f+c,0)/a.length,t=e.perWafer.reduce((f,c)=>f+c.summary.stats.totalDies,0),[s,o]=[P(a).toFixed(1),C(a).toFixed(1)],w=s!==o?`, range ${s}\u2013${o}%`:"";return[{text:`Yield ${l.toFixed(1)}% (mean of ${m(a.length,"wafer")}, ${g(t)} dies${i})${w}.`}]}function ge(e,n){const i=[],a=e.level==="wafer"?e.stats.testsConsidered.length:new Set(e.perWafer.flatMap(o=>o.summary.stats.testsConsidered)).size,l=e.level==="wafer"?e.stats.hardBinsConsidered.length:new Set(e.perWafer.flatMap(o=>o.summary.stats.hardBinsConsidered)).size;if(a>0&&i.push(m(a,"test")),l>0&&i.push(m(l,"hard bin")),i.push("the ring, quadrant and sector regions"),e.level==="lot"&&e.stats.waferCount>=3){const o=e.findings.filter(O);i.push(o.length===0?"wafer-to-wafer yield (no outlier wafers)":`wafer-to-wafer yield (${m(o.length,"outlier wafer")})`),e.stats.waferCount>=q&&i.push("the yield trend across the wafers (input order)")}const t=e.findings.filter(o=>!n.has(o.id)).length,s=t>0?` ${m(t,"smaller finding")} not listed above.`:" Nothing else was found.";return`Compared: ${i.join(", ")}.${s}`}export function buildSynthesis(e,n={}){const i=e.findings,a=e.level==="lot"?e.stats.waferCount:void 0,l=n.passBins,t=new Map(i.map(r=>[r.id,r])),s=r=>l!==void 0&&(r.variable.kind==="hardBin"?!l.includes(r.variable.bin??NaN):r.variable.kind==="softBin"),o=i.filter(r=>te.has(r.comparison.family)&&S(r)>0&&(r.variable.kind==="yield"&&r.effect.direction==="lower"||s(r)&&r.effect.direction==="higher")),f=o.some(r=>r.variable.kind==="hardBin")?o.filter(r=>r.variable.kind!=="softBin"):o,c=[],$=new Set;for(const r of se(f))c.push(re(r.map(u=>f[u]),i,a,l));for(const r of i.filter(u=>u.variable.kind==="spatialPattern")){const u=new Set((r.relatedIds??[]).filter(k=>t.has(k))),D=c.find(k=>k.findingIds.some(z=>u.has(z)));if(!D)continue;const A=r.level==="lot"&&"waferIndices"in r.highlight?r.highlight.waferIndices?.length:void 0,j=A!==void 0&&a?` on ${A}/${a} wafers`:"";D.parts.unshift({text:`${r.variable.label} pattern${j}. `,target:{kind:"finding",id:r.id}}),D.findingIds.unshift(r.id,...[...u].filter(k=>!D.findingIds.includes(k)))}e.level==="lot"&&c.push(...ce(e));const y=new Set;c.push(...he(e,y));const B=fe(e),_=de(e),d=c.filter(r=>B>0&&r.diesLost/B>=F);for(const r of d)r.impact=le(r.diesLost/B,_>0?r.diesLost/_:0);d.sort((r,u)=>u.diesLost-r.diesLost);const p=d.slice(0,N),v=d.slice(N);for(const r of[...p,...v])for(const u of r.findingIds)$.add(u);let b;v.length>0&&(b=[{text:"Also over a yield point: "}],v.slice(0,L).forEach((r,u)=>{u>0&&b.push({text:"; "}),b.push(r.brief)}),v.length>L&&b.push({text:`; and ${v.length-L} more`}),b.push({text:"."}));const x=pe(e,y);for(const r of x)for(const u of r.findingIds)$.add(u);const R=e.level==="lot"?"region, fail bin or wafer":"region or fail bin",E=p.length===0?`Nothing stands out: no ${R} costs as much as ${I(F)} yield points.`:void 0;return{headline:ue(e,n),items:p,...b?{also:b}:{},...x.length?{watch:x}:{},...E?{nothing:E}:{},checked:ge(e,$)}}export function synthesisText(e){const n=i=>i.map(a=>a.text).join("");return[n(e.headline),...e.nothing?[e.nothing]:[],...e.items.map((i,a)=>`${a+1}. ${n(i.parts)}`),...e.also?[n(e.also)]:[],...e.watch?e.watch.map(i=>`Watch: ${n(i.parts)}`):[],e.checked].join(`
2
+ `)}
@@ -1 +1 @@
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}
1
+ import{isYieldEligibleDie as x}from"../core/dies.js";import{dieHasVerdicts as M,testValuesReader as S}from"../core/dieTable.js";import{isParametricTest as R,getTestPassStatus as E,testPassStatusReader as z}from"../renderer/buildWaferMap.js";import{classifySpec as C}from"../renderer/spec.js";import{testLabel as I,derivedFields as J}from"../renderer/testLabel.js";const K=(s,o)=>o===0?null:s/o*100;export function buildTestPassRateData(s,o,n){const r=s.map(e=>e.key),t={groups:r,rows:[],disagreementDies:null};if(!o?.length)return t;const i=o.filter(e=>e.testNumber!==void 0&&G(e,n));if(!i.length)return t;const v=()=>({pass:0,fail:0,failLow:0,failHigh:0}),g=new Map;for(const e of i){const l=e.testNumber;g.set(l,{label:I(e,l),overall:v(),perGroup:r.map(()=>v())})}const N=(e,l,u)=>{const f=g.get(e);if(f)for(const p of[f.overall,f.perGroup[l]])p.pass+=u.pass??0,p.fail+=u.fail??0,p.failLow+=u.failLow??0,p.failHigh+=u.failHigh??0},T=S(i.map(e=>e.testNumber)),L=new Float64Array(i.length),V=z(i),h=new Int8Array(i.length),H=n==="spec"||n==="testFlag";let y=0,P=0;s.forEach(function(l,u){for(const f of l.items){if(n==="spec"&&f.testSpecYield&&!O(f)){for(const a of f.testSpecYield)N(a.testNumber,u,{pass:a.passDies,fail:a.failLowDies+a.failHighDies,failLow:a.failLowDies,failHigh:a.failHighDies});continue}if(n==="functional"&&f.functionalYield){for(const a of f.functionalYield)N(a.testNumber,u,{pass:a.passDies,fail:a.failDies});continue}const p=i.map(a=>g.get(a.testNumber).overall),F=i.map(a=>g.get(a.testNumber).perGroup[u]);for(const a of f.dies??[])if(x(a)){T(a,L),V(a,h);for(let c=0;c<i.length;c++){const A=i[c],D=C(L[c],A),b=h[c]===-1?void 0:h[c]===1;H&&D!==null&&b!==void 0&&(y++,D==="pass"!==b&&P++);const m=p[c],d=F[c];if(n==="spec"){if(D===null)continue;if(D==="pass"){m.pass++,d.pass++;continue}m.fail++,d.fail++,D==="failLow"?(m.failLow++,d.failLow++):(m.failHigh++,d.failHigh++)}else{if(b===void 0)continue;b?(m.pass++,d.pass++):(m.fail++,d.fail++)}}}}});const Y=e=>({passDies:e.pass,failDies:e.fail,totalDies:e.pass+e.fail,passRatePercent:K(e.pass,e.pass+e.fail)}),w=[];for(const[e,l]of g)l.overall.pass+l.overall.fail!==0&&w.push({testNumber:e,label:l.label,overall:Y(l.overall),byGroup:l.perGroup.map(Y),...n==="spec"?{failLowDies:l.overall.failLow,failHighDies:l.overall.failHigh}:{}});return w.sort((e,l)=>(e.overall.passRatePercent??101)-(l.overall.passRatePercent??101)||e.testNumber-l.testNumber),{groups:r,rows:w,disagreementDies:H&&y>0?P:null}}function O(s){return(s.dies??[]).some(M)}function G(s,o){return o==="functional"?!R(s):R(s)?o==="spec"?s.limitLow!==void 0||s.limitHigh!==void 0:!0:!1}export function hasJudgeableTests(s,o,n){if(!s?.length)return!1;const r=s.filter(t=>t.testNumber!==void 0&&G(t,o));return r.length?o!=="testFlag"?!0:(n??[]).some(t=>r.some(i=>E(t,i.testNumber,i)!==void 0)):!1}export function poolFunctionalYield(s){if(!s?.length||!s.every(r=>r.stats.functionalYield!==void 0))return;const o=new Map;for(const r of s)for(const t of r.stats.functionalYield??[]){const i=o.get(t.testNumber)??{label:t.label,...J(t),passDies:0,failDies:0,totalDies:0};i.passDies+=t.passDies,i.failDies+=t.failDies,i.totalDies+=t.totalDies,o.set(t.testNumber,i)}const n=[...o.entries()].map(([r,t])=>({testNumber:r,...t,passRatePercent:t.totalDies>0?t.passDies/t.totalDies*100:null}));return n.length?n:void 0}
@@ -1 +1 @@
1
- import{isYieldEligibleDie as f}from"../core/dies.js";export function buildTestTrendData(s,u){return s.map((n,t)=>{const c=n.label??`#${t}`,i=n.testStats?.find(e=>e.testNumber===u);if(i){const{mean:e,stddev:o,count:d}=i;return{label:c,mean:e,stddev:o,count:d,key:n.key}}const r=(n.dies??[]).filter(e=>f(e)).map(e=>e.testValues?.[u]).filter(e=>e!==void 0&&Number.isFinite(e));if(r.length===0)return{label:c,mean:NaN,stddev:0,count:0,key:n.key};const l=r.reduce((e,o)=>e+o,0)/r.length,a=r.length>1?r.reduce((e,o)=>e+(o-l)**2,0)/(r.length-1):0;return{label:c,mean:l,stddev:Math.sqrt(a),count:r.length,key:n.key}})}export function trendCentre(s){let u=0,n=0;for(const t of s)!t.count||!Number.isFinite(t.mean)||(u+=t.count,n+=t.mean*t.count);return u===0?null:n/u}
1
+ import{isYieldEligibleDie as f}from"../core/dies.js";import{testValue as m}from"../core/dieTable.js";export function buildTestTrendData(i,o){return i.map((t,n)=>{const s=t.label??`#${n}`,c=t.testStats?.find(e=>e.testNumber===o);if(c){const{mean:e,stddev:u,count:d}=c;return{label:s,mean:e,stddev:u,count:d,key:t.key}}const r=(t.dies??[]).filter(e=>f(e)).map(e=>m(e,o)).filter(e=>e!==void 0&&Number.isFinite(e));if(r.length===0)return{label:s,mean:NaN,stddev:0,count:0,key:t.key};const l=r.reduce((e,u)=>e+u,0)/r.length,a=r.length>1?r.reduce((e,u)=>e+(u-l)**2,0)/(r.length-1):0;return{label:s,mean:l,stddev:Math.sqrt(a),count:r.length,key:t.key}})}export function trendCentre(i){let o=0,t=0;for(const n of i)!n.count||!Number.isFinite(n.mean)||(o+=n.count,t+=n.mean*n.count);return o===0?null:t/o}
@@ -30,6 +30,11 @@ export interface HighlightBinTarget {
30
30
  export interface HighlightWaferTarget {
31
31
  kind: 'wafer';
32
32
  waferIndices: number[];
33
+ /**
34
+ * For a lot finding about a region, that region's dies on each counted wafer,
35
+ * keyed by wafer index — what to highlight on each wafer's map.
36
+ */
37
+ dieKeysByWafer?: Record<number, string[]>;
33
38
  }
34
39
  export interface HighlightDieTarget {
35
40
  kind: 'dies';
@@ -515,7 +520,7 @@ export interface AnalyzeWaferMapOptions {
515
520
  * Off by default. Implied by `enableTestValueAnalysis`.
516
521
  */
517
522
  computePerTestStats?: boolean;
518
- /** Number of angular sectors for sector analysis. Must be 4, 8, 16, or 32. Default 8. */
523
+ /** Number of angular sectors for sector analysis. Must be 4, 8 or 16. Default 8. */
519
524
  sectorCount?: number;
520
525
  /**
521
526
  * Restrict test value analysis to a specific subset of test numbers.
@@ -0,0 +1,31 @@
1
+ import type { RegionYieldDatum } from './regions.js';
2
+ /** A row with its signed difference from the reference, in yield points, and how strongly to tint it. */
3
+ export type ReadAgainst<T> = T & {
4
+ delta: number;
5
+ step: 0 | 1 | 2 | 3;
6
+ };
7
+ /** A wafer row also says whether the lot's outlier rule calls it one, and on which side. */
8
+ export type WaferRead<T> = ReadAgainst<T> & {
9
+ outlier?: 'low' | 'high';
10
+ };
11
+ /**
12
+ * Wafers against the median of the wafers' own yields; `median` is that reference. A wafer is an
13
+ * outlier by `outlierWafers`, the rule the lot's findings use, so a wafer a table calls an outlier is
14
+ * one the findings name, and the other way round. It is said in words ("low outlier"), never by tint
15
+ * alone: the tint follows the size of the shortfall, the word follows the statistics.
16
+ */
17
+ export declare function waferYieldRows<T extends {
18
+ yieldPercent: number;
19
+ }>(wafers: readonly T[]): {
20
+ rows: WaferRead<T>[];
21
+ median: number;
22
+ };
23
+ /**
24
+ * Regions against the yield of everything they divide. Rings, or quadrants, partition the wafer (or
25
+ * the lot's wafers), so their dies sum to it and `overall` is its yield, die-weighted.
26
+ */
27
+ export declare function regionYieldRows(data: readonly RegionYieldDatum[]): {
28
+ rows: ReadAgainst<RegionYieldDatum>[];
29
+ overall: number;
30
+ };
31
+ //# sourceMappingURL=yieldRows.d.ts.map
@@ -0,0 +1 @@
1
+ import{medianOfSorted as c}from"../core/utils.js";import{shortfallStep as s}from"./presentation.js";import{outlierWafers as d}from"./analyzeWaferLot.js";export function waferYieldRows(r){const n=r.map(t=>t.yieldPercent),o=[...n].sort((t,l)=>t-l),e=o.length===0?0:c(o),i=new Set(d(n)?.outliers.map(t=>t.index));return{median:e,rows:r.map((t,l)=>({...t,delta:t.yieldPercent-e,step:s(t.yieldPercent-e),...i.has(l)?{outlier:t.yieldPercent<e?"low":"high"}:{}}))}}export function regionYieldRows(r){const n=r.reduce((e,i)=>e+i.n,0),o=n===0?0:r.reduce((e,i)=>e+i.passDies,0)/n*100;return{overall:o,rows:r.map(e=>({...e,delta:e.yieldPercent-o,step:s(e.yieldPercent-o)}))}}
@@ -1 +1 @@
1
- export function createWafermapWorker(n){let i=0;const s=new Map;return n.onmessage=t=>{const e=t.data;if(e.type==="pong")return;const r=s.get(e.id);r&&(s.delete(e.id),e.type==="error"?r.reject(new Error(e.message)):e.type==="result"?r.resolve(e.result):e.type==="analyzed"?r.resolve({waferSummaries:e.waferSummaries,lotSummary:e.lotSummary}):e.type==="resultWithAnalysis"&&r.resolve({results:e.results,waferSummaries:e.waferSummaries,lotSummary:e.lotSummary}))},n.onerror=t=>{const e=new Error(t.message??"Worker error");for(const r of s.values())r.reject(e);s.clear()},{run(t){return new Promise((e,r)=>{const a=i++;s.set(a,{resolve:e,reject:r}),n.postMessage({type:"run",id:a,input:t})})},runAnalysis(t,e,r){return new Promise((a,l)=>{const o=i++;s.set(o,{resolve:a,reject:l}),n.postMessage({type:"analyze",id:o,results:t,options:e,hasMultiWafer:r})})},runWithAnalysis(t,e,r){return new Promise((a,l)=>{const o=i++;s.set(o,{resolve:a,reject:l}),n.postMessage({type:"runWithAnalysis",id:o,inputs:t,options:e,hasMultiWafer:r})})},terminate(){n.terminate();const t=new Error("Worker terminated");for(const e of s.values())e.reject(t);s.clear()}}}
1
+ import{attachTables as m}from"../core/dieTable.js";export function createWafermapWorker(a){let i=0;const s=new Map;return a.onmessage=t=>{const e=t.data;if(e.type==="pong")return;const r=s.get(e.id);r&&(s.delete(e.id),e.type==="error"?r.reject(new Error(e.message)):e.type==="result"?(m([e.result.dies],e.tables),r.resolve(e.result)):e.type==="analyzed"?r.resolve({waferSummaries:e.waferSummaries,lotSummary:e.lotSummary}):e.type==="resultWithAnalysis"&&(m(e.results.map(n=>n.dies),e.tables),r.resolve({results:e.results,waferSummaries:e.waferSummaries,lotSummary:e.lotSummary})))},a.onerror=t=>{const e=new Error(t.message??"Worker error");for(const r of s.values())r.reject(e);s.clear()},{run(t){return new Promise((e,r)=>{const n=i++;s.set(n,{resolve:e,reject:r}),a.postMessage({type:"run",id:n,input:t})})},runAnalysis(t,e,r){return new Promise((n,l)=>{const o=i++;s.set(o,{resolve:n,reject:l}),a.postMessage({type:"analyze",id:o,results:t,options:e,hasMultiWafer:r})})},runWithAnalysis(t,e,r){return new Promise((n,l)=>{const o=i++;s.set(o,{resolve:n,reject:l}),a.postMessage({type:"runWithAnalysis",id:o,inputs:t,options:e,hasMultiWafer:r})})},terminate(){a.terminate();const t=new Error("Worker terminated");for(const e of s.values())e.reject(t);s.clear()}}}
@@ -1,5 +1,6 @@
1
1
  import type { WaferMapInput, WaferMapResult } from '../renderer/buildWaferMap.js';
2
2
  import type { AnalyzeWaferMapOptions, StatsSummary, LotStatsSummary } from '../stats/index.js';
3
+ import type { DetachedTables } from '../core/dieTable.js';
3
4
  export type WorkerRequest = {
4
5
  type: 'run';
5
6
  id: number;
@@ -23,6 +24,7 @@ export type WorkerResponse = {
23
24
  type: 'result';
24
25
  id: number;
25
26
  result: WaferMapResult;
27
+ tables: DetachedTables;
26
28
  } | {
27
29
  type: 'analyzed';
28
30
  id: number;
@@ -34,6 +36,7 @@ export type WorkerResponse = {
34
36
  results: WaferMapResult[];
35
37
  waferSummaries: StatsSummary[];
36
38
  lotSummary: LotStatsSummary | null;
39
+ tables: DetachedTables;
37
40
  } | {
38
41
  type: 'error';
39
42
  id: number;
@@ -1 +1 @@
1
- import{buildWaferMap as o}from"../renderer/buildWaferMap.js";import{analyzeWaferMap as i,analyzeWaferLot as n}from"../stats/index.js";self.onmessage=p=>{const e=p.data;if(e.type==="ping"){self.postMessage({type:"pong"});return}if(e.type==="run")try{const s=o(e.input);self.postMessage({type:"result",id:e.id,result:s})}catch(s){const t=s instanceof Error?s.message:String(s);self.postMessage({type:"error",id:e.id,message:t})}if(e.type==="analyze")try{const s=e.results.map(r=>i(r,e.options)),t=e.hasMultiWafer?n(e.results,{...e.options,perWaferSummaries:s}):null;self.postMessage({type:"analyzed",id:e.id,waferSummaries:s,lotSummary:t})}catch(s){const t=s instanceof Error?s.message:String(s);self.postMessage({type:"error",id:e.id,message:t})}if(e.type==="runWithAnalysis")try{const s=e.inputs.map(a=>o(a)),t=s.map(a=>i(a,e.options)),r=e.hasMultiWafer?n(s,{...e.options,perWaferSummaries:t}):null;self.postMessage({type:"resultWithAnalysis",id:e.id,results:s,waferSummaries:t,lotSummary:r})}catch(s){const t=s instanceof Error?s.message:String(s);self.postMessage({type:"error",id:e.id,message:t})}};
1
+ import{buildWaferMap as o}from"../renderer/buildWaferMap.js";import{analyzeWaferMap as i,analyzeWaferLot as n}from"../stats/index.js";import{detachTables as p}from"../core/dieTable.js";self.onmessage=l=>{const s=l.data;if(s.type==="ping"){self.postMessage({type:"pong"});return}if(s.type==="run")try{const e=o(s.input),{transfer:t,...r}=p([e.dies]);self.postMessage({type:"result",id:s.id,result:e,tables:r},t)}catch(e){const t=e instanceof Error?e.message:String(e);self.postMessage({type:"error",id:s.id,message:t})}if(s.type==="analyze")try{const e=s.results.map(r=>i(r,s.options)),t=s.hasMultiWafer?n(s.results,{...s.options,perWaferSummaries:e}):null;self.postMessage({type:"analyzed",id:s.id,waferSummaries:e,lotSummary:t})}catch(e){const t=e instanceof Error?e.message:String(e);self.postMessage({type:"error",id:s.id,message:t})}if(s.type==="runWithAnalysis")try{const e=s.inputs.map(a=>o(a)),t=e.map(a=>i(a,s.options)),r=s.hasMultiWafer?n(e,{...s.options,perWaferSummaries:t}):null,{transfer:m,...f}=p(e.map(a=>a.dies));self.postMessage({type:"resultWithAnalysis",id:s.id,results:e,waferSummaries:t,lotSummary:r,tables:f},m)}catch(e){const t=e instanceof Error?e.message:String(e);self.postMessage({type:"error",id:s.id,message:t})}};
package/llms.txt CHANGED
@@ -20,7 +20,7 @@ where repo-relative paths do not resolve.
20
20
  - [API Reference](https://wafertools.github.io/wafermap/api/) - public API, input types, coordinate system rules, and configuration details.
21
21
  - [Quick start](https://wafertools.github.io/wafermap/quickstart/) - install and first map.
22
22
  - [Troubleshooting](https://wafertools.github.io/wafermap/troubleshooting/) - common failure modes.
23
- - [Examples](https://wafertools.github.io/wafermap/examples/) - 22 runnable demo pages, also downloadable at https://wafertools.github.io/wafermap/wafermap-examples.zip to run offline.
23
+ - [Examples](https://wafertools.github.io/wafermap/examples/) - 23 runnable demo pages, also downloadable at https://wafertools.github.io/wafermap/wafermap-examples.zip to run offline.
24
24
 
25
25
  ## Public package entry points
26
26
  - `@wafertools/wafermap` - `buildWaferMap()`, geometry, colour-scheme registries. Pure, no DOM.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wafertools/wafermap",
3
- "version": "0.31.0",
3
+ "version": "0.33.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",
@@ -1,3 +0,0 @@
1
- import type { StatsFinding } from './types.js';
2
- export declare function buildFindingsNarrative(findings: StatsFinding[]): string;
3
- //# sourceMappingURL=findingsNarrative.d.ts.map
@@ -1 +0,0 @@
1
- import{minOf as y}from"../core/utils.js";const S={unusual:0,notable:1,info:2},b={E:0,ENE:22.5,NE:45,NNE:67.5,N:90,NNW:112.5,NW:135,WNW:157.5,W:180,WSW:202.5,SW:225,SSW:247.5,S:270,SSE:292.5,SE:315,ESE:337.5};function m(e){return e.stats.adjustedPValue??e.stats.pValue??1}function w(e){return[...e].sort((n,t)=>{const o=S[n.severity]-S[t.severity];return o!==0?o:m(n)-m(t)})[0]}function v(e){return e.some(t=>t.variable.kind==="yield")?e.filter(t=>t.variable.kind!=="hardBin"&&t.variable.kind!=="softBin"):e}function $(e){return e.replace(/^(Rings?|Quadrants?|Sectors?)\s+/i,"").split(/[,&]|–|\band\b/).map(n=>n.trim()).filter(Boolean)}function R(e,n){const t=new Set($(e));return $(n).some(o=>t.has(o))}function E(e){return e.variable.kind==="yield"?"yield":e.variable.kind==="hardBin"?`HBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.kind==="softBin"?`SBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.label}function h(e){let n=0,t=0;for(const o of e)o.effect.direction==="higher"?n++:o.effect.direction==="lower"&&t++;return n>t?"higher":t>n?"lower":"mixed"}function d(e){return e==="higher"?"elevated":e==="lower"?"reduced":"shifted"}function p(e){const n=new Set,t=[];for(const o of e){const r=E(o);n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:t.length===3?`${t[0]}, ${t[1]}, and ${t[2]}`:`${t[0]}, ${t[1]}, and ${t.length-2} more`}function k(e){const n=new Set,t=[];for(const o of e){const r=o.comparison.left;n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`}function B(e){return e.length!==1?!1:!/^(Rings|Quadrants|Sectors)\b/.test(e[0].comparison.left)}function I(e){const n=h(e),t=e.every(c=>c.comparison.left.includes("(edge)")),o=e.every(c=>c.comparison.left.includes("(core)"));let r,a;return o?(r="The core ring",a=!0):t&&e.length>1?(r="The edge rings",a=!1):(r=k(e),a=B(e)),`${r} show${a?"s":""} ${d(n)} ${p(e)}.`}function P(e){const n=h(e),t=s=>s.highlight.kind==="region"&&s.highlight.regionKeys.length>1||s.comparison.left.startsWith("Quadrants"),o=e.filter(s=>t(s)),r=e.filter(s=>!t(s)),a=[];r.length===1?a.push(`the ${r[0].comparison.left} quadrant`):r.length>1&&a.push(`the ${k(r)} quadrants`);for(const s of o)a.push(s.comparison.left.replace(/^Quadrants/,"quadrants"));const c=a.join(" and "),u=r.length===1&&o.length===0;return`${c.charAt(0).toUpperCase()+c.slice(1)} show${u?"s":""} ${d(n)} ${p(e)}.`}function x(e){const n=h(e),t=[],o=new Set;for(const u of e){const i=u.comparison.left.replace(/^Sectors? /,"");o.has(i)||(o.add(i),t.push(i))}let r,a;return t.length===1?(r=`the ${t[0]} ${e[0].comparison.left.startsWith("Sectors")?"sectors":"region"}`,a=!e[0].comparison.left.startsWith("Sectors")):(r=`the ${t.length===2?t.join(" and "):`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`} sectors`,a=!1),`${r.charAt(0).toUpperCase()+r.slice(1)} show${a?"s":""} ${d(n)} ${p(e)}.`}function C(e){if(e.length===1)return`A failure cluster at ${e[0].comparison.left.replace(/^Cluster at /,"")}.`;const t=e.reduce((o,r)=>(o.stats.sampleSizeLeft??0)>=(r.stats.sampleSizeLeft??0)?o:r).comparison.left.replace(/^Cluster at /,"");return`${e.length} failure clusters identified; the largest at ${t}.`}function q(e){const n=e[0].comparison.left.replace(/^Edge arc ~/,"");return e.length===1?`An edge arc near ${n} shows localised failures.`:`${e.length} edge arcs detected, including near ${n}.`}function F(e){const n=h(e);return e.length===1?`Reticle-position variation: ${e[0].comparison.left} shows ${d(n)} ${p(e)}.`:`Reticle-position variation across ${e.length} cells (${d(n)} ${p(e)}).`}function O(e,n){const t=e.stats.method==="geometry"?` (${e.severity==="unusual"?"high":e.severity==="notable"?"medium":"low"} confidence)`:"",o=new Set(e.relatedIds??[]),r=n.find(c=>o.has(c.id)&&(c.comparison.family==="edge-arc"||c.comparison.family==="cluster"));let a="";return r&&(r.comparison.family==="edge-arc"?a=`; failures concentrated at the wafer edge near ${r.comparison.left.replace(/^Edge arc ~/,"")}`:a=`; failures concentrated near ${r.comparison.left.replace(/^Cluster at /,"")}`),`${e.comparison.left} failure pattern detected${t}${a}.`}function Y(e){const n=e.replace(/\.$/,"");return n.charAt(0).toLowerCase()+n.slice(1)}function N(e){const n=e.filter(t=>t.variable.kind==="yield");return n.length>0?h(n):h(e)}function M(e,n){const t=new Map;for(const i of n){const s=t.get(i.comparison.left)??[];s.push(i),t.set(i.comparison.left,s)}const o=[],r=[];for(const i of t.values()){const s=N(i);s==="higher"?o.push(...i.filter(l=>l.effect.direction==="higher")):s==="lower"?r.push(...i.filter(l=>l.effect.direction==="lower")):o.push(...i)}const a=o.map(i=>i.comparison.left),c=r.filter(i=>!a.some(s=>R(s,i.comparison.left)));if(o.length>0&&c.length>0){const i=n.some(f=>f.variable.kind==="yield"),[s,l]=i?[c,o]:[o,c];return`${e(s).replace(/\.$/,"")} while ${Y(e(l))}.`}if(o.length>0&&r.length>0&&c.length===0)return e(o);const u=N(n);if(u!=="mixed"&&n.some(i=>i.variable.kind==="yield")){const i=n.filter(s=>s.effect.direction===u);if(i.length>0)return e(i)}return e(n)}function W(e){let n=0,t=0,o=0;for(const r of e)for(const a of $(r.comparison.left)){const c=b[a];if(c===void 0)continue;const u=c*Math.PI/180;n+=Math.cos(u),t+=Math.sin(u),o++}return o===0||n===0&&t===0?null:(Math.atan2(t,n)*180/Math.PI+360)%360}function j(e){let n="E",t=360;for(const[o,r]of Object.entries(b)){const a=Math.abs((e-r+540)%360-180);a<t&&(t=a,n=o)}return n}function D(e){const n=e.filter(u=>u.effect.direction==="higher"),t=e.filter(u=>u.effect.direction==="lower");if(n.length===0||t.length===0)return null;const o=new Set(e.map(E));if(o.size!==1)return null;const r=W(n),a=W(t);return r===null||a===null||Math.abs((r-a+540)%360-180)<120?null:`${[...o][0]} increases from ${j(a)} toward ${j(r)} across the wafer.`}function T(e){const n=v(e);if(n.length===0)return"";const t=["sector","quadrant","ring"],o=new Map;for(const i of n){const s=o.get(i.comparison.family)??[];s.push(i),o.set(i.comparison.family,s)}const r=[...o.keys()].sort((i,s)=>{const l=y(o.get(i).map(m)),f=y(o.get(s).map(m));return l!==f?l-f:t.indexOf(i)-t.indexOf(s)})[0],a=o.get(r),c=D(a);return c||M(r==="ring"?I:r==="quadrant"?P:x,a)}function L(e,n){switch(e){case"cluster":return C(n);case"edge-arc":return q(n);case"reticle-position":return M(F,n);default:return""}}const A=new Set(["ring","quadrant","sector"]);export function buildFindingsNarrative(e){if(e.length===0)return"";const n=e.filter(s=>s.severity!=="info");if(n.length===0)return"Minor spatial variation detected; no strongly significant patterns.";const t=[],o=n.filter(s=>s.comparison.family==="spatial-pattern"),r=o.length?w(o):null,a=new Set(r?.relatedIds??[]);r&&t.push(O(r,n));const c=n.filter(s=>s!==r&&s.comparison.family!=="spatial-pattern"&&!a.has(s.id));let u=c.filter(s=>A.has(s.comparison.family));if(r&&u.length){const s=v(u);s.length>0&&s.every(f=>f.variable.kind==="yield"&&f.effect.direction==="higher")&&(u=[])}const i=new Set;if(u.length){const s=T(u);if(s){t.push(s);for(const l of A)i.add(l)}}else if(!r){const s=w(c);if(s){const l=s.comparison.family,f=L(l,c.filter(g=>g.comparison.family===l));f&&(t.push(f),i.add(l))}}if(t.length<3){const s=c.filter(l=>(l.comparison.family==="cluster"||l.comparison.family==="edge-arc")&&!i.has(l.comparison.family));if(s.length){const l=s[0].comparison.family,f=L(l,s.filter(g=>g.comparison.family===l));f&&t.push(f)}}return t.slice(0,3).join(" ")}