Skip to content

fix(reg)!: drop TransportProtocol's IntFlag base now that nothing composes - #836

Merged
JarryShaw merged 1 commit into
mainfrom
fix/808-drop-transportprotocol-intflag-base
Sep 26, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/808-drop-transportprotocol-intflag-base

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

Closes #808. Unblocked by #806, which merged as #815 and retyped every member's proto to a single transport — leaving nothing that builds or relies on a composite TransportProtocol value. The premise was verified with an AST sweep (validated against a known-positive control) plus the maintainer's own AST-sweep test before anything was touched.

This is a breaking change. TransportProtocol's base moves from aenum.IntFlag to a plain aenum.IntEnum, so &, ^, ~ and |'s member-composing behaviour are gone — tcp | udp falls through to int.__or__ and returns a bare int. The four transports keep their exact existing values (tcp=1, udp=2, sctp=4, dccp=8, undefined=0), not renumbered. _missing_ is removed: a plain IntEnum already rejects an undeclared value, so TransportProtocol(3) now raises ValueError where it used to compose tcp|udp. Also breaking and easy to miss: list(TransportProtocol) now yields all five members instead of four, because Flag hid the zero-valued undefined from iteration and plain IntEnum does not. Every member's own repr/str/.name/.value stay byte-identical, and nothing in pcapkit iterates the class bare — only through __members__, which is 5 either way.

Two maintainer rulings on this PR shaped the rest of it, beyond what #808 asked for.

First: "Do not allow extension of TransportProtocol at all." TransportProtocol.get no longer mints on a miss — the extend_enum call and the max_val computation are gone, and an unrecognised name raises ValueError. That closes a pre-existing defect on the way past: because .get() lowercases but does not strip, tcp and tcp used to mint permanent members literally named ' tcp' and 'tcp '. They now raise. Whether to strip was considered and ruled against, to keep the convention engine selection already sets at extraction.py:922, which lowercases only.

Second: "since it's no longer a Flag, | joined values are no longer parsed and accepted, we will treat it as a whole, instead of splitting." AppType._dispatch no longer decomposes proto through show_flag_values; a value that names no registry is refused whole. show_flag_values and ProtocolError are consequently unused here and their imports are dropped.

User-visible exception-type change. AppType.get(80, proto=3) and proto=17 both raised ProtocolError before and both raise ValueError now — 3 names no transport protocol registry of AppType. A bare int was off-contract before this PR (proto: TransportProtocol | str); the annotation now includes int, so this is a change on a newly supported input rather than a regression. Two smaller moves in the same direction: proto=None and proto=2.5 raised TypeError before — from ~ inside the bit decomposition — and now raise ValueError. One move the other way, and the only input anywhere that went from raising to resolving: proto=1.0 and proto=2.0 raised TypeError before and now return http [80 - tcp] and http [80 - udp], because hash(1.0) == hash(TransportProtocol.tcp) and the registries dict is int-keyed. A float is off-contract either way; stock's rejection was an accident of ~ and the new acceptance is an accident of dict hashing, so neither is designed behaviour.

pcapkit/foundation/registry/protocols.py is touched for one comment only, no behaviour: its NOTE justified preferring __members__ over TransportProtocol[...] on the grounds that Flag.__getitem__ silently composes 'tcp|udp' into 3 and that the class is an IntFlag — both false once the base is dropped. The guard itself is still right and unchanged: __getitem__ raises a bare KeyError where this function's contract is RegistryError, and a bare int still hashes and compares equal to its matching member.

Untouched: the four per-transport generated registries. Re-pinned after measuring rather than assuming: tests/dumpkit/test_nameless_enum_rendering_unit.py's sweep at 7→6 flag registries and 4→3 _missing_ field widths, with len(nameless) deliberately left at 5 because TransportProtocol was never in that set.

Applied identically to the vendor generator template, with the rendered output diffed against the committed const file to confirm they still agree byte-for-byte.

Tests: an unrecognised name refused rather than minted, a |-spelled string refused as an ordinary unknown, a bare-int composite refused whole, TransportProtocol no longer extensible at runtime, and the base/value/iteration-count pins — each checked to fail before the change and pass after.

@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) refactor Restructuring for its own sake — neither a fix nor a new capability (refactor: prefix) breaking Breaks public-facing behaviour or API (apply alongside the type label) const Regenerated IANA or vendor constant tables; members keep their numeric values test Pull requests that add or correct tests (test: subject prefix) review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Sep 26, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES — changing .get()'s extension from max_val * 2 to max_val + 1 breaks the single-bit invariant that _dispatch still depends on, because show_flag_values is still used to interpret proto.

Measured on this head (1c6d93c41) against 3118ed796, each in its own worktree:

                  PR #836                                         stock 3118ed796
get('bogus') ->   value=9   bits=[1, 8]                            value=16  bits=[16]
AppType.get(80, proto=it):
  PR     -> ProtocolError: tcp|dccp names 2 transport protocols, and so 2 registries…
  stock  -> ValueError: <TransportProtocol.bogus: 16> names no transport protocol registry

get('quux') → 10 → udp|dccp; get('zork') → 11 → tcp|udp|dccp. Every value from 9 up is multi-bit, so every minted member now takes the composite branch and names transports the caller never mentioned. AppType.get(80, proto='tcp|udp') reports tcp|dccp for the same reason.

The root error is keeping show_flag_values on a type that is no longer a flag. Since a composite can no longer be a member, _dispatch should test proto against the four real registries directly and let anything else — bare int or minted member — fall into "names no transport protocol registry".

Two more, smaller:

  • list(TransportProtocol) goes 4 → 5: Flag hid the zero pseudo-member, IntEnum does not. Nothing in-tree iterates it (apptype.py:63 uses __members__, 5 on both), so it is not a break, but a breaking PR body should say it.
  • TransportProtocol.get('tcp|udp') still mints a junk member rather than refusing. register_apptype refuses it; the const class's own .get() does not.

@JarryShaw
JarryShaw force-pushed the fix/808-drop-transportprotocol-intflag-base branch from 1c6d93c to 2fba732 Compare September 26, 2026 05:47
@JarryShaw

Copy link
Copy Markdown
Owner Author

Fix pushed as 2fba732bc (amended, one commit on 3118ed796). Verified independently — every branch measured on the new head:

list(TransportProtocol)              ['undefined','tcp','udp','sctp','dccp']   (5, disclosed in the body)
TP.get('tcp|udp')                    ValueError: 'tcp|udp' is not a valid TransportProtocol   (no longer mints)
TP.get('bogus') -> 9
AppType.get(80, proto=minted member)  ValueError: <TransportProtocol.bogus: 9> names no transport protocol registry
AppType.get(80, proto=TP.tcp)         <TCP.http: 80 [tcp]>
AppType.get(80, proto='tcp')          <TCP.http: 80 [tcp]>
AppType.get(80, proto=bare int 3)     ProtocolError: tcp|udp names 2 transport protocols…   (correct text now)
AppType.get(80, proto=bare int 17)    ValueError: 17 names no transport protocol registry   (stray bit)

So a minted member no longer gets bit-decomposed, and the bit-decomposition path survives only for a genuine bare int all of whose bits are real transports — which is the pre-existing tested behaviour for hand-built composites. The tcp|dccp-for-tcp|udp misreport is gone.

The third flag-count pin is fixed and the two previously-untested pins were measured rather than guessed: registries 7 → 6, nameless unchanged at 5 (TransportProtocol was never in the nameless set), widths 4 → 3. I re-derived the registry count independently — exactly 6, the survivors being CommandType, Flags and the four mh flags — and tests/dumpkit/test_nameless_enum_rendering_unit.py now runs 6 tests, all OK. Stale prose in that file updated too.

Back to review: pending for the new head; a cross-review is running.

@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

Copy link
Copy Markdown
Owner Author

Cross-review (sonnet) on 2fba732bc: GOOD TO GO on the code — all seven items confirmed. Generator/const agreement proven properly by rendering the template's BASE(...) and diffing all three regions outside the TABLE/MISS insertion points, byte-empty each time. _missing_'s removal changes nothing: TP(99), TP(None), TP(-1) and a string all raise ValueError identically on both trees, and True/False/1.0/0 resolve identically; only TP(3) differs, which is the fix. Sequential minting collides with nothing, re-requesting a minted name returns the same object, and the | refusal catches nothing legitimate — on stock those same inputs silently minted members literally named 'tcp|udp'. register_apptype still refuses a composite, now as RegistryError: unknown transport protocol: 3. 104 tests under pytest, cross-checked at 104 under plain unittest.

Two narration fixes wanted before this is ready, both verified by me:

stock first mint   value=16  bits=[16]    <-- single-bit; stock never misreads as a composite
PR   first mint    value=9   bits=[1, 8]
  1. test_a_minted_protocol_names_no_registry_rather_than_a_composite's docstring narrates the "misread as composite" defect as a property of stock. It is not — stock's max_val * 2 always lands on a single bit, so the defect existed only in this PR's own intermediate round. The test does fail on stock, but at assertEqual(int(minted), 9), not at the ProtocolError assertion the prose describes. Same objection I raised on fix(reg,corekit): retype the registry NULL sentinel and raise ProtocolError from ModuleDescriptor.klass #835's doc page: prose that asserts something untrue sends the next reader down a wrong path, and nothing will surface it.
  2. The commit message claims "89 tests via plain unittest"; the measured figure is 104, matching pytest's own total.

One pre-existing defect, out of scope here, recorded so it is not lost: TP.get(' tcp') and TP.get('tcp ') mint junk members named ' tcp' and 'tcp ' rather than resolving to the real tcp, identically on both trees (stock 32/64, PR 10/11) — while TP.get('TCP') correctly case-folds. That belongs to #775, which unblocks when this merges.

@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
JarryShaw force-pushed the fix/808-drop-transportprotocol-intflag-base branch from 2fba732 to 3567359 Compare September 26, 2026 06:19
@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO — amended to 3567359e2, and the earlier code verdict carries because this round changed no code at all.

git diff --stat 2fba732bc 3567359e2 -- pcapkit/     -> empty
git diff --stat 2fba732bc 3567359e2                 -> tests/const/test_const_apptype_split_unit.py | 25 +/ 9 -

Every changed line is docstring prose. The corrected docstring now states the thing I asked for and states it accurately: the misread-as-composite defect existed only in an intermediate round of this PR, stock 3118ed796's max_val * 2 keeps every minted value a fresh single bit (16, 32, 64, …) so the coherent "names no registry" branch always fires there, and the test fails on stock only because the two minting schemes disagree on a number — at assertEqual(int(minted), 9) with AssertionError: 16 != 9 — not because stock shares the defect. Test assertions unchanged.

The commit message's count is corrected to 104 tests under plain unittest (77 tests/const/ + 15 registry + 6 vendor generator + 6 dumpkit), matching pytest's own total. The earlier "89" came from running four individual files inside tests/const/ rather than the directory.

Zero stash entries in the main checkout and the worker's worktree; no leftover scratch worktrees.

Granting review: good-to-go. I will report it ready to merge once CI completes on this head — the label tracks the verdict, inc=0 gates the readiness call.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Sep 26, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Merged-together integration check — clean. CI only ever proved each PR alone: #834, #835 and #836 were all cut from 3118ed796, so whichever of them merges second and third meets a main it was never tested against. That matters for this pair specifically, because #836 established that pcapkit/foundation/registry/protocols.py needed no change against main — not against #835's rewritten version of it.

Merged all three into a scratch worktree at origin/main: no conflict, 15 files, +1306 −295. Targeted suites on the combined tree, pcapkit.__file__ asserted inside it:

tests/foundation/registry                      19 tests   OK
tests/const                                    77 tests   OK
tests/corekit                                 240 tests   OK (skipped=16)
tests/dumpkit/test_common_unit                 14 tests   OK
tests/dumpkit/test_nameless_enum_rendering      6 tests   OK
tests/dumpkit/test_plist_escaping_regression    4 tests   FAILED (errors=6)  <-- pre-existing

That last one is not this change. Verified with a stock control — a clean origin/main worktree gives the same 6 errors and the same cause: FileNotFoundError: sample capture 'test.pcapng' not found … most of the sample captures are generated, not committed. A fresh worktree simply has no fixtures until python examples/generators/make_samples.py runs.

So 356 tests pass on the combined tree with zero failures attributable to the merge, and the three may be merged in any order.

Comment thread pcapkit/const/reg/apptype/apptype.py Outdated
# next integer after the current maximum.
max_val = max(TransportProtocol.__members__.values())
return extend_enum(TransportProtocol, key.lower(), max_val * 2)
return extend_enum(TransportProtocol, key.lower(), max_val + 1)

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.

Do not allow extension of TransportProtocol at all.

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.

Taking this as a ruling — TransportProtocol stops being extensible. Checked what depends on minting first, and it is narrow:

  • The only in-tree caller is _dispatch's own TransportProtocol.get(proto.lower()) (apptype.py:2400, and the generator's copy at vendor/.../apptype.py:355), reached only when proto arrives as a str. So refusing an unknown name turns a silently-minted junk member into a ValueError at that one site.
  • Nothing else calls .get() with a name that could miss; tests/vendor/test_vendor_reg_apptype_generator_unit.py:183 pins the call-site count at 1, which a changed miss path does not disturb.

This also subsumes the pre-existing defect I recorded on #775: TP.get(' tcp') and TP.get('tcp ') currently mint members literally named ' tcp' and 'tcp ' because .get() lowercases but does not strip. With extension gone they raise instead, so that case dies here rather than waiting for #775.

Sending it back to implement: drop the extend_enum call and the max_val computation, refuse an unrecognised name the same way the | case already does, and fold the two probes that currently assert minting (TP.get('bogus') -> 9 and the minted-member _dispatch test) into assertions that it is refused. _dispatch's bare-int composite branch stays — a bare int can still reach it by hand even when no member can be minted.

review: good-to-go withdrawn until that lands.

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.

debating but maybe we should strip the protocol after lower cased as well?

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.

Measured the question rather than guessed, and my recommendation is refuse, do not strip — but it is a judgement call and one word from you overrides it.

There is no house precedent for stripping. Across pcapkit/const/ (132 enum files), zero use .strip() anywhere, and TransportProtocol.get is the only one that case-folds at all. The other two get() shapes differ from each other, let alone from this one:

pcapkit/const/tcp/flags.py:80      return Flags[key]        # exact, no folding at all
pcapkit/const/ftp/command.py:306   name = key.upper()       # folds, then STILL mints on miss
pcapkit/const/reg/apptype/…:76     key.lower()              # folds, minting removed by your ruling

And no in-tree source can produce a padded name. proto reaches .get() only as a keyword argument to AppType.get / register_apptype — programmatic, not parsed text — and the generator never calls .get() with IANA column data at generation time; tests/vendor/test_vendor_reg_apptype_generator_unit.py:183 pins the generated base module to exactly one TransportProtocol.get( call site. So ' tcp' can only arrive from a caller bug.

The asymmetry that decides it for me: before your ruling ' tcp' silently minted junk, which is the actual defect. After it, ' tcp' raises ValueError — loud and correct. Stripping would make it succeed, which is a third behaviour and the only one that hides the caller's mistake. .lower() earns its keep because IANA canonically spells TCP while members are lowercase; there is no equivalent upstream that emits whitespace.

One scoping note so the ruling is not over-applied: ftp/command.py:306 still mints on a miss, and should — FTP commands are genuinely open-ended, whereas this registry is partitioned over a closed set of four transports. Dropping extensibility is right here and would be wrong there.

Labelling needs: decision and holding the worker on the refuse path meanwhile. Say the word and I will add .strip() instead.

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.

Commented in the main thread.

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.

And yes, follow house convention and thus your recommendation.

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.

Confirmed and already in flight — no .strip(), following the engine-selection convention (extraction.py:922 lowercases only; no .strip() anywhere in pcapkit/foundation/). needs: decision is cleared.

One thing I should not overstate: your "yes, follow house convention and thus your recommendation" landed ten seconds after I posted a separate reading in the main thread, so I am treating it as confirming the .strip() recommendation only, not the other one.

Still my assumption rather than your ruling, flagged there and repeated here so it does not slip through: I read "treat it as a whole, instead of splitting" as covering the bare-int path too, so _dispatch stops decomposing proto via show_flag_values and a bare 3 will report "3 names no transport protocol registry" instead of "tcp|udp names 2 transport protocols". It is the coherent consequence of having no Flag base and it drops the last show_flag_values use from the apptype family — but it does change a user-visible message on an on-contract input, so one word reverses it if you meant only the string side.

Comment thread pcapkit/const/reg/apptype/apptype.py
@JarryShaw

Copy link
Copy Markdown
Owner Author

Added two inline comments

@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: good-to-go Cross-review at the current head says ready; CI state is separate labels Sep 26, 2026
@JarryShaw
JarryShaw force-pushed the fix/808-drop-transportprotocol-intflag-base branch from 3567359 to a5539ed Compare September 26, 2026 13:01
@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES — amended to a5539ed26, rebased onto ad4805f5f, one commit. Your ruling is implemented and every branch measured, but the change left a dead code path behind.

Verified on the new head:

get('bogus') / 'tcp|udp' / ' tcp' / 'tcp ' / '|'   ValueError (no minting)
get('TCP') / get('tcp')                            <TransportProtocol.tcp: 1>
__members__ 5 -> 5 after all probes                not extensible
AppType.get(80, proto='tcp')                       http [80 - tcp]
AppType.get(80, proto=TP.tcp|TP.udp)  (bare int)   ProtocolError: …names 2 transport protocols…

The defect. TransportProtocol.get now ends with two raises that are character-for-character identical:

if '|' in key:
    raise ValueError(f'{key!r} is not a valid {TransportProtocol.__name__}')
raise ValueError(f'{key!r} is not a valid {TransportProtocol.__name__}')

So the '|' branch cannot change any outcome — it is unreachable in effect. Worse, the 11-line NOTE above it (:77-87) still argues that | must be intercepted before minting because a minted composite-named member would lie about being a single transport. Nothing mints any more, so that rationale has evaporated and the comment now describes code that no longer exists — the third round of stale narration on this PR.

Two acceptable shapes, and I have asked the worker to pick one and justify it: delete the branch and its NOTE, folding any surviving rationale into a short comment on the single raise; or keep the branch and give it a genuinely different, more useful message, e.g. naming that 'tcp|udp' looks like a composite and that a port should be looked up under one transport at a time. What is not acceptable is two identical raises with a comment explaining a mechanism that is gone.

Everything else checks out: parent is ad4805f5f, seven files, zero extend_enum and zero .strip() inside get() (extracted by AST — the raw file counts are meaningless, since AppType's own _missing_ legitimately uses extend_enum hundreds of times and the generator's crawler uses .strip() for IANA CSV parsing), no _missing_ on TransportProtocol, key.lower() untouched, zero stash entries.

The needs: decision on stripping is unaffected — still yours, still unimplemented either way.

@JarryShaw
JarryShaw force-pushed the fix/808-drop-transportprotocol-intflag-base branch from a5539ed to abee187 Compare September 26, 2026 13:10
@JarryShaw

Copy link
Copy Markdown
Owner Author

Dead branch fixed in abee187f7 (one commit, parent ad4805f5f). It took option 2 — a distinct message rather than deleting the branch — and the distinction is real, verified at runtime:

get('tcp|udp') / get('|') / get('a|b')     'X' names more than one transport protocol; resolve
                                           one at a time rather than passing a '|'-joined name
get('bogus') / get(' tcp') / get('tcp ')   'X' is not a valid TransportProtocol

TransportProtocol.get extracted by AST: 0 extend_enum, 0 .strip(), two raises that now differ. __members__ stays 5 across repeated probes. The test pins the distinction in both directions — assertIn('more than one transport protocol', composite_message) and assertNotIn(…, generic_message) at tests/const/test_const_apptype_split_unit.py:1167,1178 — so the branch cannot silently collapse back to dead code. Generator template re-rendered byte-identically; 108 tests across the four selections; mypy 114/38 with an identical error set before and after; isort clean; zero stash entries.

It also removed three stale apptype.py:96 line references it had left in test docstrings and the commit message, which had already drifted from its own edits.

One imprecision I am deliberately not spending a round on: for the pathological input '|', the message says it "names more than one transport protocol" when strictly it names none. The actionable half — don't pass a |-joined name — is correct for that input too, no caller produces a bare '|', and this PR has already had three round-trips, two of them for prose. Flagging it rather than blocking; say the word if you want it tightened.

Head is new, so the previous verdict does not carry: back to review: pending with a cross-review dispatched on a different model from the author. needs: decision stands — the .strip() question is still yours and still unimplemented either way.

…poses

Closes #808. Blocked on #806 (merged as #815), which retyped every
member's `proto` to a single transport, leaving nothing that builds
or relies on a composite `TransportProtocol` value.

- `TransportProtocol` becomes a plain `aenum.IntEnum`. The four
  transports keep their exact values (tcp=1, udp=2, sctp=4, dccp=8,
  undefined=0) via `cast(...)` rather than `auto()`, since IntEnum's
  `auto()` numbers sequentially and would renumber them.
- Removed `_missing_`'s composing fallback/range guard (a plain
  IntEnum's default `_missing_` already rejects anything undeclared).
- `.get()` refuses an unrecognised name outright rather than minting
  one -- maintainer ruling: "Do not allow extension of
  TransportProtocol at all." It used to mint at `max_val + 1` (an
  intermediate revision of this PR; stock still doubles, `max_val *
  2`); there is nothing left to walk now. Two earlier rounds of this
  PR gave a `'|'`-containing string, e.g. `'tcp|udp'`, its own
  distinct message on the theory that it names a composite rather
  than merely an unknown name; the owner's final ruling drops that
  distinction outright rather than refining it -- "since it's no
  longer a Flag, `|` joined values are no longer parsed and accepted,
  we will treat it as a whole, instead of splitting" -- so `'|'` gets
  the identical generic refusal any other unrecognised name does.
  `.get()` still only case-folds, never strips whitespace, per the
  same ruling: the owner pointed at engine selection
  (`extraction.py:922`, lower-only, no `.strip()` anywhere in
  `pcapkit/foundation/`) as the convention to match, and normalising
  is the only part of that convention adopted -- engine selection
  warns and falls back to a default on a miss, `.get()` still raises.
- `_dispatch` no longer decodes a bare-int `proto`'s bits at all,
  matching the same ruling: a composite built by hand, e.g.
  `TransportProtocol.tcp | TransportProtocol.udp` (a bare `int` since
  `|` falls through to `int.__or__` now), used to be split via
  `show_flag_values` into a `ProtocolError` naming every transport
  whose bit was set -- the GitHub issue #759 fix, present on stock
  and refined once more in an intermediate round of this PR to tell
  a clean composite (`3`, real bits only) apart from a stray bit
  (`17`, one real bit plus one nothing declares). The owner's ruling
  retires that decoding entirely rather than refining it further: a
  bare-int composite is now refused exactly like any other value
  naming no registry -- one plain `ValueError`, whether the int is
  `3`, `17`, or `TransportProtocol.undefined`. This removes the last
  use of `show_flag_values` and of `ProtocolError` from this module,
  so both imports are dropped along with the docstring `Raises:`
  entries naming `ProtocolError` on `_dispatch`/`get`/`get_all`.
  User-visible consequence: `AppType.get(80, proto=17)` and
  `AppType.get(80, proto=3)` were both `ProtocolError` on stock
  `ad4805f5f` and are both `ValueError` now. Neither is a regression
  on a *supported* input -- a bare `int` was off-contract until this
  PR widened `proto`'s annotation to include it -- but the exception
  type a caller now sees for that input has changed.
- Widened `proto`'s type annotation to include `int` across
  `_dispatch`/`get`/`get_all`, and cast at the one dict-key site mypy
  cannot infer, to match the type mypy actually needs to stay clean.
- Applied identically to the vendor generator template; verified the
  generated `TransportProtocol` class and `_dispatch`/`get` bodies are
  byte-identical between the two by rendering the template's `BASE`
  lambda directly against text extracted from the committed const
  file, rather than running the network-dependent vendor crawl.
- `pcapkit/foundation/registry/protocols.py` (in scope once #835
  merged into `ad4805f5f`): corrected `register_apptype`'s own NOTE,
  which justified resolving a string transport via `__members__`
  rather than `TransportProtocol[name]` with two claims this PR made
  false -- that `Flag.__getitem__` parses `'tcp|udp'` into the value
  `3`, and that `TransportProtocol` is an `IntFlag`. Neither holds
  once `|` is retired: `TransportProtocol['tcp|udp']` now raises a
  bare `KeyError`, same as `TransportProtocol['bogus']`, which is
  the corrected reason `__members__.get(...)` is still used -- this
  function's own contract is `RegistryError` on a miss, not
  `KeyError`. The `registries.get(1)` conclusion right after it is
  unchanged and stays: `hash(TransportProtocol.tcp) == hash(1)`
  regardless of the base, so an `int` key still hits a
  `TransportProtocol`-keyed dict entry. No behaviour changed here,
  only the comment explaining it.
- `tests/dumpkit/test_nameless_enum_rendering_unit.py`'s flag-registry
  sweep drops from 7 to 6 registries (TransportProtocol no longer
  matches `issubclass(_, aenum.Flag)`) and from 4 to 3 distinct
  `_missing_` field widths; re-measured and re-pinned rather than
  assumed, prose updated to match.
- Updated tests pinning removed Flag mechanics and two enum-sweep
  size pins (Flag count 7->6, IntEnum count 111->112). Converted every
  test whose premise the rulings above removed: the `.get('bogus')`
  minting probe and its misread-as-composite regression now assert
  refusal instead; the composite-string test lost its distinct-message
  assertions; the bare-int composite test
  (`test_a_bare_int_composite_is_refused_as_a_whole`, renamed from
  `test_a_proto_naming_two_transports_is_refused_rather_than_resolved`)
  now asserts the identical plain `ValueError` for `3` that a stray
  bit and `undefined` already got, through all three entry points;
  and `test_transport_protocol_can_no_longer_be_extended_at_runtime`
  pins the registration's removal rather than its shape. Corrected two
  rounds of stale narration a cross-review caught along the way: three
  comments/docstrings citing a `TransportProtocol.__getitem__`
  contrast that no longer has anything to contrast (there is no
  composite-specific branch left to justify), and two docstrings
  attributing the `max_val + 1` minting scheme to stock rather than to
  this PR's own now-superseded intermediate revision -- stock mints at
  `max_val * 2` (`16` for `'quic'`/`'bogus'`), measured on `ad4805f5f`.
  Also normalised three `3118ed796` "stock" references to `ad4805f5f`
  for consistency with the rebased base, since the claims hold at
  either commit.
- Corrected an earlier claim: `list(TransportProtocol)` now yields all
  five members (4 on stock) since `Flag` hid the zero-valued
  `undefined` from iteration and plain `IntEnum` does not. Per-member
  repr/str/name/value are still byte-identical; nothing in-tree
  iterates the class bare, only through `__members__` (5 either way).

`register_apptype` and its own tests needed no change beyond the NOTE
above: they already reject anything that is not
`isinstance(proto, TransportProtocol)`, which a bare int (what `|`
now produces) satisfies identically, and its own no-strip case-fold
resolution was already the model `.get()`'s normalisation follows.

Built and tested against current `main` (`ad4805f5f`): `tests/const/`
(77), `tests/foundation/registry/` (19),
`tests/vendor/test_vendor_reg_apptype_generator_unit.py` (6) and
`tests/dumpkit/test_nameless_enum_rendering_unit.py` (6) all pass via
plain unittest, 108 total. mypy (114 errors/38 files) is identical
before and after this change once line-number drift from the new
`protocols.py` comment is accounted for -- zero new errors -- and
isort is clean on all three touched source files.
@JarryShaw
JarryShaw force-pushed the fix/808-drop-transportprotocol-intflag-base branch from abee187 to d2772d1 Compare September 26, 2026 13:44
@JarryShaw

Copy link
Copy Markdown
Owner Author

All three rulings and F1–F3 land in d2772d17e (one commit, parent ad4805f5f, now eight files since protocols.py came into scope). Verified every branch myself:

get('tcp|udp') / get('bogus') / get(' tcp')   ValueError: 'X' is not a valid TransportProtocol   (one generic refusal)
get('TCP')                                    <TransportProtocol.tcp: 1>
AppType.get(80, proto='tcp') / proto=TP.tcp    <TCP.http: 80 [tcp]>
AppType.get(80, proto=3) / proto=17            ValueError: N names no transport protocol registry of AppType
AppType.get_all(80, proto='tcp')               3 results — identical to stock

Ruling 3 checked for a regression rather than assumed. AppType.get_all(80) and get(80) with the default undefined sentinel raise ValueError on the PR — and identically on stock ad4805f5f, so the sentinel's behaviour is unchanged. I flagged this as a possible regression before measuring it; the stock control refutes that.

show_flag_values and ProtocolError imports are both gone from the const file and the generator. The two remaining textual mentions are a single NOTE that records the history and quotes your ruling verbatim — correct provenance, not stale narration.

F1 fixed with the right reason, not a patched sentence. The replacement NOTE drops both false claims and explains what the guard is actually for: __getitem__ raises a bare KeyError on a miss where this function's contract is RegistryError, and a bare int still hashes and compares equal to its member — IntEnum versus IntFlag changing neither. register_apptype verified still working: member, 'tcp' and case-folded 'TCP' all resolve, 3 gives RegistryError: unknown transport protocol: 3, 'nope' likewise.

F2 fixed — max_val + 1 / "minted 9" are now attributed to this PR's intermediate revision, with stock ad4805f5f's max_val * 2 named as the contrast. F3 fixed — zero 3118ed796 references remain. Generator renders byte-identically (215,013 chars); 108 tests across the four selections; mypy 114/38 with the only diff a +7 line shift in protocols.py from added comment lines; zero stash entries.

For the record, and now in scope since a bare int is on-contract: AppType.get(80, proto=3) and proto=17 were both ProtocolError on stock and are both plain ValueError now. Not a regression — a bare int was off-contract before this PR widened the annotation — but a user-visible exception-type change, and it is the direct consequence of your "treat it as a whole" ruling.

New head, so no verdict carries: review: pending, delta cross-review dispatched.

@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: needs-changes Cross-review at the current head says changes are required; see the verdict comment 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

GOOD TO GO — delta cross-review (opus) on d2772d17e. F1, F2 and F3 all verified fixed, and it could not falsify any of the six new items. One non-blocking finding, which I re-derived:

                stock ad4805f5f                          PR d2772d17e
proto=1.0       TypeError: bad operand type for unary ~   <TCP.http: 80 [tcp]>
proto=2.0       TypeError                                 <UDP.http: 80 [udp]>
proto=2.5       TypeError                                 ValueError  (toward contract)
proto=None      TypeError                                 ValueError  (toward contract)

A float proto now resolves where stock raised — the only input in its 590-row differential matrix that went from raising to resolving. Cause is ordinary dict semantics: hash(1.0) == hash(TransportProtocol.tcp) and __registries__ is int-keyed. A float is off-contract either way (proto: TransportProtocol | str | int); stock's rejection was an accident of ~ and the new acceptance is an accident of hashing, so neither is designed. Not gating on it, and it is now written into the PR body alongside the None/2.5 moves.

The asymmetry I briefed as the worst case has no reachable instance — but with a correction to the reviewer's own numbers. It reported 5 except ProtocolError sites including http.py:139,308,430; I measure 3, at esp.py:1103, http.py:117, ngap.py:1028. Its count and lines are wrong. The conclusion stands: none of those three files references AppType or apptype at all (0 hits each), and every production call site passes a hardcoded member — transport/transport.py:187, schema/transport/{tcp,udp,sctp}.py. So nothing catches ProtocolError from an apptype lookup and would now miss a ValueError.

Also confirmed independently by the review: strings show zero resolution flips and zero exception-type changes across 'bogus', ' tcp', 'tcp ', 'tcp|udp', '|', '', 'TCP|UDP', 'quic' — all raised ValueError on stock too, after minting junk, and raise it now; the 135 int-phase type changes are all the intended composite ProtocolError → ValueError. Generator/const PRE region a 6457-char exact byte match, so neither ruling can be reverted by a regeneration. Overlaying the five test files onto stock gives 10 failed / 72 passed / 39136 subtests. protocols.py's 23-line diff is entirely comment lines.

One number corrected in passing: the const file is 215,014 chars, not 215,013 — a trailing newline, identical on stock, so both figures describe agreement.

Granting review: good-to-go. CI is at 21✅/0❌/6 running on this head; I will report it ready to merge once that completes.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Ready to merge. CI on d2772d17e is complete and clean — 27 ✅ / 0 ❌ / 3 skipped / 0 incomplete at job level — and the PR sits as exactly one commit on top of current main (ad4805f5f, its own parent, so no rebase needed).

State for the record:

head          d2772d17e        one commit, parent ad4805f5f == origin/main
CI            27 ok / 0 fail / 0 incomplete
verdict       GOOD TO GO  (opus delta cross-review; author was sonnet)
labels        fix, test, breaking, refactor, const, review: good-to-go
closes        #808

breaking is on it deliberately: TransportProtocol moves from aenum.IntFlag to aenum.IntEnum, so tcp | udp yields a bare int, list(TransportProtocol) iterates five members rather than four, TransportProtocol(3) raises instead of composing, .get() no longer mints, and composite proto values are refused whole. All of it is in the body, along with the ProtocolError → ValueError change on proto=3/proto=17 and the incidental float widening.

Unpublished decisions left where they belong rather than taken silently: the '|' special case and whitespace stripping were both your calls, recorded on the threads above; the float acceptance is flagged in the body as accidental on both sides rather than designed, so it is visible if you would rather it were guarded.

Merging is yours. When this lands it closes #808, which unblocks #775 and #807, and #816 is expected to dissolve with #775.

@JarryShaw
JarryShaw merged commit 14d3d3d into main Sep 26, 2026
31 checks passed
@JarryShaw
JarryShaw deleted the fix/808-drop-transportprotocol-intflag-base branch September 26, 2026 14:24
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Sep 26, 2026
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…t minting (#860)

Step 2 of #860 (PR 2 of 2, following AppType's 8 non-AppType siblings in #869).

- Mix pcapkit.const.reg.apptype.apptype.AppType into EnumRegistry, alongside
  its four transport subclasses (TCP/UDP/SCTP/DCCP). AppType keeps its own
  get/get_all/register_alias, which already dispatch by transport protocol,
  and gains a working register (previously absent -- the base's generic one
  would have built a member with svc='<null>') and an _unregistered_member
  override reconstructing svc/port/proto.
- Convert AppType._missing_'s 766 range-bounded extend_enum calls, plus the
  one more inside get()'s own second mint site, to _unregistered_member --
  owner ruling: "only IANA registered ones are legit values ... get will not
  have sufficient information to create new ones." All 766+1 branches convert
  uniformly, including the 8 that named a real (if span-assigned rather than
  individually declared) service, since none of them mints at import time --
  unlike FEATCode's earlier fix, there is no self-mutation defect here to
  address by declaring members statically.
- Move TransportProtocol from power-of-two values to sequential auto(), per
  GitHub issue #836's ruling retiring `|`-composite decoding and the owner's
  further #860 ruling that the spacing itself then had nothing left to
  protect; delete the stale comment claiming the values must stay
  power-of-two. AppType._dispatch treats a composed or bare int identically
  as a whole either way, so the renumbering changes what specific integers
  mean, not only what hand-composed ones do -- e.g. a bare, uncomposed 4
  (previously sctp's value) now silently resolves as dccp's, with no
  exception, since a real member sits at 4 under either numbering. No test
  pins any of this, per the owner's explicit instruction; the class comment
  states it instead.
- Update corekit/enum.py's own docstring, which described AppType's
  EnumRegistry mixin as future work ("stays as it is until tier two lands"),
  and its get() docstring's str-valued-registry census (119 -> 124).
- Update tests/vendor/test_vendor_reg_apptype_generator_unit.py's #770 pin,
  which asserted the old bare-literal-vs-auto() distinction that no longer
  exists now that every TransportProtocol member (undefined included) is
  auto()-valued: under auto(), a member's own value infers as Any, which is
  assignable to TransportProtocol with no cast at all, so no single member's
  wrapper is load-bearing against a mypy error any more -- measured directly
  against all three shapes (baseline, undefined unwrapped, undefined
  reverted to the bare literal 0). What the mypy test still depends on is
  staying on auto() at all, not on which member is wrapped; its own "4
  errors" claim is corrected to the 3 [assignment] + 762 [arg-type] this
  tree's own get()/get_all() Union-typed proto (tolerates a bare int
  default) and the new _unregistered_member site (still plainly typed)
  actually produce once reverted that far.
- Fix four tests in the existing suite that pinned the old minting/power-of-
  two behaviour directly (test_const_apptype_split_unit.py,
  test_const_enum_builtin_parity.py), and add 17 new tests plus prose/count
  updates in test_const_enum_no_mint.py and test_const_registry_protocol.py.

All 5 crawlers regenerate byte-identically on a second run; git status is
clean relative to this commit. mypy/pylint deltas are the same classes of
finding this codebase already tolerates elsewhere (aenum stub gaps,
import-outside-toplevel/protected-access/no-member in test internals),
suppressed with # type: ignore[override]/pylint: disable=arguments-differ,
arguments-renamed where the codebase's own convention already does so.
403 test methods pass across the complete tests/const/ and
tests/protocols/transport/ directories, plus 87 across tests/vendor/
(excluding the live-network crawler-reachability test) and 55 across the
TransportProtocol-consuming suites outside those directories.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…t minting (#860)

Step 2 of #860 (PR 2 of 2, following AppType's 8 non-AppType siblings in #869).

- Mix pcapkit.const.reg.apptype.apptype.AppType into EnumRegistry, alongside
  its four transport subclasses (TCP/UDP/SCTP/DCCP). AppType keeps its own
  get/get_all/register_alias, which already dispatch by transport protocol,
  and gains a working register (previously absent -- the base's generic one
  would have built a member with svc='<null>') and an _unregistered_member
  override reconstructing svc/port/proto.
- Convert AppType._missing_'s 766 range-bounded extend_enum calls, plus the
  one more inside get()'s own second mint site, to _unregistered_member --
  owner ruling: "only IANA registered ones are legit values ... get will not
  have sufficient information to create new ones." All 766+1 branches convert
  uniformly, including the 8 that named a real (if span-assigned rather than
  individually declared) service, since none of them mints at import time --
  unlike FEATCode's earlier fix, there is no self-mutation defect here to
  address by declaring members statically.
- Move TransportProtocol from power-of-two values to sequential auto(), per
  PR #836's ruling retiring `|`-composite decoding and the owner's further
  #860 ruling that the spacing itself then had nothing left to protect;
  delete the stale comment claiming the values must stay power-of-two.
  AppType._dispatch treats a composed or bare int identically as a whole
  either way, so the renumbering changes what specific integers mean, not
  only what hand-composed ones do -- e.g. a bare, uncomposed 4 (previously
  sctp's value) now silently resolves as dccp's, with no exception, since a
  real member sits at 4 under either numbering. No test pins any of this,
  per the owner's explicit instruction; the class comment states it
  instead, and every docstring that used to claim a hand-built composite is
  refused unconditionally (_dispatch's and get's own) is corrected to say
  it is looked up as a whole and resolves or is refused depending on
  whether some real member happens to equal it.
- Update corekit/enum.py's own docstring, which described AppType's
  EnumRegistry mixin as future work ("stays as it is until tier two lands"),
  and its get() docstring's str-valued-registry census (119 -> 124).
- Update tests/vendor/test_vendor_reg_apptype_generator_unit.py's #770 pin,
  which asserted the old bare-literal-vs-auto() distinction that no longer
  exists now that every TransportProtocol member (undefined included) is
  auto()-valued: under auto(), a member's own value infers as Any, which is
  assignable to TransportProtocol with no cast at all, so no single member's
  wrapper is load-bearing against a mypy error any more -- measured directly
  against all three shapes (baseline, undefined unwrapped, undefined
  reverted to the bare literal 0). What the mypy test still depends on is
  staying on auto() at all, not on which member is wrapped; its "4 errors"
  claim is corrected to the 3 [assignment] + 762 [arg-type] this tree
  produces once reverted that far, and to the two separate changes that
  moved the count there: PR #836 widened get()/get_all()'s own proto
  annotations to tolerate a bare int default before this issue touched
  anything, and this issue's own new _unregistered_member site is what
  brought the count back up from 2 to 3.
- Fix four tests in the existing suite that pinned the old minting/power-of-
  two behaviour directly (test_const_apptype_split_unit.py,
  test_const_enum_builtin_parity.py), and add 17 new tests plus prose/count
  updates in test_const_enum_no_mint.py and test_const_registry_protocol.py.

All 5 crawlers regenerate byte-identically on a second run; git status is
clean relative to this commit. mypy/pylint deltas are the same classes of
finding this codebase already tolerates elsewhere (aenum stub gaps,
import-outside-toplevel/protected-access/no-member in test internals),
suppressed with # type: ignore[override]/pylint: disable=arguments-differ,
arguments-renamed where the codebase's own convention already does so.
403 test methods pass across the complete tests/const/ and
tests/protocols/transport/ directories, plus 87 across tests/vendor/
(excluding the live-network crawler-reachability test) and 55 across the
TransportProtocol-consuming suites outside those directories.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…t minting (#860)

Step 2 of #860 (PR 2 of 2, following AppType's 8 non-AppType siblings in #869).

- Mix pcapkit.const.reg.apptype.apptype.AppType into EnumRegistry, alongside
  its four transport subclasses (TCP/UDP/SCTP/DCCP). AppType keeps its own
  get/get_all/register_alias, which already dispatch by transport protocol,
  and gains a working register (previously absent -- the base's generic one
  would have built a member with svc='<null>') and an _unregistered_member
  override reconstructing svc/port/proto.
- Convert AppType._missing_'s 766 range-bounded extend_enum calls, plus the
  one more inside get()'s own second mint site, to _unregistered_member --
  owner ruling: "only IANA registered ones are legit values ... get will not
  have sufficient information to create new ones." All 766+1 branches convert
  uniformly, including the 8 that named a real (if span-assigned rather than
  individually declared) service, since none of them mints at import time --
  unlike FEATCode's earlier fix, there is no self-mutation defect here to
  address by declaring members statically.
- Move TransportProtocol from power-of-two values to sequential auto(), per
  PR #836's ruling retiring `|`-composite decoding and the owner's further
  #860 ruling that the spacing itself then had nothing left to protect;
  delete the stale comment claiming the values must stay power-of-two.
  AppType._dispatch treats a composed or bare int identically as a whole
  either way, so the renumbering changes what specific integers mean, not
  only what hand-composed ones do -- e.g. a bare, uncomposed 4 (previously
  sctp's value) now silently resolves as dccp's, with no exception, since a
  real member sits at 4 under either numbering. No test pins any of this,
  per the owner's explicit instruction; the class comment states it
  instead, and every docstring that used to claim a hand-built composite is
  refused unconditionally (_dispatch's and get's own) is corrected to say
  it is looked up as a whole and resolves or is refused depending on
  whether some real member happens to equal it. Also fixed a generator-only
  comment (process()'s own, no const twin) still claiming a member's proto
  "is a single bit" -- true under the old power-of-two spacing, not under
  auto().
- Update corekit/enum.py's own docstring, which described AppType's
  EnumRegistry mixin as future work ("stays as it is until tier two lands"),
  and its get() docstring's str-valued-registry census (119 -> 124).
- Update tests/vendor/test_vendor_reg_apptype_generator_unit.py's #770 pin,
  which asserted the old bare-literal-vs-auto() distinction that no longer
  exists now that every TransportProtocol member (undefined included) is
  auto()-valued: under auto(), a member's own value infers as Any, which is
  assignable to TransportProtocol with no cast at all, so no single member's
  wrapper is load-bearing against a mypy error any more -- measured directly
  against all three shapes (baseline, undefined unwrapped, undefined
  reverted to the bare literal 0). What the mypy test still depends on is
  staying on auto() at all, not on which member is wrapped; its "4 errors"
  claim is corrected to the 3 [assignment] + 762 [arg-type] this tree
  produces once reverted that far, and to the two separate changes that
  moved the count there: PR #836 widened get()/get_all()'s own proto
  annotations to tolerate a bare int default before this issue touched
  anything, and this issue's own new _unregistered_member site is what
  brought the count back up from 2 to 3.
- Fix four tests in the existing suite that pinned the old minting/power-of-
  two behaviour directly (test_const_apptype_split_unit.py,
  test_const_enum_builtin_parity.py), and add 17 new tests plus prose/count
  updates in test_const_enum_no_mint.py and test_const_registry_protocol.py.

All 5 crawlers regenerate byte-identically on a second run; git status is
clean relative to this commit. mypy/pylint deltas are the same classes of
finding this codebase already tolerates elsewhere (aenum stub gaps,
import-outside-toplevel/protected-access/no-member in test internals),
suppressed with # type: ignore[override]/pylint: disable=arguments-differ,
arguments-renamed where the codebase's own convention already does so.
403 test methods pass across the complete tests/const/ and
tests/protocols/transport/ directories, plus 87 across tests/vendor/
(excluding the live-network crawler-reachability test) and 55 across the
TransportProtocol-consuming suites outside those directories.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…t minting (#860)

Step 2 of #860 (PR 2 of 2, following AppType's 8 non-AppType siblings in #869).

- Mix pcapkit.const.reg.apptype.apptype.AppType into EnumRegistry, alongside
  its four transport subclasses (TCP/UDP/SCTP/DCCP). AppType keeps its own
  get/get_all/register_alias, which already dispatch by transport protocol,
  and gains a working register (previously absent -- the base's generic one
  would have built a member with svc='<null>') and an _unregistered_member
  override reconstructing svc/port/proto.
- Convert AppType._missing_'s 766 range-bounded extend_enum calls, plus the
  one more inside get()'s own second mint site, to _unregistered_member --
  owner ruling: "only IANA registered ones are legit values ... get will not
  have sufficient information to create new ones." All 766+1 branches convert
  uniformly, including the 8 that named a real (if span-assigned rather than
  individually declared) service, since none of them mints at import time --
  unlike FEATCode's earlier fix, there is no self-mutation defect here to
  address by declaring members statically.
- Move TransportProtocol from power-of-two values to sequential ones, per
  PR #836's ruling retiring `|`-composite decoding and the owner's further
  #860 ruling that the spacing itself then had nothing left to protect;
  delete the stale comment claiming the values must stay power-of-two.
  undefined is declared as an explicit cast('TransportProtocol', 0) and
  tcp/udp/sctp/dccp continue from it via plain auto(), per the owner's own
  final ruling on the declaration shape ("undefined direct uses 0. then
  other real transport use auto. so we don't have to define a _start_ and
  the undefined declaration is explicit") -- an earlier revision of this
  same change used an explicit _start_ = 0 with every member on auto(),
  which the owner's ruling superseded. AppType._dispatch treats a composed
  or bare int identically as a whole either way, so the renumbering changes
  what specific integers mean, not only what hand-composed ones do -- e.g.
  a bare, uncomposed 4 (previously sctp's value) now silently resolves as
  dccp's, with no exception, since a real member sits at 4 under either
  numbering. No test pins any of this, per the owner's explicit
  instruction; the class comment states it instead, and every docstring
  that used to claim a hand-built composite is refused unconditionally
  (_dispatch's and get's own) is corrected to say it is looked up as a
  whole and resolves or is refused depending on whether some real member
  happens to equal it.
- Update corekit/enum.py's own docstring, which described AppType's
  EnumRegistry mixin as future work ("stays as it is until tier two lands"),
  and its get() docstring's str-valued-registry census (119 -> 124).
- Update tests/vendor/test_vendor_reg_apptype_generator_unit.py's #770 pin:
  undefined is once again the one bare-literal-under-cast member among four
  auto()-valued siblings, exactly as #770 first shaped it, so its wrapper is
  load-bearing against a mypy error again -- measured directly, stripping
  it alone (leaving auto() elsewhere) now reproduces 3 [assignment] + 762
  [arg-type] errors (765 total), while stripping tcp's wrapper instead
  stays clean. The two separate changes behind the 3-not-4 count: PR #836
  widened get()/get_all()'s own proto annotations to tolerate a bare int
  default before this issue touched anything, and this issue's own new
  _unregistered_member site is what brought the count back up from 2 to 3.
- Fix four tests in the existing suite that pinned the old minting/power-of-
  two behaviour directly (test_const_apptype_split_unit.py,
  test_const_enum_builtin_parity.py), and add 17 new tests plus prose/count
  updates in test_const_enum_no_mint.py and test_const_registry_protocol.py.

All 5 crawlers regenerate byte-identically on a second run; git status is
clean relative to this commit. mypy/pylint deltas are the same classes of
finding this codebase already tolerates elsewhere (aenum stub gaps,
import-outside-toplevel/protected-access/no-member in test internals),
suppressed with # type: ignore[override]/pylint: disable=arguments-differ,
arguments-renamed where the codebase's own convention already does so.
216 test methods pass across test_const_apptype_split_unit.py,
test_const_enum_no_mint.py, test_const_enum_builtin_parity.py,
test_const_registry_protocol.py and test_vendor_reg_apptype_generator_unit.py,
plus 87 across tests/vendor/ (excluding the live-network crawler-
reachability test) and 55 across the TransportProtocol-consuming suites
outside those directories.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…t minting (#860)

Step 2 of #860 (PR 2 of 2, following AppType's 8 non-AppType siblings in #869).

- Mix pcapkit.const.reg.apptype.apptype.AppType into EnumRegistry, alongside
  its four transport subclasses (TCP/UDP/SCTP/DCCP). AppType keeps its own
  get/get_all/register_alias, which already dispatch by transport protocol,
  and gains a working register (previously absent -- the base's generic one
  would have built a member with svc='<null>') and an _unregistered_member
  override reconstructing svc/port/proto.
- Convert AppType._missing_'s 766 range-bounded extend_enum calls, plus the
  one more inside get()'s own second mint site, to _unregistered_member --
  owner ruling: "only IANA registered ones are legit values ... get will not
  have sufficient information to create new ones." All 766+1 branches convert
  uniformly, including the 8 that named a real (if span-assigned rather than
  individually declared) service, since none of them mints at import time --
  unlike FEATCode's earlier fix, there is no self-mutation defect here to
  address by declaring members statically.
- Move TransportProtocol from power-of-two values to sequential ones, per
  PR #836's ruling retiring `|`-composite decoding and the owner's further
  #860 ruling that the spacing itself then had nothing left to protect;
  delete the stale comment claiming the values must stay power-of-two.
  undefined is declared as an explicit cast('TransportProtocol', 0) and
  tcp/udp/sctp/dccp continue from it via plain auto(), per the owner's own
  final ruling on the declaration shape ("undefined direct uses 0. then
  other real transport use auto. so we don't have to define a _start_ and
  the undefined declaration is explicit") -- an earlier revision of this
  same change used an explicit _start_ = 0 with every member on auto(),
  which the owner's ruling superseded. AppType._dispatch treats a composed
  or bare int identically as a whole either way, so the renumbering changes
  what specific integers mean, not only what hand-composed ones do -- e.g.
  a bare, uncomposed 4 (previously sctp's value) now silently resolves as
  dccp's, with no exception, since a real member sits at 4 under either
  numbering. No test pins any of this, per the owner's explicit
  instruction; the class comment states it instead, and every docstring
  that used to claim a hand-built composite is refused unconditionally
  (_dispatch's and get's own) is corrected to say it is looked up as a
  whole and resolves or is refused depending on whether some real member
  happens to equal it.
- Update corekit/enum.py's own docstring, which described AppType's
  EnumRegistry mixin as future work ("stays as it is until tier two lands"),
  and its get() docstring's str-valued-registry census (119 -> 124).
- Update tests/vendor/test_vendor_reg_apptype_generator_unit.py's #770 pin:
  undefined is once again the one bare-literal-under-cast member among four
  auto()-valued siblings, exactly as #770 first shaped it, so its wrapper is
  load-bearing against a mypy error again -- measured directly, stripping
  it alone (leaving auto() elsewhere) now reproduces 3 [assignment] + 762
  [arg-type] errors (765 total), while stripping tcp's wrapper instead
  stays clean. The two separate changes behind the 3-not-4 count: PR #836
  widened get()/get_all()'s own proto annotations to tolerate a bare int
  default before this issue touched anything, and this issue's own new
  _unregistered_member site is what brought the count back up from 2 to 3.
- Fix four tests in the existing suite that pinned the old minting/power-of-
  two behaviour directly (test_const_apptype_split_unit.py,
  test_const_enum_builtin_parity.py), and add 17 new tests plus prose/count
  updates in test_const_enum_no_mint.py and test_const_registry_protocol.py.

All 5 crawlers regenerate byte-identically on a second run; git status is
clean relative to this commit. mypy/pylint deltas are the same classes of
finding this codebase already tolerates elsewhere (aenum stub gaps,
import-outside-toplevel/protected-access/no-member in test internals),
suppressed with # type: ignore[override]/pylint: disable=arguments-differ,
arguments-renamed where the codebase's own convention already does so.
216 test methods pass across test_const_apptype_split_unit.py,
test_const_enum_no_mint.py, test_const_enum_builtin_parity.py,
test_const_registry_protocol.py and test_vendor_reg_apptype_generator_unit.py,
plus 87 across tests/vendor/ (excluding the live-network crawler-
reachability test) and 55 across the TransportProtocol-consuming suites
outside those directories (tests/corekit/test_fields_numbers_port_option_
no_mint_unit.py, tests/dumpkit/test_nameless_enum_rendering_unit.py,
tests/dumpkit/test_common_unit.py, tests/foundation/registry/
test_protocols.py, tests/utilities/test_compat.py).
JarryShaw added a commit that referenced this pull request Oct 2, 2026
… 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 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) refactor Restructuring for its own sake — neither a fix nor a new capability (refactor: prefix) test Pull requests that add or correct tests (test: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

reg: drop TransportProtocol's IntFlag base once nothing builds a composite

1 participant