Skip to content

hardening/v0.6.x: strictness waves 1+2, AGPL license, MSRV honesty, gate tightening#78

Merged
utof merged 15 commits into
mainfrom
hardening/v0.6.x
Apr 20, 2026
Merged

hardening/v0.6.x: strictness waves 1+2, AGPL license, MSRV honesty, gate tightening#78
utof merged 15 commits into
mainfrom
hardening/v0.6.x

Conversation

@utof

@utof utof commented Apr 20, 2026

Copy link
Copy Markdown
Owner

Summary

Hardening sweep on a long-lived feature branch: two Rust strictness waves, license switch to AGPL-3.0-or-later, MSRV alignment with actual dep requirements, ESLint + rustdoc gate tightening, and the follow-up fixes they each surfaced. 14 atomic commits, each independently revertable; full local gate (`just ci`) green at HEAD.

Changes (by commit, bottom-up)

Prereq hardening

  • `3e73070` chore(release): sync Cargo.lock to v0.6.4 (orphaned from prior release commit)
  • `081c694` refactor(core,db): repository traits take `&self` instead of `&mut self` — unblocks `Arc` sharing across Tauri commands and future HTTP handlers; 32 unused-mut cleanups bundled
  • `9d0d932` chore(deny): drop stale RUSTSEC-2025-0099/0101 advisory ignores (crates no longer in graph)

Strictness wave 1 (Rust, zero/small-scope fallout)

  • `39105b6` chore(lints): `clippy::dbg_macro/todo/unimplemented = deny` + `undocumented_unsafe_blocks = warn`
  • `1b03afe` chore(lints): `missing_debug_implementations = warn` + 9 manual `finish_non_exhaustive()` impls
  • `b90b036` chore(lints): `clippy::unwrap_used = warn` + scoped `#[allow(..., reason = ...)]` on 2 test modules

Strictness wave 2 (Rust, policy-sensitive)

  • `2b16067` chore(lints): `clippy::print_stdout/print_stderr = warn` + CLI crate-root allow (`tracing` migration flagged for future)
  • `26f933d` chore(lints): `unreachable_pub = warn` + 38 `pub → pub(crate)` conversions in CLI binary
  • `3f6da0b` fix(cli): revert unneeded `pub(crate) mod` promotions + correct the reason string (follow-up to above)

License + MSRV + gate tightening

  • `ca86eed` chore(license): switch from MIT OR Apache-2.0 → AGPL-3.0-or-later (canonical upstream text committed; deny.toml updated)
  • `e3113ab` chore(msrv): pin `rust-toolchain.toml` to 1.88; bump `rust-version` to match actual dep requirements (sysinfo, time each require 1.88+)
  • `080bf6e` chore(gates): ESLint `--max-warnings 0` + `RUSTDOCFLAGS="-D warnings"` (caught a `Vec` HTML-tag bug in a doc comment)

Follow-up fixes surfaced by the above

  • `e1cf823` fix(msrv,lints): relax toolchain pin to `stable` (specta rc24 requires post-1.88 features); collapse 3 if-let nests that stable's clippy 1.95 flagged
  • `333c4b6` fix(desktop): 9 clippy errors in desktop crate that only surfaced when the pre-push hook ran the full workspace gate (sandbox had no gtk deps to catch them earlier)

Deferred / tracked separately

GitHub issues #63#77 cover the remaining work:

Test plan

  • `cargo clippy --workspace --all-targets -- -D warnings` clean on the user's machine (pre-push hook verified)
  • `cargo test --workspace --all-targets` passing (pre-push hook verified)
  • `cargo deny check` passing (advisories/bans/licenses/sources all ok)
  • `cargo doc --workspace --no-deps` clean under `RUSTDOCFLAGS="-D warnings"`
  • `bun run lint --max-warnings 0` clean
  • Pre-push lefthook all steps green (clippy, cargo-test, cargo-doctest, docs-coverage, cargo-deny, frontend-build, frontend-test)
  • CI confirms the above on the 3-OS matrix (Linux/macOS/Windows) — will run on this PR

Merge strategy

Create a merge commit (or rebase-and-merge). Do not squash — the 14 commits are designed to be independently revertable; each commit passes `cargo clippy -D warnings` on its own.

utof added 15 commits April 20, 2026 01:08
Housekeeping — Cargo.lock version bumps that were missed
by the v0.6.4 release commit (493d27c). Zero behavior
change; all workspace crates already at 0.6.4 in Cargo.toml.
FileRepository::upsert_file, FileRepository::upsert_location,
VolumeRepository::find_or_create, VolumeRepository::record_mount
all previously took &mut self, which forced adapters to be stored
as &mut references rather than Arc<dyn Trait>. The &mut was
spurious — the SqliteFileRepository and SqliteVolumeRepository
impls already use Mutex<Connection> for interior mutability, so
each method acquires the lock via &self.conn.lock() and holds it
for the full method body.

WHY: this is a blocker for Arc<dyn FileRepository> sharing across
Tauri commands, future axum handlers (Phase 6), and the writer-
actor pattern the architecture audit recommends. No behavior
change; all 139 tests across perima-core/-db/-fs/-hash/-media
and perima (CLI) pass.

Also applies cargo-fix unused_mut cleanups (32 fixes) across db
tests and CLI bindings that held 'let mut repo = ...' — the mut
is no longer required at call sites.
cargo deny check reported 'advisory-not-detected' for these
two entries — the upstream unic-char-property and urlpattern
deps that triggered them are no longer in the dependency
graph (tauri transitive swap). Dead config removed to keep
the ignore list accurate.

Verified: advisories/bans/licenses/sources all ok.
Adds clippy::dbg_macro=deny, clippy::todo=deny, clippy::unimplemented=deny,
clippy::undocumented_unsafe_blocks=warn to the workspace-level lint block.

All four fire zero times on HEAD; this is pure forward-insurance catching
AI-slop patterns (stray dbg!, todo!, unimplemented! stubs) before they
land. undocumented_unsafe_blocks currently has no unsafe blocks to guard
(all safe crates carry #![forbid(unsafe_code)]), so it catches the first
unsafe block any future ffi crate ships without a // SAFETY: comment.

Also fixes two pre-existing clippy::redundant_clone and
clippy::needless_pass_by_ref_mut warnings in file_repo.rs and scan.rs
that blocked the -D warnings gate before this commit.

Source: docs/research/rust_strictness.md §1, §6, §8.
Enables missing_debug_implementations=warn at the workspace level and
closes the 9 resulting warnings with manual Debug impls using
f.debug_struct(…).finish_non_exhaustive() — the standard pattern for
structs holding non-Debug fields (Mutex<Connection>, fs watcher handles,
Tokio channel handles).

WHY manual impl over #[derive(Debug)]: rusqlite::Connection does not
implement Debug; neither do several of the OS-handle / channel types
the fs/media structs hold. finish_non_exhaustive() prints
'StructName { .. }', giving observers struct identity plus an indicator
that internal state is elided. Uniform pattern across all 9 sites
avoids per-struct judgment calls about which fields are safe to log.

Source: docs/research/rust_strictness.md §1 (axum adopts this pattern).
Enables clippy::unwrap_used=warn at the workspace level. All 5 existing
call sites are in #[cfg(test)] modules; each module gets a scoped
  #[allow(clippy::unwrap_used, reason = "tests: unwrap is the assertion")]
rather than per-call-site allows.

WHY: .unwrap() in non-test code is an AI-slop red flag — LLMs reach for
it to silence borrow errors or skip error design. In tests the panic IS
the assertion, so unwrap is idiomatic. This configuration makes future
non-test .unwrap() fail the clippy gate, forcing explicit per-site
justification via #[allow(..., reason = ...)].

Source: docs/research/rust_strictness.md §1 and §8.
Enables clippy::print_stdout=warn and clippy::print_stderr=warn at the
workspace level. 35 existing fires are all in crates/cli/ (the binary
whose purpose is user-facing output). A crate-root #![allow(...)] in
crates/cli/src/main.rs covers the binary and all its cmd/*.rs modules
with a reason string that records the policy and names tracing as the
future migration target.

WHY crate-root over per-function: 35 annotations for zero added safety
in a small, single-purpose binary. Non-CLI crates (core, db, fs, hash,
media, desktop) must not println!/eprintln! — the lint still fires
there and -D warnings will block regressions.

Source: docs/research/rust_strictness.md §1, §8.
… CLI

Enables the rustc unreachable_pub lint at the workspace level and
converts 38 `pub` items in crates/cli/ to `pub(crate)`. Binary crates
have no external consumers — any `pub` item not re-exported from
main.rs should be crate-scoped.

The lint is a rustc lint (not clippy), goes in [workspace.lints.rust].
Distribution of 38 fires across 13 files is listed in the wave-2 plan.
All fixes are mechanical: leading `pub ` → `pub(crate) `. Nothing
semantically changed — binary crates have no external API surface,
so visibility tightening is a no-op observable-behavior-wise.

WHY main.rs also changed: module declarations promoted from `mod` to
`pub(crate) mod` so the pub(crate) items inside them satisfy
unreachable_pub (items inside a private module trigger the lint even
with pub(crate)). A crate-root #![allow(clippy::redundant_pub_crate)]
is required because clippy::nursery's redundant_pub_crate fires on
pub(crate)-inside-pub(crate)-module; the allow is scoped to the binary
with a reason string recording the intentional policy.

Source: docs/research/rust_strictness.md §1, §8 (research doc had it
under clippy; it's actually rustc).
Commit 26f933d (wave-2 Task 2) promoted five top-level module
declarations in main.rs from `mod` to `pub(crate) mod` on the
belief that unreachable_pub fires on pub(crate) items inside
private modules. That belief was wrong: unreachable_pub only
flags `pub` items; pub(crate) items inside a private mod are
already reachable from the crate root and the lint is silent.

The real reason main.rs needed the crate-root allow is
clippy::nursery::redundant_pub_crate firing on pub(crate) items
inside private `mod` blocks (where pub(crate) is technically
redundant). The allow still earns its keep; the pub(crate) mod
promotion was pure churn.

Reverts 5 promotions to plain `mod` and rewrites the allow's
reason string to reflect the actual mechanism. Verified: clippy
-D warnings is clean; 139 tests still pass.

Follow-up to 26f933d per plan review.
Per project-owner direction. Replaces the previous 'MIT OR Apache-2.0'
dual declaration in workspace Cargo.toml with 'AGPL-3.0-or-later'
(SPDX).

- Cargo.toml: license field updated; applies to every workspace
  crate via workspace.package inheritance.
- LICENSE: canonical GNU AGPLv3 text, fetched verbatim from
  https://www.gnu.org/licenses/agpl-3.0.txt. No paraphrasing;
  bit-identical to the upstream file (661 lines).
- deny.toml: AGPL-3.0-or-later added to [licenses].allow so
  cargo-deny accepts our own workspace crates (the allowlist
  applies to every crate in the graph, including workspace
  members).

AGPL vs MIT/Apache is a meaningful policy change: copyleft with a
network-use clause. Anyone providing a network service using perima
(or derivative works) must offer the same source under AGPL.
Deliberate choice.
The workspace previously declared rust-version = '1.85' but several
transitive dependencies (sysinfo@0.38.4, time@0.3.47, time-core@0.1.8)
require rustc 1.88+. The MSRV declaration was therefore decorative —
local builds worked only because contributors happened to run newer
stable toolchains. Anyone honoring the declared MSRV would fail to
build.

Fix:
- Cargo.toml rust-version bumped 1.85 → 1.88 to match actual dep
  requirements (the honest MSRV).
- rust-toolchain.toml added pinning channel = '1.88'. Makes every
  cargo invocation use exactly 1.88, so API usages that would only
  compile on newer stable are caught here rather than surfacing on
  a contributor's machine.
- Two clippy::similar_names errors from 1.88 (the lint's heuristic
  was stricter in 1.88 than in 1.94+ where earlier development ran):
  - crates/core/src/types.rs: nfc/nfd → precomposed/decomposed
  - crates/db/src/search_repo.rs: cam/cap → camera/captured (plus
    matching renames at the use-sites in the GroundTruthRow struct).

Verified: cargo clippy -D warnings clean; 139 tests passing; cargo
deny check passing; rustdoc -D warnings clean.

Bump rust-version + channel in lockstep on any future MSRV change.
Three coupled gate tightenings that strengthen the existing strictness
posture:

- apps/desktop/package.json: 'eslint src/' → 'eslint src/ --max-warnings 0'.
  Before this change, 'bun run lint' exited 0 on any number of warnings
  — 15 rules are currently at 'warn' level (including
  @typescript-eslint/no-explicit-any and 13 react-hooks experimental
  rules). Lefthook and CI called lint but didn't actually gate on it.
  --max-warnings 0 promotes warns to blocking errors, matching the
  '-D warnings' posture the Rust side already has.

- justfile docs-coverage: prefix with RUSTDOCFLAGS="-D warnings".
  The workspace-level rustdoc lints (broken_intra_doc_links,
  private_intra_doc_links) only fire during cargo check/clippy. rustdoc
  has a separate warning path for things like invalid HTML tags,
  malformed code spans, bare URLs, etc. This gate catches those.

- crates/cli/src/cmd/tag.rs: backtick Vec<T> in a doc comment so
  rustdoc doesn't parse <T> as an unclosed HTML tag. Caught today by
  the new RUSTDOCFLAGS=-D warnings gate — exactly the kind of silent
  slop it's meant to surface.

No behavior change at runtime. ESLint + cargo doc both pass under
the tightened gates; 139 tests still passing.
The rust-toolchain.toml pin of channel = '1.88' (commit e3113ab) was
too strict: the desktop crate's transitive dep 'specta = =2.0.0-rc.24'
uses 'debug_closure_helpers' and const 'TypeId::of', both stabilized
after 1.88. The pre-push lefthook (which runs 'just ci' including
clippy --workspace) therefore failed before the push itself could run.

Fixes:

- rust-toolchain.toml channel '1.88' -> 'stable'. Matches CI's
  dtolnay/rust-toolchain@stable so local and CI resolve identically.
  The declared rust-version = '1.88' in Cargo.toml stays put as an
  aspirational MSRV floor; true MSRV enforcement is deferred to a
  CI matrix job tracked in a follow-up issue.

- Three clippy::collapsible_if fires surfaced by stable (1.95) that
  1.88 did not flag. Collapsed each nested 'if let' using the stable
  if-let-chains syntax (Rust 2024 edition):
  * crates/fs/src/watcher.rs: map_event + bus.emit
  * crates/media/src/extractor.rs: stsd.as_ref() + Video descriptor
  * crates/cli/src/cmd/scan.rs: UpsertOutcome match + queue.enqueue

Verified: clippy -D warnings clean on all six non-desktop crates;
139 tests passing; semantically identical to the original nests.
The wave-1 and wave-2 strictness commits added workspace-level
clippy/rustc lints but were verified only on the non-desktop
crates (gtk system deps unavailable in the sandbox). The pre-push
lefthook builds the full workspace and surfaced 9 errors in
crates/desktop/. Each is a mechanical follow-up to an earlier
commit:

- commands.rs: 2 call sites of VolumeRepository::find_or_create
  and ::record_mount were passing '&mut vol_repo' even after the
  trait methods became &self (commit 081c694). Changed to '&vol_repo'.
- commands.rs: 2 functions (run_scan_live, persist_file) took
  '&mut FR' / '&mut R' params that are no longer used mutably
  after the trait-sig fix. Changed to '&FR' / '&R'. Cascaded
  unused-mut cleanup to 2 let-bindings.
- commands.rs: 1 triple-nested if-let surfaced by clippy 1.95's
  stricter collapsible_if — collapsed using if-let-chains syntax.
- events.rs (TauriEventEmitter), state.rs (AppState, WatcherState):
  3 structs needed manual 'impl Debug' using the
  finish_non_exhaustive() template introduced in wave 1 (commit
  1b03afe) for structs holding non-Debug fields (AppHandle, Arc<
  repos with Mutex<Connection>>, tokio Mutex<Option<...>>).

These fixes were authored blind (sandbox can't build desktop); the
pre-commit rustfmt hook applied one small reformat after the
signature changes, which is included.
Root cause: `just ci` calls `cargo deny check` + `typos`, but neither
binary is installed in the workflow. All 3 OS jobs failed with
`error: no such command: deny` at the `cargo deny check` step.

Fix: add `taiki-e/install-action@cargo-deny` + `@typos` after `@just`
(before `oven-sh/setup-bun`). Prebuilt binaries, not `cargo install`
— seconds per job instead of minutes.

This was pre-existing on main (last green main was before the `deny`
and `typos` recipes were added to `just ci`). PR #78 is the first run
to surface it because prior branches didn't trigger PR-on-push.
@utof
utof merged commit 4b272ec into main Apr 20, 2026
3 checks passed
@utof
utof deleted the hardening/v0.6.x branch April 20, 2026 01:23
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