# Changelog of Website Lead Enricher (`operational_zirconia/website-lead-enricher`) Actor

- **URL**: https://apify.com/operational\_zirconia/website-lead-enricher/changelog.md
- **Full Actor documentation**: https://apify.com/operational\_zirconia/website-lead-enricher.md

## Changelog

All notable changes to the Website Lead Enricher Actor. Dates use ISO 8601 (YYYY-MM-DD).

### \[Unreleased]

### \[2026-06-27] — Per-Step Error Isolation

#### Added

- **Per-record `pipelineData.steps[]`** — every enriched lead now carries an array of pipeline-step entries (`name`, `status`, `durationMs`, `required`, `error?`). Each step's `status` is `ok` or `error`, never partial. Failures emit a structured `{code, message}` error identical in shape to `scrapeError`, so consumers can branch programmatically.
- **Required vs. optional table wired in.** `scrape` is the only required step (record dropped on failure, `scrapeError` populated). Every other step (`whois`, `companyName`, `addresses`, `socials`, `emailClassification`, `companyType`, `sendability`, `qualityScore`, `phoneDedup`) is optional — a failure never kills the record.
- **Run-level `runSummary`** envelope in the local-mode JSON output: `{ stepErrors, droppedRecords, totalRecords, durationMs }`. `stepErrors` is a per-step counter so support can answer "which step is failing most often?" without grepping logs.
- **Run-level `runSummary` in cloud mode** — `main()` now writes the same `runSummary` object to the `OUTPUT_RUN_SUMMARY` Key-Value Store key in cloud mode (matching the URL advertised in `.actor/output_schema.json`). The `/stats` HTTP endpoint reads from this key when present, with a dataset-scan fallback for `stepErrors` / `droppedRecords` / `totalRecords` when the record is missing (older runs, local replays).
- **Per-step error log line** with `[runId=<id>] url=... step=... code=... msg="..."` — every failure carries the run id for log correlation across Apify runs.
- **`runId` capture** in `main()` via `getRunId()` helper (reads `Actor.getEnv().runId || process.env.APIFY_RUN_ID`, falls back to `'local'`).
- **`resetRunStats()` test hook** — clears the module-scoped `runStats` between integration test scenarios (mirrors `resetProxyUsageCounters()`).

#### Changed

- `pipelineData` field added to `.actor/dataset_schema.json` (`fields.pipelineData.steps[]` with `name` enum of all 10 pipeline steps).
- `runSummary` field added to `.actor/output_schema.json` envelope (now actually populated in cloud mode via `OUTPUT_RUN_SUMMARY`).
- `.actor/openapi.json` — `Lead` schema now exposes `pipelineData`; new `PipelineStep` component schema; `/stats` endpoint and `/leads` description updated; `Stats` schema gained `stepErrors`, `droppedRecords`, `totalRecords`, `durationMs` (the `/stats` endpoint now actually returns them); `durationMs` is now nullable in the schema because it can only be sourced from the `OUTPUT_RUN_SUMMARY` KV-store record.
- README — added 🛡️ Per-Step Error Isolation row in the *What it does* table.
- `docs/NextSteps/PerStepErrorIsolation.md` renamed to `✅ PerStepErrorIsolation.md`; status line updated to ✅ (shipped 2026-06-27). Required/optional table and AC text now list the code-faithful step names (`whois`, `companyName`, `addresses`, `socials`, `emailClassification`, `companyType`, `sendability`, `qualityScore`, `phoneDedup`) instead of the B2B Suite placeholders (`pattern`, `qualify`, `verify`).

#### Fixed

- Step failures no longer silently break the record. A failed `whois` no longer masks the rest of the record; a failed `socials` no longer hides the extracted emails.
- **`/stats` endpoint** — previously advertised `stepErrors`, `droppedRecords`, `totalRecords`, `durationMs` in OpenAPI but never returned them. Now reads from `OUTPUT_RUN_SUMMARY` KV-store when present, with dataset-scan fallback for the three derivable fields.
- **`OUTPUT_RUN_SUMMARY` Key-Value Store key** — previously advertised in `.actor/output_schema.json` but never written. Now written at the end of every cloud-mode `main()` invocation.
- **Test suite leaks** — three main test files (`main-step-isolation`, `main-scrape-required`, `main-error-fallback`) now mock the `apify` module so `Actor.main(main)` at the bottom of `src/main.js` is a no-op. Previously, those tests emitted "Cannot log after tests are done" because the real `main()` was scheduled to run asynchronously after Jest finished.

#### Migration notes

- The `pipelineData` field is additive — existing consumers reading `scrapeError`, `contacts.emails`, etc. keep working unchanged.
- The standard CSV export now carries a `pipeline_data` column (JSON-stringified `pipelineData` object) at the end. CSV consumers that don't expect the new column may need to widen their parser.
- The `/stats` endpoint now returns `stepErrors`, `droppedRecords`, `totalRecords` unconditionally (with sensible defaults) and `durationMs` only when the `OUTPUT_RUN_SUMMARY` KV-store record is present (i.e. for runs produced by this Actor). Dashboard consumers should treat `durationMs` as optional.

#### Tests

- 831+ tests passing across 42 suites (was 747 across 33 before this feature, then 830/41 after the initial ship). New coverage: step-runner utility, step-error classifier, run-context helper, run-summary aggregator, per-step isolation through `processUrl`, scrape-required drop path, `runId` capture and threading, and a 50-URL smoke test (AC #4) that drives `main()` end-to-end and asserts `Promise.allSettled` semantics — all 50 records land in the dataset even when 5 of them throw at the required step.
