@wafertools/wafermap 0.26.1 → 0.27.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/CHANGELOG.md +360 -0
- package/README.md +46 -3
- package/dist/packages/canvas-adapter/canvasTheme.d.ts +4 -0
- package/dist/packages/canvas-adapter/canvasTheme.js +1 -1
- package/dist/packages/canvas-adapter/charts/barPanel.d.ts +16 -0
- package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
- package/dist/packages/canvas-adapter/charts/binCluster.js +1 -1
- package/dist/packages/canvas-adapter/charts/boxplot.d.ts +13 -1
- package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
- package/dist/packages/canvas-adapter/charts/capability.d.ts +10 -10
- package/dist/packages/canvas-adapter/charts/capability.js +1 -1
- package/dist/packages/canvas-adapter/charts/chartShell.d.ts +337 -3
- package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
- package/dist/packages/canvas-adapter/charts/correlation.d.ts +8 -9
- package/dist/packages/canvas-adapter/charts/correlation.js +2 -1
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.d.ts +50 -0
- package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -0
- package/dist/packages/canvas-adapter/charts/histogram.d.ts +12 -1
- package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
- package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
- package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
- package/dist/packages/canvas-adapter/charts/testPassRate.d.ts +20 -0
- package/dist/packages/canvas-adapter/charts/testPassRate.js +1 -0
- package/dist/packages/canvas-adapter/charts/trend.d.ts +30 -0
- package/dist/packages/canvas-adapter/charts/trend.js +1 -0
- package/dist/packages/canvas-adapter/dieList.js +7 -7
- package/dist/packages/canvas-adapter/identityHeader.d.ts +46 -0
- package/dist/packages/canvas-adapter/identityHeader.js +1 -0
- package/dist/packages/canvas-adapter/index.d.ts +1 -1
- package/dist/packages/canvas-adapter/insightsTab.d.ts +27 -3
- package/dist/packages/canvas-adapter/insightsTab.js +1 -1
- package/dist/packages/canvas-adapter/maplessSummary.js +1 -1
- package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +15 -2
- package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -20
- package/dist/packages/canvas-adapter/renderWaferMap.d.ts +72 -17
- package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
- package/dist/packages/canvas-adapter/summaryPanel.d.ts +219 -33
- package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
- package/dist/packages/canvas-adapter/toCanvas.d.ts +6 -0
- package/dist/packages/canvas-adapter/toCanvas.js +1 -1
- package/dist/packages/canvas-adapter/toolbar.d.ts +310 -6
- 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 +97 -34
- package/dist/packages/canvas-adapter/version.d.ts +2 -2
- package/dist/packages/canvas-adapter/version.js +1 -1
- package/dist/packages/canvas-adapter/warnings.d.ts +0 -4
- package/dist/packages/canvas-adapter/warnings.js +1 -1
- package/dist/packages/core/utils.d.ts +11 -0
- package/dist/packages/core/utils.js +1 -1
- package/dist/packages/renderer/buildView.d.ts +34 -1
- package/dist/packages/renderer/buildView.js +1 -1
- package/dist/packages/renderer/buildWaferMap.d.ts +104 -5
- package/dist/packages/renderer/buildWaferMap.js +1 -1
- package/dist/packages/renderer/colorSchemes.js +1 -1
- package/dist/packages/stats/analyzeWaferMap.js +1 -1
- package/dist/packages/stats/binPareto.d.ts +15 -0
- package/dist/packages/stats/binPareto.js +1 -1
- package/dist/packages/stats/clusterDetection.js +1 -1
- package/dist/packages/stats/connectedComponents.d.ts +14 -0
- package/dist/packages/stats/connectedComponents.js +1 -0
- package/dist/packages/stats/correlation.d.ts +26 -0
- package/dist/packages/stats/correlation.js +1 -1
- package/dist/packages/stats/facets.js +1 -1
- package/dist/packages/stats/filterFindings.d.ts +18 -0
- package/dist/packages/stats/filterFindings.js +1 -1
- package/dist/packages/stats/findingsNarrative.js +1 -1
- package/dist/packages/stats/histogram.d.ts +19 -1
- package/dist/packages/stats/histogram.js +1 -1
- package/dist/packages/stats/index.d.ts +6 -0
- package/dist/packages/stats/index.js +1 -1
- package/dist/packages/stats/mergeTestDefs.d.ts +44 -0
- package/dist/packages/stats/mergeTestDefs.js +1 -0
- package/dist/packages/stats/metadataColumns.d.ts +20 -4
- package/dist/packages/stats/metadataColumns.js +1 -1
- package/dist/packages/stats/patternClassification.js +1 -1
- package/dist/packages/stats/renderFindingsReport.js +14 -14
- package/dist/packages/stats/renderSummaryReport.js +32 -29
- package/dist/packages/stats/reportHtml.js +38 -13
- package/dist/packages/stats/testPassRate.d.ts +85 -0
- package/dist/packages/stats/testPassRate.js +1 -0
- package/dist/packages/stats/trend.d.ts +40 -0
- package/dist/packages/stats/trend.js +1 -0
- package/dist/packages/stats/types.d.ts +117 -6
- package/llms.txt +6 -0
- package/package.json +7 -5
- package/dist/packages/canvas-adapter/metadataBadge.d.ts +0 -30
- package/dist/packages/canvas-adapter/metadataBadge.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const y={unusual:0,notable:1,info:2},S={E:0,ENE:22.5,NE:45,NNE:67.5,N:90,NNW:112.5,NW:135,WNW:157.5,W:180,WSW:202.5,SW:225,SSW:247.5,S:270,SSE:292.5,SE:315,ESE:337.5};function m(e){return e.stats.adjustedPValue??e.stats.pValue??1}function b(e){return[...e].sort((n,t)=>{const o=y[n.severity]-y[t.severity];return o!==0?o:m(n)-m(t)})[0]}function w(e){return e.some(t=>t.variable.kind==="yield")?e.filter(t=>t.variable.kind!=="hardBin"&&t.variable.kind!=="softBin"):e}function $(e){return e.replace(/^(Rings?|Quadrants?|Sectors?)\s+/i,"").split(/[,&]|–|\band\b/).map(n=>n.trim()).filter(Boolean)}function A(e,n){const t=new Set($(e));return $(n).some(o=>t.has(o))}function v(e){return e.variable.kind==="yield"?"yield":e.variable.kind==="hardBin"?`HBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.kind==="softBin"?`SBin ${e.variable.bin??e.variable.index??"?"}`:e.variable.label}function h(e){let n=0,t=0;for(const o of e)o.effect.direction==="higher"?n++:o.effect.direction==="lower"&&t++;return n>t?"higher":t>n?"lower":"mixed"}function d(e){return e==="higher"?"elevated":e==="lower"?"reduced":"shifted"}function p(e){const n=new Set,t=[];for(const o of e){const r=v(o);n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:t.length===3?`${t[0]}, ${t[1]}, and ${t[2]}`:`${t[0]}, ${t[1]}, and ${t.length-2} more`}function E(e){const n=new Set,t=[];for(const o of e){const r=o.comparison.left;n.has(r)||(n.add(r),t.push(r))}return t.length===1?t[0]:t.length===2?`${t[0]} and ${t[1]}`:`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`}function R(e){return e.length!==1?!1:!/^(Rings|Quadrants|Sectors)\b/.test(e[0].comparison.left)}function B(e){const n=h(e),t=e.every(c=>c.comparison.left.includes("(edge)")),o=e.every(c=>c.comparison.left.includes("(core)"));let r,a;return o?(r="The core ring",a=!0):t&&e.length>1?(r="The edge rings",a=!1):(r=E(e),a=R(e)),`${r} show${a?"s":""} ${d(n)} ${p(e)}.`}function I(e){const n=h(e),t=s=>s.highlight.kind==="region"&&s.highlight.regionKeys.length>1||s.comparison.left.startsWith("Quadrants"),o=e.filter(s=>t(s)),r=e.filter(s=>!t(s)),a=[];r.length===1?a.push(`the ${r[0].comparison.left} quadrant`):r.length>1&&a.push(`the ${E(r)} quadrants`);for(const s of o)a.push(s.comparison.left.replace(/^Quadrants/,"quadrants"));const c=a.join(" and "),u=r.length===1&&o.length===0;return`${c.charAt(0).toUpperCase()+c.slice(1)} show${u?"s":""} ${d(n)} ${p(e)}.`}function P(e){const n=h(e),t=[],o=new Set;for(const u of e){const i=u.comparison.left.replace(/^Sectors? /,"");o.has(i)||(o.add(i),t.push(i))}let r,a;return t.length===1?(r=`the ${t[0]} ${e[0].comparison.left.startsWith("Sectors")?"sectors":"region"}`,a=!e[0].comparison.left.startsWith("Sectors")):(r=`the ${t.length===2?t.join(" and "):`${t.slice(0,-1).join(", ")}, and ${t[t.length-1]}`} sectors`,a=!1),`${r.charAt(0).toUpperCase()+r.slice(1)} show${a?"s":""} ${d(n)} ${p(e)}.`}function x(e){if(e.length===1)return`A failure cluster at ${e[0].comparison.left.replace(/^Cluster at /,"")}.`;const t=e.reduce((o,r)=>(o.stats.sampleSizeLeft??0)>=(r.stats.sampleSizeLeft??0)?o:r).comparison.left.replace(/^Cluster at /,"");return`${e.length} failure clusters identified; the largest at ${t}.`}function C(e){const n=e[0].comparison.left.replace(/^Edge arc ~/,"");return e.length===1?`An edge arc near ${n} shows localised failures.`:`${e.length} edge arcs detected, including near ${n}.`}function q(e){const n=h(e);return e.length===1?`Reticle-position variation: ${e[0].comparison.left} shows ${d(n)} ${p(e)}.`:`Reticle-position variation across ${e.length} cells (${d(n)} ${p(e)}).`}function F(e,n){const t=e.stats.method==="geometry"?` (${e.severity==="unusual"?"high":e.severity==="notable"?"medium":"low"} confidence)`:"",o=new Set(e.relatedIds??[]),r=n.find(c=>o.has(c.id)&&(c.comparison.family==="edge-arc"||c.comparison.family==="cluster"));let a="";return r&&(r.comparison.family==="edge-arc"?a=`; failures concentrated at the wafer edge near ${r.comparison.left.replace(/^Edge arc ~/,"")}`:a=`; failures concentrated near ${r.comparison.left.replace(/^Cluster at /,"")}`),`${e.comparison.left} failure pattern detected${t}${a}.`}function Y(e){const n=e.replace(/\.$/,"");return n.charAt(0).toLowerCase()+n.slice(1)}function M(e){const n=e.filter(t=>t.variable.kind==="yield");return n.length>0?h(n):h(e)}function k(e,n){const t=new Map;for(const i of n){const s=t.get(i.comparison.left)??[];s.push(i),t.set(i.comparison.left,s)}const o=[],r=[];for(const i of t.values()){const s=M(i);s==="higher"?o.push(...i.filter(l=>l.effect.direction==="higher")):s==="lower"?r.push(...i.filter(l=>l.effect.direction==="lower")):o.push(...i)}const a=o.map(i=>i.comparison.left),c=r.filter(i=>!a.some(s=>A(s,i.comparison.left)));if(o.length>0&&c.length>0){const i=n.some(f=>f.variable.kind==="yield"),[s,l]=i?[c,o]:[o,c];return`${e(s).replace(/\.$/,"")} while ${Y(e(l))}.`}if(o.length>0&&r.length>0&&c.length===0)return e(o);const u=M(n);if(u!=="mixed"&&n.some(i=>i.variable.kind==="yield")){const i=n.filter(s=>s.effect.direction===u);if(i.length>0)return e(i)}return e(n)}function N(e){let n=0,t=0,o=0;for(const r of e)for(const a of $(r.comparison.left)){const c=S[a];if(c===void 0)continue;const u=c*Math.PI/180;n+=Math.cos(u),t+=Math.sin(u),o++}return o===0||n===0&&t===0?null:(Math.atan2(t,n)*180/Math.PI+360)%360}function W(e){let n="E",t=360;for(const[o,r]of Object.entries(S)){const a=Math.abs((e-r+540)%360-180);a<t&&(t=a,n=o)}return n}function D(e){const n=e.filter(u=>u.effect.direction==="higher"),t=e.filter(u=>u.effect.direction==="lower");if(n.length===0||t.length===0)return null;const o=new Set(e.map(v));if(o.size!==1)return null;const r=N(n),a=N(t);return r===null||a===null||Math.abs((r-a+540)%360-180)<120?null:`${[...o][0]} increases from ${W(a)} toward ${W(r)} across the wafer.`}function O(e){const n=w(e);if(n.length===0)return"";const t=["sector","quadrant","ring"],o=new Map;for(const i of n){const s=o.get(i.comparison.family)??[];s.push(i),o.set(i.comparison.family,s)}const r=[...o.keys()].sort((i,s)=>{const l=Math.min(...o.get(i).map(m)),f=Math.min(...o.get(s).map(m));return l!==f?l-f:t.indexOf(i)-t.indexOf(s)})[0],a=o.get(r),c=D(a);return c||k(r==="ring"?B:r==="quadrant"?I:P,a)}function j(e,n){switch(e){case"cluster":return x(n);case"edge-arc":return C(n);case"reticle-position":return k(q,n);default:return""}}const L=new Set(["ring","quadrant","sector"]);export function buildFindingsNarrative(e){if(e.length===0)return"";const n=e.filter(s=>s.severity!=="info");if(n.length===0)return"Minor spatial variation detected; no strongly significant patterns.";const t=[],o=n.filter(s=>s.comparison.family==="spatial-pattern"),r=o.length?b(o):null,a=new Set(r?.relatedIds??[]);r&&t.push(F(r,n));const c=n.filter(s=>s!==r&&s.comparison.family!=="spatial-pattern"&&!a.has(s.id));let u=c.filter(s=>L.has(s.comparison.family));if(r&&u.length){const s=w(u);s.length>0&&s.every(f=>f.variable.kind==="yield"&&f.effect.direction==="higher")&&(u=[])}const i=new Set;if(u.length){const s=O(u);if(s){t.push(s);for(const l of L)i.add(l)}}else if(!r){const s=b(c);if(s){const l=s.comparison.family,f=j(l,c.filter(g=>g.comparison.family===l));f&&(t.push(f),i.add(l))}}if(t.length<3){const s=c.filter(l=>(l.comparison.family==="cluster"||l.comparison.family==="edge-arc")&&!i.has(l.comparison.family));if(s.length){const l=s[0].comparison.family,f=j(l,s.filter(g=>g.comparison.family===l));f&&t.push(f)}}return t.slice(0,3).join(" ")}
|
|
@@ -27,7 +27,25 @@ export interface HistogramSeriesData {
|
|
|
27
27
|
* equal-width buckets. If `limitLow`/`limitHigh` are given, the axis range
|
|
28
28
|
* is expanded to include them so limit lines always draw.
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
/** Every finite recorded value for one test across `items`. Exported because a
|
|
31
|
+
* panel that wants to derive a robust fence needs the raw population, and
|
|
32
|
+
* re-walking dies in the chart layer would be a second copy of this loop. */
|
|
33
|
+
export declare function collectTestValues(items: HistogramItem[], testNumber: number): number[];
|
|
34
|
+
export declare function buildTestHistogramData(items: HistogramItem[], testNumber: number, bucketCount?: number, limitLow?: number, limitHigh?: number,
|
|
35
|
+
/**
|
|
36
|
+
* Bound the bucket range and DROP values outside it. Unlike `limitLow`/
|
|
37
|
+
* `limitHigh`, which only ever widen the range, this narrows it — for the
|
|
38
|
+
* panel's "Clip outliers" axis control, where one wild reading otherwise
|
|
39
|
+
* compresses every real bucket into the first column.
|
|
40
|
+
*
|
|
41
|
+
* Affects this chart only. No statistic anywhere is computed from a clipped
|
|
42
|
+
* population: an out-of-spec die is a distribution outlier by construction, so
|
|
43
|
+
* excluding it from yield or capability would delete real failures.
|
|
44
|
+
*/
|
|
45
|
+
clip?: {
|
|
46
|
+
lo: number;
|
|
47
|
+
hi: number;
|
|
48
|
+
}): HistogramBucket[];
|
|
31
49
|
/**
|
|
32
50
|
* Faceted histogram: one count-series per group over a *shared* set of
|
|
33
51
|
* buckets, so the series overlay and compare directly. The bucket range
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function collectTestValues(h,u){const s=[];for(const f of h)for(const c of f.dies??[]){const o=c.testValues?.[u];o!==void 0&&Number.isFinite(o)&&s.push(o)}return s}export function buildTestHistogramData(h,u,s=16,f,c,o){let n=collectTestValues(h,u);if(o&&(n=n.filter(e=>e>=o.lo&&e<=o.hi)),n.length===0)return[];let a=n[0],d=n[0];for(let e=1;e<n.length;e++)n[e]<a&&(a=n[e]),n[e]>d&&(d=n[e]);const i=f!==void 0?Math.min(a,f):a,l=((c!==void 0?Math.max(d,c):d)-i||1)/s,g=Array.from({length:s},(e,t)=>({rangeLow:i+t*l,rangeHigh:i+(t+1)*l,count:0}));for(const e of n){const t=Math.min(s-1,Math.floor((e-i)/l));g[t].count++}return g}export function buildTestHistogramSeries(h,u,s=16,f,c){const o=new Map;let n=1/0,a=-1/0;for(const t of h){const r=[];for(const x of t.items)for(const M of x.dies??[]){const m=M.testValues?.[u];m!==void 0&&Number.isFinite(m)&&(r.push(m),m<n&&(n=m),m>a&&(a=m))}o.set(t.key,r)}const d=h.map(t=>t.key).filter(t=>o.get(t).length>0);if(d.length===0)return{ranges:[],series:[]};const i=f!==void 0?Math.min(n,f):n,l=((c!==void 0?Math.max(a,c):a)-i||1)/s,g=Array.from({length:s},(t,r)=>({rangeLow:i+r*l,rangeHigh:i+(r+1)*l})),e=d.map(t=>{const r=new Array(s).fill(0);for(const x of o.get(t)){const M=Math.min(s-1,Math.floor((x-i)/l));r[M]++}return{groupKey:t,counts:r}});return{ranges:g,series:e}}
|
|
@@ -12,12 +12,18 @@ export { buildFacetTable, facetValueOf, FACET_NONE_VALUE, DEFAULT_FACET_CURATION
|
|
|
12
12
|
export type { FacetField, FacetValue, FacetCuration, FacetItem, BuildFacetTableOptions } from './facets.js';
|
|
13
13
|
export { resolveMetadataColumns, discoverDieMetadataKeys } from './metadataColumns.js';
|
|
14
14
|
export type { MetadataColumn, MetadataColumnScope, MetadataColumnSet, MetadataKeySelection, ResolveMetadataColumnsOptions, } from './metadataColumns.js';
|
|
15
|
+
export { mergeTestDefs } from './mergeTestDefs.js';
|
|
16
|
+
export type { MergedTestDefs, TestDefConflict, TestDefConflictKind } from './mergeTestDefs.js';
|
|
15
17
|
export { buildCapabilityData } from './capability.js';
|
|
16
18
|
export type { CapabilityDatum, CapabilityItem } from './capability.js';
|
|
17
19
|
export { buildCorrelationMatrix, filterCorrelationMatrix } from './correlation.js';
|
|
18
20
|
export type { CorrelationTestInfo, CorrelationCell, CorrelationMatrix, CorrelationSummary } from './correlation.js';
|
|
19
21
|
export { buildTestBoxplotData } from './boxplot.js';
|
|
20
22
|
export type { BoxplotDatum, BoxplotItem } from './boxplot.js';
|
|
23
|
+
export { buildTestTrendData, trendCentre } from './trend.js';
|
|
24
|
+
export type { TrendDatum, TrendItem } from './trend.js';
|
|
25
|
+
export { buildTestPassRateData, hasJudgeableTests } from './testPassRate.js';
|
|
26
|
+
export type { TestPassKind, TestPassRateData, TestPassRateItem, TestPassRateRow, TestPassRateValue } from './testPassRate.js';
|
|
21
27
|
export { buildTestHistogramData, buildTestHistogramSeries } from './histogram.js';
|
|
22
28
|
export type { HistogramBucket, HistogramItem, HistogramSeries, HistogramSeriesData } from './histogram.js';
|
|
23
29
|
export { buildScatterData, buildScatterDataGrouped } from './scatter.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./types.js";export*from"./regions.js";export*from"./analyzeWaferMap.js";export*from"./analyzeWaferLot.js";export*from"./filterFindings.js";export{classifyPattern}from"./patternClassification.js";export{renderFindingsReportHtml,openHtmlReport,setReportOpener}from"./renderFindingsReport.js";export{renderSummaryReportHtml,renderLotSummaryReportHtml}from"./renderSummaryReport.js";export{buildFacetTable,facetValueOf,FACET_NONE_VALUE,DEFAULT_FACET_CURATION}from"./facets.js";export{resolveMetadataColumns,discoverDieMetadataKeys}from"./metadataColumns.js";export{buildCapabilityData}from"./capability.js";export{buildCorrelationMatrix,filterCorrelationMatrix}from"./correlation.js";export{buildTestBoxplotData}from"./boxplot.js";export{buildTestHistogramData,buildTestHistogramSeries}from"./histogram.js";export{buildScatterData,buildScatterDataGrouped}from"./scatter.js";export{buildYieldData,buildYieldDataCombined}from"./yield.js";export{buildBinParetoData,buildBinClusterData}from"./binPareto.js";
|
|
1
|
+
export*from"./types.js";export*from"./regions.js";export*from"./analyzeWaferMap.js";export*from"./analyzeWaferLot.js";export*from"./filterFindings.js";export{classifyPattern}from"./patternClassification.js";export{renderFindingsReportHtml,openHtmlReport,setReportOpener}from"./renderFindingsReport.js";export{renderSummaryReportHtml,renderLotSummaryReportHtml}from"./renderSummaryReport.js";export{buildFacetTable,facetValueOf,FACET_NONE_VALUE,DEFAULT_FACET_CURATION}from"./facets.js";export{resolveMetadataColumns,discoverDieMetadataKeys}from"./metadataColumns.js";export{mergeTestDefs}from"./mergeTestDefs.js";export{buildCapabilityData}from"./capability.js";export{buildCorrelationMatrix,filterCorrelationMatrix}from"./correlation.js";export{buildTestBoxplotData}from"./boxplot.js";export{buildTestTrendData,trendCentre}from"./trend.js";export{buildTestPassRateData,hasJudgeableTests}from"./testPassRate.js";export{buildTestHistogramData,buildTestHistogramSeries}from"./histogram.js";export{buildScatterData,buildScatterDataGrouped}from"./scatter.js";export{buildYieldData,buildYieldDataCombined}from"./yield.js";export{buildBinParetoData,buildBinClusterData}from"./binPareto.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TestDef, WaferWarning } from '../renderer/buildWaferMap.js';
|
|
2
|
+
/** Which field disagreed. `'limits'` is the soft tier; the rest are hard. */
|
|
3
|
+
export type TestDefConflictKind = 'name' | 'unit' | 'testType' | 'limits';
|
|
4
|
+
/** One test number whose defs could not be reconciled across the population. */
|
|
5
|
+
export interface TestDefConflict {
|
|
6
|
+
testNumber: number;
|
|
7
|
+
kind: TestDefConflictKind;
|
|
8
|
+
/** True when the test was withheld from `defs` (the hard tier). */
|
|
9
|
+
excluded: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The distinct STATED values that disagreed, in first-seen order, already
|
|
12
|
+
* stringified for display. Absent values are not represented — they were
|
|
13
|
+
* never in disagreement with anything.
|
|
14
|
+
*/
|
|
15
|
+
values: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface MergedTestDefs {
|
|
18
|
+
/**
|
|
19
|
+
* The union of every test number across every item, each carrying a def
|
|
20
|
+
* reconciled from all items that described it. Hard-conflicting tests are
|
|
21
|
+
* absent. Ordered by first appearance, so a single-program population gets
|
|
22
|
+
* back exactly the order it supplied.
|
|
23
|
+
*/
|
|
24
|
+
defs: TestDef[];
|
|
25
|
+
/** Every unreconcilable test, both tiers. Empty for a clean population. */
|
|
26
|
+
conflicts: TestDefConflict[];
|
|
27
|
+
/**
|
|
28
|
+
* `conflicts` rendered as structured advisories, ready to hand to
|
|
29
|
+
* `collectWarnings` so the toolbar indicator and Summary banner report them
|
|
30
|
+
* with no host work. At most one warning per severity — a program mismatch
|
|
31
|
+
* hits many tests at once, and one line per test would bury everything else.
|
|
32
|
+
*/
|
|
33
|
+
warnings: WaferWarning[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reconcile every item's `testDefs` into one list for the whole population.
|
|
37
|
+
*
|
|
38
|
+
* Items with no defs are skipped, not treated as disagreeing — a wafer that
|
|
39
|
+
* describes nothing contradicts nothing.
|
|
40
|
+
*/
|
|
41
|
+
export declare function mergeTestDefs(items: Array<{
|
|
42
|
+
testDefs?: TestDef[];
|
|
43
|
+
} | null | undefined>): MergedTestDefs;
|
|
44
|
+
//# sourceMappingURL=mergeTestDefs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const k=1e-6;function w(t,e){if(t===e)return!0;if(!Number.isFinite(t)||!Number.isFinite(e))return!1;const i=Math.max(Math.abs(t),Math.abs(e));return Math.abs(t-e)<=1e-6*i}function v(t){const e=t.name?.trim();return e||void 0}function y(t){const e=t.unit?.trim();return e||void 0}function L(t){return t.toLowerCase()}function h(t,e){const i=new Set,s=[];for(const o of t){const r=e(o);i.has(r)||(i.add(r),s.push(o))}return s}function g(t){const e=[];for(const i of t)e.some(s=>w(s,i))||e.push(i);return e}function T(t){return`${t.testNumber} (${t.values.join(" vs ")})`}function b(t,e=5){const i=t.slice(0,e).map(T).join(", "),s=t.length-Math.min(e,t.length);return s>0?`${i}, and ${s} more`:i}export function mergeTestDefs(t){const e=new Map,i=[];for(const r of t)for(const u of r?.testDefs??[]){const f=u.testNumber;let d=e.get(f);d||(d=[],e.set(f,d),i.push(f)),d.push(u)}const s=[],o=[];for(const r of i){const u=e.get(r),f=h(u.map(v).filter(n=>n!==void 0),L),d=h(u.map(y).filter(n=>n!==void 0),n=>n),l=h(u.map(n=>n.testType).filter(n=>n!==void 0),n=>n),p=f.length>1?{testNumber:r,kind:"name",excluded:!0,values:f}:d.length>1?{testNumber:r,kind:"unit",excluded:!0,values:d}:l.length>1?{testNumber:r,kind:"testType",excluded:!0,values:l}:void 0;if(p){o.push(p);continue}const a=g(u.map(n=>n.limitLow).filter(n=>n!==void 0)),c=g(u.map(n=>n.limitHigh).filter(n=>n!==void 0)),m=a.length>1||c.length>1;m&&o.push({testNumber:r,kind:"limits",excluded:!1,values:(a.length>1?a:c).map(n=>String(n))}),s.push({testNumber:r,name:f[0]??"",...d[0]!==void 0?{unit:d[0]}:{},...l[0]!==void 0?{testType:l[0]}:{},...u.find(n=>n.logScale!==void 0)?{logScale:u.find(n=>n.logScale!==void 0).logScale}:{},...!m&&a[0]!==void 0?{limitLow:a[0]}:{},...!m&&c[0]!==void 0?{limitHigh:c[0]}:{}})}return{defs:s,conflicts:o,warnings:N(o)}}function N(t){const e=[],i=t.filter(o=>o.excluded),s=t.filter(o=>o.kind==="limits");return i.length>0&&e.push({code:"test-def-collision",severity:"error",message:`${i.length} test ${i.length===1?"number describes":"numbers describe"} a different measurement in different wafers, so the same number cannot identify one test across this population. Affected: `+b(i)+". These tests are withheld from every cross-wafer chart, report and map mode \u2014 pooling them would mix unrelated measurements into one distribution. Each wafer's own view is unaffected. Load one test program at a time to see them."}),s.length>0&&e.push({code:"test-limit-conflict",severity:"warning",message:`${s.length} ${s.length===1?"test is":"tests are"} held to different spec limits in different wafers. Affected: `+b(s)+". The measured values are still comparable, so distributions include them, but capability (Cp/Cpk/Pp/Ppk), spec yield and the limit lines are withheld for these tests \u2014 there is no single spec to judge the merged population against."}),e}
|
|
@@ -5,7 +5,21 @@ import type { MetadataFieldDef } from '../renderer/buildWaferMap.js';
|
|
|
5
5
|
* every row; a `'die'` column genuinely varies die to die. */
|
|
6
6
|
export type MetadataColumnScope = 'die' | 'wafer';
|
|
7
7
|
/** How metadata keys are chosen. `'auto'` = every key present in the data. */
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* `'auto'` — every displayable key.
|
|
10
|
+
* `'identity'` — only the curated keys that identify a population (lot, wafer,
|
|
11
|
+
* product, program, split, operator, date…), i.e. `DEFAULT_FACET_CURATION`.
|
|
12
|
+
* `'none'` — no columns.
|
|
13
|
+
* `string[]` — exactly these keys, when present.
|
|
14
|
+
*
|
|
15
|
+
* `'identity'` exists because `'auto'` is the wrong default for a per-test
|
|
16
|
+
* export: a host that maps an STDF header into wafer metadata carries WCR
|
|
17
|
+
* geometry (`Center X`, `Die Ht`, `Wf Flat`, `Wafr Siz`, `Pos X`…) alongside the
|
|
18
|
+
* identity fields, and `'auto'` stamped all fifteen of them as constant leading
|
|
19
|
+
* columns on every row — pushing the actual statistics off the right of the
|
|
20
|
+
* screen while answering none of "which wafer is this?".
|
|
21
|
+
*/
|
|
22
|
+
export type MetadataKeySelection = 'auto' | 'identity' | 'none' | string[];
|
|
9
23
|
export interface MetadataColumn {
|
|
10
24
|
/** The metadata key this column reads. */
|
|
11
25
|
key: string;
|
|
@@ -62,9 +76,11 @@ export declare function discoverDieMetadataKeys(dies: Die[], metadataFields?: Me
|
|
|
62
76
|
* Build the metadata column set for a population.
|
|
63
77
|
*
|
|
64
78
|
* A key present on both a die and its wafer yields exactly ONE column, scope
|
|
65
|
-
* `'die'`, carrying the die value
|
|
66
|
-
*
|
|
67
|
-
*
|
|
79
|
+
* `'die'`, carrying the die value. Two columns for one key could otherwise
|
|
80
|
+
* disagree in the same row. (This used to cite the hover tooltip as applying the
|
|
81
|
+
* same `{ ...waferMeta, ...die.metadata }` shadowing; the tooltip no longer shows
|
|
82
|
+
* wafer metadata at all — that identity moved to the identity header — so the
|
|
83
|
+
* rule now stands on its own reasoning rather than a parallel that has gone.)
|
|
68
84
|
*/
|
|
69
85
|
export declare function resolveMetadataColumns(o: ResolveMetadataColumnsOptions): MetadataColumnSet;
|
|
70
86
|
//# sourceMappingURL=metadataColumns.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{metadataDisplayValue as k}from"../core/metadata.js";import{compareNatural as w,prettyKey as
|
|
1
|
+
import{metadataDisplayValue as k}from"../core/metadata.js";import{compareNatural as w,prettyKey as A}from"../core/utils.js";import{DEFAULT_FACET_CURATION as K}from"./facets.js";const b=64;export function discoverDieMetadataKeys(e,n,s=b){const d=new Set;for(const c of e)if(c.metadata)for(const i of Object.keys(c.metadata))d.add(i);const t=[];for(const c of n??[])d.has(c.key)&&!t.includes(c.key)&&t.push(c.key);const y=[...d].filter(c=>!t.includes(c)).sort(w),a=[...t,...y];return{keys:a.slice(0,s),truncated:a.slice(s)}}function v(e){const n=Object.keys(e).filter(t=>k(e[t])!==void 0),s=Object.keys(K).filter(t=>n.includes(t)),d=n.filter(t=>!s.includes(t)).sort(w);return[...s,...d]}const h=(e,n)=>e==="none"?[]:Array.isArray(e)?e.filter(s=>n.includes(s)):e==="identity"?n.filter(s=>s in K):n;export function resolveMetadataColumns(e){const n=e.waferMetadata??{},s=e.waferPlacement??"csv",d=discoverDieMetadataKeys(e.dies??[],e.metadataFields,e.maxDieKeys??b),t=h(e.dieKeys,d.keys),y=s==="none"?[]:h(e.waferKeys,v(n)).filter(r=>!t.includes(r)),a=new Set(e.reservedLabels??[]),c=(r,o)=>{const f=e.metadataFields?.find(u=>u.key===r)?.label??A(r);if(!a.has(f))return a.add(f),f;const m=o==="die"?`${f} (metadata)`:`${f} (wafer metadata)`;if(!a.has(m))return a.add(m),m;const l=`${f} (${r})`;if(!a.has(l))return a.add(l),l;for(let u=2;;u++){const p=`${l} (${u})`;if(!a.has(p))return a.add(p),p}},i=[];for(const r of t)i.push({key:r,label:c(r,"die"),scope:"die",get:o=>k(o.metadata?.[r])??"",csvOnly:!1});for(const r of y){const o=k(n[r])??"";i.push({key:r,label:c(r,"wafer"),scope:"wafer",get:()=>o,constant:o,csvOnly:s==="csv"})}return{columns:i,truncatedKeys:d.truncated}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{findConnectedComponents as J}from"./connectedComponents.js";import{clamp01 as K}from"../core/utils.js";export const DEFAULT_PATTERN_THRESHOLDS={nearFullGlobalRdd:.6,edgeRingEdgeRdd:.18,edgeRingEdgeRddHigh:.3,edgeRingRadialSpread:.45,edgeRingAngularSpread:.6,edgeLocalEdgeRdd:.1,edgeLocalCentroidDist:.76,donutP25Dist:.4,centerCentroidDist:.22,centerCentroidDistHigh:.1,centerEdgeRdd:.35,donutEdgeRdd:.22,scratchLinearScore:.25,scratchLinearScoreHigh:.45,scratchEccentricity:.75,minimumFailingDies:5};function F(t,o,s){const i=t.physX-o.center.x,r=t.physY-o.center.y,d=Math.sqrt(i*i+r*r)/o.radius;return Math.min(s,Math.max(1,Math.floor(d*s)+1))===s}function Q(t,o,s){if(t.length<3)return 0;let i=0,r=0,d=0;for(const g of t){const l=g.physX-o,D=g.physY-s;i+=l*l,r+=D*D,d+=l*D}i/=t.length,r/=t.length,d/=t.length;const c=i+r,n=i*r-d*d,a=Math.max(0,(c/2)**2-n),m=c/2+Math.sqrt(a),R=c/2-Math.sqrt(a);if(m<=0)return 0;const f=K(R/m);return Math.sqrt(1-f)}function V(t,o){if(o===0)return 0;const s=new Map,i=new Map,r=new Map,d=new Map;for(const n of t)s.set(n.y,(s.get(n.y)??0)+1),i.set(n.x,(i.get(n.x)??0)+1),r.set(n.x-n.y,(r.get(n.x-n.y)??0)+1),d.set(n.x+n.y,(d.get(n.x+n.y)??0)+1);let c=0;for(const n of s.values())n>c&&(c=n);for(const n of i.values())n>c&&(c=n);for(const n of r.values())n>c&&(c=n);for(const n of d.values())n>c&&(c=n);return c/o}function W(t,o,s,i){const r=o.length,d=t.length,c=r>0?d/r:0,n=o.filter(e=>F(e,s,i)),a=t.filter(e=>F(e,s,i)),m=n.length>0?a.length/n.length:0,R=J(t),f=R.length>0?[...R].sort((e,h)=>h.length-e.length):[t],g=f[0],l=f.slice(0,5).flat(),D=g.length,O=d>0?D/d:0,u=s.center.x,p=s.center.y,y=s.radius;let E=0,N=0,M=1/0,x=0;for(const e of g){E+=e.physX,N+=e.physY;const h=Math.sqrt((e.physX-u)**2+(e.physY-p)**2);h<M&&(M=h),h>x&&(x=h)}const H=E/g.length,P=N/g.length,T=Math.sqrt((H-u)**2+(P-p)**2)/y,I=M===1/0?0:M/y,v=Math.min(x/y,1),C=t.map(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y).sort((e,h)=>e-h),w=C[Math.floor(C.length*.25)]??0,b=o.filter(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y<.5).length,X=o.filter(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y>=.5).length,z=t.filter(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y<.5).length,A=t.filter(e=>Math.sqrt((e.physX-u)**2+(e.physY-p)**2)/y>=.5).length,Y=b>0?z/b:0,L=X>0?A/X:0,_=L>0?Y/L:Y>0?2:1,G=l.reduce((e,h)=>e+h.physX,0)/l.length,k=l.reduce((e,h)=>e+h.physY,0)/l.length,U=Q(l,G,k),B=V(l,l.length),S=16,q=new Set;for(const e of a){const h=(Math.atan2(e.physY-p,e.physX-u)+2*Math.PI)%(2*Math.PI);q.add(Math.floor(h/(2*Math.PI)*S)%S)}const j=a.length>0?q.size/S:0;return{globalRdd:c,edgeRdd:m,centroidDistNorm:T,minDistNorm:I,maxDistNorm:v,p25DistNorm:w,eccentricity:U,linearScore:B,salienceSize:D,salienceFraction:O,edgeAngularSpread:j,innerOuterRatio:_}}function Z(t,o,s){const i=Math.max(5,Math.round(s*.003)),r=t.salienceSize>=i;if(t.globalRdd<o.nearFullGlobalRdd&&t.salienceFraction<.1)return{pattern:"random",confidence:"low"};if(t.globalRdd>=o.nearFullGlobalRdd)return{pattern:"near-full",confidence:t.globalRdd>=.8?"high":"medium"};if(r&&t.linearScore>=o.scratchLinearScore&&t.eccentricity>=o.scratchEccentricity){const a=t.linearScore>=o.scratchLinearScoreHigh?"high":"medium";return{pattern:"scratch",confidence:a,note:a==="medium"?"Scratch detection is less reliable for fragmented or diagonal patterns":void 0}}const d=r&&t.edgeRdd>=o.edgeLocalEdgeRdd&&t.maxDistNorm>=.7;if(d&&t.p25DistNorm>=o.edgeLocalCentroidDist&&t.maxDistNorm-t.minDistNorm<=o.edgeRingRadialSpread)return{pattern:"edge-ring",confidence:t.edgeRdd>=o.edgeRingEdgeRddHigh?"high":"medium"};if(d&&t.p25DistNorm>=.55&&t.p25DistNorm<o.edgeLocalCentroidDist)return{pattern:"edge-local",confidence:t.edgeRdd>=o.edgeRingEdgeRdd?"medium":"low"};const c=r&&t.centroidDistNorm<=o.centerCentroidDist&&t.edgeRdd<=o.centerEdgeRdd,n="Center and donut patterns have similar geometry; classification may be imprecise";if(c&&t.p25DistNorm<o.donutP25Dist){const a=t.centroidDistNorm<=o.centerCentroidDistHigh?"high":"medium";return{pattern:"center",confidence:a,note:a!=="high"?n:void 0}}return c&&t.p25DistNorm>=o.donutP25Dist&&t.p25DistNorm<.55?{pattern:"donut",confidence:"medium",note:n}:{pattern:"random",confidence:"low"}}export function classifyPattern(t,o,s){const i={...DEFAULT_PATTERN_THRESHOLDS},r=s.ringCount??4,d=new Set(s.passBins),c=t.filter(g=>{const l=g.hbin??g.sbin;return l!==void 0&&!d.has(l)}),n=Math.max(i.minimumFailingDies,Math.round(t.length*.003));if(c.length<n)return null;const a=W(c,t,o,r),{pattern:m,confidence:R,note:f}=Z(a,i,t.length);return{pattern:m,confidence:R,features:a,...f?{note:f}:{}}}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import{formatFindingDelta as
|
|
2
|
-
<td class="tight">${
|
|
3
|
-
<td class="tight">${
|
|
4
|
-
<td>${
|
|
5
|
-
<td class="numeric">${
|
|
6
|
-
<td class="numeric">${
|
|
1
|
+
import{formatFindingDelta as h,formatFindingCoverage as w,formatFindingTooltip as b,escHtml as o,buildMetadataRows as d,renderDefinitionList as c,renderSection as p,renderSeverityBadge as g,reportStyles as v}from"./reportHtml.js";import{buildFindingsNarrative as y}from"./findingsNarrative.js";import{plainBinTerms as u}from"../renderer/fmt.js";import{visibleFindings as R}from"./filterFindings.js";function $(e,n){if(e.level==="lot"){const a=e,l=a.perWafer.map(r=>r.summary.wafer?.wafer??r.summary.wafer?.waferId).filter(r=>r!=null).map(String).join(", ");return c([...d(a.perWafer.map(r=>({metadata:r.summary.wafer}))),...l?[{label:"Wafers",value:l}]:[],{label:"Wafer count",value:String(a.stats.waferCount)},{label:"Generated",value:n}])}const t=e,i=t.stats.yieldPercent!==null?`${t.stats.yieldPercent.toFixed(1)}%`:"N/A";return c([...d([{metadata:t.wafer}]),{label:"Total dies",value:String(t.stats.totalDies)},{label:"Analysed dies",value:String(t.stats.analyzedDies)},{label:"Yield",value:i},{label:"Generated",value:n}])}function F(e,n){return e.length?e.map(t=>`<tr title="${o(b(t))}">
|
|
2
|
+
<td class="tight">${g(t.severity)}</td>
|
|
3
|
+
<td class="tight">${o(t.comparison.left)}</td>
|
|
4
|
+
<td>${o(u(t.variable.label))}</td>
|
|
5
|
+
<td class="numeric">${o(h(t))}</td>
|
|
6
|
+
<td class="numeric">${o(w(t,n))}</td>
|
|
7
7
|
</tr>`).join(`
|
|
8
|
-
`):'<tr><td colspan="5" class="no-data">No significant findings</td></tr>'}function
|
|
8
|
+
`):'<tr><td colspan="5" class="no-data">No significant findings</td></tr>'}function S(e,n){return`<table class="report-table findings-table compact">
|
|
9
9
|
<thead>
|
|
10
10
|
<tr>
|
|
11
11
|
<th>Severity</th>
|
|
@@ -18,24 +18,24 @@ import{formatFindingDelta as b,formatFindingCoverage as g,formatFindingTooltip a
|
|
|
18
18
|
<tbody>
|
|
19
19
|
${F(e,n)}
|
|
20
20
|
</tbody>
|
|
21
|
-
</table>`}export function renderFindingsReportHtml(e,n={}){const t=e.level==="lot",i=n.title??(t?"Lot Findings Report":"Wafer Findings Report"),
|
|
22
|
-
`:"",
|
|
21
|
+
</table>`}export function renderFindingsReportHtml(e,n={}){const t=e.level==="lot",i=n.title??(t?"Lot Findings Report":"Wafer Findings Report"),a=new Date().toLocaleString(),l=R(e.findings),r=e.level==="lot"?e.stats.waferCount:void 0,s=u(y(l)??""),f=s?`<p class="findings-narrative">${o(s)}</p>
|
|
22
|
+
`:"",m=[p("Summary",$(e,a)),p("Findings",f+S(l,r))].join(`
|
|
23
23
|
`);return`<!DOCTYPE html>
|
|
24
24
|
<html lang="en">
|
|
25
25
|
<head>
|
|
26
26
|
<meta charset="UTF-8">
|
|
27
|
-
<title>${
|
|
27
|
+
<title>${o(i)}</title>
|
|
28
28
|
<style>
|
|
29
|
-
${
|
|
29
|
+
${v()}
|
|
30
30
|
</style>
|
|
31
31
|
</head>
|
|
32
32
|
<body>
|
|
33
33
|
<main class="report">
|
|
34
34
|
<header class="report-header">
|
|
35
|
-
<h1>${
|
|
36
|
-
<p class="report-subtitle">Generated ${a
|
|
35
|
+
<h1>${o(i)}</h1>
|
|
36
|
+
<p class="report-subtitle">Generated ${o(a)}</p>
|
|
37
37
|
</header>
|
|
38
|
-
${
|
|
38
|
+
${m}
|
|
39
39
|
</main>
|
|
40
40
|
</body>
|
|
41
41
|
</html>`}export function openHtmlReport(e){if(typeof window.__openHtmlReport=="function"){window.__openHtmlReport(e);return}const n=URL.createObjectURL(new Blob([e],{type:"text/html"})),t=window.open(n,"_blank");setTimeout(()=>URL.revokeObjectURL(n),3e4),t||console.warn("wmap: window.open() returned null, so the report could not be shown. If this host cannot use window.open (e.g. Tauri, or popups are blocked), call setReportOpener() to supply your own opener.")}export function setReportOpener(e){window.__openHtmlReport=e}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import{isParametricTest as
|
|
2
|
-
<td class="tight">${
|
|
3
|
-
<td class="tight">${
|
|
4
|
-
<td>${
|
|
5
|
-
<td class="numeric">${
|
|
6
|
-
<td class="numeric">${
|
|
1
|
+
import{isParametricTest as me}from"../renderer/buildWaferMap.js";import{buildRingRegions as W,buildQuadrantRegions as k}from"./regions.js";import{openHtmlReport as pe}from"./renderFindingsReport.js";import{analyzeWaferLot as ge}from"./analyzeWaferLot.js";import{computeFunctionalYield as he}from"./analyzeWaferMap.js";import{sortBinsForDisplay as C}from"./binPareto.js";import{buildFacetTable as be,facetValueOf as ye,FACET_NONE_VALUE as Se}from"./facets.js";import{visibleFindings as H}from"./filterFindings.js";import{buildYieldDataCombined as we}from"./yield.js";import{buildTestPassRateData as $e,hasJudgeableTests as xe,poolFunctionalYield as ve}from"./testPassRate.js";import{buildCapabilityData as Te}from"./capability.js";import{fmt as B}from"../renderer/fmt.js";import{getDieKey as L,isPositionedDie as j}from"../core/dies.js";import{formatFindingDelta as Fe,formatFindingCoverage as De,formatFindingTooltip as Pe,escHtml as S,renderMetadataSection as I,renderMetricGrid as A,renderSection as v,renderSeverityBadge as Be,renderTable as F,reportStyles as R}from"./reportHtml.js";function Y(a,l){return l===0?"\u2014":`${(a/l*100).toFixed(1)}%`}function Ye(a){if(!a)return"";const l=a.lot??a.lotId,i=a.wafer??a.waferId,n=[l,i].filter(Boolean).map(String);return n.length?` \u2014 ${n.map(S).join(" \xB7 ")}`:""}function O(a,l,i,n,s=[1]){const e=new Map;if(n)for(const[t,c]of Object.entries(n))e.set(Number(t),c);else for(const t of a){if(t.partial||t.edgeExcluded)continue;const c=i==="hard"?t.hbin:t.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const o=[...e.values()].reduce((t,c)=>t+c,0),r=l?new Map(l.map(t=>[t.bin,t])):null,d=C(e.entries(),s).map(([t,c])=>{const m=r?.get(t)?.name??"\u2014";return[String(t),m,String(c),Y(c,o)]});return v(i==="hard"?"Hard Bin Breakdown":"Soft Bin Breakdown",F(["Bin","Name","Count","%"],d,{className:"compact"}))}function z(a,l,i,n){const s=new Set(n),e=new Map(i.map(d=>[L(d),d]));if(!i.some(d=>d.hbin!=null||d.sbin!=null)||!l.length)return"";const r=[];for(const d of l){let u=0,t=0;for(const c of d.dieKeys){const g=e.get(c);if(!g||g.partial||g.edgeExcluded)continue;const m=g.hbin??g.sbin;m!=null&&(t++,s.has(m)&&u++)}t&&r.push([d.label,String(u),String(t),Y(u,t)])}return r.length?v(a,F(["Region","Pass","Total","Yield"],r,{className:"compact"})):""}function G(a,l,i){if(!l.length)return"";const n=a.filter(o=>!o.partial&&!o.edgeExcluded),s=new Map((i??[]).map(o=>[o.testNumber,o])),e=[];for(const o of l.filter(me)){const r=o.testNumber,d=o.unit||void 0,u=s.get(r);let t,c,g;if(u)({min:t,max:c,mean:g}=u);else{const m=n.map(w=>w.testValues?.[r]).filter(w=>w!==void 0&&isFinite(w));if(!m.length)continue;m.sort((w,p)=>w-p),t=m[0],c=m[m.length-1],g=m.reduce((w,p)=>w+p,0)/m.length}e.push([S(o.name),B(t,d),B(g,d),B(c,d)])}return e.length?v("Test Values",F(["Test","Min","Mean","Max"],e,{className:"compact"})):""}function K(a,l,i){const n=i??he(a,l);if(!n?.length)return"";const s=n.map(e=>[S(e.label),String(e.totalDies),String(e.passDies),String(e.failDies),e.passRatePercent!==null?`${e.passRatePercent.toFixed(1)}%`:"\u2014"]);return v("Functional Tests",F(["Test","N","Pass","Fail","Pass Rate"],s,{className:"compact"}))}function U(a,l){const i=Te(a,l);if(!i.length)return"";const n=e=>e===null?"\u2014":e.toFixed(2),s=i.map(e=>[S(e.label),e.hasSpec?`${B(e.lsl,e.unit)} \u2013 ${B(e.usl,e.unit)}`:"\u2014",B(e.mean,e.unit),n(e.cp),n(e.cpk),n(e.pp),n(e.ppk)]);return v("Process Capability",F(["Test","Spec (LSL\u2013USL)","Mean","Cp","Cpk","Pp","Ppk"],s,{className:"compact"}))}function V(a,l){const i=a.map(s=>`<tr title="${S(Pe(s))}">
|
|
2
|
+
<td class="tight">${Be(s.severity)}</td>
|
|
3
|
+
<td class="tight">${S(s.comparison.left)}</td>
|
|
4
|
+
<td>${S(s.variable.label)}</td>
|
|
5
|
+
<td class="numeric">${S(Fe(s))}</td>
|
|
6
|
+
<td class="numeric">${S(De(s,l))}</td>
|
|
7
7
|
</tr>`).join(`
|
|
8
|
-
`)
|
|
9
|
-
<th>Severity</th><th>Region</th><th>Metric</th><th class="numeric">Delta</th><th class="numeric">${
|
|
10
|
-
</tr></thead><tbody>${i}</tbody></table
|
|
11
|
-
`)),
|
|
8
|
+
`);return`<table class="report-table findings-table compact"><thead><tr>
|
|
9
|
+
<th>Severity</th><th>Region</th><th>Metric</th><th class="numeric">Delta</th><th class="numeric">${l!==void 0?"Wafers":"N (region/rest)"}</th>
|
|
10
|
+
</tr></thead><tbody>${i}</tbody></table>`}function _(a,l){const i=H(a);return i.length?v("Findings",V(i,l)):""}export function renderSummaryReportHtml(a,l={}){const{wafer:i,dies:n,yieldSummary:s,dataCoverage:e,hbinDefs:o,sbinDefs:r,testDefs:d=[],statsSummary:u,passBins:t=[1],ringCount:c=4}=a,g=i.metadata,m=Ye(g),w=l.title??`Wafer Summary${m}`,p=new Date().toLocaleString(),f=n.some(P=>P.hbin!=null),b=n.some(P=>P.sbin!=null),h=n.filter(j),$=W(h,i,c),x=k(h,i,c),T=[{label:"Total dies",value:String(s.totalDies)},{label:"Filled dies",value:String(e.filledDies),hint:`${Y(e.filledDies,e.totalDies)} fill`},{label:"Pass dies",value:String(s.passDies)},{label:"Fail dies",value:String(s.failDies)},...s.partialDies>0?[{label:"Partial",value:String(s.partialDies)}]:[],...s.yieldPercent!==null?[{label:`Yield (pass: ${t.length===1?`bin ${t[0]}`:`bins ${t.join(", ")}`})`,value:`${s.yieldPercent.toFixed(1)}%`}]:[],...s.edgeExcludedDies>0?[{label:"Edge excluded (outer zone)",value:String(s.edgeExcludedDies)}]:[]],N=[I([{metadata:i.metadata}]),v("Summary",[A(T)].filter(Boolean).join(`
|
|
11
|
+
`)),f?O(n,o,"hard",u?.stats.hardBinCounts,t):b?O(n,r,"soft",u?.stats.softBinCounts,t):"",z("Ring Yield",$,n,t),z("Quadrant Yield",x,n,t),G(n,d,u?.stats.perTestStats),K(n,d,u?.stats.functionalYield),U([{dies:n}],d),u?_(u.findings):""].filter(Boolean).join(`
|
|
12
12
|
`);return`<!DOCTYPE html>
|
|
13
13
|
<html lang="en">
|
|
14
14
|
<head>
|
|
15
15
|
<meta charset="UTF-8">
|
|
16
|
-
<title>${
|
|
16
|
+
<title>${S(w)}</title>
|
|
17
17
|
<style>
|
|
18
18
|
${R()}
|
|
19
19
|
</style>
|
|
@@ -21,48 +21,51 @@ ${R()}
|
|
|
21
21
|
<body>
|
|
22
22
|
<main class="report">
|
|
23
23
|
<header class="report-header">
|
|
24
|
-
<h1>${
|
|
25
|
-
<p class="report-subtitle">Generated ${
|
|
24
|
+
<h1>${S(w)}</h1>
|
|
25
|
+
<p class="report-subtitle">Generated ${S(p)}</p>
|
|
26
26
|
</header>
|
|
27
27
|
${N}
|
|
28
|
-
<p class="footer">Generated ${
|
|
28
|
+
<p class="footer">Generated ${S(p)}</p>
|
|
29
29
|
</main>
|
|
30
30
|
</body>
|
|
31
|
-
</html>`}function
|
|
32
|
-
`);return{
|
|
31
|
+
</html>`}function Me(a,l){const i=a.perWafer.map(n=>{const s=l[n.waferIndex]?.label??`W${n.waferIndex+1}`,e=n.summary.stats.yieldPercent;return[s,e!==null?`${e.toFixed(1)}%`:"N/A"]});return F(["Wafer","Yield"],i,{className:"compact"})}function Ne(a){const l=[];for(const e of a.perWafer){const o=H(e.summary.findings);if(!o.length)continue;const r=e.summary.wafer?.waferId??`Wafer ${e.waferIndex+1}`;l.push(`<h3 class="report-subheading">${S(r)}</h3>`+V(o))}if(!l.length)return"";const i=l.length,n=a.perWafer.length,s=`<p class="report-note">${i} of ${n} wafer${n===1?"":"s"} have per-wafer findings; the rest had none.</p>`;return v("Findings by Wafer",s+l.join(`
|
|
32
|
+
`))}const Re=3;function Ee(a,l,i){const n=a.map(d=>({metadata:d.wafer?.metadata})),s=be(n,{facetableOnly:!0}).filter(d=>d.splittable);if(!s.length)return"";const e=s.slice(0,Re),o=[];for(const d of e){const u=new Map;a.forEach((f,b)=>{const h=ye(f.wafer?.metadata,d.key)??Se;(u.get(h)??u.set(h,[]).get(h)).push(b)});const t=[...u.entries()].map(([f,b])=>({key:f,items:b.map(h=>a[h])}));if(t.length<2)continue;const c=t.map(f=>[S(f.key),S(f.items.map(b=>b.label).join(", "))]),g=we(t.map(f=>({key:f.key,items:f.items.map(b=>({label:b.label,dies:b.dies}))})),i).map(f=>[S(f.label),String(f.itemCount),`${f.percent.toFixed(1)}%`]),m=[F([d.label,"Wafers"],c,{className:"compact"}),F([d.label,"Wafers","Yield"],g,{className:"compact"})],w=a.flatMap(f=>f.dies??[]);let p=0;for(const f of["spec","testFlag","functional"]){if(!xe(l,f,w))continue;const b=$e(t.map(x=>({key:x.key,items:x.items.map(T=>({dies:T.dies,testSpecYield:T.statsSummary?.stats.testSpecYield,functionalYield:T.statsSummary?.stats.functionalYield}))})),l,f);if(!b.rows.length)continue;const h=b.rows.map(x=>[S(x.label),...x.byGroup.map(T=>T.passRatePercent===null?"\u2014":`${T.passRatePercent.toFixed(1)}%`),x.overall.passRatePercent===null?"\u2014":`${x.overall.passRatePercent.toFixed(1)}%`]),$=f==="spec"?"Pass rate \xB7 spec limits":f==="testFlag"?"Pass rate \xB7 tester flag":"Pass rate \xB7 functional";b.disagreementDies&&(p=b.disagreementDies),m.push(`<h4 class="report-subheading">${S($)}</h4>`+F(["Test",...b.groups,"All"],h,{className:"compact"}))}p&&m.push(`<p class="report-note">${p.toLocaleString()} dies judged differently by limits and tester flag.</p>`),o.push(`<h3 class="report-subheading">${S(d.label)}</h3>${m.join(`
|
|
33
|
+
`)}`)}if(!o.length)return"";const r=s.length>e.length?`<p class="report-note">${s.length-e.length} further splittable field(s) not shown.</p>`:"";return v("Split Comparison",o.join(`
|
|
34
|
+
`)+r)}function Q(a,l,i,n,s=[1]){const e=new Map,o=i==="hard"?"hardBinCounts":"softBinCounts";if(n?.length&&n.every(t=>t.stats[o]!==void 0))for(const t of n)for(const[c,g]of Object.entries(t.stats[o])){const m=Number(c);e.set(m,(e.get(m)??0)+g)}else for(const t of a){if(t.partial||t.edgeExcluded)continue;const c=i==="hard"?t.hbin:t.sbin;c!=null&&e.set(c,(e.get(c)??0)+1)}if(!e.size)return"";const r=[...e.values()].reduce((t,c)=>t+c,0),d=l?new Map(l.map(t=>[t.bin,t])):null,u=C(e.entries(),s).map(([t,c])=>{const g=d?.get(t);return[g?.name?`Bin ${t} \xB7 ${g.name} (${c})`:`Bin ${t} (${c})`,Y(c,r)]});return v(i==="hard"?"Hard Bin Breakdown (All Wafers)":"Soft Bin Breakdown (All Wafers)",F(["Bin","Yield"],u,{className:"compact"}))}function q(a,l,i,n,s,e){const o=new Set(e),r=new Map,d=[];for(let t=0;t<n.length;t++){const c=n[t],g=i[t]??[];if(!g.length)continue;const m=l(g.filter(j),c,s),w=new Map(g.map(p=>[L(p),p]));for(const p of m){d.includes(p.label)||d.push(p.label);const f=r.get(p.label)??{pass:0,total:0};for(const b of p.dieKeys){const h=w.get(b);if(!h||h.partial||h.edgeExcluded)continue;const $=h.hbin??h.sbin;$!=null&&(f.total++,o.has($)&&f.pass++)}r.set(p.label,f)}}if(!r.size)return"";const u=d.filter(t=>r.get(t)?.total).map(t=>{const c=r.get(t);return[t,Y(c.pass,c.total)]});return u.length?v(a,F(["Region","Yield"],u,{className:"compact"})):""}function We(a,l,i){let n;if(i?.length&&i.every(s=>s.stats.perTestStats!==void 0)){const s=new Map;for(const e of i)for(const o of e.stats.perTestStats??[]){const r=s.get(o.testNumber);r?(r.n+=o.count,r.sum+=o.mean*o.count,r.min=Math.min(r.min,o.min),r.max=Math.max(r.max,o.max)):s.set(o.testNumber,{n:o.count,sum:o.mean*o.count,min:o.min,max:o.max})}n=[...s.entries()].map(([e,o])=>({testNumber:e,min:o.min,max:o.max,mean:o.sum/o.n}))}return G(a,l,n)}function ke(a,l,i){const n=ve(i);return K(a,l,n)}const E=["lot","product","testProgram","temperature"];function Ce(a){const l=new Map(E.map(e=>[e,new Set]));for(const e of a)for(const o of E){const r=e.wafer?.metadata?.[o];r!=null&&r!==""&&l.get(o).add(String(r))}const i=E.filter(e=>l.get(e).size>1);if(i.length===0)return[{label:"",items:a,varying:i}];const n=new Map,s=[];for(const e of a){const o=i.map(r=>String(e.wafer?.metadata?.[r]??"(none)")).join(" \xB7 ");n.has(o)||(n.set(o,[]),s.push(o)),n.get(o).push(e)}return s.map(e=>({label:e,items:n.get(e),varying:i}))}function J(a,l,i,n,s,e,o){const r=ge(a,{perWaferSummaries:a.map(y=>y.statsSummary),...o}),d=[],u=[],t=[];for(const y of a){const D=y.dies??[];y.wafer&&(d.push(y.wafer),t.push(D)),D.length&&u.push(...D)}const c=u.some(y=>y.hbin!=null),g=u.some(y=>y.sbin!=null),m=c||g,w=r.perWafer.map(y=>y.summary.stats.yieldPercent).filter(y=>y!==null),p=w.length?w.reduce((y,D)=>y+D,0)/w.length:null,f=new Set(s);let b=0,h=0,$=0,x=0,T=0;for(const y of u){if(b++,y.edgeExcluded&&x++,y.partial&&T++,y.partial||y.edgeExcluded)continue;const D=y.hbin??y.sbin;D!=null&&(h++,f.has(D)&&$++)}const N=h-$,P=h>0?$/h*100:null,Z=[{label:"Wafers",value:String(r.stats.waferCount)},{label:"Total dies",value:String(b)},...m?[{label:"Good dies",value:String($)},{label:"Bad dies",value:String(N)}]:[],...x>0?[{label:"Edge excluded",value:String(x)}]:[],...T>0?[{label:"Partial dies",value:String(T)}]:[],...p!==null?[{label:"Mean wafer yield",value:`${p.toFixed(1)}%`}]:[],...P!==null?[{label:"Total yield",value:`${P.toFixed(1)}%`}]:[]],ee=v("Lot Summary",A(Z)),te=I(a.map(y=>({metadata:y.wafer?.metadata}))),ne=v("Per-Wafer Yield",Me(r,a)),se=Ee(a,n,s),M=r.perWafer.map(y=>y.summary),ae=m?c?Q(u,l,"hard",M,s):Q(u,i,"soft",M,s):"",oe=m&&d.length?q("Ring Yield (All Wafers)",W,t,d,e,s):"",ie=m&&d.length?q("Quadrant Yield (All Wafers)",k,t,d,e,s):"",le=n.length?We(u,n,M):"",re=n.length?ke(u,n,M):"",ce=n.length?U(a,n):"",de=r.findings.length?_(r.findings,r.stats.waferCount):"",ue=Ne(r),fe=[ee,te,ne,se,ae,oe,ie,le,re,ce,de,ue].filter(Boolean).join(`
|
|
35
|
+
`);return{lotSummary:r,sections:fe}}function X(a,l,i){return`<main class="report">
|
|
33
36
|
<header class="report-header">
|
|
34
|
-
<h1>${
|
|
35
|
-
<p class="report-subtitle">Generated ${
|
|
37
|
+
<h1>${S(a)}</h1>
|
|
38
|
+
<p class="report-subtitle">Generated ${S(i)}</p>
|
|
36
39
|
</header>
|
|
37
|
-
${
|
|
38
|
-
<p class="footer">Generated ${
|
|
39
|
-
</main>`}export function renderLotSummaryReportHtml(a,
|
|
40
|
+
${l}
|
|
41
|
+
<p class="footer">Generated ${S(i)}</p>
|
|
42
|
+
</main>`}export function renderLotSummaryReportHtml(a,l={}){const{items:i,hbinDefs:n,sbinDefs:s,testDefs:e=[],passBins:o=[1],ringCount:r=4,analyzeOptions:d}=a,u=new Date().toLocaleString(),t=Ce(i);if(t.length===1){const{lotSummary:p,sections:f}=J(t[0].items,n,s,e,o,r,d),b=(()=>{const $=p.lot;if(!$)return"";const x=$.lot??$.lotId;return x?` \u2014 ${S(String(x))}`:""})(),h=l.title??`Lot Summary${b}`;return`<!DOCTYPE html>
|
|
40
43
|
<html lang="en">
|
|
41
44
|
<head>
|
|
42
45
|
<meta charset="UTF-8">
|
|
43
|
-
<title>${h
|
|
46
|
+
<title>${S(h)}</title>
|
|
44
47
|
<style>
|
|
45
48
|
${R()}
|
|
46
49
|
</style>
|
|
47
50
|
</head>
|
|
48
51
|
<body>
|
|
49
|
-
${
|
|
52
|
+
${X(h,f,u)}
|
|
50
53
|
</body>
|
|
51
|
-
</html>`}const
|
|
52
|
-
${
|
|
54
|
+
</html>`}const c=l.title??"Lot Summary",g=t.map(p=>{const{lotSummary:f,sections:b}=J(p.items,n,s,e,o,r,d),h=p.varying.includes("lot")?void 0:f.lot?.lot??f.lot?.lotId,$=h?`${p.label} \xB7 Lot ${String(h)}`:p.label;return X(`${c} \u2014 ${$}`,b,u)}),m=`<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 ${t.length} groups by identity \u2014 shown separately below so stats are never pooled across them.</p>`,w=g.map((p,f)=>`${f===0?"":'<hr style="max-width:1080px;margin:32px auto 0;border:none;border-top:3px solid var(--report-line-strong);">'}
|
|
55
|
+
${p}`).join(`
|
|
53
56
|
`);return`<!DOCTYPE html>
|
|
54
57
|
<html lang="en">
|
|
55
58
|
<head>
|
|
56
59
|
<meta charset="UTF-8">
|
|
57
|
-
<title>${
|
|
60
|
+
<title>${S(c)}</title>
|
|
58
61
|
<style>
|
|
59
62
|
${R()}
|
|
60
63
|
</style>
|
|
61
64
|
</head>
|
|
62
65
|
<body>
|
|
63
66
|
<div class="report" style="padding-top:24px;">
|
|
64
|
-
${
|
|
67
|
+
${m}
|
|
65
68
|
</div>
|
|
66
|
-
${
|
|
69
|
+
${w}
|
|
67
70
|
</body>
|
|
68
|
-
</html>`}export{
|
|
71
|
+
</html>`}export{pe as openHtmlReport};
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import{fmt as
|
|
2
|
-
<h2>${escHtml(
|
|
3
|
-
${
|
|
4
|
-
</section>`}export function renderDefinitionList(t
|
|
5
|
-
`);return`<dl class="${
|
|
1
|
+
import{fmt as n}from"../renderer/fmt.js";import{buildFacetTable as s,prettyKey as d}from"./facets.js";export function escHtml(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}export function renderSection(e,t,r=""){return`<section class="${["report-section",r].filter(Boolean).join(" ")}">
|
|
2
|
+
<h2>${escHtml(e)}</h2>
|
|
3
|
+
${t}
|
|
4
|
+
</section>`}export function renderDefinitionList(e,t="meta-list"){if(!e.length)return"";const r=e.map(o=>`<div class="definition-item"><dt>${escHtml(o.label)}</dt><dd>${escHtml(o.value)}</dd></div>`).join(`
|
|
5
|
+
`);return`<dl class="${t}">
|
|
6
6
|
${r}
|
|
7
|
-
</dl>`}export function buildMetadataRows(
|
|
8
|
-
${
|
|
7
|
+
</dl>`}export function buildMetadataRows(e){return s(e,{facetableOnly:e.length>1}).map(r=>({label:d(r.key),value:r.values.map(o=>o.value).join(", ")}))}export function renderMetadataSection(e){const t=buildMetadataRows(e);return t.length?renderSection("Wafer Info",renderDefinitionList(t)):""}export function renderMetricGrid(e){return e.length?`<dl class="metric-grid">
|
|
8
|
+
${e.map(r=>`<div class="metric">
|
|
9
9
|
<dt>${escHtml(r.label)}</dt>
|
|
10
10
|
<dd>${escHtml(r.value)}</dd>
|
|
11
11
|
${r.hint?`<span class="metric-hint">${escHtml(r.hint)}</span>`:""}
|
|
12
12
|
</div>`).join(`
|
|
13
13
|
`)}
|
|
14
|
-
</dl>`:""}export function renderTable(t,
|
|
15
|
-
`);return`<table class="${
|
|
16
|
-
<thead><tr>${
|
|
14
|
+
</dl>`:""}export function renderTable(e,t,r={}){if(!t.length)return`<p class="no-data">${escHtml(r.emptyMessage??"No data")}</p>`;const o=["report-table",r.className].filter(Boolean).join(" "),i=e.map(a=>`<th>${escHtml(a)}</th>`).join(""),p=t.map(a=>`<tr>${a.map(l=>`<td>${l}</td>`).join("")}</tr>`).join(`
|
|
15
|
+
`);return`<table class="${o}">
|
|
16
|
+
<thead><tr>${i}</tr></thead>
|
|
17
17
|
<tbody>${p}</tbody>
|
|
18
|
-
</table>`}export function renderSeverityBadge(
|
|
18
|
+
</table>`}export function renderSeverityBadge(e){const t=e.charAt(0).toUpperCase()+e.slice(1);return`<span class="badge badge-${e}">${t}</span>`}export function formatFindingDelta(e){const t=e.effect.absoluteDelta??0,r=t>0?"+":t<0?"\u2212":"";if(e.variable.kind==="yield"||e.variable.kind==="hardBin"||e.variable.kind==="softBin")return`${r}${Math.abs(t*100).toFixed(1)} pp`;if(e.variable.kind==="test"){if(e.effect.relativeDelta!==void 0&&Number.isFinite(e.effect.relativeDelta)){const o=Math.abs(e.effect.relativeDelta*100).toFixed(1);return`${r}${o}%`}return`${r}${n(Math.abs(t),e.variable.unit)}`}return`${r}${n(Math.abs(t),e.variable.unit)}`}export function formatFindingTooltip(e){return e.summary}export function formatFindingCoverage(e,t){return t!==void 0?`${e.stats.sampleSizeLeft}/${t}`:`${e.stats.sampleSizeLeft}/${e.stats.sampleSizeRight}`}export function reportStyles(){return`
|
|
19
19
|
:root {
|
|
20
20
|
--report-font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
21
21
|
--report-text: #1f2328;
|
|
@@ -72,6 +72,26 @@ ${t.map(r=>`<div class="metric">
|
|
|
72
72
|
font-size: 12px;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/* Sub-headings and notes inside a section \u2014 used by the split comparison,
|
|
76
|
+
which nests one block per facet under a single section heading. */
|
|
77
|
+
.report-subheading {
|
|
78
|
+
margin: 14px 0 6px;
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
color: var(--report-text);
|
|
82
|
+
}
|
|
83
|
+
h4.report-subheading {
|
|
84
|
+
margin: 10px 0 4px;
|
|
85
|
+
font-size: 12px;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
color: var(--report-muted);
|
|
88
|
+
}
|
|
89
|
+
.report-note {
|
|
90
|
+
margin: 4px 0 0;
|
|
91
|
+
font-size: 11px;
|
|
92
|
+
color: var(--report-muted);
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
section.report-section {
|
|
76
96
|
margin-top: 18px;
|
|
77
97
|
break-inside: avoid;
|
|
@@ -186,7 +206,12 @@ ${t.map(r=>`<div class="metric">
|
|
|
186
206
|
padding-top: 3px;
|
|
187
207
|
padding-bottom: 5px;
|
|
188
208
|
color: var(--report-muted);
|
|
189
|
-
|
|
209
|
+
/* 11px, not 10px. This document prints (see @page below), where 10px is
|
|
210
|
+
about 7.5pt \u2014 too small to read on paper. The report keeps its own
|
|
211
|
+
document scale (20 title / 18 stat / 13 / 12 body / 11 label) rather than
|
|
212
|
+
the app's denser tiers; this is a print-legibility floor, not an
|
|
213
|
+
alignment to those. */
|
|
214
|
+
font-size: 11px;
|
|
190
215
|
font-weight: 600;
|
|
191
216
|
text-transform: uppercase;
|
|
192
217
|
letter-spacing: 0.08em;
|
|
@@ -230,7 +255,7 @@ ${t.map(r=>`<div class="metric">
|
|
|
230
255
|
padding: 1px 6px;
|
|
231
256
|
border: 1px solid transparent;
|
|
232
257
|
border-radius: 999px;
|
|
233
|
-
font-size:
|
|
258
|
+
font-size: 11px; /* print floor \u2014 see the table-header note above */
|
|
234
259
|
font-weight: 600;
|
|
235
260
|
line-height: 1.35;
|
|
236
261
|
letter-spacing: 0.04em;
|