# hxprobe: end-of-run summary footer — summary Plan: `docs/plans/2026-07-02-14-05-hxprobe-run-summary-footer.md`. Trigger: a follow-up to `docs/explanations/2026-07-02-13-25-hxprobe-worst-exit-code-and-render-loop.md` — does a single worst-code exit even make sense across multiple URLs with different possible errors? The answer landed on: keep the scalar exit code (it's a documented cross-implementation contract with `latprobe`/Go — see `hxprobe/USAGE.md`'s Exit codes table, asserted by 13 existing tests), and instead add the missing visibility as an end-of-run summary footer. ## What changed All in `hxprobe/hxprobe/cli.py` unless noted, matching the plan exactly (no deviations): 1. **`_EXIT_LABELS`** (new, next to `_PHASE_EXIT`): inverse mapping from exit code → short label (`ok`, `dns`, `connect`, `timeout`, `tls`, `http`), used to render the footer's `→ exit N (label)` line. 2. **Accumulation loop refactor**: previously computed a single running `worst` directly inside the per-URL loop via two different idioms (`if c > worst: worst = c` for network failures, `max(worst, EXIT_HTTP)` for `--fail`). Now each URL first gets its own `code` (`EXIT_OK` folded up via `max()` across its failed samples and, if `--fail`, its ≥400 successes), appended to a new `url_codes: list[int]` (index-aligned with `urls`), and *then* folded into `worst = max(worst, code)`. Unifies both idioms into one. 3. **`_print_run_summary(urls, url_codes, worst, out)`** (new helper, next to `_print_failure_summary`): writes a separator line, an `N URLs — X ok[, Y failed]` header, one `✗ {label:<8}: {count}` line per non-OK class present (first-seen order, same dedup style as `_summarize_failures`), and the closing `→ exit N (label)` line. 4. **Call site**: `elif len(urls) > 1: _print_run_summary(urls, url_codes, worst, stdout)` — added after the per-URL loop, in the `else` branch of the existing `if ns.json_out:` check (so it's text-mode-only), right before `return worst`. JSON path (`json.dumps(json_items, ...)`) is completely untouched — still a bare array, no top-level summary object, preserving the documented "same shape as `latprobe`'s JSON" contract. ## Design decisions (confirmed with user before implementing) - **Exit code stays a scalar** (worst/highest severity across URLs) — not count-of-failed-URLs, not binary 0/1. Both alternatives were presented and rejected because they'd break the documented 0–6 table, Go/`latprobe` parity, and the 13 existing exit-code tests. - **Summary is a text footer, multi-URL only** (`len(urls) > 1`) — not always shown, and not also duplicated into JSON as a top-level object (which would turn the JSON array into an object and break the documented array-shape parity). Single-URL text output is untouched; JSON output is untouched. ## Tests `hxprobe/tests/test_cli.py`: new `TestCLIRunSummary` class, 3 tests, reusing the existing `_OKHandler`/`_start_server`/`_free_port`/`_invoke` harness: - `test_multi_url_mixed_shows_summary` — one OK URL + one connection-refused URL: asserts `EXIT_CONNECT`, and the footer strings (`"Summary: 2 URLs"`, `"1 ok"`, `"1 failed"`, `"connect : 1"`, `"→ exit 3"`). - `test_multi_url_all_ok_summary` — two OK URLs: asserts `EXIT_OK`, `"Summary: 2 URLs — 2 ok"`, and no `"✗"` anywhere in output. - `test_single_url_has_no_summary` — one OK URL: asserts `"Summary:"` is absent (locks the multi-URL-only rule). All 33 pre-existing tests pass unedited (33 + 3 new = 36 total). ## Verification - `hxprobe/.venv/bin/python -m pytest tests/test_cli.py tests/test_probe.py -q` → **36 passed**. - `hxprobe/.venv/bin/ruff check hxprobe/ tests/` → **all checks passed**. - Manual smoke tests, all matching the plan's expected behavior exactly: - `hxprobe https://example.com https://example.org` → footer `Summary: 2 URLs — 2 ok` / `→ exit 0 (ok)`. - `hxprobe https://example.com http://no.such.host.invalid` → footer `Summary: 2 URLs — 1 ok, 1 failed` / `✗ dns : 1` / `→ exit 2 (dns)`; process exit code confirmed `2` via `echo $?`. - `hxprobe https://example.com` (single URL) → **no** footer, output byte-for-byte the same shape as before this change. - `hxprobe --json https://example.com https://example.org` → still a bare JSON array, no summary object. - Also captured a 3-URL mixed run (`example.com` ok, DNS failure, TLS failure against `self-signed.badssl.com`) to confirm severity ordering in the footer: DNS (2) and TLS (5) both counted, exit reported as `5 (tls)` — the higher-severity class correctly wins the scalar while the footer still shows the DNS failure that the scalar alone would hide. ## Docs updated (per CLAUDE.md conventions) - `hxprobe/USAGE.md`: new "Multi-URL summary footer" section with a real 3-URL mixed-outcome capture; refreshed the pre-existing "Multiple URLs" and `-f configs/all-ok.txt` / `-f configs/dns-failure.txt` examples, which were captured before this feature existed and were now stale (missing the footer) — replaced with fresh live captures; added a sentence to the Exit codes section pointing at the new section. - `docs/usage/hxprobe.md`: one-line addition to the exit-codes bullet pointing at `hxprobe/USAGE.md`'s new section. - `docs/explanations/2026-07-02-13-25-hxprobe-worst-exit-code-and-render-loop.md`: appended an "Update (2026-07-02)" paragraph pointing forward to this work, per the plan's "optional — pointer instead of a new file" option. - `CHANGELOG.md`: new entry at the top. - This file. No deviations from the approved plan.