@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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +162 -1
- package/README.md +2 -2
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +2 -12
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +30 -0
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +22 -3
- package/dist/packages/canvas-adapter/summaryPanel.js +4 -3
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +54 -1
- package/dist/packages/canvas-adapter/toolbar.js +2 -2
- package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
- package/dist/packages/canvas-adapter/userGuideHtml.js +76 -13
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/core/aggregates.d.ts +6 -0
- package/dist/packages/core/aggregates.js +1 -1
- package/dist/packages/core/compact.d.ts +205 -0
- package/dist/packages/core/compact.js +3 -0
- package/dist/packages/core/passBins.d.ts +6 -0
- package/dist/packages/core/passBins.js +1 -1
- package/dist/packages/renderer/axisTicks.d.ts +14 -0
- package/dist/packages/renderer/axisTicks.js +1 -1
- package/dist/packages/renderer/buildView.d.ts +26 -1
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/compactView.d.ts +22 -0
- package/dist/packages/renderer/compactView.js +1 -0
- package/dist/packages/renderer/xyIndicator.d.ts +27 -0
- package/dist/packages/renderer/xyIndicator.js +1 -0
- package/dist/packages/stats/analyzeWaferLot.d.ts +28 -0
- package/dist/packages/stats/analyzeWaferLot.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.d.ts +36 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/binRows.d.ts +38 -0
- package/dist/packages/stats/binRows.js +1 -0
- package/dist/packages/stats/filterFindings.d.ts +25 -0
- package/dist/packages/stats/filterFindings.js +1 -1
- package/dist/packages/stats/findingText.d.ts +9 -0
- package/dist/packages/stats/findingText.js +2 -0
- package/dist/packages/stats/lotDrift.d.ts +16 -0
- package/dist/packages/stats/lotDrift.js +1 -0
- package/dist/packages/stats/lotPattern.d.ts +33 -0
- package/dist/packages/stats/lotPattern.js +1 -0
- package/dist/packages/stats/math.d.ts +51 -0
- package/dist/packages/stats/math.js +1 -1
- package/dist/packages/stats/patternClassification.d.ts +31 -0
- package/dist/packages/stats/patternClassification.js +1 -1
- package/dist/packages/stats/presentation.d.ts +42 -0
- package/dist/packages/stats/presentation.js +1 -0
- package/dist/packages/stats/regions.d.ts +9 -0
- package/dist/packages/stats/regions.js +1 -1
- package/dist/packages/stats/renderSummaryReport.d.ts +15 -2
- package/dist/packages/stats/renderSummaryReport.js +32 -27
- package/dist/packages/stats/reportHtml.d.ts +54 -9
- package/dist/packages/stats/reportHtml.js +130 -47
- package/dist/packages/stats/synthesis.d.ts +66 -0
- package/dist/packages/stats/synthesis.js +2 -0
- package/dist/packages/stats/types.d.ts +1 -1
- package/dist/packages/stats/yieldRows.d.ts +31 -0
- package/dist/packages/stats/yieldRows.js +1 -0
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/dist/packages/stats/findingsNarrative.d.ts +0 -3
- package/dist/packages/stats/findingsNarrative.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{classifyDie as
|
|
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
|
|
2
|
-
`)),d
|
|
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
|
|
8
|
+
<title>${S(y)}</title>
|
|
8
9
|
<style>
|
|
9
|
-
${
|
|
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
|
|
16
|
-
<p class="report-subtitle">Generated ${
|
|
16
|
+
<h1>${S(y)}</h1>
|
|
17
|
+
<p class="report-subtitle">Generated ${S(l)}</p>
|
|
17
18
|
</header>
|
|
18
|
-
${
|
|
19
|
-
<p class="footer">Generated ${
|
|
19
|
+
${ee(x)}
|
|
20
|
+
<p class="footer">Generated ${S(l)}</p>
|
|
20
21
|
</main>
|
|
22
|
+
${t.live?k:""}
|
|
21
23
|
</body>
|
|
22
|
-
</html>`}function
|
|
23
|
-
`))}const
|
|
24
|
-
`)}`)}if(!
|
|
25
|
-
`)+
|
|
26
|
-
`);return{
|
|
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>${
|
|
29
|
-
<p class="report-subtitle">Generated ${
|
|
31
|
+
<h1>${S(t)}</h1>
|
|
32
|
+
<p class="report-subtitle">Generated ${S(i)}</p>
|
|
30
33
|
</header>
|
|
31
|
-
${
|
|
32
|
-
<p class="footer">Generated ${
|
|
33
|
-
</main>`}export function renderLotSummaryReportHtml(t,
|
|
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>${
|
|
40
|
+
<title>${S(g)}</title>
|
|
38
41
|
<style>
|
|
39
|
-
${
|
|
42
|
+
${j()}
|
|
40
43
|
</style>
|
|
41
44
|
</head>
|
|
42
45
|
<body>
|
|
43
|
-
${
|
|
46
|
+
${me(g,l,m)}
|
|
47
|
+
${t.live?k:""}
|
|
44
48
|
</body>
|
|
45
|
-
</html>`}const
|
|
46
|
-
${
|
|
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>${
|
|
55
|
+
<title>${S(p)}</title>
|
|
52
56
|
<style>
|
|
53
|
-
${
|
|
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
|
-
${
|
|
64
|
+
${w}
|
|
65
|
+
${t.live?k:""}
|
|
61
66
|
</body>
|
|
62
|
-
</html>`}export{
|
|
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
|
-
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(" "),
|
|
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>${
|
|
17
|
-
<tbody>${
|
|
18
|
-
</table>`}export function
|
|
19
|
-
${e}
|
|
20
|
-
|
|
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(
|
|
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
|
-
${
|
|
38
|
+
${s}
|
|
38
39
|
</tbody>
|
|
39
|
-
</table>${derivedFindingsKeyHtml(t)}`}export function
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
--report-
|
|
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
|
-
.
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
border-color: #d9c0bb;
|
|
360
|
+
.meter-dots {
|
|
361
|
+
font-size: 12px;
|
|
362
|
+
letter-spacing: 2px;
|
|
291
363
|
}
|
|
292
364
|
|
|
293
|
-
.
|
|
294
|
-
color: var(--report-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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;
|