@wafertools/wafermap 0.32.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 (68) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +162 -1
  3. package/README.md +2 -2
  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/insightsTab.js +1 -1
  7. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  8. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +30 -0
  9. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  10. package/dist/packages/canvas-adapter/summaryPanel.d.ts +22 -3
  11. package/dist/packages/canvas-adapter/summaryPanel.js +4 -3
  12. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  13. package/dist/packages/canvas-adapter/toolbar.d.ts +54 -1
  14. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  15. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  16. package/dist/packages/canvas-adapter/userGuideHtml.js +76 -13
  17. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  18. package/dist/packages/canvas-adapter/version.js +1 -1
  19. package/dist/packages/core/aggregates.d.ts +6 -0
  20. package/dist/packages/core/aggregates.js +1 -1
  21. package/dist/packages/core/compact.d.ts +205 -0
  22. package/dist/packages/core/compact.js +3 -0
  23. package/dist/packages/core/passBins.d.ts +6 -0
  24. package/dist/packages/core/passBins.js +1 -1
  25. package/dist/packages/renderer/axisTicks.d.ts +14 -0
  26. package/dist/packages/renderer/axisTicks.js +1 -1
  27. package/dist/packages/renderer/buildView.d.ts +26 -1
  28. package/dist/packages/renderer/buildView.js +1 -1
  29. package/dist/packages/renderer/buildWaferMap.js +1 -1
  30. package/dist/packages/renderer/compactView.d.ts +22 -0
  31. package/dist/packages/renderer/compactView.js +1 -0
  32. package/dist/packages/renderer/xyIndicator.d.ts +27 -0
  33. package/dist/packages/renderer/xyIndicator.js +1 -0
  34. package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
  35. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  36. package/dist/packages/stats/analyzeWaferMap.d.ts +36 -1
  37. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  38. package/dist/packages/stats/binRows.d.ts +38 -0
  39. package/dist/packages/stats/binRows.js +1 -0
  40. package/dist/packages/stats/filterFindings.d.ts +25 -0
  41. package/dist/packages/stats/filterFindings.js +1 -1
  42. package/dist/packages/stats/findingText.d.ts +9 -0
  43. package/dist/packages/stats/findingText.js +2 -0
  44. package/dist/packages/stats/lotDrift.d.ts +16 -0
  45. package/dist/packages/stats/lotDrift.js +1 -0
  46. package/dist/packages/stats/lotPattern.d.ts +33 -0
  47. package/dist/packages/stats/lotPattern.js +1 -0
  48. package/dist/packages/stats/math.d.ts +51 -0
  49. package/dist/packages/stats/math.js +1 -1
  50. package/dist/packages/stats/patternClassification.d.ts +31 -0
  51. package/dist/packages/stats/patternClassification.js +1 -1
  52. package/dist/packages/stats/presentation.d.ts +42 -0
  53. package/dist/packages/stats/presentation.js +1 -0
  54. package/dist/packages/stats/regions.d.ts +9 -0
  55. package/dist/packages/stats/regions.js +1 -1
  56. package/dist/packages/stats/renderSummaryReport.d.ts +15 -2
  57. package/dist/packages/stats/renderSummaryReport.js +32 -27
  58. package/dist/packages/stats/reportHtml.d.ts +54 -9
  59. package/dist/packages/stats/reportHtml.js +130 -47
  60. package/dist/packages/stats/synthesis.d.ts +66 -0
  61. package/dist/packages/stats/synthesis.js +2 -0
  62. package/dist/packages/stats/types.d.ts +1 -1
  63. package/dist/packages/stats/yieldRows.d.ts +31 -0
  64. package/dist/packages/stats/yieldRows.js +1 -0
  65. package/llms.txt +1 -1
  66. package/package.json +1 -1
  67. package/dist/packages/stats/findingsNarrative.d.ts +0 -3
  68. package/dist/packages/stats/findingsNarrative.js +0 -1
@@ -1 +1 @@
1
- import{classifyDie as N,getRingLabel as W}from"../core/classify.js";import{getReticleCell as b}from"../core/reticle.js";import{isPositionedDie as x,diePassStatus as M,getDieKey as R}from"../core/dies.js";function p(n){return R(n)}export function buildRegionYieldData(n,o,r,e,t){const i=new Map,a=[];for(let s=0;s<o.length;s++){const c=n[s];if(!c?.length)continue;const u=new Set(typeof e=="function"?e(s):e),d=t(c.filter(x),o[s],r);for(const l of d){a.includes(l.key)||a.push(l.key);const g=i.get(l.key)??{label:l.label,pass:0,total:0};for(const f of l.dies){if(f.partial||f.edgeExcluded)continue;const y=M(f,u);y!==void 0&&(g.total++,y&&g.pass++)}i.set(l.key,g)}}return a.map(s=>{const c=i.get(s);return c.total===0?null:{key:s,label:c.label,n:c.total,passDies:c.pass,yieldPercent:c.pass/c.total*100}}).filter(s=>s!==null)}const k=["E","ENE","NE","NNE","N","NNW","NW","WNW","W","WSW","SW","SSW","S","SSE","SE","ESE"],q=["E","NE","N","NW","W","SW","S","SE"],K=["E","N","W","S"];export function sectorCompassNames(n){const o=[4,8,16,32].includes(n)?n:16;return o===4?K:o===8?q:k}const P={NE:["NW","SE"],NW:["NE","SW"],SE:["NE","SW"],SW:["NW","SE"]};export function areQuadrantsAdjacent(n,o){return P[n]?.includes(o)??!1}export function parseRegionKey(n){return n.startsWith("ring:")?{family:"ring",ring:Number(n.slice(5))}:n.startsWith("quadrant:")?{family:"quadrant",quadrant:n.slice(9)}:n.startsWith("sector:")?{family:"sector",sector:n.slice(7)}:{family:"unknown"}}export function buildRingRegions(n,o,r){const e=new Map;for(const t of n){const{ring:i}=N(t,o,{ringCount:r}),a=`ring:${i}`,s=e.get(a)??{family:"ring",key:a,label:W(i,r),dieKeys:[],dies:[]};s.dieKeys.push(p(t)),s.dies.push(t),e.set(a,s)}return[...e.values()].sort((t,i)=>t.key.localeCompare(i.key))}export function buildQuadrantRegions(n,o,r){const e=new Map;for(const i of n){const{quadrant:a}=N(i,o,{ringCount:r}),s=`quadrant:${a}`,c=e.get(s)??{family:"quadrant",key:s,label:a,dieKeys:[],dies:[]};c.dieKeys.push(p(i)),c.dies.push(i),e.set(s,c)}const t=new Map([["quadrant:NE",0],["quadrant:NW",1],["quadrant:SE",2],["quadrant:SW",3]]);return[...e.values()].sort((i,a)=>(t.get(i.key)??4)-(t.get(a.key)??4))}export function buildReticlePositionRegions(n,o){if(!o)return[];const r=new Map;for(const e of n){const{column:t,row:i}=b(e,o),a=`reticle-position:cell:${t},${i}`,s=r.get(a)??{family:"reticle-position",key:a,label:`Reticle cell (${t}, ${i})`,dieKeys:[],dies:[]};s.dieKeys.push(p(e)),s.dies.push(e),r.set(a,s)}return[...r.values()].sort((e,t)=>e.key.localeCompare(t.key))}const w=3;export function buildTestSiteRegions(n,o=!1){const r=new Map;for(const e of n){if(e.siteNum===void 0)continue;const t=r.get(e.siteNum)??[];t.push(e),r.set(e.siteNum,t)}if(!o){let e=0;for(const t of r.values())t.length>=w&&e++;if(e<2)return[]}return[...r.entries()].sort(([e],[t])=>e-t).map(([e,t])=>({family:"test-site",key:`test-site:${e}`,label:`Site ${e}`,dieKeys:t.map(p),dies:t}))}export function buildSectorRegions(n,o,r){const e=[4,8,16,32].includes(r)?r:16,t=sectorCompassNames(e),i=new Map,a=o.center.x,s=o.center.y,c=o.radius;for(const u of n){const d=u.physX-a,l=u.physY-s;if(Math.hypot(d,l)/c<.2)continue;const f=Math.PI/e,y=(Math.atan2(l,d)+2*Math.PI+f)%(2*Math.PI),E=Math.floor(y/(2*Math.PI)*e)%e,h=t[E],S=`sector:${h}`,m=i.get(S)??{family:"sector",key:S,label:`Sector ${h}`,dieKeys:[],dies:[]};m.dieKeys.push(p(u)),m.dies.push(u),i.set(S,m)}return[...i.values()].sort((u,d)=>u.key.localeCompare(d.key))}
1
+ import{classifyDie as E,getRingLabel as b}from"../core/classify.js";import{getReticleCell as x}from"../core/reticle.js";import{isPositionedDie as M,diePassStatus as R,getDieKey as k}from"../core/dies.js";function g(s){return k(s)}export function buildRegionYieldData(s,r,a,e,t){const o=new Map,n=[];for(let i=0;i<r.length;i++){const c=s[i];if(!c?.length)continue;const u=new Set(typeof e=="function"?e(i):e),d=t(c.filter(M),r[i],a);for(const l of d){n.includes(l.key)||n.push(l.key);const y=o.get(l.key)??{label:l.label,pass:0,total:0};for(const p of l.dies){if(p.partial||p.edgeExcluded)continue;const S=R(p,u);S!==void 0&&(y.total++,S&&y.pass++)}o.set(l.key,y)}}return n.map(i=>{const c=o.get(i);return c.total===0?null:{key:i,label:c.label,n:c.total,passDies:c.pass,yieldPercent:c.pass/c.total*100}}).filter(i=>i!==null)}const q=["E","ENE","NE","NNE","N","NNW","NW","WNW","W","WSW","SW","SSW","S","SSE","SE","ESE"],w=["E","NE","N","NW","W","SW","S","SE"],A=["E","N","W","S"];export function sectorCompassNames(s){const r=[4,8,16].includes(s)?s:16;return r===4?A:r===8?w:q}export const QUADRANT_CYCLE=["NE","NW","SW","SE"];const K={NE:["NW","SE"],NW:["NE","SW"],SE:["NE","SW"],SW:["NW","SE"]};export function areQuadrantsAdjacent(s,r){return K[s]?.includes(r)??!1}const f=1440;export function regionAngleBins(s,r){const a=parseRegionKey(s);let e,t;if(a.family==="quadrant"){const n=QUADRANT_CYCLE.indexOf(a.quadrant);if(n<0)return;e=n*f/4,t=f/4}else if(a.family==="sector"){const n=[4,8,16].includes(r)?r:void 0,i=n?sectorCompassNames(n).indexOf(a.sector??""):-1;if(!n||i<0)return;t=f/n,e=i*t-t/2}else return;const o=new Set;for(let n=0;n<t;n++)o.add(((e+n)%f+f)%f);return o}export function parseRegionKey(s){return s.startsWith("ring:")?{family:"ring",ring:Number(s.slice(5))}:s.startsWith("quadrant:")?{family:"quadrant",quadrant:s.slice(9)}:s.startsWith("sector:")?{family:"sector",sector:s.slice(7)}:{family:"unknown"}}export function buildRingRegions(s,r,a){const e=new Map;for(const t of s){const{ring:o}=E(t,r,{ringCount:a}),n=`ring:${o}`,i=e.get(n)??{family:"ring",key:n,label:b(o,a),dieKeys:[],dies:[]};i.dieKeys.push(g(t)),i.dies.push(t),e.set(n,i)}return[...e.values()].sort((t,o)=>t.key.localeCompare(o.key))}export function buildQuadrantRegions(s,r,a){const e=new Map;for(const o of s){const{quadrant:n}=E(o,r,{ringCount:a}),i=`quadrant:${n}`,c=e.get(i)??{family:"quadrant",key:i,label:n,dieKeys:[],dies:[]};c.dieKeys.push(g(o)),c.dies.push(o),e.set(i,c)}const t=new Map([["quadrant:NE",0],["quadrant:NW",1],["quadrant:SE",2],["quadrant:SW",3]]);return[...e.values()].sort((o,n)=>(t.get(o.key)??4)-(t.get(n.key)??4))}export function buildReticlePositionRegions(s,r){if(!r)return[];const a=new Map;for(const e of s){const{column:t,row:o}=x(e,r),n=`reticle-position:cell:${t},${o}`,i=a.get(n)??{family:"reticle-position",key:n,label:`Reticle cell (${t}, ${o})`,dieKeys:[],dies:[]};i.dieKeys.push(g(e)),i.dies.push(e),a.set(n,i)}return[...a.values()].sort((e,t)=>e.key.localeCompare(t.key))}const P=3;export function buildTestSiteRegions(s,r=!1){const a=new Map;for(const e of s){if(e.siteNum===void 0)continue;const t=a.get(e.siteNum)??[];t.push(e),a.set(e.siteNum,t)}if(!r){let e=0;for(const t of a.values())t.length>=P&&e++;if(e<2)return[]}return[...a.entries()].sort(([e],[t])=>e-t).map(([e,t])=>({family:"test-site",key:`test-site:${e}`,label:`Site ${e}`,dieKeys:t.map(g),dies:t}))}export function buildSectorRegions(s,r,a){const e=[4,8,16].includes(a)?a:16,t=sectorCompassNames(e),o=new Map,n=r.center.x,i=r.center.y,c=r.radius;for(const u of s){const d=u.physX-n,l=u.physY-i;if(Math.hypot(d,l)/c<.2)continue;const p=Math.PI/e,S=(Math.atan2(l,d)+2*Math.PI+p)%(2*Math.PI),W=Math.floor(S/(2*Math.PI)*e)%e,h=t[W],m=`sector:${h}`,N=o.get(m)??{family:"sector",key:m,label:`Sector ${h}`,dieKeys:[],dies:[]};N.dieKeys.push(g(u)),N.dies.push(u),o.set(m,N)}return[...o.values()].sort((u,d)=>u.key.localeCompare(d.key))}
@@ -1,5 +1,6 @@
1
1
  import type { Die } from '../core/dies.js';
2
2
  import type { Wafer } from '../core/wafer.js';
3
+ import { type BinColors } from '../renderer/binColors.js';
3
4
  import { type BinDef, type TestDef, type YieldSummary, type WaferMapResult } from '../renderer/buildWaferMap.js';
4
5
  import type { StatsSummary, AnalyzeWaferMapOptions } from './types.js';
5
6
  import { openHtmlReport } from './renderFindingsReport.js';
@@ -19,6 +20,10 @@ export interface SummaryReportParams {
19
20
  statsSummary?: StatsSummary;
20
21
  passBins?: number[];
21
22
  ringCount?: number;
23
+ /** @internal The map's own bin colours, so a bar here is the colour of that bin on the map. Resolved with the default palette when omitted. */
24
+ binColors?: BinColors;
25
+ /** @internal Shown inside the app: a click on a finding's row selects it on the map (see `LIVE_FINDINGS_SCRIPT`). */
26
+ live?: boolean;
22
27
  }
23
28
  /** Generate a full wafer summary as a standalone HTML string suitable for `openHtmlReport()`. */
24
29
  export declare function renderSummaryReportHtml(params: SummaryReportParams, options?: {
@@ -54,6 +59,10 @@ export interface LotSummaryReportParams {
54
59
  ringCount?: number;
55
60
  /** Passthrough to the internal per-group `analyzeWaferLot` call, e.g. `{ enableTestValueAnalysis: true }`. */
56
61
  analyzeOptions?: AnalyzeWaferMapOptions;
62
+ /** @internal The gallery's bin colours, so a bar here is the colour of that bin on the cards. Resolved per group with the default palette when omitted. */
63
+ binColors?: BinColors;
64
+ /** @internal Shown inside the app: a click on a finding's row selects it on the map. */
65
+ live?: boolean;
57
66
  }
58
67
  /**
59
68
  * Generate a full lot summary as a standalone HTML string suitable for
@@ -86,7 +95,9 @@ export declare function renderWaferReportHtml(map: ReportMap & {
86
95
  yield: YieldSummary;
87
96
  dataCoverage: SummaryReportParams['dataCoverage'];
88
97
  }, summary?: StatsSummary, options?: {
89
- title?: string;
98
+ title?: string; /** @internal */
99
+ binColors?: BinColors; /** @internal */
100
+ live?: boolean;
90
101
  }): string;
91
102
  /**
92
103
  * The lot summary report for several built maps, as a standalone HTML document.
@@ -96,6 +107,8 @@ export declare function renderWaferReportHtml(map: ReportMap & {
96
107
  */
97
108
  export declare function renderLotReportHtml(maps: readonly ReportMap[], options?: {
98
109
  title?: string;
99
- analyzeOptions?: AnalyzeWaferMapOptions;
110
+ analyzeOptions?: AnalyzeWaferMapOptions; /** @internal */
111
+ binColors?: BinColors; /** @internal */
112
+ live?: boolean;
100
113
  }): string;
101
114
  //# sourceMappingURL=renderSummaryReport.d.ts.map
@@ -1,62 +1,67 @@
1
- import{waferDisplayLabel as H}from"../core/waferLabel.js";import{binPassSets as $e,binPassSetsByWafer as xe,mergeBinDefs as I}from"../renderer/binColors.js";import{itemPassBins as R,passBinsLabel as De}from"../core/passBins.js";import{isParametricTest as Be,isBuiltMap as j}from"../renderer/buildWaferMap.js";import{buildRingRegions as z,buildQuadrantRegions as A,buildRegionYieldData as ve}from"./regions.js";import{openHtmlReport as Te}from"./renderFindingsReport.js";import{analyzeWaferLot as Pe}from"./analyzeWaferLot.js";import{computeFunctionalYield as Fe,analyzeWaferMap as Ye}from"./analyzeWaferMap.js";import{mergeTestDefs as We}from"./mergeTestDefs.js";import{sortBinsForDisplay as O}from"./binPareto.js";import{buildFacetTable as Me,facetValueOf as Ce,FACET_NONE_VALUE as Re}from"./facets.js";import{visibleFindings as G}from"./filterFindings.js";import{buildYieldDataCombined as Ne}from"./yield.js";import{describeWaferPopulation as U,populationLabel as Ee}from"./population.js";import{buildTestPassRateData as Le,hasJudgeableTests as ke,poolFunctionalYield as He}from"./testPassRate.js";import{buildCapabilityData as Ie}from"./capability.js";import{fmt as T}from"../renderer/fmt.js";import{getDieKey as je,isPositionedDie as ze,diePassStatus as V}from"../core/dies.js";import{testValue as Ae}from"../core/dieTable.js";import{findingsTableHtml as _,renderMetadataSection as K,renderMetricGrid as Q,renderSection as $,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 Oe(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,u=O(e.entries(),$e(t,r)[a]).map(([s,c])=>{const b=l?.get(s)?.name??"\u2014";return[String(s),b,String(c),C(c,i)]});return $(a==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",v(["Bin","Name","Count","%"],u,{className:"compact"}))}function J(t,o,a,n){const r=new Set(n),e=new Map(a.map(u=>[je(u),u]));if(!a.some(u=>u.hbin!=null||u.sbin!=null)||!o.length)return"";const l=[];for(const u of o){let f=0,s=0;for(const c of u.dieKeys){const g=e.get(c);if(!g||g.partial||g.edgeExcluded)continue;const b=V(g,r);b!==void 0&&(s++,b&&f++)}s&&l.push([u.label,String(f),String(s),C(f,s)])}return l.length?$(t,v(["Region","Pass","Total","Yield"],l,{className:"compact"})):""}function X(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(Be)){const l=i.testNumber,u=i.unit||void 0,f=r.get(l);let s,c,g;if(f)({min:s,max:c,mean:g}=f);else{let b=0,B=0;s=1/0,c=-1/0;for(const h of n){const d=Ae(h,l);d===void 0||!isFinite(d)||(b++,B+=d,d<s&&(s=d),d>c&&(c=d))}if(b===0)continue;g=B/b}e.push([y(i.name),T(s,u),T(g,u),T(c,u)])}return e.length?$("Test Values",v(["Test","Min","Mean","Max"],e,{className:"compact"})):""}function Z(t,o,a){const n=a??Fe(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 $("Functional Tests",v(["Test","N","Pass","Fail","Pass Rate"],r,{className:"compact"}))}function ee(t,o){const a=Ie(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 $("Process Capability",v(["Test","Limits","Mean","Cp","Cpk","Pp","Ppk"],r,{className:"compact"}))}function te(t,o){const a=G(t);return a.length?$("Findings",_(a,o)):""}export function renderSummaryReportHtml(t,o={}){const{wafer:a,dies:n,yieldSummary:r,dataCoverage:e,hbinDefs:i,sbinDefs:l,testDefs:u=[],statsSummary:f,passBins:s=[1],ringCount:c=4}=t,g=a.metadata,b=Oe(g),B=o.title??`Wafer Summary${b}`,h=new Date().toLocaleString(),d=n.some(W=>W.hbin!=null),p=n.some(W=>W.sbin!=null),S=n.filter(ze),x=z(S,a,c),w=A(S,a,c),D=[{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: ${De([s])})`,value:`${r.yieldPercent.toFixed(1)}%`}]:[],...r.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(r.edgeExcludedDies)}]:[]],Y=[K([{metadata:a.metadata}]),$("Summary",[Q(D)].filter(Boolean).join(`
2
- `)),d?q(n,i,"hard",f?.stats.hardBinCounts,s):p?q(n,l,"soft",f?.stats.softBinCounts,s):"",J("Ring Yield",x,n,s),J("Quadrant Yield",w,n,s),X(n,u,f?.stats.perTestStats),Z(n,u,f?.stats.functionalYield),ee([{dies:n}],u),f?te(f.findings):""].filter(Boolean).join(`
1
+ import{waferDisplayLabel as O}from"../core/waferLabel.js";import{binPassSets as Ye,binPassSetsByWafer as We,mergeBinDefs as _,resolveBinColors as Ne,resolveBinColorsByWafer as Re}from"../renderer/binColors.js";import{commonPassBins as Ie,itemPassBins as L,passBinsLabel as Le}from"../core/passBins.js";import{isParametricTest as Me,isBuiltMap as Q}from"../renderer/buildWaferMap.js";import{buildRingRegions as V,buildQuadrantRegions as U,buildRegionYieldData as Ee}from"./regions.js";import{openHtmlReport as ke}from"./renderFindingsReport.js";import{analyzeWaferLot as He}from"./analyzeWaferLot.js";import{computeFunctionalYield as je,analyzeWaferMap as Ae}from"./analyzeWaferMap.js";import{mergeTestDefs as qe}from"./mergeTestDefs.js";import{buildFacetTable as ze,facetValueOf as Ge,FACET_NONE_VALUE as Oe}from"./facets.js";import{visibleFindings as M}from"./filterFindings.js";import{buildYieldDataCombined as _e}from"./yield.js";import{describeWaferPopulation as K,populationLabel as Qe}from"./population.js";import{buildTestPassRateData as Ve,hasJudgeableTests as Ue,poolFunctionalYield as Ke}from"./testPassRate.js";import{buildCapabilityData as Je}from"./capability.js";import{buildSynthesis as Xe}from"./synthesis.js";import{binBreakdownRows as Ze,binBreakdownTitle as et,binCountsFrom as tt,pooledBinCounts as nt,totalOf as st}from"./binRows.js";import{MEAN_WAFER_YIELD_LABEL as at,shortfallLegend as E}from"./presentation.js";import{regionYieldRows as J,waferYieldRows as it}from"./yieldRows.js";import{describeValues as ot}from"./math.js";import{fmt as B}from"../renderer/fmt.js";import{getDieKey as rt,isPositionedDie as lt,diePassStatus as X}from"../core/dies.js";import{testValue as ct}from"../core/dieTable.js";import{findingsTableHtml as Z,synthesisSectionHtml as dt,LIVE_FINDINGS_SCRIPT as k,barCell as W,deltaCell as H,withSectionNav as ee,renderMetadataSection as te,renderMetricGrid as ne,renderSection as D,renderTable as P,reportStyles as j}from"./reportHtml.js";import{escHtml as S}from"../core/utils.js";function se(t,a){return a===0?"\u2014":`${(t/a*100).toFixed(1)}%`}function ut(t){if(!t)return"";const a=t.lot??t.lotId,i=t.wafer??t.waferId,n=[a,i].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(S).join(" \xB7 ")}`:""}function ae(t,a,i,n,r){if(!t.size)return"";const e=Ze(t,a,n).map(c=>{const s=c.passing?"muted-row":"",d=m=>({html:S(m),className:s});return[d(String(c.bin)),d(c.name??"\u2014"),d(String(c.count)),W(c.percent,`${c.percent.toFixed(1)}%`,s,r?.get(c.bin))]});return D(et(i,st(t)),P(["Bin","Name","Dies","Share"],e,{className:"compact"}))}function ft(t,a,i,n,r,e){const c=Ye(t,r);return["hard","soft"].map(s=>ae(tt(t,s,s==="hard"?n.hard:n.soft),s==="hard"?a:i,s,c[s],e[s])).join(`
2
+ `)}function ie(t,a,i,n){const r=new Set(n),e=new Map(i.map(o=>[rt(o),o]));if(!i.some(o=>o.hbin!=null||o.sbin!=null)||!a.length)return"";const s=[];for(const o of a){let p=0,h=0;for(const b of o.dieKeys){const w=e.get(b);if(!w||w.partial||w.edgeExcluded)continue;const y=X(w,r);y!==void 0&&(h++,y&&p++)}h&&s.push({label:o.label,pass:p,total:h})}if(!s.length)return"";const{rows:d}=J(s.map(o=>({key:o.label,label:o.label,n:o.total,passDies:o.pass,yieldPercent:o.pass/o.total*100}))),m=d.map(o=>[S(o.label),String(o.passDies),String(o.n),W(o.yieldPercent,se(o.passDies,o.n)),H(o.delta)]);return D(t,P(["Region","Pass","Total","Yield","vs wafer"],m,{className:"compact"})+`<p class="report-legend">${E("the wafer")}</p>`)}function mt(t,a){const i=new Float64Array(t.length);let n=0;for(const h of t){const b=ct(h,a);b!==void 0&&Number.isFinite(b)&&(i[n++]=b)}if(n===0)return;const{count:r,min:e,mean:c,max:s,q1:d,median:m,q3:o,stddev:p}=ot(i.subarray(0,n));return{testNumber:a,count:r,min:e,mean:c,max:s,q1:d,median:m,q3:o,stddev:p}}function oe(t,a,i,n,r){if(!a.length)return"";const e=t.filter(l=>!l.partial&&!l.edgeExcluded),c=new Map((i??[]).map(l=>[l.testNumber,l])),s=new Map((n??[]).map(l=>[l.testNumber,l.yieldPercent])),d=[];for(const l of a.filter(Me)){const u=c.get(l.testNumber)??mt(e,l.testNumber);u&&d.push({name:l.name,unit:l.unit||void 0,stats:u,limitYield:s.get(l.testNumber)})}if(!d.length)return"";const m=l=>d.every(u=>u.stats[l]!==void 0),o=m("q1")&&m("median")&&m("q3"),p=m("stddev"),h=d.some(l=>l.limitYield!==void 0&&l.limitYield!==null),b=(l,u)=>l===void 0?"\u2014":B(l,u),w=["Test","N","Min",...o?["Q1","Median"]:[],"Mean",...o?["Q3"]:[],"Max",...p?["StdDev"]:[],...h?["Limit yield"]:[]],y=d.map(({name:l,unit:u,stats:g,limitYield:$})=>[S(l),{html:String(g.count),className:"numeric"},b(g.min,u),...o?[b(g.q1,u),b(g.median,u)]:[],b(g.mean,u),...o?[b(g.q3,u)]:[],b(g.max,u),...p?[b(g.stddev,u)]:[],...h?[$==null?"\u2014":`${$.toFixed(1)}%`]:[]]);return D("Test Values",P(w,y,{className:"compact"})+`<p class="report-legend">${p?"StdDev is the population standard deviation. ":""}${r??""}</p>`)}function re(t,a,i){const n=i??je(t,a);if(!n?.length)return"";const r=n.map(e=>[S(e.label),String(e.totalDies),String(e.passDies),String(e.failDies),e.passRatePercent!==null?`${e.passRatePercent.toFixed(1)}%`:"\u2014"]);return D("Functional Tests",P(["Test","N","Pass","Fail","Pass Rate"],r,{className:"compact"}))}function le(t,a){const i=Je(t,a);if(!i.length)return"";const n=e=>e===null?"\u2014":e.toFixed(2),r=i.map(e=>[S(e.label),e.hasSpec?e.limitBasis==="spec"?`LSL ${B(e.lsl,e.unit)} \u2013 USL ${B(e.usl,e.unit)}`:`Lo limit ${B(e.lsl,e.unit)} \u2013 Hi limit ${B(e.usl,e.unit)}`:"\u2014",B(e.mean,e.unit),n(e.cp),n(e.cpk),n(e.pp),n(e.ppk)]);return D("Process Capability",P(["Test","Limits","Mean","Cp","Cpk","Pp","Ppk"],r,{className:"compact"}))}function ce(t,a,i="",n=!1){const r=M(t);return r.length?D("Findings",Z(r,a,i,n)):""}function de(t,a,i=""){const n=new Set(M(t.findings).map(r=>r.id));return dt(Xe(t,{passBins:a?[...a]:void 0}),n,i)}export function renderSummaryReportHtml(t,a={}){const{wafer:i,dies:n,yieldSummary:r,dataCoverage:e,hbinDefs:c,sbinDefs:s,testDefs:d=[],statsSummary:m,passBins:o=[1],ringCount:p=4}=t,h=t.binColors??Ne(n,{passBins:o,hbinDefs:c,sbinDefs:s}),b=i.metadata,w=ut(b),y=a.title??`Wafer Summary${w}`,l=new Date().toLocaleString(),u=n.filter(lt),g=V(u,i,p),$=U(u,i,p),v=[{label:"Total dies",value:String(r.totalDies)},{label:"Filled dies",value:String(e.filledDies),hint:`${se(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: ${Le([o])})`,value:`${r.yieldPercent.toFixed(1)}%`}]:[],...r.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(r.edgeExcludedDies)}]:[]],x=[te([{metadata:i.metadata}]),D("Summary",[ne(v)].filter(Boolean).join(`
3
+ `)),m?de(m,o):"",ft(n,c,s,{hard:m?.stats.hardBinCounts,soft:m?.stats.softBinCounts},o,h),ie("Ring Yield",g,n,o),ie("Quadrant Yield",$,n,o),oe(n,d,m?.stats.perTestStats,m?.stats.testSpecYield),re(n,d,m?.stats.functionalYield),le([{dies:n}],d),m?ce(m.findings,void 0,"",t.live):""].filter(Boolean).join(`
3
4
  `);return`<!DOCTYPE html>
4
5
  <html lang="en">
5
6
  <head>
6
7
  <meta charset="UTF-8">
7
- <title>${y(B)}</title>
8
+ <title>${S(y)}</title>
8
9
  <style>
9
- ${N()}
10
+ ${j()}
10
11
  </style>
11
12
  </head>
12
13
  <body>
13
14
  <main class="report">
14
15
  <header class="report-header">
15
- <h1>${y(B)}</h1>
16
- <p class="report-subtitle">Generated ${y(h)}</p>
16
+ <h1>${S(y)}</h1>
17
+ <p class="report-subtitle">Generated ${S(l)}</p>
17
18
  </header>
18
- ${Y}
19
- <p class="footer">Generated ${y(h)}</p>
19
+ ${ee(x)}
20
+ <p class="footer">Generated ${S(l)}</p>
20
21
  </main>
22
+ ${t.live?k:""}
21
23
  </body>
22
- </html>`}function Ge(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 Ue(t){const o=[];for(const e of t.perWafer){const i=G(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>`+_(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 $("Findings by Wafer",r+o.join(`
23
- `))}const Ve=3;function _e(t,o,a){const n=t.map(u=>({metadata:u.wafer?.metadata})),r=Me(n,{facetableOnly:!0}).filter(u=>u.splittable);if(!r.length)return"";const e=r.slice(0,Ve),i=[];for(const u of e){const f=new Map;t.forEach((d,p)=>{const S=Ce(d.wafer?.metadata,u.key)??Re;(f.get(S)??f.set(S,[]).get(S)).push(p)});const s=[...f.entries()].map(([d,p])=>({key:d,items:p.map(S=>t[S])}));if(s.length<2)continue;const c=s.map(d=>[y(d.key),y(d.items.map(p=>p.label).join(", "))]),g=Ne(s.map(d=>({key:d.key,items:d.items.map(p=>({label:p.label,dies:p.dies,passBins:R(p,a)}))})),a).map(d=>[y(d.label),String(d.itemCount),`${d.percent.toFixed(1)}%`]),b=[v([u.label,"Wafers"],c,{className:"compact"}),v([u.label,"Wafers","Yield"],g,{className:"compact"})],B=t.flatMap(d=>d.dies??[]);let h=0;for(const d of["spec","testFlag","functional"]){if(!ke(o,d,B))continue;const p=Le(s.map(w=>({key:w.key,items:w.items.map(D=>({dies:D.dies,testSpecYield:D.statsSummary?.stats.testSpecYield,functionalYield:D.statsSummary?.stats.functionalYield}))})),o,d);if(!p.rows.length)continue;const S=p.rows.map(w=>[y(w.label),...w.byGroup.map(D=>D.passRatePercent===null?"\u2014":`${D.passRatePercent.toFixed(1)}%`),w.overall.passRatePercent===null?"\u2014":`${w.overall.passRatePercent.toFixed(1)}%`]),x=d==="spec"?"Pass rate \xB7 test limits":d==="testFlag"?"Pass rate \xB7 tester flag":"Pass rate \xB7 functional";p.disagreementDies&&(h=p.disagreementDies),b.push(`<h4 class="report-subheading">${y(x)}</h4>`+v(["Test",...p.groups,"All"],S,{className:"compact"}))}h&&b.push(`<p class="report-note">${h.toLocaleString()} dies judged differently by limits and tester flag.</p>`),i.push(`<h3 class="report-subheading">${y(u.label)}</h3>${b.join(`
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 $("Split Comparison",i.join(`
25
- `)+l)}function ne(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,g]of Object.entries(s.stats[i])){const b=Number(c);e.set(b,(e.get(b)??0)+g)}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),u=o?new Map(o.map(s=>[s.bin,s])):null,f=O(e.entries(),xe(r)[a]).map(([s,c])=>{const g=u?.get(s);return[g?.name?`Bin ${s} \xB7 ${g.name} (${c})`:`Bin ${s} (${c})`,C(c,l)]});return $(a==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",v(["Bin","Yield"],f,{className:"compact"}))}function se(t,o,a,n,r,e){const i=ve(a,n,r,l=>e[l],o).map(l=>[l.label,`${l.yieldPercent.toFixed(1)}%`]);return i.length?$(t,v(["Region","Yield"],i,{className:"compact"})):""}function Ke(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 X(t,o,n)}function Qe(t,o,a){const n=He(a);return Z(t,o,n)}const E=["lot","product","testProgram","temperature"];function qe(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 ae(t,o,a,n,r,e,i){const l=Pe(t.map(m=>m.source??m),{perWaferSummaries:t.map(m=>m.statsSummary),...i}),u=[],f=[],s=[],c=[];for(const m of t){const F=m.dies??[];m.wafer&&(u.push(m.wafer),s.push(F),c.push(R(m,r)));for(const P of F)f.push(P)}const g=t.map(m=>({dies:m.dies??[],passBins:R(m,r)})),b=f.some(m=>m.hbin!=null),B=f.some(m=>m.sbin!=null),h=b||B,d=l.perWafer.map(m=>m.summary.stats.yieldPercent).filter(m=>m!==null),p=d.length?d.reduce((m,F)=>m+F,0)/d.length:null;let S=0,x=0,w=0,D=0,Y=0;for(const m of g){const F=new Set(m.passBins);for(const P of m.dies){if(S++,P.edgeExcluded&&D++,P.partial&&Y++,P.partial||P.edgeExcluded)continue;const k=V(P,F);k!==void 0&&(x++,k&&w++)}}const W=x-w,L=x>0?w/x*100:null,oe=[{label:"Wafers",value:String(l.stats.waferCount)},{label:"Total dies",value:String(S)},...h?[{label:"Good dies",value:String(w)},{label:"Bad dies",value:String(W)}]:[],...D>0?[{label:"Edge excluded",value:String(D)}]:[],...Y>0?[{label:"Partial dies",value:String(Y)}]:[],...p!==null?[{label:"Mean wafer yield",value:`${p.toFixed(1)}%`}]:[],...L!==null?[{label:"Total yield",value:`${L.toFixed(1)}%`}]:[]],re=U(l.perWafer.map(m=>m.summary.wafer)),le=$(re.lotId!==void 0?"Lot Summary":"Summary",Q(oe)),ce=K(t.map(m=>({metadata:m.wafer?.metadata}))),de=$("Per-Wafer Yield",Ge(l,t)),ue=_e(t,n,r),M=l.perWafer.map(m=>m.summary),fe=h?b?ne(f,o,"hard",M,g):ne(f,a,"soft",M,g):"",me=h&&u.length?se("Ring Yield (All Wafers)",z,s,u,e,c):"",pe=h&&u.length?se("Quadrant Yield (All Wafers)",A,s,u,e,c):"",ge=n.length?Ke(f,n,M):"",be=n.length?Qe(f,n,M):"",he=n.length?ee(t,n):"",ye=l.findings.length?te(l.findings,l.stats.waferCount):"",Se=Ue(l),we=[le,ce,de,ue,fe,me,pe,ge,be,he,ye,Se].filter(Boolean).join(`
26
- `);return{lotSummary:l,sections:we}}function ie(t,o,a){return`<main class="report">
24
+ </html>`}function pt(t,a){const i=t.perWafer.flatMap(e=>{const c=e.summary.stats.yieldPercent;return c===null?[]:[{waferIndex:e.waferIndex,yieldPercent:c}]}),n=new Map(it(i).rows.map(e=>[e.waferIndex,e])),r=t.perWafer.map(e=>{const c=S(O(a[e.waferIndex],e.waferIndex)),s=n.get(e.waferIndex);return s?[s.outlier?`${c} \xB7 ${s.outlier} outlier`:c,W(s.yieldPercent,`${s.yieldPercent.toFixed(1)}%`),H(s.delta)]:[c,"N/A",""]});return P(["Wafer","Yield","vs lot median"],r,{className:"compact"})+`<p class="report-legend">${E("the lot median")}</p>`}function gt(t){const a=[];for(const e of t.perWafer){const c=M(e.summary.findings);if(!c.length)continue;const s=e.summary.wafer?.waferId??`Wafer ${e.waferIndex+1}`;a.push(`<h3 class="report-subheading">${S(s)}</h3>`+Z(c))}if(!a.length)return"";const i=a.length,n=t.perWafer.length,r=`<p class="report-note">${i} of ${n} wafer${n===1?"":"s"} have per-wafer findings; the rest had none.</p>`;return D("Findings by Wafer",r+a.join(`
25
+ `))}const ht=3;function bt(t,a,i){const n=t.map(d=>({metadata:d.wafer?.metadata})),r=ze(n,{facetableOnly:!0}).filter(d=>d.splittable);if(!r.length)return"";const e=r.slice(0,ht),c=[];for(const d of e){const m=new Map;t.forEach((l,u)=>{const g=Ge(l.wafer?.metadata,d.key)??Oe;(m.get(g)??m.set(g,[]).get(g)).push(u)});const o=[...m.entries()].map(([l,u])=>({key:l,items:u.map(g=>t[g])}));if(o.length<2)continue;const p=o.map(l=>[S(l.key),S(l.items.map(u=>u.label).join(", "))]),h=_e(o.map(l=>({key:l.key,items:l.items.map(u=>({label:u.label,dies:u.dies,passBins:L(u,i)}))})),i).map(l=>[S(l.label),String(l.itemCount),`${l.percent.toFixed(1)}%`]),b=[P([d.label,"Wafers"],p,{className:"compact"}),P([d.label,"Wafers","Yield"],h,{className:"compact"})],w=t.flatMap(l=>l.dies??[]);let y=0;for(const l of["spec","testFlag","functional"]){if(!Ue(a,l,w))continue;const u=Ve(o.map(v=>({key:v.key,items:v.items.map(x=>({dies:x.dies,testSpecYield:x.statsSummary?.stats.testSpecYield,functionalYield:x.statsSummary?.stats.functionalYield}))})),a,l);if(!u.rows.length)continue;const g=u.rows.map(v=>[S(v.label),...v.byGroup.map(x=>x.passRatePercent===null?"\u2014":`${x.passRatePercent.toFixed(1)}%`),v.overall.passRatePercent===null?"\u2014":`${v.overall.passRatePercent.toFixed(1)}%`]),$=l==="spec"?"Pass rate \xB7 test limits":l==="testFlag"?"Pass rate \xB7 tester flag":"Pass rate \xB7 functional";u.disagreementDies&&(y=u.disagreementDies),b.push(`<h4 class="report-subheading">${S($)}</h4>`+P(["Test",...u.groups,"All"],g,{className:"compact"}))}y&&b.push(`<p class="report-note">${y.toLocaleString()} dies judged differently by limits and tester flag.</p>`),c.push(`<h3 class="report-subheading">${S(d.label)}</h3>${b.join(`
26
+ `)}`)}if(!c.length)return"";const s=r.length>e.length?`<p class="report-note">${r.length-e.length} further splittable field(s) not shown.</p>`:"";return D("Split Comparison",c.join(`
27
+ `)+s)}function yt(t,a,i,n,r,e){const c=We(r);return["hard","soft"].map(s=>ae(nt(n,t,s),s==="hard"?a:i,s,c[s],e[s])).join(`
28
+ `)}function ue(t,a,i,n,r,e){const c=Ee(i,n,r,d=>e[d],a);if(!c.length)return"";const s=J(c).rows.map(d=>[S(d.label),W(d.yieldPercent,`${d.yieldPercent.toFixed(1)}%`),H(d.delta)]);return D(t,P(["Region","Yield","vs lot"],s,{className:"compact"})+`<p class="report-legend">${E("the lot")}</p>`)}function St(t,a,i,n){let r;if(i?.length&&i.every(e=>e.stats.perTestStats!==void 0)){const e=new Map;for(const c of i)for(const s of c.stats.perTestStats??[]){const d=e.get(s.testNumber)??{n:0,sum:0,min:1/0,max:-1/0,parts:[]};d.n+=s.count,d.sum+=s.mean*s.count,d.min=Math.min(d.min,s.min),d.max=Math.max(d.max,s.max),d.parts.push({n:s.count,mean:s.mean,sd:s.stddev}),e.set(s.testNumber,d)}r=[...e.entries()].map(([c,s])=>{const d=s.sum/s.n,o=s.parts.every(p=>p.sd!==void 0)?s.parts.reduce((p,h)=>p+h.n*(h.sd**2+(h.mean-d)**2),0)/s.n:void 0;return{testNumber:c,count:s.n,min:s.min,max:s.max,mean:d,...o!==void 0?{stddev:Math.sqrt(o)}:{}}})}return oe(t,a,r,n,r?"Quartiles are not shown for a lot: they cannot be combined from each wafer\u2019s own.":void 0)}function wt(t,a,i){const n=Ke(i);return re(t,a,n)}const A=["lot","product","testProgram","temperature"];function $t(t){const a=new Map(A.map(e=>[e,new Set]));for(const e of t)for(const c of A){const s=e.wafer?.metadata?.[c];s!=null&&s!==""&&a.get(c).add(String(s))}const i=A.filter(e=>a.get(e).size>1);if(i.length===0)return[{label:"",items:t,varying:i}];const n=new Map,r=[];for(const e of t){const c=i.map(s=>String(e.wafer?.metadata?.[s]??"(none)")).join(" \xB7 ");n.has(c)||(n.set(c,[]),r.push(c)),n.get(c).push(e)}return r.map(e=>({label:e,items:n.get(e),varying:i}))}function fe(t,a,i,n,r,e,c,s,d="",m=!1){const o=He(t.map(f=>f.source??f),{perWaferSummaries:t.map(f=>f.statsSummary),...c}),p=[],h=[],b=[],w=[];for(const f of t){const T=f.dies??[];f.wafer&&(p.push(f.wafer),b.push(T),w.push(L(f,r)));for(const F of T)h.push(F)}const y=t.map(f=>({dies:f.dies??[],passBins:L(f,r)})),l=s??Re(y,{hbinDefs:a,sbinDefs:i}).colors,u=h.some(f=>f.hbin!=null),g=h.some(f=>f.sbin!=null),$=u||g,v=o.perWafer.map(f=>f.summary.stats.yieldPercent).filter(f=>f!==null),x=v.length?v.reduce((f,T)=>f+T,0)/v.length:null;let q=0,C=0,Y=0,N=0,R=0;for(const f of y){const T=new Set(f.passBins);for(const F of f.dies){if(q++,F.edgeExcluded&&N++,F.partial&&R++,F.partial||F.edgeExcluded)continue;const G=X(F,T);G!==void 0&&(C++,G&&Y++)}}const pe=C-Y,z=C>0?Y/C*100:null,ge=[{label:"Wafers",value:String(o.stats.waferCount)},{label:"Total dies",value:String(q)},...$?[{label:"Good dies",value:String(Y)},{label:"Bad dies",value:String(pe)}]:[],...N>0?[{label:"Edge excluded",value:String(N)}]:[],...R>0?[{label:"Partial dies",value:String(R)}]:[],...x!==null?[{label:at,value:`${x.toFixed(1)}%`}]:[],...z!==null?[{label:"Total yield",value:`${z.toFixed(1)}%`}]:[]],he=K(o.perWafer.map(f=>f.summary.wafer)),be=D(he.lotId!==void 0?"Lot Summary":"Summary",ne(ge)),ye=te(t.map(f=>({metadata:f.wafer?.metadata}))),Se=D("Per-Wafer Yield",pt(o,t)),we=bt(t,n,r),I=o.perWafer.map(f=>f.summary),$e=$?yt(h,a,i,I,y,l):"",ve=$&&p.length?ue("Ring Yield (All Wafers)",V,b,p,e,w):"",xe=$&&p.length?ue("Quadrant Yield (All Wafers)",U,b,p,e,w):"",De=n.length?St(h,n,I,o.stats.testSpecYield):"",Pe=n.length?wt(h,n,I):"",Fe=n.length?le(t,n):"",Te=o.findings.length?ce(o.findings,o.stats.waferCount,d,m):"",Be=gt(o),Ce=[be,de(o,Ie(y.map(f=>f.passBins)),d),ye,Se,we,$e,ve,xe,De,Pe,Fe,Te,Be].filter(Boolean).join(`
29
+ `);return{lotSummary:o,sections:Ce}}function me(t,a,i,n=""){return`<main class="report">
27
30
  <header class="report-header">
28
- <h1>${y(t)}</h1>
29
- <p class="report-subtitle">Generated ${y(a)}</p>
31
+ <h1>${S(t)}</h1>
32
+ <p class="report-subtitle">Generated ${S(i)}</p>
30
33
  </header>
31
- ${o}
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:u}=t,f=new Date().toLocaleString(),s=qe(a);if(s.length===1){const{lotSummary:h,sections:d}=ae(s[0].items,n,r,e,i,l,u),p=U(h.perWafer.map(x=>x.summary.wafer)),S=o.title??(p.lotId!==void 0?`Lot Summary \u2014 ${p.lotId}`:`Summary \u2014 ${Ee(p)}`);return`<!DOCTYPE html>
34
+ ${ee(a,n)}
35
+ <p class="footer">Generated ${S(i)}</p>
36
+ </main>`}export function renderLotSummaryReportHtml(t,a={}){const{items:i,hbinDefs:n,sbinDefs:r,testDefs:e=[],passBins:c=[1],ringCount:s=4,analyzeOptions:d}=t,m=new Date().toLocaleString(),o=$t(i);if(o.length===1){const{lotSummary:y,sections:l}=fe(o[0].items,n,r,e,c,s,d,t.binColors,"",t.live),u=K(y.perWafer.map($=>$.summary.wafer)),g=a.title??(u.lotId!==void 0?`Lot Summary \u2014 ${u.lotId}`:`Summary \u2014 ${Qe(u)}`);return`<!DOCTYPE html>
34
37
  <html lang="en">
35
38
  <head>
36
39
  <meta charset="UTF-8">
37
- <title>${y(S)}</title>
40
+ <title>${S(g)}</title>
38
41
  <style>
39
- ${N()}
42
+ ${j()}
40
43
  </style>
41
44
  </head>
42
45
  <body>
43
- ${ie(S,d,f)}
46
+ ${me(g,l,m)}
47
+ ${t.live?k:""}
44
48
  </body>
45
- </html>`}const c=o.title??"Summary",g=s.map(h=>{const{lotSummary:d,sections:p}=ae(h.items,n,r,e,i,l,u),S=h.varying.includes("lot")?void 0:d.lot?.lot??d.lot?.lotId,x=S?`${h.label} \xB7 Lot ${String(S)}`:h.label;return ie(`${c} \u2014 ${x}`,p,f)}),b=`<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>`,B=g.map((h,d)=>`${d===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong);">'}
46
- ${h}`).join(`
49
+ </html>`}const p=a.title??"Summary",h=o.map((y,l)=>{const u=`g${l+1}-`,{lotSummary:g,sections:$}=fe(y.items,n,r,e,c,s,d,t.binColors,u,t.live),v=y.varying.includes("lot")?void 0:g.lot?.lot??g.lot?.lotId,x=v?`${y.label} \xB7 Lot ${String(v)}`:y.label;return me(`${p} \u2014 ${x}`,$,m,u)}),b=`<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 ${o.length} groups by identity \u2014 shown separately below so stats are never pooled across them.</p>`,w=h.map((y,l)=>`${l===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong);">'}
50
+ ${y}`).join(`
47
51
  `);return`<!DOCTYPE html>
48
52
  <html lang="en">
49
53
  <head>
50
54
  <meta charset="UTF-8">
51
- <title>${y(c)}</title>
55
+ <title>${S(p)}</title>
52
56
  <style>
53
- ${N()}
57
+ ${j()}
54
58
  </style>
55
59
  </head>
56
60
  <body>
57
61
  <div class="report" style="padding-top:24px;">
58
62
  ${b}
59
63
  </div>
60
- ${B}
64
+ ${w}
65
+ ${t.live?k:""}
61
66
  </body>
62
- </html>`}export{Te 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??(j(t)?Ye(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)?We([...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:j(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})}
67
+ </html>`}export{ke as openHtmlReport};export function renderWaferReportHtml(t,a,i={}){return renderSummaryReportHtml({wafer:t.wafer,dies:t.dies,yieldSummary:t.yield,dataCoverage:t.dataCoverage,hbinDefs:t.hbinDefs,sbinDefs:t.sbinDefs,testDefs:t.testDefs,statsSummary:a??t.statsSummary??(Q(t)?Ae(t):void 0),passBins:[...t.passBins],ringCount:t.ringCount,binColors:i.binColors,live:i.live},i)}export function renderLotReportHtml(t,a={}){const i=_(t.map(e=>e.hbinDefs)),n=_(t.map(e=>e.sbinDefs)),r=t.some(e=>e.testDefs?.length)?qe([...t]).defs:void 0;return renderLotSummaryReportHtml({items:t.map((e,c)=>({label:O(e,c),wafer:e.wafer,dies:e.dies,passBins:[...e.passBins],statsSummary:e.statsSummary,source:Q(e)?e:void 0})),hbinDefs:i.length?i:void 0,sbinDefs:n.length?n:void 0,testDefs:r,ringCount:t[0]?.ringCount,analyzeOptions:a.analyzeOptions,binColors:a.binColors,live:a.live},{title:a.title})}
@@ -1,4 +1,7 @@
1
1
  import type { StatsFinding, StatsSeverity } from './types.js';
2
+ import type { Synthesis } from './synthesis.js';
3
+ import { type MeterTier } from './presentation.js';
4
+ export type { MeterTier };
2
5
  import { type FacetItem } from './facets.js';
3
6
  export interface MetricItem {
4
7
  label: string;
@@ -31,19 +34,38 @@ export declare function buildMetadataRows(items: FacetItem[]): Array<{
31
34
  /** `buildMetadataRows` wrapped in a titled section. Returns `''` when there's nothing to show. */
32
35
  export declare function renderMetadataSection(items: FacetItem[]): string;
33
36
  export declare function renderMetricGrid(items: MetricItem[]): string;
34
- export declare function renderTable(headers: string[], rows: string[][], options?: {
37
+ /** A table cell: raw HTML (the caller escapes), or HTML with a class for the `<td>`. */
38
+ export type TableCell = string | {
39
+ html: string;
40
+ className?: string;
41
+ };
42
+ export declare function renderTable(headers: string[], rows: TableCell[][], options?: {
35
43
  className?: string;
36
44
  emptyMessage?: string;
37
45
  }): string;
38
- export declare function renderSeverityBadge(severity: StatsSeverity): string;
39
- export declare function formatFindingDelta(finding: Pick<StatsFinding, 'effect' | 'variable'>): string;
40
46
  /**
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.
47
+ * Three dots filled by level, with the word beneath. Colour only reinforces it: the dots and the
48
+ * word survive a black-and-white print and colour-blind reading (WCAG 1.4.1), which a bare coloured
49
+ * badge did not. The dots are decoration, hidden from a screen reader that reads the word.
50
+ */
51
+ export declare function renderMeter(tier: MeterTier, word: string): string;
52
+ export declare function renderSeverityMeter(severity: StatsSeverity): string;
53
+ export declare const severityTier: (severity: StatsSeverity) => MeterTier;
54
+ /**
55
+ * A value with a bar behind it, for scanning a column of yields or shares. The figure is always
56
+ * printed; the bar is a second way to read it, scaled 0–100 (so a yield bar is never stretched
57
+ * to its own column's range).
45
58
  */
46
- export declare function formatFindingTooltip(finding: StatsFinding): string;
59
+ export declare function barCell(percent: number, text: string, className?: string, color?: string): TableCell;
60
+ /** A signed difference in yield points, tinted by `shortfallStep` (only a shortfall of a point or more). */
61
+ export declare function deltaCell(points: number): TableCell;
62
+ /**
63
+ * Section ids and a contents line for a report body. Every section is produced by `renderSection`,
64
+ * so its `<h2>` is the title; the ids carry `scope` so several reports in one document cannot clash.
65
+ * Under four sections there is nothing to navigate and the body is returned unchanged.
66
+ */
67
+ export declare function withSectionNav(sectionsHtml: string, scope?: string): string;
68
+ export declare function formatFindingDelta(finding: Pick<StatsFinding, 'effect' | 'variable'>): string;
47
69
  /**
48
70
  * The `†` key under a findings table, as HTML — `''` when no finding in it is
49
71
  * about a derived test. A printed report has no hover, so unlike the on-screen
@@ -64,7 +86,30 @@ export declare function derivedFindingsKeyHtml(findings: StatsFinding[]): string
64
86
  * `totalWafers` switches the coverage column from "N (region/rest)" to a count
65
87
  * of wafers, for lot-level findings.
66
88
  */
67
- export declare function findingsTableHtml(findings: StatsFinding[], totalWafers?: number): string;
89
+ /**
90
+ * The anchor a finding's table row carries when the table is the page's one table for that
91
+ * summary. Injective (every character outside A–Z a–z 0–9 becomes `_` and its hex code), so two
92
+ * finding ids can never share an anchor, and valid in an `id` attribute whatever the id holds.
93
+ */
94
+ export declare function findingAnchor(id: string, scope?: string): string;
95
+ /**
96
+ * `anchorScope`: give each row an `id` so the synthesis can link to it, prefixed so two summaries in
97
+ * one document cannot share one. Only for the page's one table per summary — a per-wafer table
98
+ * repeats ids that another wafer's table also carries — so it is left out there.
99
+ */
100
+ export declare function findingsTableHtml(findings: StatsFinding[], totalWafers?: number, anchorScope?: string, live?: boolean): string;
101
+ /**
102
+ * The synthesis as a section: the headline, the loss items each linked to the finding rows that
103
+ * carry their figures, and what was compared. A part links only when its row is on the page
104
+ * (`anchorIds`), so a sentence never points at nothing.
105
+ */
106
+ export declare function synthesisSectionHtml(synthesis: Synthesis, anchorIds: ReadonlySet<string>, scope?: string): string;
107
+ /**
108
+ * For a report shown inside the app (the panel's modal): a click on a finding's row tells the page that
109
+ * holds the report, which selects that finding on the map. Inert when the report is opened on its own
110
+ * (no parent window) or where scripts are blocked, and the rows stay a plain table either way.
111
+ */
112
+ export declare const LIVE_FINDINGS_SCRIPT = "<script>\ndocument.addEventListener('click', function (e) {\n var row = e.target.closest && e.target.closest('[data-finding]');\n if (row && window.parent !== window) window.parent.postMessage({ wmapFinding: row.getAttribute('data-finding') }, '*');\n});\n</script>";
68
113
  export declare function formatFindingCoverage(finding: Pick<StatsFinding, 'stats'>, total?: number): string;
69
114
  export declare function reportStyles(): string;
70
115
  //# sourceMappingURL=reportHtml.d.ts.map
@@ -1,25 +1,26 @@
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(" ")}">
1
+ import{formatFindingTooltip as f}from"./findingText.js";import{arrangeFindings as u}from"./filterFindings.js";import{SEVERITY_MARK as g,METER_DOTS as x,filledDots as v,impactWord as $,shortfallColor as c,shortfallStep as w,formatPoints as y,barPercent as k}from"./presentation.js";import{fmt as b,plainBinTerms as z}from"../renderer/fmt.js";import{buildFacetTable as S,prettyKey as j}from"./facets.js";import{escHtml as o}from"../core/utils.js";import{derivedKeyText as M,derivedFields as F}from"../renderer/testLabel.js";export function renderSection(t,e,r=""){return`<section class="${["report-section",r].filter(Boolean).join(" ")}">
2
2
  <h2>${o(t)}</h2>
3
3
  ${e}
4
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
5
  `);return`<dl class="${e}">
6
6
  ${r}
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">
7
+ </dl>`}export function buildMetadataRows(t){return S(t,{facetableOnly:t.length>1}).map(r=>({label:j(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
8
  ${t.map(r=>`<div class="metric">
9
9
  <dt>${o(r.label)}</dt>
10
10
  <dd>${o(r.value)}</dd>
11
11
  ${r.hint?`<span class="metric-hint">${o(r.hint)}</span>`:""}
12
12
  </div>`).join(`
13
13
  `)}
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(`
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(" "),p=t.map(l=>`<th>${o(l)}</th>`).join(""),n=l=>typeof l=="string"?`<td>${l}</td>`:`<td${l.className?` class="${l.className}"`:""}>${l.html}</td>`,s=e.map(l=>`<tr>${l.map(n).join("")}</tr>`).join(`
15
15
  `);return`<table class="${a}">
16
- <thead><tr>${i}</tr></thead>
17
- <tbody>${l}</tbody>
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>
16
+ <thead><tr>${p}</tr></thead>
17
+ <tbody>${s}</tbody>
18
+ </table>`}export function renderMeter(t,e){const r=v(t);return`<span class="meter"><span class="meter-dots" aria-hidden="true">${"\u25CF".repeat(r)}${"\u25CB".repeat(x-r)}</span><span class="meter-word">${o(e)}</span></span>`}export function renderSeverityMeter(t){const{tier:e,word:r}=g[t];return renderMeter(e,r)}export const severityTier=t=>g[t].tier;export function barCell(t,e,r="",a){const p=a?`;background:${o(a)}`:"";return{html:`<span class="cellbar"><i style="width:${k(t).toFixed(1)}%${p}"></i><span>${o(e)}</span></span>`,className:["barcell",r].filter(Boolean).join(" ")}}export function deltaCell(t){const e=w(t);return{html:o(y(t)),className:["numeric",e?`low-${e}`:""].filter(Boolean).join(" ")}}export function withSectionNav(t,e=""){const r=[];let a=0;const p=t.replace(/<section class="(report-section[^"]*)">(\s*<h2>)(.*?)(<\/h2>)/g,(s,l,i,d,h)=>{const m=`sec-${e}${++a}`;return r.push({id:m,title:d}),`<section class="${l}" id="${m}">${i}${d}${h}`});return r.length<4?t:`<nav class="report-toc" aria-label="Sections">${r.map(s=>`<a href="#${s.id}">${s.title}</a>`).join("")}</nav>
19
+ ${p}`}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}${b(Math.abs(e),t.variable.unit)}`}return`${r}${b(Math.abs(e),t.variable.unit)}`}export function derivedFindingsKeyHtml(t){const e=M(t.map(r=>({label:r.variable.label,...F(r.variable)})));return e?`<p class="report-note">${o(e)}</p>`:""}export function findingAnchor(t,e=""){return`finding-${e}`+t.replace(/[^A-Za-z0-9]/g,r=>`_${r.charCodeAt(0).toString(16)}_`)}export function findingsTableHtml(t,e,r,a=!1){const p=u(t),n=[...p.patterns.flatMap(i=>[{f:i.finding,nested:!1},...i.children.map(d=>({f:d,nested:!0}))]),...p.groups.flatMap(i=>i.findings.map(d=>({f:d,nested:!1})))],s=n.length?n.map(({f:i,nested:d})=>`<tr class="tier-${severityTier(i.severity)}${d?" nested":""}${a?" live":""}"${r!==void 0?` id="${findingAnchor(i.id,r)}"`:""}${a?` data-finding="${o(i.id)}"`:""} title="${o(f(i))}${a?`
20
+ Click to show it on the map`:""}">
21
+ <td class="tight">${renderSeverityMeter(i.severity)}</td>
21
22
  <td class="tight">${o(i.comparison.left)}</td>
22
- <td>${o(d(i.variable.label))}</td>
23
+ <td>${o(z(i.variable.label))}</td>
23
24
  <td class="numeric">${o(formatFindingDelta(i))}</td>
24
25
  <td class="numeric">${o(formatFindingCoverage(i,e))}</td>
25
26
  </tr>`).join(`
@@ -34,9 +35,21 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
34
35
  </tr>
35
36
  </thead>
36
37
  <tbody>
37
- ${r}
38
+ ${s}
38
39
  </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`
40
+ </table>${derivedFindingsKeyHtml(t)}`}export function synthesisSectionHtml(t,e,r=""){const a=n=>n.map(s=>s.target&&e.has(s.target.id)?`<a href="#${findingAnchor(s.target.id,r)}">${o(s.text)}</a>`:o(s.text)).join(""),p=t.items.length?`<ol class="synthesis-items">
41
+ ${t.items.map(n=>`<li class="tier-${n.impact}">${renderMeter(n.impact,$(n.impact))}<span>${a(n.parts)}</span></li>`).join(`
42
+ `)}
43
+ </ol>`:`<p class="synthesis-none">${o(t.nothing??"Nothing stands out.")}</p>`;return renderSection("What stands out",`<p class="synthesis-headline">${a(t.headline)}</p>
44
+ ${p}
45
+ ${t.also?`<p class="synthesis-also">${a(t.also)}</p>`:""}
46
+ ${t.watch?`<ul class="synthesis-watch">${t.watch.map(n=>`<li><strong>Watch</strong> ${a(n.parts)}</li>`).join("")}</ul>`:""}
47
+ <p class="synthesis-checked">${o(t.checked)}</p>`,"synthesis")}export const LIVE_FINDINGS_SCRIPT=`<script>
48
+ document.addEventListener('click', function (e) {
49
+ var row = e.target.closest && e.target.closest('[data-finding]');
50
+ if (row && window.parent !== window) window.parent.postMessage({ wmapFinding: row.getAttribute('data-finding') }, '*');
51
+ });
52
+ <\/script>`;export function formatFindingCoverage(t,e){return e!==void 0?`${t.stats.sampleSizeLeft}/${e}`:`${t.stats.sampleSizeLeft}/${t.stats.sampleSizeRight}`}export function reportStyles(){return`
40
53
  :root {
41
54
  --report-font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
42
55
  --report-text: #1f2328;
@@ -46,9 +59,18 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
46
59
  --report-line-strong: #c7ced8;
47
60
  --report-surface: #f7f8fa;
48
61
  --report-surface-alt: #fbfcfd;
49
- --report-severity-unusual: #8b3f35;
50
- --report-severity-notable: #8b6428;
51
- --report-severity-info: #446883;
62
+ /* A tint says "look here"; the dots and the word say what. Text on a tint is always full ink:
63
+ the muted grey is 3.3:1 on the strongest pastel, below AA. */
64
+ --report-tier-high-bg: #fbe4e2;
65
+ --report-tier-high-edge: #b4372f;
66
+ --report-tier-medium-bg: #fdf0d5;
67
+ --report-tier-medium-edge: #a8741a;
68
+ --report-tier-low-bg: #edf2f7;
69
+ --report-tier-low-edge: #5f7f9f;
70
+ --report-low-1: ${c(1)};
71
+ --report-low-2: ${c(2)};
72
+ --report-low-3: ${c(3)};
73
+ --report-bar: #9db8d3;
52
74
  }
53
75
 
54
76
  html {
@@ -95,6 +117,29 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
95
117
 
96
118
  /* Sub-headings and notes inside a section \u2014 used by the split comparison,
97
119
  which nests one block per facet under a single section heading. */
120
+ .synthesis-headline { font-weight: 600; margin: 0 0 8px; }
121
+ .synthesis-items { list-style: none; margin: 0 0 8px; padding: 0; }
122
+ .synthesis-items li {
123
+ display: grid;
124
+ grid-template-columns: 96px 1fr;
125
+ gap: 10px;
126
+ align-items: start;
127
+ margin: 0 0 8px;
128
+ padding: 9px 12px;
129
+ border-left: 4px solid var(--report-tier-low-edge);
130
+ border-radius: 0 4px 4px 0;
131
+ background: var(--report-tier-low-bg);
132
+ line-height: 1.45;
133
+ }
134
+ .synthesis-items li.tier-high { border-color: var(--report-tier-high-edge); background: var(--report-tier-high-bg); }
135
+ .synthesis-items li.tier-medium { border-color: var(--report-tier-medium-edge); background: var(--report-tier-medium-bg); }
136
+ .synthesis-also { margin: 0 0 6px; }
137
+ .synthesis-watch { list-style: none; margin: 0 0 6px; padding: 0; }
138
+ .synthesis-watch li { margin: 0 0 3px; }
139
+ .synthesis-watch strong { margin-right: 6px; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--report-muted); }
140
+ .synthesis-checked, .synthesis-none { color: var(--report-muted); margin: 0; }
141
+ .synthesis a { color: inherit; text-decoration: underline dotted; }
142
+
98
143
  .report-subheading {
99
144
  margin: 14px 0 6px;
100
145
  font-size: 13px;
@@ -139,14 +184,6 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
139
184
  font-style: italic;
140
185
  }
141
186
 
142
- .findings-narrative {
143
- margin: 0 0 14px;
144
- color: var(--report-muted);
145
- font-size: 13px;
146
- font-style: italic;
147
- line-height: 1.6;
148
- }
149
-
150
187
  .definition-item {
151
188
  display: contents;
152
189
  }
@@ -268,39 +305,83 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
268
305
  font-size: 12px;
269
306
  }
270
307
 
271
- .badge {
308
+ /* Tiles for the key figures, so they read as a row of facts and not a line of the list. */
309
+ .metric-grid .metric {
310
+ padding: 10px 12px;
311
+ background: var(--report-surface);
312
+ border: 1px solid var(--report-line);
313
+ border-radius: 6px;
314
+ }
315
+
316
+ .report-toc {
317
+ display: flex;
318
+ flex-wrap: wrap;
319
+ gap: 4px 16px;
320
+ margin: 0 0 18px;
321
+ padding: 8px 0;
322
+ border-top: 1px solid var(--report-line);
323
+ border-bottom: 1px solid var(--report-line);
324
+ font-size: 12px;
325
+ }
326
+
327
+ .report-toc a { color: var(--report-muted); text-decoration: none; }
328
+ .report-toc a:hover { color: var(--report-text); text-decoration: underline; }
329
+
330
+ /* The value is always printed; the bar behind it is for scanning a column. */
331
+ table.report-table td.barcell { min-width: 150px; }
332
+ .cellbar { position: relative; display: block; }
333
+ .cellbar i {
334
+ position: absolute;
335
+ left: 0;
336
+ top: 50%;
337
+ height: 12px;
338
+ transform: translateY(-50%);
339
+ background: var(--report-bar);
340
+ border-radius: 0 2px 2px 0;
341
+ }
342
+ .cellbar span { position: relative; padding-left: 4px; }
343
+
344
+ /* Shortfall below the reference, light to strong. Full ink on every tint (see the tier tokens). */
345
+ table.report-table td.low-1 { background: var(--report-low-1); }
346
+ table.report-table td.low-2 { background: var(--report-low-2); }
347
+ table.report-table td.low-3 { background: var(--report-low-3); }
348
+ table.report-table td.muted-row, table.report-table td.muted-row .cellbar { color: var(--report-muted); }
349
+ .report-legend { margin: 6px 0 0; color: var(--report-muted); font-size: 11px; }
350
+
351
+ .meter {
272
352
  display: inline-flex;
273
- align-items: center;
274
- justify-content: center;
275
- min-width: 58px;
276
- padding: 1px 6px;
277
- border: 1px solid transparent;
278
- border-radius: 999px;
279
- font-size: 11px; /* print floor \u2014 see the table-header note above */
280
- font-weight: 600;
281
- line-height: 1.35;
282
- letter-spacing: 0.04em;
283
- text-transform: uppercase;
353
+ flex-direction: column;
354
+ align-items: flex-start;
355
+ gap: 1px;
356
+ line-height: 1.2;
284
357
  white-space: nowrap;
285
358
  }
286
359
 
287
- .badge-unusual {
288
- color: var(--report-severity-unusual);
289
- background: #f4eeec;
290
- border-color: #d9c0bb;
360
+ .meter-dots {
361
+ font-size: 12px;
362
+ letter-spacing: 2px;
291
363
  }
292
364
 
293
- .badge-notable {
294
- color: var(--report-severity-notable);
295
- background: #f5f0e6;
296
- border-color: #ddcfb5;
365
+ .meter-word {
366
+ color: var(--report-text);
367
+ font-size: 11px;
368
+ font-weight: 600;
369
+ letter-spacing: 0.04em;
297
370
  }
298
371
 
299
- .badge-info {
300
- color: var(--report-severity-info);
301
- background: #eef4f8;
302
- border-color: #cfdae4;
303
- }
372
+ /* In the app, a row shows its finding on the map. */
373
+ table.findings-table tr.live { cursor: pointer; }
374
+ table.findings-table tr.live:hover td { box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.04); }
375
+
376
+ /* A finding a pattern explains sits beneath it, indented. */
377
+ table.findings-table tr.nested td:first-child { padding-left: 22px; }
378
+ table.findings-table tr.nested td { font-size: 12px; }
379
+
380
+ /* Findings table: the row carries the tier as a tint and a left edge; the meter carries it in words. */
381
+ table.findings-table tr.tier-high td { background: var(--report-tier-high-bg); }
382
+ table.findings-table tr.tier-medium td { background: var(--report-tier-medium-bg); }
383
+ table.findings-table tr.tier-high td:first-child { box-shadow: inset 4px 0 0 var(--report-tier-high-edge); }
384
+ table.findings-table tr.tier-medium td:first-child { box-shadow: inset 4px 0 0 var(--report-tier-medium-edge); }
304
385
 
305
386
  .footer {
306
387
  margin-top: 14px;
@@ -325,6 +406,8 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
325
406
  padding: 0;
326
407
  }
327
408
 
409
+ .report-toc { display: none; }
410
+
328
411
  section.report-section,
329
412
  tr {
330
413
  break-inside: avoid;