@wafertools/wafermap 0.32.0 → 0.34.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 (167) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +374 -1
  3. package/README.md +5 -5
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +15 -10
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -1
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +7 -0
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/binCluster.d.ts +7 -1
  9. package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  12. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +57 -15
  13. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  14. package/dist/packages/canvas-adapter/charts/correlation.js +1 -2
  15. package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +2 -0
  16. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/histogram.d.ts +16 -0
  18. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  19. package/dist/packages/canvas-adapter/charts/palette.d.ts +2 -0
  20. package/dist/packages/canvas-adapter/charts/palette.js +1 -1
  21. package/dist/packages/canvas-adapter/charts/plotChart.d.ts +27 -0
  22. package/dist/packages/canvas-adapter/charts/plotChart.js +1 -0
  23. package/dist/packages/canvas-adapter/charts/pointInteractions.d.ts +45 -0
  24. package/dist/packages/canvas-adapter/charts/pointInteractions.js +1 -0
  25. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.d.ts +2 -0
  26. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  27. package/dist/packages/canvas-adapter/charts/scatter.d.ts +19 -0
  28. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  29. package/dist/packages/canvas-adapter/charts/sweep.d.ts +12 -0
  30. package/dist/packages/canvas-adapter/charts/sweep.js +1 -1
  31. package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +7 -0
  32. package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -1
  33. package/dist/packages/canvas-adapter/charts/trend.d.ts +11 -0
  34. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  35. package/dist/packages/canvas-adapter/confirmDialog.d.ts +13 -0
  36. package/dist/packages/canvas-adapter/confirmDialog.js +1 -0
  37. package/dist/packages/canvas-adapter/dataTab.d.ts +131 -0
  38. package/dist/packages/canvas-adapter/dataTab.js +1 -0
  39. package/dist/packages/canvas-adapter/dieList.d.ts +39 -4
  40. package/dist/packages/canvas-adapter/dieList.js +6 -7
  41. package/dist/packages/canvas-adapter/drilldown.d.ts +20 -3
  42. package/dist/packages/canvas-adapter/drilldown.js +1 -1
  43. package/dist/packages/canvas-adapter/guideWindow.d.ts +96 -0
  44. package/dist/packages/canvas-adapter/guideWindow.js +1 -0
  45. package/dist/packages/canvas-adapter/identityHeader.d.ts +16 -0
  46. package/dist/packages/canvas-adapter/identityHeader.js +1 -1
  47. package/dist/packages/canvas-adapter/index.d.ts +3 -3
  48. package/dist/packages/canvas-adapter/index.js +1 -1
  49. package/dist/packages/canvas-adapter/insightsTab.d.ts +73 -54
  50. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  51. package/dist/packages/canvas-adapter/maplessSummary.d.ts +2 -2
  52. package/dist/packages/canvas-adapter/plotEditor.d.ts +29 -0
  53. package/dist/packages/canvas-adapter/plotEditor.js +1 -0
  54. package/dist/packages/canvas-adapter/plotItems.d.ts +32 -0
  55. package/dist/packages/canvas-adapter/plotItems.js +1 -0
  56. package/dist/packages/canvas-adapter/plotModal.d.ts +30 -0
  57. package/dist/packages/canvas-adapter/plotModal.js +1 -0
  58. package/dist/packages/canvas-adapter/plotStore.d.ts +52 -0
  59. package/dist/packages/canvas-adapter/plotStore.js +1 -0
  60. package/dist/packages/canvas-adapter/plotTab.d.ts +30 -0
  61. package/dist/packages/canvas-adapter/plotTab.js +1 -0
  62. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +3 -2
  63. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  64. package/dist/packages/canvas-adapter/renderWaferMap.d.ts +51 -2
  65. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  66. package/dist/packages/canvas-adapter/summaryPanel.d.ts +49 -11
  67. package/dist/packages/canvas-adapter/summaryPanel.js +2 -3
  68. package/dist/packages/canvas-adapter/summaryRail.d.ts +7 -0
  69. package/dist/packages/canvas-adapter/summaryRail.js +1 -0
  70. package/dist/packages/canvas-adapter/sweepEditor.d.ts +17 -0
  71. package/dist/packages/canvas-adapter/sweepEditor.js +1 -0
  72. package/dist/packages/canvas-adapter/tableExport.d.ts +22 -0
  73. package/dist/packages/canvas-adapter/tableExport.js +3 -0
  74. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  75. package/dist/packages/canvas-adapter/toolbar.d.ts +74 -93
  76. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  77. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  78. package/dist/packages/canvas-adapter/userGuideHtml.js +409 -51
  79. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  80. package/dist/packages/canvas-adapter/version.js +1 -1
  81. package/dist/packages/canvas-adapter/viewSwitch.d.ts +13 -0
  82. package/dist/packages/canvas-adapter/viewSwitch.js +1 -0
  83. package/dist/packages/canvas-adapter/virtualTable.d.ts +44 -0
  84. package/dist/packages/canvas-adapter/virtualTable.js +1 -0
  85. package/dist/packages/core/aggregates.d.ts +6 -0
  86. package/dist/packages/core/aggregates.js +1 -1
  87. package/dist/packages/core/classify.d.ts +3 -2
  88. package/dist/packages/core/classify.js +1 -1
  89. package/dist/packages/core/compact.d.ts +205 -0
  90. package/dist/packages/core/compact.js +3 -0
  91. package/dist/packages/core/passBins.d.ts +23 -4
  92. package/dist/packages/core/passBins.js +1 -1
  93. package/dist/packages/core/ringCount.d.ts +22 -0
  94. package/dist/packages/core/ringCount.js +1 -0
  95. package/dist/packages/core/tableCsv.d.ts +43 -0
  96. package/dist/packages/core/tableCsv.js +3 -0
  97. package/dist/packages/renderer/axisTicks.d.ts +14 -0
  98. package/dist/packages/renderer/axisTicks.js +1 -1
  99. package/dist/packages/renderer/binColors.d.ts +6 -6
  100. package/dist/packages/renderer/binColors.js +1 -1
  101. package/dist/packages/renderer/buildView.d.ts +27 -2
  102. package/dist/packages/renderer/buildView.js +1 -1
  103. package/dist/packages/renderer/buildWaferMap.js +1 -1
  104. package/dist/packages/renderer/compactView.d.ts +22 -0
  105. package/dist/packages/renderer/compactView.js +1 -0
  106. package/dist/packages/renderer/xyIndicator.d.ts +27 -0
  107. package/dist/packages/renderer/xyIndicator.js +1 -0
  108. package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
  109. package/dist/packages/stats/analyzeWaferLot.js +1 -1
  110. package/dist/packages/stats/analyzeWaferMap.d.ts +36 -1
  111. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  112. package/dist/packages/stats/binPareto.d.ts +1 -1
  113. package/dist/packages/stats/binPareto.js +1 -1
  114. package/dist/packages/stats/binRows.d.ts +38 -0
  115. package/dist/packages/stats/binRows.js +1 -0
  116. package/dist/packages/stats/filterFindings.d.ts +25 -0
  117. package/dist/packages/stats/filterFindings.js +1 -1
  118. package/dist/packages/stats/findingText.d.ts +9 -0
  119. package/dist/packages/stats/findingText.js +2 -0
  120. package/dist/packages/stats/histogram.d.ts +27 -0
  121. package/dist/packages/stats/histogram.js +1 -1
  122. package/dist/packages/stats/index.d.ts +2 -0
  123. package/dist/packages/stats/index.js +1 -1
  124. package/dist/packages/stats/lotDrift.d.ts +16 -0
  125. package/dist/packages/stats/lotDrift.js +1 -0
  126. package/dist/packages/stats/lotPattern.d.ts +33 -0
  127. package/dist/packages/stats/lotPattern.js +1 -0
  128. package/dist/packages/stats/math.d.ts +60 -6
  129. package/dist/packages/stats/math.js +1 -1
  130. package/dist/packages/stats/patternClassification.d.ts +32 -1
  131. package/dist/packages/stats/patternClassification.js +1 -1
  132. package/dist/packages/stats/plotData.d.ts +258 -0
  133. package/dist/packages/stats/plotData.js +1 -0
  134. package/dist/packages/stats/plotId.d.ts +3 -0
  135. package/dist/packages/stats/plotId.js +1 -0
  136. package/dist/packages/stats/plotSpec.d.ts +119 -0
  137. package/dist/packages/stats/plotSpec.js +4 -0
  138. package/dist/packages/stats/presentation.d.ts +44 -0
  139. package/dist/packages/stats/presentation.js +1 -0
  140. package/dist/packages/stats/regions.d.ts +15 -0
  141. package/dist/packages/stats/regions.js +1 -1
  142. package/dist/packages/stats/renderSummaryReport.d.ts +23 -5
  143. package/dist/packages/stats/renderSummaryReport.js +32 -27
  144. package/dist/packages/stats/reportHtml.d.ts +54 -9
  145. package/dist/packages/stats/reportHtml.js +145 -55
  146. package/dist/packages/stats/scatter.d.ts +6 -0
  147. package/dist/packages/stats/scatter.js +1 -1
  148. package/dist/packages/stats/sweep.d.ts +37 -1
  149. package/dist/packages/stats/sweep.js +1 -1
  150. package/dist/packages/stats/sweepText.d.ts +14 -0
  151. package/dist/packages/stats/sweepText.js +1 -0
  152. package/dist/packages/stats/sweepXFromName.d.ts +14 -0
  153. package/dist/packages/stats/sweepXFromName.js +1 -1
  154. package/dist/packages/stats/synthesis.d.ts +76 -0
  155. package/dist/packages/stats/synthesis.js +2 -0
  156. package/dist/packages/stats/testPassRate.d.ts +6 -0
  157. package/dist/packages/stats/testPassRate.js +1 -1
  158. package/dist/packages/stats/trend.js +1 -1
  159. package/dist/packages/stats/types.d.ts +2 -2
  160. package/dist/packages/stats/yield.d.ts +15 -6
  161. package/dist/packages/stats/yield.js +1 -1
  162. package/dist/packages/stats/yieldRows.d.ts +31 -0
  163. package/dist/packages/stats/yieldRows.js +1 -0
  164. package/llms.txt +16 -4
  165. package/package.json +3 -3
  166. package/dist/packages/stats/findingsNarrative.d.ts +0 -3
  167. package/dist/packages/stats/findingsNarrative.js +0 -1
@@ -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';
@@ -17,8 +18,14 @@ export interface SummaryReportParams {
17
18
  sbinDefs?: BinDef[];
18
19
  testDefs?: TestDef[];
19
20
  statsSummary?: StatsSummary;
20
- passBins?: number[];
21
- ringCount?: number;
21
+ /** The map's own pass bins (`WaferMapResult.passBins`): required, there is no default to judge by instead. */
22
+ passBins: number[];
23
+ /** The map's own ring count (`WaferMapResult.ringCount`): required, there is no default. */
24
+ ringCount: number;
25
+ /** @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. */
26
+ binColors?: BinColors;
27
+ /** @internal Shown inside the app: a click on a finding's row selects it on the map (see `LIVE_FINDINGS_SCRIPT`). */
28
+ live?: boolean;
22
29
  }
23
30
  /** Generate a full wafer summary as a standalone HTML string suitable for `openHtmlReport()`. */
24
31
  export declare function renderSummaryReportHtml(params: SummaryReportParams, options?: {
@@ -34,6 +41,8 @@ export interface LotSummaryReportParams {
34
41
  dies?: Die[];
35
42
  /** This wafer's own pass bins (`WaferMapResult.passBins`). Omitted ⇒ the top-level `passBins`. */
36
43
  passBins?: number[];
44
+ /** This wafer's own ring count (`WaferMapResult.ringCount`). Omitted ⇒ the top-level `ringCount`. */
45
+ ringCount?: number;
37
46
  /** Reused directly as `analyzeWaferLot`'s `perWaferSummaries` — the expensive
38
47
  * per-wafer pass (`analyzeWaferMap`) is never re-run here. */
39
48
  statsSummary?: StatsSummary;
@@ -49,11 +58,16 @@ export interface LotSummaryReportParams {
49
58
  hbinDefs?: BinDef[];
50
59
  sbinDefs?: BinDef[];
51
60
  testDefs?: TestDef[];
52
- /** Fallback for items that carry no `passBins` of their own. Default `[1]`. */
61
+ /** For items that carry no `passBins` of their own (a hand-built map). No default: an item with neither is refused. */
53
62
  passBins?: number[];
63
+ /** For items that carry no `ringCount` of their own (a hand-built map); none ⇒ the first item's. No default. */
54
64
  ringCount?: number;
55
65
  /** Passthrough to the internal per-group `analyzeWaferLot` call, e.g. `{ enableTestValueAnalysis: true }`. */
56
66
  analyzeOptions?: AnalyzeWaferMapOptions;
67
+ /** @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. */
68
+ binColors?: BinColors;
69
+ /** @internal Shown inside the app: a click on a finding's row selects it on the map. */
70
+ live?: boolean;
57
71
  }
58
72
  /**
59
73
  * Generate a full lot summary as a standalone HTML string suitable for
@@ -86,7 +100,9 @@ export declare function renderWaferReportHtml(map: ReportMap & {
86
100
  yield: YieldSummary;
87
101
  dataCoverage: SummaryReportParams['dataCoverage'];
88
102
  }, summary?: StatsSummary, options?: {
89
- title?: string;
103
+ title?: string; /** @internal */
104
+ binColors?: BinColors; /** @internal */
105
+ live?: boolean;
90
106
  }): string;
91
107
  /**
92
108
  * The lot summary report for several built maps, as a standalone HTML document.
@@ -96,6 +112,8 @@ export declare function renderWaferReportHtml(map: ReportMap & {
96
112
  */
97
113
  export declare function renderLotReportHtml(maps: readonly ReportMap[], options?: {
98
114
  title?: string;
99
- analyzeOptions?: AnalyzeWaferMapOptions;
115
+ analyzeOptions?: AnalyzeWaferMapOptions; /** @internal */
116
+ binColors?: BinColors; /** @internal */
117
+ live?: boolean;
100
118
  }): string;
101
119
  //# 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 _}from"../core/waferLabel.js";import{binPassSets as Re,binPassSetsByWafer as We,mergeBinDefs as Q,resolveBinColors as Ne,resolveBinColorsByWafer as Ie}from"../renderer/binColors.js";import{commonPassBins as Me,itemPassBins as M,passBinsLabel as Le}from"../core/passBins.js";import{itemRingCount as Ee}from"../core/ringCount.js";import{isParametricTest as ke,isBuiltMap as V}from"../renderer/buildWaferMap.js";import{buildRingRegions as U,buildQuadrantRegions as K,buildRegionYieldData as He}from"./regions.js";import{openHtmlReport as je}from"./renderFindingsReport.js";import{analyzeWaferLot as Ae}from"./analyzeWaferLot.js";import{computeFunctionalYield as qe,analyzeWaferMap as ze}from"./analyzeWaferMap.js";import{mergeTestDefs as Ge}from"./mergeTestDefs.js";import{buildFacetTable as Oe,facetValueOf as _e,FACET_NONE_VALUE as Qe}from"./facets.js";import{visibleFindings as L}from"./filterFindings.js";import{buildYieldDataCombined as Ve}from"./yield.js";import{describeWaferPopulation as J,populationLabel as Ue}from"./population.js";import{buildTestPassRateData as Ke,hasJudgeableTests as Je,poolFunctionalYield as Xe}from"./testPassRate.js";import{buildCapabilityData as Ze}from"./capability.js";import{buildSynthesis as et}from"./synthesis.js";import{binBreakdownRows as tt,binBreakdownTitle as nt,binCountsFrom as st,pooledBinCounts as at,totalOf as it}from"./binRows.js";import{MEAN_WAFER_YIELD_LABEL as ot,shortfallLegend as E}from"./presentation.js";import{regionYieldRows as X,waferYieldRows as rt}from"./yieldRows.js";import{describeValues as lt}from"./math.js";import{fmt as C}from"../renderer/fmt.js";import{getDieKey as ct,isPositionedDie as dt,diePassStatus as Z}from"../core/dies.js";import{testValue as ut}from"../core/dieTable.js";import{findingsTableHtml as ee,synthesisSectionHtml as ft,LIVE_FINDINGS_SCRIPT as k,barCell as R,deltaCell as H,withSectionNav as te,renderMetadataSection as ne,renderMetricGrid as se,renderSection as D,renderTable as P,reportStyles as j}from"./reportHtml.js";import{escHtml as S}from"../core/utils.js";function ae(t,a){return a===0?"\u2014":`${(t/a*100).toFixed(1)}%`}function mt(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 ie(t,a,i,n,r){if(!t.size)return"";const e=tt(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)),R(c.percent,`${c.percent.toFixed(1)}%`,s,r?.get(c.bin))]});return D(nt(i,it(t)),P(["Bin","Name","Dies","Share"],e,{className:"compact"}))}function pt(t,a,i,n,r,e){const c=Re(t,r);return["hard","soft"].map(s=>ie(st(t,s,s==="hard"?n.hard:n.soft),s==="hard"?a:i,s,c[s],e[s])).join(`
2
+ `)}function oe(t,a,i,n){const r=new Set(n),e=new Map(i.map(o=>[ct(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,g=0;for(const b of o.dieKeys){const w=e.get(b);if(!w||w.partial||w.edgeExcluded)continue;const y=Z(w,r);y!==void 0&&(g++,y&&p++)}g&&s.push({label:o.label,pass:p,total:g})}if(!s.length)return"";const{rows:d}=X(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),R(o.yieldPercent,ae(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 gt(t,a){const i=new Float64Array(t.length);let n=0;for(const g of t){const b=ut(g,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}=lt(i.subarray(0,n));return{testNumber:a,count:r,min:e,mean:c,max:s,q1:d,median:m,q3:o,stddev:p}}function re(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(ke)){const u=c.get(l.testNumber)??gt(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"),g=d.some(l=>l.limitYield!==void 0&&l.limitYield!==null),b=(l,u)=>l===void 0?"\u2014":C(l,u),w=["Test","N","Min",...o?["Q1","Median"]:[],"Mean",...o?["Q3"]:[],"Max",...p?["StdDev"]:[],...g?["Limit yield"]:[]],y=d.map(({name:l,unit:u,stats:h,limitYield:$})=>[S(l),{html:String(h.count),className:"numeric"},b(h.min,u),...o?[b(h.q1,u),b(h.median,u)]:[],b(h.mean,u),...o?[b(h.q3,u)]:[],b(h.max,u),...p?[b(h.stddev,u)]:[],...g?[$==null?"\u2014":`${$.toFixed(1)}%`]:[]]);return D("Test Values",P(w,y,{className:"compact"})+`<p class="report-legend">${p?"StdDev is the sample standard deviation (n\u22121). ":""}${r??""}</p>`)}function le(t,a,i){const n=i??qe(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 ce(t,a){const i=Ze(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 ${C(e.lsl,e.unit)} \u2013 USL ${C(e.usl,e.unit)}`:`Lo limit ${C(e.lsl,e.unit)} \u2013 Hi limit ${C(e.usl,e.unit)}`:"\u2014",C(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 de(t,a,i="",n=!1){const r=L(t);return r.length?D("Findings",ee(r,a,i,n)):""}function ue(t,a,i=""){const n=new Set(L(t.findings).map(r=>r.id));return ft(et(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,ringCount:p}=t,g=t.binColors??Ne(n,{passBins:o,hbinDefs:c,sbinDefs:s}),b=i.metadata,w=mt(b),y=a.title??`Wafer Summary${w}`,l=new Date().toLocaleString(),u=n.filter(dt),h=U(u,i,p),$=K(u,i,p),v=[{label:"Total dies",value:String(r.totalDies)},{label:"Filled dies",value:String(e.filledDies),hint:`${ae(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=[ne([{metadata:i.metadata}]),D("Summary",[se(v)].filter(Boolean).join(`
3
+ `)),m?ue(m,o):"",pt(n,c,s,{hard:m?.stats.hardBinCounts,soft:m?.stats.softBinCounts},o,g),oe("Ring Yield",h,n,o),oe("Quadrant Yield",$,n,o),re(n,d,m?.stats.perTestStats,m?.stats.testSpecYield),le(n,d,m?.stats.functionalYield),ce([{dies:n}],d),m?de(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
+ ${te(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 ht(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(rt(i).rows.map(e=>[e.waferIndex,e])),r=t.perWafer.map(e=>{const c=S(_(a[e.waferIndex],e.waferIndex)),s=n.get(e.waferIndex);return s?[s.outlier?`${c} \xB7 ${s.outlier} outlier`:c,R(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 bt(t){const a=[];for(const e of t.perWafer){const c=L(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>`+ee(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 yt=3;function St(t,a,i){const n=t.map(d=>({metadata:d.wafer?.metadata})),r=Oe(n,{facetableOnly:!0}).filter(d=>d.splittable);if(!r.length)return"";const e=r.slice(0,yt),c=[];for(const d of e){const m=new Map;t.forEach((l,u)=>{const h=_e(l.wafer?.metadata,d.key)??Qe;(m.get(h)??m.set(h,[]).get(h)).push(u)});const o=[...m.entries()].map(([l,u])=>({key:l,items:u.map(h=>t[h])}));if(o.length<2)continue;const p=o.map(l=>[S(l.key),S(l.items.map(u=>u.label).join(", "))]),g=Ve(o.map(l=>({key:l.key,items:l.items.map(u=>({label:u.label,dies:u.dies,passBins:M(u,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"],g,{className:"compact"})],w=t.flatMap(l=>l.dies??[]);let y=0;for(const l of["spec","testFlag","functional"]){if(!Je(a,l,w))continue;const u=Ke(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 h=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"],h,{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 wt(t,a,i,n,r,e){const c=We(r);return["hard","soft"].map(s=>ie(at(n,t,s),s==="hard"?a:i,s,c[s],e[s])).join(`
28
+ `)}function fe(t,a,i,n,r,e){const c=He(i,n,r,d=>e[d],a);if(!c.length)return"";const s=X(c).rows.map(d=>[S(d.label),R(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 $t(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,m=s.parts.every(p=>p.sd!==void 0),o=m&&s.n>1?s.parts.reduce((p,g)=>p+Math.max(0,g.n-1)*g.sd**2+g.n*(g.mean-d)**2,0)/(s.n-1):m?0:void 0;return{testNumber:c,count:s.n,min:s.min,max:s.max,mean:d,...o!==void 0?{stddev:Math.sqrt(o)}:{}}})}return re(t,a,r,n,r?"Quartiles are not shown for a lot: they cannot be combined from each wafer\u2019s own.":void 0)}function vt(t,a,i){const n=Xe(i);return le(t,a,n)}const A=["lot","product","testProgram","temperature"];function xt(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 me(t,a,i,n,r,e,c,s,d="",m=!1){const o=Ae(t.map(f=>f.source??f),{perWaferSummaries:t.map(f=>f.statsSummary),...c}),p=[],g=[],b=[],w=[];for(const f of t){const T=f.dies??[];f.wafer&&(p.push(f.wafer),b.push(T),w.push(M(f,r)));for(const F of T)g.push(F)}const y=t.map(f=>({dies:f.dies??[],passBins:M(f,r)})),l=s??Ie(y,{hbinDefs:a,sbinDefs:i}).colors,u=g.some(f=>f.hbin!=null),h=g.some(f=>f.sbin!=null),$=u||h,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,B=0,Y=0,W=0,N=0;for(const f of y){const T=new Set(f.passBins);for(const F of f.dies){if(q++,F.edgeExcluded&&W++,F.partial&&N++,F.partial||F.edgeExcluded)continue;const O=Z(F,T);O!==void 0&&(B++,O&&Y++)}}const ge=B-Y,z=B>0?Y/B*100:null,he=[{label:"Wafers",value:String(o.stats.waferCount)},{label:"Total dies",value:String(q)},...$?[{label:"Good dies",value:String(Y)},{label:"Bad dies",value:String(ge)}]:[],...W>0?[{label:"Edge excluded",value:String(W)}]:[],...N>0?[{label:"Partial dies",value:String(N)}]:[],...x!==null?[{label:ot,value:`${x.toFixed(1)}%`}]:[],...z!==null?[{label:"Total yield",value:`${z.toFixed(1)}%`}]:[]],be=J(o.perWafer.map(f=>f.summary.wafer)),ye=D(be.lotId!==void 0?"Lot Summary":"Summary",se(he)),Se=ne(t.map(f=>({metadata:f.wafer?.metadata}))),we=D("Per-Wafer Yield",ht(o,t)),G=e??Ee(t[0]),$e=St(t,n,r),I=o.perWafer.map(f=>f.summary),ve=$?wt(g,a,i,I,y,l):"",xe=$&&p.length?fe("Ring Yield (All Wafers)",U,b,p,G,w):"",De=$&&p.length?fe("Quadrant Yield (All Wafers)",K,b,p,G,w):"",Pe=n.length?$t(g,n,I,o.stats.testSpecYield):"",Fe=n.length?vt(g,n,I):"",Te=n.length?ce(t,n):"",Ce=o.findings.length?de(o.findings,o.stats.waferCount,d,m):"",Be=bt(o),Ye=[ye,ue(o,Me(y.map(f=>f.passBins)),d),Se,we,$e,ve,xe,De,Pe,Fe,Te,Ce,Be].filter(Boolean).join(`
29
+ `);return{lotSummary:o,sections:Ye}}function pe(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
+ ${te(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,ringCount:s,analyzeOptions:d}=t,m=new Date().toLocaleString(),o=xt(i);if(o.length===1){const{lotSummary:y,sections:l}=me(o[0].items,n,r,e,c,s,d,t.binColors,"",t.live),u=J(y.perWafer.map($=>$.summary.wafer)),h=a.title??(u.lotId!==void 0?`Lot Summary \u2014 ${u.lotId}`:`Summary \u2014 ${Ue(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(h)}</title>
38
41
  <style>
39
- ${N()}
42
+ ${j()}
40
43
  </style>
41
44
  </head>
42
45
  <body>
43
- ${ie(S,d,f)}
46
+ ${pe(h,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",g=o.map((y,l)=>{const u=`g${l+1}-`,{lotSummary:h,sections:$}=me(y.items,n,r,e,c,s,d,t.binColors,u,t.live),v=y.varying.includes("lot")?void 0:h.lot?.lot??h.lot?.lotId,x=v?`${y.label} \xB7 Lot ${String(v)}`:y.label;return pe(`${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=g.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{je 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??(V(t)?ze(t):void 0),passBins:[...t.passBins],ringCount:t.ringCount,binColors:i.binColors,live:i.live},i)}export function renderLotReportHtml(t,a={}){const i=Q(t.map(e=>e.hbinDefs)),n=Q(t.map(e=>e.sbinDefs)),r=t.some(e=>e.testDefs?.length)?Ge([...t]).defs:void 0;return renderLotSummaryReportHtml({items:t.map((e,c)=>({label:_(e,c),wafer:e.wafer,dies:e.dies,passBins:[...e.passBins],statsSummary:e.statsSummary,source:V(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,27 +1,28 @@
1
- import{fmt as p,plainBinTerms as d}from"../renderer/fmt.js";import{buildFacetTable as c,prettyKey as m}from"./facets.js";import{escHtml as o}from"../core/utils.js";import{derivedTestNote as b,derivedKeyText as u,derivedFields as f}from"../renderer/testLabel.js";export function renderSection(t,e,r=""){return`<section class="${["report-section",r].filter(Boolean).join(" ")}">
2
- <h2>${o(t)}</h2>
1
+ import{SYNTHESIS_SMALLER_HEADING as f,synthesisRowShare as u}from"./synthesis.js";import{formatFindingTooltip as x}from"./findingText.js";import{arrangeFindings as v}from"./filterFindings.js";import{SEVERITY_MARK as h,METER_DOTS as $,filledDots as w,impactWord as y,impactShortWord as k,shortfallColor as g,shortfallStep as S,formatPoints as z,barPercent as M}from"./presentation.js";import{fmt as b,plainBinTerms as j}from"../renderer/fmt.js";import{buildFacetTable as T,prettyKey as F}from"./facets.js";import{escHtml as a}from"../core/utils.js";import{derivedKeyText as N,derivedFields as A}from"../renderer/testLabel.js";export function renderSection(t,e,r=""){return`<section class="${["report-section",r].filter(Boolean).join(" ")}">
2
+ <h2>${a(t)}</h2>
3
3
  ${e}
4
- </section>`}export function renderDefinitionList(t,e="meta-list"){if(!t.length)return"";const r=t.map(a=>`<div class="definition-item"><dt>${o(a.label)}</dt><dd>${o(a.value)}</dd></div>`).join(`
4
+ </section>`}export function renderDefinitionList(t,e="meta-list"){if(!t.length)return"";const r=t.map(o=>`<div class="definition-item"><dt>${a(o.label)}</dt><dd>${a(o.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 T(t,{facetableOnly:t.length>1}).map(r=>({label:F(r.key),value:r.values.map(o=>o.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
- <dt>${o(r.label)}</dt>
10
- <dd>${o(r.value)}</dd>
11
- ${r.hint?`<span class="metric-hint">${o(r.hint)}</span>`:""}
9
+ <dt>${a(r.label)}</dt>
10
+ <dd>${a(r.value)}</dd>
11
+ ${r.hint?`<span class="metric-hint">${a(r.hint)}</span>`:""}
12
12
  </div>`).join(`
13
13
  `)}
14
- </dl>`:""}export function renderTable(t,e,r={}){if(!e.length)return`<p class="no-data">${o(r.emptyMessage??"No data")}</p>`;const a=["report-table",r.className].filter(Boolean).join(" "),i=t.map(n=>`<th>${o(n)}</th>`).join(""),l=e.map(n=>`<tr>${n.map(s=>`<td>${s}</td>`).join("")}</tr>`).join(`
15
- `);return`<table class="${a}">
16
- <thead><tr>${i}</tr></thead>
14
+ </dl>`:""}export function renderTable(t,e,r={}){if(!e.length)return`<p class="no-data">${a(r.emptyMessage??"No data")}</p>`;const o=["report-table",r.className].filter(Boolean).join(" "),s=t.map(n=>`<th>${a(n)}</th>`).join(""),m=n=>typeof n=="string"?`<td>${n}</td>`:`<td${n.className?` class="${n.className}"`:""}>${n.html}</td>`,l=e.map(n=>`<tr>${n.map(m).join("")}</tr>`).join(`
15
+ `);return`<table class="${o}">
16
+ <thead><tr>${s}</tr></thead>
17
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>
21
- <td class="tight">${o(i.comparison.left)}</td>
22
- <td>${o(d(i.variable.label))}</td>
23
- <td class="numeric">${o(formatFindingDelta(i))}</td>
24
- <td class="numeric">${o(formatFindingCoverage(i,e))}</td>
18
+ </table>`}export function renderMeter(t,e){const r=w(t);return`<span class="meter"><span class="meter-dots" aria-hidden="true">${"\u25CF".repeat(r)}${"\u25CB".repeat($-r)}</span><span class="meter-word">${a(e)}</span></span>`}export function renderSeverityMeter(t){const{tier:e,word:r}=h[t];return renderMeter(e,r)}export const severityTier=t=>h[t].tier;export function barCell(t,e,r="",o){const s=o?`;background:${a(o)}`:"";return{html:`<span class="cellbar"><i style="width:${M(t).toFixed(1)}%${s}"></i><span>${a(e)}</span></span>`,className:["barcell",r].filter(Boolean).join(" ")}}export function deltaCell(t){const e=S(t);return{html:a(z(t)),className:["numeric",e?`low-${e}`:""].filter(Boolean).join(" ")}}export function withSectionNav(t,e=""){const r=[];let o=0;const s=t.replace(/<section class="(report-section[^"]*)">(\s*<h2>)(.*?)(<\/h2>)/g,(l,n,i,p,d)=>{const c=`sec-${e}${++o}`;return r.push({id:c,title:p}),`<section class="${n}" id="${c}">${i}${p}${d}`});return r.length<4?t:`<nav class="report-toc" aria-label="Sections">${r.map(l=>`<a href="#${l.id}">${l.title}</a>`).join("")}</nav>
19
+ ${s}`}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 o=Math.abs(t.effect.relativeDelta*100).toFixed(1);return`${r}${o}%`}return`${r}${b(Math.abs(e),t.variable.unit)}`}return`${r}${b(Math.abs(e),t.variable.unit)}`}export function derivedFindingsKeyHtml(t){const e=N(t.map(r=>({label:r.variable.label,...A(r.variable)})));return e?`<p class="report-note">${a(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,o=!1){const s=v(t),m=[...s.patterns.flatMap(i=>[{f:i.finding,nested:!1},...i.children.map(p=>({f:p,nested:!0}))]),...s.groups.flatMap(i=>i.findings.map(p=>({f:p,nested:!1})))],l=m.length?m.map(({f:i,nested:p})=>`<tr class="tier-${severityTier(i.severity)}${p?" nested":""}${o?" live":""}"${r!==void 0?` id="${findingAnchor(i.id,r)}"`:""}${o?` data-finding="${a(i.id)}"`:""} title="${a(x(i))}${o?`
20
+ Click to show it on the map`:""}">
21
+ <td class="tight">${renderSeverityMeter(i.severity)}</td>
22
+ <td class="tight">${a(i.comparison.left)}</td>
23
+ <td>${a(j(i.variable.label))}</td>
24
+ <td class="numeric">${a(formatFindingDelta(i))}</td>
25
+ <td class="numeric">${a(formatFindingCoverage(i,e))}</td>
25
26
  </tr>`).join(`
26
27
  `):'<tr><td colspan="5" class="no-data">No significant findings</td></tr>';return`<table class="report-table findings-table compact">
27
28
  <thead>
@@ -34,9 +35,25 @@ ${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
+ ${l}
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 o=d=>d.map(c=>c.target&&e.has(c.target.id)?`<a href="#${findingAnchor(c.target.id,r)}">${a(c.text)}</a>`:a(c.text)).join(""),[s,...m]=t.items,l=[...m,...t.also?.items??[]],n=t.also?.more??0,i=s?`<ol class="synthesis-items">
41
+ <li class="tier-${s.impact}">${renderMeter(s.impact,y(s.impact))}<span>${o(s.parts)}</span></li>
42
+ </ol>`:`<p class="synthesis-none">${a(t.nothing??"Nothing stands out.")}</p>`,p=l.length?`<p class="synthesis-smaller">${a(f)}</p>
43
+ <ul class="synthesis-more">
44
+ ${l.map(d=>`<li>${renderMeter(d.impact,k(d.impact))}<span>${o([d.brief])}</span><span class="synthesis-share">${a(u(d))}</span></li>`).join(`
45
+ `)}${n?`
46
+ <li class="synthesis-rest"><span></span><span>and ${n} more</span><span></span></li>`:""}
47
+ </ul>`:"";return renderSection("What stands out",`<p class="synthesis-headline">${o(t.headline)}</p>
48
+ ${i}
49
+ ${p}
50
+ ${t.watch?`<ul class="synthesis-watch">${t.watch.map(d=>`<li><strong>Watch</strong> ${o(d.parts)}</li>`).join("")}</ul>`:""}
51
+ <p class="synthesis-checked">${a(t.checked)}</p>`,"synthesis")}export const LIVE_FINDINGS_SCRIPT=`<script>
52
+ document.addEventListener('click', function (e) {
53
+ var row = e.target.closest && e.target.closest('[data-finding]');
54
+ if (row && window.parent !== window) window.parent.postMessage({ wmapFinding: row.getAttribute('data-finding') }, '*');
55
+ });
56
+ <\/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
57
  :root {
41
58
  --report-font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
42
59
  --report-text: #1f2328;
@@ -46,9 +63,18 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
46
63
  --report-line-strong: #c7ced8;
47
64
  --report-surface: #f7f8fa;
48
65
  --report-surface-alt: #fbfcfd;
49
- --report-severity-unusual: #8b3f35;
50
- --report-severity-notable: #8b6428;
51
- --report-severity-info: #446883;
66
+ /* A tint says "look here"; the dots and the word say what. Text on a tint is always full ink:
67
+ the muted grey is 3.3:1 on the strongest pastel, below AA. */
68
+ --report-tier-high-bg: #fbe4e2;
69
+ --report-tier-high-edge: #b4372f;
70
+ --report-tier-medium-bg: #fdf0d5;
71
+ --report-tier-medium-edge: #a8741a;
72
+ --report-tier-low-bg: #edf2f7;
73
+ --report-tier-low-edge: #5f7f9f;
74
+ --report-low-1: ${g(1)};
75
+ --report-low-2: ${g(2)};
76
+ --report-low-3: ${g(3)};
77
+ --report-bar: #9db8d3;
52
78
  }
53
79
 
54
80
  html {
@@ -95,6 +121,32 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
95
121
 
96
122
  /* Sub-headings and notes inside a section \u2014 used by the split comparison,
97
123
  which nests one block per facet under a single section heading. */
124
+ .synthesis-headline { font-weight: 600; margin: 0 0 8px; }
125
+ .synthesis-items { list-style: none; margin: 0 0 8px; padding: 0; }
126
+ .synthesis-items li {
127
+ display: grid;
128
+ grid-template-columns: 96px 1fr;
129
+ gap: 10px;
130
+ align-items: start;
131
+ margin: 0 0 8px;
132
+ padding: 9px 12px;
133
+ border-left: 4px solid var(--report-tier-low-edge);
134
+ border-radius: 0 4px 4px 0;
135
+ background: var(--report-tier-low-bg);
136
+ line-height: 1.45;
137
+ }
138
+ .synthesis-items li.tier-high { border-color: var(--report-tier-high-edge); background: var(--report-tier-high-bg); }
139
+ .synthesis-items li.tier-medium { border-color: var(--report-tier-medium-edge); background: var(--report-tier-medium-bg); }
140
+ .synthesis-smaller { margin: 0 0 4px; color: var(--report-muted); }
141
+ .synthesis-more { list-style: none; margin: 0 0 8px; padding: 0; }
142
+ .synthesis-more li { display: grid; grid-template-columns: 96px 1fr auto; gap: 10px; align-items: baseline; margin: 0 0 3px; }
143
+ .synthesis-share { font-variant-numeric: tabular-nums; color: var(--report-muted); }
144
+ .synthesis-watch { list-style: none; margin: 0 0 6px; padding: 0; }
145
+ .synthesis-watch li { margin: 0 0 3px; }
146
+ .synthesis-watch strong { margin-right: 6px; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--report-muted); }
147
+ .synthesis-checked, .synthesis-none { color: var(--report-muted); margin: 0; }
148
+ .synthesis a { color: inherit; text-decoration: underline dotted; }
149
+
98
150
  .report-subheading {
99
151
  margin: 14px 0 6px;
100
152
  font-size: 13px;
@@ -139,14 +191,6 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
139
191
  font-style: italic;
140
192
  }
141
193
 
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
194
  .definition-item {
151
195
  display: contents;
152
196
  }
@@ -268,39 +312,83 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
268
312
  font-size: 12px;
269
313
  }
270
314
 
271
- .badge {
315
+ /* Tiles for the key figures, so they read as a row of facts and not a line of the list. */
316
+ .metric-grid .metric {
317
+ padding: 10px 12px;
318
+ background: var(--report-surface);
319
+ border: 1px solid var(--report-line);
320
+ border-radius: 6px;
321
+ }
322
+
323
+ .report-toc {
324
+ display: flex;
325
+ flex-wrap: wrap;
326
+ gap: 4px 16px;
327
+ margin: 0 0 18px;
328
+ padding: 8px 0;
329
+ border-top: 1px solid var(--report-line);
330
+ border-bottom: 1px solid var(--report-line);
331
+ font-size: 12px;
332
+ }
333
+
334
+ .report-toc a { color: var(--report-muted); text-decoration: none; }
335
+ .report-toc a:hover { color: var(--report-text); text-decoration: underline; }
336
+
337
+ /* The value is always printed; the bar behind it is for scanning a column. */
338
+ table.report-table td.barcell { min-width: 150px; }
339
+ .cellbar { position: relative; display: block; }
340
+ .cellbar i {
341
+ position: absolute;
342
+ left: 0;
343
+ top: 50%;
344
+ height: 12px;
345
+ transform: translateY(-50%);
346
+ background: var(--report-bar);
347
+ border-radius: 0 2px 2px 0;
348
+ }
349
+ .cellbar span { position: relative; padding-left: 4px; }
350
+
351
+ /* Shortfall below the reference, light to strong. Full ink on every tint (see the tier tokens). */
352
+ table.report-table td.low-1 { background: var(--report-low-1); }
353
+ table.report-table td.low-2 { background: var(--report-low-2); }
354
+ table.report-table td.low-3 { background: var(--report-low-3); }
355
+ table.report-table td.muted-row, table.report-table td.muted-row .cellbar { color: var(--report-muted); }
356
+ .report-legend { margin: 6px 0 0; color: var(--report-muted); font-size: 11px; }
357
+
358
+ .meter {
272
359
  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;
360
+ flex-direction: column;
361
+ align-items: flex-start;
362
+ gap: 1px;
363
+ line-height: 1.2;
284
364
  white-space: nowrap;
285
365
  }
286
366
 
287
- .badge-unusual {
288
- color: var(--report-severity-unusual);
289
- background: #f4eeec;
290
- border-color: #d9c0bb;
367
+ .meter-dots {
368
+ font-size: 12px;
369
+ letter-spacing: 2px;
291
370
  }
292
371
 
293
- .badge-notable {
294
- color: var(--report-severity-notable);
295
- background: #f5f0e6;
296
- border-color: #ddcfb5;
372
+ .meter-word {
373
+ color: var(--report-text);
374
+ font-size: 11px;
375
+ font-weight: 600;
376
+ letter-spacing: 0.04em;
297
377
  }
298
378
 
299
- .badge-info {
300
- color: var(--report-severity-info);
301
- background: #eef4f8;
302
- border-color: #cfdae4;
303
- }
379
+ /* In the app, a row shows its finding on the map. */
380
+ table.findings-table tr.live { cursor: pointer; }
381
+ table.findings-table tr.live:hover td { box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.04); }
382
+
383
+ /* A finding a pattern explains sits beneath it, indented. */
384
+ table.findings-table tr.nested td:first-child { padding-left: 22px; }
385
+ table.findings-table tr.nested td { font-size: 12px; }
386
+
387
+ /* Findings table: the row carries the tier as a tint and a left edge; the meter carries it in words. */
388
+ table.findings-table tr.tier-high td { background: var(--report-tier-high-bg); }
389
+ table.findings-table tr.tier-medium td { background: var(--report-tier-medium-bg); }
390
+ table.findings-table tr.tier-high td:first-child { box-shadow: inset 4px 0 0 var(--report-tier-high-edge); }
391
+ table.findings-table tr.tier-medium td:first-child { box-shadow: inset 4px 0 0 var(--report-tier-medium-edge); }
304
392
 
305
393
  .footer {
306
394
  margin-top: 14px;
@@ -325,6 +413,8 @@ ${e}`:t.summary}export function derivedFindingsKeyHtml(t){const e=u(t.map(r=>({l
325
413
  padding: 0;
326
414
  }
327
415
 
416
+ .report-toc { display: none; }
417
+
328
418
  section.report-section,
329
419
  tr {
330
420
  break-inside: avoid;