Skip to content

design: is AppType's multi-bit proto necessary, given the enum is already split per transport? #801

Description

@JarryShaw

Is this a bug or a feature request? A design question, raised by the maintainer.

The question, verbatim:

one question tho, should we maintain full transport list in each AppType's enum? like for TCP.http on port 80, the current enum still carries the other transport info like UDP/TSCP/etc. is that necessary? if so, then we dont care. if not, we should then bring back the TransportProtocol retyping.

What it decides. #732's ruling was conditional — "proceed with retyping on TransportProtocol - if we no longer need it to be a Flag. But, if in any case we still prefer using a Flag, then we discard the retyping." This issue is that condition. It exists as its own issue so the reasoning is tracked rather than buried in #732, whose subject is the str-valued/port=-1 shape.

State today. AppType is split into four per-transport registries — TCP 6,147 / UDP 6,143 / SCTP 91 / DCCP 10 = 12,391 members — reached via AppType.__registries__. A member also carries a proto attribute holding a TransportProtocol Flag, which for a service IANA registers on several transports is multi-bit: TCP.http.proto names UDP too. 10,625 of 12,391 rows (85.7%) are multi-bit, so this is most of the enum, not an edge.

Two arguments that the list is redundant:

  1. The split already encodes it. "Which transports bind port 80" is answerable by asking which of the four registries contain it. The multi-bit proto is then a second copy of a fact the structure already carries — and a second copy can drift from the first.
  2. _dispatch already rejects multi-bit. AppType.get(80, TCP|UDP) raises, telling the caller to look one transport up at a time. So the entry point insists on a single transport while the members it returns advertise four.

The one site that decides it is pcapkit/foundation/registry/protocols.py:846, if test not in proto: — the only read known to treat proto as a set rather than a label. Load-bearing if it needs the other bits; decoration if it is only reachable once a registry has been selected.

Being measured now, before any answer — a wrong one costs a 12,391-member migration:

  • Every read of proto across pcapkit/, tests/, docs/: is multi-bit ever the only source of a fact?
  • Derivability: for every port present in more than one registry, does "the set of registries containing it" equal "its proto bit set"? Any disagreement means the two copies have already drifted — a defect independent of this design question.

Findings get appended here as they land. Related: #732, #775, #783.

Activity

  1. added
    questionIssues asking how something works rather than reporting a defect
    designA design or decision issue: a pattern being decided rather than a defect or a request
    wipWork in flight - a covering PR is open or an agent is actively on it
    on Sep 25, 2026
  2. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Measured: the multi-bit proto is fully derivable from the four registries. So no — the full transport list is not necessary.

    Keyed on (service name, port), 7,054 groups, 0 disagreements, checked in both directions:

    • No member's proto omits its own registry — 0 across all four.
    • No single-registry group carries a multi-bit proto — 0. This is the direction the obvious test misses, and it is what closes the argument: proto never claims a transport the registries do not.
    • Arithmetic cross-check falls out exactly. Popcount distribution over all 12,391 members is 1→1766, 2→10486, 3→123, 4→16; group-span distribution is 1→1766, 2→5243, 3→41, 4→4; 5243×2=10486, 41×3=123, 4×4=16. Every member's popcount equals the number of registries its (svc, port) occupies.

    And the generator makes it true by construction — pcapkit/vendor/reg/apptype/apptype.py:806:

    if self.TRANSPORT not in record.protos or record.port == '-1': continue

    A record enters registry X iff X ∈ record.protos, so registry-set and proto-set are the same set. The multi-bit value is a second copy of a fact the split already carries.

    Per-registry: TCP 6147 (5287 multi / 860 single), UDP 6143 (5276 / 867), SCTP 91 (52 / 39), DCCP 10 (10 / 0). Empty proto: 0 — no third case.

    But retyping is not a cheap attribute change, and this is the part worth deciding on. proto.name is baked into each member's underlying str value, not merely its display — vendor/reg/apptype/apptype.py:251, :271, :274:

    temp = '%s [%d - %s]' % (name, value, proto.name)      # __new__ -> obj._value_
    return "<%s.%s: %d [%s]>" % (..., self.proto.name)     # __repr__
    return '%s [%d - %s]' % (self.svc, self.port, self.proto.name)   # __str__

    So TCP['http'].value is 'http [80 - tcp|udp|sctp]' today and would become 'http [80 - tcp]'. That changes the value, repr and str of 10,625 members — a public-contract change deserving breaking, not a refactor. Identity is safe either way: pickle and both copies all return the same object.

    Still outstanding: pcapkit/foundation/registry/protocols.py:833 does proto = code.proto and :846 does if test not in proto: — a real consumer that receives the whole set from one member. The read audit establishing whether it needs the set is still running; that is the last input before I can say the retyping is safe rather than merely lossless.

  3. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Answer: the multi-bit list is not necessary, and my recommendation is to retype. It is load-bearing at exactly one site, and that site is a convenience default that contradicts the rest of the library.

    The one site. register_apptype(member, module) called without an explicit proto=. pcapkit/foundation/registry/protocols.py:833 does proto = code.proto, and the loop at :846 fans registration out across a hardcoded {tcp: TCP, udp: UDP} dict — not __registries__, and SCTP/DCCP are deliberately excluded. I verified it myself on 477ed00c4:

    member: <TCP.http: 80 [tcp|udp|sctp]> proto = 7
    TCP.__proto__[80] changed: True -> Dummy      (displaced httpv1.HTTP)
    UDP.__proto__[80] changed: True -> Dummy      (displaced http.HTTP)
    --- single-bit contrast: <TCP.imap: 143 [tcp]> ---
    TCP.__proto__[143] changed: True
    UDP.__proto__[143] changed: False
    

    Two RegistryWarnings fired, one per registry. That is the whole of the multi-bit value's job.

    Everywhere else the composite is already refused, because it gave wrong answers. Per #759, feeding a member's own .proto back into AppType.get() silently returned the wrong service in 46 of 10,625 cases — including port 888 accessbuilder→cddbp and port 999 puprouter→garcon, services that do not exist on that transport. _dispatch/get/get_all now raise ProtocolError on any composite, and tests/const/test_const_apptype_split_unit.py:709-836 AST-sweeps the library to prove no lookup site ever builds one.

    So the library is internally contradictory today: hand a member to register_apptype and its composite proto is trusted to fan out; hand the same member's .proto to get() and you get a ProtocolError telling you to look one transport up at a time. Retyping removes the contradiction in favour of the half that was arrived at by fixing real bugs.

    What retyping costs — this is what needs your ruling:

    1. breaking, for 10,625 members. proto.name is folded into _value_ at vendor/reg/apptype/apptype.py:251, so TCP['http'].value goes from 'http [80 - tcp|udp|sctp]' to 'http [80 - tcp]'. repr and str change with it. Identity survives pickle and both copies.
    2. register_apptype's no-proto= default loses the fan-out. Either require an explicit proto= from the caller — consistent with _dispatch's "one transport at a time" — or rewrite it to look the port up across __registries__, which is not what it does now. It is documented public API, so external callers may rely on the current default.
    3. Four test sites and the docstrings pin the present behaviour and must change with it: test_const_apptype_split_unit.py:78-80, :630-658, :709-836, tests/foundation/registry/test_protocols.py:421-499, plus vendor/reg/apptype/apptype.py:522-526 and the same text generated into all four const files — "Members carry the whole transport protocol set IANA assigned the service."

    My recommendation: retype, and make register_apptype require an explicit proto= when the fan-out is wanted. It trades a silent convenience for an explicit argument, and it aligns the registration path with the lookup path that was already corrected. Labelling needs: decision — points 1 and 2 are yours, not mine.

  4. added
    needs: decisionWaiting on the maintainer to decide — not blocked by other work
    breakingBreaks public-facing behaviour or API (apply alongside the type label)
    and removed
    wipWork in flight - a covering PR is open or an agent is actively on it
    on Sep 25, 2026
  5. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    I feel like now that we're already using transport specific enums, we should avoid ambiguity from the enums in all sense. so... yes, go with your recommendation.

    one note tho, the Flag type might be useful when doing register_apptype but its not used in other places. so we might actually just make the function signature as register_apptype(port: int, name: str, *transport: TransportProtocol) so that we can supply a list of transport without having to maintain a Flag for it and keeping everything singular and explicit.

  6. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    and maybe allow aliasing, like register_apptype(port: int, name: str, *transport: TransportProtocol, aliases: list[str] = None), where aliases is an optional keyword argument using name as the canonical name with aliases for the port's aliases like port 80's.

  7. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Ruling recorded, in your words:

    I feel like now that we're already using transport specific enums, we should avoid ambiguity from the enums in all sense. so... yes, go with your recommendation.

    one note tho, the Flag type might be useful when doing register_apptype but its not used in other places. so we might actually just make the function signature as register_apptype(port: int, name: str, *transport: TransportProtocol) so that we can supply a list of transport without having to maintain a Flag for it and keeping everything singular and explicit.

    Retyping is accepted. proto becomes the registry's own single transport; the composite goes.

    The varargs idea is the right shape and I agree with the reasoning — "singular and explicit" is exactly what removes the ambiguity, and it beats my own suggestion of a required proto= kwarg, because a kwarg taking a composite keeps the Flag alive for one caller's benefit. Varargs kill it properly.

    But your sketch drops three things the current signature carries, so I want the caller inventory before I write it. Today it is:

    def register_apptype(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]',
                         class_: 'str' = NULL, *, proto: 'TransportProtocol | str' = NULL) -> 'None':
    1. code accepts an Enum_AppType member, not just an int. port: int removes that, which is a larger break than the retyping itself.
    2. module accepts a ModuleDescriptor or a class object, not just a str; class_ is the second half of the string form. A single name: str cannot express either.
    3. *transport would swallow class_, since class_ is currently positional — so existing three-positional-argument calls would silently bind the class name as a transport.

    None of that argues against varargs; it argues that the transport part wants *transport while code/module/class_ need deciding separately. I am measuring every in-repo caller and every place TransportProtocol's Flag-ness is actually relied on (show_flag_values, in tests, | construction) before proposing the exact line — your "not used in other places" is the claim I most want to confirm or refute, since if it holds, TransportProtocol can stop being a Flag entirely and that is a much cleaner end state.

    wip. I will post the concrete signature plus the migration plan here, then split the implementation into its own issues.

  8. added
    wipWork in flight - a covering PR is open or an agent is actively on it
    and removed
    needs: decisionWaiting on the maintainer to decide — not blocked by other work
    on Sep 25, 2026
  9. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    my scatches are just POC not how it should actually look like - just using as a refernce/example of the idea.

  10. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Both recorded, in your words:

    and maybe allow aliasing, like register_apptype(port: int, name: str, *transport: TransportProtocol, aliases: list[str] = None), where aliases is an optional keyword argument using name as the canonical name with aliases for the port's aliases like port 80's.

    my scatches are just POC not how it should actually look like - just using as a refernce/example of the idea.

    Understood, and my three objections were aimed at the wrong target then — withdrawn as written. I read the sketch as a literal signature; the idea is singular-and-explicit transports plus canonical-name-with-aliases, and both survive whatever the final parameter list turns out to be. The audit still runs, because "match the sibling register_*" is what should decide the exact line, not me.

    Aliasing is real, and the numbers are small: 44 ports carry more than one member — 23 TCP (49 members), 21 UDP (45), 0 SCTP, 0 DCCP. Port 80 is exactly your example:

    TCP port 80:  'http'      value='http [80 - tcp|udp|sctp]'   proto=7
                  'www'       value='www [80 - tcp|udp]'         proto=3
                  'www-http'  value='www-http [80 - tcp|udp]'    proto=3
    

    But a flat aliases: list[str] cannot express 9 of those 44 ports, because the members disagree about the transports. http binds SCTP; www and www-http do not. So www is not an alias of http — it is a different registration that happens to share a port. All nine:

    TCP   80 [http 7, www 3, www-http 3]   113 [ident 1, auth 3]      631 [ipp 3, ipps 1]
         888 [accessbuilder 3, cddbp 1]    999 [garcon 1, puprouter 3]  2049 [shilp 3, nfs 7]
    UDP   80 (same three)                  999 [applix 2, puprouter 3]  2049 [shilp 3, nfs 7]
    

    And this is the same defect as the retyping, seen from the other side. Ports 888 and 999 are precisely the two #759 cases where a composite-proto lookup returned a service that does not exist on that transport — accessbuilder→cddbp, puprouter→garcon. The wrong answer came from picking the wrong member of an aliased pair whose proto sets disagree. Retyping to single-bit removes the ambiguity within a registry; aliasing removes it across names on one port. Doing one without the other leaves half of #759's mechanism standing.

    So the alias relation needs a per-alias transport set, or aliases must be confined to the 35 ports where every member already agrees. I will fold this into the plan I post here rather than answer it off the cuff.

  11. 1 remaining item

  12. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Ruling recorded, in your words:

    alias should thus be per-transport. and we should probably allow registering aliases for a given enum in the transport apptype enum.

    Agreed, and this dissolves the objection I raised rather than working around it. The 9-port disagreement I flagged is a disagreement about other transports, so it cannot survive per-transport aliasing. Measured:

    TREE: /tmp/v801/pcapkit/__init__.py
    port 80 per registry:   TCP -> ['http', 'www', 'www-http']
                            UDP -> ['http', 'www', 'www-http']
                           SCTP -> ['http']
                           DCCP -> []
    members whose proto OMITS their own registry: 0   (over 12391 members)
    self-test  (tcp in tcp|udp) = True   (sctp in tcp|udp) = False
    

    Port 80 has two aliases under TCP and under UDP, one member and no aliases under SCTP, and does not exist under DCCP. So the alias set is genuinely a per-transport property, exactly as you say — www is an alias of http on TCP and UDP, and is not an alias of anything on SCTP because it is not registered there.

    And since every member's proto already contains its own registry's transport (0 exceptions in 12,391), retyping makes all members on a shared port within one registry carry the identical proto by construction. The http(7) vs www(3) split at port 80 is not a conflict to resolve; it is the cross-transport information that the split already encodes and that retyping stops duplicating. Aliases then need no per-alias transport field — the registry is the transport.

    One correction to my own last comment, and a bad probe behind it. I wrote that a flat aliases list "cannot express 9 of those 44 ports". That framing was wrong: per-transport, it expresses all 44. Today's counts stand — TCP 23 shared ports (6 disagreeing), UDP 21 (3), SCTP 0, DCCP 0 — but the disagreement is not an obstacle to your design.

    I also ran a check that reported 94 violations of registry membership and did not publish it, because the probe was broken, not the data: I tested 'tcp' not in str(m.proto), and str(m.proto) renders as '3', so the condition was true for every row. The corrected flag-membership version gives 0, and I self-tested it against a known-false case first. Same lesson as #796 — a probe is not evidence until it is shown to detect the thing it looks for.

    Ports 888 and 999 remain the connection worth keeping: they are both #759 wrong-service cases and disagreeing shared ports, which is why the retyping and the aliasing belong in one design rather than two.

  13. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    and on each apptype enum's repr/str, it should expose its associated aliases (if any) as well. And the enum should also have a public property/member to fetch its aliases list.

  14. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Ruling recorded, in your words:

    and on each apptype enum's repr/str, it should expose its associated aliases (if any) as well. And the enum should also have a public property/member to fetch its aliases list.

    Both agreed. One hard constraint the implementation has to respect, measured rather than assumed: the aliases must not reach _value_.

    AppType is a StrEnum and _value_ is the lookup key — it is what _value2member_map_ is keyed on, and the map is built once at class creation. Since your design makes aliases registerable at runtime, an alias list inside _value_ would go stale the moment one is registered:

    TREE: /tmp/v801/pcapkit/__init__.py
    str(m) == m.value ?  True   'http [80 - tcp|udp|sctp]'
    m.value in TCP._value2member_map_ ?  True
    TCP('http [80 - tcp|udp|sctp]') is TCP['http'] ->  True
    -- mutate _value_ after class creation --
      TCP(old value) -> resolves fine        <- stale map still holds the old string
      TCP(new value) -> ValueError: 'http [80 - tcp]' is not a valid TCP
    

    So __repr__ and __str__ grow the aliases, _value_ does not. _value_ still changes once — dropping the composite proto.name per the retyping — but that is a one-off at generation time, not a runtime mutation.

    Consequence worth stating up front: str(m) == m.value is True today and would become False. That coupling is incidental rather than designed (both happen to use the same format at vendor/reg/apptype/apptype.py:251 and :274), but anything relying on it breaks. I grepped for the bracketed form in tests/ and docs/source/ and found 5 occurrences — listed in the plan when I post it, so none is missed.

    Second consequence: repr/str output becomes mutable at runtime, since registering an alias changes it. Any doctest or assertion pinning a member's repr must therefore not assume a fixed alias list — worth a note in the docstring so the next person does not pin it.

    Naming: .aliases as the public property, returning an empty tuple rather than None when there are none, so callers need no guard. Tuple over list because the members are otherwise immutable and a mutable return invites in-place edits of registry state.

  15. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Correcting the "5 occurrences" in my previous comment — it is 0, and my grep was broken again.

    I searched for " - tcp" and friends, which matched five lines of tcp.last - tcp.first + 1 arithmetic in tests/toolkit/. Nothing to do with AppType. The correct pattern, self-tested against the real string first so a zero means something:

    $ grep -rnE "\[[0-9]+ - (tcp|udp|sctp|dccp)" tests/ docs/source/ examples/ | wc -l
    0
    $ printf 'http [80 - tcp|udp|sctp]\n' | grep -cE "\[[0-9]+ - (tcp|udp|sctp|dccp)"
    1        <- the pattern does detect the form, so the 0 above is real
    

    So nothing in tests/, docs/source/ or examples/ pins the bracketed value form. That makes the str(m) == m.value divergence cheaper than I said — no assertion depends on it. The one nearby coupling is tests/const/test_const_apptype_split_unit.py:406, which does cls._value2member_map_.pop(member.value, None) in a teardown; it reads .value as an opaque key rather than pinning its text, so it is unaffected by the format change but does confirm the value is the live lookup key, which is the constraint from my last comment.

    That is the third bad search in this thread — the #796 shell loop, the str(m.proto) membership test, and now this. The failure mode is identical each time: a pattern that cannot match, whose empty result reads as a finding. I am self-testing every pattern against a known-positive before quoting a count from it, which is what produced the 1 above.

  16. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Correcting my own line citations — most of them were wrong, and one whole file's worth do not exist. I re-read every one against main (477ed00c4) rather than trusting the reports they came from.

    I published Truth
    vendor/reg/apptype/apptype.py:251 __new__ :250
    :271 __repr__ :270
    :274 __str__ :273
    :522-526 docstring :473
    :806 emit condition :752
    test_const_apptype_split_unit.py:78-80 :80, one line
    test_const_apptype_split_unit.py:630-658 does not exist
    test_const_apptype_split_unit.py:709-836 does not exist
    "the 10,625/1,766 split is pinned in that test file" false — 0 hits for either number anywhere in tests/

    That file is 415 lines long with 15 test methods. Every citation I gave above 415 was fabricated, and I passed them on without opening the file. tests/foundation/registry/test_protocols.py is real at 664 lines with the four register_apptype calls at :476, :484, :493, :499.

    And the correction matters, because reading the real file shows canonical-and-alias is already half-built — which changes the shape of what you asked for:

    TCP.__canonical__ entries = 23    UDP = 21    SCTP = 0    DCCP = 0
    TCP.get(80).svc      = http
    TCP.get_all(80) svcs = ['http', 'www', 'www-http']
    TCP.http == TCP.www  = True   (is: False)
    

    __canonical__ is a per-registry port → canonical service map curated from /etc/services, and test_a_port_lookup_returns_the_canonical_service documents why: IANA names no precedence, and of the 44 colliding pairs the registry row order answers 28 the way /etc/services does and 16 differently. get() returns the canonical member, get_all() returns canonical-first then the rest. Its 23+21 also independently reproduces the 44 shared ports I measured separately.

    So your ruling is additive, not new machinery. The alias data already exists and needs no storage — I derived it directly:

    derived aliases of TCP.http -> ['www', 'www-http']
    derived aliases of TCP.www  -> ['http', 'www-http']
    

    That is __registry__.getlist(self.port) minus self. What is genuinely missing is only the two things you named: a public .aliases property on the member, and exposing them in __repr__/__str__.

  17. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    The plan, with the signature settled. Your varargs idea stands; the audit's concrete rendering of it did not survive testing.

    The audit recommended def register_apptype(code, module, class_: 'str' = NULL, *transport: 'TransportProtocol'), arguing that putting the vararg after class_ is "precisely what avoids the swallowing bug". It does the opposite — class_ is positional with a default, so the third positional argument binds to it:

    proposed(member, Unit, TP.udp)               -> class_='TP.udp'  transport=()      <- transport eaten
    alt(member, Unit, TP.udp)                    -> transport=('TP.udp',)  class_=NULL
    alt(65000, 'pkg.mod', 'Raw')                 -> transport=('Raw',)                 <- 2-string form breaks
    alt(65000, 'pkg.mod', TP.tcp, class_='Raw')  -> transport=('TP.tcp',) class_='Raw'
    

    The audit's own migration example hits this. My recommendation:

    def register_apptype(code: 'int | Enum_AppType', module: 'str | ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]',
                         *transport: 'TransportProtocol', class_: 'str' = NULL) -> 'None':

    code and module stay positional as every sibling has them; transports are varargs — singular and explicit, no Flag; class_ becomes keyword-only. That last part is the one divergence from the siblings, and I think it is a fix rather than a cost: a bare positional class-name string sitting next to a module string is itself ambiguous, which is exactly the property you are removing elsewhere. It breaks one call site in the whole repo — tests/foundation/registry/test_protocols.py:493, register_apptype(65000, *raw_module, proto='tcp') — and it breaks it loudly.

    One sub-case the audit flagged as unruled, which I am deciding rather than sending back to you: an Enum_AppType member with no explicit *transport uses the member's own proto. Post-retyping that is single-bit, so it names exactly one registry and there is no ambiguity left to remove — the convenience survives precisely because the thing that made it dangerous is gone. A bare int with no *transport stays an error, as today.

    Confirmed by the audit and re-verified by me: every sibling is (code, module, class_: 'str' = NULL) with no proto — register_linktype :385, register_ethertype :509, and register_transtype/register_pcap/register_pcapng/register_tcp/register_udp/register_sctp. Only one file calls register_apptype at all (4 calls). And zero uses of &, ^ or ~ on TransportProtocol anywhere in pcapkit/, tests/, examples/, docs/ — pattern self-tested against a known-positive.

    Your "not used in other places" is very nearly right. Three sites outside register_apptype do rely on Flag semantics today — TransportProtocol._missing_'s bit-composition validator, TransportProtocol.get()'s max_val * 2 power-of-two doubling, and _dispatch's show_flag_values rejection guard — but all three exist only to handle composites that trace back to AppType.proto, so all three become dead once the retyping lands. It becomes literally true after step 1, not before.

    Splitting into three issues, filed next, because they are sequential and one of them is much riskier than the others: the retyping plus the signature (coupled — retyping removes the fan-out the signature exposes), then .aliases plus the repr/str exposure, then dropping the IntFlag base. Tracking stays here.

  18. JarryShaw commented on Sep 25, 2026

    @JarryShaw
    OwnerAuthor

    Retracting my own "Correcting my own line citations" comment above — it was wrong in every row, and the agents' original citations were right.

    Root cause: I ran grep -n on a relative path in the main checkout, whose main branch is stale at fe80b8525, instead of reading origin/main at 477ed00c4. That checkout's copy of the file is 62 lines shorter. My own standing rule says to read origin/main explicitly for exactly this reason, and I did not.

    I "corrected" to Actually, on origin/main
    vendor/reg/apptype/apptype.py:250 __new__ :251
    :270 __repr__ :271
    :273 __str__ :274
    :473 docstring :523
    :752 emit condition :802

    So the audit's :251/:271/:274 were exact, and its :802 for the emit condition was exact — the drift I "found" was my own stale tree, not theirs. The only real drift in the original reports is :806 vs :802 from the earlier measurement agent, and :522-526 vs :523 which is a range around the right line.

    Worse, I called two citations fabricated and they exist. tests/const/test_const_apptype_split_unit.py is 863 lines with 19 test methods on origin/main, not the 415/15 I reported from the stale checkout:

    :630  def test_one_transport_protocol_still_resolves_every_port_it_resolved_before
    :709  def test_no_lookup_call_site_in_the_library_builds_a_composite_proto
    :578  self.assertEqual(swept, 10625)
    :643  * of those, **10,625** carry a multi-bit ``.proto`` and **1,766** a
    :835  self.assertIn('proto = code.proto', source)
    :836  self.assertIn('if test not in proto:', source)
    

    Both ranges I declared non-existent are real, assertEqual(swept, 10625) is real at :578, and the 10,625/1,766 populations are pinned in that file at :643 — I reported 0 hits for those numbers, from a file that did not yet contain them. Every one of those agent claims stands; the retraction is mine.

    The migration surface is therefore larger than #806 currently says, and :709-836's test_no_lookup_call_site_in_the_library_builds_a_composite_proto matters most: it hard-asserts the literal strings 'proto = code.proto' and 'if test not in proto:' at :835-836, so #806's signature change breaks that test by design. Correcting #806 now.

  19. added
    blockedDeferred pending another issue or decision; see the last comment for what unblocks it
    and removed
    wipWork in flight - a covering PR is open or an agent is actively on it
    on Sep 26, 2026
  20. JarryShaw commented on Sep 26, 2026

    @JarryShaw
    OwnerAuthor

    Checkable blocker: #815 merged.

    gh pr view 815 -R JarryShaw/PyPCAPKit --json state,mergedAt
    

    Relabelled blocked rather than wip, because no open PR closes this issue — #815's body closes #806 and #809 only, and a wip with no covering PR reads as work in progress when nothing is in progress on it.

    This issue is the design question you asked; #806 was the implementation and #815 is its PR. So the answer is already settled and shipping — the multi-bit proto is redundant given the per-transport split, which is what #815 implements — and what remains here is to close this out once that lands and record the conclusion against the question. Nothing to build.

    Found by an audit prompted by your note that #831 had no blocked/wip label: I checked all nine open issues for whether each is genuinely held, and found three label errors rather than one — this, plus #809 carrying blocked when an open PR closes it directly, and #831 sitting idle with no worker.

  21. removed
    blockedDeferred pending another issue or decision; see the last comment for what unblocks it
    on Sep 26, 2026
  22. JarryShaw commented on Sep 26, 2026

    @JarryShaw
    OwnerAuthor

    Answered and shipping — closing. #815 merged as 3118ed796.

    Your question was whether AppType's multi-bit proto is necessary given the enum is already split per transport. The answer established over this thread was no, and it is now implemented:

    • Fully derivable. 12,391 member rows across the four per-transport registries, 10,625 multi-bit and 1,766 single-bit. Grouping by (service, port) gave 7,054 groups with 0 disagreements and no single-registry group carrying a multi-bit value — so the multi-bit field held nothing the registry split did not already encode.
    • Retyped, with register_apptype taking varargs instead of a proto kwarg, and — per your later rulings — accepting a transport name case-insensitively, and keeping class_ positional with the swallow disambiguated by type(module).

    Two things deliberately left out of that PR and tracked separately rather than dropped: #807 for exposing aliases as a public property and registering them per transport, and #808 for dropping TransportProtocol's IntFlag base now that nothing builds a composite — which this work is what made possible.

    Closing as answered rather than leaving it open with a blocked label, since there is no build work left in it.

  23. added this to the 1.5 milestone on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    breakingBreaks public-facing behaviour or API (apply alongside the type label)designA design or decision issue: a pattern being decided rather than a defect or a requestquestionIssues asking how something works rather than reporting a defect

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions