Skip to content

fix(vendor): make main() exit non-zero when any crawler target raises (#872) - #873

Merged
JarryShaw merged 1 commit into
mainfrom
fix/872-vendor-exit-code
Sep 28, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/872-vendor-exit-code

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 28, 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

Fixes #872. python -m pcapkit.vendor <target> could not fail: run() swallowed every crawler exception as a filterable VendorRuntimeWarning, and main() always return 0. This grew into three pieces after review, following the owner's ruling (quoted verbatim):

I take (b). And only discard changes made by a non-zero sub-vendor. zero-exited sub-vendor's updates should be kept. So the idea is for cron jobs, at most warn about failed sub-vendors, but never fail the entire job.

1. Exit code (pcapkit/vendor/__main__.py) — run() returns whether its target succeeded; main() attempts every target regardless of earlier failures and returns 1 if any raised. A raising target's qualified name and exception repr also reach stderr unconditionally via print(), since the warning is filterable and PCAPKIT_VERBOSE defaults false. This exit code stays load-bearing for every consumer — including a manual run — and is unchanged from earlier rounds.

2. Atomic write (pcapkit/vendor/default.py) — Vendor.__init__'s previous last line, open(const_file, 'w'), truncates the file immediately, before any content is written. A failure during the write itself (not just fetch or render, which already left the file untouched) could therefore destroy a good file and leave a truncated one to be committed. _write_atomic() now renders to a temp file in the same directory and os.replace()s onto the target — atomic on the same filesystem — so every failure mode leaves the previous file bit-for-bit untouched.

On per-target restore: I concluded the workflow does not need a git checkout -- <path> step per failed target, and did not add one. With the atomic write, a failed target's own const file is never touched at all — not modified, not truncated — so git diff/git status shows no change for that path in the first place; there is nothing to discard. Consequently I also did not add a formal, machine-parseable failed-target contract to the CLI: the workflow's grep ' failed: ' against the existing human-readable stderr line (added in round 1) is best-effort only, used to enrich the $GITHUB_STEP_SUMMARY/::warning:: text — if the format ever drifts, the workflow falls back to a generic "see the step log" annotation rather than depending on the grep for correctness.

3. Workflow (.github/workflows/cron-vendor.yml) — the "Update Vendor" step no longer aborts under bash -e when pcapkit-vendor exits non-zero: the exit code is captured explicitly (set +e / set -e around the one command, not a bare || true), so targets that succeeded still get isorted, diffed, committed and pushed. Failures are surfaced loudly: the full stderr is replayed into the step log, a ### :warning: block naming the failed target(s) is appended to $GITHUB_STEP_SUMMARY, and a ::warning:: annotation is emitted per failed target — visible on the run without opening the log. Verified directly against stub pcapkit-vendor/isort executables in a scratch sandbox: multi-failure, clean success, and a non-zero exit with no parseable target line all behave as intended, and the step's own exit code is always 0 so later steps proceed.

Tests: tests/vendor/test_vendor_exit_code_unit.py (5 methods) and tests/vendor/test_vendor_atomic_write_unit.py (2 methods), both through the real entrypoints with stub Vendor subclasses — no network calls. The atomic-write regression method simulates a failure during the write by making print() raise once the destination is already open; run against pre-fix default.py it fails (the previous file is truncated to ''), and passes with the fix. 7/7 methods green together under plain unittest.

@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) ci Pull requests that change CI or workflow configuration (ci: subject prefix) review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 28, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES on 58f883c83 — cross-review (opus; author sonnet). The production change in pcapkit/vendor/__main__.py is correct as written and I found no defect in it. One test is vacuous, and one consequence needs your ruling.

1. The test guarding the new print() passes without the fix, so nothing pins it. test_failing_target_and_error_reach_stderr_unconditionally never suppresses warnings, and the pre-existing warn() already writes the qualified name and the exception repr to stderr under default filters. Measured:

default filters   -> stderr non-empty: True   contains repr: True
warnings ignored  -> stderr non-empty: False

So delete the print() line and that test still passes — the whole premise of its own comment ("the only thing guaranteed to reach a CI log regardless of warning filters") is untested. Wrap the call in warnings.simplefilter('ignore'); that takes the pre-fix failure count from 3/5 to 4/5. Also prefer assertEqual(exit_code, 1) over assertNotEqual(..., 0), since main()'s docstring promises exactly 1.

2. Needs a decision from you — this changes what a broken crawler costs. GitHub Actions runs run: under bash -e, so a non-zero pcapkit-vendor at cron-vendor.yml:123 aborts the Update Vendor step before the isort at :132, and Verify Changed files (:134), Bump Version, Commit changes and Push changes all then skip. Net effect the first time any single registry goes down: the other ~116 crawlers regenerate to disk and every one of them is thrown away — nothing isorted, committed or pushed. Pre-fix, those 116 landed. That defeats in CI the very intent the commit message states in-process ("a dead crawler should not block the rest from regenerating").

Two ways to go, and it is your call: (a) accept it — a red cron and no partial commits until a human fixes the crawler; or (b) defer the exit in the workflow so the good crawls still land — pcapkit-vendor || rc=$? … exit $rc after the commit steps, or continue-on-error plus an explicit final gate. If (b), it belongs in this same change.

Good news on the risk: nothing is broken today. The latest successful cron run (36423035362, 2026-09-28T12:37Z) has zero VendorRuntimeWarning/VendorRequestWarning lines across all ~117 crawlers — and since warn() demonstrably reaches stderr under default filters, absence in the log is real evidence, not the vacuous "it was green" (pre-fix it could not go red).

Verified and not disputed: sys is imported unconditionally at __main__.py:14, so the new print() in the exception handler cannot NameError; exit codes are 0 all-succeed / 1 any-fail, in both orders, with the succeeding target still running; main() consumes falsiness so a None return fails safe; PCAPKIT_CI_MODE raises rather than prompting, and PCAPKIT_VENDOR_RETRY=10 only widens retries, so neither masks the new exit code; 5 methods, 3 failing pre-fix exactly as claimed, stubs touch no network.

Non-blocking omission for a follow-up: a mixed list like pcapkit-vendor arp typo still exits 0 — a renamed vendor module is warned about and skipped, which is #872's defect one layer up.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment needs: decision Waiting on the maintainer to decide — not blocked by other work and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 28, 2026
@JarryShaw
JarryShaw force-pushed the fix/872-vendor-exit-code branch from 58f883c to 54639e1 Compare September 28, 2026 13:28
@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 28, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES on 54639e181 — cross-review round 2 (opus; author sonnet). The fix round 2 was asked for is done and verified; what is left is a two-line comment that now contradicts both the measured output and its own module docstring.

Round 1's defect is repaired and now genuinely pinned. With origin/main's pcapkit/vendor/__main__.py swapped in, the new file fails 4 of 5 methods — exactly the four claimed, and the stderr one fails with '…FailingCrawler' not found in '', i.e. the buffer is genuinely empty pre-fix. Restoring gives 5/5. Stronger still: deleting only the print() line (120→119 lines, both return statements intact) fails exactly one test, that one; and with the suppression removed but print() still absent, the buffer fills with 172 bytes of VendorRuntimeWarning and the assertion passes — which is precisely why the suppression had to be added. The reviewer also ruled out a second stderr path: pcapkit/utilities/logging.py attaches only a NullHandler by default and its module-level StreamHandler binds the real stderr at import, so it cannot forge a pass into the buffer.

The remaining item — tests/vendor/test_vendor_exit_code_unit.py:140-142:

# The regression this issue is about: pre-fix, main() ends on a bare
# `return 0` with no accounting at all, so this assertion is the one
# that fails (0 != 0) against origin/main.

Both specifics are stale residue of round 2's own edit. (0 != 0) was the failure text of the old assertNotEqual(exit_code, 0); the assertion is now assertEqual(exit_code, 1) and the measured text is 0 != 1. And "this assertion is the one that fails", singular, is the very claim round 2 was dispatched to correct — the module docstring 100 lines above now correctly says 4 of the 5 methods fail, so the file contradicts itself. One line fixes it.

Verified and not disputed: the round-2 diff (58f883c83..54639e181) contains only the three requested changes plus import warnings — pcapkit/vendor/__main__.py and .github/workflows/cron-vendor.yml are untouched, which I confirmed independently by an empty restricted diff; assertNotEqual(exit_code, …) appears nowhere in the file any more; the module docstring's four names and per-method reasons match the measurements exactly; no live crawl was run and pcapkit/const/ stayed clean across every suite run.

Still open and not mine to settle: the bash -e consequence — one dead registry aborts Update Vendor before the isort at cron-vendor.yml:132, so the week's ~116 good regenerations are discarded rather than committed. That is the needs: decision on this PR.

@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 28, 2026
@JarryShaw
JarryShaw force-pushed the fix/872-vendor-exit-code branch from 54639e1 to 08d0fea Compare September 28, 2026 13:35
@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO on 08d0fea45 — round 3 is the one-line comment fix and nothing else, which I verified completely rather than sending another review round.

git diff 54639e181 08d0fea45 is +3/−2 in one file, entirely inside the comment at tests/vendor/test_vendor_exit_code_unit.py:139-142, and the file is token-identical to the reviewed head with comments dropped and strings collapsed (534 vs 534). So nothing executable moved from the head that round 2's review already cleared, and the previous verdict's sole outstanding item is closed: the comment now says (0 != 1), matching the measured failure text, and points at the module docstring for the full 4-of-5 list instead of re-asserting a singular failure.

The record for this PR, carried forward: the production change in pcapkit/vendor/__main__.py is correct — run() returns bool, main() attempts every target and returns 0 all-succeed / 1 any-fail, with an unconditional print(..., file=sys.stderr) beside the still-filterable warn and traceback.print_exc() left gated on VERBOSE. sys is imported unconditionally at :14, so the new print cannot NameError inside the handler. The test genuinely pins it: 4 of 5 methods fail against origin/main, deleting only the print() line fails exactly the stderr test, and removing the suppression makes that assertion pass with the print absent. PCAPKIT_CI_MODE raises rather than prompting and PCAPKIT_VENDOR_RETRY=10 only widens retries, so neither masks the new exit code.

This verdict covers the change as it stands. It does not settle the scope question still open on this PR: under bash -e a non-zero pcapkit-vendor aborts Update Vendor before the isort at .github/workflows/cron-vendor.yml:132, so Verify Changed files, Bump Version, Commit changes and Push changes all skip and a week's ~116 good regenerations are discarded rather than committed. If you want the red build and the good crawls landed, the workflow tweak belongs in this same PR and I will have it added before you merge. needs: decision stays on until you rule.

Non-blocking, for a follow-up issue rather than this PR: a mixed target list (pcapkit-vendor arp typo) still exits 0, since a bogus entry is only warned about — #872's defect one layer up.

@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 28, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

What needs my decision?

@JarryShaw

Copy link
Copy Markdown
Owner Author

What needs my decision?

Whether this PR should also change .github/workflows/cron-vendor.yml, because the exit-code fix changes what one dead registry costs you.

The code itself is settled — review: good-to-go on 08d0fea45, 53 green with 5 still running. The open question is scope.

Today pcapkit-vendor cannot fail, so a broken crawler is a silent no-op and the other ~116 crawlers still regenerate, get isorted, committed and pushed. After this merge it exits 1, and GitHub Actions runs run: under bash -e, so that non-zero aborts the Update Vendor step at :123 before the isort at :132 — and Verify Changed files (:134), Bump Version (:138), Commit changes (:148) and Push changes all then skip. Net: the week's ~116 good regenerations are computed and thrown away instead of landing.

Two options:

  • (a) Accept it. Red cron, nothing committed until a human fixes the crawler. No further change; merge as is.
  • (b) Defer the exit so the good crawls still land. Capture the status and exit at the end of the job — pcapkit-vendor || rc=$? in the step, then a final exit ${rc:-0} after the commit/push steps (or continue-on-error on that step plus an explicit gate at the end). Red cron and the 116 land. If you pick this, it belongs in this same PR and I will have it added and re-reviewed before you merge.

For what it is worth: nothing is broken right now. Cron run 36423035362 (12:37Z) has zero VendorRuntimeWarning/VendorRequestWarning lines across all ~117 crawlers, and since warn() demonstrably reaches stderr under default filters, that absence is real evidence rather than "the run was green" — pre-fix the job could not go red at all.

My recommendation is (b): the whole point of main() attempting every target is that one dead registry should not cost the rest, and under set -e option (a) undoes that at the only place it matters.

@JarryShaw

Copy link
Copy Markdown
Owner Author

I take (b). And only discard changes made by a non-zero sub-vendor. zero-exited sub-vendor's updates should be kept. So the idea is for cron jobs, at most earn about failed sub-vendors, but never fail the entire job.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Recorded, verbatim:

I take (b). And only discard changes made by a non-zero sub-vendor. zero-exited sub-vendor's updates should be kept. So the idea is for cron jobs, at most earn about failed sub-vendors, but never fail the entire job.

So the target shape is per-target granularity, and a cron that warns rather than fails — which is more than option (b) as I framed it. Three requirements, and I have relayed them:

  1. A failed sub-vendor's changes are discarded; a zero-exited one's are kept. In the common case there is nothing to discard — Vendor.__init__ (pcapkit/vendor/default.py:408+) does _request() and context(data) before the open(const_file, 'w') on its last line, so a fetch or render failure leaves the previous const file untouched. The gap is a failure during the write, which can truncate a good file; making the write atomic (temp file + rename) closes it, and that is now in scope even though python -m pcapkit.vendor exits 0 when a crawler raises, so a no-op regeneration looks like success #872 originally excluded default.py.
  2. The cron never fails the whole job — it warns. .github/workflows/cron-vendor.yml:123 runs bare pcapkit-vendor under bash -e, so today's non-zero would abort the step before the isort at :132 and skip Verify Changed files, Bump Version, Commit changes and Push changes. That step must tolerate the non-zero, surface the failed targets loudly (job summary and/or an annotation), and let the successful regenerations commit and push.
  3. The exit code stays load-bearing for everyone else. main() keeps returning 1 when any target raised — that is what makes a manual run and any other consumer able to tell a no-op from a real regeneration. Only the cron chooses to tolerate it.

needs: decision removed. Round 4 is with the author; the PR stays review: needs-changes until the new head is reviewed, since the scope now includes a workflow change and possibly default.py.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: good-to-go Cross-review at the current head says ready; CI state is separate needs: decision Waiting on the maintainer to decide — not blocked by other work labels Sep 28, 2026
@JarryShaw
JarryShaw force-pushed the fix/872-vendor-exit-code branch from 08d0fea to a0748fb Compare September 28, 2026 14:08
@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 28, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES on a0748fb17 — cross-review (opus; author sonnet). The mechanism holds: set +e/vendor_exit=$?/set -e really does survive GHA's bash -e (step exit 0 on vendor exit 1, 0, 3 and 127; the step's status becomes isort's, as intended), shellcheck finds nothing new, no crawler overrides the write path, and the atomic-write pin fails on main with '' != 'GOOD CONTENT\n'. Three things to fix.

1. cat "$RUNNER_TEMP/vendor-stderr.log" >&2 at :140 is unguarded and sits after set -e is restored. It is the one command in the new block that can abort the step — exactly the property this PR exists to establish. || true.

2. The _write_atomic permission comment is false as written. I measured it rather than taking the report: open(p,'w') ignores mode on an existing file, and creates with 0o666 & ~umask on a new one.

case old open(w) new atomic
existing file at 0600 0o600 0o644
new file at umask 077 0o600 0o644

Only true at umask 022. Practical impact here is near zero (git records just the exec bit, CI is umask 022), so fix the comment or os.stat the destination's mode — but do not leave the claim standing.

3. assertFalse(self.vendor_main.run(failing)) passes against pre-fix code, since pre-fix run() returns None. The module docstring at :48-51 says it is kept "for the property it does pin post-fix (an explicit False, not a merely-falsy value)" — assertFalse cannot check that. assertIs(..., False).

Cosmetic, and I would take the second: grep ' failed: ' also matches the VendorRuntimeWarning line and any exception whose text contains failed: , so one target can emit two annotations — anchor it. And mkstemp's fd leaks if os.chmod or os.fdopen raises; let os.fdopen own it immediately.

Confirmed and closed out: per-target restore is genuinely unnecessary. PCAPKIT_VENDOR_RETRY is request-level only (default.py:39 used solely inside _request's fetch loop at :589-623), so a retry never follows a write — there is no window where a target both replaces its const file and then fails.

@JarryShaw JarryShaw added the review: needs-changes Cross-review at the current head says changes are required; see the verdict comment label Sep 28, 2026
@JarryShaw JarryShaw removed review: good-to-go Cross-review at the current head says ready; CI state is separate needs: decision Waiting on the maintainer to decide — not blocked by other work labels Sep 28, 2026
@JarryShaw
JarryShaw force-pushed the fix/872-vendor-exit-code branch from 73fc66a to 16a73b4 Compare September 28, 2026 16:32
@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 28, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES on 16a73b49f — cross-review (opus; author sonnet). The snapshot/restore design is sound and every claim about shipped behaviour checks out. What fails is the pin.

1. Deleting the raise leaves the suite fully green. I reproduced this myself — removed the raise from _snapshot_and_restore's except BaseException: and ran both files:

TREE …/pcapkit/vendor/__main__.py  MUTANT: True
run=8 failures=0 errors=0

Two other mutations are caught (deleting os.replace and removing the with from run() both fail the byte-for-byte pin), but this one is not, and the regression it hides is precisely what #872 exists to prevent — a target no longer reports its own error:

head    -> run()=False | __main__.StubCrawler failed: RuntimeError('simulated failure')
mutant  -> run()=False | __main__.StubCrawler failed: FileNotFoundError(2, 'No such file or directory')

assertFalse(run(...)) still passes by accident: with raise gone, control falls through to os.remove(backup), the backup has just been renamed away, so FileNotFoundError propagates and the target fails anyway. Fix: assert on what run() reported, not merely that it returned falsy — assertWarnsRegex(VendorRuntimeWarning, 'simulated failure') or a stderr capture requiring the crawler's own text. And make it structural while you are there: else: os.remove(backup) rather than relying on raise to skip the cleanup, since the comment at __main__.py:106-109 is currently load-bearing prose.

2. There is a second capability regression, and only one is disclosed. A writable file inside a non-writable directory: pre-fix regenerated it, this head fails the target because mkstemp cannot create the backup (PermissionError(13), content preserved). Fail-safe, and rounds 4-8 needed directory write too — but disclose both, and note the asymmetry that a mkstemp failure fails the target while a _dest_path failure does not.

Confirmed clean: the @classmethod conversion is safe — _dest_path's old body used self exactly once as inspect.getfile(type(self)), no crawler overrides it, and class-call equals instance-call for 121/121 crawlers; test_vendor_dest_path_unit.py is 4/0/0. The backup is always in the destination's own directory (SAME DIR = True, same st_dev), so my EXDEV worry was structurally impossible. Mode restoration works — 0o640 in, 0o640 out, content intact, mtime preserved, which does justify collapsing the three mode branches, though nothing now asserts it. Workflow byte-identical (600fd5b1f), sandbox still 0 / 0 / 1-when-isort-fails. run=3 / run=5 / run=8 all reproduce, and the pre-fix run fails 2 methods, not 1 — test_successful_run_replaces_the_previous_file_content also fails with None is not true, which you undersold.

Cosmetic: add the 0o640 mode assertion to the byte-for-byte test; reword "there is nothing left to clean up on this path either way" (true only when os.replace succeeds — a restore failure masks the original error and orphans the .bak); the symlink divergence moved to the restore path and round 9 dropped the discussion rounds 4-8 had; and os.remove(backup) on the success path is unguarded where rounds 4-8 used contextlib.suppress(OSError), which is also the owner's stated preference.

@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 28, 2026
…#872)

run() swallowed every crawler exception as a filterable
VendorRuntimeWarning, and main() always returned 0 regardless -- so a
crawler failure looked identical to a successful regeneration at the
only boundary CI or a script checks. Vendor.__init__ fetches and
renders before it ever opens the const file for writing, so a failed
crawler leaves the previous file untouched: a no-op indistinguishable
from success (hit for real in #870's docstring-propagation bug).

- run() now returns whether its target succeeded; main() attempts
  every target regardless of earlier failures (a dead crawler should
  not block the rest) and returns 1 if any of them raised.
- A raising target's qualified name and exception repr now also go to
  stderr unconditionally via print(), since VendorRuntimeWarning is
  filterable and PCAPKIT_VERBOSE defaults false.
- run() now wraps every vendor() call in
  pcapkit.vendor.__main__._snapshot_and_restore, a
  @contextlib.contextmanager that copies a target's existing const
  file aside before the crawler runs and restores it if the run
  raises, dropping the copy on success (via contextlib.suppress, not
  a bare call, so a cleanup failure after a successful run cannot
  itself turn that success into run() -> False). Per the owner's
  ruling ("I prefer we use contextlib over manually manage the temp
  file deletion based on pure best intent (try-finally) and for
  atomic writing, an easier path is simply keep a copy before running
  the sub-vendor and revert if anything failed"), this supersedes
  rounds 4-8's atomic write inside Vendor.__init__ entirely --
  _write_atomic is gone, and __init__'s last line is once again a
  plain open(const_file, 'w'). Snapshot-and-restore is a *wider*
  guarantee: it covers any way a crawler's own code could touch its
  const file, not just one open()/print() pair, and it puts the
  discarding exactly at the per-target boundary the owner's earlier
  (b) ruling asked for. The except/else split is structural, not a
  comment: an except clause that always re-raises is followed by an
  else that only runs on success, rather than an unindented statement
  after the whole try/except relying on "raise never falls through"
  -- a mutation test (deleting the raise) proved the fallthrough form
  lets the exception escape the generator entirely undetected once an
  else exists to gate the cleanup, whereas the comment-only version
  let a *different*, wrong exception (FileNotFoundError from the
  orphaned backup) reach run() instead of the crawler's real one,
  with every existing test still green either way.
- Two capability regressions relative to `main`, both measured and
  disclosed rather than assumed: a 0o444 (read-only) destination now
  fails to regenerate (a plain open() needs file write permission,
  unlike os.replace()'s directory-only requirement), and a writable
  file inside a non-writable directory also now fails (mkstemp needs
  directory write permission to create the backup, which open() on an
  existing file never needed). Both leave the previous file untouched
  -- fail-safe -- and neither is new relative to rounds 4-8's
  os.replace()-based design, only relative to `main`. The two failure
  modes are asymmetric: an unresolvable _dest_path skips the snapshot
  and lets the target proceed (it fails loudly anyway, since
  Vendor.__init__ calls _dest_path itself), while a mkstemp failure
  fails the target outright before vendor() is ever attempted.
- Vendor._dest_path is now a classmethod rather than an instance
  method -- it only ever consulted type(self), so cls is exactly
  that, with no instance required. This lets _snapshot_and_restore
  resolve a crawler's destination via vendor._dest_path() before
  instantiating it at all. No crawler overrides _dest_path, so this
  is a behaviour-preserving refactor for all of them; confirmed
  against tests/vendor/test_vendor_dest_path_unit.py (not part of
  this PR's file set, run read-only for verification), which still
  passes unchanged.
- .github/workflows/cron-vendor.yml's "Update Vendor" step is
  unchanged this round; re-verified against the snapshot/restore
  rewrite.
- tests/vendor/test_vendor_exit_code_unit.py unchanged this round
  (still 5 methods) -- its stub crawlers don't override _dest_path,
  so they hit the "skip the snapshot" fallback and behave exactly as
  before.
- tests/vendor/test_vendor_atomic_write_unit.py renamed to
  tests/vendor/test_vendor_snapshot_restore_unit.py (git mv), since
  the mechanism it tests is _snapshot_and_restore, not an atomic
  write. Its regression pin now asserts on the error run() actually
  *reported* (assertWarnsRegex against the crawler's own RuntimeError
  text), not merely that run() returned falsy -- the mutation above
  is exactly what that distinction catches, and a bare
  assertFalse(run(...)) passed on it. It also asserts the
  destination's mode (0o640 in, 0o640 out) alongside content, which
  is the measurement that actually justifies collapsing rounds 6-8's
  three mode-permutation tests into one.

Verified this round: tests/vendor/test_vendor_snapshot_restore_unit.py
(3 methods) and tests/vendor/test_vendor_exit_code_unit.py (5
methods), 8/8 together, under plain unittest. All three mutations are
now caught: deleting the `raise` fails 2 of 3 methods, deleting
os.replace() fails 1 of 3, and removing the `with` from run() fails 1
of 3. Against both of __main__.py and default.py taken from
origin/main together, 2 of 3 methods fail (the byte-for-byte pin with
'' != 'GOOD CONTENT\n', and the happy path with None is not true,
since pre-fix run() returns nothing); the 0o444 method passes there
too, since it is a measurement of a case main() already left
untouched, not a regression pin.
@JarryShaw
JarryShaw force-pushed the fix/872-vendor-exit-code branch from 16a73b4 to 89e5996 Compare September 28, 2026 16:58
@JarryShaw JarryShaw added review: pending No verdict for the current head - never reviewed, or the head moved since the last one and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Sep 28, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO on 89e5996c0 — cross-review (opus; author sonnet). The round-9 blocker is properly fixed, all three of your mutation numbers reproduce exactly, both capability regressions and the symlink divergence match measurement clause for clause, and the workflow is provably untouched (600fd5b1f at both shas).

The else: is justified by a stronger version of your own argument. Deleting raise does not merely return the wrong value — it is totally silent: contextlib._GeneratorContextManager.__exit__ returns truthy, so run()'s own except never runs and there is no warning and no stderr line at all, with main() exiting 0 on a failed crawl. That is #872 reintroduced with the diagnostic removed. Data stays safe; the lie is confined to the exit code.

Four non-blocking items, and I am asking for the first three in one short round:

1. except BaseException: is unpinned. I mutated it to except Exception: myself — run=3 failures=0 errors=0, suite fully green — and the measured consequence of a KeyboardInterrupt mid-crawl is a truncated const file and an orphaned .const.py.*.bak left inside pcapkit/const/. The shipped code is right; nothing holds it there. A ~10-line test closes it.

2. A false clause in _snapshot_and_restore's docstring (__main__.py:73-75): "this crawler goes on to fail loudly anyway once Vendor.__init__ calls the same _dest_path itself." True for the first trigger it enumerates (a crawler outside the pcapkit.vendor tree), false for the second — an instance-method _dest_path raises TypeError on the class call but resolves fine on the instance call, so the target runs unprotected and succeeds silently (run() -> True, content replaced, no warning). The PR's own test-module docstring describes this correctly, so two docstrings in one PR disagree.

3. The # pylint: disable=no-else-raise pragma is inert. R1720 applies to if/elif/else after a raise, never to try/except/else; with the pragma removed and --enable=no-else-raise the file still scores 10.00/10, and pylint itself reports I0021: Useless suppression of 'no-else-raise'. Not a CI gate — the Makefile's PYLINT_FLAGS enables F,E,W,R, never I — so drop it rather than work around it.

4. Not asking: deleting os.close(fd) survives as an untested fd leak (harmless on Linux, the path still exists and copy2 reopens it), and swapping the else: back to an unindented statement while keeping raise survives too — correctly, since the two forms are semantically identical and no test can distinguish them.

Also confirmed: contextlib.suppress(OSError) wraps exactly one statement and cannot mask a success-path bug, and the success-path cleanup is pinned (neutering os.remove(backup) fails the listdir assertion); assertWarnsRegex cannot pass for the wrong reason, since the warning text is derived from the reported error; except BaseException: also covers GeneratorExit; the rename is a rename (R073) with no stale references and the PR is 5 files; run=3 / run=5 / run=4 and the pre-fix run=3 failures=2 all reproduce with both files swapped from origin/main; the anchored grep still emits exactly one annotation where the warning line also contains failed: 503; pylint 4.0.8 on the changed file is 10.00/10.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 28, 2026
@JarryShaw
JarryShaw merged commit 6877210 into main Sep 28, 2026
63 checks passed
@JarryShaw
JarryShaw deleted the fix/872-vendor-exit-code branch September 28, 2026 17:24
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Sep 28, 2026
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…lause

GitHub issue #875, a follow-up to #872/#873 (merged as 6877210).

- Pin pcapkit.vendor.__main__._snapshot_and_restore's
  `except BaseException:` itself, not just its effect. Narrowing it to
  `except Exception:` left the existing suite fully green, because
  nothing had raised anything BaseException-but-not-Exception during
  a run. Added a test whose stub raises KeyboardInterrupt after the
  destination has already been truncated (via the same print()-patch
  technique the other failure tests use); it asserts the content is
  restored byte-for-byte, that no backup file survives in the
  directory, and that the KeyboardInterrupt itself still propagates
  out of run() rather than being converted into a plain False return.
- Corrected a false clause in _snapshot_and_restore's own docstring
  (pcapkit/vendor/__main__.py). It claimed a crawler whose _dest_path
  cannot be resolved without instance state "goes on to fail loudly
  anyway" once Vendor.__init__ calls the same method -- true of the
  first trigger listed (a crawler outside the real pcapkit.vendor
  tree, which fails the identical way whether or not the snapshot
  ran) but false of the second: an instance-method _dest_path raises
  TypeError on the classmethod-style call this function makes, but
  resolves fine on the bound instance call inside __init__, so that
  crawler actually runs unprotected and succeeds silently -- run()
  returns True, the file is replaced, nothing warns that no snapshot
  was ever taken. The clause is narrowed to the first trigger and the
  second is now stated plainly, matching what the test module's own
  docstring already said correctly.
- The `# pylint: disable=no-else-raise` pragma at
  _snapshot_and_restore's `try:` line is kept, not dropped. Re-measured
  with the Makefile's own PYLINT_FLAGS (what CI actually runs): R1720
  does fire on this try/except/else without the pragma (9.87 -> 9.74),
  because pylint 4 extends no-else-raise to a try/except whose except
  ends in raise. The structure it flags is deliberate -- the comment
  at :153-163 already explains why the else: is not a style default
  pylint would pick -- so the finding is suppressed rather than the
  structure changed.

Verified: tests/vendor/test_vendor_snapshot_restore_unit.py (4
methods) and tests/vendor/test_vendor_exit_code_unit.py (5 methods),
9/9 together, under plain unittest. The new
test_a_keyboard_interrupt_still_restores_and_propagates method passes
on this code (content and directory listing restored, the
KeyboardInterrupt still propagates). Mutated `except BaseException:`
to `except Exception:` at its exact line (content of that line
asserted before mutating, since `os.replace(backup, const_file)` also
appears in prose elsewhere in the same file and a string-index
mutation would otherwise silently hit that instead): the method then
fails on its content assertion ('' != 'GOOD CONTENT\n'); a direct
follow-up check confirmed the predicted orphaned backup file
('.const.py.<random>.bak') left in the directory afterwards.

pylint $(PYLINT_FLAGS) pcapkit/vendor/__main__.py, pragma present: no
R1720, 9.87/10 (only W1203, pre-existing). Pragma absent (checked and
reverted, not shipped): R1720 at the try: line, 9.74/10.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
JarryShaw added a commit that referenced this pull request Sep 28, 2026
JarryShaw added a commit that referenced this pull request Sep 28, 2026
JarryShaw added a commit that referenced this pull request Oct 2, 2026
#719

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Refs #719

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

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

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

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

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

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

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

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

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

Restored as quotations, correctly cited:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- PR #836 restored for the TransportProtocol-extension refusal, the
  |-composite decoding retirement, and the stale-comment deletion; the
  rulings are not on #808 at all.
- PR #783 for the f-string convention; PR #847 for the mint criterion.
- The de-quotation stands: wording stays as statements, no quotation marks.
JarryShaw added a commit that referenced this pull request Oct 2, 2026
tests/vendor/test_vendor_snapshot_restore_unit.py:14 and :58 carried
:meth: roles pointing at Vendor._write_atomic. Nothing defines it: grep
finds no `def _write_atomic` anywhere in the tree, and pcapkit/vendor/
default.py has no such method. Line 14 is the odder of the two, since the
sentence it sits in says the method no longer exists while using a role
that asserts it does.

The history is sharper than "removed". `def _write_atomic` appears only in
6fde283 and a0748fb, both intermediate revisions of #873 that the
squash dropped, so the method never reached main at all. Under pcapkit/ the
only commit touching the name is that squash, 6877210, and what it added
was the dangling role in __main__.py rather than the method -- #991 fixes
that one.

- Both roles become plain inline literals, so the prose still names the
  method without claiming a resolvable target.

tests/ sits outside the Sphinx source tree, which is why neither emitted a
build warning. A sweep of all 346 roles across tests/vendor/ found no other
unresolved target.

Prose only: both sites are inside the module docstring, which spans lines
2-165. Token sequences are identical with strings masked, the AST with
docstrings blanked compares equal, and maximum line length stays 99.
@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

ci Pull requests that change CI or workflow configuration (ci: subject prefix) fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

python -m pcapkit.vendor exits 0 when a crawler raises, so a no-op regeneration looks like success

1 participant