# Plan: extract `hxprobe` into a fully standalone top-level project ## Context `hxprobe` currently lives at `python/hxprobe/` and imports shared code from `latprobe` (`latprobe.probe.{Options,Phase,Result,VerboseDetail,_parse_cert}`, `latprobe.cli.run`). That coupling was a deliberate reuse choice at the time, but the user now wants `hxprobe` to be a genuinely independent project — no `from latprobe import ...` anywhere, duplication accepted — and, per their follow-up answer, independent enough that it could be `cp -r`'d into its own repo tomorrow: own `pyproject.toml`, own venv, own Makefile section, own top-level directory (sibling to `go/` and `python/`), not nested under `python/`. Confirmed via `git status`/`git log`: everything hxprobe-related (`python/hxprobe/`, `python/pyproject.toml`, `python/tests/test_hx_*.py`, `docs/usage/py-hxprobe.md`, the two `docs/plans`/`docs/summaries` entries) is still uncommitted from this session, and `python/latprobe/cli.py` / `python/latprobe/probe.py` only diverge from the last commit (`24ea9c9`) by the reuse-oriented additions made to support hxprobe. So this is a clean, low-risk restructuring: move already-debugged code, revert latprobe with `git checkout --`, no git history surgery needed. ## Target layout ``` hxprobe/ (new, top-level, sibling of go/ and python/) ├── pyproject.toml (own manifest: httpx[http2] dependency) ├── hxprobe/ │ ├── __init__.py │ ├── probe.py (own Options/Phase/Result/VerboseDetail/CertInfo │ │ + own _parse_cert/_parse_cert_date + existing │ │ _Trace/_TimingStream/_TimingBackend/ │ │ _TimingTransport/measure() — unchanged logic) │ ├── aggregate.py (verbatim copy of latprobe/aggregate.py — │ │ its `from .probe import Result` is already │ │ package-relative, needs zero edits) │ ├── duration.py (verbatim copy of latprobe/duration.py — no │ │ imports at all) │ ├── cli.py (full standalone CLI — see below) │ └── __main__.py (unchanged: `from .cli import run`) └── tests/ ├── __init__.py (empty, matches python/tests/__init__.py) ├── test_probe.py (moved from python/tests/test_hx_probe.py) ├── test_cli.py (moved from python/tests/test_hx_cli.py) └── test_integration.py (moved from python/tests/test_integration_hx.py) ``` `python/` reverts to containing only `latprobe` — zero third-party deps, no `pyproject.toml`, no venv, exactly its pre-hxprobe state. ## Step-by-step **1. Move already-debugged files (preserve the bug fixes already made):** `git mv`/`mv` (untracked, so plain `mv` is fine) `python/hxprobe/{probe.py,__init__.py,__main__.py}` to `hxprobe/hxprobe/`, and the three `python/tests/test_hx_*.py` / `test_integration_hx.py` files to `hxprobe/tests/` with the `hx_`/`_hx` name segments dropped (`test_probe.py`, `test_cli.py`, `test_integration.py`). Do **not** rewrite these from scratch — they already have the mark_dns / verbose-on-failure-path / Content-Length-on-keep-alive fixes found during the original implementation. **2. Inline the dataclasses into `hxprobe/hxprobe/probe.py`:** Replace `from latprobe.probe import Options, Phase, Result, VerboseDetail, _parse_cert` with local definitions copied verbatim from `python/latprobe/probe.py`: `CertInfo`, `VerboseDetail` (its `http_version`/`redirect_count` fields are now simply always-meaningful, no more "populated only by hxprobe" caveat comment needed), `Options` (with `follow_redirects`/`http2` as normal fields, no more "ignored by socket measure()" caveat), `Phase`, `Result`, `_parse_cert`, `_parse_cert_date`. Everything else in the file (`_Trace`, `_TimingStream`, `_TimingBackend`, `_TimingTransport`, `measure()`, `_classify`, `_unwrap`, `_fill_phases`, `_fill_verbose`) is untouched. **3. Create `hxprobe/hxprobe/aggregate.py` and `duration.py`:** Verbatim copies of `python/latprobe/aggregate.py` and `duration.py`. **4. Write `hxprobe/hxprobe/cli.py` as a full standalone CLI:** Start from the *current* `python/latprobe/cli.py` (it already has 100% of the needed logic, including the `--no-http2`/`--no-follow-redirects` flags, the verbose "Protocol" row, and the JSON `http_version`/`redirect_count` keys — all added earlier specifically for hxprobe). Strip the generalization scaffolding that only existed to let `latprobe` share this code: - Remove the `MeasureFn` type alias and the `measure_fn` parameter from `_run_samples`/`run` — call `measure` directly (module-level import from `.probe`). - Remove `run()`'s `prog`/`description`/`protocol_flags` parameters — hardcode `prog="hxprobe"` and the httpx-specific description. - Make the `--no-http2`/`--no-follow-redirects` `add_argument` calls unconditional (drop the `if protocol_flags:` guard). - Everything else (exit codes, `_ArgExit`/`_Parser`, phase-label/verbose constants, all `_print_*`/`_build_json_entry` rendering) carries over unchanged — it's already correct standalone logic. **5. Revert `python/latprobe/` to its pre-hxprobe state:** `git checkout -- python/latprobe/cli.py python/latprobe/probe.py` (safe: confirmed these are the only diffs since the last commit, and both diffs are exactly the reuse scaffolding being removed here). **6. Clean up the old shared-package artifacts:** Delete `python/hxprobe/`, `python/pyproject.toml`, `python/.venv/`, and the three `python/tests/test_hx_*`/`test_integration_hx.py` files (now moved). **7. Makefile:** - Remove `PY_VENV`/`PY_VENV_PYTHON` vars and the `py-deps` target; revert `py-test`/`py-test-integration`/`py-check` to invoke `$(PYTHON)` directly with no `py-deps` prerequisite (their pre-hxprobe form). Remove `hx-run`/ `hx-test-integration` from the Python section (moving out). - Add `HX_DIR := hxprobe`, `HX_VENV := $(HX_DIR)/.venv`, `HX_VENV_PYTHON := $(HX_VENV)/bin/$(PYTHON)` and a new "── hxprobe (standalone) ──" section: `hx-deps` (idempotent venv+pip install, mirrors the removed `py-deps`), `hx-run`, `hx-test` (hermetic, `test_[!i]*.py` glob), `hx-test-integration` (`test_integration.py` pattern), `hx-check`, `hx-clean`. - Fold into the umbrella targets: `test: go-test py-test hx-test`, `check: go-check py-check hx-check`, `clean: go-clean py-clean hx-clean`. **8. Docs:** - Rename `docs/usage/py-hxprobe.md` → `docs/usage/hxprobe.md` (drop the `py-` prefix — it's no longer part of the Python port). Update: remove the "reuses `latprobe.cli.run()` in full" claim (now false — say it's a fully standalone implementation sharing only the *design*, not the code, with `latprobe`); update setup/Makefile-target sections to `make hx-deps`/ `hx-run`/`hx-test`/`hx-test-integration`; update file paths from `python/hxprobe/` to `hxprobe/hxprobe/`. Keep the TCP_NODELAY/Nagle finding and the example transcripts (still accurate — behavior is unchanged, only location/packaging changed). - `CHANGELOG.md`: new entry describing the extraction. - `docs/summaries/`: new dated summary per the CLAUDE.md convention (leave the original `2026-07-02-00-29-py-hxprobe-httpx.md` as-is — it's a historical record of that implementation; this is a follow-up). - `docs/plans/`: save this plan as `docs/plans/-hxprobe-standalone-project.md` at implementation start, per CLAUDE.md. ## Verification 1. `rm -rf hxprobe/.venv python/.venv` (fresh state) then `make hx-deps` — creates `hxprobe/.venv`, installs `httpx[http2]` from `hxprobe/pyproject.toml`. 2. `grep -rn "latprobe" hxprobe/` — must return nothing (proves the decoupling). 3. `make hx-run ARGS="-v http://github.com"` — same output as before (HTTP/2, 1 redirect followed, IP/TLS/cert shown). 4. `make hx-test` — all hermetic hxprobe tests pass standalone. 5. `make hx-test-integration` — live HTTP/2 + redirect tests still pass. 6. `make py-test` — confirms `latprobe`'s own suite is back to its original, dependency-free form and still green (no `py-deps` needed to run it). 7. `make check` (top-level) — Go + latprobe + hxprobe all green in one gate. 8. `diff <(python -m latprobe --help) <(git show 24ea9c9:python/latprobe/cli.py | ...)` — or simpler: confirm `python -m latprobe --help` output is byte-identical to before this whole feature existed (no leftover `--no-http2` etc.).