decode_path_len_byte, encode_path_len_byte, parse_trace_payload_route_hashes and calculate_packet_hash move verbatim to packet_decode.py, which already handled packet framing and imported decode_path_len_byte from utils; verify_meshcore_advert_ed25519 moves next to the advert parser in meshcore_payload_decode.py. utils re-exports all five as the same objects, and no test patched them. get_packet_hash_placeholder stays in utils with the response template it serves. utils.py drops to 1,630 lines.
decode_meshcore_packet, the path processor, the path chunkers, format_path_string and the route/payload type-name tables are now plain functions in packet_decode.py. MessageHandler keeps every old method as a delegate that supplies the configured prefix width and its logger, so callers and tests that patch decode_meshcore_packet on a handler still work. The existing quirk is preserved: path_info uses the configured width while path/path_hex follow the packet's own bytes-per-hop.
Parity: decoding a sample of 4,009 real packets from the dev database (plus malformed inputs) at prefix widths 2 and 4, with the path and format helpers, produces byte-identical output before and after. A handler built with no bot attribute now decodes path_info at width 2 instead of falling into the error branch; only bare test objects can hit that.