docs: hxprobe plans, summaries, explanations, usage, and changelog

Plans (docs/plans/):
- 2026-07-01-23-47-py-hxprobe-httpx.md — initial httpx probe design
- 2026-07-02-09-32 through 14-05 — standalone project, toolchain, usage
  doc + Makefile, file input (-f), simplification pass, run-summary footer

Summaries (docs/summaries/): one per completed feature, recording what
was actually built, deviations from the plan, and verification steps

Explanations (docs/explanations/): two deep-dives written during review —
hxprobe concurrency model and worst-exit-code + render-loop analysis

Usage (docs/usage/hxprobe.md): overview with pointer to hxprobe/USAGE.md
for the full runnable reference

Walkthrough (docs/py-latprobe-walkthrough.md): narrative tour of the
latprobe Python package for interview / code-review context

CHANGELOG.md: entries for all hxprobe features (toolchain, usage doc,
file input, simplification, run-summary footer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 13:22:59 +02:00
parent 13a562e966
commit f487a4b1bd
19 changed files with 2367 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
# hxprobe: end-of-run summary footer for multi-URL runs
## Context
A single scalar exit code is inherently lossy when probing multiple URLs with
different failure classes: today `run()` returns the numeric max across all
URLs (e.g. DNS-fail on one URL + TLS-fail on another → exit `5`, and the DNS
failure is invisible in the code). The failures *are* all printed per-URL, but
there's no consolidated view — for many URLs you must scroll and eyeball each
block to know what happened overall.
The exit code's job is a coarse pass/fail + severity hint for scripts, and it's
a **deliberate cross-implementation contract** shared with `latprobe` and the
Go version (documented in `hxprobe/USAGE.md:475-488`, asserted by 13 tests in
`tests/test_cli.py`). So we keep the worst-code exit unchanged and instead give
humans the full picture the scalar can't: an **end-of-run summary footer** that
tallies every URL's outcome and shows how the exit code was derived.
Decisions (confirmed with the user):
- Exit code: **unchanged** (worst/highest severity across URLs).
- Summary: **text footer, multi-URL only** (`len(urls) > 1`). Single-URL text
output stays byte-identical; JSON output stays a bare array (unchanged).
## Design
### Per-URL classification (reuses existing helpers)
Each URL gets one "worst outcome code" using the *existing* mapping — no new
severity scheme:
- start at `EXIT_OK`;
- for each failed sample: `code = max(code, _phase_code(r.fail_phase))`
(`_phase_code` at `cli.py:31`);
- if `--fail`: for each succeeded sample with `status_code >= 400`:
`code = max(code, EXIT_HTTP)`.
A URL is "ok" iff its code is `EXIT_OK`, else "failed" and bucketed by its code.
A partially-failed URL (some samples ok, some failed) classifies by its worst
sample — consistent with how its own block and the global exit code already
behave.
### Footer format (text, only when `len(urls) > 1`)
Printed once after the last URL block, before `return worst`. Failure classes
use the same `✗` bullet style as `_print_failure_summary` (`cli.py:219-224`).
The final `→ exit N (label)` line explicitly ties the tally to the returned
code — directly answering "why is the exit code what it is". Example (3 URLs):
```
https://example.com (200)
... phase rows ...
http://no.such.host.invalid (FAILED)
✗ dns: [Errno 8] nodename nor servname provided
https://self-signed.badssl.com (FAILED)
✗ tls: certificate verify failed
─────────────────────────────────────────────────
Summary: 3 URLs — 1 ok, 2 failed
✗ dns : 1
✗ tls : 1
→ exit 5 (tls)
```
All-ok multi-URL run → `Summary: 3 URLs — 3 ok`, no `✗` lines, `→ exit 0 (ok)`.
## Changes
All in `hxprobe/hxprobe/cli.py` unless noted.
1. **Reverse label map** next to the exit-code constants (`cli.py:16-32`): a
small `_EXIT_LABELS: dict[int, str]` mapping `EXIT_DNS→"dns"`,
`EXIT_CONNECT→"connect"`, `EXIT_TIMEOUT→"timeout"`, `EXIT_TLS→"tls"`,
`EXIT_HTTP→"http"`, `EXIT_OK→"ok"`. (Inverse of the existing forward mapping;
kept explicit for readability, matching the codebase's style.)
2. **Refactor the accumulation loop** (`cli.py:454-462`) to compute a per-URL
code and fold it into `worst`, collecting `url_codes: list[int]` (one per
URL, index-aligned with `urls`). This also unifies the two "running max"
idioms flagged in
`docs/explanations/2026-07-02-13-25-hxprobe-worst-exit-code-and-render-loop.md`
(`if c > worst` vs `max(...)`) into one — a small simplification bonus.
3. **New `_print_run_summary(urls, url_codes, worst, out)` helper** (near the
other `_print_*` renderers): builds the ok/failed tally + per-class counts
from `url_codes` and writes the footer. No-op guard is the caller's
`len(urls) > 1` check.
4. **Call site** after the loop (`cli.py:470-475`): in text mode only
(`if not ns.json_out and len(urls) > 1:`), call `_print_run_summary(...)`
before `return worst`. JSON path untouched — still `json.dumps(json_items)`
as a bare array.
## Tests (`hxprobe/tests/test_cli.py`)
Add hermetic tests (reuse the existing `_OKHandler`/`_start_server`/`_invoke`
harness and `_free_port` for a refused connection):
- **multi-URL mixed** — one OK server URL + one `http://127.0.0.1:<free>`
(connection refused): assert `code == EXIT_CONNECT`, and the footer strings
are present (`"Summary: 2 URLs"`, `"1 ok"`, `"1 failed"`, `"connect : 1"`,
`"→ exit 3"`).
- **multi-URL all ok** — two OK URLs: assert `code == EXIT_OK` and
`"Summary: 2 URLs — 2 ok"` present; verify no `"✗"` in the footer region.
- **single URL has NO footer** — one OK URL: assert `"Summary:"` NOT in `out`
(locks the multi-URL-only rule).
Backward-compat guard: the footer contains no `"200"` substring, so the
existing `test_reads_urls_from_file` assertion `out.count("200") == 2` still
holds; all 13 existing exit-code tests are unaffected (worst-code unchanged).
## Docs / project conventions (per CLAUDE.md)
- **Copy this approved plan** into
`docs/plans/<yyyy-mm-dd-hh-mm>-hxprobe-run-summary-footer.md` as the first
implementation step (before code) — see the `feedback_plan_mode_docs_plans`
memory note.
- `hxprobe/USAGE.md`: add a short "Multi-URL summary footer" subsection with a
real captured example; add a sentence to the Exit-codes section noting the
footer shows the per-class breakdown behind the scalar. Exit-code table
itself is unchanged.
- `docs/usage/hxprobe.md`: one-line mention if it lists features.
- New `docs/explanations/<ts>-hxprobe-run-summary-footer.md` is optional; the
existing worst-exit-code explanation can get a short "Update:" pointer.
- `CHANGELOG.md`: new timestamped entry.
- `docs/summaries/<ts>-hxprobe-run-summary-footer.md`: implementation summary.
## Verification
1. `cd hxprobe && .venv/bin/python -m pytest tests/test_cli.py tests/test_probe.py -q`
— all existing + new tests pass.
2. `.venv/bin/ruff check hxprobe/` — clean.
3. Manual, capturing exit codes:
```
.venv/bin/python -m hxprobe https://example.com https://example.org # footer, exit 0
.venv/bin/python -m hxprobe https://example.com http://no.such.host.invalid; echo $? # footer w/ dns:1, exit 2
.venv/bin/python -m hxprobe https://example.com # single URL: NO footer, unchanged
.venv/bin/python -m hxprobe --json https://example.com https://example.org # bare JSON array, NO footer
```