Using wafermap with an AI coding agent¶
For: developers whose day-to-day coding runs through Claude Code, Codex, Copilot, Cursor or similar. See also: Developer Guide · API Reference
Wafer maps drive yield calls, lot dispositions and process changes. The dangerous
failure is not code that crashes — it is a map that looks entirely reasonable and is
wrong. Several of this library's inputs invite a confident wrong guess:
die.hbin ?? 0 reads as ordinary defensive coding but turns no-data dies into bin 0
and moves the yield number; activeTest reads like an array index but is a test
number.
The rules below are the ones worth loading before an agent writes wafer map code.
Give these rules to your agent¶
Copy this block into your project's agent config — CLAUDE.md, AGENTS.md,
.cursorrules, .github/copilot-instructions.md, whichever your tool reads.
## wafermap — usage rules
`@wafertools/wafermap` renders wafer maps from semiconductor die test data.
Wrong-but-plausible output drives real yield and lot decisions, so prefer failing
loudly over guessing.
### Entry points
- `@wafertools/wafermap` — `buildWaferMap()`, `registerBinColorScheme()` / `registerValueColorScheme()`. Pure, no DOM, server-safe.
- `@wafertools/wafermap/render` — `renderWaferMap()`, `renderWaferGallery()`. Needs the DOM.
- `@wafertools/wafermap/stats` — `analyzeWaferMap()`, `analyzeWaferLot()`. Pure analysis.
- `@wafertools/wafermap/worker` — `createWafermapWorker()` for off-main-thread builds.
Default path: `buildWaferMap()` once when data loads, then `renderWaferMap()` for a
single wafer or `renderWaferGallery()` for several.
**There is no low-level drawing pipeline, chart-data builder or region builder to reach
for.** The library draws through the renderers, and every figure it shows (yield, bin
counts, region yield, per-test statistics, capability) comes back from
`analyzeWaferMap()` / `analyzeWaferLot()`.
If the only way to do something is to rebuild one of those pieces, that is a library gap
worth reporting, not a pattern to build on.
### Traps that produce silently wrong maps
- **Never `die.hbin ?? 0` or `die.sbin ?? 0`.** A missing bin is not bin 0 — it is
no-data, and must render grey. Defaulting to 0 invents a bin and changes the
yield number. Leave the field absent.
- **`x` and `y` are prober step positions (integers), not millimetres.** Pass them
through unchanged; `dieConfig.width`/`height` convert to physical units. Do not
pre-multiply. The geometry inputs are `waferConfig` (type `WaferConfig`) and
`dieConfig` (type `DieConfig`) — both optional, both inferred when omitted.
- **Bins and test values must be numbers, and verdicts booleans.** Every parser —
CSV, JSON, a spreadsheet export — hands you `"1"`, and `"1"` is not pass bin 1.
`buildWaferMap` never converts them: each is treated as missing and reported as
`input-values-not-numbers` (severity `'error'`), so those dies have no bin or no value
and a build that "works" can be mostly empty — read the warnings. Convert with
`Number()` at parse time.
- **`passBins` and `ringCount` are set once, on `buildWaferMap`, and travel on the
result.** Neither is an option on `analyzeWaferMap`, `analyzeWaferLot`,
`renderWaferMap` or `renderWaferGallery` — passing one there is a type error in
TypeScript, and in JavaScript it is ignored with an `analysis-option-corrected`
warning while the real value is read from the map. `passBins` decides both the yield
number and the wording of its label, so set it from the actual test program: do not
assume `[1]`, and never re-default to `[1]` downstream — read `result.passBins`.
- **`testValues` is keyed by test number**, e.g. `{ 1050: 0.42 }` — not a positional
array. `activeTest` likewise takes a *test number* (`1050`), not an index.
- **Functional tests (`testType: 'F'`) have no measured value.** Read their verdicts
only via `getTestPassStatus(die, testNumber, def)`; never read `die.testPass`
directly and never interpret a 0/1 in `testValues`. A missing verdict is no-data,
never a fail.
- **Show users `die.x` / `die.y` only.** Never surface internal display or
transformed coordinates in tooltips, labels or reports, whatever the rotation or
flip state.
- **A die with test results is always fully on the wafer.** A prober only steps to
sites that fit. Never recompute a `partial` flag by testing die corners against
the wafer circle — that manufactures fake partial dies which are then greyed out
and dropped from yield. A die outside the wafer means the *geometry* is wrong.
- **Check `result.warnings` and `summary.stats.warnings`.** Both carry
`WaferWarning` — `{ code, message, severity }`. Branch on `code`, never on the
prose. **The two differ in shape and the difference throws:**
`result.warnings` is required and always an array (`[]` when clean), but
`summary.stats.warnings` is *optional* and is `undefined` when there is nothing
to report. Write `summary.stats.warnings?.length` — a bare
`summary.stats.warnings.length` is a TypeError on every clean wafer, which is
most of them, so it will pass your testing and fail in production.
Geometry advisories are severity `'error'`: they mean dies may be drawn in
the wrong place. The renderers surface these themselves in a toolbar indicator, so
do NOT hand-roll a second display — pass
`warnings: { display: false, onWarning }` if the app has its own notification UI.
- **Give the container a resolved height.** `renderWaferMap` fills its container.
A bare block-flow `<div>` is fine — it grows to the canvas. The real failure is a
flex/grid child whose ancestors never resolve a height: it stays 0-tall and the
map is invisible. The library detects exactly that case after layout settles and
`console.warn`s with the fix, so read the console before debugging further. Either
give the container a real CSS height, or pass `{ height: 600 }` in the render
options and the library will size it for you.
### API facts that are easy to guess wrong
- `PlotMode` values are camelCase: `'hardBin'`, `'softBin'`, `'value'`, `'metadata'`,
`'stackedValues'`, `'stackedBins'`, `'stackedSoftBins'`. Never snake_case.
- `retestCount` is the total probe count — `2` means probed twice. Do not add 1.
- `retestPolicy: 'best'`/`'worst'` is pass/fail-aware via `passBins`; bin number only
breaks ties within a category.
- Hard bins (`hbin`) and soft bins (`sbin`) are independent number spaces. Never merge them.
- **Do not colour bins yourself.** The maps give each bin one colour from its number
and its pass/fail verdict (pass bins, per `passBins`, take green pass colours), so a
bin is the same colour in every lot. To choose colours, set `BinDef.color` or register
a palette with `registerBinColorScheme`. Bin maps and value maps have separate
schemes: `binColorScheme` and `valueColorScheme`. Need the colours for a surface of
your own (a table swatch, an export)? Read `controller.getBinColors()` for a live map,
or `binColorsForMaps(results)` — never a palette lookup of your own.
- Build once, render many: `buildWaferMap()` handles data + geometry; re-render UI
changes through the controller's `setOptions()`, not by rebuilding. New data for a
map that is already mounted goes through `setResult()` — do not `destroy()` and
remount.
- **The analysis surfaces are already built — do not reimplement them.** Pass
`statsSummary` to `renderWaferMap` and it mounts the Summary panel. The
chart suite (Insights) is on by default (`insights: { enabled: false }` removes it): yield, bin pareto,
boxplot, histogram, correlation, scatter, capability, and a Plot tab where the reader's own plots
and sweeps (`chart: 'sweep'`, in `insights.plots`) are cards. `renderWaferGallery` takes the same option across a whole
lot. Supply or replace the analysis later with `setStatsSummary()`. There is no
chart-data API to hand-build them from. Charting a selection or one wafer (right-click → histogram, capability,
saved plots and sweeps) is built in too, with no wiring.
- **A value computed from other tests is a derived test, not a host-side column.** Pass
`derivedTests` (a `TestDef` plus an `expression`, e.g. `'abs(t[1020] - t[1010])'`) to
`buildWaferMap`; it then behaves as a measured test everywhere, marked `†` as not
measured. Computing it in the host and injecting it into `testValues` loses that
mark, the missing-input rule (absent, never 0) and the collision check. A boolean
expression must be declared `testType: 'F'`.
- **A sweep's x values are data, not guesses.** In a sweep plot's `sweep.series`, give `xValues`, or `xFromName` (a
`{x}` placeholder pattern, not a regex) when the swept value is only in the test
name. Never derive x from test numbers: they are identifiers, not a scale.
- **Click-to-highlight is wired, not hand-rolled.** `onSelect` reports what the user
picked; `setSelection(dies)` / `clearSelection()` drive it from your own UI — for
example from a finding, whose `dieKeys` match `getDieKey(die)` exactly.
- A die layout with no test data — a map of the reticle or the grid alone — is
`buildWaferMap({ layout: true, waferConfig, dieConfig })`, not a synthesized results
array.
- `valueColorScheme` and `reverseValueScheme` travel as a pair. Every built-in gradient
but `'traffic'` and `'jet'` reads low = dark, high = light; if you draw your own
colorbar or swatch, resolve the colour through `resolveValueColorFn(name, reversed)`
so it cannot disagree with the dies.
- What a map was built with is on the result: `result.plotMode`, `result.metadata`,
`result.isLotStack`, `result.hbinDefs`, `result.sbinDefs`, `result.testDefs`. The renderers
read them; pass nothing again.
- Die keys come from `getDieKey(die)`. A hand-rolled `` `${x},${y}` `` breaks
click-to-highlight silently, because findings carry `dieKeys` in that exact format.
- `stats.warnings` is `WaferWarning[]` (it was `string[]` before 0.22.0). Read
`w.message` to display, branch on `w.code`. Code that calls a string method on an
entry — `warnings[0].includes('…')` — is the old shape and will throw.
- **`summary.findings` is the complete list and contains restatements of the same
fact.** Building a list for a human to read? Pass it through `visibleFindings()`,
which drops what other findings absorb — do not re-implement that filter.
Skip that and one edge failure is reported up to three times per region — a hard
bin row, its soft-bin twin, and the yield row that restates the pass bin. Do NOT
use `relatedIds` for this; it is a different relationship and some ids it names
no longer exist in `findings`.
### Scale: do not turn this into a data explorer
The most common performance mistake is treating the library as somewhere to dump
an entire test program and browse it. It is a *renderer* — it analyses everything
it is handed, because it has no way to know which tests anyone will look at.
- **Pass only the tests you will actually chart or analyse in `testDefs`.** A real
parametric program can carry hundreds of tests while the user ever looks at a
handful. Cost scales with test count, and test correlation scales
*quadratically*: on a ~1,000-die wafer, `enableTestValueAnalysis` costs ~25 ms at
6 tests and ~91 ms at 60, while the correlation matrix goes from 15 pairs to 1,770.
- **Do not "load everything, filter in the UI".** Filtering after the fact means you
already paid for the parse, the transfer and the analysis.
- **The right shape is pre-scan → select → load.** Scan the source for which test
numbers exist (and their names/limits if available), let the user choose, then
parse and build only the chosen tests. A scan that reads test identity without
reading every value is dramatically cheaper than a full load, and it is what makes
a large file feel instant.
- **Above 250 discovered tests, `analyzeWaferMap` gives up on test-value analysis
entirely** — it returns no test findings rather than a trimmed set, and records a
`WaferWarning` with code `'test-count-capped'` in `stats.warnings`. Silence is not
success here: an empty findings list is indistinguishable from "nothing to report"
unless you check. Pass `testNumbers: [...]` to scope the analysis explicitly.
- **Reach for options deliberately.** Plain `analyzeWaferMap()` is cheap;
`computePerTestStats` is modest; `enableTestValueAnalysis` is the expensive one
(roughly 10× the base analysis on a large wafer) and exists to find spatial
patterns automatically — do not enable it by default just because it sounds good.
- **A Web Worker buys responsiveness, not speed.** `createWafermapWorker` moves a
result's test values to the page without copying them, but still copies the dies and the
input, so total time goes *up*. Use it when a build would otherwise visibly freeze the
page, not for small datasets.
- **Capability, pass rates, region yield and the spatial-pattern label come back from
the analysis** — `stats.capability` (with `computePerTestStats`), `stats.testSpecYield`,
`stats.testFlagYield`, `stats.functionalYield`, `stats.regionYield` and
`stats.spatialPattern`, on wafer and lot summaries alike. Do not compute Cp/Cpk or ring yield yourself: the pooled
within-wafer stddev and per-wafer pass bins are easy to get subtly wrong.
- **Reports from code: `renderWaferReportHtml(result, summary)` and
`renderLotReportHtml(results)`** — they take the built maps, so pass bins and ring
count cannot be wrong. They run in Node.
- **In a gallery, pass `perWaferSummaries` to `analyzeWaferLot`** so it reuses the
per-wafer analysis you already ran instead of redoing it.
### Removed — do not emit these
| Never | Use instead |
| --- | --- |
| `DieResult.values` / `Die.values` | `testValues` (keyed by test number) |
| `TestDef.index` | `TestDef.testNumber` (required) |
| `ViewOptions.colorBySpec` | `passFailDisplay: 'spec'` |
| `View.colorBySpec` | `view.passFailDisplay` |
| `WaferMapResult.inference.warnings` | `WaferMapResult.warnings` (structured, with a `code`) |
| `ViewOptions.testIndex` | `activeTest` |
| `mountWaferCanvas` | `renderWaferMap` |
| `HARD_BIN_COLORS` / `SOFT_BIN_COLORS` | `BinDef.color`, or `registerBinColorScheme` — there is no exported palette constant |
| `GalleryItem` | `WaferMapDisplayItem` |
| `MountOptions` | `RenderOptions` |
| `WaferCanvasController` | `WaferMapController` |
| `CanvasHitTarget` | `HitTarget` |
| `buildScene` / `BuildSceneOptions` / `SceneOptions` | `renderWaferMap` |
| `WaferFlat`, field `flat` | `WaferNotch`, field `notch` |
| `isInsideWaferWithFlat` | nothing — `buildWaferMap` resolves the geometry |
| `DieSample` / `WaferMapPoint` | `DieResult` |
| `colorScheme` / `WaferViewOptions.colorScheme` | `binColorScheme` (bin maps) and `valueColorScheme` (value and stacked maps) |
| `registerColorScheme` / `getColorScheme` / `listColorSchemes` | `registerBinColorScheme` / `registerValueColorScheme` and their `get` / `list` pairs |
| `hardBinColor` / `softBinColor` / `hardBinGreyscale` | `BinDef.color`, or `registerBinColorScheme` |
| `plotMode: 'specLimit'` | `passFailDisplay: 'spec'` |
| standalone `getDieAtPoint` | `onHover` / `onClick` on `renderWaferMap` |
| `RenderOptions.tooltipTestLimit` | (was a no-op; nothing replaces it) |
| `enableYieldAnalysis` / `enableHardBinAnalysis` / `enableSoftBinAnalysis` / `enableReticlePositionAnalysis` / `enableTestSiteAnalysis` / `enableClusterAnalysis` / `enableAngularAnalysis` / `enablePatternClassification` | nothing — every analysis runs; scope cost with `testNumbers` instead |
| `WaferMapController.setIdentityVisible` | `showIdentity` in `RenderOptions` |
Passing a removed option is a type error, and is ignored at runtime. Do not add
compatibility shims for them.
### Terminology in user-facing text
- Never write "channel" — use "index", "slot" or "test". "Channel" is tester
hardware jargon that confuses the engineers reading these maps.
- Label what is actually shown. Name the real pass bins rather than assuming bin 1;
say "Hard Bin Breakdown" not "Bin Breakdown"; identify an aggregated or filtered
population (`N=50`, "6 wafers · mean") so nobody mistakes a lot stack for one wafer.
Keeping it honest¶
An agent guide that names a removed API is worse than no guide, because an agent
will follow it confidently. Everything checkable in the block above is asserted
against the built type declarations by scripts/check-agents-guide.mjs, which runs
in npm run check and in CI: every recommended symbol must still exist, every
symbol listed as removed must still be absent, and structural claims — that
DieResult has no values, that TestDef has no index — are verified against
dist/**/*.d.ts rather than trusted.
If a rule here and the API Reference ever disagree, the API Reference is correct and this page has a bug worth reporting.
Also shipped¶
AGENTS.mdin the repository and in the npm package, sonode_modules/@wafertools/wafermap/AGENTS.mdis readable when you point an agent at the installed package.llms.txt— a short machine-readable map of the docs and entry points, served at wafertools.github.io/wafermap/llms.txt and shipped inside the npm package.AGENTS.mdat the root of the examples package, since opening an agent inside that folder to adapt an example is a realistic way to start.