Skip to content

refactor: extract duplicated wire-unit arithmetic across mh/ipv6_route/hopopt/ipv6_opts - #509

Merged
JarryShaw merged 2 commits into
mainfrom
refactor/495-extract-wire-arithmetic
Sep 19, 2026
Merged

JarryShaw merged 2 commits into
mainfrom
refactor/495-extract-wire-arithmetic

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Closes #495 — the last open item from the post-wave-1 consistency sweep. Pure refactor: 168 duplicated arithmetic sites replaced by five helpers, with no behaviour change.

Why this is not tidying

Two shipped defects came from exactly this duplication. #487 computed Hdr Ext Len in four places and got four different wrong answers. #398 (29dfd351b) was one conceptual +2/-2 unit mismatch that had to be diagnosed once and then patched independently in six files — hopopt.py 130 lines, ipv6_opts.py 131, mh.py 130, plus all three schema files at 46/46/47.

What changed

scope sites helper
ipv6_route.py total header octets 4 ipv6_route_header_length(), beside the existing ipv6_route_data_length()
mh.py message length 25 MH._mh_message_length()
mh.py option length 92 MH._mh_option_length()
hopopt.py option length 17 HOPOPT._hopopt_option_length()
ipv6_opts.py option length 17 IPv6_Opts._ipv6_opts_option_length()

The mh.py message-length case is the clearest: the write side was already unified at mh.py:1438 while the read side stayed duplicated 25 ways, and that value feeds _decode_next_layer(mh, schema.next, length - mh.length) — the same role Hdr Ext Len played in #487.

ipv6_route_header_length() is defined as 4 + ipv6_route_data_length(hdr_ext_len), which keeps the two related-but-distinct quantities visibly related: 8 + 8k total header octets versus 4 + 8k data octets. Confusing those two was #487.

Deliberately NOT done

The hopopt.py/ipv6_opts.py shared base class. Those two files are one implementation twice (406 changed lines out of ~1950, overwhelmingly renaming), and collapsing them would subsume a large part of this issue — but it is a cross-cutting change that deserves its own issue and its own review, so the option-length helpers here are per-class rather than one shared helper across the two files. That boundary was drawn on purpose.

Also untouched, and already retired with reasons in #495: pcap.py/pcapng.py not scaling fragment offsets (they consume already-scaled values, unlike the third-party adapters, so the asymmetry is correct); pypcap.py/pcap_ct.py raising UnsupportedCall throughout; HIP's single length=schema.len * 8 + 8 call site, which has nothing to share with.

The trap this change had to avoid

reassembly/tcp.py and reassembly/ip.py look like near-duplicates and must not be merged: RFC 791 lines 1892-1894 mandate last-write-wins for IP, RFC 9293 §3.10 mandates first-write-wins for TCP, and #443 settled that deliberately. Neither file is in this diff — 0 reassembly files, verifiable from git diff --name-only origin/main...HEAD.

Verification

Helper arithmetic checked independently of the tests — 240 comparisons over k = 0..39 against the original expressions, 0 mismatches:

ipv6_route_header_length(k) == k*8 + 8      ipv6_route_data_length(k) == 4 + k*8
MH._mh_message_length(k)    == (k+1)*8      MH._mh_option_length(k)   == k + 2
HOPOPT._hopopt_option_length(k) == k + 2    IPv6_Opts._ipv6_opts_option_length(k) == k + 2

Every old pattern is gone, and the counts match #495's measurements exactly:

                            main   branch
mh  (header.length + 1) * 8   25      0
ipv6_route  length * 8 + 8     4      0
hopopt      .len + 2          17      0
ipv6_opts   .len + 2          17      0

Byte-identity against main, which is the only proof that matters for a refactor. Construct-then-parse across IPv6-Route source-route with 0/1/2/3 addresses, six MH message types, and the HOPOPT/IPv6-Opts Router-Alert option, dumped to JSON from both trees:

BEFORE (main):     md5 d797c44b0c1b
AFTER  (#495):     md5 d797c44b0c1b
diff: IDENTICAL

Critically, 0 error-ish lines in that output — so this is a comparison of real results, not of two identical crashes. (A driver that dies the same way on both trees reports "identical" while proving nothing; that happened earlier in this programme and is worth guarding against explicitly.)

Tests on the directly affected paths: test_option_roundtrip_unit.py + test_mh_unit.py + test_ipv6_extension_unit.py → 98 passed, 860 subtests passed. EXPECTED_FAILURES inspected by importing the module (it cannot be grepped — ** unpacking): 59 entries, none needing an update.

One deliberate omission

No dedicated unit tests for the five helpers. That follows the house precedent set by #487/#489: IPv6_Route._make_hdr_ext_len and ipv6_route_data_length have no dedicated tests either and are exercised through the existing round-trip suite. Adding a new convention here rather than following the existing one seemed the wrong call for a refactor, but it is a judgement worth disagreeing with — the arithmetic check above is a script, not a committed test.

…popt/ipv6_opts (#495)

Per-class helpers for read-side length arithmetic that had been copy-pasted
across many call sites -- the same pattern that caused #487 and #398.

- ipv6_route.py: add ipv6_route_header_length() beside the existing
  ipv6_route_data_length(), replacing the 4 `header.length * 8 + 8` sites
  in _read_data_type_*. Finishes the read-side half of #487/#489.
- mh.py: add MH._mh_message_length(), replacing the 25 identical
  `(header.length + 1) * 8` sites in _read_msg_*. Mirrors the write side
  already unified at make() (`(len(data_val) + 6) // 8 - 1`).
- mh.py, hopopt.py, ipv6_opts.py: add one per-class option-length helper
  each (_mh_option_length, _hopopt_option_length, _ipv6_opts_option_length),
  replacing the 92/17/17 = 126 `<schema>.length + 2` / `<schema>.len + 2`
  sites in _read_opt_*. This is the exact +2/-2 mismatch #398 fixed six
  times independently.

Deliberately left alone: the hopopt.py/ipv6_opts.py shared base class
(cross-cutting, its own issue) and the TCP/IP reassembly pair, which must
NOT be merged since RFC 791 and RFC 9293 mandate opposite overlap
resolution.

Pure refactor, no behaviour change: full protocols test suite (466 passed,
1260 subtests) and test_option_roundtrip_unit.py (6 passed, 358 subtests)
are identical before and after, and construct-then-parse byte comparisons
for MH/IPv6-Route/HOPOPT/IPv6-Opts match exactly pre- and post-change.
Comment thread pcapkit/protocols/schema/internet/ipv6_route.py
@JarryShaw
JarryShaw merged commit 27bb315 into main Sep 19, 2026
24 checks passed
@JarryShaw
JarryShaw deleted the refactor/495-extract-wire-arithmetic branch September 19, 2026 05:47
JarryShaw added a commit that referenced this pull request Sep 20, 2026
#528)

Closes #512. Closes #517.

`MH._mh_option_length` adds the 2 octets an RFC 6275 §6.2 mobility option
spends on its 1-octet Option Type and 1-octet Option Length. The three CGA
extension readers reused it, but a CGA extension's Extension Type and
Extension Data Length are 2 octets each (RFC 4581 §2, which formally updates
RFC 3972), so every parsed extension reported a length two octets short of
what it consumed: the 8-octet `00120004deadbeef` came back as 6.

- mh.py: add `_mh_extension_length`, returning `schema_length + 4`, and route
  `_read_ext_none`, `_read_ext_multiprefix` and `_read_ext_exp` through it.
  `_mh_option_length` is correct and is left alone -- the defect was in its
  callers, not in the helper.
- mh.py: convert `_read_opt_pad`'s open-coded `clen + 2` to the helper #509
  created to collect exactly that arithmetic, and add the `Note:` its own
  docstring already pointed at ("the surviving explanation of that fix ...
  below"), which had never travelled with the copied paragraph.
- ipv6_opts.py: cite RFC 8200 §4.2, where the quoted `Opt Data Len` sentence
  actually appears, rather than §4.3 -- that is the Hop-by-Hop Options header,
  and this class implements Destination Options (§4.6).
- tests: pin both header widths side by side, assert every CGA reader's
  reported length equals `len(schema.pack())`, and pin `_read_opt_pad`'s
  delegation by patching the helper with a sentinel. Corrects
  `test_mh_experimental_cga_extensions_round_trip`, which asserted the packed
  length as payload+4 and the parsed length as payload+2 four lines apart.

tests/protocols/internet: 191 passed, 654 subtests, no regressions.
@JarryShaw JarryShaw added ci Pull requests that change CI or workflow configuration (ci: subject prefix) refactor Restructuring for its own sake — neither a fix nor a new capability (refactor: prefix) and removed ci Pull requests that change CI or workflow configuration (ci: subject prefix) labels Sep 22, 2026
JarryShaw added a commit that referenced this pull request Sep 24, 2026
…arning count

Round 8: ran the merge-base/changed-files/cited-path intersection to
completion (0c7f2b7..origin/main: 43 commits/124 files; 54 cited paths,
34 of them touched by that range) instead of trusting a tense grep.

- :1540/:1542 -- "93 of the 95 sites"/"48 of the 49 record lengths" ->
  94 of 95 / 49 of 49; zero old-expression sites remain on main.
- :1544-1546, :1566 -- "LOCATOR_SET keeps the old expression ... is
  currently right" / "is unchanged in both respects" -> past tense;
  #679 fixed both LOCATOR_SET sites.
- :1566 -- "HIP_COPIES stays at two" -> past tense; #679 fixed
  LOCATOR_SET's Length unit, #689 then dropped HIP_COPIES to one.
- :1976-1977 -- "pcapng.txt ... wants a separate refresh" -> past
  tense; #685 removed it from the index instead of regenerating it.
- :2327-2328 -- "55 warnings on main before this change, 56 after"
  (-b html) -> 53/54; the raw `grep -c WARNING:` double-counts two
  Scapy import lines as Sphinx warnings, confirmed live on this head
  with both -b dummy and -b html (real 36/raw 38 with const/reg.rst
  excluded, same +2 gap either way).
- :834 -- stale ``protocol.py:1016`` -> ``:1413`` (the actual
  ``self._file.read()`` call inside ``_read_fileng``).
- :1969 -- the #646 entry's coverage renumbering was wrong twice over
  (first ``1153 to 1265``, then ``1153 to 1443``, the latter being
  main's ``def`` line, which always executes and can never be the
  single miss). Corrected to ``1248 to 1360``, the ``warn(...)``
  statement's line before/after #646's own diff.

Regenerated CHANGELOG.md from the edited entries.

changelog_md.py --check: exit 0. pytest tests/project/test_changelog_md.py -q:
47 passed, 37 subtests.

Follow-up: origin/main advanced through #726/#740/#741/#742 (to 0a3abff)
and then #747/#748 (to 074c53e) while this sat at good-to-go; #726 moved
three more claims anchored on files it touched.

- :834 -- ``protocol.py:1413`` -> ``:1411``; #726 shifted the
  ``self._file.read()`` call in ``_read_fileng`` by -2 lines.
- :2382-83 -- traceflow.py "Line 406" -> "Line 424"; #742 inserted 18
  lines above the ``#: Type[Dumper]: Dumper class.`` comment.
- :2099-2104, :2187-89 -- the "seven code-keyed parser registrars" and
  ``Option.register`` are no longer presence-only. #726, fixing #718,
  gave all seven -- and ``Option.register`` itself -- the same identity
  guard ``register_protocol`` already had; reworded both passages to
  say so, confirmed against the guards' own current docstrings.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at 948ac49 came back NEEDS CHANGES: the round-12 edit fixed two
sites of the harmonisation claim and left its twin, plus its own reasoning,
asserting the opposite; and four numbers anchored on files the merges touched
had drifted independently of #726.

- :1877-1878, :1883-1884 (#675) -- "carries the guarded ``if code in
  cls.__xxx__: warn(...)``" / "every sibling warns on mere presence" ->
  past tense, noting #726 later gave all seven the identity guard this
  entry's own comparison assumes they lack.
- :2100-2109 -- dropped the retained "yields two keys and never reaches
  one key twice" (false: ``Internet.register(TransType.TCP, TCP)`` warns
  once, incumbent.klass is TCP) and "leaves a different-class test
  undecidable" (contradicted by :2404-2406's own ``incumbent is not
  protocol`` definition); replaced with the actual false positive the
  guard has -- pre-seeded ``ModuleDescriptor`` incumbents never compare
  equal to the resolved class.
- :2192 -- reflowed the ``Option.register`` paragraph (orphan lines
  fixed alongside).
- :2387-2388 -- the ``Type[Dumper]`` quote now matches what is actually
  at line 424 (post-#709-fix), rather than the pre-fix bare form.
- :945 -- ``README.md`` (103) -> (102).
- :1136 -- "75 of the 117 modules" -> "77 ... after #647 below adds
  the same ending to three more" (drifted via #647, independent of the
  four merges).
- :2119 -- dropped the irreproducible pylint "364 messages" figure;
  kept mypy's 112, which does reproduce.
- :2119 -- "326 registry writes" -> 327 (``R1CounterParameter``'s
  second code, from #690).

Also fixed six false claims in the PR body (separate from the .rst):
hunk/line counts, six-commits -> 46, the 5-row table's implied total,
"not trimmed", main's red/green state, and the now-unreachable
cherry-pick target.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at 1749cc0 came back NEEDS CHANGES: round 13 fixed five of
the nine sites and introduced four new false claims doing it, including
two inside the flagship rewrite -- swapping one inaccuracy for another
is this document's recurring failure mode.

- :1136-37 -- "75 ... 77 now, after #647 ... adds ... three more" was
  internally inconsistent (75+3=78, not 77). Traced #647's own diff
  (fc32d1b): it adds ``_missing_`` to three IntFlag classes across
  only two *new* files -- ``tcp/flags.py`` and ``ftp/command.py`` --
  since the third, ``TransportProtocol``, shares ``reg/apptype.py``
  with the already-counted ``AppType``. Module delta is +2, matching
  75+2=77; reworded to say so.
- :1879-88 -- dropped "the comparison below assumes they still lack"
  it, which was false about text 8 lines below in the same diff
  (already past-tensed). Also reflowed three orphan lines this
  introduced (`passes, whereas`, `it twice with nothing`, `none of
  the`).
- :2107-19 -- "These tables also ship pre-seeded" over-generalised:
  verified live (``ProtocolBase.__proto__`` is 0 entries,
  ``Transport.__proto__ is ProtocolBase.__proto__`` -- True) that 2 of
  7 have nothing pre-seeded. Scoped to the five that do (Link 7,
  Internet 16, Frame 3, PCAPNG 3, SCTP 2). Also fixed "the guard
  resolves only the incoming class", which contradicts the guard's own
  docstring ("the comparison itself resolves nothing") -- resolution is
  the earlier ``isinstance(protocol, ModuleDescriptor)`` step, three
  lines above the guard, not something the guard does.
- :2129-30 -- dropped the invented "327th" ordinal (327 total stays;
  traced-write instrumentation via ``sys`` hooks found the seeding is
  literal dict construction, not ``.register()`` calls, so I could not
  reproduce an ordinal with confidence -- said "one of them" instead
  of guessing).
- :7-8 -- "between #326 and #509" now says the programme continued
  past it (verified: 193 distinct #nnn refs, max #726, 103 above 509).
- PR body -- "7 hunks, 1168+/11-" was the previous head's figure, not
  this one's; replaced with the actual command
  (``git diff --shortstat da697fa -- docs/source/changelog/1.5.0.rst``)
  and today's figure (9 hunks, 1152+/16-), since a hardcoded count here
  has now gone stale twice.

Left alone per this round's scope: :1969/:1974 (before/after claim,
not falsified by #726's later +1), mypy "112" (correct, re-ran with
the project's own flags), ":2122" 13-to-14 (correct at its delta
scope), and the other 121 cited paths (unaffected by main's one new
commit, #745, confirmed test-only).

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.
JarryShaw added a commit that referenced this pull request Sep 24, 2026
…arning count

Round 8: ran the merge-base/changed-files/cited-path intersection to
completion (0c7f2b7..origin/main: 43 commits/124 files; 54 cited paths,
34 of them touched by that range) instead of trusting a tense grep.

- :1540/:1542 -- "93 of the 95 sites"/"48 of the 49 record lengths" ->
  94 of 95 / 49 of 49; zero old-expression sites remain on main.
- :1544-1546, :1566 -- "LOCATOR_SET keeps the old expression ... is
  currently right" / "is unchanged in both respects" -> past tense;
  #679 fixed both LOCATOR_SET sites.
- :1566 -- "HIP_COPIES stays at two" -> past tense; #679 fixed
  LOCATOR_SET's Length unit, #689 then dropped HIP_COPIES to one.
- :1976-1977 -- "pcapng.txt ... wants a separate refresh" -> past
  tense; #685 removed it from the index instead of regenerating it.
- :2327-2328 -- "55 warnings on main before this change, 56 after"
  (-b html) -> 53/54; the raw `grep -c WARNING:` double-counts two
  Scapy import lines as Sphinx warnings, confirmed live on this head
  with both -b dummy and -b html (real 36/raw 38 with const/reg.rst
  excluded, same +2 gap either way).
- :834 -- stale ``protocol.py:1016`` -> ``:1413`` (the actual
  ``self._file.read()`` call inside ``_read_fileng``).
- :1969 -- the #646 entry's coverage renumbering was wrong twice over
  (first ``1153 to 1265``, then ``1153 to 1443``, the latter being
  main's ``def`` line, which always executes and can never be the
  single miss). Corrected to ``1248 to 1360``, the ``warn(...)``
  statement's line before/after #646's own diff.

Regenerated CHANGELOG.md from the edited entries.

changelog_md.py --check: exit 0. pytest tests/project/test_changelog_md.py -q:
47 passed, 37 subtests.

Follow-up: origin/main advanced through #726/#740/#741/#742 (to 0a3abff)
and then #747/#748 (to 074c53e) while this sat at good-to-go; #726 moved
three more claims anchored on files it touched.

- :834 -- ``protocol.py:1413`` -> ``:1411``; #726 shifted the
  ``self._file.read()`` call in ``_read_fileng`` by -2 lines.
- :2382-83 -- traceflow.py "Line 406" -> "Line 424"; #742 inserted 18
  lines above the ``#: Type[Dumper]: Dumper class.`` comment.
- :2099-2104, :2187-89 -- the "seven code-keyed parser registrars" and
  ``Option.register`` are no longer presence-only. #726, fixing #718,
  gave all seven -- and ``Option.register`` itself -- the same identity
  guard ``register_protocol`` already had; reworded both passages to
  say so, confirmed against the guards' own current docstrings.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at 948ac49 came back NEEDS CHANGES: the round-12 edit fixed two
sites of the harmonisation claim and left its twin, plus its own reasoning,
asserting the opposite; and four numbers anchored on files the merges touched
had drifted independently of #726.

- :1877-1878, :1883-1884 (#675) -- "carries the guarded ``if code in
  cls.__xxx__: warn(...)``" / "every sibling warns on mere presence" ->
  past tense, noting #726 later gave all seven the identity guard this
  entry's own comparison assumes they lack.
- :2100-2109 -- dropped the retained "yields two keys and never reaches
  one key twice" (false: ``Internet.register(TransType.TCP, TCP)`` warns
  once, incumbent.klass is TCP) and "leaves a different-class test
  undecidable" (contradicted by :2404-2406's own ``incumbent is not
  protocol`` definition); replaced with the actual false positive the
  guard has -- pre-seeded ``ModuleDescriptor`` incumbents never compare
  equal to the resolved class.
- :2192 -- reflowed the ``Option.register`` paragraph (orphan lines
  fixed alongside).
- :2387-2388 -- the ``Type[Dumper]`` quote now matches what is actually
  at line 424 (post-#709-fix), rather than the pre-fix bare form.
- :945 -- ``README.md`` (103) -> (102).
- :1136 -- "75 of the 117 modules" -> "77 ... after #647 below adds
  the same ending to three more" (drifted via #647, independent of the
  four merges).
- :2119 -- dropped the irreproducible pylint "364 messages" figure;
  kept mypy's 112, which does reproduce.
- :2119 -- "326 registry writes" -> 327 (``R1CounterParameter``'s
  second code, from #690).

Also fixed six false claims in the PR body (separate from the .rst):
hunk/line counts, six-commits -> 46, the 5-row table's implied total,
"not trimmed", main's red/green state, and the now-unreachable
cherry-pick target.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at 1749cc0 came back NEEDS CHANGES: round 13 fixed five of
the nine sites and introduced four new false claims doing it, including
two inside the flagship rewrite -- swapping one inaccuracy for another
is this document's recurring failure mode.

- :1136-37 -- "75 ... 77 now, after #647 ... adds ... three more" was
  internally inconsistent (75+3=78, not 77). Traced #647's own diff
  (fc32d1b): it adds ``_missing_`` to three IntFlag classes across
  only two *new* files -- ``tcp/flags.py`` and ``ftp/command.py`` --
  since the third, ``TransportProtocol``, shares ``reg/apptype.py``
  with the already-counted ``AppType``. Module delta is +2, matching
  75+2=77; reworded to say so.
- :1879-88 -- dropped "the comparison below assumes they still lack"
  it, which was false about text 8 lines below in the same diff
  (already past-tensed). Also reflowed three orphan lines this
  introduced (`passes, whereas`, `it twice with nothing`, `none of
  the`).
- :2107-19 -- "These tables also ship pre-seeded" over-generalised:
  verified live (``ProtocolBase.__proto__`` is 0 entries,
  ``Transport.__proto__ is ProtocolBase.__proto__`` -- True) that 2 of
  7 have nothing pre-seeded. Scoped to the five that do (Link 7,
  Internet 16, Frame 3, PCAPNG 3, SCTP 2). Also fixed "the guard
  resolves only the incoming class", which contradicts the guard's own
  docstring ("the comparison itself resolves nothing") -- resolution is
  the earlier ``isinstance(protocol, ModuleDescriptor)`` step, three
  lines above the guard, not something the guard does.
- :2129-30 -- dropped the invented "327th" ordinal (327 total stays;
  traced-write instrumentation via ``sys`` hooks found the seeding is
  literal dict construction, not ``.register()`` calls, so I could not
  reproduce an ordinal with confidence -- said "one of them" instead
  of guessing).
- :7-8 -- "between #326 and #509" now says the programme continued
  past it (verified: 193 distinct #nnn refs, max #726, 103 above 509).
- PR body -- "7 hunks, 1168+/11-" was the previous head's figure, not
  this one's; replaced with the actual command
  (``git diff --shortstat da697fa -- docs/source/changelog/1.5.0.rst``)
  and today's figure (9 hunks, 1152+/16-), since a hardcoded count here
  has now gone stale twice.

Left alone per this round's scope: :1969/:1974 (before/after claim,
not falsified by #726's later +1), mypy "112" (correct, re-ran with
the project's own flags), ":2122" 13-to-14 (correct at its delta
scope), and the other 121 cited paths (unaffected by main's one new
commit, #745, confirmed test-only).

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.

Cross-review at b2ac58b came back NEEDS CHANGES: round 15 fixed four
sites clean but swapped in two new inaccuracies, and left one
round-fourteen defect (body :26) unfixed.

- :7 -- "past #726" was wrong direction: #726 is the max ref in the
  document (193 distinct, min #251, max #726), not one exceeded ->
  "reaching #726".
- :2110-19 -- "ProtocolBase and Transport share one dict, starting and
  staying empty until a subclass registers" was false three ways,
  verified live against origin/main (pcapkit.__file__ asserted):
  Transport.register() itself raises UnsupportedCall (abstract); TCP
  and UDP keep their own separate __proto__ (4 and 3 entries), not the
  shared one, so registering on them leaves the shared dict at 0; only
  a direct ProtocolBase.register() call fills it. Narrowing to "five
  of these seven" also hid that TCP/UDP are pre-seeded too, which is
  exactly where the false positive bites in the transport family --
  restored that.
- body :26 -- "26 entry commits" -> 27 (commits whose subject starts
  "docs(changelog): the 1.5.0 entry/entries for", verified by grep),
  28 bullets added and 0 removed (verified via the .rst diff against
  da697fa; one commit, 6a956c4, adds two bullets for #648/#649).
- body :41 -- dropped the hardcoded "9 hunks, 1152+/16-" figure
  entirely (it had already drifted to 1154+ by the time of this
  commit) and named the second command needed for the hunk count,
  since --shortstat cannot print one.

On the ordinal question raised last round: dropping it was still right
(the asserted "327th" was wrong), but "no ordinal is derivable" does
not hold either -- the writes are at pcapkit/protocols/schema/schema.py,
not the 8 dict-literal registrar sites my instrumentation covered, and
they are traceable. Left the text as "one of them being
R1CounterParameter's second code" (no ordinal asserted, no false
derivability claim either) rather than reopen a site outside this
round's scope.

Regenerated CHANGELOG.md again. changelog_md.py --check: exit 0. pytest
tests/project/test_changelog_md.py -q: 47 passed, 37 subtests.
JarryShaw added a commit that referenced this pull request Sep 29, 2026
…#900)

- Turned the toctree-only changelog.rst index into a summary list: each of
  the 37 entries gets a one- or two-line summary drawn from its own page
  under changelog/, followed by :doc:`Full changelog <changelog/X.Y.Z>`.
- The toctree itself moved to the bottom and gained :hidden: -- it is still
  the newest-first ordering util/changelog_md.py reads to pick the release
  CHANGELOG.md is generated from, just no longer the page's own rendering.
- No entry under docs/source/changelog/ changes, and changelog.rst itself
  ships in neither the sdist nor CHANGELOG.md (MANIFEST.in prunes docs/ and
  re-adds only changelog/*.rst), so nothing shipped moves.
- Cross-review fixes: dropped the 1.5.0 bullet's "~140 issues/PRs between
  #326 and #509" claim (wrong count, root cause is #657's own prose, raised
  there separately); dropped "silently" from 1.3.5, which the page itself
  contradicts; named the three workflows 1.4.1's unit-test CI gates instead
  of "every release pipeline"; reworded 0.15.5 for readability.
- Added RepositoryStateTests.test_index_summary_bullets_track_each_pages_own_heading
  in tests/project/test_changelog_md.py: asserts the summary bullets' order
  matches read_toctree() and each bullet's (version, date) matches that
  page's own heading -- so 1.5.0 shipping with a real date and a stale
  "(unreleased)" bullet fails the suite instead of drifting silently.
  Verified failing against the pre-#909 toctree-only file (AssertionError
  naming all 37 missing bullets) before this commit added the bullets.

Build/test: util/changelog_md.py --check exits 0; tests/project/
test_changelog_md.py passes (48 tests, 37 subtests); a Sphinx build into a
fresh BUILDDIR reports Sphinx's own "42 warnings" summary line identically
before and after, sorted WARNING lines byte-identical apart from
timestamps/order on 4 unrelated dependency-check lines.
JarryShaw added a commit that referenced this pull request Sep 29, 2026
The paragraph claimed the defect programme ran "across some 140 issues and
pull requests between #326 and #509" and reached "#805 by the entries below".
Both are wrong, and re-measuring on this revision gives:

* 254 distinct issue/PR references in the entries, not ~140.
* 87 of them fall in [#326, #509]; 164 are above #509 and 3 below #326.
* The highest is #883, not #805.

The range framing was the root cause -- it encoded the window the programme
opened on as though it were its extent, so every merge since made it more
wrong. Replaced with a snapshot that says it is one, and regenerated
CHANGELOG.md so the two stay in step (`util/changelog_md.py --check` exits 0).
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
@JarryShaw JarryShaw moved this to Done in PyPCAPKit Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Restructuring for its own sake — neither a fix nor a new capability (refactor: prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Duplicated wire-unit arithmetic wants a shared module: 25 MH sites, 126 option sites, and two near-identical option files

1 participant