Skip to content

fix(vendor): stop get() minting; _missing_ fixed for 21 of 105 (#775) - #838

Merged
JarryShaw merged 1 commit into
mainfrom
gh-775-tier1-no-mint-on-lookup
Sep 27, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
gh-775-tier1-no-mint-on-lookup

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Please follow the guide below

What is the purpose of your pull request?

  • fix — corrects a defect

Description of your pull request and other information

Tier 1 of #775. The maintainer's ruling: an unrecognised value/key should
never permanently register a new member unless the caller explicitly asks
for one. Today both get()'s string-key miss and _missing_'s bounded
range branch call extend_enum(), so e.g. Hardware(40) mints a
permanent member on first call.

This edits the two shared sites in pcapkit/vendor/default.py and adds a
base register(value, name) classmethod, the explicit caller-named path
that still mints — value-first, matching _unregistered_member(value, name)'s own order, so the two no longer disagree. 105 generated
pcapkit/const/ modules are hand-edited to match, then proved equivalent by
regenerating all 21 changed registries from live IANA — the resulting diff
is byte-identical to the hand-edit. Seven further registries across both
template kinds regenerate with no change at all, so no upstream drift is
riding along. (An earlier revision of this description claimed regeneration
was not possible offline; that was wrong — the network is reachable and
Vendor.__init__ crawls and rewrites on construction.) New tests in tests/const/test_const_enum_no_mint.py cover the
no-mint behaviour and register()/_unregistered_member() across all 105
registries. Pickling an unregistered member works by re-derivation rather
than identity, so two equal unregistered members are not the same object.

Public behaviour change: get()'s no-longer-minting miss path applies
to all 105 registries this PR edits. _missing_'s bounded-range fix,
however, only actually changes behaviour for the 21 registries whose
vendor crawler leaves Vendor.process() unmodified (REGISTRIES_WITH_UNASSIGNED_RANGES in
the new test module) — those are the ones where an unregistered value now
stays absent from __members__/_value2member_map_. The other 84 edited
files' _missing_ is unchanged from base: 83 of them override process()
with their own bespoke logic and still call extend_enum there, and the
last one shares the same unmodified template but has no unassigned range
to mint against in the first place.

This PR also fixes two toolkit call sites that fed get() a string key
with no default and so now raise where they used to get back a minted
placeholder — pcapkit/toolkit/scapy.py and pcapkit/toolkit/pyshark.py now let
that KeyError through as MissingKeyError instead, since LinkType.NULL
and LinkType.RAW are genuine DLTs with their own handler protocol classes
and neither is an honest stand-in for "unknown link type" — and four protocol reader
sites (hopopt.py/ipv6_opts.py's SeedID and TaggerID lookups) that now
convert the same KeyError into their own ProtocolError. That guard is for
an unresolvable string key from a hand-built schema, not a wire value — an
unassigned wire value still mints through _missing_ and is deferred to tier 2.
(An earlier revision of this description said "wire value", which overstated
the scope and contradicted the commit message.)

Out of scope here, now counted exactly by an AST census rather than
estimated: 1026 minting call sites remain — 1007 in _missing_ and 7
in get across pcapkit/const/**, plus the 12 hand-written ones below.
register/register_alias account for a further 106 sites which are the
by-design caller-named mint path and are deliberately untouched.

Two things that census corrected. pcapkit/vendor/** holds zero real
call sites
— grep finds 105 hits in 91 files, but every one is inside
an f-string template the crawler writes into a generated file, so any
grep-based figure for this work is wrong by construction. And editing
the shared template again would fix nothing further
: of the 26 vendor
modules that use pcapkit/vendor/default.py untouched, none generates a
still-minting const file, while all 89 that do come from the 95 modules
carrying their own process() or template. AppType alone is 767 of the
1026 sites.

One more population, added after review: 12 extend_enum sites live
outside pcapkit/const/ and pcapkit/vendor/ entirely
, in two
hand-written protocol modules — pcapkit/protocols/internet/mh.py
(:618, :632, :676, :690, :731, :745, :782, :796) and
pcapkit/protocols/application/ngap.py (:366, :379, :847, :860).
These carry the exact pre-#838 get/_missing_ pattern, and no vendor
regeneration will ever reach them. All left for tier 2.

Per the maintainer's ruling, the pyshark name mismatch is fixed here rather than
deferred: pyshark reports Wireshark's PDML filter name, not a DLT name, so
FILTER_NAME_TO_LINKTYPE in pcapkit/toolkit/pyshark.py translates the two names
that are unambiguous against Wireshark's own dissector registrations — eth →
ETHERNET and tr → IEEE802_5, each bound to exactly one WTAP_ENCAP_*. It is
consulted before LinkType.get, which still resolves a filter name that already
spells a member (ppp, fddi — verified). sll, fr, ip and ipv6 are
deliberately absent, though for different reasons — measured, not assumed:
sll is genuinely shared, one filter name for both the linux-sll (DLT 113) and
linux-sll2 (DLT 276) encapsulations. ip and ipv6 are absent because they never
arrive as the root layer at all. And fr turns out not to be ambiguous — both its
encapsulations write DLT 107 — so it is mappable and is left out only as unneeded scope. A genuinely unknown name still raises.

Corrections found across review rounds, all measured with tshark 4.6.9 and editcap -T:
raw does not raise — it serves RAW/IPV4/IPV6 (101/228/229) and the fallback answers
101 for all three. null does not raise — serves NULL/LOOP (0/108), answers 0. And
ip/ipv6 never arrive as the root layer at all: across the 158 of the 226 encapsulations editcap -T
accepts that an Ethernet source can be rewritten into, neither is ever layers[0], because a raw IPv6 capture roots at raw. Those silent
wrong answers are #843, not something this PR introduces.

Refs #840 rather than closes it. That issue also asks for ip → IPV4 and
loop → NULL. Only ip is refused. loop already resolves to LinkType.LOOP (108)
through the fallback — and is the wrong filter name anyway: tshark registers loop as
Configuration Test Protocol (loopback), an Ethernet payload, while a DLT_NULL or DLT_LOOP
capture presents null. null resolves to LinkType.NULL (0), so a DLT_LOOP capture silently
reads as DLT 0. That ambiguity is #843. #840 stays open.

Generalising register_alias so ETH could instead be a real alias on LinkType
is #842, filed separately at the maintainer's direction.

@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) const Regenerated IANA or vendor constant tables; members keep their numeric values 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 26, 2026
@JarryShaw
JarryShaw force-pushed the gh-775-tier1-no-mint-on-lookup branch from 48e02dc to d40eeb1 Compare September 26, 2026 18:08
@JarryShaw

Copy link
Copy Markdown
Owner Author

Verified the tier 1 implementation myself before any verdict. Head is now d40eeb120 — I amended 48e02dc37 for one house-convention slip (GH-775 → #775 in a test comment; #nnn is the house form). That one word is the whole delta.

Shape: one commit, author and committer Jarry Shaw <jarryshaw@icloud.com>, parent add2a8255. 108 files = 105 pcapkit/const/** + 2 tests + 1 pcapkit/vendor/default.py. pcapkit/const/reg/apptype/** and pcapkit/vendor/reg/apptype/** are untouched, so AppType.register_alias — the sole caller-named extend_enum site, exempt per "unless user/caller explicitly created them" — is intact. All 105 const files are IntEnum, which is what makes int.__new__ sound in _unregistered_member.

Verified on d40eeb120:

  • All 108 changed files compile clean under python -W error::SyntaxWarning -m py_compile.
  • tests/const/ 98 passed / 39458 subtests, against baseline add2a8255 of 87 / 39184.
  • Coverage rose on both axes. Branch mode (the repo default): 57.24% → 57.79%, 45038 stmts / 15505 miss → 46298 / 15778. Statement-only on the same two trees: 65.57% → 65.92%. I first read the 57.79% as inconsistent with those counts and was wrong — it is the branch-inclusive figure, and the PR body quotes it correctly.
  • The new tests genuinely fail without the fix. Reverting pcapkit/vendor/default.py, pcapkit/const/ and the old test to add2a8255 while keeping only tests/const/test_const_enum_no_mint.py gives 279 failed / 6 passed.
  • Template-vs-generated equivalence re-derived independently: rendered the edited LINE template and compared ast.get_source_segment for get, register and _unregistered_member across a random 12-file sample — 36/36 identical.

One thing I checked because the diff does not make it obvious: the new get() string branch raises a bare KeyError when default == -1. That is not a new convention — the pre-existing int branch immediately above already does except ValueError: if default == -1: raise, so the two paths now match. The exception comes from the lookup itself rather than being synthesised.

A full cross-review is running on a different model from the author, briefed to attack the parts my checks did not cover — all 105 files' _missing_ bodies rather than a sample, and _unregistered_member under pickle, deepcopy and identity comparison, which the author verified by reasoning rather than testing and flagged as such. review: pending until that verdict lands and CI on the new head is complete.

@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES — cross-review (opus, a different model from the sonnet author) on d40eeb120. Two blocking findings, both of which I reproduced myself.

B1 — a real in-tree caller now raises where it used to return. Enum_LinkType.get() is called with a string key and no default at two sites this PR does not touch: pcapkit/toolkit/scapy.py:324 and pcapkit/toolkit/pyshark.py:87. pcapkit/const/reg/linktype.py is among the 105 edited files, so its get changed. Measured on both trees:

BASE get('IP') -> <LinkType.IP: -1>        HEAD get('IP')       -> KeyError: 'IP'
BASE get('ETH') -> <LinkType.IP: -1>       HEAD get('ETH')      -> KeyError: 'ETH'
BASE __members__ 220 -> 223                HEAD get('ETHERNET') -> <LinkType.ETHERNET: 1>

Neither behaviour is good — on base every unresolvable name collapsed to one junk member at value -1 — but raising breaks callers that previously returned. scapy.py has no try around it, so it propagates out of scapy_tcp_traceflow. 'ETHERNET' resolves, so the Ether-rooted scapy path is safe; (IP()/TCP()).name.upper() == 'IP' is not. The pyshark side is UNVERIFIED (needs tshark and a capture), but its layer names are Wireshark abbreviations like eth, so it may fail for every packet.

B2 — the body's headline claim is false for most files it edits. It says an unregistered value/key "now stays absent from __members__/_value2member_map_". Measured on head, in a file the PR edits:

HEAD Cipher(9999) -> <Cipher.Reserved_for_Private_Use_9999: 9999>   __members__ 66 -> 67   9999 in _value2member_map_: True

By ast over the 105 edited files: 21 changed _missing_, 84 byte-identical, and 83 still call extend_enum inside _missing_. Those files inherit the shared get but override process(), so only their get was fixed. That matches the agreed tier-1 scope, so it is a disclosure defect, not a code one — but the title reads as if _missing_ is done, and the "Out of scope" paragraph names only the 12 wholesale-template files and register_alias, omitting the largest deferral: roughly 1007 _missing_ sites still mint.

Two corrections to numbers I had relayed. The split is 21/84, not 22/83 (49 sites is right, and replaying the substitution on each base _missing_ reproduces head byte-exactly 21/21). And the issue's LinkType(424242) "mints twice" framing is wrong — measured identically on both trees, __members__ 220→221 and the second call returns the same object; the 220→223 figure came from three string-key misses, which I reproduced exactly.

_unregistered_member survived a hard attack with no finding, including the gap the author had only reasoned about: pickle round-trips on all six protocols, deepcopy returns self, it is absent from all four lookup tables, gc shows no retention, and nothing in pcapkit/ outside const/vendor compares enum members by identity. The one real difference is that a is b is False for two equal unregistered members — pickle works by re-derivation, not identity, which is worth stating in the PR.

Dispatching the fix. Also taking the reviewer's aligned-signature point: register(name, value) and _unregistered_member(value, name) take the same two types in opposite order, and the ruling's wording was value-first — better fixed now than after tier 2 writes 112 signatures against it.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 26, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

CI root cause on d40eeb120: all 10 red legs are one defect, and it is B1 in a third place. Failing step "Run unit tests" on Python 3.10–3.14 and Engines Python 3.10–3.14:

tests/protocols/internet/test_ipv6_extension_unit.py:1358 -> :1139
E   KeyError: 'Bogus_Seed_For_Test'    .../aenum/_enum.py:1878

pcapkit/protocols/internet/hopopt.py:1022 does kind = Enum_SeedID.get(schema.flags['type']) — a production caller with a possibly-string key and no default — and pcapkit/const/ipv6/seed_id.py is among the 105 edited files. The test at :1360 feeds 'Bogus_Seed_For_Test' and expects ProtocolError; on base the string miss minted a member so execution reached that error, and on head a bare KeyError escapes assertRaises.

I swept all 95 Enum_*.get(...) sites in pcapkit/ by ast to bound the exposure properly. The int branch of get is untouched by this PR, so every site passing an int expression — schema.type >> 6, int.from_bytes(...), reader.datalink() — is unaffected. Only string-capable callers on a changed registry matter, and that is exactly six:

pcapkit/toolkit/scapy.py:324                  Enum_LinkType.get(packet.name.upper())
pcapkit/toolkit/pyshark.py:87                 Enum_LinkType.get(packet.layers[0].layer_name.upper())
pcapkit/protocols/internet/hopopt.py:1022     Enum_SeedID.get(schema.flags['type'])
pcapkit/protocols/internet/hopopt.py:798      Enum_TaggerID.get(schema.info['type'])
pcapkit/protocols/internet/ipv6_opts.py:1025  Enum_SeedID.get(schema.flags['type'])
pcapkit/protocols/internet/ipv6_opts.py:801   Enum_TaggerID.get(schema.info['type'])

ipv6/seed_id.py, ipv6/tagger_id.py and reg/linktype.py are all in the 105; tcp/flags.py, ftp/command.py, ftp/return_code.py and http/method.py are not, so Enum_Flags.get(key.upper()) at tcp.py:507/:615 and the FTP/HTTP string callers are genuinely safe and need no change. That is the useful part of the sweep — the blast radius is six sites, not ninety-five.

The two site families need different fixes. The toolkit pair gets default=LinkType.NULL as already described. The four protocol sites feed ProtocolError paths, and the contract the tests assert is that an unknown seed-id or tagger-id type is a protocol error — so the lookup failure gets converted into a ProtocolError from pcapkit.utilities.exceptions, naming the bad value in the style of the neighbouring invalid seed-id length: 2 and unknown QS function: 1 messages. The test at :1360 is not being weakened to expect KeyError; the reader's contract stays ProtocolError.

Redirected the fix worker accordingly. review: needs-changes stands until the new head is green and re-reviewed.

@JarryShaw
JarryShaw force-pushed the gh-775-tier1-no-mint-on-lookup branch from d40eeb1 to 364c8d8 Compare September 26, 2026 18:58
@JarryShaw JarryShaw added review: pending No verdict for the current head - never reviewed, or the head moved since the last one and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Sep 26, 2026
@JarryShaw JarryShaw changed the title fix(vendor): stop the shared get()/_missing_ template from minting (#775) fix(vendor): stop get() minting; _missing_ fixed for 21 of 105 (#775) Sep 26, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Fixes pushed at 364c8d8ce, and CI on this head is clean: 27✅ / 0❌ / 3 skipped / 0 incomplete. Still one commit on add2a8255, now 115 files = 105 const + pcapkit/vendor/default.py + the 4 fixed modules + 5 test files.

B1 — all six affected call sites fixed, with two different strategies. The toolkit pair passes an explicit default: Enum_LinkType.get(packet.name.upper(), Enum_LinkType.NULL) at scapy.py and the pyshark equivalent, each with a NOTE explaining that NULL (0) is the honest "unknown link type" and is distinct from the -1 no-default sentinel. The four protocol readers — hopopt.py and ipv6_opts.py, SeedID and TaggerID — wrap the lookup and raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] unknown seed-id type: …') from None, matching the neighbouring unknown QS function: style. An unresolvable wire value is a protocol error, not a raise-through, and the test at test_ipv6_extension_unit.py:1360 keeps asserting ProtocolError rather than being weakened to expect KeyError.

B2 — title and body rewritten. The title is now fix(vendor): stop get() minting; _missing_ fixed for 21 of 105 (#775), and the body states the split explicitly: get()'s fix applies to all 105 edited registries, _missing_'s bounded-range fix changes behaviour for only 21, the other 84 are unchanged from base (83 still calling extend_enum in their own process()-driven _missing_, one with no unassigned range at all), and the ~1007 deferred sites are named as the largest tier-2 item rather than omitted.

Signature aligned: register(cls, value, name), value-first to match _unregistered_member(value, name), so a swapped call can no longer fail obscurely inside aenum.

Verified by me on this head: all 115 files compile clean under -W error::SyntaxWarning; the signature swap is complete — 0 occurrences of the old name-first order, value-first present in 106 files (105 const + the template), so nothing was missed; ProtocolError comes from the house module (hopopt.py:93); and pytest tests/const/ tests/toolkit/ tests/protocols/internet/test_ipv6_extension_unit.py gives 217 passed, 11 skipped, 39584 subtests, 0 failures.

One correction to something I asserted earlier, and it was my error. I had said the PR body needed "22/83 → 21/84" and the "49 sites" figure corrected. Neither ever appeared in the body — both live only in the cross-review comment above, which had already self-corrected. grep -cE '22/83|49 sites' against the body returns 0. The body's numbers were never wrong on that point; only the "Public behaviour change" paragraph and the title were, and both are now fixed.

One thing I am not yet asserting: tests/const/ alone dips 57.79% → 57.77% (46298/15778 → 46310/15790), because the 12 new try/except statements live in modules that directory never imports. The claim is that those exact lines are fully covered by test_ipv6_extension_unit.py and that the combined figure rises to 61.76%. That is plausible but unverified by me, so a re-review on a different model is checking precisely it, along with whether LinkType.NULL makes an unresolvable name indistinguishable from a genuine DLT_NULL capture downstream.

review: pending until that verdict lands.

@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES — re-review (opus) on 364c8d8ce. The code is sound and CI is clean; the narrative is wrong in three places, and one of them exposes a real behaviour question for you. I reproduced all three.

B1 — "an unresolvable wire value is a protocol error" is false, and the wire path still mints. The four guarded lookups read a BitField namespace slice, i.e. an int, never a string. Measured:

SeedID.get(0..3) -> all four resolve      SeedID.get(4) -> ValueError (not KeyError; 4 unreachable from 2 bits)
TaggerID.get(5)  -> <TaggerID.Unassigned_5: 5>   members 4 -> 5   5 in _value2member_map_: True

So the except KeyError is reachable only from a hand-built schema carrying a string; a bogus wire value (TaggerID 4–7) returns successfully having permanently minted, because TaggerID._missing_ is one of the 84 deferred. The PR body and four identical in-code NOTEs assert the opposite. They must say the guard covers an unresolvable string key and that the unassigned-wire-value path still mints and is tier 2.

B2 — the ~1007 is attributed to the wrong bucket. By ast, extend_enum inside _missing_: 226 across the 84 changed-but-unfixed registries, 781 in the not-edited group (the 12 wholesale-template modules, pcapng/option_type.py the bulk), 0 in the 21 fixed — total 1007. The body credits all 1007 to "the other 84", while the same sentence already defers the 12 separately. 77% is double-counted.

B3 — LinkType.NULL is a specific wrong DLT and it reaches a file on disk. This is the part I want your call on. NULL = 0 is DLT_NULL, BSD loopback, which prepends a 4-byte AF field. packet.protocol flows traceflow/tcp.py:274 → dumpkit/pcap.py:141 network=protocol, so an unresolvable link name now writes a traceflow PCAP whose global header claims DLT_NULL for frames with no such header, and a genuine DLT_NULL capture becomes indistinguishable from a lookup miss. RAW = 101 (DLT_RAW, raw IP) exists and is exactly the case the scapy comment describes.

Worse, the reviewer found the pyshark test double hides the common case: test_pyshark_unit.py:27 names the Ethernet layer 'ethernet', but pyshark's XmlLayer.__init__ takes xml_obj.attrib['name'] — Wireshark's filter name, eth — and 'ETH' is not a LinkType member. If that holds, every ordinary Ethernet capture through toolkit/pyshark.py now yields NULL and dumps DLT_NULL, and the new test cannot see it. Source-derived, not observed: there is no tshark on this host, so treat the 'eth' spelling as UNVERIFIED end-to-end.

In fairness it does not regress something that worked — the old path minted LinkType['ETH'] = -1 and handed network=-1 to the dumper. It converts a broken path into a silently-wrong one.

The ask: for an unresolvable link-layer name at those two toolkit sites, do you want (1) LinkType.RAW, (2) keep LinkType.NULL with the comment corrected to admit it is a lossy sentinel the dumper writes as DLT_NULL, or (3) let it raise and have the callers handle it? Labelling needs: decision. B1, B2 and the non-blocking items (four new E127s, and an except KeyError whose f-string re-reads the subscript it may have failed on, so from None never runs) are unambiguous and I am fixing them now.

Held and confirmed: the register swap is complete across all 105 files with docstrings in the new order and register introduced by this commit, so no released API moves; the 12-wholesale exclusion is exact; the 21/84 split reproduces; all 16 new statements are executed with no partial branches, so the tests/const/ dip is fully explained; and both spot-checks fail without their fix.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment needs: decision Waiting on the maintainer to decide — not blocked by other work and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 26, 2026
@JarryShaw
JarryShaw force-pushed the gh-775-tier1-no-mint-on-lookup branch from 364c8d8 to 53d8ed3 Compare September 26, 2026 19:52
@JarryShaw

Copy link
Copy Markdown
Owner Author

The unambiguous findings are fixed at 53d8ed319 — still one commit, still 115 files, the delta being only hopopt.py and ipv6_opts.py. Verified by me on that head: both files compile clean under -W error::SyntaxWarning, zero E12 hits in each (the four E127s are gone), and tests/const/ tests/toolkit/ tests/protocols/internet/test_ipv6_extension_unit.py → 217 passed, 11 skipped, 39584 subtests, 0 failures.

B1 fixed. The four NOTEs now read that only a hand-constructed schema reaches the guard and that an unassigned wire value still mints through _missing_, deferred to tier 2.

The robustness fix went further than I asked, and it was right to. My snippet bound the key outside the try, which still let a bare KeyError escape when the dict lacks 'type' — a method whose docstring promises ProtocolError. The implementation pre-binds to None and keeps the subscript inside the guard. Measured on the head:

string key : ProtocolError: HOPOPT: [OptNo 109] unknown seed-id type: 'Bogus_Seed_For_Test'
missing key: ProtocolError: HOPOPT: [OptNo 109] unknown seed-id type: None

One nit I am not sending back: the missing-key message reports None as the type, conflating "key absent" with "type is None". Unreachable in real parsing, since a BitField always populates its namespace.

A number I published was wrong, and this corrects it. I relayed that the 781 deferred _missing_ sites live mostly in the 12 wholesale-template files with pcapkit/const/pcapng/option_type.py "the bulk of it". Re-derived by ast over the 16 not-edited const modules:

766  pcapkit/const/reg/apptype/apptype.py
  8  pcapkit/const/http/status_code.py
  3  pcapkit/const/ftp/return_code.py
  2  pcapkit/const/ftp/command.py
  1  pcapkit/const/pcapng/option_type.py

766 of 781 (98%) are AppType._missing_, and option_type.py has exactly one. The 226 + 781 = 1007 total stands. Worth stating for tier 2 scoping: the single largest remaining minting site in the repo is AppType, the registry #837 just touched — and it is excluded from tier 1 only because its vendor file replaces the shared template wholesale.

Still needs: decision on the LinkType.RAW / keep-NULL / raise question for the two toolkit sites; the fake's Ethernet layer name is entangled with it and deliberately untouched. Once that is settled I will re-review on the resulting head.

@JarryShaw

Copy link
Copy Markdown
Owner Author

B1 — "an unresolvable wire value is a protocol error" is false, and the wire path still mints.

Not sure I followed but my idea is that, if it is a unregistered value but legit according to the Enum's range, then we are good to return a temporary enum using something like Enum.__new__(...) perhaps so that we dont have to mint it permanently while being able to return an actual Enum for it.

B2 — the ~1007 is attributed to the wrong bucket.

Same, not entirely sure if I understood. I used _missing_ as a generator for IANA records where it says from NN to NN values are reserved, then the Enum will automatically build them as reserved_NN when being called. But if one caller specifically registered an NN to a name through either .register(...) call (same as the one we added in AppType enums) or through extend_enum(...) calls, then we will directly honour it for the runtime session.

But also, I'm not sure how industry handle these. Should we actually not to mint reserve_NN or unassigned_NN alike into the Enum upon _missing_?

B3 — LinkType.NULL is a specific wrong DLT and it reaches a file on disk.

Good catch! I say we raise, if NULL or RAW are genuine values that should go with their corresponding handler protocol classes.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Recorded, verbatim: "I say we raise, if NULL or RAW are genuine values that should go with their corresponding handler protocol classes." Acting on that. Answers to the other two, both measured.

B1 — your mechanism is already what the PR implements, so nothing is in dispute. "return a temporary enum using something like Enum.__new__(...) … so that we dont have to mint it permanently" is exactly _unregistered_member: int.__new__(cls, value), _name_/_value_ set, absent from __members__, _member_map_, _value2member_map_ and _member_names_. B1 was never about the mechanism — only that the four in-code NOTEs claimed it covered a bogus wire value when it only covers a string key. Those NOTEs are already corrected on 53d8ed319.

B2 — your instinct matches CPython exactly. Measured on this repo's 3.14.7:

stdlib IntEnum with a pseudo-member _missing_:  E(99) -> <E.x_99: 99>
  __members__ 1 -> 1   _value2member_map_ 1 -> 1   E(99) is E(99): False
stdlib IntFlag composite:  F(3) -> <F.a|b: 3>
  _value2member_map_ 2 -> 3   'a|b' in __members__: False

So no, do not mint reserved_NN/unassigned_NN into the enum on _missing_ — the stdlib returns an unregistered pseudo-member and never touches __members__. IntFlag caches composites in _value2member_map_ only, and still keeps them out of __members__. Your .register(...) / extend_enum(...) path staying honoured for the session is the other half of that same pattern, and is what this PR preserves. That confirms tier 1's direction and tier 2's plan; I am removing needs: decision on that basis, so say the word if you read it differently.

B3 — your premise checks out, and there is a consequence worth naming. Frame.__proto__ registers exactly three link types — ETHERNET, IPV4, IPV6 (pcapkit/protocols/misc/pcap/frame.py:93-95). NULL (0, DLT_NULL) and RAW (101, DLT_RAW) are genuine DLTs with no handler registered, so neither can serve as a fallback. Raising it is.

The consequence: scapy is fine — (Ether()/IP()/TCP()).name.upper() is 'ETHERNET', which resolves; only an IP-rooted packet raises, which is the intent. pyshark may not be. Its layer names are Wireshark filter names (eth, not ethernet), and 'ETH' is not a LinkType member — so under "raise", every pyshark capture could raise, not just the odd one. That is still the source-derived claim I could not confirm without tshark. I am having the worker fix the test double from 'ethernate'-style 'ethernet' to the real 'eth' so the tests show the truth instead of hiding it; if pyshark tracing then raises across the board, it needs a name→DLT mapping and I will bring that back to you rather than inventing one.

@JarryShaw JarryShaw removed the needs: decision Waiting on the maintainer to decide — not blocked by other work label Sep 27, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Can check documentations for the PyShark case if you cannot have tshark available/installed on the host to test live. And let's add the handlers (protocols) for DLT_NULL and DLT_RAW as part of wave 2/3 work in the work tracker then.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Recorded, verbatim: "Can check documentations for the PyShark case if you cannot have tshark available/installed on the host to test live. And let's add the handlers (protocols) for DLT_NULL and DLT_RAW as part of wave 2/3 work in the work tracker then."

Checked the docs and the installed source, and the eth reading holds. layer_name is a bare pass-through — pyshark/packet/layers/base.py:30-31 is return self._layer_name, with no normalisation, and .upper() appears only in __repr__. For PDML the name comes straight from <proto name> (tshark/output_parser/tshark_xml.py:93, XmlLayer(proto)). Three independent local confirmations that those are Wireshark filter names:

  • pyshark's Packet.__getattr__ matches layer.layer_name.lower() == item.lower(), documented as "For instance: pkt.ip", and consts.TRANSPORT_LAYERS = ['UDP', 'TCP'].
  • pcapkit's own toolkit depends on it: packet.ip, packet.ipv6, packet.tcp, packet.frame_info.
  • The test double names its layers 'frame', 'ip', 'tcp', 'ipv6' — all real filter names — and then 'ethernet', which is not one. Wireshark's is eth, hence eth.src/eth.dst and the fake's own src= field. So 'ethernet' is the outlier in its own file.

That makes the consequence concrete rather than speculative: layers[0].layer_name.upper() is 'ETH', 'ETH' is not a LinkType member, and once the fallback goes every Ethernet capture through pcapkit/toolkit/pyshark.py raises — not just the odd one. Still no tshark here, so this is source- and docs-derived, not live PDML.

Two items filed:

  • protocols: add DLT_NULL and DLT_RAW link-layer handlers #839 — protocols: add DLT_NULL and DLT_RAW link-layer handlers, enhancement,feat, for wave 2/3 as instructed. Notes the two real hazards: DLT_NULL's AF_ family field is in the capturing host's byte order, so it cannot be read fixed-endian, and DLT_RAW needs the IP version nibble to choose v4 vs v6.
  • toolkit: pyshark layer names are Wireshark filter names, so LinkType lookup misses #840 — toolkit: pyshark layer names are Wireshark filter names, so LinkType lookup misses, bug,fix. This is the mapping gap above. Note pcapkit/toolkit/scapy.py resolves today only because Scapy happens to call its layer Ethernet, so it is correct by coincidence of naming rather than by design — same fix applies.

Neither blocks #838: raising is right with or without the handlers. The worker applying the ruling is instructed to correct the double to 'eth' and let the tests show the real behaviour rather than paper over it.

JarryShaw added a commit that referenced this pull request Sep 27, 2026
Tier 1 of #775: get()'s string-key miss no longer permanently registers
a member, across all 105 edited pcapkit.const registries. _missing_'s
bounded-range fix only changes behaviour for the 21 of those 105 whose
vendor crawler leaves process() unmodified -- the other 84 keep their
own bespoke process()-driven _missing_, which still mints via
extend_enum, same as before.

- pcapkit/vendor/default.py: get() raises (or falls back to a value
  lookup) instead of extend_enum()-ing a name; the 21-registry _missing_
  range branch mints via a fresh _unregistered_member() instead. Both
  gain register(value, name), the explicit caller-named path that still
  grows the registry -- value-first, matching _unregistered_member's own
  order, so the two no longer disagree.
- pcapkit/toolkit/scapy.py, pyshark.py: raise instead of falling back to
  LinkType.NULL in Enum_LinkType.get() -- per the #838 ruling, NULL and
  RAW are genuine DLTs with their own handler protocol classes, not
  stand-ins for "unknown link type". The bare KeyError is caught and
  re-raised as MissingKeyError, this package's own house exception for a
  lookup miss. A real caller -- an IP-rooted scapy packet, or any pyshark
  capture, since PyShark's layer name is Wireshark's PDML filter name
  (e.g. `eth`, not `ethernet`) and matches no LinkType member -- now
  raises instead of getting a minted placeholder; #840 tracks the
  name -> DLT mapping pyshark tracing will need.
- pcapkit/protocols/internet/hopopt.py, ipv6_opts.py: the SeedID/TaggerID
  get() misses in _read_opt_mpl/_read_opt_smf_dpd now raise this reader's
  own ProtocolError instead of a bare KeyError, matching the neighbouring
  "unknown QS function"-style wording. The guard is for an unresolvable
  string key from a hand-built schema, not a wire value -- an unassigned
  wire value still mints via _missing_, deferred to tier 2. The key is
  pre-bound to None so the except handler can report it even when the
  namespace dict itself lacks the key, instead of re-subscripting and
  letting a second, bare KeyError escape.
- 105 generated pcapkit/const/ modules hand-edited to match; proved
  equivalent by rendering the template per class and diffing
  get/register/_unregistered_member via ast.get_source_segment.
- tests/const/test_const_enum_no_mint.py, tests/toolkit/test_scapy_unit.py,
  tests/toolkit/test_pyshark_unit.py and tests/protocols/internet/
  test_ipv6_extension_unit.py: new/updated coverage for all of the above.

Out of scope: the 12 wholesale-template vendor files' own _missing_ (781
extend_enum call sites), AppType.register_alias, and the other 84
registries' process()-driven _missing_ overrides (226 more) -- 1007
extend_enum call sites in total, deferred to tier 2. Build: py_compile
clean on all 114 touched files. Tests: coverage run -m pytest
tests/const/, 98 passed, 39458 subtests, 57.77% branch (57.79% baseline,
steady); combined with the new/updated test paths above, 61.76%.
@JarryShaw
JarryShaw force-pushed the gh-775-tier1-no-mint-on-lookup branch from 53d8ed3 to dd9eee8 Compare September 27, 2026 01:50
Tier 1 of #775: get()'s string-key miss no longer permanently registers
a member, across all 105 edited pcapkit.const registries. _missing_'s
bounded-range fix only changes behaviour for the 21 of those 105 whose
vendor crawler leaves process() unmodified -- of the remaining 84, 83
keep their own bespoke process()-driven _missing_ and still mint via
extend_enum, and the 84th has no unassigned range to mint from at all.

- pcapkit/vendor/default.py: get() raises (or falls back to a value
  lookup) instead of extend_enum()-ing a name; the 21-registry _missing_
  range branch mints via a fresh _unregistered_member() instead. Both
  gain register(value, name), the explicit caller-named path that still
  grows the registry -- value-first, matching _unregistered_member's own
  order, so the two no longer disagree.
- pcapkit/toolkit/scapy.py, pyshark.py: let an unresolvable link-layer
  name raise. On base these sites minted: Enum_LinkType.get() defaulted to
  -1, so get('ETH') ran extend_enum(LinkType, 'ETH', -1) and returned a
  freshly minted LinkType.ETH = -1, and a second unresolvable name aliased
  onto the same -1 member. Asked to pick a real DLT as the fallback
  instead, the ruling was to raise: NULL and RAW are genuine DLTs with
  their own handler protocol classes, not stand-ins for "unknown link
  type". The bare KeyError is caught and
  re-raised as MissingKeyError, this package's own house exception for a
  lookup miss. An IP-rooted scapy packet now raises rather than getting a
  minted placeholder.
- pcapkit/toolkit/pyshark.py: PyShark reports Wireshark's PDML filter
  name, not a DLT name -- `eth`, not `ethernet` -- so every Ethernet
  capture would otherwise raise, and engines/pyshark.py calls
  tcp_traceflow for each TCP packet under trace=True. A curated
  FILTER_NAME_TO_LINKTYPE table translates the two names that are
  unambiguous against Wireshark's dissector registrations -- `eth` ->
  ETHERNET and `tr` -> IEEE802_5, each bound to exactly one
  WTAP_ENCAP_* -- and is consulted before LinkType.get, which still
  resolves a filter name that already spells a member (`ppp`, `fddi`).
  A genuinely unknown name still raises. Measured with tshark 4.6.9 and
  editcap -T across the 158 of its 226 encapsulations an Ethernet
  source can be rewritten into: `sll` serves both
  LINUX_SLL (113) and LINUX_SLL2 (276) under one filter name, so it gets
  no entry and raises. `raw` serves RAW/IPV4/IPV6 (101/228/229) and
  `null` serves NULL/LOOP (0/108); neither raises, because 'RAW' and
  'NULL' are member names, so the fallback answers 101 and 0 -- silently
  wrong for 228, 229 and 108, which is #843 rather than anything this
  table introduces. `ip` and `ipv6` never arrive as the root layer in
  any of those 158; a raw IPv6 capture roots at `raw`. The other 68
  refuse the rewrite and are untested. `fr` turns out to be single-DLT
  (both its encapsulations write 107) so it is mappable, left out as
  unneeded scope; `wlan` was not investigated. Refs #840, which also asks for
  `ip` -> IPV4 and `loop` -> NULL. Only `ip` is refused; `loop` already
  resolves to LinkType.LOOP (108) through the fallback, and is anyway
  the wrong filter name -- tshark registers `loop` as Configuration
  Test Protocol (loopback), an Ethernet payload, while a DLT_NULL or
  DLT_LOOP capture presents `null`. `null` resolves to LinkType.NULL
  (0), so DLT_LOOP silently reads as DLT_NULL; that ambiguity is #843.
  The issue stays open.
- pcapkit/protocols/internet/hopopt.py, ipv6_opts.py: the SeedID/TaggerID
  get() misses in _read_opt_mpl/_read_opt_smf_dpd now raise this reader's
  own ProtocolError instead of a bare KeyError, matching the neighbouring
  "unknown QS function"-style wording. The guard is for an unresolvable
  string key from a hand-built schema, not a wire value -- an unassigned
  wire value still mints via _missing_, deferred to tier 2. The key is
  pre-bound to None so the except handler can report it even when the
  namespace dict itself lacks the key, instead of re-subscripting and
  letting a second, bare KeyError escape.
- The non-minting pseudo-member now carries the bare registry word --
  Unassigned, not Unassigned_39 -- per the maintainer's ruling. The
  numeric suffix existed to keep a *minted* member unique in __members__;
  nothing is minted here, and a pseudo-member is absent from __members__,
  _member_map_ and _value2member_map_ alike, so two same-named ones
  coexist and repr still disambiguates by value. The 49 call sites across
  those 21 registries drop the suffix; every extend_enum name is left
  numbered, because a minted Unassigned would collide.
- 105 generated pcapkit/const/ modules hand-edited to match, then proved
  equivalent by regenerating all 21 changed registries from live IANA:
  the resulting diff is byte-identical to the hand-edit, and seven
  further registries across both template kinds regenerate with no change
  at all, so no upstream drift is riding along.
- tests/const/test_const_enum_no_mint.py, tests/toolkit/test_scapy_unit.py,
  tests/toolkit/test_pyshark_unit.py and tests/protocols/internet/
  test_ipv6_extension_unit.py: new/updated coverage for all of the above.

Out of scope, counted exactly by AST census rather than estimated: 1026
minting call sites remain -- 1007 in _missing_ and 7 in get() across
pcapkit/const/, plus 12 hand-written ones in protocols/internet/mh.py and
protocols/application/ngap.py that no regeneration can reach. AppType
alone holds 767 of them. register()/register_alias account for a further
106 sites which are the by-design caller-named mint path and stay.
Deferred to tier 2. Build: py_compile
clean on all 117 touched files. Tests: coverage run -m pytest
tests/const/, 98 passed, 39458 subtests, 57.77% branch (57.79% baseline,
steady); combined with the new/updated test paths above, 61.76%.
@JarryShaw
JarryShaw force-pushed the gh-775-tier1-no-mint-on-lookup branch from 1840f9a to f5d4bda Compare September 27, 2026 06:15
@JarryShaw

Copy link
Copy Markdown
Owner Author

Fifth round, three blockers, all mine again — head is now f5d4bda7d. Two were prose; one was
a real measurement error where I overstated a sweep.

The substantive one. I claimed a sweep of "all 158 encapsulations editcap -T accepts".
Measured properly: editcap -T lists and accepts 226. 158 is the subset an Ethernet source can
be rewritten into; the other 68 refuse with can't be written as, and none refuse as an
unknown type. So my number conflated "accepted" with "swept", and 68 encapsulations were untested
while I described the sweep as exhaustive.

That matters beyond the count, exactly as the review said: ether-nettl and tr-nettl are among
the 68
— the most plausible second encapsulation for each of the table's only two entries. So the
adjacent "registers it against one encapsulation" claims rested on the same partial sweep. Both now
say what is actually known: ether and tr are the only swept encapsulations rooting at eth
and tr, with their -nettl variants unswept and "exactly one" therefore unproven.

Second: the test file contradicted the comment this round had just fixed. The double's stub
comment and the test docstring both said a raw-IP capture's layers[0] is 'ip' — false, it roots
at raw, which is precisely what pyshark.py now says, and the docstring even cross-referenced
that comment for the opposite claim. The test was always a valid guard; only its rationale was
wrong. Both now state that 'ip' is a synthetic shape exercising the unknown-name path, not a
reproduction of any real capture.

Third: a stale PR-body paragraph still said sll, fr, ip, ipv6 are "each shared by two
or three DLTs"
— true only of sll.

And the review turned up something that improves the table rather than just correcting it:
fr is not ambiguous at all. Both frelay and frelay-with-direction write DLT 107, so it
is mappable — I had it filed under "not investigated". Recorded as mappable-but-out-of-scope rather
than quietly added, since widening the table is not what this PR is for. Same check cleared fddi:
three encapsulations, all DLT 10, which is why excluding it from the wrong-answer list was right.

22 targeted tests pass; 100 across the wider selection on the prior head.

One limit the reviewer hit and named, which I cannot close on this host: pyshark.FileCapture is
broken under Python 3.14 (asyncio has no attribute 'set_child_watcher'), so the raise-or-answer
behaviour was verified by exercising the lookup byte-for-byte against tshark-measured root names
rather than through pyshark end-to-end. The layers[0] extraction itself is inferred from PDML with
geninfo/frame stripped.

@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO on f5d4bda7d — cross-review (opus, a different model from the sonnet author).
The first round where every load-bearing claim in that comment block survives independent
measurement.
No blockers.

What it re-derived rather than accepted: editcap -T accepts 226, of which 158 an Ethernet
source can be rewritten into and 68 refuse — "every failure carrying can't be written as",
none as an unknown type, so the partition is exact. ether is the only one of the 158 rooting at
eth, tr the only one rooting at tr, and ether-nettl/tr-nettl are genuinely among the 68 —
so the hedges I added are conservative rather than merely defensible, which it noted is the
first time this PR has under-claimed rather than over-claimed.

It also confirmed the fr correction by writing both encapsulations and reading the pcap header:
frelay and frelay-with-direction both DLT 107, both rooting at fr. And it broke the test
rather than reading it — replacing the raise with protocol = Enum_LinkType.RAW makes
test_tcp_traceflow_raises_for_an_ip_rooted_packet fail, so that guard is live, not vacuous.

One discrepancy it flagged instead of assuming, and it was right: my test count. It measured
91, against the 100 I have been quoting. Resolved — my brief listed six modules; my earlier
figure included a seventh:

six modules from the brief:        Ran 91 tests — OK
plus tests.foundation.engines.test_pyshark_engine:   Ran 9 tests — OK (skipped=1)

91 + 9 = 100, 1 skip. The count was right and my brief was incomplete; nothing regressed.

Two nits I am not revising for: of the 158 swept, 9 yield no determinable root and 30 root at
fake-field-wrapper, so "neither is ever layers[0]" is strictly evidenced for the 149 with a real
root — the conclusion is unaffected, since ip/ipv6 appear nowhere in the sweep. And the sentence
opens absolute before scoping itself in the next clause.

Label flipped to review: good-to-go. CI on this head was 23✅ / 0❌ / 3 skipped / 4 running at last
read; I will confirm 30 of 30 before calling it mergeable.

Still outstanding on your make vendor ask — and I have been reporting its status wrongly, so:
timeout N cmd in this environment is a harness shim that backgrounds the command and prints its
own notice to stdout, which polluted the log I was reading, and my pgrep -f 'pcapkit.vendor' was
matching its own command line. Both "still running" reports were unfounded. The pcapkit-vendor
console script also resolves against the venv's editable install rather than a worktree, so it
reports invalid vendor updater: LinkType for a target that is in vendor.__all__. Re-running it
through an explicit worktree-first entry point; I will post the real diff, or the real absence of
one, rather than another premature zero.

@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 27, 2026
@JarryShaw
JarryShaw merged commit 5e25db2 into main Sep 27, 2026
31 checks passed
@JarryShaw
JarryShaw deleted the gh-775-tier1-no-mint-on-lookup branch September 27, 2026 12:16
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Sep 27, 2026
JarryShaw added a commit that referenced this pull request Sep 27, 2026
get()'s string-key miss and every generated registry's _missing_
bounded-range branch both called aenum.extend_enum() on an unrecognised
value, so e.g. Hardware(40) minted a permanent member on first call,
forever. Fixed at the two shared sites in pcapkit/vendor/default.py, plus
a new register(value, name) classmethod for the caller-named path that is
still meant to mint.

All 105 generated pcapkit/const/ registries are hand-edited to match and
proved by regenerating the 21 changed ones from live IANA data --
byte-identical. get()'s fix applies to all 105 registries; the _missing_
fix only changes runtime behaviour for the 21 whose vendor crawler leaves
Vendor.process() unmodified. Two toolkit call sites and four protocol
reader sites now raise MissingKeyError/ProtocolError instead of getting a
minted placeholder back. 1026 minting call sites remain out of scope,
deferred to a later tier.
JarryShaw added a commit that referenced this pull request Sep 27, 2026
…ap_type

pcapkit/toolkit/pyshark.py resolved a frame's link type from
packet.layers[0].layer_name, a Wireshark display-filter name, with a
blanket Enum_LinkType.get(name.upper()) fallback. One filter name serves
several encapsulations, so that fallback returned wrong DLTs silently --
null for a DLT_LOOP capture, 101 (RAW) for rawip6.

Re-keyed onto frame.encap_type, which is distinct per encapsulation, and
the fallback is gone: an unrecognised encapsulation or filter name now
raises MissingKeyError. Both lookup tables were measured through
editcap/tshark round trips rather than transcribed from Wireshark source,
which is not on the build host: ENCAP_TYPE_TO_LINKTYPE grows to 152
entries and FILTER_NAME_TO_LINKTYPE grows from #838's 2 entries to 58.
JarryShaw added a commit that referenced this pull request Sep 27, 2026
Adds util/pyshark_encap_map.py, a generator that regenerates
ENCAP_TYPE_TO_LINKTYPE (152 entries) and FILTER_NAME_TO_LINKTYPE (58) in
place inside pcapkit/toolkit/pyshark.py, so the two tables #850 hand-built
stop being hand-maintained. Guards against its own worst failure mode: a
naive LinkType(dlt) lookup cannot detect an unmapped DLT, because
_missing_ mints a placeholder rather than raising, so the generator
snapshots every known value before any lookup.

The real-tshark sweep is gated on a new HAS_WIRESHARK flag and is
version-pinned -- editcap -T accepts 226 encapsulations on Wireshark
4.6.9, 224 on 4.2.2 (CI's Ubuntu noble) -- so the count assertions run
only under the measured version. One line of pyshark.py itself changes as
a result: the IPMB_LINUX table entry becomes I2C_LINUX, the canonical
name #848 gave value 209.

util/changelog_md.py regenerated CHANGELOG.md for all seven entries added
across this and the six preceding commits (#838, #846, #847, #848, #849,
#850, #853); `--check` exit 0.
JarryShaw added a commit that referenced this pull request Sep 27, 2026
#846 and #850

- #838's "the 21 whose vendor crawler leaves Vendor.process() unmodified"
  measures 22, not 21 -- pcapkit.const.hip.transport.Transport also has an
  unmodified process(), but its FLAG-bound range has no unassigned gap, so
  its _missing_ carries no bounded-range extend_enum branch to fix. The
  real discriminator is that branch, not the process() override; reworded,
  and the "other 84" split into the 83 that do override process() and the
  one that doesn't but has nothing to fix either. Re-derived independently
  against 5e25db2^..5e25db2 (not main, which would fold in #847's own
  edit to ipx/socket.py): 22 registries have no process() override, 21 of
  them have the bounded-range branch, matching
  tests/const/test_const_enum_no_mint.py's own REGISTRIES_WITH_UNASSIGNED_RANGES
  (21 entries) vs ALL_REGISTRIES (22, +hip.transport, with a NOTE explaining
  the exclusion).

- #846's "Only tests/integration/test_engine_runtime.py and
  test_engine_parity.py change" is false -- 30bca99's own numstat also
  touches .github/workflows/unit-tests.yml (17+/2-). Qualified to "only
  these two test files change".

- #850 gets the breaking marker: its own prose already says the fallback
  is gone and an unrecognised encapsulation or filter name now raises
  MissingKeyError instead of substituting a plausible DLT -- the same
  shape as the two existing markers at AppType.get (1.5.0.rst:2456) and
  the four .get()-backed enum fields (1.5.0.rst:2720). Restructured to
  lead with the marker and the subject, matching their wording and
  placement; the later restatement of the same fact is dropped.

None of these are code or test changes -- prose only, matching the
cross-review's own framing. #848 stays unmarked (its own "Not breaking"
paragraph holds); #838, #849 and #853 stay unmarked per explicit
instruction not to add markers beyond what was asked.
JarryShaw added a commit that referenced this pull request Sep 27, 2026
util/changelog_md.py regenerated from 1.5.0.rst after the #838/#846/#850
corrections; --check exit 0.
JarryShaw added a commit that referenced this pull request Sep 27, 2026
…registries (#775)

- classify every one of the 89 `_missing_` bodies still calling `extend_enum`
  against the owner's ruling on #775/#847: a final concrete assigned name
  mints, a notation for the reader (Unassigned/Reserved/Deprecated/etc.)
  unmints
- convert 82 registries wholly and two more (`EtherType`, `Socket`) partially,
  172 branches total, from `extend_enum` to `_unregistered_member`, in both
  the vendor crawler and the generated const file, following #838's/#858's
  precedent; proved a 5-file sample (Form-A and Form-B crawlers, both mixed
  registries) regenerates byte-identically
- leave 89-82=7 untouched: `CGAType`'s mint is not an IANA-style range at all,
  and 6 files sit on classes without `EnumRegistry` yet (`AppType` and
  friends), tracked separately by #860 pending #859
- extend `tests/const/test_const_enum_no_mint.py` with the ruling-derived
  registry lists and behavioural/source coverage for all 82+2, correct its
  stale "~92 still mint" docstring to the measured 89, and fix collateral
  breakage in three sibling test files and `tests/vendor/test_ipx_socket_
  unit.py` that pinned the pre-ruling mint behaviour

- fix the last two collateral pins the ruling invalidates, in files the first
  pass missed and CI caught: `tests/corekit/test_fields_numbers_unassigned_
  enum.py` expected `BlockType(0x0bad0bad).name == 'Reserved_0bad0bad'`, now
  `Reserved` with the value asserted explicitly since the name no longer
  carries it; and `tests/protocols/misc/test_pcapng_unit.py` read
  `FilterType.Unassigned_0` by attribute, a member that existed only because
  of the import-time mint at `pcapkit/protocols/misc/pcapng.py:4593` which
  this change removes

Build: plain `unittest` on tests/const (179), tests/vendor (86),
tests/corekit/test_fields_numbers_unassigned_enum.py (11) and
tests/protocols/misc/test_pcapng_unit.py (92 + 1 skipped) all green; both
newly-fixed files fail with their const file reverted to main.
JarryShaw added a commit that referenced this pull request Sep 27, 2026
…registries (#775)

- classify every one of the 89 `_missing_` bodies still calling `extend_enum`
  against the owner's ruling on #775/#847: a final concrete assigned name
  mints, a notation for the reader (Unassigned/Reserved/Deprecated/etc.)
  unmints
- convert 82 registries wholly and two more (`EtherType`, `Socket`) partially,
  172 branches total, from `extend_enum` to `_unregistered_member`, in both
  the vendor crawler and the generated const file, following #838's/#858's
  precedent; proved a 5-file sample (Form-A and Form-B crawlers, both mixed
  registries) regenerates byte-identically
- leave 89-82=7 untouched: `CGAType`'s mint is not an IANA-style range at all,
  and 6 files sit on classes without `EnumRegistry` yet (`AppType` and
  friends), tracked separately by #860 pending #859
- extend `tests/const/test_const_enum_no_mint.py` with the ruling-derived
  registry lists and behavioural/source coverage for all 82+2, correct its
  stale "~92 still mint" docstring to the measured 89, and fix collateral
  breakage in three sibling test files and `tests/vendor/test_ipx_socket_
  unit.py` that pinned the pre-ruling mint behaviour

- fix the last two collateral pins the ruling invalidates, in files the first
  pass missed and CI caught: `tests/corekit/test_fields_numbers_unassigned_
  enum.py` expected `BlockType(0x0bad0bad).name == 'Reserved_0bad0bad'`, now
  `Reserved` with the value asserted explicitly since the name no longer
  carries it; and `tests/protocols/misc/test_pcapng_unit.py` read
  `FilterType.Unassigned_0` by attribute, a member that existed only because
  of the import-time mint at `pcapkit/protocols/misc/pcapng.py:4593` which
  this change removes

Build: plain `unittest` on tests/const (179), tests/vendor (86),
tests/corekit/test_fields_numbers_unassigned_enum.py (11) and
tests/protocols/misc/test_pcapng_unit.py (92 + 1 skipped) all green; both
newly-fixed files fail with their const file reverted to main.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
get()'s string-key miss and every generated registry's _missing_
bounded-range branch both called aenum.extend_enum() on an unrecognised
value, so e.g. Hardware(40) minted a permanent member on first call,
forever. Fixed at the two shared sites in pcapkit/vendor/default.py, plus
a new register(value, name) classmethod for the caller-named path that is
still meant to mint.

All 105 generated pcapkit/const/ registries are hand-edited to match and
proved by regenerating the 21 changed ones from live IANA data --
byte-identical. get()'s fix applies to all 105 registries; the _missing_
fix only changes runtime behaviour for the 21 whose vendor crawler leaves
Vendor.process() unmodified. Two toolkit call sites and four protocol
reader sites now raise MissingKeyError/ProtocolError instead of getting a
minted placeholder back. 1026 minting call sites remain out of scope,
deferred to a later tier.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…ap_type

pcapkit/toolkit/pyshark.py resolved a frame's link type from
packet.layers[0].layer_name, a Wireshark display-filter name, with a
blanket Enum_LinkType.get(name.upper()) fallback. One filter name serves
several encapsulations, so that fallback returned wrong DLTs silently --
null for a DLT_LOOP capture, 101 (RAW) for rawip6.

Re-keyed onto frame.encap_type, which is distinct per encapsulation, and
the fallback is gone: an unrecognised encapsulation or filter name now
raises MissingKeyError. Both lookup tables were measured through
editcap/tshark round trips rather than transcribed from Wireshark source,
which is not on the build host: ENCAP_TYPE_TO_LINKTYPE grows to 152
entries and FILTER_NAME_TO_LINKTYPE grows from #838's 2 entries to 58.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
Adds util/pyshark_encap_map.py, a generator that regenerates
ENCAP_TYPE_TO_LINKTYPE (152 entries) and FILTER_NAME_TO_LINKTYPE (58) in
place inside pcapkit/toolkit/pyshark.py, so the two tables #850 hand-built
stop being hand-maintained. Guards against its own worst failure mode: a
naive LinkType(dlt) lookup cannot detect an unmapped DLT, because
_missing_ mints a placeholder rather than raising, so the generator
snapshots every known value before any lookup.

The real-tshark sweep is gated on a new HAS_WIRESHARK flag and is
version-pinned -- editcap -T accepts 226 encapsulations on Wireshark
4.6.9, 224 on 4.2.2 (CI's Ubuntu noble) -- so the count assertions run
only under the measured version. One line of pyshark.py itself changes as
a result: the IPMB_LINUX table entry becomes I2C_LINUX, the canonical
name #848 gave value 209.

util/changelog_md.py regenerated CHANGELOG.md for all seven entries added
across this and the six preceding commits (#838, #846, #847, #848, #849,
#850, #853); `--check` exit 0.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
#846 and #850

- #838's "the 21 whose vendor crawler leaves Vendor.process() unmodified"
  measures 22, not 21 -- pcapkit.const.hip.transport.Transport also has an
  unmodified process(), but its FLAG-bound range has no unassigned gap, so
  its _missing_ carries no bounded-range extend_enum branch to fix. The
  real discriminator is that branch, not the process() override; reworded,
  and the "other 84" split into the 83 that do override process() and the
  one that doesn't but has nothing to fix either. Re-derived independently
  against 5e25db2^..5e25db2 (not main, which would fold in #847's own
  edit to ipx/socket.py): 22 registries have no process() override, 21 of
  them have the bounded-range branch, matching
  tests/const/test_const_enum_no_mint.py's own REGISTRIES_WITH_UNASSIGNED_RANGES
  (21 entries) vs ALL_REGISTRIES (22, +hip.transport, with a NOTE explaining
  the exclusion).

- #846's "Only tests/integration/test_engine_runtime.py and
  test_engine_parity.py change" is false -- 30bca99's own numstat also
  touches .github/workflows/unit-tests.yml (17+/2-). Qualified to "only
  these two test files change".

- #850 gets the breaking marker: its own prose already says the fallback
  is gone and an unrecognised encapsulation or filter name now raises
  MissingKeyError instead of substituting a plausible DLT -- the same
  shape as the two existing markers at AppType.get (1.5.0.rst:2456) and
  the four .get()-backed enum fields (1.5.0.rst:2720). Restructured to
  lead with the marker and the subject, matching their wording and
  placement; the later restatement of the same fact is dropped.

None of these are code or test changes -- prose only, matching the
cross-review's own framing. #848 stays unmarked (its own "Not breaking"
paragraph holds); #838, #849 and #853 stay unmarked per explicit
instruction not to add markers beyond what was asked.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
util/changelog_md.py regenerated from 1.5.0.rst after the #838/#846/#850
corrections; --check exit 0.
JarryShaw added a commit that referenced this pull request Oct 2, 2026
#719

Swept tests/ for owner-ruling quotes attributed to the wrong GitHub
thread, the same defect class #719 fixed under pcapkit/. Confirmed each
by grepping the quote's distinctive text against the cited thread's
body/comments; a hit elsewhere is a re-quote or a different thread's
own words, not the source.

- test_sentinel_exports_unit.py: the already-reported #937->#719 fix
  for AbsentType's privacy ruling.
- test_enum_lookup_reparent_930_unit.py (4 sites) and test_mh_unit.py:
  "I prefer (2) directly" and the question that drew it are in pull
  request #940's thread, not issue #935 -- #935 only carries the first
  ruling ("I lean on 1").
- test_vendor_snapshot_restore_unit.py: the contextlib/atomic-write
  ruling is in pull request #873's thread; issue #872 has zero
  comments.
- test_vendor_reg_apptype_generator_unit.py (2 sites): the "undefined
  direct uses 0" ruling is in pull request #874's thread, not issue
  #860 or #770.
- test_const_enum_no_mint.py (2 sites): the mint/unmint criterion was
  settled on pull request #847 and confirmed on #775 -- the reverse of
  what the text said, per #861's own description of the same ruling;
  and "Q1 - bare it is." is pull request #838's thread, not #775's.

One occurrence left unresolved rather than guessed at: the "Preserve
each branch's existing name argument..." quote (4 sites in
test_const_enum_no_mint.py, attributed to "#775's final round") does
not appear verbatim in #775, #847, or #878 (the implementing PR) by
body, comments, review comments, or commit message -- only a
paraphrase in #878's own PR description/commit message, which is the
author's prose rather than a quoted ruling. Flagged for the owner
rather than fixed.

tests/corekit/, tests/vendor/, tests/const/ pass (400/16, 118, 299
respectively, pcapkit.__file__ confirmed inside this worktree);
tests/protocols/internet/test_mh_unit.py passes standalone (52/0) --
the full directory has 5 unrelated pre-existing failures from
ungenerated examples/captures/ fixtures, untouched by this change.

Refs #719
JarryShaw added a commit that referenced this pull request Oct 2, 2026
…quest (#719) (#982)

* docs(pcapkit,ci): cite the issue a defect belongs to, not the pull request (#719)

Per the owner's ruling on #719, replace every reference to a pull-request
number in pcapkit/** and .github/workflows/** comments and docstrings with
the issue it closed, or a description where no issue covers it.

- 92 real PR citations in pcapkit/ (93 was the estimate; the gap is RFC
  packet-diagram and hex-format-spec false positives, plus one cross-repo
  issue citation that only coincidentally matched a PyPCAPKit PR number).
- 13 PR citations in .github/workflows/, matching the estimate exactly.
- Several citations named two or three numbers for one claim where a PR
  closed several issues, or several PRs closed the same issue;
  deduplicated rather than left reading "#425 and #425".

Four review rounds caught the same category error recurring: several sites
had relocated a verbatim quote or a specific finding into the issue number
rather than describing where the ruling was actually given, so the quote
no longer existed where the sentence pointed. Fixed each by naming the
issue while locating the ruling honestly -- "a ruling given in review of
the work for #N" -- the same shape already used on this repo's conventions
docs. Two sites needed the inverse correction instead: the #923 quote in
enum.py/exceptions.py genuinely is recorded on #923's own thread, just
attributed there to the pull request that implemented it, so those read
"a ruling recorded on GitHub issue #923" rather than pointing elsewhere.
Also fixed a lost conjunction and an ordinal/number mismatch in
corekit/enum.py, a self-contradicting below/above pointer repeated across
three internet/ files, and a number collision in http.py where one issue
ended up naming both a defect and the change that closed it.

Final sweep: grepped the whole tree for the word "verbatim" -- the marker
that makes a quote-attribution claim falsifiable -- across all 30 files
under pcapkit/ that carry it, and checked every quote this way names
against the actual issue thread. Caught two more of the same defect:
vendor/__main__.py's #872 citation (the quote is in the implementing pull
request's review, not #872 itself) and four sites across mh.py
attributing to #935 a ruling that only exists in the review of the pull
request that implemented it -- #935's own thread holds just the
superseded widen-not-delete proposal. Both fixed the same way. Every
other quote-bearing claim the sweep found -- #911, #937, three distinct
#877 quotes, both #842 quotes, and the #860/#808/#806/#886/#917 rewrites
from earlier in this pass -- resolves to the thread it names.

Verified: targeted pytest across every touched module passes, including
the test that pins the vendor/const apptype.py get() region as
byte-identical, reconfirmed after each amendment. Both edited workflow
YAML files parse before and after with unchanged key counts.

* docs(corekit): cite #719, not #937, for the AbsentType ruling

AbsentType's docstring attributed the owner's "document it as private
type/class... not for public use is enough" quote to #937. #937 itself
quotes that ruling verbatim under "The owner's ruling, verbatim (from
#719)" -- it re-attributes rather than originates it. Per the house
rule to cite the issue a ruling was settled on
(docs/source/contributing/conventions/documentation.rst:196-200), point
the attribution at #719 and re-wrap the paragraph to the file's
existing ~78-column width. The neighbouring, unrelated #937 citation
describing what #937 did to sentinel naming is untouched.

tests/corekit/ passes (400 passed, 16 skipped) against this worktree's
own pcapkit (confirmed via pcapkit.__file__); pylint on the file is
9.77/10, unchanged by this edit -- the one finding is a pre-existing,
unrelated too-few-public-methods warning on NoValueType.

* docs(tests): re-point six ruling citations at their actual threads, per #719

Swept tests/ for owner-ruling quotes attributed to the wrong GitHub
thread, the same defect class #719 fixed under pcapkit/. Confirmed each
by grepping the quote's distinctive text against the cited thread's
body/comments; a hit elsewhere is a re-quote or a different thread's
own words, not the source.

- test_sentinel_exports_unit.py: the already-reported #937->#719 fix
  for AbsentType's privacy ruling.
- test_enum_lookup_reparent_930_unit.py (4 sites) and test_mh_unit.py:
  "I prefer (2) directly" and the question that drew it are in pull
  request #940's thread, not issue #935 -- #935 only carries the first
  ruling ("I lean on 1").
- test_vendor_snapshot_restore_unit.py: the contextlib/atomic-write
  ruling is in pull request #873's thread; issue #872 has zero
  comments.
- test_vendor_reg_apptype_generator_unit.py (2 sites): the "undefined
  direct uses 0" ruling is in pull request #874's thread, not issue
  #860 or #770.
- test_const_enum_no_mint.py (2 sites): the mint/unmint criterion was
  settled on pull request #847 and confirmed on #775 -- the reverse of
  what the text said, per #861's own description of the same ruling;
  and "Q1 - bare it is." is pull request #838's thread, not #775's.

One occurrence left unresolved rather than guessed at: the "Preserve
each branch's existing name argument..." quote (4 sites in
test_const_enum_no_mint.py, attributed to "#775's final round") does
not appear verbatim in #775, #847, or #878 (the implementing PR) by
body, comments, review comments, or commit message -- only a
paraphrase in #878's own PR description/commit message, which is the
author's prose rather than a quoted ruling. Flagged for the owner
rather than fixed.

tests/corekit/, tests/vendor/, tests/const/ pass (400/16, 118, 299
respectively, pcapkit.__file__ confirmed inside this worktree);
tests/protocols/internet/test_mh_unit.py passes standalone (52/0) --
the full directory has 5 unrelated pre-existing failures from
ungenerated examples/captures/ fixtures, untouched by this change.

Refs #719

* docs(tests): paraphrase four fabricated or altered owner quotations (#719)

Per #719's citation ruling (de-quote, never reproduce a verbatim quote that
may have come from outside GitHub):

- test_const_enum_no_mint.py (4 sites): a quotation attributed to "the
  owner's ruling, verbatim" never appears in #775, #847, #861 or #878 (or
  anywhere in the repo's comment corpus). Replaced with a paraphrase
  attributed to PR #878's own body, which carries the real design note in
  different words.
- test_sentinel_exports_unit.py / test_const_registry_protocol.py: a quote
  attributed to #911 silently dropped half of what the owner wrote on #719
  and swapped `__all__` for "users". Replaced with a paraphrase naming #719
  as where it was settled and #911 as the issue that carried it out.
- test_const_enum_no_mint.py / test_const_enum_builtin_parity.py (4 sites):
  a "verbatim" quote of #860 silently corrected the owner's typo ("entires"
  -> "entries"). Paraphrased, which drops the question of reproducing or
  flagging the typo.
- test_enum_lookup_reparent_930_unit.py: "the owner's final ruling there"
  had #935 as its nearest antecedent instead of #940; named #940 explicitly
  and paraphrased the adjacent quote.

Verified: ast.parse and reST markup pairing clean on every touched file;
tests/const (299 tests) and the targeted pytest sweep of all touched files
(261 passed, 2013 subtests) are green. tests/corekit's full discover run
shows 5 pre-existing failures in test_sentinel_exports_unit.py, confirmed
identical on the unedited originals -- a cross-file test-order dependency
unrelated to this change.

* test(vendor,corekit): fix a surviving fabricated ruling and a wrong citation (#719)

- tests/vendor/test_ipx_socket_unit.py: the "owner's ruling" attribution for
  keeping the hex-suffixed Xerox name survived in this file after the prior
  commit removed the same false attribution from four sites in
  test_const_enum_no_mint.py. Reworded to credit PR #878's own design note,
  matching the wording already used at the repaired sites.
- tests/corekit/test_sentinel_exports_unit.py: the docstring cited the #719
  export ruling ("only export objects, not types") as grounds for keeping
  ABSENT out of __all__, but ABSENT is an object, so that ruling argues for
  including it, not excluding it. Re-grounded the sentence on the privacy
  ruling already quoted ~15 lines below instead, without re-quoting it.

Both changes are prose-only: tokenizing each file before and after with
comments and docstrings stripped produces identical token sequences.
tests/vendor passes 118/118 except one pre-existing, test-order-dependent
flake in test_vendor_snapshot_restore_unit.py (reproduces identically on the
pre-edit tree); tests/project/test_conventions_doc_claims.py passes 38/38.

* test(corekit,const): narrow the blanket paraphrase, restoring quotations that cite correctly (#719)

The last two commits paraphrased every disputed owner quotation away.
That was right for one case and wrong for two: a quotation that exists
nowhere has to be paraphrased, but a quotation that is real and was
only cited to the wrong thread lost its audit trail for nothing, since
the defect was the pointer, not the words. Per the owner's ruling,
narrow the fix to match.

Restored as quotations, correctly cited:

- tests/corekit/test_sentinel_exports_unit.py (~L4-7) and
  tests/const/test_const_registry_protocol.py (~L1363): the sentinel
  export rule, split back into its two real sources instead of one
  spliced sentence -- #719's "we should ONLY export the objects ...
  and leave the types ... out", and #911's own "we only expose the
  final objects to users", with #911 noted as both executor and
  source.

- tests/const/test_const_enum_no_mint.py (~L88, ~L1876, ~L2363) and
  tests/const/test_const_enum_builtin_parity.py (~L655): the #860
  minting ruling, including its load-bearing first sentence ("I think
  we should not mint on get still actually") and the owner's own
  "entires" typo, marked [sic] rather than silently corrected.

Left alone: the four #878 fabricated-quote sites in
test_const_enum_no_mint.py, which cite no real thread and stay
paraphrased, and the ABSENT privacy sentence, which is a correct
paraphrase of a different ruling.

Verified: ast.parse and reST markup clean on all four files; code
token sequences (docstrings/comments stripped) identical before and
after; each restored quotation substring-matches its source comment
after whitespace/markup normalisation. tests/const: 299 OK. tests/
project/test_conventions_doc_claims: 38 OK, 1 skipped.

* test(corekit,const): convert restored quotations to statements with context, per #719

The previous commit restored eight verbatim quotations to fix a narrowing
that had dropped their context. The owner has since ruled that neither
form is right: a narrowed paraphrase without context does not help a
reader who was not in the thread, but a verbatim quotation makes the
docstring read as a discussion rather than documentation.

- Sentinel export rule (corekit/test_sentinel_exports_unit.py,
  const/test_const_registry_protocol.py): state that a module's `__all__`
  lists a sentinel's object but deliberately leaves its type out, and why
  (the type is not part of the public surface), citing #719 as where it
  was settled and #911 as where the implementing work belongs.
- #860 minting rule, four sites (const/test_const_enum_no_mint.py x3,
  const/test_const_enum_builtin_parity.py): state that `get()` must not
  mint and only `register()` creates a new entry, and why (only
  IANA-registered values are legitimate and `get()` lacks the information
  to construct one), citing #860. Each site is fitted to its own
  surrounding prose rather than one paragraph pasted four times. Drops
  the `[sic]` each quotation carried, since there is nothing left to
  reproduce.
- Fixed two sentences left orphaned by the quotations' removal: an
  antecedent ("the three") that depended on the deleted quote's wording,
  and a sentence whose "get() as well as _missing_" had the emphasis
  backwards relative to the rule's own subject.

The four PR #878 paraphrase sites in test_const_enum_no_mint.py were
already in this third form and are unchanged.

Verified: ast.parse on all four files; tokenize with comments and
docstrings stripped shows an identical token sequence before/after
(prose-only); tests/const (299) and tests/project/test_conventions_doc_claims.py
(38, 1 skip) pass; tests/corekit (400, 5 failures, 16 skipped) matches the
documented pre-existing sentinel-identity failures.

* test(corekit,const): state the remaining owner rulings in our own words, per #719

The four files still carried owner-attributed quotations beside the eight
converted earlier, so each read half as documentation and half as a thread.

- Replace each quoted ruling with a statement of the rule, the reason a
  reader needs, and the issue where it was given (#842, #864, #775, #860,
  #911, #719, #647, #808, #759, #857).
- Rename the dangling "privacy ruling quoted below" reference to point at
  the statement that replaced the quotation.
- Leave RFC text, code literals and ordinary prose untouched.

Prose only: tokens with comments and docstrings stripped are identical
before and after; tests/const 299 OK, tests/corekit unchanged (5 known).

* test(const): restore ruling citations to the pull requests that carry them, per #719

tests/ is exempt from the issue-citation rule (documentation.rst, ruled on
#719): the fact cited lives in the pull request, not the issue.

- PR #836 restored for the TransportProtocol-extension refusal, the
  |-composite decoding retirement, and the stale-comment deletion; the
  rulings are not on #808 at all.
- PR #783 for the f-string convention; PR #847 for the mint criterion.
- The de-quotation stands: wording stays as statements, no quotation marks.
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
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) const Regenerated IANA or vendor constant tables; members keep their numeric values fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant