Skip to content

feat(reg)!: split AppType into per-transport registries, dropping the portless rows - #754

Merged
JarryShaw merged 1 commit into
mainfrom
worktree-agent-ae72eea7a10762d80
Sep 24, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
worktree-agent-ae72eea7a10762d80

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

What is the purpose of your pull request?

  • fix — corrects a defect
  • feat — adds a feature
  • perf — changes performance, not behaviour
  • refactor — changes neither behaviour nor performance
  • test — tests only
  • docs — documentation only
  • ci — workflows or build tooling
  • chore — anything else

Description of your pull request and other information

Part of #732. pcapkit/{const,vendor}/reg/apptype.py become packages: a memberless AppType base plus TCP/UDP/SCTP/DCCP. aenum refuses to subclass an enumeration that has members, so the base holds none — and isinstance(TCP.http, AppType) still holds, so nothing under pcapkit/protocols/ changes.

before after
members 8,182, one class 6,147 TCP / 6,143 UDP / 91 SCTP / 10 DCCP = 12,391
not members — 1,004 portless + 704 transportless rows, as list-table docstrings (704/366/122/2/1 rows, verified rendering)
port lookup __members_proto__; last row won; extend_enum displaced it per-registry MultiDict; canonical member; extend_enum appends
  • Member shape — unique non-port _value_ with .port/__int__, per the ruling that int-parity is droppable. A plain IntEnum cannot hold two members on port 80: list() is 1 and www.name reads 'http'.
  • Canonical + aliases — get(port) answers with the /etc/services name (80→http, 1701→l2tp, 2049→nfs, 3478→stun, 113→**auth**, not ident); get_all(port) returns it plus its aliases. Row order answers only 28 of the 44 colliding pairs that way, so __canonical__ is curated and an uncurated collision warns.
  • Behaviour diff, measured — 12,391 member-port lookups against the pre-split module: 28 differ, all curated collisions, 0 otherwise. Separately and by ruling, the 124 real-port rows IANA gives no transport lose the __members_proto__[undefined] fallback: get(51, proto=tcp) mints rather than answering reserved.
  • breaking: yes — APPTYPE is public (pcapkit/__init__.py:107) and AppType.<member> no longer resolves; 35 AppType_* members become TCP_*/UDP_*/SCTP_*.
  • docs: full Sphinx build hangs in typing.get_type_hints() while documenting pcapkit/const/reg #744, measured here for the first time — one-file sphinx-build -b dummy over these autoclasses: 680.3 s → 759.1 s (1.12×). The split worsens the get_type_hints cost by 11.6%; it does not relieve it. Identical 6 warnings before and after.
  • Left out deliberately — the .get() sentinel/warning spec; docs: bare 'dict' in :type: fields resolves to AppType.dict, an IANA service enum member, not the builtin #717's :no-typesetting: stub; _make_port's deletion (its four files belong to another PR in flight); pep.rst's "out of 8182"; enum-keying __proto__. get(<str>) is dropped, not fixed — dead today and minting port=-1 junk; AppType.get(name) never matches an existing member and silently mints a port=-1 member instead #734 stays open.
  • Open, and not blocking — whether TransportProtocol stops being a Flag. 10,625 of the 12,391 declarations carry a multi-transport .proto, but regenerating them is one crawler command, so this PR need not wait.

Tests run (not make test — the full suite is out of budget here): tests/const 52, tests/vendor 59, tests/protocols/transport+tests/dumpkit 164, tests/project+toolkit+corekit+utilities 487 (9 skipped), tests/foundation/registry+test_protocol_code_registration_unit 29. All passing. tests/const/test_const_apptype_split_unit.py is new: 12 tests, all 12 shown failing against 5c0df9248.

@JarryShaw JarryShaw added feat Pull requests that add a new capability (feat: subject prefix) breaking Breaks public-facing behaviour or API (apply alongside the type label) review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 24, 2026
Comment thread pcapkit/vendor/reg/apptype/apptype.py Outdated
'''.strip() # type: Callable[[str, str, str, str, str, str, str], str]


class Record:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be an Info class or dataclasses.dataclass.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — Info, since it is the house idiom and it fits.

The one snag is that Info.__setattr__ raises UnsupportedCall, and the record is
accumulated across several CSV rows. Rather than reach for a mutable dataclass I made
every growing field a container appended to in place, which Info permits. That turned out
to be the better shape anyway: the #: comment used to be accumulated by string surgery
(if cmmt.startswith('-')), re-deriving from its own output whether it had already been
merged. It is now parts: list[str], one wrapped comment per registry row, joined at
emission — so the startswith branch is gone.

Verified the refactor changes no generated output: tcp.py, udp.py, sctp.py and
dccp.py are byte-identical before and after.

return extend_enum(TransportProtocol, key.lower(), max_val * 2)

@classmethod
def _missing_(cls, value: 'int') -> 'TransportProtocol':

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think we need _missing_ for TransportProtocol. anything not existing should not be accepted.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one is backwards, and I have left it in place — please overrule me if so.

_missing_ is what implements "anything not existing should not be accepted". aenum on
its own is permissive. Measured on aenum 3.1.17, same class body with the method removed:

TransportProtocol(-1) -> <tcp|udp|sctp|dccp: 15>     # #647's recorded defect, verbatim
TransportProtocol(16) -> <TransportProtocol: 16>     # accepted, .name is None
TransportProtocol(32) -> accepted

versus, with it:

TransportProtocol(-1) / (16) / (32) -> ValueError
TransportProtocol(3) / (15)          -> tcp|udp / tcp|udp|sctp|dccp   (composition kept)

So removing it widens what is accepted, and fails three existing tests:
test_every_registry_rejects_a_negative_value, and TransportProtocol(0x10) / (0x20)
raising in test_the_other_two_flag_registries_compose and
test_transport_protocol_can_still_be_extended_at_runtime. ISSUE_647_OUTLIERS names this
class with exactly the symptom above. I have added a Note: to the docstring recording the
measurement, so the next reader does not have to ask.

Note the guard is not itself permissive: 0 <= value <= max(members) * 2 - 1 is precisely
"no undeclared bits", and it tracks extend_enum.

On get() minting (:143, one line up) — you are right that it is the same question, and
I read the two paths as genuinely different rather than inconsistent:

  • TransportProtocol(value) is the constructor over a closed space. The declared bits are
    all the bits there are, so an undeclared bit is unanswerable and must be rejected.
  • TransportProtocol.get(name) is fed IANA's transport column verbatim by the crawler, and
    that space is open — it has carried ddp, and a new protocol appearing there should grow
    the registry rather than crash the crawl. That is the same extend_enum growth policy the
    library's other registries have, and it is pinned by
    test_transport_protocol_can_still_be_extended_at_runtime.

If you want the string path to reject too, say so and I will make it raise and re-pin that
test — it is a one-line change, but it is a behaviour change to a shared registry that this PR
otherwise only moves, so I have not taken it unasked.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No only the specified protocols are accepted.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recorded: only the specified protocols are accepted. That overrules the author, and I think it also
settles the Flag → Enum question you were weighing — the two are the same decision.

Measured on 73f09aecb: AppType's 8,182 members carry 11 distinct .proto values — 0, 1, 2, 3, 4, 5, 7, 8, 11, 12, 15. Five are declared members (undefined/tcp/udp/sctp/dccp = 0/1/2/4/8). The
other six — 3, 5, 7, 11, 12, 15 — are compositions, and they cover 5,354 members, 65% of the registry.

So "only the specified" forces a choice, and only one branch is viable:

  • Reject compositions outright → 5,354 members cannot express their IANA transport at all. Not viable.
  • Declare the compositions → the six observed combinations become named members alongside the five
    singles, 11 in total, and then "only the specified are accepted" is exactly true with nothing lost.
    This is the Enum move, and it is why the sparse-and-closed measurement matters: 11 of 16 occur, so
    naming them is finite and complete.

Under that shape _missing_ is no longer load-bearing for rejection — the member list is the whitelist,
and an undeclared value has no member to resolve to. The author's measurement stands on today's IntFlag
(removing _missing_ there widens acceptance, since aenum turns -1 into all-bits-set — #647's recorded
defect), but it stops applying once compositions are named rather than computed. So the author was right
about the code as it is, and you are right about where it should go.

One consequence to accept deliberately: tcp | udp as an expression stops working, because there is no
bitwise composition to perform. Anything that builds a transport by OR-ing — including
register_apptype's fan-out — moves to an explicit ~11-entry membership map. The author enumerated those
sites; none is in this PR.

This is bigger than #754 and I am not folding it in. #754 moves AppType to a package; retyping
TransportProtocol changes a shared registry's public semantics. Say the word and I will file it as its own
issue with the 11 combinations and the migration sites enumerated, so #754 can merge on its current
behaviour-preserving footing.

… portless rows

- pcapkit/{const,vendor}/reg/apptype.py become packages: a memberless AppType
  base plus one registry per transport protocol (TCP, UDP, SCTP, DCCP). aenum
  refuses to subclass an enumeration that has members, so the base holds none;
  isinstance(TCP.http, AppType) still holds, preserving all 7 isinstance sites.
- The 1,004 rows IANA assigns no port, and the 704 it assigns no transport
  protocol, stop being members: they become list-table docstrings on the registry
  that owns them, which autodoc publishes.
- __members_proto__ becomes a per-registry MultiDict keyed on port, so all three
  services IANA registers on port 80 survive. extend_enum on an occupied port
  appends instead of displacing what a lookup returned.
- get() answers with the canonical service, curated from /etc/services because
  IANA names no precedence among them; get_all() reaches the aliases. get()'s dead
  str branch is removed rather than carried over.
- AppType.get(port, proto=...) proxies to the registry proto names, so nothing
  under pcapkit/protocols/ changes.

Re-pinned the const sweep to 9 string registries / 127 classes / 121 modules, and
extended the _dest_path sweep over the nested crawlers. tests/const 52,
tests/vendor + registry 88, tests/protocols/transport + dumpkit 164, all passing.
@JarryShaw
JarryShaw force-pushed the worktree-agent-ae72eea7a10762d80 branch from ea18db0 to 8744aa6 Compare September 24, 2026 21:25
@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 24, 2026
@JarryShaw JarryShaw added review: pending No verdict for the current head - never reviewed, or the head moved since the last one review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: good-to-go Cross-review at the current head says ready; CI state is separate review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 24, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE @ 8744aa6cf (base main = 73f09aecb)

Third independent cross-review (different model), landing after two others above already covered ea18db096 in detail. This one specifically re-verifies the follow-up commit that moved the head ea18db096 → 8744aa6cf after those were posted.

The follow-up (2 files, +61/-31) is documentation + crawler-refactor only, not a behaviour change:

  • pcapkit/const/reg/apptype/apptype.py (+ vendor mirror): adds a Raises/Note docstring to TransportProtocol._missing_ explaining GitHub issue the generated enum guards raise a bare ValueError in 113 of 117 const modules, and tcp.flags.Flags has no guard at all #647's guard — measured: without it, aenum returns tcp|udp|sctp|dccp for TransportProtocol(-1) and a nameless member for TransportProtocol(16). The guard's code is unchanged, only documented.
  • pcapkit/vendor/reg/apptype/apptype.py: Record moves from a hand-rolled __init__ to @info_final class Record(Info) (house convention), and multi-row comment accumulation moves from string-surgery (cmmt.startswith('-') re-inspection of its own prior output) to a parts: list[str] join — a crawler-only robustness fix; same rendered output for well-formed input.

Re-ran the relevant suite fresh at 8744aa6cf: test_const_apptype_split_unit.py + test_const_enum_builtin_parity.py + test_const_enum_get.py + tests/vendor/ → 98 passed, 881 subtests passed. Member counts (12,391 = 6,147/6,143/91/10), docstring rows (704/366/122/2/1), and the 28-difference safety claim were independently re-derived from scratch against ea18db096 (matching the two reviews above); nothing in this follow-up touches registry data, get(), or extend_enum semantics, so that verdict carries forward unchanged to the current head.

No new findings beyond the five wording corrections already flagged above (33 renamed + 2 removed, not 35; the /etc/services TCP/9100 nuance; the test-tally wording; _make_port's undisclosed new dependency on the backport get(port, proto=); three unnamed caller-visible breaks) — all wording/disclosure, no code defect.

✅ GOOD TO MERGE @ 8744aa6cf

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE @ 8744aa6cf — follow-up since ea18db096 is docs + crawler-refactor only (no registry/behaviour change); full suite re-run clean (98 passed / 881 subtests); verdict from the two reviews above carries forward unchanged.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 24, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE @ 8744aa6cf (supersedes my ✅ at ea18db096; base main = 73f09aecb)

Housekeeping: the 4 duplicate "@ ea18db0" comments above are stale — posted in error by two of my own subagents that overstepped their briefs. Disregard them; this is the current, authoritative verdict.

Re-reviewed the 2-file diff only (+61/-31): const/apptype.py (+15) and vendor/apptype.py (77-changed), per the two review threads.

item result
Record→Info, byte-identity of tcp/udp/sctp/dccp.py ✅ verified myself — ran pcapkit-vendor reg.apptype against live IANA at this head; md5sum identical on all 5 files, even the module:: header
Const-side +15 lines read directly: pure Raises:/Note: docstring on TransportProtocol._missing_, no table/member changes — the 28-diff, member counts and pinned sweeps stand unchanged from ea18db096
_missing_ 3-test claim confirmed by reading all three: test_every_registry_rejects_a_negative_value (builtin_parity.py:231), test_the_other_two_flag_registries_compose, test_transport_protocol_can_still_be_extended_at_runtime — all assert TransportProtocol(0x10)/(0x20)→ValueError and would break
mypy / isort ✅ re-ran, clean
tests ✅ ran myself — tests/const+tests/vendor: 111 passed/1015 subtests; tests/foundation/registry+test_protocol_code_registration_unit.py: 34 passed/95 subtests (body says 29 — minor miscount; that file lives under tests/protocols/)
CI settled: 20 pass / 3 skip (gated) / 0 fail

_missing_ disagreement — I side with the author. ISSUE_647_OUTLIERS independently names TransportProtocol for exactly the defect removing it reintroduces (-1→tcp\|udp\|sctp\|dccp). The closed-constructor vs. open-crawler-name distinction is a real technical fact, not rationalization: implementing the ask literally would widen acceptance, the opposite of its stated intent. Maintainer's call either way.

Commit message — leave it out. The byte-identity claim is durably recorded in the thread reply and here; a purely internal, output-neutral refactor is noise in main's history.

Threads stay open for the maintainer to resolve — required_review_thread_resolution blocks merge regardless of this verdict.

@JarryShaw

Copy link
Copy Markdown
Owner Author

✅ GOOD TO MERGE @ 8744aa6cf (supersedes ✅ at ea18db096; good for main=73f09aecb) — byte-identity, 3-test _missing_ claim and CI (20 pass/0 fail) independently verified; disregard the 4 stale duplicate comments above; threads stay open for the maintainer.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Housekeeping addendum: this thread now has 8 automated review comments from 3 independent passes (mine, plus two subagents that posted despite being told not to — a coordination failure on my side, not a technical disagreement). All 4 comments at 8744aa6cf agree: GOOD TO MERGE, main=73f09aecb. Label is stable at feat, breaking, review: good-to-go. No further comments or label changes coming from this review.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Follow-up, and fairly strongly — the retype would blind this PR's own safety argument

#754's whole safety case is the behaviour comparison: 12,391 member-port lookups against pre-split, 28
differing, all of them the curated /etc/services collisions, 0 otherwise.
That argument only works
because .proto values are unchanged — the diff is structural. Retyping TransportProtocol in the same
PR changes .proto on 5,354 members, and then a reviewer cannot tell whether a difference came from the
package split or from the retype. The one measurement that makes a 56k-line diff reviewable stops being
interpretable.

Three more reasons, in order of weight:

And deferring is cheap, which is the deciding fact. The author measured regeneration as one crawler
command, ~4 s, concentrated in a single 6-line method
(AppType.flag). So landing #754 now and retyping
later costs one regeneration, not a rewrite — my earlier "66k lines to regenerate" was too pessimistic and I
have already retracted it.

So: merge #754 as it stands, and I will file the retype with the 11 named combinations and the migration
sites enumerated.
The only thing still blocking is the unresolved review thread — the gate has
required_review_thread_resolution = true, so it needs resolving even though the outcome there is settled:
the author was right about today's IntFlag, you are right about the destination.

One caveat so the deferral is not silently permanent: until the retype lands, TransportProtocol still
accepts computed compositions, so "only the specified protocols are accepted" is not yet true. That is a
known gap with an owner, not an oversight.

@JarryShaw
JarryShaw merged commit 932cb48 into main Sep 24, 2026
24 checks passed
@JarryShaw
JarryShaw deleted the worktree-agent-ae72eea7a10762d80 branch September 24, 2026 22:49
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Sep 24, 2026
JarryShaw added a commit that referenced this pull request Sep 24, 2026
- `str | bytes | dict[...]` and three `dict[str, ModuleDescriptor[...] | ...]`
  fields resolved against `AppType`'s (now `TCP`'s) IANA service member
  named `dict`, not the builtin, because `PyXrefMixin.make_xref` sets
  `refspecific=True` unconditionally for hand-written `:type:` fields.
- Qualified each as `python:dict`, the intersphinx explicit-inventory form:
  it fails the project's own domain lookup (no local object contains a
  colon) so `resolve_xref` returns `None`, and intersphinx's
  `resolve_reference_detect_inventory` then splits on the colon and
  resolves `dict` against the configured `python` inventory. Renders as
  plain `dict`, no warning.
- Left `exec` alone: swept all 64 `:type:` lines and every `:type`/`:vartype`
  docstring field in `pcapkit/`, and it appears nowhere as a bare
  reference, so it has no live wrong link to fix today (it stays a latent
  registry-level collision, same class, different member).

Verified by reading the installed Sphinx 9.1.0's resolution path and by a
minimal standalone Sphinx project in /tmp reproducing the TCP/UDP `dict`
ambiguity against a local intersphinx inventory: bare `dict` reproduced
the "more than one target" warning and linked to the wrong member;
`python:dict` resolved externally to the stub builtin with no warning.
No full docs build was run (~759s post-#754, tracked in #744).

Fixes #717.
JarryShaw added a commit that referenced this pull request Sep 25, 2026
…e stale pylint flags

Makefile:126's isort glob (`pcapkit/{const,vendor}/*/*.py`) stopped at a fixed
two levels, so #754's `pcapkit/const/reg/apptype/` package landed its
`__init__.py` a level deeper than either that line or line 125's
`--skip-glob '**/__init__.py'` reached -- sorted by no `make isort` line at
all. Widen it to `*/**/*.py` under `shopt -s globstar`, reaching any depth
below the first subdirectory while still skipping `pcapkit/const/__init__.py`
and `pcapkit/vendor/__init__.py` themselves, whose hand-grouped import order
isort would otherwise flatten. `tests/project/test_isort_clean.py`'s mirror of
that line is updated the same way. The identical two-level glob also lived in
`.github/workflows/cron-vendor.yml`, on the path that actually regenerates the
deep files; fixed there too, with `find -mindepth 2` rather than
`shopt -s globstar`, since that job's macOS runner does not provably have a
globstar-capable `bash` on PATH the way the Makefile's own resolution does.

`PYLINT_FLAGS` loaded `pylint.extensions.emptystring`, which pylint removed at
3.0 (`E0013 bad-plugin-value` every run), and named four `--disable=` checks
pylint has since removed (`old-division`, `no-absolute-import`,
`input-builtin`) or left unloaded (`eq-without-hash`, live in
`pylint.extensions.eq_without_hash` but never in `--load-plugins=`) -- five
stale entries, dropped. This is flag hygiene, not enablement: `E0013` did not
gate `compare-to-empty-string`, which reaches the core `refactoring` checker's
`use-implicit-booleaness-not-comparison-to-string` (C1804) via an `old_names`
alias regardless of the plugin's own load failure, confirmed with
`--list-msgs-enabled` (identical, 399 lines, before and after). `lint.yml`'s
header is corrected to match and re-pinned to 1a85269, since `pcapkit/` had
in fact moved from the previous pin (932cb48) in four files, even though
E/W/C read the same on both.

Fixes #765.
Fixes #767.

Build: `make isort` clean on all three lines; pylint re-measured once, output
redirected to a file and parsed (E 90, W 4765, C 542, R 648, exit 30).
JarryShaw added a commit that referenced this pull request Sep 25, 2026
…e stale pylint flags

Makefile:126's isort glob (`pcapkit/{const,vendor}/*/*.py`) stopped at a fixed
two levels, so #754's `pcapkit/const/reg/apptype/` package landed its
`__init__.py` a level deeper than either that line or line 125's
`--skip-glob '**/__init__.py'` reached -- sorted by no `make isort` line at
all. A first pass here widened it to `*/**/*.py` under `shopt -s globstar`,
which needs bash >= 4.0 and is not what the Makefile's own
`SHELL := $(shell command -v bash ...)` guarantees on a machine without a
brew bash ahead of macOS's system 3.2 -- silently: `shopt` on such a machine
just prints one stderr line and the next command still runs, under the old,
non-recursive glob, reopening the #765 hole one level up. Line 126 is now
`$(find pcapkit/const -mindepth 2 -name '*.py') $(find pcapkit/vendor
-mindepth 2 -name '*.py')` instead, needing no particular bash and no shell
option at all, with the same fix applied the same way in
`.github/workflows/cron-vendor.yml`, the one other place this glob lived, on
the path that actually regenerates the deep files.
`tests/project/test_isort_clean.py`'s mirror of line 126 is updated to match.

`PYLINT_FLAGS` loaded `pylint.extensions.emptystring`, which pylint removed at
3.0 (`E0013 bad-plugin-value` every run), and named four `--disable=` checks
pylint has since removed (`old-division`, `no-absolute-import`,
`input-builtin`) or left unloaded (`eq-without-hash`, live in
`pylint.extensions.eq_without_hash` but never in `--load-plugins=`) -- five
stale entries, dropped. This is flag hygiene, not enablement: `E0013` did not
gate `compare-to-empty-string`, which reaches the core `refactoring` checker's
`use-implicit-booleaness-not-comparison-to-string` (C1804) via an `old_names`
alias regardless of the plugin's own load failure, confirmed with
`--list-msgs-enabled` (identical, 399 lines, before and after). `lint.yml`'s
header is corrected to match, re-pinned to 1a85269 since `pcapkit/` had in
fact moved from the previous pin (932cb48) in four files even though E/W/C
read the same on both, and its R/total range is restated on a consistent
basis (the old range's own figures included the five stale messages too).

Fixes #765.
Fixes #767.

Build: `make isort` clean on all three lines; pylint re-measured once, output
redirected to a file and parsed (E 90, W 4765, C 542, R 648, exit 30).
JarryShaw added a commit that referenced this pull request Sep 25, 2026
…e stale pylint flags

Makefile:126's isort glob (`pcapkit/{const,vendor}/*/*.py`) stopped at a fixed
two levels, so #754's `pcapkit/const/reg/apptype/` package landed its
`__init__.py` a level deeper than either that line or line 125's
`--skip-glob '**/__init__.py'` reached -- sorted by no `make isort` line at
all. A first pass here widened it to `*/**/*.py` under `shopt -s globstar`,
which needs bash >= 4.0 and is not what the Makefile's own
`SHELL := $(shell command -v bash ...)` guarantees on a machine without a
brew bash ahead of macOS's system 3.2 -- silently: `shopt` on such a machine
just prints one stderr line and the next command still runs, under the old,
non-recursive glob, sorting none of the 32 depth-two `__init__.py` files it
used to cover (the two three levels down under `.../reg/apptype/` stay
reachable either way) -- the hole reopened one level up. A second pass
replaced that with a single `isort` invocation fed by
`$(find pcapkit/const -mindepth 2 -name '*.py')` and the `vendor` equivalent,
needing no particular bash and no shell option at all, but merging both
`find`s into one invocation meant only a fully-empty expansion would fail
loudly; a partially-empty one would exit 0 having quietly skipped half the
tree. Unreachable today -- neither directory can be absent where
`make isort` runs, and no path under either contains whitespace -- but the
same shape of latent gap as the original bug, so line 126 is now split into
two calls, one per directory, matching `.github/workflows/cron-vendor.yml`'s
own two calls exactly (the one other place this glob lived, on the path that
actually regenerates the deep files, fixed the same way for the same reason).
`tests/project/test_isort_clean.py` is restructured for the same four lines,
and the Makefile's own bash pin is re-justified: brace expansion, its
original reason, is gone from the file entirely.

`PYLINT_FLAGS` loaded `pylint.extensions.emptystring`, which pylint removed at
3.0 (`E0013 bad-plugin-value` every run), and named four `--disable=` checks
pylint has since removed (`old-division`, `no-absolute-import`,
`input-builtin`) or left unloaded (`eq-without-hash`, live in
`pylint.extensions.eq_without_hash` but never in `--load-plugins=`) -- five
stale entries, dropped. This is flag hygiene, not enablement: `E0013` did not
gate `compare-to-empty-string`, which reaches the core `refactoring` checker's
`use-implicit-booleaness-not-comparison-to-string` (C1804) via an `old_names`
alias regardless of the plugin's own load failure, confirmed with
`--list-msgs-enabled` (identical, 399 lines, before and after). `lint.yml`'s
header is corrected to match and re-pinned to 1a85269, since `pcapkit/` had
in fact moved from the previous pin (932cb48) in four files even though
E/W/C read the same on both; its R/total range is restated on a consistent
basis and labelled with the tree it was measured on.

Fixes #765.
Fixes #767.

Build: `make isort` clean on all four lines; pylint re-measured once last
round, output redirected to a file and parsed (E 90, W 4765, C 542, R 648,
exit 30) -- unchanged this round, no pcapkit/ file touched.
JarryShaw added a commit that referenced this pull request Sep 25, 2026
…e stale pylint flags

Makefile:126's isort glob (`pcapkit/{const,vendor}/*/*.py`) stopped at a fixed
two levels, so #754's `pcapkit/const/reg/apptype/` package landed its
`__init__.py` a level deeper than either that line or line 125's
`--skip-glob '**/__init__.py'` reached -- sorted by no `make isort` line at
all. A first pass here widened it to `*/**/*.py` under `shopt -s globstar`,
which needs bash >= 4.0 and is not what the Makefile's own
`SHELL := $(shell command -v bash ...)` guarantees on a machine without a
brew bash ahead of macOS's system 3.2 -- silently: `shopt` on such a machine
just prints one stderr line and the next command still runs, under the old,
non-recursive glob, sorting none of the 32 depth-two `__init__.py` files it
used to cover (the two three levels down under `.../reg/apptype/` stay
reachable either way) -- the hole reopened one level up. A second pass
replaced that with a single `isort` invocation fed by
`$(find pcapkit/const -mindepth 2 -name '*.py')` and the `vendor` equivalent,
needing no particular bash and no shell option at all, but merging both
`find`s into one invocation meant only a fully-empty expansion would fail
loudly; a partially-empty one would exit 0 having quietly skipped half the
tree. Unreachable today -- neither directory can be absent where
`make isort` runs, and no path under either contains whitespace -- but the
same shape of latent gap as the original bug, so the const/vendor line is now
split into two calls, one per directory, matching
`.github/workflows/cron-vendor.yml`'s own two calls exactly (the one other
place this glob lived, on the path that actually regenerates the deep files,
fixed the same way for the same reason). The Makefile's own bash pin is
re-justified: brace expansion, its original reason, is gone from the file
entirely.

`tests/project/test_isort_clean.py` is restructured for the resulting four
recipe lines, and its own naming is fixed along with it: it used to key its
target-resolution functions on line numbers (`_line_125_targets` and so on),
which is exactly the failure this file exists to catch, and expanding the
Makefile's bash-pin comment above the `isort:` target this same round moved
the real lines to 133-136 while the names still said 125-128 -- caught by a
reviewer's measurement, not by the test. Functions are now named for what
they target (`_pcapkit_tree_targets`, `_const_targets`, `_vendor_targets`,
`_util_examples_targets`), `MAKEFILE_LINES` is a plain list paired with line
numbers positionally rather than keyed by one, and a new
`_isort_recipe_line_numbers()` reads `Makefile` and returns those numbers
fresh every run -- verified by shifting the target down a line in a scratch
copy and confirming the resolver's output shifts with it (132->133,
133-136->134-137). Every prose reference to a specific line number in the
module docstring is swept to a descriptive name for the same reason.

`PYLINT_FLAGS` loaded `pylint.extensions.emptystring`, which pylint removed at
3.0 (`E0013 bad-plugin-value` every run), and named four `--disable=` checks
pylint has since removed (`old-division`, `no-absolute-import`,
`input-builtin`) or left unloaded (`eq-without-hash`, live in
`pylint.extensions.eq_without_hash` but never in `--load-plugins=`) -- five
stale entries, dropped. This is flag hygiene, not enablement: `E0013` did not
gate `compare-to-empty-string`, which reaches the core `refactoring` checker's
`use-implicit-booleaness-not-comparison-to-string` (C1804) via an `old_names`
alias regardless of the plugin's own load failure, confirmed with
`--list-msgs-enabled` (identical, 399 lines, before and after). `lint.yml`'s
header is corrected to match and re-pinned to 1a85269, since `pcapkit/` had
in fact moved from the previous pin (932cb48) in four files even though
E/W/C read the same on both; its R/total range is restated on a consistent
basis and labelled with the tree it was measured on.

Fixes #765.
Fixes #767.

Build: `make isort` clean on all four lines; pylint re-measured once several
rounds back, output redirected to a file and parsed (E 90, W 4765, C 542,
R 648, exit 30) -- unchanged since, no pcapkit/ file touched.
JarryShaw added a commit that referenced this pull request Sep 25, 2026
…e stale pylint flags

Makefile:126's isort glob (`pcapkit/{const,vendor}/*/*.py`) stopped at a fixed
two levels, so #754's `pcapkit/const/reg/apptype/` package landed its
`__init__.py` a level deeper than either that line or line 125's
`--skip-glob '**/__init__.py'` reached -- sorted by no `make isort` line at
all. A first pass here widened it to `*/**/*.py` under `shopt -s globstar`,
which needs bash >= 4.0 and is not what the Makefile's own
`SHELL := $(shell command -v bash ...)` guarantees on a machine without a
brew bash ahead of macOS's system 3.2 -- silently: `shopt` on such a machine
just prints one stderr line and the next command still runs, under the old,
non-recursive glob, sorting none of the 32 depth-two `__init__.py` files it
used to cover (the two three levels down under `.../reg/apptype/` stay
reachable either way) -- the hole reopened one level up. A second pass
replaced that with a single `isort` invocation fed by
`$(find pcapkit/const -mindepth 2 -name '*.py')` and the `vendor` equivalent,
needing no particular bash and no shell option at all, but merging both
`find`s into one invocation meant only a fully-empty expansion would fail
loudly; a partially-empty one would exit 0 having quietly skipped half the
tree. Unreachable today -- neither directory can be absent where
`make isort` runs, and no path under either contains whitespace -- but the
same shape of latent gap as the original bug, so the const/vendor line is now
split into two calls, one per directory, matching
`.github/workflows/cron-vendor.yml`'s own two calls exactly (the one other
place this glob lived, on the path that actually regenerates the deep files,
fixed the same way for the same reason). The Makefile's own bash pin is
re-justified: brace expansion, its original reason, is gone from the file
entirely.

`tests/project/test_isort_clean.py` is restructured for the resulting four
recipe lines, and its own naming is fixed along with it: it used to key its
target-resolution functions on line numbers (`_line_125_targets` and so on),
which is exactly the failure this file exists to catch, and expanding the
Makefile's bash-pin comment above the `isort:` target this same round moved
the real lines to 133-136 while the names still said 125-128 -- caught by a
reviewer's measurement, not by the test. Functions are now named for what
they target (`_pcapkit_tree_targets`, `_const_targets`, `_vendor_targets`,
`_util_examples_targets`), `MAKEFILE_LINES` is a plain list paired with line
numbers positionally rather than keyed by one, and a new
`_isort_recipe_line_numbers()` reads `Makefile` and returns those numbers
fresh every run -- verified by shifting the target down a line in a scratch
copy and confirming the resolver's output shifts with it (132->133,
133-136->134-137). Every prose reference to a specific line number in the
module docstring is swept to a descriptive name for the same reason.

`_isort_recipe_line_numbers()` itself needed one more fix: it broke on the
first line that was not tab-indented, which is not what ends a recipe as far
as `make` is concerned -- a blank line or a column-0 `#` comment inside the
recipe is transparent to `make` (measured with `make -n isort` as the
oracle: still four commands with either dropped between two recipe lines),
and the resolver undercounted both, silently, in exactly the way this PR
exists to stop. It now treats those two as transparent too. Documented, not
defended against: a `define`/`endef` block containing a bare `isort:` would
still fool it -- nothing in this Makefile uses `define`. The `MAKEFILE_LINES`
comment is reworded to separate why the list has to match the Makefile
(the "clean locally, red in CI" risk) from what actually checks that it does
(a length comparison, nothing about the flags or targets themselves), and to
note the asymmetry that bounds all of this: flags and targets are literals
in `MAKEFILE_LINES`, never read back out of the Makefile, so a line-number
bug can only mislabel where a failure points, never change which isort
invocation the test actually runs.

`PYLINT_FLAGS` loaded `pylint.extensions.emptystring`, which pylint removed at
3.0 (`E0013 bad-plugin-value` every run), and named four `--disable=` checks
pylint has since removed (`old-division`, `no-absolute-import`,
`input-builtin`) or left unloaded (`eq-without-hash`, live in
`pylint.extensions.eq_without_hash` but never in `--load-plugins=`) -- five
stale entries, dropped. This is flag hygiene, not enablement: `E0013` did not
gate `compare-to-empty-string`, which reaches the core `refactoring` checker's
`use-implicit-booleaness-not-comparison-to-string` (C1804) via an `old_names`
alias regardless of the plugin's own load failure, confirmed with
`--list-msgs-enabled` (identical, 399 lines, before and after). `lint.yml`'s
header is corrected to match and re-pinned to 1a85269, since `pcapkit/` had
in fact moved from the previous pin (932cb48) in four files even though
E/W/C read the same on both; its R/total range is restated on a consistent
basis and labelled with the tree it was measured on.

Fixes #765.
Fixes #767.

Build: `make isort` clean on all four lines; pylint re-measured once several
rounds back, output redirected to a file and parsed (E 90, W 4765, C 542,
R 648, exit 30) -- unchanged since, no pcapkit/ file touched.
JarryShaw added a commit that referenced this pull request Sep 25, 2026
…k included

`main` moved from 73f09ae to 4530424 while this PR sat open, and the 1.5.0
section cited none of the 25 commits in between. Ten new bullets cover thirteen
of them, appended in merge order, with the file's own `**a breaking change**`
lead sentence on the three that are breaking:

- #754 -- AppType split into per-transport registries; the 1,004 portless and
  704 transportless rows stop being members. Breaking.
- #764 -- an out-of-range port in `AppType.get` is refused, not minted, so
  `TCP.make(srcport=99999)` raises; per-transport `_missing_` spans. Breaking.
- #778 -- `@final` enforced at runtime on `Info`/`Schema`: bare `@final` raises
  `InfoError`/`SchemaError` at first construction, deriving from a finalised
  class raises, and `SchemaError` is a `ValueError` where a caller may have
  been catching `TypeError`. Breaking.
- #772 (with #790's docstring reword), #766, #759, #787, #794 (with #791's
  citation repoint), #792/#798 and #802 -- the remaining seven.

Also re-ran the citation sweep against `origin/main` rather than the checkout.
One stale line number fixed: the `httpv2.py` `header.length != 9` guard the
`#692` entry calls out moved from `:562` to `:650` under #789 and #802. The
preamble's "reaching #726" becomes #805, the new maximum reference. Verified
unmoved on 4530424: `protocol.py:1411`, `schema/internet/ipv4.py:336`,
`traceflow.py` 146/149/162/424, the four `:type:` fields in `engine.rst`,
`reassembly.rst` and `traceflow.rst`, and `EXPECTED_FAILURES` at 43 entries.

Carries the previous round's #651/#646 corrections unchanged. Two literals were
reflowed so no ``literal`` wraps a line, which the generator's residual guard
refuses. `changelog_md.py --check` exit 0; `test_changelog_md.py` 47 passed,
37 subtests.
JarryShaw added a commit that referenced this pull request Sep 26, 2026
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit
(relative to its parent, 4233555) now names all 20 bullets it carries, not
just the 8 this session added on top of the 10 already there -- a first
draft of this message named only its own 8 and left the other 10 silent,
which a cross-review caught. Four are breaking:

- #754 -- AppType split into per-transport registries; the 1,004 portless
  and 704 transportless rows stop being members.
- #764 -- an out-of-range port in `AppType.get` is refused, not minted.
- #778 -- `@final` enforced at runtime on `Info`/`Schema`.
- #575 -- four `.get()`-backed enum fields fall through to
  `_unregistered_member` instead of minting; 14 of 23 sample captures
  change output.
- #772 (with #790's docstring reword), #766, #759, #787, #794 (with #791's
  citation repoint), #792/#798, #802, #782 (a further #745-hazard instance),
  #704, #723, #739, #743/#746 (cross-dependent, one bullet each), #805
  (closes #802's own filed-as-out-of-scope), #796, #800 -- the other 16,
  non-breaking.

Also restores a measurement an earlier round in this same diff dropped
while updating an adjacent one: the #692 entry's "mypy is unmoved at 112
errors" silently lost its "and pylint ... 364 messages" half when
`EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence.
Restored to the last value earlier rounds signed off on rather than
re-measured, since this branch's own `pcapkit/` tree predates several
since-merged PRs and a fresh run would not be measuring the same thing the
original round measured. Unmoved, and not silently dropped this time:
`93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES`
44 to 43 in the several other places that already carried the fix.

On which PRs get a bullet: there is no clean "user-facing only" rule --
#766 and #791 are pure CI/test/lint-comment entries that are in, while
#773 and #763 are the same kind of thing and are out. The real pattern
across this file's 46 commits is closer to "each round's author judged it
worth a reader's time," which is inconsistent by construction. This round
leaves that inconsistency as found rather than trying to retrofit a rule,
but did add #782 on reconsideration -- its own PR body names it as sharing
#766's hazard, and pre-existing precedent already treats that hazard's
instances as bullet-worthy.

`changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0.
`test_changelog_md.py` 47 passed.
JarryShaw added a commit that referenced this pull request Sep 26, 2026
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit
(relative to its parent, 4233555) now names all 20 bullets it carries, not
just the 8 this session added on top of the 10 already there -- a first
draft of this message named only its own 8 and left the other 10 silent,
which a cross-review caught. Six are breaking, matching the crediting PRs'
own `breaking` label in each case:

- #754 -- AppType split into per-transport registries; the 1,004 portless
  and 704 transportless rows stop being members.
- #764 -- an out-of-range port in `AppType.get` is refused, not minted.
- #778 -- `@final` enforced at runtime on `Info`/`Schema`.
- #575 -- four `.get()`-backed enum fields fall through to
  `_unregistered_member` instead of minting; 14 of 23 sample captures
  change output.
- #759 -- `AppType._dispatch` on a multi-transport `proto` now raises
  `ProtocolError` instead of silently resolving to whichever transport
  owns the lowest set bit.
- #805 -- `FieldBase.length` on a negative resolved length now raises
  `ProtocolError` instead of letting a bare `struct.error` escape.
  A second cross-review caught both: their crediting PRs (#783, #811) both
  carry GitHub's own `breaking` label, and neither bullet said so.

- #772 (with #790's docstring reword), #766, #787, #794 (with #791's
  citation repoint), #792/#798, #802, #779 (via #782, a further
  #745-hazard instance), #704, #723, #739, #743/#746 (cross-dependent, one
  bullet each), #796, #800 -- the other 14, non-breaking.

Also restores a measurement an earlier round in this same diff dropped
while updating an adjacent one: the #692 entry's "mypy is unmoved at 112
errors" silently lost its "and pylint ... 364 messages" half when
`EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence.
Restored to the last value earlier rounds signed off on rather than
re-measured, since this branch's own `pcapkit/` tree predates several
since-merged PRs and a fresh run would not be measuring the same thing the
original round measured. Unmoved, and not silently dropped this time:
`93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES`
44 to 43 in the several other places that already carried the fix.

On which PRs get a bullet: there is no clean "user-facing only" rule --
#766 and #791 are pure CI/test/lint-comment entries that are in, while
#773 and #763 are the same kind of thing and are out. The real pattern
across this file's 46 commits is closer to "each round's author judged it
worth a reader's time," which is inconsistent by construction. This round
leaves that inconsistency as found rather than trying to retrofit a rule,
but did add #779 (via #782) on reconsideration -- its own PR body names it
as sharing #766's hazard, and pre-existing precedent already treats that
hazard's instances as bullet-worthy.

`changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0.
`test_changelog_md.py` 47 passed.
JarryShaw added a commit that referenced this pull request Sep 26, 2026
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit
(relative to its parent, 4233555) now names all 20 bullets it carries, not
just the 8 this session added on top of the 10 already there -- a first
draft of this message named only its own 8 and left the other 10 silent,
which a cross-review caught. Six are breaking, matching the crediting PRs'
own `breaking` label in each case:

- #754 -- AppType split into per-transport registries; the 1,004 portless
  and 704 transportless rows stop being members.
- #764 -- an out-of-range port in `AppType.get` is refused, not minted.
- #778 -- `@final` enforced at runtime on `Info`/`Schema`.
- #575 -- four `.get()`-backed enum fields fall through to
  `_unregistered_member` instead of minting; 14 of 23 sample captures
  change output.
- #759 -- `AppType._dispatch` on a multi-transport `proto` now raises
  `ProtocolError` instead of silently resolving to whichever transport
  owns the lowest set bit.
- #805 -- `FieldBase.length` on a negative resolved length now raises
  `ProtocolError` instead of letting a bare `struct.error` escape.
  A second cross-review caught both: their crediting PRs (#783, #811) both
  carry GitHub's own `breaking` label, and neither bullet said so.

- #772 (with #790's docstring reword), #766, #787, #794 (with #791's
  citation repoint), #792/#798, #802, #779 (via #782, a further
  #745-hazard instance), #704, #723, #739, #743/#746 (cross-dependent, one
  bullet each), #796, #800 -- the other 14, non-breaking.

Also restores a measurement an earlier round in this same diff dropped
while updating an adjacent one: the #692 entry's "mypy is unmoved at 112
errors" silently lost its "and pylint ... 364 messages" half when
`EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence.
Restored to the last value earlier rounds signed off on rather than
re-measured, since this branch's own `pcapkit/` tree predates several
since-merged PRs and a fresh run would not be measuring the same thing the
original round measured. Unmoved, and not silently dropped this time:
`93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES`
44 to 43 in the several other places that already carried the fix.

On which PRs get a bullet: there is no clean "user-facing only" rule --
#766 and #791 are pure CI/test/lint-comment entries that are in, while
#773 and #763 are the same kind of thing and are out. The real pattern
across this file's 46 commits is closer to "each round's author judged it
worth a reader's time," which is inconsistent by construction. This round
leaves that inconsistency as found rather than trying to retrofit a rule,
but did add #779 (via #782) on reconsideration -- its own PR body names it
as sharing #766's hazard, and pre-existing precedent already treats that
hazard's instances as bullet-worthy.

`changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0.
`test_changelog_md.py` 47 passed.
JarryShaw added a commit that referenced this pull request Sep 26, 2026
`main` moved from 73f09ae to 3cbdf89 while this PR sat open. This commit
(relative to its parent, 4233555) now names all 20 bullets it carries, not
just the 8 this session added on top of the 10 already there -- a first
draft of this message named only its own 8 and left the other 10 silent,
which a cross-review caught. Six are breaking, matching the crediting PRs'
own `breaking` label in each case:

- #754 -- AppType split into per-transport registries; the 1,004 portless
  and 704 transportless rows stop being members.
- #764 -- an out-of-range port in `AppType.get` is refused, not minted.
- #778 -- `@final` enforced at runtime on `Info`/`Schema`.
- #575 -- four `.get()`-backed enum fields fall through to
  `_unregistered_member` instead of minting; 14 of 23 sample captures
  change output.
- #759 -- `AppType._dispatch` on a multi-transport `proto` now raises
  `ProtocolError` instead of silently resolving to whichever transport
  owns the lowest set bit.
- #805 -- `FieldBase.length` on a negative resolved length now raises
  `ProtocolError` instead of letting a bare `struct.error` escape.
  A second cross-review caught both: their crediting PRs (#783, #811) both
  carry GitHub's own `breaking` label, and neither bullet said so.

- #772 (with #790's docstring reword), #766, #787, #794 (with #791's
  citation repoint), #792/#798, #802, #779 (via #782, a further
  #745-hazard instance), #704, #723, #739, #743/#746 (cross-dependent, one
  bullet each), #796, #800 -- the other 14, non-breaking.

Also restores a measurement an earlier round in this same diff dropped
while updating an adjacent one: the #692 entry's "mypy is unmoved at 112
errors" silently lost its "and pylint ... 364 messages" half when
`EXPECTED_FAILURES` was corrected 44 to 43 elsewhere in the same sentence.
Restored to the last value earlier rounds signed off on rather than
re-measured, since this branch's own `pcapkit/` tree predates several
since-merged PRs and a fresh run would not be measuring the same thing the
original round measured. Unmoved, and not silently dropped this time:
`93 of the 95 sites` corrected to `94 of the 95` and `EXPECTED_FAILURES`
44 to 43 in the several other places that already carried the fix.

On which PRs get a bullet: there is no clean "user-facing only" rule --
#766 and #791 are pure CI/test/lint-comment entries that are in, while
#773 and #763 are the same kind of thing and are out. The real pattern
across this file's 46 commits is closer to "each round's author judged it
worth a reader's time," which is inconsistent by construction. This round
leaves that inconsistency as found rather than trying to retrofit a rule,
but did add #779 (via #782) on reconsideration -- its own PR body names it
as sharing #766's hazard, and pre-existing precedent already treats that
hazard's instances as bullet-worthy.

`changelog_md.py` regenerated `CHANGELOG.md`; `--check` exit 0.
`test_changelog_md.py` 47 passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) feat Pull requests that add a new capability (feat: subject prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant