@wafertools/wafermap 0.30.1 → 0.30.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/AGENTS.md +71 -11
  2. package/CHANGELOG.md +464 -8
  3. package/README.md +6 -3
  4. package/dist/packages/canvas-adapter/chartPopulation.d.ts +48 -0
  5. package/dist/packages/canvas-adapter/chartPopulation.js +1 -0
  6. package/dist/packages/canvas-adapter/charts/barPanel.d.ts +4 -1
  7. package/dist/packages/canvas-adapter/charts/barPanel.js +1 -1
  8. package/dist/packages/canvas-adapter/charts/boxplot.d.ts +4 -1
  9. package/dist/packages/canvas-adapter/charts/boxplot.js +1 -1
  10. package/dist/packages/canvas-adapter/charts/capability.js +1 -1
  11. package/dist/packages/canvas-adapter/charts/chartShell.d.ts +81 -7
  12. package/dist/packages/canvas-adapter/charts/chartShell.js +1 -1
  13. package/dist/packages/canvas-adapter/charts/correlation.js +2 -2
  14. package/dist/packages/canvas-adapter/charts/groupedBarPlot.js +1 -1
  15. package/dist/packages/canvas-adapter/charts/histogram.js +1 -1
  16. package/dist/packages/canvas-adapter/charts/regionYieldDiagram.js +1 -1
  17. package/dist/packages/canvas-adapter/charts/scatter.js +1 -1
  18. package/dist/packages/canvas-adapter/charts/sweep.d.ts +31 -0
  19. package/dist/packages/canvas-adapter/charts/sweep.js +1 -0
  20. package/dist/packages/canvas-adapter/charts/trend.d.ts +3 -1
  21. package/dist/packages/canvas-adapter/charts/trend.js +1 -1
  22. package/dist/packages/canvas-adapter/chunked.d.ts +45 -0
  23. package/dist/packages/canvas-adapter/chunked.js +1 -0
  24. package/dist/packages/canvas-adapter/dieList.js +2 -2
  25. package/dist/packages/canvas-adapter/drilldown.d.ts +23 -0
  26. package/dist/packages/canvas-adapter/drilldown.js +1 -0
  27. package/dist/packages/canvas-adapter/icons.js +1 -1
  28. package/dist/packages/canvas-adapter/insightsTab.d.ts +55 -3
  29. package/dist/packages/canvas-adapter/insightsTab.js +1 -1
  30. package/dist/packages/canvas-adapter/renderWaferGallery.d.ts +55 -0
  31. package/dist/packages/canvas-adapter/renderWaferGallery.js +1 -1
  32. package/dist/packages/canvas-adapter/renderWaferMap.js +1 -1
  33. package/dist/packages/canvas-adapter/summaryPanel.d.ts +55 -9
  34. package/dist/packages/canvas-adapter/summaryPanel.js +3 -3
  35. package/dist/packages/canvas-adapter/toCanvas.js +1 -1
  36. package/dist/packages/canvas-adapter/toolbar.d.ts +30 -2
  37. package/dist/packages/canvas-adapter/toolbar.js +2 -2
  38. package/dist/packages/canvas-adapter/userGuideHtml.d.ts +1 -1
  39. package/dist/packages/canvas-adapter/userGuideHtml.js +126 -3
  40. package/dist/packages/canvas-adapter/version.d.ts +2 -2
  41. package/dist/packages/canvas-adapter/version.js +1 -1
  42. package/dist/packages/canvas-adapter/warnings.js +1 -1
  43. package/dist/packages/core/aggregates.js +1 -1
  44. package/dist/packages/core/utils.d.ts +55 -0
  45. package/dist/packages/core/utils.js +1 -1
  46. package/dist/packages/renderer/axisTicks.d.ts +32 -0
  47. package/dist/packages/renderer/axisTicks.js +1 -0
  48. package/dist/packages/renderer/binColors.d.ts +14 -0
  49. package/dist/packages/renderer/binColors.js +1 -1
  50. package/dist/packages/renderer/buildView.d.ts +11 -37
  51. package/dist/packages/renderer/buildView.js +1 -1
  52. package/dist/packages/renderer/buildWaferMap.d.ts +58 -1
  53. package/dist/packages/renderer/buildWaferMap.js +1 -1
  54. package/dist/packages/renderer/deprecate.d.ts +13 -5
  55. package/dist/packages/renderer/deprecate.js +1 -1
  56. package/dist/packages/renderer/derivedTests/apply.d.ts +85 -0
  57. package/dist/packages/renderer/derivedTests/apply.js +1 -0
  58. package/dist/packages/renderer/derivedTests/evaluate.d.ts +48 -0
  59. package/dist/packages/renderer/derivedTests/evaluate.js +1 -0
  60. package/dist/packages/renderer/derivedTests/parser.d.ts +148 -0
  61. package/dist/packages/renderer/derivedTests/parser.js +2 -0
  62. package/dist/packages/renderer/fmt.d.ts +24 -2
  63. package/dist/packages/renderer/fmt.js +1 -1
  64. package/dist/packages/renderer/index.d.ts +1 -0
  65. package/dist/packages/renderer/index.js +1 -1
  66. package/dist/packages/renderer/spec.d.ts +49 -0
  67. package/dist/packages/renderer/spec.js +1 -0
  68. package/dist/packages/renderer/testLabel.d.ts +154 -0
  69. package/dist/packages/renderer/testLabel.js +1 -0
  70. package/dist/packages/stats/analyzeWaferMap.js +1 -1
  71. package/dist/packages/stats/capability.d.ts +60 -0
  72. package/dist/packages/stats/capability.js +1 -1
  73. package/dist/packages/stats/correlation.d.ts +33 -0
  74. package/dist/packages/stats/correlation.js +1 -1
  75. package/dist/packages/stats/deprecated.d.ts +3 -0
  76. package/dist/packages/stats/deprecated.js +1 -1
  77. package/dist/packages/stats/findingsNarrative.js +1 -1
  78. package/dist/packages/stats/index.d.ts +2 -2
  79. package/dist/packages/stats/index.js +1 -1
  80. package/dist/packages/stats/math.d.ts +27 -2
  81. package/dist/packages/stats/math.js +1 -1
  82. package/dist/packages/stats/mergeTestDefs.js +1 -1
  83. package/dist/packages/stats/renderFindingsReport.js +8 -28
  84. package/dist/packages/stats/renderSummaryReport.js +22 -31
  85. package/dist/packages/stats/reportHtml.d.ts +27 -0
  86. package/dist/packages/stats/reportHtml.js +35 -14
  87. package/dist/packages/stats/sweep.d.ts +178 -0
  88. package/dist/packages/stats/sweep.js +1 -0
  89. package/dist/packages/stats/sweepXFromName.d.ts +27 -0
  90. package/dist/packages/stats/sweepXFromName.js +1 -0
  91. package/dist/packages/stats/testPassRate.js +1 -1
  92. package/dist/packages/stats/types.d.ts +28 -1
  93. package/llms.txt +14 -8
  94. package/package.json +2 -2
package/AGENTS.md CHANGED
@@ -25,8 +25,24 @@ loudly over guessing.
25
25
  - `@wafertools/wafermap/worker` — `createWafermapWorker()` for off-main-thread builds.
26
26
 
27
27
  Default path: `buildWaferMap()` once when data loads, then `renderWaferMap()` for a
28
- single wafer or `renderWaferGallery()` for several. Do not use `toCanvas()`, `buildView()`
29
- or the other low-level pipeline functions: they are deprecated and removed in 0.31.0.
28
+ single wafer or `renderWaferGallery()` for several.
29
+
30
+ **The types still export a large deprecated surface that is removed in 0.31.0 — do not
31
+ reach into it just because autocomplete offers it.** Four groups, all replaced by the
32
+ default path above:
33
+
34
+ - the low-level drawing pipeline — `buildView()`, `toCanvas()`, `createWafer()`,
35
+ `generateDies()`, and the geometry and transform helpers around them;
36
+ - the chart-data builders — `buildYieldData()`, `buildCorrelationMatrix()`,
37
+ `buildTestBoxplotData()` and the rest: these were the internals of the Insights tab,
38
+ which the renderers now mount for you (see below);
39
+ - the region builders — `buildRingRegions()`, `buildQuadrantRegions()` and friends:
40
+ region yield comes back from `analyzeWaferMap()`;
41
+ - per-die and per-colour helpers — `getDieTestValue()`, `buildHoverText()`,
42
+ `resolveBinColors()`, `getValueColorScheme()`, `valueToViridis()`.
43
+
44
+ If the only way to do something is through one of these, that is a library gap worth
45
+ reporting, not a pattern to build on.
30
46
 
31
47
  ### Traps that produce silently wrong maps
32
48
 
@@ -37,8 +53,20 @@ or the other low-level pipeline functions: they are deprecated and removed in 0.
37
53
  through unchanged; `dieConfig.width`/`height` convert to physical units. Do not
38
54
  pre-multiply. The geometry inputs are `waferConfig` (type `WaferConfig`) and
39
55
  `dieConfig` (type `DieConfig`) — both optional, both inferred when omitted.
40
- - **`passBins` decides both the yield number and the wording of its label.** Set it
41
- from the actual test program. Do not assume `[1]`.
56
+ - **Bins and test values must be numbers, and verdicts booleans.** Every parser —
57
+ CSV, JSON, a spreadsheet export — hands you `"1"`, and `"1"` is not pass bin 1: those
58
+ dies count as fails and the yield is wrong, while a test value left as text is not
59
+ plotted or analysed correctly. Convert with `Number()` at parse time. `buildWaferMap`
60
+ samples the input and reports `input-values-not-numbers` (severity `'error'`) rather
61
+ than coercing behind your back, so a build that "works" can still be wrong — read the
62
+ warnings.
63
+ - **`passBins` and `ringCount` are set once, on `buildWaferMap`, and travel on the
64
+ result.** Neither is an option on `analyzeWaferMap`, `analyzeWaferLot`,
65
+ `renderWaferMap` or `renderWaferGallery` — passing one there is a type error in
66
+ TypeScript, and in JavaScript it is ignored with an `analysis-option-corrected`
67
+ warning while the real value is read from the map. `passBins` decides both the yield
68
+ number and the wording of its label, so set it from the actual test program: do not
69
+ assume `[1]`, and never re-default to `[1]` downstream — read `result.passBins`.
42
70
  - **`testValues` is keyed by test number**, e.g. `{ 1050: 0.42 }` — not a positional
43
71
  array. `activeTest` likewise takes a *test number* (`1050`), not an index.
44
72
  - **Functional tests (`testType: 'F'`) have no measured value.** Read their verdicts
@@ -88,7 +116,37 @@ or the other low-level pipeline functions: they are deprecated and removed in 0.
88
116
  your own (a table swatch, an export)? Read `controller.getBinColors()` for a live map,
89
117
  or `binColorsForMaps(results)` — never a palette lookup of your own.
90
118
  - Build once, render many: `buildWaferMap()` handles data + geometry; re-render UI
91
- changes through the controller's `setOptions()`, not by rebuilding.
119
+ changes through the controller's `setOptions()`, not by rebuilding. New data for a
120
+ map that is already mounted goes through `setResult()` — do not `destroy()` and
121
+ remount.
122
+ - **The analysis surfaces are already built — do not reimplement them.** Pass
123
+ `statsSummary` to `renderWaferMap` and it mounts the Summary panel; pass
124
+ `insights: { enabled: true }` and it mounts the chart suite (yield, bin pareto,
125
+ boxplot, histogram, correlation, scatter, capability, and one card per
126
+ `insights.sweeps` entry). `renderWaferGallery` takes the same option across a whole
127
+ lot. Supply or replace the analysis later with `setStatsSummary()`. Hand-building
128
+ those charts is what the deprecated chart-data builders were for, and they go in
129
+ 0.31.0. Charting a selection or one wafer (right-click → histogram, capability,
130
+ sweeps) is built in too, with no wiring.
131
+ - **A value computed from other tests is a derived test, not a host-side column.** Pass
132
+ `derivedTests` (a `TestDef` plus an `expression`, e.g. `'abs(t[1020] - t[1010])'`) to
133
+ `buildWaferMap`; it then behaves as a measured test everywhere, marked `†` as not
134
+ measured. Computing it in the host and injecting it into `testValues` loses that
135
+ mark, the missing-input rule (absent, never 0) and the collision check. A boolean
136
+ expression must be declared `testType: 'F'`.
137
+ - **A sweep's x values are data, not guesses.** Give `xValues`, or `xFromName` (a
138
+ `{x}` placeholder pattern, not a regex) when the swept value is only in the test
139
+ name. Never derive x from test numbers: they are identifiers, not a scale.
140
+ - **Click-to-highlight is wired, not hand-rolled.** `onSelect` reports what the user
141
+ picked; `setSelection(dies)` / `clearSelection()` drive it from your own UI — for
142
+ example from a finding, whose `dieKeys` match `getDieKey(die)` exactly.
143
+ - A die layout with no test data — a map of the reticle or the grid alone — is
144
+ `buildWaferMap({ layout: true, waferConfig, dieConfig })`, not a synthesized results
145
+ array.
146
+ - `valueColorScheme` and `reverseValueScheme` travel as a pair. Every built-in gradient
147
+ but `'traffic'` and `'jet'` reads low = dark, high = light; if you draw your own
148
+ colorbar or swatch, resolve the colour through `resolveValueColorFn(name, reversed)`
149
+ so it cannot disagree with the dies.
92
150
  - `result.view` is internal. Use the promoted fields: `result.plotMode`,
93
151
  `result.metadata`, `result.isLotStack`, `result.hbinDefs`, `result.sbinDefs`,
94
152
  `result.testDefs`.
@@ -135,10 +193,10 @@ it is handed, because it has no way to know which tests anyone will look at.
135
193
  - **A Web Worker buys responsiveness, not speed.** `createWafermapWorker` copies data
136
194
  across `postMessage`, so total time goes *up*. Use it when a build would otherwise
137
195
  visibly freeze the page, not for small datasets.
138
- - **Capability, pass rates and region yield come back from the analysis** —
139
- `stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
140
- `stats.testFlagYield`, `stats.functionalYield` and `stats.regionYield`, on wafer and
141
- lot summaries alike. Do not compute Cp/Cpk or ring yield yourself: the pooled
196
+ - **Capability, pass rates, region yield and the spatial-pattern label come back from
197
+ the analysis** — `stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
198
+ `stats.testFlagYield`, `stats.functionalYield`, `stats.regionYield` and
199
+ `stats.spatialPattern`, on wafer and lot summaries alike. Do not compute Cp/Cpk or ring yield yourself: the pooled
142
200
  within-wafer stddev and per-wafer pass bins are easy to get subtly wrong.
143
201
  - **Reports from code: `renderWaferReportHtml(result, summary)` and
144
202
  `renderLotReportHtml(results)`** — they take the built maps, so pass bins and ring
@@ -157,7 +215,7 @@ it is handed, because it has no way to know which tests anyone will look at.
157
215
  | `WaferMapResult.inference.warnings` | `WaferMapResult.warnings` (structured, with a `code`) |
158
216
  | `ViewOptions.testIndex` | `activeTest` |
159
217
  | `mountWaferCanvas` | `renderWaferMap` |
160
- | `HARD_BIN_COLORS` / `SOFT_BIN_COLORS` | `BIN_PALETTE` |
218
+ | `HARD_BIN_COLORS` / `SOFT_BIN_COLORS` | `BinDef.color`, or `registerBinColorScheme` — there is no exported palette constant |
161
219
  | `GalleryItem` | `WaferMapDisplayItem` |
162
220
  | `MountOptions` | `RenderOptions` |
163
221
  | `WaferCanvasController` | `WaferMapController` |
@@ -172,6 +230,8 @@ it is handed, because it has no way to know which tests anyone will look at.
172
230
  | `plotMode: 'specLimit'` | `passFailDisplay: 'spec'` |
173
231
  | standalone `getDieAtPoint` | `onHover` / `onClick` on `renderWaferMap` |
174
232
  | `RenderOptions.tooltipTestLimit` | (was a no-op; nothing replaces it) |
233
+ | `enableYieldAnalysis` / `enableHardBinAnalysis` / `enableSoftBinAnalysis` / `enableReticlePositionAnalysis` / `enableTestSiteAnalysis` / `enableClusterAnalysis` / `enableAngularAnalysis` / `enablePatternClassification` | nothing — every analysis runs; scope cost with `testNumbers` instead |
234
+ | `WaferMapController.setIdentityVisible` | `showIdentity` in `RenderOptions` |
175
235
 
176
236
  Passing a removed option is a type error, and is ignored at runtime. Do not add
177
237
  compatibility shims for them.
@@ -191,7 +251,7 @@ compatibility shims for them.
191
251
  - [API reference](https://wafertools.github.io/wafermap/api/) — every type, option and return value
192
252
  - [Developer guide](https://wafertools.github.io/wafermap/guide/) — worked walkthroughs
193
253
  - [Troubleshooting](https://wafertools.github.io/wafermap/troubleshooting/)
194
- - [Examples](https://wafertools.github.io/wafermap/examples/) — 20 runnable pages, also
254
+ - [Examples](https://wafertools.github.io/wafermap/examples/) — 22 runnable pages, also
195
255
  [downloadable](https://wafertools.github.io/wafermap/wafermap-examples.zip) to run offline
196
256
 
197
257
  When a rule here and the API reference disagree, the API reference wins — tell the
package/CHANGELOG.md CHANGED
@@ -22,18 +22,474 @@ under `### Breaking`.
22
22
 
23
23
  ---
24
24
 
25
+ ## [0.30.3] — 2026-09-23
26
+
27
+ ### Added
28
+
29
+ - **Sweep x values from test names, an x unit, and a log x axis.** For a
30
+ program that records the swept quantity only in the test text — a
31
+ resistance CDF named `Normalized_LRS= LRS_STATS_12K / …`, one test per
32
+ threshold:
33
+ - `SweepSeriesSpec.xFromName` reads each test's x from its name with a
34
+ placeholder pattern, `"LRS_STATS_{x}"`: `{x}` reads a number with its SI
35
+ prefix (`12K` → 12,000; case-sensitive, `K` accepted as kilo; a letter is a
36
+ prefix only alone or before a unit, so `12Kangaroos` reads 12; in an all-capitals
37
+ name the unit's case is gone, so `5NS` is 5 ns and `2MV` — milli or mega? — is
38
+ reported, not guessed), `*` matches
39
+ anything, literal text matches in any case, anywhere in the name. Not a
40
+ regular expression: a sweeps file is shared, and JavaScript cannot
41
+ interrupt a runaway regex, whereas this matcher's cost is bounded whatever
42
+ the pattern (`stats/sweepXFromName.ts`). Each x stays attached to its own
43
+ test, so a missing test loses one point instead of shifting the rest.
44
+ - `SweepSpec.xUnit` prints the x axis, crossing and widths SI-prefixed
45
+ (`47.3 kΩ`), trailing zeros dropped (`2 kΩ`, not `2.00 kΩ`).
46
+ - `SweepSpec.xScale: 'log'` places x on a log axis; the crossing is
47
+ interpolated along log x and a width is reported as a ratio with both ends
48
+ (`×2.49 (15.9 kΩ → 39.7 kΩ)`), via new `SweepSeparation.ratio`/`from`/`to`.
49
+ `SweepData.xScale` is the axis actually drawn: a non-positive x keeps it
50
+ linear, with a warning.
51
+ - Reported, never guessed, and then not measured: a name the pattern does not
52
+ fit, a series giving both `xValues` and `xFromName`, and — for every x
53
+ source — a series that revisits an x value, which the crossing previously
54
+ mis-paired silently.
55
+
56
+ - **Drilldown: chart a selection or a wafer.** Right-click a population for a
57
+ menu of charts drawn from just those dies, opened in the same modal as an
58
+ expanded Insights card:
59
+ - **Populations:** the dies selected on a map (a single map or any gallery
60
+ card); a whole wafer, from empty map space with nothing selected, from a
61
+ gallery card anywhere outside its map, or from one wafer's bar, box or point
62
+ in *Yield by wafer*, *Test value distribution* or *Wafer-to-wafer trend* in
63
+ Insights (their tooltips say "right-click to chart this wafer"). The Menu
64
+ key, Shift+F10 and a new map toolbar **Chart** button (the selection if
65
+ there is one, else the wafer) reach the same menu without a mouse.
66
+ - **Charts:** value histogram (opening on the test the map shows), process
67
+ capability, and the sweeps defined in `insights.sweeps` — the gallery now
68
+ passes those to each card, and a single map offers them whether or not
69
+ `insights.enabled` is set.
70
+ - **Population stated on every chart**, in the modal title and on the card
71
+ ("12 dies selected on W03", "10 of 12 … (partial and edge-excluded dies left
72
+ out)"). Capability below 30 dies adds that each Ppk is a rough estimate.
73
+ Charts are snapshots: changing the selection afterwards does not change them.
74
+ The wafer is named by the item's `label`, else its `metadata.waferId` — never a
75
+ positional "Wafer 3 (no ID)" — on the map, the gallery and Insights alike.
76
+ - **The library decides what is offered:** a chart that cannot be drawn stays
77
+ listed, greyed, with the reason (no values for its tests; fewer than two
78
+ dies for capability; a lot-stack map, whose dies are per-position aggregates
79
+ rather than measured dies). Right-clicking an unselected die selects it
80
+ first. A map with no parametric tests and no sweeps leaves right-click to
81
+ the browser or host, and the map canvas owns right-click on itself: one it
82
+ declines (the bin legend) never reaches a gallery card or host handler.
83
+ - Documented in api.md §5.12, the user guide §4.4 (with screenshots from
84
+ `capture-definitions.mjs`) and the developer guide.
85
+ - Built on one source/target mechanism (`canvas-adapter/chartPopulation.ts`
86
+ builds the populations, `drilldown.ts` the menu and charts, loaded on first
87
+ use and pinned by `tests/bundle-size.test.mjs`), so a further population or
88
+ chart is one more of either rather than a menu per pairing.
89
+
90
+ - **Derived tests** — `WaferMapInput.derivedTests`: tests computed from other tests
91
+ on the same die, rather than measured. Each entry is a `TestDef` plus an
92
+ `expression`, and from the build onwards it is an ordinary test — it appears in
93
+ `result.testDefs`, in the value plot modes, the colorbar, tooltips,
94
+ `analyzeWaferMap`, Insights and the report, with no other change required.
95
+
96
+ ```ts
97
+ derivedTests: [
98
+ { testNumber: 900001, name: 'Leakage Shift', unit: 'uA',
99
+ expression: 'abs(t[1020] - t[1010])', limitHigh: 5 },
100
+ { testNumber: 900002, name: 'Sweep All Pass', testType: 'F',
101
+ expression: 'all(testPass[1010..1025])' },
102
+ ]
103
+ ```
104
+
105
+ Expressions read `t[n]` (measured value), `testPass[n]` (the tester's recorded
106
+ verdict), `specPass[n]` (spec-limit judgement) and `diePass()` (the die's bin
107
+ verdict) — the same `'test'`/`'spec'` distinction `passFailDisplay` already
108
+ draws, because they are different questions and a single accessor would have
109
+ silently meant "recorded verdict" for parametric data that has none. A range
110
+ (`t[1010..1015]`) yields a set that must be reduced by `mean`, `sum`, `min`,
111
+ `max`, `all`, `any`, `none`, `countTrue`, `countFalse` or `countKnown`; there is
112
+ no vector arithmetic, which is what stops the grammar becoming an array
113
+ language. Operators `+ - * / % ^`, comparisons, `and`/`or`/`not`, and
114
+ `if(cond, a, b)`.
115
+
116
+ Parsed to a typed tree at build time and walked per die — **no `eval`, no
117
+ `Function`, no third-party expression engine, and no way to name a host
118
+ object** — so a chart spec can be shared between teams as plain JSON. This is
119
+ why the root bundle grows ~5 KB gzip.
120
+
121
+ Evaluated on raw probe records **before** lot stacking and retest resolution, so
122
+ every derived value comes from one real touchdown: deriving after a stack would
123
+ subtract one aggregate from another, and deriving after retest collapse could mix
124
+ a value from one touchdown with a verdict from another.
125
+
126
+ A boolean expression is a verdict — declare `testType: 'F'` and the result lands
127
+ in `die.testPass`, never as a 1/0 in `testValues` where it would enter the
128
+ correlation matrix and the Cpk table. An unknown input yields an **absent** value
129
+ (no-data grey), never `0` and never `false`.
130
+
131
+ Everything statically knowable is checked at build and reported as a
132
+ `derived-test-invalid` warning naming the position — a parse or type error, a
133
+ `testNumber` colliding with measured data, a `testType` that disagrees with the
134
+ expression, `t[n]` on a functional test, `specPass[n]` on a test with no limits.
135
+ A rejected derived test is **dropped**, never half-applied.
136
+
137
+ - **Parametric sweeps** — `insights.sweeps` on `renderWaferMap`/`renderWaferGallery`:
138
+ an ordered run of tests read as a response curve rather than as independent
139
+ tests, with the crossing point of the first two series and the horizontal
140
+ separation at named levels measured and stated. `xValues` supplies the real
141
+ swept quantity so the crossing is reported in physical units; without it the
142
+ axis is the ordinal position, because test numbers are identifiers and are not
143
+ guaranteed to be evenly spaced. Each line is the population median with a
144
+ p10–p90 band. A multiple crossing says so; a level neither curve reaches reads
145
+ "not measurable" naming the series, never `0`.
146
+ - **Test ranges in a sweep's `tests`** — an entry may be `"1010..1030"`, the exact
147
+ syntax of a derived-test expression's `t[1010..1030]` and parsed by the same code
148
+ (`parseTestReference`), so the same text always names the same tests. A range
149
+ expands to the declared tests inside it, ascending — a program numbered in steps
150
+ of 2 needs no step syntax. `xValues` is checked against the expanded list: when
151
+ a test is missing from a range, the footer lists what the range matched and the
152
+ card is drawn in test order with the crossing and widths **not measured**
153
+ (`SweepData.notMeasured`), rather than sliding every later x value onto the
154
+ wrong test. The same now applies when only some series carry `xValues`, which
155
+ before put physical and ordinal x positions on one axis and reported an ordinal
156
+ crossing under the physical x label. A range resolves by filtering the declared
157
+ tests rather than counting through every integer, so `0..2000000000` in a shared
158
+ template is not a two-billion-step loop — in expressions too.
159
+ - **Nested derived tests** — a derived test may read another. Compilation
160
+ topologically sorts by what each expression reads, so declaration order does
161
+ not matter; cycles, self-references and dependents of a rejected test are
162
+ dropped with the cause named.
163
+ - New example: **Derived tests and sweeps** (`docs/examples/derived-tests.html`).
164
+ - **Sweeps have their own Insights sub-tab**, shown only when `insights.sweeps`
165
+ defines at least one; `InsightsView` gains `'sweeps'` for `defaultView`, which
166
+ falls back to `'overview'` when there are none. Not an option and not a count
167
+ threshold: a threshold would move a sweep between tabs the day a colleague
168
+ added another, and an option would be a layout flag with one right answer.
169
+ They never belonged in Distributions either — that view is driven by one
170
+ selected test (capability → boxplot → histogram → trend), while a sweep draws
171
+ many tests as one curve and ignores the selection.
172
+ - New example: **Parametric sweeps** (`docs/examples/sweeps.html`) — four
173
+ characterisation sweeps in the Sweeps tab, each a different use of the
174
+ crossing and the width: the temperature-inversion voltage (Fmax vs VDD at
175
+ 25/125 °C, with the iso-frequency VDD gap at two speeds), DIBL from an NMOS
176
+ transfer curve at two drain biases, the retention bake at which the read
177
+ window closes (uneven read times, which is what `xValues` is for), and an
178
+ output driver's maximum drive current (VOH vs VOL under load). The transfer
179
+ curve is swept over **derived** `log10(t[n])` tests — a sweep reads any test,
180
+ which is how a log-scale curve is drawn without a log option.
181
+ - **Derived tests are marked on the sweep card**: `SweepPoint` carries `derived`
182
+ and `expression`, ordinal tick labels keep the `†` in front of a truncated name, the
183
+ tooltip marks each series row and adds each expression, and the footer shows
184
+ the key.
185
+ - **Sweep card text.** The crossing line lower-cased the y label ("Cell Vt"
186
+ became "cell vt", "Fmax" became "fmax") — the width line had already been
187
+ fixed for the same reason ("dBm" → "dbm"); widths printed in engineering
188
+ notation (`32.7E-3`); and an x label carrying its own unit doubled the
189
+ brackets (`(VDD (V))`). X values — ticks, crossing and widths — now share one
190
+ plain-decimal rule, and a width reads "0.0327 on the VDD (V) axis".
191
+ - The derived-tests example opened on a Switching Window measured at the step
192
+ where its two curves cross, so the gap sat around zero against `limitLow: 0`
193
+ and nearly every die carried a ▽. It is now measured at a fixed read step
194
+ with a 0.33 V margin, so only an edge band fails.
195
+ - **A derived test is now identifiable from `result.testDefs` alone.** `derived`
196
+ was declared only on the `DerivedTestDef` *input* type, while `result.testDefs`
197
+ is a homogeneous `TestDef[]` — so every display surface needed a cast to ask
198
+ whether a value was measured or derived, and `expression`/`constants` were
199
+ stripped off the admitted def entirely, leaving nothing to say where a number
200
+ came from. All three now travel on `TestDef` as `readonly` fields set by
201
+ `buildWaferMap`. This is the prerequisite for marking derived tests in the UI:
202
+ a display that cannot ask the question cannot mark it, and an engineer reading a
203
+ Cpk table should never be left to assume a number came off the tester. The
204
+ alternative — hosts holding their own `derivedTests` input beside the result and
205
+ joining by test number — is a second source of truth that can disagree with the
206
+ def actually admitted, which carries the resolved `testType`.
207
+ - **`testLabel` / `isDerivedTest` / `derivedTestSource` / `hasDerivedTests`**
208
+ (`renderer/testLabel.ts`, internal) — one definition of how a test is named and
209
+ how a derived one is distinguished. The "name, or *Test N* when it has none"
210
+ fallback existed in six places (`stats/sweep.ts`, `capability.ts`, `testPassRate.ts`,
211
+ `analyzeWaferMap.ts`, `charts/chartShell.ts`), all now routed through it. Around
212
+ sixteen surfaces display a test name; a marker added per surface would have been
213
+ that rule twenty-odd times over, which is how two of them end up disagreeing.
214
+ - **Derived tests are marked in the process capability panel.** A `†` precedes
215
+ the test name, the legend gains the key "† Derived, not measured", and the
216
+ tooltip says the same in words and shows the expression the value was
217
+ computed from. A dagger, not `ƒ`: in this domain `f` reads as femto,
218
+ and a `ƒ` beside `fA`/`fF` units is a real misread.
219
+ `TestCapability`/`CapabilityDatum` gain `derived` and `expression`.
220
+ - **…and everywhere else a test is named, always in front of the name.** In a
221
+ list the marks then form a column down the left edge, so a derived test is found
222
+ at a glance, and truncating a long name can never cut the mark off; a list
223
+ holding a derived test pads its measured names by the mark's width so names stay
224
+ aligned, and a list without one is unchanged. The map title and colorbar
225
+ (`† Leak Shift (nA)`)
226
+ with the key on its own line beneath, drawn via a new optional
227
+ `MapTitleParts.note`; the map tooltip, which adds the key and the expression;
228
+ finding labels and sentences, marked at the source so no reader of a finding
229
+ can show it unmarked, with `StatsFinding.variable.derived`/`expression` as the
230
+ structured form; `stats.functionalYield[].label` (plus `derived`/`expression`);
231
+ the Summary panel's parametric and functional tables and its findings list,
232
+ each with a key line naming every expression; and both HTML reports, whose key
233
+ lists the expressions because a printed report has no hover. CSV exports carry
234
+ plain names plus a trailing **Derived from** column, only when a row is
235
+ derived — a CSV has nowhere to put a key. The key is a single short string,
236
+ *Derived, not measured*, so it fits the corner of a map and cannot drift
237
+ between surfaces. Every surface goes through `renderer/testLabel.ts`, which
238
+ also replaced two more hand-written *Test N* fallbacks in `buildView.ts` and
239
+ one in the gallery's stacked cards, whose synthetic def now keeps the flag.
240
+ The finding tooltip now has one rule, `formatFindingTooltip`, which the
241
+ Summary panel shares with both reports instead of using `summary` directly.
242
+ The places a test is *chosen* are marked too: the plot-mode test menu (a slot
243
+ in front of each name, reserved only when a derived test is listed, and a key
244
+ line at the foot), the Insights test pickers (`testOptionLabels`), the
245
+ correlation matrix's axis labels and tooltip (key on its hint line; its CSV
246
+ gains per-side *derived from* columns) and the die list's column headers (key
247
+ line above the table; the CSV header states the expression). The last two
248
+ hand-written *Test N* fallbacks, in the plot-mode menu and the die list, now go
249
+ through `testLabel`.
250
+ - **`DERIVED_MARK` and `DERIVED_KEY` are exported** (root and `/renderer`), so a
251
+ host listing tests in its own UI marks derived tests exactly as the library does
252
+ instead of keeping its own copy of the glyph and the words. tsmap's test selector
253
+ is the first use.
254
+ - Getting there turned up two places that dropped the flag. **`mergeTestDefs`**,
255
+ which builds a gallery's single test list, rebuilt each def field by field and
256
+ never named `derived`, so every cross-wafer panel would have shown a derived
257
+ test unmarked while the single-wafer view marked it. It now carries `derived`
258
+ and `expression`, marking a test derived when **any** wafer says so: marking
259
+ a measured test is an oddity someone queries, while leaving a derived one
260
+ unmarked goes unnoticed. The Summary panel's per-test table narrowed defs the
261
+ same way and now keeps both fields. It also held a seventh copy of the
262
+ *Test N* fallback, now routed through `testLabel`.
263
+
264
+ ### Changed
265
+
266
+ - **Docs and examples brought up to date with this release.** The developer
267
+ guide's sweep section no longer says `tests` has no range syntax or that a
268
+ sweep has no log option; it covers ranges, `xFromName`, `xUnit`, `xScale` and
269
+ drilldown. The sweeps example adds a fifth sweep (an RRAM resistance
270
+ distribution read from test names on a log axis) and gives the output-driver
271
+ sweep an x unit; the derived-tests example's sweep uses ranges and a real swept
272
+ quantity (pulse amplitude in volts) instead of ordinal steps, and places the †
273
+ in front of the name as everywhere else. The README, `AGENTS.md` and
274
+ `llms.txt` now mention derived tests, sweeps and drilldown.
275
+ Those two examples' code cards, a four-across grid that cut every code
276
+ block off at about half its width in 12 px type, now show one example per
277
+ row — the code whole, in monospace at 13.5 px, beside notes at 14.5 px,
278
+ stacked on narrow screens — from one shared style in `demo.css`. `--mono`,
279
+ which every example's code referenced and nothing defined, is now set.
280
+ The derived-tests example is now in two labelled parts — derived tests are
281
+ *data* (`buildWaferMap`'s `derivedTests`; tsmap's test-definitions file), a
282
+ sweep is a *chart* (the renderer's `insights.sweeps`; tsmap's separate sweeps
283
+ file) — because a run of look-alike snippets read as one list. Its snippets
284
+ show the whole call each belongs to, it says the sweep is in Insights →
285
+ Sweeps, with a *Show the sweep* button that presses the gallery's own Insights
286
+ button, and it links to the sweeps example. The sweeps example opens with
287
+ where its entries go.
288
+
289
+ - **`scripts/check-bundle-size.mjs` attributes lazy chunks by what imports
290
+ them, not by file name.** Once drilldown and Insights shared the chart panels,
291
+ esbuild split the shared code into an anonymous `chunk-*.js`, which a file-name
292
+ test counted as core. That would have added ~5 KB of lazy code to the "always
293
+ downloaded" figure. The guide, Insights and drilldown now count as their own
294
+ static closure minus what the entry already loads. The Insights figure now
295
+ includes the chart code it shares with drilldown, because opening Insights
296
+ downloads it.
297
+ - **Axis ticks sit on round values, and are labelled to their spacing.** Tick
298
+ labels used to take their decimals from each value's own size, never from the
299
+ gap between ticks, so a 1–10 axis read "2.00 4.00 6.00 8.00" and a bandgap's
300
+ 1.195–1.205 V read "1.20" five times — a sloped curve labelled as a flat line.
301
+ And only the colorbar placed ticks on round numbers: the box plot, scatter,
302
+ trend and sweep divided the data range into equal fractions (2.54 / 2.04 /
303
+ 1.53 GHz), the histogram labelled its bucket edges, and its count axis carried
304
+ an inline copy of the rounding rule with different breakpoints. Every numeric
305
+ axis now goes through `renderer/axisTicks.ts` — one 1-2-5 rounding rule
306
+ (`niceStep`), round ticks inside the data range, and labels with exactly the
307
+ decimals the step needs (`stepDecimals`): "2 4 6 8", "1.196 1.198 1.200".
308
+ **Density comes from the space, not a tick count:** `fitTicks` takes the
309
+ finest round step whose labels fit — measured label widths plus an 8 px gap
310
+ on a horizontal axis, a line spacing on a vertical one. A fixed count, tried
311
+ first, rounded a 200 px Idsat box plot (0.863–1.56 mA) to 1.0 / 1.2 / 1.4 —
312
+ fewer ticks than the five it replaced; it now reads 0.9 / 1.0 / … / 1.5. The colorbar's exact min and max are data values rather than grid
313
+ values, so they get one decimal more, dropped when it is a zero — a 9.87
314
+ maximum no longer rounds to "10". Log-scale and integer-valued colorbars, die
315
+ labels and limit labels keep size-based formatting; they are not tick grids.
316
+ Every chart's tick labels change, so doc screenshots need recapturing.
317
+ - **The wafer and lot reports' findings table now uses plain bin terms**
318
+ ("hard bin 2", not "HBin 2"). Both HTML reports render one findings table,
319
+ `findingsTableHtml`; each report module had kept its own, and only the
320
+ findings-only report translated the bin terms.
321
+
322
+ ### Deprecated
323
+
324
+ - **`renderFindingsReportHtml`** — removed in 0.31.0. Use
325
+ `renderWaferReportHtml(result, summary)` or `renderLotReportHtml(results)`:
326
+ their Findings section is the same table, alongside the population and yield
327
+ it was found in. It was kept in 0.30.1 on the grounds that the Summary panel
328
+ used it, which had not been true since 0.20.0; nothing in wmap or tsmap calls
329
+ it. It ships deprecated in this release, so it goes in 0.31.0 with the 0.30.0
330
+ deprecations. `deprecated()` now takes a removal version per name, for a name
331
+ deprecated too late to go with the rest.
332
+
333
+ ### Fixed
334
+
335
+ - **A menu row's hint is no longer drawn under its own menu.** Menus sit in a
336
+ dedicated layer (`menuLayerFor`) that outranks everything else in its root,
337
+ and the shared tooltip was placed beside that layer — so the reason on a
338
+ greyed-out row (the drilldown menu's, the Overlays menu's) appeared behind the
339
+ menu it explained. `positionTooltip` now places the tooltip inside the
340
+ anchor's menu layer at `Z_ABOVE2`, above every menu there, still resolved per
341
+ root (a host `<dialog>`, a wmap modal).
342
+
343
+ - **The map's hover tooltip no longer jumps away from the die.** The shared
344
+ tooltip steps clear of its anchor when the anchor is under 40% of the window
345
+ tall — right for a toolbar button, which its tip must not cover, and wrong
346
+ for the map canvas, where the pointer is on the die the tip describes. Every
347
+ map that small (every gallery card, the smaller maps on a page such as the
348
+ geometry example, any map in a tall window) had its tip thrown below or above
349
+ the whole canvas, measured at up to 266 px from the pointer. The map now
350
+ passes `followPointer` to `positionTooltip` and the tip stays beside the
351
+ pointer; control tips keep stepping around their buttons.
352
+
353
+ - **An expanded chart grows into its modal, in the direction that suits it.**
354
+ Several cards kept their grid-card size inside the expand modal: the trend
355
+ and sweep plots stayed a few hundred pixels tall, ring and quadrant yield
356
+ kept a fixed-size circle, the correlation matrix kept small cells, process
357
+ capability stopped at ~160 px per test, and row charts (yield by wafer,
358
+ pareto, pass rate, value distribution) left most of a 700 px box empty. Each
359
+ card now declares how it grows (`setChartGrow`): **plots** fill width and
360
+ height in a wider box; **row charts** keep bars full-width, show every row the
361
+ box has room for, and the box opens sized to its rows (within the viewport);
362
+ **square** charts grow to the shorter side in a square box. Capability's
363
+ columns spread across the width while each box keeps its grid maximum width.
364
+ Growth past the grid size happens only while expanded, so the Insights grid
365
+ is unchanged; maximise/restore and drag-resize shrink back correctly.
366
+ - **Chart cards measure their chrome, not their stretch.**
367
+ `growCardToFitContent` took the card's overhead as `card height − body
368
+ height`, which counts any stretch as overhead and grows the card on every
369
+ redraw — why several panels opted out of stretching (`alignSelf: 'start'`),
370
+ which is what kept them small in the modal. It now sums the card's other
371
+ children, so a stretched card is harmless. Trend and sweep no longer ask for
372
+ the height they were given as their minimum, which would have stopped a
373
+ shrinking modal.
374
+ - **Chart tooltips wrap inside their background.** They were `nowrap` under a
375
+ 280 px cap, so a longer line — a sweep row with its p10–p90 and n, a derived
376
+ test's expression — ran out past the dark box. They now wrap within 320 px,
377
+ breaking an unbroken expression if they must. The sweep tooltip puts each
378
+ series' spread and n on a line of their own under its median.
379
+
380
+ - **A functional-test finding merged across adjacent regions reported no
381
+ difference, and hid the real one.** Since 0.20.3, when a functional pass-rate
382
+ signal spanned adjacent rings, sectors or quadrants, the merge pass that joins
383
+ them into one finding (`Rings 1–2`) had no functional branch and fell through
384
+ to the bin one. It counted dies whose hard bin equalled `variable.bin` — which
385
+ a functional finding does not have — so both sides counted 0, and the merged
386
+ finding read *"Rings 1–2 has HBin undefined occurrence 0.0 percentage points
387
+ lower than the rest of the map"*, with a meaningless p-value. It also
388
+ **replaced** the correct per-region findings it merged, so a real functional
389
+ signal spanning adjacent regions was shown as no difference at all. The merge
390
+ now recomputes the pass rate over the merged region exactly as the per-region
391
+ builder does (verdicts through `getTestPassStatus`, dies without a verdict
392
+ excluded), and the sentence names the test. Found while testing the
393
+ derived-test marker, whose test wafer happened to have exactly this shape.
394
+ - **Merged-region findings used a singular verb:** *"Rings 1–2 has HBin 3
395
+ occurrence…"*, *"Quadrants NE & SE has…"*. A merged region is plural; bin and
396
+ functional pass-rate sentences now say "have". The Summary panel, which strips
397
+ the region from a row shown under its own group header, accepts either verb.
398
+ - **A `NaN` test value was classified as in-spec.** The spec-limit rule existed in
399
+ two places — `classifySpec` (die colouring, ▽/△ markers, the legend's spec tally)
400
+ and a private `specJudgement` in `stats/testPassRate.ts` (spec pass-rate tables) —
401
+ and they disagreed on two cases. `classifySpec` compared with `<` and `>`, which
402
+ are both false for `NaN`, so a non-finite measurement fell through to `'pass'`:
403
+ the exact "never drawn as plain in-spec" failure its own doc comment described.
404
+ It also reported `'pass'` for a test declaring no limits at all, inflating a spec
405
+ pass rate to 100% for every unlimited test. There is now one implementation
406
+ (`renderer/spec.ts`), strict on both, used by the renderer, stats and the
407
+ `specPass[n]` accessor.
408
+
409
+ ## [0.30.2] — 2026-09-20
410
+
411
+ ### Added
412
+
413
+ - **A gallery now reports its own progress, so a host's indicator can be honest about a long
414
+ load.** `GalleryOptions` gains two callbacks (API §6.2):
415
+ - `onItemResolved(resolved, total)` — the **advance** signal, fired as each card is built.
416
+ `total` is the count you passed, so it is right from the first call and a progress bar can be
417
+ sized before anything arrives.
418
+ - `onItemsResolved()` — the **settled** signal, fired once the gallery has finished: every
419
+ factory resolved *and* the lot-wide legend, shared colours and Summary panel up to date. On a
420
+ large lot the panel keeps filling in for seconds after the last card, so this is deliberately
421
+ later than "the cards are in" — a host clearing its indicator when the cards land leaves the
422
+ rest of the wait unexplained.
423
+ - Both fire whichever form the items took, so no host branches on the path the gallery chose: a
424
+ fully pre-built mount is one `onItemResolved` call with `resolved === total`, and
425
+ `onItemsResolved` always fires asynchronously, after `renderWaferGallery` has returned. Both
426
+ fire again on a rebuild (`setItems`, or switching into a stacked mode).
427
+ - **`CorrelationMatrix.sample`** — present only when the matrix was computed from a sample of the
428
+ dies, with `of` (the dies that carried test values) and `used` (how many were read).
429
+ The Insights correlation panel labels a sampled matrix from it; a host reading the matrix
430
+ directly must do the same.
431
+
432
+ ### Changed
433
+
434
+ - **Correlation is computed from at most 25,000 dies.** It is the only analysis here that is
435
+ quadratic in tests and linear in dies — a 400,000-die, 50-test lot is 490 million pair updates,
436
+ which in a browser presents as a panel that never renders. Above that budget the dies are
437
+ sampled by an even stride across the whole population, never a prefix (dies arrive grouped by
438
+ wafer, so the first 25,000 of a lot are its first few wafers and would describe those rather
439
+ than the lot). At 25,000 dies the standard error of `r` is under 0.007, so the coefficients are
440
+ unchanged at the precision they are displayed to. **A sampled matrix always says so** — in the
441
+ Insights panel and in `CorrelationMatrix.sample`.
442
+ - **The lot and wafer Summary panels are built across tasks instead of in one block, and fill in
443
+ section by section.** Pooling every die of every wafer and deriving the bin, region and
444
+ per-test sections from it is the longest single piece of work this library does: 15.3 s on a
445
+ 50-wafer lot of 4,000 dies × 100 tests, which is where a browser starts offering to kill the
446
+ page. It is now around 335 steps of a few hundred ms at most. An ordinary lot still renders in
447
+ one synchronous pass and looks exactly as it did. A panel render in flight is abandoned when
448
+ the lot, filter or highlight changes, and on `destroy()`.
449
+ - **The same panel work is also about 2.3× cheaper, and a lot pays for it once.** The per-test
450
+ pass over a lot's pooled dies is memoised on the population it describes, so the lot Summary
451
+ panel, the Insights Overview and the Insights capability chart share one computation instead of
452
+ running three; the sort inside it uses a `Float64Array` (4× on the whole pooled pass); and the
453
+ Test Values rows are built in one pass instead of three.
454
+ - **Switching Insights tabs no longer rebuilds the panel you are returning to.** A built section
455
+ is kept per view, so going back to Distributions is instant rather than re-running capability,
456
+ boxplot, histogram and trend (about 5 s at 400,000 dies). Only a pure tab switch reuses it —
457
+ new data, Group by, scope or an axis preference still rebuilds everything.
458
+ - **A lot-wide option is pushed to the cards only when its value actually changed.** `binColors`,
459
+ `valueRange` and `metadataValueOrder` are re-derived from the whole population as each wafer
460
+ resolves, and each derivation allocates a fresh object, so every card was being rebuilt and
461
+ redrawn for an identical value. On a progressive load that is quadratic: on 50 wafers × 8,000
462
+ dies × 50 tests in Chrome the bin-colour map alone was 1,275 pushes and 24 s of a 64 s load,
463
+ and is now one push.
464
+ - **The gallery's lot Summary panel now settles once, when the last wafer resolves, instead of
465
+ re-rendering after every one.** A progressive load was re-pooling and redrawing the whole
466
+ panel on each newly resolved wafer, which is quadratic in wafer count and was the single
467
+ largest cost on this path: 175 s of a 182 s, 50-card progressive load. The panel still renders
468
+ once at mount and again whenever a user opens it. Combined with the change above, 50 cards of
469
+ 8,000 dies now load in **7.7 s of non-blocking work with no task over 440 ms**, down from
470
+ 182 s.
471
+
472
+ ### Fixed
473
+
474
+ - **A large wafer or lot could fail outright with "Maximum call stack size exceeded".**
475
+ `Math.min(...)`/`Math.max(...)` spread a whole die array into a call, which throws once the
476
+ array passes roughly 100,000 entries — reachable on a single 400,000-die wafer in aggregated
477
+ values, inferred wafer geometry, reticle bounds and several chart scales. Every such call now
478
+ uses the `minOf`/`maxOf` helpers, and `scripts/check-spread-limits.mjs` (wired into
479
+ `npm run check`) fails the build if one comes back.
480
+ - **A non-finite test value could make a whole test's statistics `NaN`.** `computePerTestStats`
481
+ sorted and summarised whatever `testValues` held, with no `Number.isFinite` screen, so one
482
+ `NaN` or `Infinity` from a parser silently turned that test's mean, median, sigma and Cpk into
483
+ `NaN` rather than being dropped as no data.
484
+
25
485
  ## [0.30.1] — 2026-09-16
26
486
 
27
487
  ### Security
28
488
 
29
- - **Names from a data file could run script in a tooltip.** The die tooltip and the Insights
30
- chart tooltips (correlation, capability, boxplot, trend, histogram, bar and grouped-bar
31
- charts), plus the legend-row and toolbar tooltips, built HTML from test names, units, bin
32
- names, wafer labels and metadata values without escaping them. A file with a test named
33
- `<img src=x onerror=…>` ran that script when a user hovered, in a Tauri, Electron or
34
- WebView2 host inside the app's own webview. Every such value is now escaped. The escape
35
- helper, formerly private to the reports, is the one copy for the library (`core/utils.ts`),
36
- and `tests/htmlEscaping.test.mjs` fails if a tooltip puts a raw label into HTML again.
489
+ - **Fixes a security issue in how names from a data file are displayed.** Earlier versions are
490
+ affected; update to 0.30.1, particularly if your app opens files from sources you don't
491
+ control. Names now always display as plain text, so a name that contains markup shows it
492
+ literally.
37
493
 
38
494
  ### Changed
39
495