Skip to content

fix(mft): restore OFFLINE path sorting (5/6 SORTED MATCH)#5

Merged
githubrobbi merged 1 commit into
mainfrom
fix-f-drive-parity
Mar 16, 2026
Merged

fix(mft): restore OFFLINE path sorting (5/6 SORTED MATCH)#5
githubrobbi merged 1 commit into
mainfrom
fix-f-drive-parity

Conversation

@githubrobbi

Copy link
Copy Markdown
Collaborator

Summary

Restores OFFLINE MFT processing to 5/6 SORTED MATCH state after regression from commit 90ee522.

What was broken

  • Commit 90ee522 removed ALL sort_directory_children() calls for C++ parity (Drive S fix)
  • This broke OFFLINE path — all 6 drives went from 5/6 SORTED MATCH to 0/6 output

Fix

  1. Re-added sort_directory_children() to OFFLINE paths:

    • persistence.rs (loading saved index)
    • merge.rs (post-processing)
    • builder.rs (already fixed by previous agent)
  2. Fixed verify_parity.rs to use --format custom for C++ footer parity

Result

  • ✅ Drive D: SORTED MATCH (7,065,330 lines)
  • ✅ Drive E: SORTED MATCH (2,929,497 lines)
  • ❌ Drive F: MISMATCH (Bug 3 - extension records, deferred)
  • ✅ Drive G: SORTED MATCH (15,071 lines)
  • ✅ Drive M: SORTED MATCH (1,908,783 lines)
  • ✅ Drive S: SORTED MATCH (8,278,080 lines)

5/6 SORTED MATCH restored ✅

🤖 Generated with Claude Code

… pass)

Commit 90ee522 removed all sort_directory_children() calls to match C++
LIVE behavior, but this broke OFFLINE mode (0/6 drives passing). OFFLINE
paths need sorting for deterministic output since MFT dump parsing order
differs from live IOCP traversal.

Changes:
- Restore sort_directory_children() before compute_tree_metrics() in all
  3 OFFLINE code paths: builder.rs, persistence.rs, merge.rs
- Add --format custom to verify_parity.rs to match C++ baseline footer

Verified: D, E, G, M, S all SORTED MATCH. F is known deferred (Bug 3).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@githubrobbi githubrobbi merged commit dce9abd into main Mar 16, 2026
5 of 6 checks passed
@githubrobbi githubrobbi deleted the fix-f-drive-parity branch March 16, 2026 16:52
githubrobbi added a commit that referenced this pull request Apr 12, 2026
fix(mft): restore OFFLINE path sorting (5/6 SORTED MATCH)
githubrobbi added a commit that referenced this pull request Apr 24, 2026
#6

Preview run 24873800282 (SHA 1404df5) completed end-to-end after bugs #3 and #4 were fixed.  Items #2 and #3 now have sufficient evidence to tick:

## Item #2: Same-SHA integrity

The `manifest` job is `needs:`-coupled to `smoke-windows`, which failed on two product-code test bugs surfaced for the first time in CI (see bugs #5 and #6 below).  `manifest.json` therefore did not emit.  Validated integrity directly from the uploaded artifacts instead:

- PR head SHA: `1404df5509dde8f462275e794dfdf3197b29897e`

- `nextest-archive-1404df5509...` (73.9 MB, 1 file)

- `windows-preview-1404df5509...` (17 release binaries)

- `sha256sum` computed locally on 2026-04-24 for all 18 files

This is mechanically what the `manifest` job would emit as JSON.  Full `manifest.json` emission deferred to the Phase 5 wrap-up PR after #53/#54 land.

## Item #3: Nextest archive round-trip

`smoke-windows` on `windows-latest` downloaded the nextest archive, unpacked 21 test binaries, and executed 1322 tests against the pinned SHA.  Result: 1320 passed, 2 failed.  The round-trip MECHANISM is proven; the two test failures are orthogonal product-code bugs:

## Bugs #5 and #6 (surfaced by preview lane; out of scope)

### Bug #5: `uffs-security::pipe::tests::fnv1a_known_vector`

  left:  9625390261332436968

  right: 9620965969329804336

FNV-1a is endianness-agnostic byte-wise, so the mismatch points to a test-vector bug (expected hash computed on Linux/macOS byte layout) or a platform-dependent input encoding (UTF-16 on Windows vs UTF-8 elsewhere, or `usize` vs `u64` width).

### Bug #6: `uffs-mft::io::readers::pipelined::tests::test_pipelined_reader_creation`

  left:  2097152  (2 MiB, Linux default hugepage)

  right: 65536    (64 KiB, Windows `dwAllocationGranularity`)

Ratio = 32x.  The test asserts an allocation-rounded buffer size computed from a platform-dependent default.

## Why these are separate PRs

  - Different crates, different owners (`uffs-security`, `uffs-mft`).

  - Single-responsibility: PR #52 is the preview-lane PR.  Mixing in product-code fixes would couple rollback and force reviewers to context-switch.

  - These tests had never run on Windows in CI before (pr-fast.yml `Windows compile check` is compile-only; `Tests` runs on ubuntu).  The preview lane earned its keep by exposing them.

## Status updates

- `\u00a710.3 Phase 5`: items #2 and #3 ticked with evidence + caveat.

- `\u00a710.5 Deviations`: added rows for bug #5 and bug #6 with investigation hints for the crate owners.

- `\u00a710.6 Open blockers`: added Phase 5 wrap-up PR + PR #53/#54 tracking as deferred non-blockers.
githubrobbi added a commit that referenced this pull request Apr 24, 2026
…H fix (#52)

Closes out Phase 5 preview-lane validation.  See PR body for full bug sequence (#1 nextest multi-line, #2 polling budget, #3 build-test-archive on windows-latest, #4 winresource RC_PATH).  Items #2 and #3 validated; bugs #5 and #6 filed as issues #53 / #54 for separate crate-owner PRs.
githubrobbi added a commit that referenced this pull request Apr 24, 2026
…cked

PR #55 preview run 24889490616 produced the first fully green end-to-end preview bake ever: all 6 jobs succeeded including `manifest` emission.  This commit upgrades §10.3 and §10.6 with the real evidence that supersedes the out-of-band sha256 placeholders from PR #52.

## §10.3 updates

- **Item #2** (Same-SHA integrity): ticked with real manifest.json contents (git_sha, tested_sha, cargo_lock_sha256, rustc_version, nextest_version, target, build_os, files[]).  All 17 files[].sha256 values verified against locally downloaded artifacts on 2026-04-24.

- **Item #3** (Nextest archive round-trip): upgraded from 1320/1322 partial to 1322/1322 full pass after bugs #5 and #6 fixed.

- **Item #5** (Fork-PR behaviour): upgraded from static-grep-only to a full security-model analysis covering runners, secrets, label-trigger, and concurrency.  Live fork-PR bake deferred to first natural external contribution (with explicit TODO list for what to verify).

## §10.6 updates

Moved to Resolved:

- Phase 5 wrap-up + full green end-to-end bake (2026-04-24)

- Issue #53 (uffs-security fnv1a) — closed by this PR

- Issue #54 (uffs-mft pipelined buffer) — closed by this PR

Remaining Active: Polars-ops xwin-debug DX blocker, Real-world bake gaps, Phase 4b release.yml permissions refactor — all unrelated to Phase 5.
githubrobbi added a commit that referenced this pull request Apr 24, 2026
…#55)

Closes #53, #54.

Two hardcoded test vectors had drifted from reality and never executed in CI because both containing modules are `#[cfg(windows)]`-gated and `pr-fast.yml` Tests runs on ubuntu / Windows compile check is compile-only.  Preview lane (run 24873800282 on PR #52) surfaced them on first execution; this PR fixes both and re-bakes the preview lane to full green (run 24889490616: 1322/1322 tests pass, manifest.json emitted with real sha256 integrity data for all 17 artifacts).

Also ticks the remaining §10.3 Phase 5 items (#2, #3, #5) with real evidence and marks §10.6 Phase 5 work as fully Resolved.
githubrobbi added a commit that referenced this pull request May 14, 2026
…es + dead-code removal (refs #190)

Phase 3a (of 3a + 3b split) of the code-clean roadmap: curate each
crate's lib.rs to a minimal, intentional public surface — addressing
playbook Refactor steps #1 (default to private modules), #2 (curated
lib.rs storefront), #3 (shrink visibility), and #5 (deliberate
re-exports), plus plan-doc audit dimensions §3.1, §3.2, §3.8, §3.9,
§3.10, §3.11.

Phase 3b will follow as a separate PR covering the remaining audit
dimensions:

  §3.4 — public struct field discipline (playbook Refactor #4)
  §3.5 — #[must_use] opportunity scan (playbook Refactor #7)
  §3.6 — #[non_exhaustive] decisions (playbook Refactor #6)
  §3.7 — sealed trait pattern decisions (playbook Refactor #8)

#190 stays open until Phase 3b lands.

Builds on Phase 2.5 (#220 — which demoted 791 individual `pub` items
workspace-wide) by tightening the **`pub mod` declarations themselves**
to `pub(crate) mod` where the module has zero external module-path
consumers, and deleting dead modules outright where evidence supports
it.

Five audit-target crates curated (94 → 70 pub@root items, −24, −25.5%):

  Crate            baseline    post    delta    action
  ---------------- --------- ------- -------- ----------------------------------
  uffs-format          13       5     −8     All 8 pub mod → pub(crate) mod
  uffs-mcp             14       7     −7     8 of 10 pub mod demoted; handler /
                                             http / text stay pub (tests + bin)
  uffs-client          12      10     −2     types + verify → pub(crate) mod
  uffs-mft             26      23     −3     error + flags + ntfs → pub(crate)
                                             mod (surface preserved via lib.rs
                                             flat re-exports)
  uffs-core            29      25     −4     compact_mmap + glob → pub(crate)
                                             mod; deleted format.rs + compact_
                                             reader.rs files (zero callers)

== Dead code deleted (~535 LOC) ==

- crates/uffs-core/src/format.rs (203 LOC) — pure dead duplicate of
  uffs_client::format::*; zero callers anywhere in the workspace.
- crates/uffs-core/src/compact_reader.rs (261 LOC) — zero callers for
  ExtraRecordFields, read_record_fields_lazy, etc.
- uffs-mft::ntfs::records::INDX_RECORD_MAGIC (only consumer was
  is_index_record, also unused).
- uffs-mft::ntfs::records::MultiSectorHeader::is_index_record method.
- uffs-mft::ntfs::records::FileRecordFlags enum (only test consumer;
  replaced with inline 0x0001 + NTFS-spec doc comment).
- uffs-mft::ntfs::metadata::FileNamespace enum (zero callers; namespace
  u8 documented inline on FileNameAttribute.file_name_namespace).
- uffs-mft::ntfs::file_reference_to_sequence fn (only test consumer;
  replaced with inline (file_ref >> 48) as u16 + doc).
- uffs-mft::flags::raw_flags module + its test (only consumer was the
  test itself; replaced with stronger bits_round_trip_matches_ntfs_layout
  test that exercises all 9 FileFlags::*.bits() values against NTFS spec).

== Surgical fixes encountered ==

- E0446 in uffs-mft: demoting MultiSectorHeader to pub(crate) broke the
  pub field FileRecordSegmentHeader.multi_sector_header (pub type
  required by pub field).  Resolution: re-promote MultiSectorHeader and
  add to lib.rs pub use ntfs::{…} list.  Preserves API consistency.

- SECTOR_SIZE_U64 (uffs-mft): all 15 consumers live in #[cfg(windows)]
  reader modules; gated the const + its pub(crate) use re-export with
  #[cfg(windows)] so non-Windows dead-code analysis stays honest.

- clippy::too_long_first_doc_paragraph fired on a multi-line ///
  comment in uffs-mcp::lib.rs `mod handler` — fixed by inserting the
  blank-doc separator.

- uffs_format::footer doc-link in uffs-cli/src/commands/output/parity.rs
  — updated to reference the public re-export
  uffs_format::write_legacy_drive_footer (the module became pub(crate),
  the doc-link was the lone textual reference and not an API contract).

== Phase 2 carry-overs resolved ==

- Q1 (#216 — uffs_mft::index::types::len_to_u16 placement): Phase 2
  assumed pub mod types, but reality (verified during Phase 3a audit) is
  that `mod types;` is already private in index.rs:42 with external
  reachability via `pub use self::types::{… len_to_u16, …}`.  Decision
  Option A from Phase 3 plan §3.10 was already in effect; #216 ready
  to close as "resolved by current code layout (no action needed)".

- Q4 (uffs-core pub mod discoverability): 13 of 18 remaining pub mods
  have legitimate external module-path consumers; 2 demoted
  (compact_mmap, glob); 1 stays pub pending a focused follow-up
  (see uffs-core::index_search below).

== Deferred to focused follow-ups ==

- uffs-core::index_search dead-code cleanup — demoting `pub mod
  index_search` exposed 91 pre-existing dead-code warnings.  Phase 3a
  keeps the module pub to avoid scope-creep; deletion of the dead items
  is recommended as a separate PR (logged in plan §11.6).

- Phase 3b — 4 remaining audit dimensions per §3.4, §3.5, §3.6, §3.7
  (logged in plan §11.0).

== Test improvements (rules 3 + 4) ==

- uffs-mft::ntfs::tests::file_reference_extraction — inlined the
  high-16-bit sequence-number extraction (the deleted production
  helper is no longer in the API; the bit-layout contract is still
  asserted with an explicit comment naming the NTFS spec).

- uffs-mft::flags::tests::bits_round_trip_matches_ntfs_layout
  (replaces `raw_flags` test) — verifies ALL 9 FileFlags::*.bits()
  values match the NTFS spec (READONLY=0x0001, HIDDEN=0x0002, SYSTEM=
  0x0004, DIRECTORY=0x0010, ARCHIVE=0x0020, SPARSE=0x0200, REPARSE=
  0x0400, COMPRESSED=0x0800, ENCRYPTED=0x4000).  Broader coverage
  than the previous 2-flag test.

== Workspace policy compliance ==

1. No suppression hacks — zero new #[allow] / lint-disable / cfg-gate
   workarounds.  All cfg(windows) gates added in this PR are correct
   per the items' actual platform constraints (verified by surveying
   consumers).
2. Surgical correct fixes — every demotion and deletion is justified
   by workspace grep showing zero callers.  No speculative changes.
3. Preserve behavior & contracts — public API surface tightened only
   where verifiably unused.  When E0446 prevented a demotion, the
   public type was preserved (MultiSectorHeader).
4. Improve tests, don't dodge them — 2 tests strengthened with broader
   coverage; 0 tests skipped, relaxed, or deleted to dodge a failure.

== Verification ==

- cargo check -p uffs-format --all-targets   ✓ 0 warnings
- cargo check -p uffs-mcp    --all-targets   ✓ 0 warnings
- cargo check -p uffs-client --all-targets   ✓ 0 warnings
- cargo check -p uffs-mft    --all-targets   ✓ 0 warnings
- cargo check -p uffs-core   --all-targets   ✓ 0 warnings
- just lint-prod                              ✓ green
- cargo nextest run --workspace               ✓ 1,710 / 1,710
- Local pre-push hook (9 gates incl. rustdoc + doc-tests + tests +
  smoke + lint-ci-windows)                   ✓ 105s
githubrobbi added a commit that referenced this pull request May 14, 2026
…es + dead-code removal (refs #190) (#221)

Phase 3a (of 3a + 3b split) of the code-clean roadmap: curate each
crate's lib.rs to a minimal, intentional public surface — addressing
playbook Refactor steps #1 (default to private modules), #2 (curated
lib.rs storefront), #3 (shrink visibility), and #5 (deliberate
re-exports), plus plan-doc audit dimensions §3.1, §3.2, §3.8, §3.9,
§3.10, §3.11.

Phase 3b will follow as a separate PR covering the remaining audit
dimensions:

  §3.4 — public struct field discipline (playbook Refactor #4)
  §3.5 — #[must_use] opportunity scan (playbook Refactor #7)
  §3.6 — #[non_exhaustive] decisions (playbook Refactor #6)
  §3.7 — sealed trait pattern decisions (playbook Refactor #8)

#190 stays open until Phase 3b lands.

Builds on Phase 2.5 (#220 — which demoted 791 individual `pub` items
workspace-wide) by tightening the **`pub mod` declarations themselves**
to `pub(crate) mod` where the module has zero external module-path
consumers, and deleting dead modules outright where evidence supports
it.

Five audit-target crates curated (94 → 70 pub@root items, −24, −25.5%):

  Crate            baseline    post    delta    action
  ---------------- --------- ------- -------- ----------------------------------
  uffs-format          13       5     −8     All 8 pub mod → pub(crate) mod
  uffs-mcp             14       7     −7     8 of 10 pub mod demoted; handler /
                                             http / text stay pub (tests + bin)
  uffs-client          12      10     −2     types + verify → pub(crate) mod
  uffs-mft             26      23     −3     error + flags + ntfs → pub(crate)
                                             mod (surface preserved via lib.rs
                                             flat re-exports)
  uffs-core            29      25     −4     compact_mmap + glob → pub(crate)
                                             mod; deleted format.rs + compact_
                                             reader.rs files (zero callers)

== Dead code deleted (~535 LOC) ==

- crates/uffs-core/src/format.rs (203 LOC) — pure dead duplicate of
  uffs_client::format::*; zero callers anywhere in the workspace.
- crates/uffs-core/src/compact_reader.rs (261 LOC) — zero callers for
  ExtraRecordFields, read_record_fields_lazy, etc.
- uffs-mft::ntfs::records::INDX_RECORD_MAGIC (only consumer was
  is_index_record, also unused).
- uffs-mft::ntfs::records::MultiSectorHeader::is_index_record method.
- uffs-mft::ntfs::records::FileRecordFlags enum (only test consumer;
  replaced with inline 0x0001 + NTFS-spec doc comment).
- uffs-mft::ntfs::metadata::FileNamespace enum (zero callers; namespace
  u8 documented inline on FileNameAttribute.file_name_namespace).
- uffs-mft::ntfs::file_reference_to_sequence fn (only test consumer;
  replaced with inline (file_ref >> 48) as u16 + doc).
- uffs-mft::flags::raw_flags module + its test (only consumer was the
  test itself; replaced with stronger bits_round_trip_matches_ntfs_layout
  test that exercises all 9 FileFlags::*.bits() values against NTFS spec).

== Surgical fixes encountered ==

- E0446 in uffs-mft: demoting MultiSectorHeader to pub(crate) broke the
  pub field FileRecordSegmentHeader.multi_sector_header (pub type
  required by pub field).  Resolution: re-promote MultiSectorHeader and
  add to lib.rs pub use ntfs::{…} list.  Preserves API consistency.

- SECTOR_SIZE_U64 (uffs-mft): all 15 consumers live in #[cfg(windows)]
  reader modules; gated the const + its pub(crate) use re-export with
  #[cfg(windows)] so non-Windows dead-code analysis stays honest.

- clippy::too_long_first_doc_paragraph fired on a multi-line ///
  comment in uffs-mcp::lib.rs `mod handler` — fixed by inserting the
  blank-doc separator.

- uffs_format::footer doc-link in uffs-cli/src/commands/output/parity.rs
  — updated to reference the public re-export
  uffs_format::write_legacy_drive_footer (the module became pub(crate),
  the doc-link was the lone textual reference and not an API contract).

== Phase 2 carry-overs resolved ==

- Q1 (#216 — uffs_mft::index::types::len_to_u16 placement): Phase 2
  assumed pub mod types, but reality (verified during Phase 3a audit) is
  that `mod types;` is already private in index.rs:42 with external
  reachability via `pub use self::types::{… len_to_u16, …}`.  Decision
  Option A from Phase 3 plan §3.10 was already in effect; #216 ready
  to close as "resolved by current code layout (no action needed)".

- Q4 (uffs-core pub mod discoverability): 13 of 18 remaining pub mods
  have legitimate external module-path consumers; 2 demoted
  (compact_mmap, glob); 1 stays pub pending a focused follow-up
  (see uffs-core::index_search below).

== Deferred to focused follow-ups ==

- uffs-core::index_search dead-code cleanup — demoting `pub mod
  index_search` exposed 91 pre-existing dead-code warnings.  Phase 3a
  keeps the module pub to avoid scope-creep; deletion of the dead items
  is recommended as a separate PR (logged in plan §11.6).

- Phase 3b — 4 remaining audit dimensions per §3.4, §3.5, §3.6, §3.7
  (logged in plan §11.0).

== Test improvements (rules 3 + 4) ==

- uffs-mft::ntfs::tests::file_reference_extraction — inlined the
  high-16-bit sequence-number extraction (the deleted production
  helper is no longer in the API; the bit-layout contract is still
  asserted with an explicit comment naming the NTFS spec).

- uffs-mft::flags::tests::bits_round_trip_matches_ntfs_layout
  (replaces `raw_flags` test) — verifies ALL 9 FileFlags::*.bits()
  values match the NTFS spec (READONLY=0x0001, HIDDEN=0x0002, SYSTEM=
  0x0004, DIRECTORY=0x0010, ARCHIVE=0x0020, SPARSE=0x0200, REPARSE=
  0x0400, COMPRESSED=0x0800, ENCRYPTED=0x4000).  Broader coverage
  than the previous 2-flag test.

== Workspace policy compliance ==

1. No suppression hacks — zero new #[allow] / lint-disable / cfg-gate
   workarounds.  All cfg(windows) gates added in this PR are correct
   per the items' actual platform constraints (verified by surveying
   consumers).
2. Surgical correct fixes — every demotion and deletion is justified
   by workspace grep showing zero callers.  No speculative changes.
3. Preserve behavior & contracts — public API surface tightened only
   where verifiably unused.  When E0446 prevented a demotion, the
   public type was preserved (MultiSectorHeader).
4. Improve tests, don't dodge them — 2 tests strengthened with broader
   coverage; 0 tests skipped, relaxed, or deleted to dodge a failure.

== Verification ==

- cargo check -p uffs-format --all-targets   ✓ 0 warnings
- cargo check -p uffs-mcp    --all-targets   ✓ 0 warnings
- cargo check -p uffs-client --all-targets   ✓ 0 warnings
- cargo check -p uffs-mft    --all-targets   ✓ 0 warnings
- cargo check -p uffs-core   --all-targets   ✓ 0 warnings
- just lint-prod                              ✓ green
- cargo nextest run --workspace               ✓ 1,710 / 1,710
- Local pre-push hook (9 gates incl. rustdoc + doc-tests + tests +
  smoke + lint-ci-windows)                   ✓ 105s
githubrobbi added a commit that referenced this pull request Jun 14, 2026
$UpCase's live read used SetFilePointerEx, which fails on the broker's
FILE_FLAG_OVERLAPPED handle (no synchronous file pointer) — the
"$UpCase: seek to offset ... The parameter is incorrect. (0x80070057)" warning
on every broker-backed load, after which it fell back to the compiled-in default
table (correct for standard volumes, but noisy and wrong for a customised
$UpCase).

Extract FU-3's overlapped read into a shared pub(crate) read_handle_at(handle,
offset, buf) and route both the $UpCase FRS-10 read and its cluster reads
through it (replacing upcase's SetFilePointerEx volume_read_at, now deleted).
The MFT-extent bootstrap (FU-3) uses the same function. The overlapped-offset
ReadFile works on both the broker's overlapped handle and a plain synchronous
(elevated) handle, so one path covers both.

Removes the last broker-flow warning; a live $UpCase read now succeeds
non-elevated. Bumps the TEMP-BROKER-FLOW build tag to #5. Exact xwin clippy
--workspace --all-features clean; 222 host mft tests pass. Also flips FU-3 to
done (#409, VM-confirmed: num_extents=2, records=403209).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
githubrobbi added a commit that referenced this pull request Jun 30, 2026
…lish

#1: from a non-elevated terminal the broker (its LocalSystem service + process)
cannot be stopped/removed, but the old gate refused the WHOLE uninstall. Mark the
broker process as admin-only too, and instead of bailing, offer to skip the
admin-only items and remove everything else now (or abort to re-run elevated).
Adds RemovalPlan::drop_elevation_required.

#2: show 'legacy' instead of '-' for versionless (old) binaries.
#5: blank line between the 'found elsewhere' heading and the file list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deep-soft pushed a commit to deep-soft/UltraFastFileSearch-Rust that referenced this pull request Jul 3, 2026
…ast, broker leave-cleanly (skyllc-ai#502)

* fix(cli): strip Windows \\?\ verbatim prefix in uninstall path resolution

On Windows, std::fs::canonicalize returns the \\?\ extended-length form, which
never matches a bare C:\... PATH entry. The live v0.6.18 run showed every binary
(including the active uffs) mislabeled 'off-path' and the install dir displayed
as \\?\C:\Users\rnio\bin, and it means the PATH-safety gate can't recognize a
PATH entry either.

Add strip_verbatim_prefix and apply it at the canonicalize sites (detect's
upsert_root, the uninstall PATH scan) and to the current-exe dir in search_dirs,
so stored dirs match plain PATH entries and display cleanly. No-op off Windows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall deep sweep decodes every search payload (was finding zero strays)

The live v0.6.18 run surfaced no strays despite dozens of stray uffs.exe copies
on disk. Root cause: the daemon delivers search results in four shapes — inline
rows, a memory-mapped rows file, an inline pre-formatted blob, or a memory-mapped
blob (chosen by size + output shape). A real multi-hit Windows sweep returns a
CSV/path *blob*, but the old code walked the JSON for `"path"` object keys,
which only exist in the inline-rows case — so every blob/shmem result was
silently dropped.

Switch to the typed `search_cli` + `--columns path` (single-column output) and
decode all payload variants via the client's shmem/blob helpers
(read_search_results / stream_paths_blob_into), parsing the path-per-line blob
(header + CSV quotes stripped). Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall drive-coverage prompt warns that indexing builds a cache

The live v0.6.18 dry-runs grew a ~4 GB index cache because each 'y' to the
coverage offer indexed more drives. That is by design (indexing is
non-destructive and the deep sweep needs it), but the prompt gave no hint that
saying yes builds an on-disk cache that persists even under --dry-run. Spell it
out so the choice is informed. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall drive-coverage polls status_drives for readiness, not a blind wait

A freshly load_drive_letters-requested shard starts parked/cold and only becomes
searchable once its body is resident. The previous fixed await_ready(120s) could
return while shards were still loading, so the deep sweep searched a not-yet-ready
index and found nothing.

Poll status_drives until every requested drive reports a loaded tier (hot/warm)
or the deadline elapses — so the sweep waits exactly as long as needed and never
searches a still-parked shard. Best-effort: RPC errors keep polling to the
deadline, then proceed with whatever is loaded. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): stamp the git commit into `uffs --version` to verify the running build

The live Windows test ran an OLD uffs.exe: the shell prompt said the
fix/uninstall-windows-followups branch was checked out, but the output still
showed the pre-fix behaviour (off-path, \\?\ paths) — a stale binary never
rebuilt/redeployed. The CLI's --version printed only the crate version, so there
was no way to tell which build was running.

The daemon already stamps UFFS_GIT_SHA into its startup log to close exactly
this 'ran the wrong/stale binary' trap; port the same build.rs stamp to the CLI
and surface it: `uffs --version` now prints 'uffs <ver> (<sha>[-dirty])'. Match
the sha against `git rev-parse --short HEAD` to confirm the build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall deep sweep keeps only real family files, not substring noise

The live Windows sweep worked but listed non-binaries as removable strays: the
daemon search matches `uffs.exe` as a *contains* query, so it also returned
prefetch traces (UFFS.EXE-1234.pf), localized resources (uffs.exe.mui),
checksums (uffs.exe.sha256), build recipes (uffs.exe.recipe), and NTFS
alternate-data-stream entries (uffs.exe:com.dropbox.attrs).

Add is_family_artifact: keep a hit only when its file name is exactly a family
executable (uffs.exe / uffsd.exe / uffs-broker.exe / uffs-tui*.exe / …) or a
cache file (*_compact.uffs / *_usn.cursor); drop anything ending in
.pf/.mui/.sha256/.recipe or containing ':' (an ADS entry). Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(client): exe-resolution fallbacks carry .exe on Windows (no bare uffs name)

Audit of the `uffs` -> `uffs.com` concern: every actual spawn site already uses
a full current-exe-relative path, and Rust's Command appends .exe (never .com)
on Windows — which is why the uninstall run completed correctly. The only bare
names were the $PATH fallbacks in find_uffs_exe / find_daemon_exe, hit only when
current_exe + sibling lookup both fail.

Harden those to the platform binary name (uffs.exe / uffsd.exe on Windows) so a
bare `uffs` can never be resolved to a legacy uffs.com via PATHEXT (.COM precedes
.EXE) if the path is ever handed to a shell, a registry entry, or a logged
command rather than spawned directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall drops the legacy C++ uffs.exe GUI binary (PE subsystem check)

On a machine that still has the predecessor C++ UFFS installed, the deep sweep
listed its uffs.exe copies and — worse — version_strays ran each with --version,
launching a GUI window per copy (the slow, 'CPP version keeps popping up'
behaviour). The C++ product is a Windows GUI app; our Rust CLI is a console app.

Read the PE Optional-Header Subsystem field (headers only, never executing the
file): if a uffs.exe is a GUI-subsystem binary, drop it from the strays before
probing. Only uffs.exe collides with the predecessor; the other family names are
Rust-only and untouched. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): uninstall prints the running build's version + commit at the top

Add a 'uffs <ver> (<sha>) — uninstall' header to the dry-run and live output, so
any captured run is unambiguously tied to the exact binary (the same stamp
`uffs --version` shows). Makes a stale-binary run obvious at a glance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall drive-coverage waits long enough + shows index progress

The live 7-drive load took ~2.5 min but the readiness poll capped at 120 s, so it
returned at 6/7 drives and the sweep searched a not-yet-ready index — missing the
still-loading drive (D:). Raise the cap to 600 s and print 'indexing for the
sweep: N/M drives ready...' as drives come online, so the wait covers a real cold
multi-drive index and never looks like a hang. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall indexes drives with live progress + clearer prompt

#3/#4 from the Windows test. The load RPC blocks until the daemon finishes every
drive (loaded sequentially), which overran the client timeout on a 7-drive
system — so it returned at 6/7 (missed D:) and no progress ever showed.

Fire the load on a background connection and poll status_drives on this thread,
printing 'indexing for the sweep: N/M drives ready...' as drives come online —
the poll, not the RPC return, decides when the drives are searchable, so a
background timeout is harmless and the sweep no longer searches a partial index.
Also reword the coverage prompt to be less repetitive. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall broker is optional when non-elevated + display polish

#1: from a non-elevated terminal the broker (its LocalSystem service + process)
cannot be stopped/removed, but the old gate refused the WHOLE uninstall. Mark the
broker process as admin-only too, and instead of bailing, offer to skip the
admin-only items and remove everything else now (or abort to re-run elevated).
Adds RemovalPlan::drop_elevation_required.

#2: show 'legacy' instead of '-' for versionless (old) binaries.
skyllc-ai#5: blank line between the 'found elsewhere' heading and the file list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall always indexes drives for the deep sweep (no prompt)

Indexing every NTFS drive is a non-elevated, non-destructive read that the deep
sweep requires, so it should just happen — not be a [y/N] choice. Drop the
prompt: ensure_drive_coverage now always loads any not-yet-indexed drives (with
the live N/M progress) and no longer takes a confirm callback.

This also removes the elevated-vs-non-elevated output divergence: the runs only
differed because one had drives already loaded (no prompt) and the other didn't
(prompt). Now both just index what's missing. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): uninstall removes the full workspace binary set, not just the core

The live run left ~15 dev/diagnostic binaries in ~/bin untouched (analyze-diff,
dump-mft-records, gen-hooks, uffs-bench, uffs-ci-pipeline, …) — a from-source /
cargo install build drops them next to the core set. Add them to
EXTRA_BINARY_STEMS so the install-dir sweep removes them.

Refactor the deep sweep to derive its search patterns + family filter from the
shared family set (KNOWN_BINARIES + EXTRA_BINARY_STEMS) instead of a second
hardcoded list — so adding a binary in one place now updates both the
install-dir removal and the cross-drive sweep. None of these are managed by
--update, so KNOWN_BINARIES (the update set) is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall gathers decisions up front, runs once, defers the running binary

Three fixes from the live runs:

1. Decisions up front, no post-removal prompts: the broker keep/skip (elevation
   gate) and the deep-sweep strays opt-in are both decided before the single
   'Proceed with removal?' go, then everything executes once into one combined
   outcome — so the summary + retry hint print once, not per-phase.

2. Platform-correct failure hint: elevation only exists on Windows (the broker
   is a LocalSystem service); a non-Windows uninstall is all user-land, so it no
   longer suggests 'sudo' there — a failure is a file in use.

3. The chicken-and-egg self-delete: the OS locks a running image, so deleting
   uffs.exe / uffs-update.exe in place is the 'access denied' seen in the live
   run. SystemEffects now skips the running self-binaries (matched verbatim-
   stripped, case-insensitive) and the existing spawned-cmd schedule_self_delete
   removes them after exit — the same deferred-delete installers (NSIS/Inno) use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall does not taskkill the broker (it is a service; sc handles it)

In the elevated run, 'broker (pid 9064)' failed with exit 128: it was a
StopProcess item executed via taskkill /F, but the broker is a LocalSystem
service — taskkill can't stop it (and even forced, the SCM restarts it). The
RemoveService item already stops + deletes it the right way (uffs_winsvc::stop +
sc delete).

Filter the broker out of the Processes group so it is never taskkill'd. Only the
user-owned daemon / MCP remain there (no admin needed); the broker is handled
solely by RemoveService. Removes the guaranteed-failure line from the outcome.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall decides the broker/elevation up front, before the deep sweep

A non-elevated run should be told immediately that the broker needs Administrator
— not after sitting through a multi-minute drive index + sweep. Move the
elevation decision to right after the plan is shown, before platform_stray_plan:

- Not elevated + broker installed: flag it and offer to continue (uninstall
  everything except the broker) or abort to re-run from an elevated terminal.
- Elevated: skipped entirely — just remove everything (incl. the broker), and
  the running binary is self-deleted at the end as before.
- Dry-run: only previews (the plan already marks the broker 'needs Administrator').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(uninstall): parallelize + timeout the deep-sweep version probes; anchor the query

The deep sweep's post-query step ran `<bin> --version` sequentially with no
timeout, once per family hit. On a dev box (hundreds of `uffs*.exe` under
`target/`) that took minutes, and any binary that doesn't cleanly handle
`--version` (half-written artifact, something waiting on stdin) hung the whole
sweep indefinitely.

version_strays:
- probe in parallel via a bounded `std::thread::scope` worker pool
  (cursor-stealing; no new dependency)
- `probe_version_bounded`: stdin nulled, piped output, poll `try_wait` to a 2s
  deadline then `kill` — a hung binary goes unversioned instead of stalling

DaemonSearch::find:
- anchor each `stem.exe` query with `--name-only --ext exe` instead of a bare
  full-path substring. Measured 158 -> 46 hits for `uffs.exe`; identical to the
  exact-regex count, so it's lossless. Drops `.mui`/prefetch/ADS/path-substring
  noise at the daemon before rows cross the wire. Glob cache patterns untouched.

Plus temporary `[sweep]` diagnostics (per-pattern raw/kept counts, phase
timings, timeout count) routed through one `dbg_line` helper, to verify the
live Windows run. To be removed once signed off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(uninstall): redesign the discovered-binary table (one row/binary, header, plain labels)

The old output was two lines per binary (a `stem:` header + an indented row)
with cryptic columns: ACTIVE/shadowed/off-path, a raw channel word
(`unmanaged`), and a bare `-` scope. Redesign to a single aligned row per copy
with a header and a STATUS legend:

- ACTIVE -> `runs` (the copy a bare command executes, first on PATH);
  on-PATH-but-later -> `shadowed`; not-on-PATH -> `off PATH`.
- Fold the channel + scope into one SOURCE column: `hand-placed` (was
  `unmanaged`), `dev build`, `winget (user)`/`winget (machine)` — scope only
  means something for winget, so the lone `-` is gone.
- Columns are width-sized to header+cells; LOCATION is last / free-width.

Display-only; resolution logic in resolve_order.rs is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(uninstall): make deep-sweep drive coverage robust (kill+start, not a racing hot-load)

The old coverage fired its own load_drive_letters for any not-yet-loaded drive,
racing the daemon's background startup load over the single-instance Access
Broker pipe (ERROR_PIPE_BUSY). That churned the registry (observed 2/6 -> 0/6),
intermittently dropped a drive (6/7), and could spin to the wait cap.

Replace it with the proven CLI flow, only when needed:
- Managed set = every `status_drives` row (any tier — hot/warm/parked/cold; a
  search re-promotes a parked drive on demand).
- If the daemon already covers every system drive: do nothing.
- If ANY drive is missing: `uffs --daemon kill`, wait for full shutdown, then a
  clean `uffs --daemon start` (loads every drive with broker warm-up, returns
  only once Ready), then poll until coverage is complete.

No `restart`, no hot-load, no competing loader thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(uninstall): never start the daemon in-process for coverage; degrade gracefully

Spawning `uffs --daemon start` as a child of the uninstall intermittently hangs
the drive load (daemon stuck at 5/7, zero progress) even though a standalone
`uffs --daemon start` loads all drives in seconds. Rather than chase that
spawn-context bug, stop bringing the daemon up in-process:

- Fully covered already (warm daemon): proceed silently — the common case.
- Daemon up but mid-load: wait briefly (60s cap), then proceed with whatever
  loaded. Never blocks indefinitely.
- No daemon reachable: print a one-line notice telling the user to run
  `uffs --daemon start` and re-run for a complete sweep; continue best-effort.

No kill, no start, no restart, no competing loader — the deep sweep now covers
whatever the daemon already has and never hangs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(uninstall): reload daemon for coverage via the real CLI handlers (kill+start in-process)

Previous attempt shelled out to `uffs.exe --daemon start` as a subprocess, which
spawned the daemon as a grandchild and intermittently hung its drive load
(stuck at 5/7). A standalone `uffs --daemon start` loads all drives in seconds.

Reuse the exact handlers the CLI dispatches instead: call
`daemon_mgmt::daemon(&DaemonAction::Kill)` then `daemon(&Start{..})` in-process,
so the daemon is a DIRECT child of this process — identical topology to a shell
`uffs --daemon start`. When coverage is complete, no-op silently; when a drive
is missing, kill, wait for full shutdown, start (blocks until Ready = all drives
loaded), then proceed. Any handler error is best-effort: note it and sweep with
whatever is loaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build(windows): embed icon + version-info + manifest into the 4 bare binaries

Only uffs.exe and uffs-update.exe carried Windows PE resources; uffsd, uffsmcp,
uffs-broker, and uffs-mft shipped bare. A metadata-less unsigned binary is both
unbranded and a mild antivirus ML false-positive signal (all 7 tripped the same
generic Defender heuristic on the 0.6.18 release).

Add a per-crate build.rs to each (mirroring uffs-cli/uffs-update) that embeds
via winresource on MSVC-Windows only:
- the UFFS icon (shared assets/brand/icons/uffs.ico)
- version info: ProductName, FileDescription, CompanyName, LegalCopyright,
  OriginalFilename (winresource auto-fills File/ProductVersion from the crate)
- a new shared assets/brand/app.manifest (asInvoker, PerMonitorV2, longPathAware)

winresource added as a build-dependency of each crate. No-op off Windows; the
uffs-mft library target is unaffected. Validated with cargo xwin clippy for
x86_64-pc-windows-msvc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build(windows): embed UFFS icon + metadata into the 14 dev/CI binaries

Branding consistency across the whole family: the diagnostic + CI tools shipped
bare (no icon/version info). Add a per-crate build.rs embedding the shared UFFS
icon + version info + app.manifest via winresource (MSVC-Windows only, no-op
elsewhere) to the 6 crates that produce them:

- uffs-diag (9 bins: analyze-diff, analyze-mft-parents, compare-raw-mft,
  compare-scan-parity, cross-check-mft-reference, dump-mft-extents,
  dump-mft-records, inspect-mft-record-flow, scan-mft-magic)
- uffs-bench (uffs-bench)
- scripts/ci/gen-hooks, scripts/ci/gen-workflow, scripts/ci/manifest-audit
- scripts/ci-pipeline (uffs-ci-pipeline)

winresource added as a build-dependency of each. Validated with cargo xwin
clippy for x86_64-pc-windows-msvc. Completes the all-binaries icon-branding task.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(mft): bound the overlapped $UpCase/FRS read with a dedicated event (fixes fresh-load hang)

Root cause of the daemon's fresh (no-cache) load hanging at 5/6-of-7 drives:
after the main MFT read, each drive re-reads the $UpCase table via
read_handle_at -> read_handle_at_once, which issued an overlapped ReadFile on
the broker's FILE_FLAG_OVERLAPPED handle with a NULL-hEvent OVERLAPPED and then
GetOverlappedResult(bWait=true). With no event, GetOverlappedResult waits on the
file object itself; the broker vends duplicate handles to the same volume file
object, so under a concurrent multi-drive load it cannot tell which read
completed and blocks FOREVER (Microsoft's documented pitfall). Debug logs showed
1-2 drives stall right after "Adopted Access Broker volume handle", never
reaching "Parsed $UpCase data runs" — a silent, error-less hang. This affected
every fresh `uffs --daemon start`, not just the uninstall.

Fix: bind each read to a dedicated manual-reset event and wait on the event
(never the shared handle), bounded by IOCP_WAIT_COMPLETION_DEADLINE. On timeout,
CancelIoEx + drain, then return a retryable ERROR_OPERATION_ABORTED so the
existing read_handle_at retry loop reissues it. The event makes the wait
specific to this operation (the documented fix); the bound guarantees the load
can never hang forever again — a genuinely wedged read fails fast and the daemon
reaches Ready instead of stalling.

Validated with cargo xwin clippy (x86_64-pc-windows-msvc).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(uninstall): quieter UX — elevation asked first, one final summary, -v for detail

The interactive flow was noisy and the elevation choice confusing: the full
binary table, inventory, and plan (broker item included, "(needs
Administrator)") printed BEFORE a double-clause elevation question, and
answering y echoed "Leaving the broker installed" mid-flow.

Restructure to: decide -> gather -> summarize -> confirm.

- Elevation is THE FIRST question (right after the header, before any analysis
  output): list what needs an Administrator terminal and why, then one clear
  choice — continue without it, or abort to re-run elevated. `--yes` continues
  without asking. Declined items are dropped from the plan entirely, so the
  summary never shows work that will not happen.
- Default output is compact: a one-line scan summary replaces the resolution
  table + inventory; the `[sweep]` diagnostics are silent. New `-v/--verbose`
  restores the full tables and sweep detail (dbg_line now verbose-gated).
- The removal plan prints ONCE, at the very end after the deep sweep, as the
  final "here is what this run will do" — followed by a "NOT removed in this
  run (needs Administrator)" note for anything skipped at the gate, then the
  strays opt-in and the single final confirmation.
- Dry-run keeps the complete preview (admin markers intact) plus a note that a
  real non-elevated run asks up front.

drop_elevation_required now returns the dropped items' descriptions for the
summary note. Validated with cargo xwin clippy (prod + tests) and the uninstall
unit tests (52 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(uninstall): one-click elevate — 3-way gate + UAC helper at removal time

The elevation gate previously offered only continue-without or abort. Windows
cannot elevate a running process in place, so add the middle path the user
actually wants: elevate exactly what needs it, exactly when it is needed.

- Gate becomes a 3-way choice on Windows (still the FIRST question):
    e = elevate at removal time (one UAC prompt), c = continue without,
    a = abort (default). Non-Windows keeps the binary continue/abort. `--yes`
  still means continue-without — a scripted run must never pop a surprise UAC.
- Choosing `e` only records the decision: the admin items stay in the plan
  (final summary notes "will show one Windows UAC prompt when removal starts"),
  and the whole flow continues in the same window. Nothing elevates before the
  final confirmation.
- At execution, SystemEffects::remove_service routes through a one-shot
  elevated helper: PowerShell `Start-Process -Verb RunAs -Wait -PassThru`
  relaunches uffs.exe in the hidden `--uninstall --remove-service-helper
  <name>` mode (refuses to run non-elevated; performs the exact same
  stop+delete as the elevated in-process path), then verifies the service is
  actually gone. Keeps the crate unsafe-free per the module's shell-out design.
- A declined UAC prompt (catch -> exit 223) degrades gracefully: the item is
  reported as skipped with the elevated re-run hint, everything else is still
  removed, and no mid-execution question is asked (decisions stay up front).
- The helper never touches binaries, so there is no self-delete race with the
  waiting parent process.

Validated with cargo clippy (host) + cargo xwin clippy (Windows, prod + tests);
uninstall unit tests pass (38), including the hidden-flag parse test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(uninstall): background gather + spinner, CORE/EXTRA tables, 3-way final choice

Three UX upgrades to the interactive flow (decide -> gather -> present -> confirm):

1. No wasted wall-clock: the drive-coverage reload + deep sweep start on a
   background thread the instant `--uninstall` fires, overlapping the elevation
   question. The daemon handlers gain a quiet mode (daemon_mgmt::daemon_quiet,
   RAII-reset static) and coverage defers its narration as notes, so background
   work never prints over the prompt. After the gate, a small spinner
   ("Gathering artifacts (indexing the drives / searching the drives)...")
   runs until the gather finishes. `-v` keeps the sequential, live-printing
   flow for diagnosis; a panicked gather degrades to "no strays".

2. Nothing is shown until everything is gathered, then the COMPLETE picture
   prints at once as two aligned table sections: CORE (the install — binary
   resolution table + data/cache inventory) and EXTRA (deep-sweep strays, now
   a BINARY / VERSION / LOCATION table matching CORE's shape), followed by the
   action plan and the gate notes.

3. The two trailing questions collapse into one 3-way tied to the sections:
     a = ALL (CORE + EXTRA), c = CORE only, q/Enter = ABORT.
   Without EXTRA files it stays the classic "Proceed with removal? [y/N]".
   `--yes` still means ALL. Execution extracted into execute_all (no prompts
   past consent).

The quiet-mode plumbing pushed daemon_mgmt.rs past the 800-LOC budget; fixed
at the root by decomposing, not excepting: the read-only status/stats rendering
(daemon_status, print_drive_line, tier_marker, print_not_running, daemon_stats,
compute_hit_rate_percent) moves to the new sibling commands/daemon_status.rs
(254 LOC), leaving daemon_mgmt.rs at 625 LOC with dispatch, the elevation gate,
and the mutating handlers. Display code unchanged byte-for-byte.

Validated with cargo clippy (host) + cargo xwin clippy (Windows, prod + tests);
44 unit tests pass; file-size gate passes with no new exception.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): presentation order + EXTRA in the plan summary; quiet the [diag] leak

Follow-ups from the live Windows dry-run of the new flow:

- Presentation order: the data/cache/config inventory + broker-service state
  now print right after the coverage note, BEFORE the CORE binary table (was
  sandwiched between CORE and EXTRA).
- The removal-plan summary now includes the deep-sweep findings instead of
  silently omitting them: a "Found elsewhere (EXTRA)" group line ("N UFFS
  file(s) outside the standard install locations (listed above; removed only
  with ALL)") and a reclaim line that reads "~X across N CORE item(s), plus M
  EXTRA file(s) with ALL" — alluding to the ALL/CORE/ABORT question the real
  run asks. "Nothing to remove" now only prints when BOTH plans are empty.
- The daemon_start [diag] spawn-chain dump is also silenced in quiet mode: with
  UFFS_LOG=debug set it printed from the background reload straight over the
  spinner.
- Dry-run keeps its established gate behavior (no elevation question; markers +
  the explanatory note), confirmed as the intended design.

Validated with cargo clippy (host) + cargo xwin clippy (Windows); 44 unit
tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style(uninstall): breathing room before the gate list and both Choice prompts

Blank line between the elevation gate's intro and its item list, and before
the "Choice [e/c/A]:" and "Choice [a/c/Q]:" lines, so the questions stand
apart from their option lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): teardown-last execution order + 5 live-run bugs from LOG/Output

The first real Windows run surfaced a cluster of execution bugs. All fixed at
the root:

1. Teardown-last plan order (the user-prescribed sequence): tool binaries ->
   PATH -> "Shutdown (stopped last)" (daemon stop + broker service removal) ->
   data/cache/config (a running daemon holds handles inside them) -> "Runtime
   binaries (after shutdown)" (uffsd/uffs-broker/uffsmcp/uffs-mcp-http, whose
   images are locked while running) -> deferred self-delete of uffs.exe +
   uffs-update.exe. The working tools stay usable for the whole run.

2. One locked file no longer traps a whole directory: delete_binaries is now
   best-effort across the set (the original run lost 21 deletions to one
   lingering uffsd.exe), with a 750ms settle-and-retry pass, reporting exactly
   which files failed.

3. The daemon stop re-discovers the CURRENT daemon via the real
   `uffs --daemon kill` handler (pid file/socket) instead of the analyzed pid
   (stale after the deep sweep's coverage reload), then waits for IPC-down +
   image release before the runtime binaries are deleted.

4. Windows daemon-management elevation gate now mirrors the Unix owner gate:
   no elevation needed when there is no PID file (nothing to protect) or when
   the daemon's launch-state sidecar records a NON-elevated launch (the daemon
   now writes an "elevated" flag into daemon.state.json) — a user-level daemon
   is the user's to kill even with the broker gone. Falls back to the broker
   probe otherwise.

5. The deferred self-delete never actually deleted anything: std's Windows arg
   quoting backslash-escaped the `del "path"` quotes inside the `cmd /c`
   payload, which cmd.exe does not parse. Passed verbatim via raw_arg now.

6. Formatting: the coverage failure notes put "Continuing the deep sweep..."
   on its own line, and a blank separator precedes the `[sweep]` block (-v).

7. `uffs-broker --install` narrates its steps (sc create -> ok, sc start with
   a "can take a minute" note -> ok) instead of a silent minute-long wait.

plan.rs crossed the 800-LOC budget with the reorder; fixed by decomposing (the
established sibling-tests pattern): its unit tests moved to plan/tests.rs,
leaving plan.rs at 513 LOC. Tests updated to the new order contract plus a
regression test pinning the runtime-stem split. Validated with cargo clippy
(host) + cargo xwin clippy (Windows, prod + tests); 38 uninstall tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(uninstall): sweep-elevation gate for the no-broker path + graceful daemon stop

Without the Access Broker a non-elevated daemon cannot read the MFT, so the
deep sweep silently came up empty ("could not start the daemon: Failed to
start daemon" -> 0 candidates). Now the sweep is an explicit up-front decision:

- New sweep gate (asked FIRST, before the gather starts) — only when it
  matters: coverage incomplete AND not elevated AND no broker pipe serving.
    d = deep sweep — start the daemon now (one UAC prompt)
    s = skip the deep sweep (standard locations only)
  Choosing d routes the coverage reload through the existing
  `--daemon start --elevate` machinery (connect_with_elevation), so the UAC
  prompt happens right then and there. `--yes` / `--dry-run` never pop a
  surprise UAC: they skip with an explanatory note. Broker-serving, elevated,
  or already-covered runs proceed silently as before.

- The daemon stop now tries the graceful shutdown RPC first: it needs no OS
  privileges, so it also stops the ELEVATED daemon the sweep may have started
  (which taskkill cannot touch), before falling back to the kill handler and
  the recorded pid.

SweepDecision is threaded through start/finish_stray_gather and
ensure_drive_coverage(quiet, elevate_daemon); --no-deep-sweep folds into the
same decision. Validated with cargo clippy (host) + cargo xwin clippy
(Windows, prod + tests); 44 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): tear down a sweep-started daemon (fixes uffsd.exe Access-denied)

The no-broker deep sweep can START the index daemon (its UAC start) AFTER the
removal plan was snapshotted with no daemon running. The plan's shutdown group
was therefore built from an empty `report.running` and carried no stop for that
daemon, so at teardown nothing stopped it — its (possibly elevated) image stayed
locked and the runtime-binary delete failed:

    FAILED  4 binaries in C:\Users\rnio\bin
            (C:\Users\rnio\bin\uffsd.exe: Access is denied. (os error 5))

Windows offers no way to retain/reuse the UAC elevation token in the
non-elevated parent — the prompt elevated a separate child (the daemon), not
us. But we do not need to: the executor already stops the daemon with a graceful
shutdown RPC, which needs no caller privilege and so stops even an elevated
daemon; once it exits, its user-owned binary deletes fine non-elevated. The only
missing piece was a stop item in the plan.

After the gather, re-discover the live daemon (running_daemon_pid) and fold it
into the plan via RemovalPlan::ensure_daemon_shutdown(pid): prepend a daemon
StopProcess to the "Shutdown (stopped last)" group, creating that group in the
correct position (before the data / runtime-binary groups) when it does not yet
exist. No-op when a daemon stop already exists. Group titles are now shared
consts so the find/recreate matches build_plan verbatim.

Two regression tests: injection lands the stop before the runtime binaries;
an existing stop is not duplicated. cargo clippy (host) + cargo xwin clippy
(Windows prod + tests) clean; 126 uffs-cli tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): silence the thin-client auto-start chatter during the quiet sweep

The deep-sweep coverage reload (kill + start) runs on a background thread under
a spinner, but the freshly-restarted daemon's start-wait went through the thin
client's auto-start connect loop, whose "[uffs] connect attempt N/M (socket:
missing)" retry line prints straight to stderr — a layer below the CLI's QUIET
flag — and bled onto the spinner:

    ⠼ Gathering artifacts (indexing the drives ...)   [uffs] connect attempt 1/20 (socket: missing)

Give uffs-client its own suppression toggle (set_quiet_autostart) gating that
eprintln, and drive it from daemon_quiet alongside the existing CLI QUIET flag
(QuietGuard restores both on drop, so it never sticks past the reload). The
spinner line now stays clean.

cargo clippy (host) + cargo xwin clippy (Windows) clean; 332 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(uninstall): count binary sizes in the reclaim total (no more "~0 B")

Binary-delete plan items carried bytes: 0, so a run removing only binaries +
daemon + runtime (no data/cache dirs present) reported "Reclaims ~0 B across 3
CORE item(s)" while permanently deleting ~22 real binaries.

Add RemovalPlan::size_binaries(size_of): the pure plan module stays IO-free and
takes a caller-supplied sizer; analyze_and_plan (the IO layer that already pulls
dir sizes from the inventory) stats each stem's file (uffsd -> uffsd.exe on
Windows, best-effort — an absent file contributes 0) and folds the totals in.
WinGet delegations and dir/process items are untouched. exe_file_name is now
pub(crate) so the sizer reuses the executor's naming.

Regression test: size_binaries fills only DeleteBinaries items and leaves the
inventory's dir sizes alone. cargo clippy (host) + cargo xwin clippy (Windows,
prod + tests) clean; rustdoc -Dwarnings clean; 127 uffs-cli tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(ci): record why the rustdoc gate omits private_intra_doc_links

We hit this question live: -Dwarnings + --document-private-items reports 0
warnings, but bolting on -D rustdoc::private_intra_doc_links flips 2 valid
internal //! links in uffs-cli/src/main.rs to errors. That looks like a gap and
invites someone to "harden" the flag in — but the current posture is the
desired one.

Pin the rationale in the rustdoc gate's notes so it is not flipped by accident:
--document-private-items documents the internals, so a public/crate-root link to
a pub(crate) sibling is a valid internal cross-reference, not a downstream leak;
private_intra_doc_links guards a *published* crate's public docs against
dangling to items a consumer cannot see, which does not apply to our internal
doc build. Enabling it would demote valid internal [symbol] links to dead code
spans for zero correctness gain. The real failure class (broken / unresolved
links) is already caught by broken-intra-doc-links under -Dwarnings.

Notes-only manifest edit: gates-drift, hooks-drift, workflow-drift all clean
(notes do not feed generated hooks/workflows).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): flatten the removal plan + coherent decline wording

Two UX fixes from live Windows runs:

1. The consent surface split binaries into "Binaries" and "Runtime binaries
   (after shutdown)" (17 + 4 in the same folder), plus group headers like
   "Shutdown (stopped last)" — the internal teardown-ordering detail leaked to
   the user, who just wants "21 binaries in <dir>". print_plan now coalesces
   every DeleteBinaries item per directory into one line, drops the group
   headings, folds EXTRA into the same numbered list, and simplifies the reclaim
   footer ("Reclaims ~121.2 MB." / "…, plus N file(s) removed only with ALL.").
   The plan's internal group order is unchanged — this is presentation only, so
   the teardown still stops the daemon before deleting its locked image.

2. When the no-broker sweep's elevated daemon start was declined at the UAC
   prompt, the note read:
       "Note: the index daemon was reloaded (kill + start) ..."
       "  could not start the daemon: Failed to start daemon (with elevation)"
   — it claimed success then contradicted itself. The optimistic "reloaded" note
   was pushed up front, before the start was even attempted. It is now pushed
   ONLY after start succeeds; a failed start emits one coherent note that names
   the likely cause ("the UAC prompt was likely declined") and reassures the
   sweep continues on the drives already indexed.

cargo clippy (host) + cargo xwin clippy (Windows, prod + tests) clean; rustdoc
-Dwarnings clean; 127 uffs-cli tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): leave the broker cleanly when elevation is declined + honest verify

Three fixes from the broker-installed, non-elevated `e` (elevate-at-removal)
path — the one path never exercised before now.

1. Declining the UAC prompt used to attempt-and-fail the dependent broker work:
       Removal finished: 2 removed, 2 failed.
         FAILED  Stop + delete service UffsAccessBroker  (UAC declined ...)
         FAILED  4 binaries in C:\Users\rnio\bin  (uffs-broker.exe: Access is denied ...)
   The second failure is a *consequence* of the first — the still-running broker
   locks its own image. Now the declined UAC is a typed signal (ElevationDeclined)
   the executor recognises: it records the service as LEFT (not FAILED), stops
   attempting the doomed broker binary (deletes the other runtime binaries, leaves
   uffs-broker.exe), and prints ONE clear next step. New ItemStatus::Skipped
   distinguishes "deliberately left" from "failed to remove".

2. The final line claimed "Verified: all targeted UFFS locations are gone" while
   the broker service + binary were still there (they are not among the
   stat-checked paths). The upbeat claim is now gated on a clean run (nothing
   failed, nothing left); a declined broker no longer reads as success.

3. The self-delete note listed uffs.exe + uffs-update.exe by full path — a
   mechanism detail. Collapsed to one line: "The uffs command removes itself once
   this process exits."

Also: one blank line between the sweep-decision prompt and the gather spinner
(it butted right up against "Choice [d/S]: d").

New unit test: a declined elevation leaves the broker service + binary as two
LEFT items with zero hard failures, and never attempts the broker image while
still removing the other runtime binaries. cargo clippy (host) + cargo xwin
clippy (Windows, prod + tests) clean; rustdoc -Dwarnings clean; 128 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): also leave the broker binary on the non-elevated "continue" path

The prior fix cleaned the `e` (elevate-at-removal) decline, but the `c`
(continue-without-elevation) path still hit the same wall: the elevation gate
drops the broker SERVICE item up front, yet the broker BINARY stayed in the
plan and failed with a raw Access-denied because the left-behind service keeps
uffs-broker.exe locked:

    NOT removed in this run (needs Administrator):
      - Stop + delete service UffsAccessBroker      <- clean, up front
    ...
    Removal finished: 4 removed, 1 failed.
      FAILED  4 binaries in C:\Users\rnio\bin  (uffs-broker.exe: Access is denied)

Generalise the decline handling into one condition: the broker binary is LEFT
whenever the broker service REMAINS — computed up front (broker installed AND
the plan carries no RemoveService item, i.e. the `c` path dropped it) and also
flipped by an in-plan declined UAC (the `e` path). execute() takes a
`broker_remains` flag seeded from that, so both paths leave uffs-broker.exe
cleanly (recorded LEFT) while still deleting the other runtime binaries.

New test: broker_remains=true up front leaves only the broker binary, makes no
remove_service call, and still removes the non-broker runtime binary. cargo
clippy (host) + cargo xwin clippy (Windows, prod + tests) clean; rustdoc
-Dwarnings clean; 129 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(security): risk-accept the two quick-xml advisories (unreachable, no fix exists)

RUSTSEC-2026-0195 (unbounded namespace-declaration allocation) and
RUSTSEC-2026-0194 (quadratic duplicate-attribute check) both landed against
quick-xml <0.41 and turned every CI run red, kicking skyllc-ai#502 out of the merge
queue.

Why an ignore and not a fix: there is nothing to bump to. quick-xml is
transitive-only (polars-io -> object_store 0.13 -> quick-xml ^0.39); the newest
object_store on crates.io (0.14.0) still requires quick-xml ^0.40.1, below the
fixed 0.41, and we are already on the latest polars (0.54.4). The vulnerable
paths are object_store's cloud-store XML LIST parsing — UFFS reads the local
NTFS MFT and local index files only, and never opens a cloud object path, so
the NsReader code is unreachable in every UFFS binary.

Both entries carry the removal condition in-line: drop them when object_store
ships a quick-xml >=0.41 release AND polars adopts it. Validated locally:
`cargo deny check advisories` -> ok.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
deep-soft pushed a commit to deep-soft/UltraFastFileSearch-Rust that referenced this pull request Jul 3, 2026
… confinement, exemption burn-down (skyllc-ai#504)

* fix(cli): strip Windows \\?\ verbatim prefix in uninstall path resolution

On Windows, std::fs::canonicalize returns the \\?\ extended-length form, which
never matches a bare C:\... PATH entry. The live v0.6.18 run showed every binary
(including the active uffs) mislabeled 'off-path' and the install dir displayed
as \\?\C:\Users\rnio\bin, and it means the PATH-safety gate can't recognize a
PATH entry either.

Add strip_verbatim_prefix and apply it at the canonicalize sites (detect's
upsert_root, the uninstall PATH scan) and to the current-exe dir in search_dirs,
so stored dirs match plain PATH entries and display cleanly. No-op off Windows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall deep sweep decodes every search payload (was finding zero strays)

The live v0.6.18 run surfaced no strays despite dozens of stray uffs.exe copies
on disk. Root cause: the daemon delivers search results in four shapes — inline
rows, a memory-mapped rows file, an inline pre-formatted blob, or a memory-mapped
blob (chosen by size + output shape). A real multi-hit Windows sweep returns a
CSV/path *blob*, but the old code walked the JSON for `"path"` object keys,
which only exist in the inline-rows case — so every blob/shmem result was
silently dropped.

Switch to the typed `search_cli` + `--columns path` (single-column output) and
decode all payload variants via the client's shmem/blob helpers
(read_search_results / stream_paths_blob_into), parsing the path-per-line blob
(header + CSV quotes stripped). Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall drive-coverage prompt warns that indexing builds a cache

The live v0.6.18 dry-runs grew a ~4 GB index cache because each 'y' to the
coverage offer indexed more drives. That is by design (indexing is
non-destructive and the deep sweep needs it), but the prompt gave no hint that
saying yes builds an on-disk cache that persists even under --dry-run. Spell it
out so the choice is informed. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall drive-coverage polls status_drives for readiness, not a blind wait

A freshly load_drive_letters-requested shard starts parked/cold and only becomes
searchable once its body is resident. The previous fixed await_ready(120s) could
return while shards were still loading, so the deep sweep searched a not-yet-ready
index and found nothing.

Poll status_drives until every requested drive reports a loaded tier (hot/warm)
or the deadline elapses — so the sweep waits exactly as long as needed and never
searches a still-parked shard. Best-effort: RPC errors keep polling to the
deadline, then proceed with whatever is loaded. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): stamp the git commit into `uffs --version` to verify the running build

The live Windows test ran an OLD uffs.exe: the shell prompt said the
fix/uninstall-windows-followups branch was checked out, but the output still
showed the pre-fix behaviour (off-path, \\?\ paths) — a stale binary never
rebuilt/redeployed. The CLI's --version printed only the crate version, so there
was no way to tell which build was running.

The daemon already stamps UFFS_GIT_SHA into its startup log to close exactly
this 'ran the wrong/stale binary' trap; port the same build.rs stamp to the CLI
and surface it: `uffs --version` now prints 'uffs <ver> (<sha>[-dirty])'. Match
the sha against `git rev-parse --short HEAD` to confirm the build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall deep sweep keeps only real family files, not substring noise

The live Windows sweep worked but listed non-binaries as removable strays: the
daemon search matches `uffs.exe` as a *contains* query, so it also returned
prefetch traces (UFFS.EXE-1234.pf), localized resources (uffs.exe.mui),
checksums (uffs.exe.sha256), build recipes (uffs.exe.recipe), and NTFS
alternate-data-stream entries (uffs.exe:com.dropbox.attrs).

Add is_family_artifact: keep a hit only when its file name is exactly a family
executable (uffs.exe / uffsd.exe / uffs-broker.exe / uffs-tui*.exe / …) or a
cache file (*_compact.uffs / *_usn.cursor); drop anything ending in
.pf/.mui/.sha256/.recipe or containing ':' (an ADS entry). Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(client): exe-resolution fallbacks carry .exe on Windows (no bare uffs name)

Audit of the `uffs` -> `uffs.com` concern: every actual spawn site already uses
a full current-exe-relative path, and Rust's Command appends .exe (never .com)
on Windows — which is why the uninstall run completed correctly. The only bare
names were the $PATH fallbacks in find_uffs_exe / find_daemon_exe, hit only when
current_exe + sibling lookup both fail.

Harden those to the platform binary name (uffs.exe / uffsd.exe on Windows) so a
bare `uffs` can never be resolved to a legacy uffs.com via PATHEXT (.COM precedes
.EXE) if the path is ever handed to a shell, a registry entry, or a logged
command rather than spawned directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall drops the legacy C++ uffs.exe GUI binary (PE subsystem check)

On a machine that still has the predecessor C++ UFFS installed, the deep sweep
listed its uffs.exe copies and — worse — version_strays ran each with --version,
launching a GUI window per copy (the slow, 'CPP version keeps popping up'
behaviour). The C++ product is a Windows GUI app; our Rust CLI is a console app.

Read the PE Optional-Header Subsystem field (headers only, never executing the
file): if a uffs.exe is a GUI-subsystem binary, drop it from the strays before
probing. Only uffs.exe collides with the predecessor; the other family names are
Rust-only and untouched. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): uninstall prints the running build's version + commit at the top

Add a 'uffs <ver> (<sha>) — uninstall' header to the dry-run and live output, so
any captured run is unambiguously tied to the exact binary (the same stamp
`uffs --version` shows). Makes a stale-binary run obvious at a glance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall drive-coverage waits long enough + shows index progress

The live 7-drive load took ~2.5 min but the readiness poll capped at 120 s, so it
returned at 6/7 drives and the sweep searched a not-yet-ready index — missing the
still-loading drive (D:). Raise the cap to 600 s and print 'indexing for the
sweep: N/M drives ready...' as drives come online, so the wait covers a real cold
multi-drive index and never looks like a hang. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall indexes drives with live progress + clearer prompt

#3/#4 from the Windows test. The load RPC blocks until the daemon finishes every
drive (loaded sequentially), which overran the client timeout on a 7-drive
system — so it returned at 6/7 (missed D:) and no progress ever showed.

Fire the load on a background connection and poll status_drives on this thread,
printing 'indexing for the sweep: N/M drives ready...' as drives come online —
the poll, not the RPC return, decides when the drives are searchable, so a
background timeout is harmless and the sweep no longer searches a partial index.
Also reword the coverage prompt to be less repetitive. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall broker is optional when non-elevated + display polish

#1: from a non-elevated terminal the broker (its LocalSystem service + process)
cannot be stopped/removed, but the old gate refused the WHOLE uninstall. Mark the
broker process as admin-only too, and instead of bailing, offer to skip the
admin-only items and remove everything else now (or abort to re-run elevated).
Adds RemovalPlan::drop_elevation_required.

#2: show 'legacy' instead of '-' for versionless (old) binaries.
skyllc-ai#5: blank line between the 'found elsewhere' heading and the file list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall always indexes drives for the deep sweep (no prompt)

Indexing every NTFS drive is a non-elevated, non-destructive read that the deep
sweep requires, so it should just happen — not be a [y/N] choice. Drop the
prompt: ensure_drive_coverage now always loads any not-yet-indexed drives (with
the live N/M progress) and no longer takes a confirm callback.

This also removes the elevated-vs-non-elevated output divergence: the runs only
differed because one had drives already loaded (no prompt) and the other didn't
(prompt). Now both just index what's missing. Windows-only module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): uninstall removes the full workspace binary set, not just the core

The live run left ~15 dev/diagnostic binaries in ~/bin untouched (analyze-diff,
dump-mft-records, gen-hooks, uffs-bench, uffs-ci-pipeline, …) — a from-source /
cargo install build drops them next to the core set. Add them to
EXTRA_BINARY_STEMS so the install-dir sweep removes them.

Refactor the deep sweep to derive its search patterns + family filter from the
shared family set (KNOWN_BINARIES + EXTRA_BINARY_STEMS) instead of a second
hardcoded list — so adding a binary in one place now updates both the
install-dir removal and the cross-drive sweep. None of these are managed by
--update, so KNOWN_BINARIES (the update set) is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall gathers decisions up front, runs once, defers the running binary

Three fixes from the live runs:

1. Decisions up front, no post-removal prompts: the broker keep/skip (elevation
   gate) and the deep-sweep strays opt-in are both decided before the single
   'Proceed with removal?' go, then everything executes once into one combined
   outcome — so the summary + retry hint print once, not per-phase.

2. Platform-correct failure hint: elevation only exists on Windows (the broker
   is a LocalSystem service); a non-Windows uninstall is all user-land, so it no
   longer suggests 'sudo' there — a failure is a file in use.

3. The chicken-and-egg self-delete: the OS locks a running image, so deleting
   uffs.exe / uffs-update.exe in place is the 'access denied' seen in the live
   run. SystemEffects now skips the running self-binaries (matched verbatim-
   stripped, case-insensitive) and the existing spawned-cmd schedule_self_delete
   removes them after exit — the same deferred-delete installers (NSIS/Inno) use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall does not taskkill the broker (it is a service; sc handles it)

In the elevated run, 'broker (pid 9064)' failed with exit 128: it was a
StopProcess item executed via taskkill /F, but the broker is a LocalSystem
service — taskkill can't stop it (and even forced, the SCM restarts it). The
RemoveService item already stops + deletes it the right way (uffs_winsvc::stop +
sc delete).

Filter the broker out of the Processes group so it is never taskkill'd. Only the
user-owned daemon / MCP remain there (no admin needed); the broker is handled
solely by RemoveService. Removes the guaranteed-failure line from the outcome.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): uninstall decides the broker/elevation up front, before the deep sweep

A non-elevated run should be told immediately that the broker needs Administrator
— not after sitting through a multi-minute drive index + sweep. Move the
elevation decision to right after the plan is shown, before platform_stray_plan:

- Not elevated + broker installed: flag it and offer to continue (uninstall
  everything except the broker) or abort to re-run from an elevated terminal.
- Elevated: skipped entirely — just remove everything (incl. the broker), and
  the running binary is self-deleted at the end as before.
- Dry-run: only previews (the plan already marks the broker 'needs Administrator').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(uninstall): parallelize + timeout the deep-sweep version probes; anchor the query

The deep sweep's post-query step ran `<bin> --version` sequentially with no
timeout, once per family hit. On a dev box (hundreds of `uffs*.exe` under
`target/`) that took minutes, and any binary that doesn't cleanly handle
`--version` (half-written artifact, something waiting on stdin) hung the whole
sweep indefinitely.

version_strays:
- probe in parallel via a bounded `std::thread::scope` worker pool
  (cursor-stealing; no new dependency)
- `probe_version_bounded`: stdin nulled, piped output, poll `try_wait` to a 2s
  deadline then `kill` — a hung binary goes unversioned instead of stalling

DaemonSearch::find:
- anchor each `stem.exe` query with `--name-only --ext exe` instead of a bare
  full-path substring. Measured 158 -> 46 hits for `uffs.exe`; identical to the
  exact-regex count, so it's lossless. Drops `.mui`/prefetch/ADS/path-substring
  noise at the daemon before rows cross the wire. Glob cache patterns untouched.

Plus temporary `[sweep]` diagnostics (per-pattern raw/kept counts, phase
timings, timeout count) routed through one `dbg_line` helper, to verify the
live Windows run. To be removed once signed off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(uninstall): redesign the discovered-binary table (one row/binary, header, plain labels)

The old output was two lines per binary (a `stem:` header + an indented row)
with cryptic columns: ACTIVE/shadowed/off-path, a raw channel word
(`unmanaged`), and a bare `-` scope. Redesign to a single aligned row per copy
with a header and a STATUS legend:

- ACTIVE -> `runs` (the copy a bare command executes, first on PATH);
  on-PATH-but-later -> `shadowed`; not-on-PATH -> `off PATH`.
- Fold the channel + scope into one SOURCE column: `hand-placed` (was
  `unmanaged`), `dev build`, `winget (user)`/`winget (machine)` — scope only
  means something for winget, so the lone `-` is gone.
- Columns are width-sized to header+cells; LOCATION is last / free-width.

Display-only; resolution logic in resolve_order.rs is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(uninstall): make deep-sweep drive coverage robust (kill+start, not a racing hot-load)

The old coverage fired its own load_drive_letters for any not-yet-loaded drive,
racing the daemon's background startup load over the single-instance Access
Broker pipe (ERROR_PIPE_BUSY). That churned the registry (observed 2/6 -> 0/6),
intermittently dropped a drive (6/7), and could spin to the wait cap.

Replace it with the proven CLI flow, only when needed:
- Managed set = every `status_drives` row (any tier — hot/warm/parked/cold; a
  search re-promotes a parked drive on demand).
- If the daemon already covers every system drive: do nothing.
- If ANY drive is missing: `uffs --daemon kill`, wait for full shutdown, then a
  clean `uffs --daemon start` (loads every drive with broker warm-up, returns
  only once Ready), then poll until coverage is complete.

No `restart`, no hot-load, no competing loader thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(uninstall): never start the daemon in-process for coverage; degrade gracefully

Spawning `uffs --daemon start` as a child of the uninstall intermittently hangs
the drive load (daemon stuck at 5/7, zero progress) even though a standalone
`uffs --daemon start` loads all drives in seconds. Rather than chase that
spawn-context bug, stop bringing the daemon up in-process:

- Fully covered already (warm daemon): proceed silently — the common case.
- Daemon up but mid-load: wait briefly (60s cap), then proceed with whatever
  loaded. Never blocks indefinitely.
- No daemon reachable: print a one-line notice telling the user to run
  `uffs --daemon start` and re-run for a complete sweep; continue best-effort.

No kill, no start, no restart, no competing loader — the deep sweep now covers
whatever the daemon already has and never hangs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(uninstall): reload daemon for coverage via the real CLI handlers (kill+start in-process)

Previous attempt shelled out to `uffs.exe --daemon start` as a subprocess, which
spawned the daemon as a grandchild and intermittently hung its drive load
(stuck at 5/7). A standalone `uffs --daemon start` loads all drives in seconds.

Reuse the exact handlers the CLI dispatches instead: call
`daemon_mgmt::daemon(&DaemonAction::Kill)` then `daemon(&Start{..})` in-process,
so the daemon is a DIRECT child of this process — identical topology to a shell
`uffs --daemon start`. When coverage is complete, no-op silently; when a drive
is missing, kill, wait for full shutdown, start (blocks until Ready = all drives
loaded), then proceed. Any handler error is best-effort: note it and sweep with
whatever is loaded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build(windows): embed icon + version-info + manifest into the 4 bare binaries

Only uffs.exe and uffs-update.exe carried Windows PE resources; uffsd, uffsmcp,
uffs-broker, and uffs-mft shipped bare. A metadata-less unsigned binary is both
unbranded and a mild antivirus ML false-positive signal (all 7 tripped the same
generic Defender heuristic on the 0.6.18 release).

Add a per-crate build.rs to each (mirroring uffs-cli/uffs-update) that embeds
via winresource on MSVC-Windows only:
- the UFFS icon (shared assets/brand/icons/uffs.ico)
- version info: ProductName, FileDescription, CompanyName, LegalCopyright,
  OriginalFilename (winresource auto-fills File/ProductVersion from the crate)
- a new shared assets/brand/app.manifest (asInvoker, PerMonitorV2, longPathAware)

winresource added as a build-dependency of each crate. No-op off Windows; the
uffs-mft library target is unaffected. Validated with cargo xwin clippy for
x86_64-pc-windows-msvc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build(windows): embed UFFS icon + metadata into the 14 dev/CI binaries

Branding consistency across the whole family: the diagnostic + CI tools shipped
bare (no icon/version info). Add a per-crate build.rs embedding the shared UFFS
icon + version info + app.manifest via winresource (MSVC-Windows only, no-op
elsewhere) to the 6 crates that produce them:

- uffs-diag (9 bins: analyze-diff, analyze-mft-parents, compare-raw-mft,
  compare-scan-parity, cross-check-mft-reference, dump-mft-extents,
  dump-mft-records, inspect-mft-record-flow, scan-mft-magic)
- uffs-bench (uffs-bench)
- scripts/ci/gen-hooks, scripts/ci/gen-workflow, scripts/ci/manifest-audit
- scripts/ci-pipeline (uffs-ci-pipeline)

winresource added as a build-dependency of each. Validated with cargo xwin
clippy for x86_64-pc-windows-msvc. Completes the all-binaries icon-branding task.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(mft): bound the overlapped $UpCase/FRS read with a dedicated event (fixes fresh-load hang)

Root cause of the daemon's fresh (no-cache) load hanging at 5/6-of-7 drives:
after the main MFT read, each drive re-reads the $UpCase table via
read_handle_at -> read_handle_at_once, which issued an overlapped ReadFile on
the broker's FILE_FLAG_OVERLAPPED handle with a NULL-hEvent OVERLAPPED and then
GetOverlappedResult(bWait=true). With no event, GetOverlappedResult waits on the
file object itself; the broker vends duplicate handles to the same volume file
object, so under a concurrent multi-drive load it cannot tell which read
completed and blocks FOREVER (Microsoft's documented pitfall). Debug logs showed
1-2 drives stall right after "Adopted Access Broker volume handle", never
reaching "Parsed $UpCase data runs" — a silent, error-less hang. This affected
every fresh `uffs --daemon start`, not just the uninstall.

Fix: bind each read to a dedicated manual-reset event and wait on the event
(never the shared handle), bounded by IOCP_WAIT_COMPLETION_DEADLINE. On timeout,
CancelIoEx + drain, then return a retryable ERROR_OPERATION_ABORTED so the
existing read_handle_at retry loop reissues it. The event makes the wait
specific to this operation (the documented fix); the bound guarantees the load
can never hang forever again — a genuinely wedged read fails fast and the daemon
reaches Ready instead of stalling.

Validated with cargo xwin clippy (x86_64-pc-windows-msvc).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(uninstall): quieter UX — elevation asked first, one final summary, -v for detail

The interactive flow was noisy and the elevation choice confusing: the full
binary table, inventory, and plan (broker item included, "(needs
Administrator)") printed BEFORE a double-clause elevation question, and
answering y echoed "Leaving the broker installed" mid-flow.

Restructure to: decide -> gather -> summarize -> confirm.

- Elevation is THE FIRST question (right after the header, before any analysis
  output): list what needs an Administrator terminal and why, then one clear
  choice — continue without it, or abort to re-run elevated. `--yes` continues
  without asking. Declined items are dropped from the plan entirely, so the
  summary never shows work that will not happen.
- Default output is compact: a one-line scan summary replaces the resolution
  table + inventory; the `[sweep]` diagnostics are silent. New `-v/--verbose`
  restores the full tables and sweep detail (dbg_line now verbose-gated).
- The removal plan prints ONCE, at the very end after the deep sweep, as the
  final "here is what this run will do" — followed by a "NOT removed in this
  run (needs Administrator)" note for anything skipped at the gate, then the
  strays opt-in and the single final confirmation.
- Dry-run keeps the complete preview (admin markers intact) plus a note that a
  real non-elevated run asks up front.

drop_elevation_required now returns the dropped items' descriptions for the
summary note. Validated with cargo xwin clippy (prod + tests) and the uninstall
unit tests (52 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(uninstall): one-click elevate — 3-way gate + UAC helper at removal time

The elevation gate previously offered only continue-without or abort. Windows
cannot elevate a running process in place, so add the middle path the user
actually wants: elevate exactly what needs it, exactly when it is needed.

- Gate becomes a 3-way choice on Windows (still the FIRST question):
    e = elevate at removal time (one UAC prompt), c = continue without,
    a = abort (default). Non-Windows keeps the binary continue/abort. `--yes`
  still means continue-without — a scripted run must never pop a surprise UAC.
- Choosing `e` only records the decision: the admin items stay in the plan
  (final summary notes "will show one Windows UAC prompt when removal starts"),
  and the whole flow continues in the same window. Nothing elevates before the
  final confirmation.
- At execution, SystemEffects::remove_service routes through a one-shot
  elevated helper: PowerShell `Start-Process -Verb RunAs -Wait -PassThru`
  relaunches uffs.exe in the hidden `--uninstall --remove-service-helper
  <name>` mode (refuses to run non-elevated; performs the exact same
  stop+delete as the elevated in-process path), then verifies the service is
  actually gone. Keeps the crate unsafe-free per the module's shell-out design.
- A declined UAC prompt (catch -> exit 223) degrades gracefully: the item is
  reported as skipped with the elevated re-run hint, everything else is still
  removed, and no mid-execution question is asked (decisions stay up front).
- The helper never touches binaries, so there is no self-delete race with the
  waiting parent process.

Validated with cargo clippy (host) + cargo xwin clippy (Windows, prod + tests);
uninstall unit tests pass (38), including the hidden-flag parse test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(uninstall): background gather + spinner, CORE/EXTRA tables, 3-way final choice

Three UX upgrades to the interactive flow (decide -> gather -> present -> confirm):

1. No wasted wall-clock: the drive-coverage reload + deep sweep start on a
   background thread the instant `--uninstall` fires, overlapping the elevation
   question. The daemon handlers gain a quiet mode (daemon_mgmt::daemon_quiet,
   RAII-reset static) and coverage defers its narration as notes, so background
   work never prints over the prompt. After the gate, a small spinner
   ("Gathering artifacts (indexing the drives / searching the drives)...")
   runs until the gather finishes. `-v` keeps the sequential, live-printing
   flow for diagnosis; a panicked gather degrades to "no strays".

2. Nothing is shown until everything is gathered, then the COMPLETE picture
   prints at once as two aligned table sections: CORE (the install — binary
   resolution table + data/cache inventory) and EXTRA (deep-sweep strays, now
   a BINARY / VERSION / LOCATION table matching CORE's shape), followed by the
   action plan and the gate notes.

3. The two trailing questions collapse into one 3-way tied to the sections:
     a = ALL (CORE + EXTRA), c = CORE only, q/Enter = ABORT.
   Without EXTRA files it stays the classic "Proceed with removal? [y/N]".
   `--yes` still means ALL. Execution extracted into execute_all (no prompts
   past consent).

The quiet-mode plumbing pushed daemon_mgmt.rs past the 800-LOC budget; fixed
at the root by decomposing, not excepting: the read-only status/stats rendering
(daemon_status, print_drive_line, tier_marker, print_not_running, daemon_stats,
compute_hit_rate_percent) moves to the new sibling commands/daemon_status.rs
(254 LOC), leaving daemon_mgmt.rs at 625 LOC with dispatch, the elevation gate,
and the mutating handlers. Display code unchanged byte-for-byte.

Validated with cargo clippy (host) + cargo xwin clippy (Windows, prod + tests);
44 unit tests pass; file-size gate passes with no new exception.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): presentation order + EXTRA in the plan summary; quiet the [diag] leak

Follow-ups from the live Windows dry-run of the new flow:

- Presentation order: the data/cache/config inventory + broker-service state
  now print right after the coverage note, BEFORE the CORE binary table (was
  sandwiched between CORE and EXTRA).
- The removal-plan summary now includes the deep-sweep findings instead of
  silently omitting them: a "Found elsewhere (EXTRA)" group line ("N UFFS
  file(s) outside the standard install locations (listed above; removed only
  with ALL)") and a reclaim line that reads "~X across N CORE item(s), plus M
  EXTRA file(s) with ALL" — alluding to the ALL/CORE/ABORT question the real
  run asks. "Nothing to remove" now only prints when BOTH plans are empty.
- The daemon_start [diag] spawn-chain dump is also silenced in quiet mode: with
  UFFS_LOG=debug set it printed from the background reload straight over the
  spinner.
- Dry-run keeps its established gate behavior (no elevation question; markers +
  the explanatory note), confirmed as the intended design.

Validated with cargo clippy (host) + cargo xwin clippy (Windows); 44 unit
tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style(uninstall): breathing room before the gate list and both Choice prompts

Blank line between the elevation gate's intro and its item list, and before
the "Choice [e/c/A]:" and "Choice [a/c/Q]:" lines, so the questions stand
apart from their option lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(uninstall): teardown-last execution order + 5 live-run bugs from LOG/Output

The first real Windows run surfaced a cluster of execution bugs. All fixed at
the root:

1. Teardown-last plan order (the user-prescribed sequence): tool binaries ->
   PATH -> "Shutdown (stopped last)" (daemon stop + broker service removal) ->
   data/cache/config (a running daemon holds handles inside them) -> "Runtime
   binaries (after shutdown)" (uffsd/uffs-broker/uffsmcp/uffs-mcp-http, whose
   images are locked while running) -> deferred self-delete of uffs.exe +
   uffs-update.exe. The working tools stay usable for the whole run.

2. One locked file no longer traps a whole directory: delete_binaries is now
   best-effort across the set (the original run lost 21 deletions to one
   lingering uffsd.exe), with a 750ms settle-and-retry pass, reporting exactly
   which files failed.

3. The daemon stop re-discovers the CURRENT daemon via the real
   `uffs --daemon kill` handler (pid file/socket) instead of the analyzed pid
   (stale after the deep sweep's coverage reload), then waits for IPC-down +
   image release before the runtime binaries are deleted.

4. Windows daemon-management elevation gate now mirrors the Unix owner gate:
   no elevation needed when there is no PID file (nothing to protect) or when
   the daemon's launch-state sidecar records a NON-elevated launch (the daemon
   now writes an "elevated" flag into daemon.state.json) — a user-level daemon
   is the user's to kill even with the broker gone. Falls back to the broker
   probe otherwise.

5. The deferred self-delete never actually deleted anything: std's Windows arg
   quoting backslash-escaped the `del "path"` quotes inside the `cmd /c`
   payload, which cmd.exe does not parse. Passed verbatim via raw_arg now.

6. Formatting: the coverage failure notes put "Continuing the deep sweep..."
   on its own line, and a blank separator precedes the `[sweep]` block (-v).

7. `uffs-broker --install` narrates its steps (sc create -> ok, sc start with
   a "can take a minute" note -> ok) instead of a silent minute-long wait.

plan.rs crossed the 800-LOC budget with the reorder; fixed by decomposing (the
established sibling-tests pattern): its unit tests moved to plan/tests.rs,
leaving plan.rs at 513 LOC. Tests updated to the new order contract plus a
regression test pinning the runtime-stem split. Validated with cargo clippy
(host) + cargo xwin clippy (Windows, prod + tests); 38 uninstall tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(deps): bump rmcp 2.1.0, aes-gcm 0.11.0, indicatif 0.18.6, rand 0.10.2 + full cargo-vet coverage

Version bumps in workspace Cargo.toml: rmcp 1.8.0 -> 2.1.0 (MCP
2025-11-25 spec), aes-gcm 0.10 -> 0.11.0, rand 0.10.1 -> 0.10.2,
indicatif 0.18.4 -> 0.18.6, plus the transitive lockfile fallout.

Supply-chain: 17 newly-unvetted crates cleared with real audits and
publisher trust, no exemption bumps. Ten RustCrypto crates (aead, aes,
cipher, cmov, cpubits, ctr, ctutils, ghash, polyval, universal-hash)
are covered by trusted-publisher entries for the RustCrypto GitHub org
accounts, consistent with the existing digest/hybrid-array precedent.
Six deltas were reviewed line-by-line and certified (notes in
supply-chain/audits.toml). cargo vet prune dropped the superseded
exemptions; cargo vet --locked passes.

Vet-Reviewed-Diff: rmcp@1.8.0->2.1.0
Vet-Reviewed-Diff: rmcp-macros@1.8.0->2.1.0
Vet-Reviewed-Diff: aes-gcm@0.10.3->0.11.0
Vet-Reviewed-Diff: rand@0.10.1->0.10.2
Vet-Reviewed-Diff: console@0.16.3->0.16.4
Vet-Reviewed-Diff: indicatif@0.18.4->0.18.6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(deps): migrate uffs-security to aes-gcm 0.11 and uffs-mcp to rmcp 2.x APIs

aes-gcm 0.10 -> 0.11 (crates/uffs-security/src/crypto.rs):
GenericArray is gone (hybrid-array now); AeadInPlace is deprecated in
aead 0.6, so move to AeadInOut: encrypt_in_place_detached ->
encrypt_inout_detached with InOutBuf, decrypt likewise. Tag slice
conversion is now fallible (try_into + bad_data) instead of the old
panicking from_slice. On-disk format unchanged; v1/v2 round-trip and
compatibility tests pass (49/49).

rmcp 1.8 -> 2.x (crates/uffs-mcp): model reorg renames.
Content -> ContentBlock, RawContent::resource_link(..).no_annotation()
-> ContentBlock::resource_link(..), RawResource/RawResourceTemplate ->
Resource/ResourceTemplate (annotations are plain Option fields now, so
.no_annotation() disappears), PromptMessageRole -> Role, and the
Annotated wrapper's .raw field access flattens away in tests. The
ResourceContents match in mcp_protocol.rs names BlobResourceContents
explicitly (wildcard_enum_match_arm). All 99 uffs-mcp tests pass.

Known leftover: rmcp 2.x deprecates model::Root (SEP-2577); roots.rs
carries a scoped #![expect(deprecated)] with reason until the Roots
migration follow-up on this branch.

lint-prod and lint-tests both pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bytes): clear the 13 anti-pattern-gate lossy-decode sites (first just-go since skyllc-ai#346)

The anti-pattern gate (a `just go`-lane check, not in pre-push/PR CI) flagged
13 from_utf8_lossy/from_utf16_lossy sites accumulated since its bytes rules
landed (2026-06-04, skyllc-ai#346) — a month of uninstall/update/broker code shipped
through lanes that never run it. Site-by-site treatment, no blanket waivers:

True fixes (bytes feed real decisions):
- uffs-mft platform/process.rs: image path via OsString::from_wide — lossless
  (Windows paths are arbitrary u16s), so path comparisons are exact.
- uninstall/sweep.rs ShmemBlob: strict from_utf8 — these paths feed the EXTRA
  delete list; a corrupt blob is rejected outright, never lossy-mangled toward
  a delete. (The daemon always emits valid UTF-8.)
- update/acquire.rs latest_version: strict from_utf8 — our own helper emits
  ASCII `latest=` lines; non-UTF-8 means something is wrong -> None.

AUDIT-OK(bytes) with per-site safety arguments (house style, cf. uffs-client):
- version probes (sweep.rs + binaries.rs, 4 sites): U+FFFD cannot fabricate an
  ASCII MAJOR.MINOR.PATCH token; lossy only fails toward "legacy"/None.
- broker sc_output (2): operator-facing diagnostic text, never parsed.
- procinfo (4): pgrep pids (lossy cannot create digits), Windows console tools
  emit OEM/ANSI bytes (strict parse would break localized systems), kernel
  cmdline + ps field bytes carry no encoding guarantee — best-effort
  display/probe text throughout.

Gate: ✅ no forbidden patterns. Full `just go` green (tests, doc-tests, prod +
test lint, dependency security, rustdoc links). cargo xwin clippy (Windows)
clean for the from_wide change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(mcp): confine the SEP-2577 Roots deprecation to one boundary hook

SEP-2577 deprecates the MCP Roots capability upstream with NO replacement API
(workspace context moves to explicit tool inputs); rmcp 2.x marks Root +
Peer::list_roots deprecated with removal planned. UFFS's roots -> NTFS-prefix
search scoping is real, working functionality that clients still exercise, so
dropping it now would regress scoping for Roots-advertising clients.

The for-good fix is architectural: make the feature survive the API's removal.

- roots.rs is now transport-agnostic: new AdvertisedRoot { uri, name } replaces
  every rmcp::model::Root reference (resolve_root, update_roots_state, all 11
  tests) — the module-level #![expect(deprecated)] is GONE.
- handler/mod.rs's on_roots_list_changed is the single remaining place that
  speaks the deprecated wire API: one scoped #[expect(deprecated)] around the
  list_roots call + the Root -> AdvertisedRoot conversion, with the exit plan
  in the comment: when rmcp removes the API, delete this hook — scoping then
  flows from the explicit path filters the search tools already accept.

Deprecation surface: whole module -> ~10 boundary lines with a documented
deletion path. 98 uffs-mcp tests pass; full `just go` green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(vet): burn down 13 exemptions with real audits (11 full + 2 delta)

First tranche of the exemption long-tail (332 -> 319; fully-audited 158 -> 171).
Every audit is a real source review — capability sweep (fs/net/process/env/
build.rs), every unsafe block read against its invariant, publisher records
verified on crates.io — with the findings in each audits.toml note:

Full audits (exemption removed for each):
- itoa 1.0.18            dtolnay     bounded table reads, ASCII-only utf8_unchecked
- ryu 1.0.23             dtolnay     24-byte buffer vs proven <=24-byte max write
- unicode-ident 1.0.24   dtolnay     trie get_unchecked covered by exhaustive char test
- semver 1.0.28          dtolnay     tagged-pointer small-string invariants verified,
                                     parser validates ASCII before every new_unchecked
- serde_path_to_error    dtolnay     zero unsafe, zero capability, pure serde adapter
  0.1.20
- scopeguard 1.2.0       Amanieu     ManuallyDrop/ptr::read pairs read-once-each
- same-file 1.0.6        BurntSushi  is_std Drop leaks std fds (no double-close)
- winapi-util 0.1.11     BurntSushi  every Win32 call: sized out-params + rc checks
- thiserror 1.0.69       dtolnay     build.rs = canonical $RUSTC feature probe (read fully)
- thiserror-impl 1.0.69  dtolnay     zero real unsafe, pure token transform
- async-trait 0.1.89     dtolnay     zero real unsafe, no unsafe emitted

Delta audits (the workspace also carries thiserror 2.x):
- thiserror 1.0.69 -> 2.0.18       no_std rework; build.rs writes a static-template
                                   OUT_DIR shim + rustc --version gate; no new capability
- thiserror-impl 1.0.69 -> 2.0.18  zero unsafe/capability in added lines

`cargo vet --locked` -> Vetting Succeeded. No trust-entry shortcuts: an earlier
blanket `cargo vet trust` pass was reverted in favour of these source audits.

Vet-Reviewed-Diff: thiserror@1.0.69->2.0.18
Vet-Reviewed-Diff: thiserror-impl@1.0.69->2.0.18

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.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