# Changelog of Houzz Professionals Scraper (`devilscrapes/houzz-professionals-scraper`) Actor

- **URL**: https://apify.com/devilscrapes/houzz-professionals-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/devilscrapes/houzz-professionals-scraper.md

## Houzz Professionals Scraper — Changelog

### 0.0.2 — 2026-08-06

- **Fix: root cause of the 0-row-but-SUCCEEDED cloud QA failure (build
  0.0.1).** A live cloud-run diagnostic capture proved the real Houzz
  listing/profile page nests its Flux store dump under
  `hz_ctx.data.stores.data.<StoreName>` — not the flattened top-level
  `hz_ctx.<StoreName>` shape `src/parser.py` assumed (and the spec's
  bounded recon fixtures baked in). `iter_listing_professionals` and
  `extract_profile_extra` always read the wrong path, so every page
  fetched+parsed fine but merged 0 professionals — hence a green,
  11-second, 0-row run. Fixed via `parser._locate_store()`, which checks
  the real nested path first and falls back to the flat shape (keeps
  every existing fixture/test valid). Confirmed with a real local run
  against a live cloud-IP-served page: 5/5 real leads, correct license
  numbers, websites, review counts.
- **Fix: silent-empty-success — a whole run can no longer exit 0 with 0
  rows silently.** `scraper.py` now flags any listing page that
  fetches+parses OK but merges 0 professionals (`_flag_if_empty_listing`),
  logs `ERROR` with UserStore/ProfessionalStore counts, and captures the
  raw `hz_ctx` on `RunStats.diagnostic_dump`. `main.py` writes that dump
  to the KVS (`HOUZZ_EMPTY_LISTING_DEBUG`) and `scraper._raise_if_whole_run_empty`
  fails the run loud (`RuntimeError`, non-zero exit) when the whole run
  ends with 0 kept rows *because of this* — deliberately narrower than
  "0 rows", so a legitimate 0-match-after-location-filter run (REQ-3)
  still succeeds with its honest shortfall status message.
- 90/90 pytest (83 baseline + 3 empty-guard/diagnostic tests +
  4 nested-wire-format tests).

### 0.0.1 — 2026-08-06

- Scaffolded: `.actor/` (actor.json, input/output/dataset schemas, PPE,
  Dockerfile), `src/models.py` fully implemented (`ActorInput` + `ResultRow`
  per spec, kebab-case slug validation, bounds), placeholder `src/main.py`
  (boots, validates input, pushes one `{"placeholder": true, ...}` row,
  exits — no real crawler yet), tests, README generated via
  `scripts/regenerate_readmes.py` from `scaffold.json`.
- Real implementation (`src/categories.py`, `src/parser.py`,
  `src/scraper.py`, wiring `src/main.py` to `scraper.run()`) is the next
  step per `docs/specs/houzz-professionals-scraper/design.md` (T01-T09) —
  wire format is already live-confirmed, no recon blocker.
