# hxprobe simplification — summary Plan: `docs/plans/2026-07-02-12-05-hxprobe-simplification.md` (five targeted removals of speculative scaffolding / redundant state / duplicated logic, no behavior change). Trigger: a question about a dead `file=` parameter on `_Parser.print_help`/`print_usage` in `hxprobe/hxprobe/cli.py`, which led to a broader pass over the whole package for the same pattern. ## What changed 1. **`hxprobe/hxprobe/cli.py` — deleted the `_Parser` subclass and `_ArgExit` exception** (~32 lines). It re-implemented, by hand, behavior the standard library already provides: `argparse.ArgumentParser` already writes help to `sys.stdout` and usage/errors to `sys.stderr`, and already raises `SystemExit` rather than hard-exiting. `run()` now builds a plain `argparse.ArgumentParser`, wraps the parse + validation calls in `contextlib.redirect_stdout(stdout)` / `redirect_stderr(stderr)`, and catches `SystemExit` at a single site: ```python except SystemExit as exc: return EXIT_OK if not exc.code else EXIT_USAGE ``` Deviation from the original plan sketch: the plan's snippet used `EXIT_OK if not exc.code else EXIT_USAGE`, matching what was actually implemented (equivalent to, but slightly more defensive than, the `exc.code == 0` check first drafted, since argparse can in principle pass `None`). 2. **`hxprobe/hxprobe/probe.py` — trimmed `_TimingStream.get_extra_info`** to the single branch actually consumed on the request path (`ssl_object`). Verified by grepping the installed `httpcore`/`httpx` packages: the `server_addr`/`client_addr` branches were only ever queried by `httpx`'s own `_main.py` (the `httpx` CLI command), never by anything hxprobe's request path touches. 3. **`hxprobe/hxprobe/cli.py` — simplified `_load_urls`**: dropped the `line.split()[0]` "forward-compatible with future `url key=value` annotations" scaffolding; a stripped line is used directly. Docstring updated to describe only what the function does today. 4. **`hxprobe/hxprobe/probe.py` — removed `_dns_set`/`_connect_set`/ `_tls_set`** from `_Trace`. These booleans duplicated `Phase.present` on `self.dns`/`self.connect`/`self.tls` (each starts as `Phase()`, i.e. `present=False`). Guards rewritten from `if not self._dns_set:` to `if not self.dns.present:` (and analogously for connect/tls) — identical first-hop-wins semantics for redirects, one less piece of parallel state. 5. **`hxprobe/hxprobe/cli.py` — extracted `_summarize_failures`**, a shared helper that dedupes `failed: list[Result]` into `[(phase, message, count), ...]` in first-seen order. `_print_failure_summary` (text rendering) and `_build_json_entry` (JSON rendering) previously each carried an identical ~12-line dedup loop; both now call the helper and only differ in how they format the tuple. ## Not changed (considered, kept — per the plan) - The custom httpcore backend (`_TimingBackend`/`_TimingStream`/ `_TimingTransport`) — this is the tool's actual reason to exist. - `ThreadPoolExecutor` concurrency — backs the shipped `-c/--concurrency` and multi-URL/`-f` features. - Explicit per-phase dataclass fields in `probe.py`/`aggregate.py`. - `CertInfo.sans` — unused in text output but real (emitted in JSON verbose output). ## Verification - `hxprobe/.venv/bin/python -m pytest tests/test_cli.py tests/test_probe.py -q` → **33 passed**, no test file edits. - `hxprobe/.venv/bin/ruff check hxprobe/` → **all checks passed**. - Manual smoke tests (`python -m hxprobe -h`, no-args, and a live `https://example.com -v` request): - `-h` → help text on stdout, exit `0`. - No args → usage + `no URLs given` error on **stderr only** (confirmed via separate stdout/stderr redirection to files — stdout was empty), exit `1`. - Live verbose request → correct 6-phase timing, resolved IP, HTTP/2 protocol, TLS version/cipher, and certificate detail — confirms the `get_extra_info` trim didn't break `ssl_object` retrieval and the `_Trace` refactor didn't break phase/first-hop-wins tracking. Net: `cli.py` and `probe.py` are shorter and carry less parallel/duplicated state; no observable behavior changed.