Skip to content

fix(lint): burn down the kanon-lint baseline — 104 entries down to 25 - #358

Merged
forkwright merged 16 commits into
mainfrom
fix/akroasis-lint-burndown
Aug 4, 2026
Merged

fix(lint): burn down the kanon-lint baseline — 104 entries down to 25#358
forkwright merged 16 commits into
mainfrom
fix/akroasis-lint-burndown

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Summary

Burns down the .kanon-lint-baseline.toml debt from akroasis#261: 104 baseline entries → 25. Every entry deleted here was fixed, not re-suppressed — the underlying finding was addressed in the code, and the baseline was regenerated (kanon lint --write-baseline) rather than hand-edited, so no anchor drift is hiding in this diff.

Rule-classes fixed (baseline entries deleted)

  • RUST/non-exhaustive-enum#[non_exhaustive] on 6 CLI/error enums
  • RUST/import-order — reorder external-before-crate-local imports (2 files)
  • CONTEXT/preamble-required — AGENTS.md scope/defers_to/tightens preamble
  • DOCS/stale-local-link — 2 dead links reworded to plain-text pointers
  • RUST/unreachable-in-match — removed a real unreachable!() panic path
  • RUST/test-missing-use-super — real use super:: instead of an unused glob
  • WRITING/weasel-word, WRITING/temporal-staleness, WRITING/elegant-variation — doc prose
  • RUST/todo-marker-needs-quadrant-and-artifact — Fowler quadrant tags on 4 yaesu TODOs
  • RUST/prefer-expect-over-allow#[allow]#[expect] on 4 test modules
  • RUST/no-debug-derive-on-public-types — manual Debug impls redacting the one field that's actually secret material (a decrypted plaintext, a credential name) on 4 types; the rest mirror the derived output exactly since nothing else is secret
  • RUST/primitive-for-domain-idNodeIdStr/MeshChannelId newtypes replacing bare String mesh-id fields
  • TESTING/tautological-test — 4 tests that asserted nothing now assert real behavior (one via a minimal tracing::Subscriber event counter)
  • TESTING/no-tests — integration tests/ added for akroasis and akroasis-server (this rule only inspects lib.rs/tests/, not module-local #[cfg(test)])
  • RUST/indexing-slicing (error severity) — 2 fixed-array bracket writes rewritten with split_at_mut
  • RUST/no-silent-result-swallow — 8 previously-discarded Results now traced or documented as genuinely infallible
  • RUST/no-result-unwrap-or-default — 15 silent-default fallbacks replaced with explicit error handling, a retyped constant, or a saturating (not zero-defaulting) conversion in a calibration-write guard
  • ARCHITECTURE/trait-impl-colocation — 2 misplaced/missing inline suppressions fixed (one anchor was one line off; rustfmt needed #[rustfmt::skip] to keep the trailing comment on the anchor line)
  • TOPOLOGY/shallow-struct — 23 structs: 4 gained real behavior extracted from duplicated call-site logic (is_expired, is_stale, elapsed_since_heard, allows_frequency/allows_power_level), 19 are genuinely pure-data DTOs marked with the rule's own // WHY: pure data carve-out

Also fixed (pre-existing debt that was never baselined — surfaced by unrelated PRs after the original #261 baseline was written, but blocking a clean kanon lint .):

  • RUST/file-too-longtopology.rs (834→496 lines) and tamper_log.rs (851→739 lines) split via the sibling-file convention already used elsewhere in this codebase
  • YAML/missing-concurrency — concurrency group on gate-attestation.yml
  • SHELL/unpinned-actionactions/stale@v11 pinned to its SHA

Left deliberately (25 remaining baseline entries — not deferred, evaluated and kept)

  • RUST/no-arc-mutex-anti-pattern (17, all collector.rs) — already tokio::sync::Mutex, the rule's own recommended async-safe primitive. The rule is a pure text match on Arc<Mutex< and can't see which Mutex is imported. Converting further to RwLock needs a per-callsite read/write classification across 43+ call sites in 5 files — an architecture change, not a lint fix.
  • VOCAB/crate-name-collision + NAMING/no-fleet-collision (koinon) — a cross-repo naming call the team already explicitly deferred (akroasis#264: "left for an explicit naming decision rather than resolved unilaterally").
  • NAMING/no-owner-prefix (akroasis-server) — needs a GNOMON-reviewed rename; an identity decision, not a mechanical fix.
  • ARCH/substrate-dead-dep (sphragis) — deliberately staged git dependency; tracked follow-up (akroasis#172) is "wire sphragis into the pinax reference-store workflow" + a cryptographic review, neither of which has happened yet because pinax doesn't exist as code yet.
  • RUST/plain-string-secret (ListEntryReport.credential_type) — confirmed rule false positive: the field is a JSON-serialized category label ("ApiKey", "Password"), not secret material. The rule's allowlist covers _hash/_digest/_fingerprint/public_key suffixes but not _type.
  • RUST/doc-promised-observability (delivery.rs::prune_completed) — confirmed rule false positive: the doc contains "records" (the data-record noun) + a structured "WARNING:" comment tag, which collide with the rule's verb/object keyword heuristic. Nothing here claims tracing.
  • CI/release-yml-missing-attestation — confirmed rule false positive: release-please.yml only creates a version-bump PR/tag, it builds no artifacts to attest. release.yml (the actual artifact publisher) already has SLSA/SBOM attestation.
  • TOML/missing-trailing-comma (.gitleaks.toml) — confirmed rule false positive via minimal repro: a trailing # comment after a valid comma on the last multi-line array element defeats the checker regardless of quote style.

Verification

Refs #261

admin added 16 commits August 3, 2026 21:31
…order

RUST/non-exhaustive-enum: add #[non_exhaustive] to FileFormat, RadioCommand,
ExportFormat, RadioVariant, VaultCommand, VaultCliError — none of these are
matched exhaustively cross-crate, so this is additive.

RUST/unreachable-in-match: resolve_target's single-radio arm replaces an
unreachable!() panic with a direct ok_or() — the len()==1 invariant is now
expressed without a panic path at all.

ARCHITECTURE/trait-impl-colocation: mirror the established #[rustfmt::skip]
+ trailing kanon:ignore pattern (already used for SerialPort/AlertSink/
Collector) on Hardware's stub impl — the real impl (SerialHardware) lives
in serial_hardware.rs.

RUST/import-order: reorder mesh/mod.rs test imports (external before
crate-local).

TOPOLOGY/shallow-struct: mark DetectedRadio as pure data (a detection
result snapshot with no derived invariant).

Refs #261
CONTEXT/preamble-required: add the scope/defers_to/tightens preamble to
AGENTS.md's hand-authored section (the generated kanon:auto block already
carried its own).

DOCS/stale-local-link: docs/lexicon.md pointed at a GNOMON.md that has
never existed in this repo (canonical copy lives in kanon); reword as a
plain-text pointer instead of a promised-but-broken link. AGENTS.md's
generated block linked workflow/AGENTS-mcp-tools.md the same broken way —
CLAUDE.md already states the identical fact as a backtick path, not a
link; match that.

WRITING/weasel-word: drop "mostly" from a claim that holds without
qualification (fjall-column-encryption.md).

WRITING/elegant-variation, WRITING/temporal-staleness: reference-store.md
cycled data/payload/content for the same concept within one section —
settled on "content"; dropped "currently" from a fact that doesn't need
temporal qualification.

Refs #261
…ends

RUST/primitive-for-domain-id: introduce NodeIdStr and MeshChannelId
newtypes (types.rs) for the raw hex node-id and channel-name strings that
mqtt.rs's GatewayInfo and node_db.rs's UserInfo.id carried as bare String.
#[serde(transparent)] keeps the wire shape unchanged.

TOPOLOGY/shallow-struct: MeshNode gains elapsed_since_heard(now), moving
the last_heard-vs-now computation duplicated at its one call site
(discovery.rs) onto the type that owns the field.

RUST/test-missing-use-super: lib.rs's proto import now goes through
super:: (functionally identical to crate::, but makes the `use super::`
line real instead of adding a redundant unused glob).

RUST/no-silent-result-swallow: discovery.rs's two `let _ = tx.send(..)`
broadcast sends (no-receivers is a legitimate, non-fatal condition) now
trace the miss instead of silently dropping it.

TESTING/tautological-test: mqtt.rs's decode_invalid_bytes_returns_error
asserted nothing; every byte in the fixture has its varint continuation
bit set, so the decode deterministically errors — assert that.

Refs #261
RUST/doc-promised-observability: ensure_active's doc says it "emits a
failover event," but the cooldown-skip branch was previously silent — an
operator watching logs would see nothing when a needed reselection was
suppressed. Trace it.

TOPOLOGY/shallow-struct: mark GatewayState as pure data (a tracked
snapshot; nothing currently queries staleness on it).

Refs #261
…havior

TOPOLOGY/shallow-struct: PendingMessage and InflightMessage gain
is_expired(now) (and InflightMessage a has_timed_out(now) for the ACK
timeout), deduplicating the `now.duration_since(x.created) >= x.ttl`
check that was repeated at four call sites in OutboundQueue. LinkQuality
gains is_stale(cutoff), matching the fresh/stale cutoff comparison
already duplicated in remove_stale_nodes/remove_stale_links.

RUST/file-too-long: topology.rs's #[cfg(test)] block (338 lines) moves to
topology_tests.rs via the #[path = "..."] sibling-file convention already
used by collector_tests.rs/processor_tests.rs — 834 lines down to 496.

Refs #261
…orts

RUST/indexing-slicing (error severity): build_nonce wrote into a fixed
[u8; 16] via bracket-range indexing; rewritten with split_at_mut so the
8/4-byte layout has no panic-shaped `nonce[a..b]` access at all.

RUST/no-silent-result-swallow: clearing DTR/RTS on connect is
best-effort (some backends don't support the control lines); both calls
now trace a failure instead of silently discarding it.

RUST/no-result-unwrap-or-default: store_forward.rs's drain_for used
`.unwrap_or_default()` after HashMap::remove — that's an Option, not a
Result (the rule's static heuristic doesn't special-case `remove`); added
the WHY the rule's own carve-out asks for rather than leaving it flagged.

RUST/import-order: transport/mod.rs's `use tracing::instrument` (external)
sorted after the crate-local block; moved it into the external group.

Refs #261
… split entry types

RUST/no-debug-derive-on-public-types: LogEntryKind derived Debug while
carrying a credential name (VaultMutation.credential_name) — a label, not
the secret itself, but Debug output lands in logs. Manual Debug impl
redacts just that field; every other variant mirrors the derived output
exactly.

RUST/no-result-unwrap-or-default: decode_entry and verify_chain both
silently defaulted a failed u64->usize payload_len conversion to a
zero-length buffer (only reachable on 32-bit-usize targets, since
payload_len is already bounded by MAX_ENTRY_BYTES) — now report it as the
same corruption/oversized-payload failure the surrounding code already
uses instead of reading a truncated buffer.

RUST/file-too-long: LogEntryKind (enum + its manual Debug impl) moves to
tamper_log_entry.rs via the established #[path = "..."] sibling-file
convention — tamper_log.rs was pushed to 851 lines by the Debug impl
above; now 739.

TOPOLOGY/shallow-struct: mark KnownUsbDevice (koinon/hardware.rs) and
VerificationResult (tamper_log.rs) as pure data — a static lookup-table
row and a verification result bag, neither with a derived invariant.

Refs #261
…rite

RUST/no-debug-derive-on-public-types: DecryptedEntry.secret is the actual
decrypted plaintext credential — redact it in a manual Debug impl instead
of deriving. EntryInfo and VaultEntry only touch `credential_type` (an
enum tag) and `encrypted_data` (ChaCha20-Poly1305 ciphertext), neither of
which is secret material; their manual impls mirror the derived output
exactly.

RUST/no-silent-result-swallow: hex()'s `write!` into a String is
infallible (fmt::Write's Result exists only for the trait's generality
over fallible writers) — documented with the WHY the rule's own carve-out
asks for.

TOPOLOGY/shallow-struct: mark EntryHistory as pure data (a lifecycle
query result bag).

Refs #261
…tor channel

TESTING/tautological-test: tracing_sink_emit_does_not_panic only checked
that emit() didn't panic; it never verified the sink emitted anything.
Replaced with a minimal tracing::Subscriber that counts events, asserting
TracingSink::emit records exactly one. smoke.rs's two constructor-only
tests gained real assertions: SignalAggregator::extract_feature returns
the expected dBm for an RF jamming signal; a fresh ConvergenceGrid with
no ingested signals detects nothing.

RUST/no-silent-result-swallow: pipeline.rs's fan-to-aggregator send
(channel closed = aggregator exited, not fatal) now traces the miss
instead of silently discarding it, matching the sibling grid-channel send
right below it.

TOPOLOGY/shallow-struct: mark AggregatedSignal, Alert, DomainHit, and
Convergence as pure data — pipeline carriers and result records with no
derived invariant.

Refs #261
…-range saturation

RUST/indexing-slicing (error severity): encode_channel wrote the rx/tx
frequency and tone fields into a fixed [u8; 16] via bracket-range
indexing; rewritten with split_at_mut, matching the codebase's own
existing SAFETY-commented precedent for the two already-suppressed
single-index writes in the same function.

RUST/no-result-unwrap-or-default: BLOCK_SIZE was declared `usize` and
converted at every one of its 10 use sites via
`u16::try_from(BLOCK_SIZE).unwrap_or_default()`, even though every use
site sends it as u16 — retyped the constant to u16 (matching the sibling
u8 READ_BLOCK_SIZE/WRITE_BLOCK_SIZE constants' infallible-From pattern),
which drops the conversion entirely. is_forbidden's `len` conversion
(a real runtime value, unlike the constant) now saturates to u16::MAX
instead of defaulting to 0 on overflow — this is a calibration-write
guard, so a conversion failure must widen the checked range, not silently
collapse it to a zero-length check that could let a forbidden-address
write through. tone_codec.rs's DCS-index conversion (bounded 1..=104,
matching the neighboring already-safe cast) switched from
try_from+unwrap_or_default to a plain `as u16` with the same SAFETY
comment its sibling arm already carries.

TOPOLOGY/shallow-struct: mark BlockOp and PowerMapping as pure data (a
protocol operation descriptor and a static lookup-table row).

Refs #261
…colocation

RUST/prefer-expect-over-allow: cables.rs/detect.rs/usb.rs/warnings.rs test
modules used #[allow(...)] — switched to #[expect(...)], which fails
loudly once a listed lint stops firing instead of silently going stale.

RUST/no-silent-result-swallow: try_magic_sequence's closing ACK write (the
ident bytes are already read at that point, so a write failure doesn't
invalidate the identification) now traces the failure instead of
discarding it.

ARCHITECTURE/trait-impl-colocation: RadioProber's kanon:ignore marker sat
on the line inside the impl block, one line below the `impl X for Y {`
the violation actually anchors on — moved it onto that line, matching the
established #[rustfmt::skip] + trailing-comment pattern used elsewhere in
this fleet.

TOPOLOGY/shallow-struct: mark KnownCable, RadioIdent, DetectedRadio, and
UsbCable as pure data — static lookup-table rows and detection results
with no derived invariant.

Refs #261
TOPOLOGY/shallow-struct: RadioConstraints gained allows_frequency() and
allows_power_level() methods, replacing the free freq_in_bands() function
and the direct .power_levels.contains() call in validate_channel — the
"does this constraint permit X" logic now lives on the constraint type
itself instead of being scattered across the validating function.

Refs #261
RUST/todo-marker-needs-quadrant-and-artifact: the four TODO(#80) markers
in yaesu/{codec,protocol,variant}.rs carried a tracking issue but no
debt-quadrant tag. All four are a deliberate, scoped-out stub awaiting
real hardware capture (ADMS-14) — [deliberate-prudent]. Format is
TODO(#NNN)[quadrant]: text rather than TODO[quadrant] #NNN: text, because
the older RUST/todo-no-issue and META/rule-todo-without-issue rules
specifically require the literal TODO(#NNN) substring.

Refs #261
…b crates

TESTING/no-tests: this rule only inspects lib.rs and the crate-root
tests/ directory — module-local #[cfg(test)] blocks in mesh/radio/vault
don't satisfy it, and neither crate had a tests/ directory. Added one
exercising each crate's public boundary rather than a placeholder:
akroasis's resolve_target()/StubHardware wiring and RadioVariant display
names; akroasis-server's router actually serving a request (a malformed
route table panics axum at build time, so a real response is a genuine
assertion) plus ApiError's status-code mapping.

Refs #261
… SHA

YAML/missing-concurrency: gate-attestation.yml had no concurrency group;
added one keyed on PR number (falling back to ref for the push trigger),
matching the pattern already used by release-please/release/dependabot-
auto-merge/security.

SHELL/unpinned-action: actions/stale@v11 was a floating tag; pinned to
its resolved commit SHA, matching every other action in this repo.

Refs #261
Entry count: 104 -> 25 (23 further entries were already dead-anchor drift
from PRs merged since the last refresh; --write-baseline recomputed every
surviving entry's hash/line against current main rather than hand-editing
line numbers, per the anchor-drift trap this campaign was warned about).

Rule-classes cleared entirely (fixed, not suppressed):
RUST/non-exhaustive-enum, RUST/import-order, CONTEXT/preamble-required,
DOCS/stale-local-link, RUST/unreachable-in-match, RUST/test-missing-use-super,
WRITING/weasel-word, WRITING/temporal-staleness, WRITING/elegant-variation,
RUST/todo-marker-needs-quadrant-and-artifact, RUST/prefer-expect-over-allow,
RUST/no-debug-derive-on-public-types, RUST/primitive-for-domain-id,
TESTING/tautological-test, TESTING/no-tests, RUST/indexing-slicing,
RUST/no-silent-result-swallow, RUST/no-result-unwrap-or-default,
ARCHITECTURE/trait-impl-colocation, TOPOLOGY/shallow-struct.

Also fixed, never baselined: RUST/file-too-long (topology.rs, tamper_log.rs
split), YAML/missing-concurrency, SHELL/unpinned-action.

Remaining 25 entries (17 RUST/no-arc-mutex-anti-pattern + 8 singles) are
deliberate exceptions, not deferred mechanical work — see the baseline
`reason` field and the PR description for the rationale on each.

Refs #261
@forkwright
forkwright merged commit 7004d73 into main Aug 4, 2026
8 checks passed
@forkwright
forkwright deleted the fix/akroasis-lint-burndown branch August 4, 2026 02:40
forkwright pushed a commit that referenced this pull request Aug 4, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.1.21](v0.1.20...v0.1.21)
(2026-08-04)


### Bug Fixes

* **lint:** burn down the kanon-lint baseline — 104 entries down to 25
([#358](#358))
([7004d73](7004d73))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant