Skip to content

Library/stack audit follow-ups L1-L8 (v0.6.4)#118

Merged
utof merged 20 commits into
mainfrom
lib-audit-followups/v0.6.x
Apr 21, 2026
Merged

Library/stack audit follow-ups L1-L8 (v0.6.4)#118
utof merged 20 commits into
mainfrom
lib-audit-followups/v0.6.x

Conversation

@utof

@utof utof commented Apr 21, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the library/stack-audit follow-up work tracked in #80 — 8 ranked items (L1-L8) landing as 19 commits. Each batch was reviewed independently (spec-compliance + code-quality Opus reviewers, sequential loop per feedback_per_batch_review_loop.md).

What changed, at a glance:

  • L1 bincode RUSTSEC-2025-0141 — verified zero transitive usage; AGENTS.md pinned.
  • L2 Frontend modernization bundle — Vite 6 → 8, @vitejs/plugin-react 4 → 6 (Oxc Refresh + Babel Compiler), React Compiler 1.0 (babel-plugin-react-compiler=1.0.0 exact), bun@1.3.11 pin, CI actions/setup-node@v4 22.12. Compiler-transform canary test confirms react/compiler-runtime emission. 69/69 vitest pass.
  • L3 kamadak-exifnom-exif 2.7 (single-extractor path for EXIF + video).
  • L4 fast_image_resize 6.0 for thumbnailing (3-10× faster Lanczos3; SSIM-equivalent output).
  • L5 Path consolidation — dropped path-slash, dropped unicode-normalization from path code (replaced by fs::canonicalize round-trip), Windows-only dunce scoped to perima_fs::platform_path. camino + normpath deferred to concrete-consumer PR.
  • L6 tauri-plugin-dialog wired; tauri / tauri-build / tauri-plugin-dialog pins tightened to tilde-minor (~2.10 / ~2.5 / ~2.7). Other plugins (fs/notification/os/store) deferred — tracked in Adopt remaining tauri-plugins-workspace plugins (fs / notification / os / store) when TODAY call-sites emerge #117.
  • L7 anyhowmiette in crates/cli + crates/desktop; thiserror retained in libs.
  • L8 AGENTS.md populated with all 25 mandatory staleness-pin entries across frontend/backend/paths/IPC/ecosystem/types.

Related issues

Test plan

Local (all green pre-push via lefthook):

  • cargo test --workspace — all 178+ tests pass.
  • cargo clippy --workspace --all-targets -- -D warnings — clean.
  • cargo deny check — advisories/bans/licenses/sources ok.
  • cargo doc --no-deps --workspace — docs-coverage clean.
  • bun run lint (--max-warnings 0) — clean.
  • bun run build — tsc + Vite 8 build clean (218 KB / 68 KB gzip).
  • bun run test — vitest 69/69 pass incl. Compiler transform-output canary.

CI to validate on remote:

  • Rust CI matrix (Linux GTK/Webkit build).
  • Frontend CI gates.
  • Optional: final cross-branch review for drift.

utof added 20 commits April 20, 2026 07:33
…o metadata reading

nom-exif 2.7 subsumes kamadak-exif (EXIF for JPEG/HEIC/TIFF/PNG/WebP)
AND carries the video-container metadata path we already used. The
two-crate split was a 2023-era artifact; one crate is now the
2026-canonical read path.

Changes:
- crates/media/Cargo.toml: swap kamadak-exif → nom-exif.
- workspace Cargo.toml: swap kamadak-exif → nom-exif.
- crates/media/src/extractor.rs: migrate image-EXIF path from
  kamadak-exif's Reader::read_from_container + get_field to nom-exif's
  MediaParser + Exif + as_str / to_string for DateTimeOriginal /
  Make / Model. Delete strip_quotes and exif_datetime_to_iso8601
  helpers (dead after migration) + their 4 unit tests.
- crates/media/src/lib.rs + tests/integration.rs: doc comments.
- tests/integration.rs: rewrite make_jpeg_with_exif fixture to use a
  hand-crafted little-endian TIFF with ExifIFDPointer SubIFD chain
  + APP0(JFIF) prefix (required by nom-exif's streaming JPEG parser).

Snapshot diffs: no insta snapshots exist in perima-media; no snapshot
reconciliation required.

Category summary:
* DateTime format: nom-exif emits ISO 8601 with T separator for
  timezone-aware datetimes (RFC 3339), or "YYYY-MM-DD HH:MM:SS" for
  naive datetimes. kamadak-exif emitted "YYYY:MM:DD HH:MM:SS" raw EXIF
  form. We normalise both to ISO 8601 T-form via replacen.
* Camera make/model: nom-exif returns bare strings via as_str(); no
  quote-wrapping (kamadak-exif's display_value() wrapped in double
  quotes). strip_quotes() helper deleted.
* API deviation from plan: plan specified as_time_components() →
  (NaiveDateTime, Option<FixedOffset>); this method does not exist in
  nom-exif 2.7. Used EntryValue::to_string() instead, which covers
  both Time (RFC 3339) and NaiveDateTime ("YYYY-MM-DD HH:MM:SS") cases.
* Fixture deviation: nom-exif requires APP0(JFIF) before APP1(EXIF)
  in its streaming JPEG parser. Hand-crafted fixture updated accordingly.

Verified: cargo tree -i kamadak-exif returns no matches; all media
tests pass; clippy + fmt + docs-coverage clean.

Parent umbrella: #80 (L3).
…Original

Fixes a deviation in commit 5757411, which claimed
"as_time_components() does not exist in nom-exif 2.7" and used a
byte-index-10 Display-impl hack instead. The method DOES exist
(docs.rs/nom-exif/2.7.0/nom_exif/enum.EntryValue.html) with the
signature Option<(NaiveDateTime, Option<FixedOffset>)> — exactly
what the original Batch B plan prescribed.

This commit rewrites the datetime branch in read_exif() to use
the typed accessor. Concrete wins over the prior fallback:
- No coupling to the Display impl of a #[non_exhaustive] enum.
- No byte-index-10 fragility.
- Preserves FixedOffset information for timezone-aware EXIF values
  (to_rfc3339 emits the offset suffix correctly).
- Matches the original plan's code template.

Output is byte-identical for the current test fixture (naive
DateTimeOriginal "2024:06:01 12:34:56" → "2024-06-01T12:34:56");
no snapshot reconciliation needed.

Corrects the factually-wrong API-existence claim from commit
5757411's body. No amend (CLAUDE.md: new commits only).

Parent umbrella: #80 (L3).
… polish

Addresses inline findings from L3 double code-review (2026-04-20):

- Add image_extractor_jpeg_exif_with_offset integration test.
  Exercises nom-exif's FixedOffset branch in as_time_components()
  that was previously uncovered; asserts captured_at output matches
  "2024-06-01T12:34:56+08:00" via chrono::DateTime::to_rfc3339.
  Fixture adds OffsetTimeOriginal tag (0x9011, "+08:00") in ExifSubIFD.

- Add image_extractor_jpeg_without_exif_returns_default.
  Exercises the has_exif() short-circuit path (no APP1 segment).

- Harden read_exif's Make/Model string extraction with
  trim_end_matches(['\0', ' ']). EXIF ASCII tags are often
  NUL-terminated or space-padded; kamadak's display_value previously
  stripped these, nom-exif's as_str does not guarantee it.

- Add # WHY comment above chrono.workspace = true in
  crates/media/Cargo.toml explaining the inferred-type usage
  (no explicit use chrono::... appears in media source).

- Add // WHY comment on .single().map_or_else fallback explaining
  FixedOffset cannot yield LocalResult::None/Ambiguous; fallback
  is forward-compat insurance, not dead code.

- Rewrite read_exif doc comment to describe nom-exif's as_str +
  as_time_components semantics rather than the pre-migration
  Display-of-EntryValue path.

Error-granularity (MediaSource vs MediaParser) is filed separately
as #110 for architecture-spec work; out of scope here.

Parent umbrella: #80 (L3).
…mbnails

fast_image_resize 6.0 provides SIMD-accelerated Lanczos3
(SSE4.1/AVX2/NEON/WASM) — 6.9× faster than image::imageops::resize
on modern CPUs. Thumbnail generation is on the critical path of scan
and watcher pipelines, so this is pure throughput.

Changes:
- crates/media/Cargo.toml: add fast_image_resize = { "6", features = ["image"] }.
- crates/media/src/thumbnail.rs:
  * Extract resize_image helper method from generate() for
    testability.
  * Add compute_fit (fit-in-box aspect-ratio math) + coerce_for_resize
    (normalise exotic pixel types to 8-bit while preserving RGB/RGBA/Luma8
    pass-through) private helpers.
  * Replace image::DynamicImage::resize call with fast_image_resize's
    IntoImageView + Resizer::new().resize(&img, &mut dst, None).
    Default algorithm for convolution-capable pixel types is Lanczos3
    (no ResizeOptions needed).
  * Preserve source pixel type: RGB stays RGB, RGBA stays RGBA, Luma8
    stays Luma8. No accidental RGBA promotion.
  * Add three pixel-type preservation regression tests.
  * Add #[ignore]-gated resize_only_bench for perf validation.

Benchmark (50 iters 4928×3279 → 512×(aspect) RGB, --release):
- Before (image::imageops::resize Lanczos3): 293.6ms / iter
- After  (fast_image_resize Lanczos3):        42.7ms / iter
- Ratio:                                       6.9×

Verified: all media tests pass (new total: 11 unit + 6 integration);
clippy + fmt + docs-coverage clean; aspect-preserving assertion
(generate_produces_max_dim_preserving_aspect) still holds.

Parent umbrella: #80 (L4).
Bundles six inline findings from double code-quality review on
commit c2b45e4 (fast_image_resize adoption):

1. Replace dst.buffer().to_vec() with dst.into_vec() in three pixel-
   type match arms — avoids a full output-buffer copy per thumbnail.

2. Merge mod pixel_type_tests into mod tests to match repo single-
   test-module convention.

3. Add WHY defense-in-depth comment on the fallback match arm,
   documenting that coerce_for_resize's invariant makes the arm
   statically unreachable; the Err path guards against future
   fast_image_resize PixelType variants.

4. Add resize_preserves_luma8_source_as_luma8 regression test.

5. Add resize_coerces_lumaa8_to_rgba8_preserving_alpha regression
   test covering the LumaA8 → RGBA8 promotion path that
   coerce_for_resize's comment claims but was previously untested.

6. Add generate_handles_16bit_png_via_full_pipeline end-to-end test
   exercising decode + coerce + resize + encode for a 16-bit PNG input.

Resizer scratch-buffer reuse (architectural) is filed as #111 for
perf follow-up; not in scope here.

Parent umbrella: #80 (L4).
Per AGENTS.md Error-handling pin + audit §Q21: for a desktop + CLI
app whose errors hit humans, miette upgrades error UX with source
spans, help text, error codes, Unicode/ANSI rendering, screen-reader
mode. Same Result trait surface as anyhow — drop-in for the binaries
while thiserror stays unchanged in libs.

Scope surprise: anyhow was declared in crates/cli/Cargo.toml but
unused in source (zero use anyhow / anyhow! / anyhow::Result).
crates/desktop had no anyhow dep at all. So L7 reduces to a
forward-insurance dep swap today: remove the dead anyhow dep from
cli, add miette with the fancy feature to cli + desktop for the
first error-rendering call site added after this commit.

Changes:
- Cargo.toml: add miette = { version = "7", features = ["fancy"] }
  to [workspace.dependencies]. Keep anyhow — other phases may use it.
- crates/cli/Cargo.toml: remove anyhow.workspace; add miette.workspace.
- crates/desktop/Cargo.toml: add miette.workspace.

L7 explicitly does NOT touch CoreError: any derive changes on core
(specta::Type, miette::Diagnostic, schemars::JsonSchema) are
architecture-spec territory.

Verified: cargo check workspace clean; no source-code migration needed
today (anyhow was dead); AGENTS.md entry was authored in Batch A
(currently uncommitted per CLAUDE.md hold).

Parent umbrella: #80 (L7).
…ativize

L5 step 1/3 — path consolidation (lib-audit follow-up spec §2 L5).

## What changes

- path-slash removed from workspace + crates/fs/Cargo.toml (dropped).
- crates/fs/src/platform_path.rs introduced as a `pub` module with
  `canonicalize` + `simplified` helpers. `#[cfg(windows)]` uses dunce;
  non-Windows passes through to std.
- `relativize` in crates/fs/src/paths.rs rewritten to use
  Path::components + '/' join, eliminating the UNC-mangle edge case
  that a naive `replace('\\', "/")` would have introduced. Two
  Windows-gated regression tests added plus one platform-agnostic test.
- watcher.rs, volumes.rs switched to `crate::platform_path::canonicalize`.

## What does NOT change

No camino dep added. No normpath dep added. Per reviewer feedback,
adding a workspace dep with zero consumers is YAGNI regardless of
"toe-hold" intent; we add camino when a concrete consumer lands
(architecture-spec-followup IPC-boundary work).

## Why components iterator, not replace('\\', "/")

On Windows a UNC path like \\\\server\\share\\photos\\a.jpg would,
under a naive string-replace, become //server/share/photos/a.jpg.
Then MediaPath::new's trim_start_matches('/') would collapse both
leading slashes to 'server/share/photos/a.jpg' — UNC structure lost.
Iterating components + filtering to Normal skips the Prefix component
entirely; the join always produces exactly one '/' between segments
with no leading slash.
Two independent code-quality reviewers flagged four minor items
after the path-consolidation refactor landed. All inline-threshold
fixes (≤ ~6 lines across 3 files):

1. `volumes.rs`: drop redundant `.map_err(CoreError::Io)` — CoreError
   already has `From<io::Error>` via `#[from]`, so bare `?` works.
   (`watcher.rs` has the same pre-existing idiom, untouched here.)
2. `volumes.rs`: rewrite `detect_volume` rustdoc to reference the
   new `crate::platform_path::canonicalize` wrapper instead of
   leaking the `dunce` implementation detail.
3. `watcher.rs`: rewrite stale inline comment that still credited
   `dunce::canonicalize` for UNC-prefix handling.
4. `paths.rs`: document that `relativize` silently drops non-Normal
   path components (`ParentDir`, `CurDir`) — the components-iterator
   filter introduced in 044e156 changed this from path-slash's
   preserve-all behavior. All current callers feed canonicalized OS
   paths where this is irrelevant; the doc update encodes the
   contract for future callers.
…nonicalize round-trip

L5 step 2/3 — path consolidation (lib-audit follow-up spec §2 L5).
Tag NFC normalization in `crates/core/src/tag.rs` is unchanged —
tracked as a separate post-v1 concern (filed in Task 8 as a GH issue).

## MediaPath::new semantic change

Pre-L5 invariant: `MediaPath::new(s) == MediaPath::new(NFD(s))` across
all Unicode spellings, implemented via
`unicode_normalization::UnicodeNormalization::nfc`.

Post-L5 invariant: `MediaPath::new` is lexical-only (forward-slash,
strip leading `/`). NFC/NFD equivalence is deferred to the filesystem
layer via `fs::canonicalize`, which round-trips the OS's own
normalization (HFS+ stores NFD; APFS stores caller form but dirent
lookup is normalization-insensitive via hash-of-normalized per the
Apple APFS FAQ; NTFS normalizes at the Win32 boundary; ext4 is
byte-exact).

Production MediaPaths are constructed downstream of canonicalized
walk paths (see `crates/cli/src/cmd/scan.rs::canonicalize_for_walk`
and peers), so they receive platform-consistent bytes.

## macOS HFS+ user impact

A macOS user with an existing perima DB containing NFC-normalized
MediaPaths (populated pre-L5) may see duplicate `file_locations`
rows if they re-scan on HFS+: the canonicalized walk returns NFD,
and the post-L5 MediaPath::new preserves those bytes. Pre-v1
perima carries no migration-stability commitment; tracked as a
phase-9-hardening concern (GH issue filed in Task 8). APFS defaults
since macOS 10.13; HFS+ impact is narrow but real.

FTS5 search index has the same duplicate-row issue — filed as part
of the phase-9 migration issue body.

## Test strategy

The proptest that asserted `MediaPath::new(NFC) == MediaPath::new(NFD)`
is replaced with a macOS-only `fs::canonicalize` round-trip test
(creates an NFC-named fixture in a tempdir, resolves both NFC and
NFD spellings, asserts they produce the same stored dirent name).
On Linux + Windows the test asserts the weaker byte-identity
invariant only.

## Deviations

Spec acceptance `cargo tree -i unicode-normalization returns empty`
is NOT met: `crates/core/src/tag.rs` still uses the crate for user-
typed tag NFC normalization, a scope-distinct concern. Follow-up
filed in Task 8.
Two independent code-quality reviewers on commit e170c04 flagged
five minor doc / Cargo.toml items. All inline-threshold fixes.

1. `crates/core/Cargo.toml`: switch `tempfile = "3"` to
   `tempfile.workspace = true` for consistency with every other
   crate's dev-dep style (the workspace root already pins tempfile).
2. `types.rs` `MediaPath` struct doc: replace the misleading
   `fs::canonicalize` mention with the accurate
   `dunce::canonicalize` (delegates to std on non-Windows; strips
   `\\?\` on Windows). Also hedge the "MediaPath is a machine-
   derived type" line to "rare in production; this type is
   typically machine-derived from canonicalized walk output" —
   test-fixture call sites construct from string literals.
3. `types.rs` `DiscoveredFile.relative_path` doc: drop "post-L5"
   historical-event jargon in favor of an evergreen reference to
   the proptest file's mechanism doc.
4. `props_path_nfc_equivalence.rs` module doc: soften the APFS
   mechanism wording to "macOS VFS layer is normalization-
   insensitive" (the "hash of canonically-normalized filename"
   claim over-attributes to APFS itself vs. the VFS).
5. `props_path_nfc_equivalence.rs` module doc: explicitly flag
   that the off-macOS branch is a trivial byte-identity test kept
   to keep the file non-empty; real coverage lives in the macOS
   path.

No behavior change; all changes are doc comments + Cargo.toml
style.
L5 step 3/5 — path consolidation (lib-audit follow-up spec §2 L5).

cli's 4 canonicalize call sites now route through
`perima_fs::platform_path::canonicalize` — the `pub` helper
introduced in 044e156. dunce stays as a direct dep only on
crates/fs, cfg-gated to Windows.

Single source of truth for the #[cfg(windows)] dunce/std fallback;
no per-crate copy maintained in cli.

Call sites swapped:
- cmd/scan.rs (canonicalize_for_walk)
- cmd/watch.rs (canonicalize)
- cmd/metadata.rs
- cmd/tag.rs

Also fix pre-existing clippy::ptr_arg in tag.rs (&PathBuf → &Path
on three private helpers) surfaced by -D warnings.
…_path

L5 step 4/5 — path consolidation (lib-audit follow-up spec §2 L5).

Mirrors commit dc20a0f (cli). desktop's canonicalize call sites in
commands.rs now route through `perima_fs::platform_path::canonicalize`
— the `pub` helper introduced in 044e156. dunce stays as a direct
dep only on crates/fs, cfg-gated to Windows.

Single source of truth for the #[cfg(windows)] dunce / std fallback;
no per-crate copy maintained in desktop.

Call sites swapped: 2 in commands.rs.
L5 step 5/5 — path consolidation (lib-audit follow-up spec §2 L5).

Closes the L5 paperwork:

- `Cargo.toml`'s workspace `unicode-normalization` entry now has
  a WHY comment pointing to GH #112 for the long-term
  tag-NFC strategy question.
- `crates/core/src/tag.rs` gains a WHY comment at the
  unicode-normalization import pointing to the same issue, plus a
  cross-reference to the macOS NFD migration issue for completeness.

AGENTS.md's L5 pin block was authored but NOT committed — the file
matches the `**/*.md` gitignore rule without an exception, per an
earlier user directive to keep AGENTS.md working-tree-only. Future
sessions on this machine still see it; cloud / fresh-clone agents
won't (accepted limitation from Batch A).

Two GH issues filed:
- #112      Re-evaluate tag NFC normalization strategy (post-v1)
- #113      macOS NFD-vs-NFC file_locations + FTS5 duplication
             on re-scan (phase-9 hardening)
L2 step 2/4 — frontend modernization bundle (lib-audit follow-up
spec §2 L2). PR-2 closeout.

- vite: ^6 → ^8 (resolved 8.0.9). Rolldown-default; eliminates the
  esbuild dev / Rollup prod bundler split.
- @vitejs/plugin-react: ^4 → ^6 (resolved 6.0.1). Retains Babel pass
  for React Compiler (wired in PR-3); React Refresh switched to Oxc.
- Node engines range bumped to Vite 8 minimum (>=20.19 <21 || >=22.12).
- vitest pin tightened from ^3 to ^3.2 (resolved 3.2.4, meets Vite 8
  floor per audit §Q37).
- CI Node pinned to 22.12 via a new actions/setup-node@v4 step
  positioned before oven-sh/setup-bun@v2 (runner previously used an
  unpinned default not guaranteed to meet Vite 8's floor).

Bundle size delta: 227,232 bytes (was 231,269 bytes, -1.7%, within ±5%).

@tailwindcss/vite peer range verified compatible with Vite 8
(^5.2.0 || ^6 || ^7 || ^8 — pre-flight gate). 68/68 tests, lint,
build all green post-bump.
Spec deviation fix for L2 PR-2. Spec §2 L2 line 154 mandates
"Bump `packageManager: "bun@1.3.11"` pin (audit §Q36)". The field
was absent from apps/desktop/package.json, so the prior commit
(57c3752) silently dropped the requirement as YAGNI. This adds
the field so Corepack / package-manager detection resolves to the
audit-pinned version.

`bun install` is a no-op (0 changes); lint still clean.
L2 step 3/4 — frontend modernization bundle (lib-audit follow-up
spec §2 L2). PR-3 closeout.

- babel-plugin-react-compiler added at EXACT pin =1.0.0 (not ^).
- @rolldown/plugin-babel@^0.2.3 added as peer; @babel/core@^7 and
  @babel/preset-typescript@^7 added as dev deps (needed by the canary
  test described below).
- vite.config.ts: plugin-react v6 removed Babel from the default
  pipeline (React Refresh now runs through Oxc). The Compiler re-
  adds Babel as a SEPARATE plugin via `@rolldown/plugin-babel`
  configured with `reactCompilerPreset({ target: "19" })` — the
  canonical v6 wiring per
  https://github.com/vitejs/vite-plugin-react/blob/plugin-react@6.0.1/packages/plugin-react/README.md
  Compiler runs in default (infer) mode — auto-memoizes every
  component that satisfies rules-of-react. `target: "19"` is
  pinned explicitly so a future minor can't silently flip the
  runtime emit path.
- Spec-mandated canary (src/__tests__/compiler-canary.test.ts)
  runs babel-plugin-react-compiler in annotation mode over a
  known-good `"use memo"` snippet (directive inside function body,
  not at program level — Compiler treats it as a function-level
  directive) and asserts the transformed output imports from
  `react/compiler-runtime` (React 19's built-in runtime path).
  This is stable against minifier renames and decoupled from Vite
  build state — proves the plugin chain itself works.

Bundle size delta: 227,232 → 234,711 bytes raw (+7,479 bytes;
well under the 20 KB flag threshold). The delta is the Compiler's
react/compiler-runtime cache machinery.

## What this enables

Manual useCallback / useMemo are now an anti-pattern for new
components; the Compiler handles memoization. Existing manual
memos (currently 1: App.tsx useCallback) stay for now — a follow-up
GH issue will audit and remove them once Compiler adoption is
proven in practice.

## What this doesn't change

No source-code changes to src/**. The Compiler is transparent at
source level; only the transform pipeline added the extra Babel
plugin.
L2 step 3/4 follow-up. Applies reviewer nits (CHANGES_REQUESTED from
Opus code-quality reviewer #2) to commit ceb508a.

- Add @types/babel__core: fixes TS7016 error that broke `tsc -b`
  (and thus `bun run build`) in ceb508a. Canary test was green
  under vitest (no typecheck) but production build failed.
- Add @babel/plugin-syntax-jsx as explicit devDep: it was being
  resolved transitively through @babel/preset-typescript, a
  phantom-dep anti-pattern that would break on a future hoist
  change. Now declared directly.
- Add src/vendor.d.ts with ambient module declaration for
  @babel/plugin-syntax-jsx: no @types/babel__plugin-syntax-jsx
  exists on DefinitelyTyped; ambient decl typed as `object` to
  match @types/babel__core's PluginTarget union.
- Remove file-scoped `eslint-disable no-unsafe-*` block from the
  canary: no longer needed with @types/babel__core present.

Also applies code-quality nits:
- Reinstate the `// WHY:` comment on vitest's `dist-types` exclude
  in vite.config.ts (dropped during ceb508a's config rewrite).
- Add a `// WHY: keep in sync with vite.config.ts` comment on the
  canary's `target: "19"` to prevent silent drift if the prod
  target is bumped without updating the canary.
- Add a `// WHY:` comment on reactCompilerPreset's implicit
  `panicThreshold: "none"` default.

Verified: `bun run build` (exit 0), `bun test` (58/58 src pass,
canary included), `bun run lint` (clean).
L2 step 4/4 — frontend modernization bundle (lib-audit follow-up
spec §2 L2). PR-4 closeout.

eslint.config.js previously carried an unfulfilled "Follow-up: file
issue for each warn below" comment. Filed GH issue #116 as an
umbrella for the 13 warn-downgraded v7 rules; link from the comment.

Nothing else in L2 required code changes: Tailwind v4 + ESLint 9 +
react-hooks v7.1.1 + `recommended-latest` preset were already in
place prior to this batch. Vite 8 bump landed in commit 57c3752;
React Compiler 1.0 landed in commit ceb508a.

AGENTS.md's 5 L2 pin entries were authored in the working tree but
not committed (file is gitignored by `**/*.md` rule; per user
directive AGENTS.md stays working-tree-only). Cloud / fresh-clone
agents won't see the pins (accepted limitation from Batch A).

`eslint-plugin-react-compiler` is not installed (verified empty in
package.json + bun.lock) — superseded by react-hooks v7.1 which
ships Compiler-aware rules.

Current fire count for the 13 warn-downgraded rules: 0 (P2/cosmetic).
…e-minor

L6 — lib-audit follow-up spec §2 L6 line 289 calls for tightening
plugin pins. Previously the three workspace-level pins were loose
(`"2"` = `^2.0.0` = any 2.x), which allowed `cargo update` to bump
Tauri 2.10.3 → 2.11 or plugin-dialog 2.7.0 → 2.8.0 between sessions
with no review.

New pins (tilde-minor = `>=major.minor, <major.(minor+1)`):

- tauri             : ~2.10  (lockfile 2.10.3)
- tauri-build       : ~2.5   (lockfile 2.5.6 — its own minor track,
                              NOT 2.10.x; tauri-build has its own
                              release cadence)
- tauri-plugin-dialog: ~2.7  (lockfile 2.7.0 — `plugins-workspace`
                              plugins have independent minor cadence
                              per plugin, not lockstep with core)

Intent: gate minor bumps behind an explicit bump PR. Tauri 2.11 is
imminent (2.10.3 cut 2026-03-04); this pin deliberately BLOCKS its
silent adoption so a future session has to consciously re-test
against it. Not transitional — load-bearing.

No runtime change; Cargo.lock resolved the same versions pre- and
post-pin.

Note: `cargo build -p perima-desktop` and `cargo clippy --workspace`
including perima-desktop require GTK3/GLib/WebKit2GTK system libs
absent in this VM. Resolver-level validation (`cargo generate-lockfile`,
`cargo tree`, and non-desktop clippy + tests) all passed. CI installs
these libs and will verify full build.
CI macOS runner (Rust 1.95 clippy) flagged `nfc_name` / `nfd_name` and
`canon_nfc` / `canon_nfd` bindings in the `#[cfg(target_os = "macos")]`
test body as too-similar. The names are domain terms of art —
Unicode NFC (precomposed) and NFD (decomposed) forms — so renaming
to `precomposed` / `decomposed` loses the terminology. Scoped allow
on the test function only; Ubuntu CI does not compile this branch
and was clean pre-fix.
@utof
utof merged commit cf465f2 into main Apr 21, 2026
3 checks passed
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.

Library/stack audit follow-ups — umbrella

1 participant