Skip to content

refactor(uffs-mft): Frs / ParentFrs newtypes — parse layer (Phase 4 sub-phase 5d.1, refs #191)#259

Merged
githubrobbi merged 1 commit into
mainfrom
refactor/phase-4-5d1-frs-newtype-parse
May 16, 2026
Merged

refactor(uffs-mft): Frs / ParentFrs newtypes — parse layer (Phase 4 sub-phase 5d.1, refs #191)#259
githubrobbi merged 1 commit into
mainfrom
refactor/phase-4-5d1-frs-newtype-parse

Conversation

@githubrobbi
Copy link
Copy Markdown
Collaborator

Summary

Phase 4 sub-phase 5d.1 — first slice of the FRS migration along
the playbook's data-flow split (5d.1 parse → 5d.2 index → 5d.3 query
→ 5d.4 wire).

Introduces two typed newtypes that close the historic own-FRS vs
parent-FRS swap
hazard and the sentinel-meaning ambiguity
(frs == 0 meaning both "null reference" and "$MFT self-record")
without touching the on-disk or wire format.

Newtypes (crates/uffs-mft/src/frs.rs)

  • Frs(u64) — typed FRS value with documented sentinels
    Frs::ZERO / Frs::ROOT and predicates is_zero / is_root.
  • ParentFrs(Frs) — composed-over-Frs newtype for parent-directory
    references; the type system makes parent-vs-child swaps a compile
    error. Convert via ParentFrs::of(Frs) / ParentFrs::as_frs()
    at the explicit promotion / demotion sites.

Both are #[repr(transparent)] (byte-identical to u64) and derive
Copy + Eq + Ord + Hash + Default so they slot into HashMap /
BTreeMap keys and tracing fields.

Migrated parse-layer surface

Field Before After
NameInfo.parent_frs u64 ParentFrs
NameInfo.source_frs u64 Frs
ParsedRecord.frs u64 Frs
ParsedRecord.parent_frs u64 ParentFrs
ParsedRecord.base_frs u64 Frs
ExtensionAttributes.base_frs u64 Frs
ExtensionAttributes.extension_frs u64 Frs
PrimaryNameTracker.parent_frs u64 ParentFrs

The parse_record* (data, frs: u64) public input signatures stay
u64 because the FRS is derived arithmetically from chunk offsets in
the read loop; the typed surface is the output contract.

Boundary down-casts (5d.2 will erase these)

The index storage, Polars Vec<u64> columns, and the uffs-diag CLI
are still u64. Each crossing point uses an explicit .raw() /
Frs::new() lift with a citation comment:

  • index/builder.rs::from_parsed_records — single typed→raw demote
    at loop entry (parsed_frs, parsed_parent_frs, parsed_base_frs
    locals) with a doc-comment naming sub-phase 5d.2 as the eraser.
  • parse/columns.rs::push_record{,_expanded} — Polars-bound columns.
  • reader/dataframe_build.rs (4 sites) — same pattern.
  • parse/merger.rs (3 sites) — frs_to_usize / TryFrom<u64>
    index-arithmetic boundaries.
  • uffs-diag/src/bin/dump_mft_records.rs — wraps the CLI-parsed
    u64 for the merged-record lookup.

Wire format unchanged

Pinned by:

  • frs::tests::frs_raw_roundtrip_preserves_u64_exactly /
    parent_frs_raw_roundtrip_preserves_u64_exactly — any u64
    survives Frs::new.raw() byte-identically (covers 0, 1,
    5, 42, 1 << 47, u64::MAX).
  • frs::tests::frs_sentinels_match_literal_values /
    parent_frs_sentinels_match_literal_valuesZERO / ROOT are
    the documented 0 / 5 literals exactly.
  • frs::tests::parent_frs_of_and_as_frs_roundtrip — promotion /
    demotion across the FrsParentFrs boundary is bit-identical.
  • Updated parse::tests::* — every existing parse regression test
    compares against the typed surface (Frs::new(...), Frs::ROOT,
    ParentFrs::ROOT, etc.) preserving the prior numeric expectations.

Verification

  • cargo check --workspace --all-targets
  • cargo xwin check --target x86_64-pc-windows-msvc --workspace --all-targets
  • cargo test --workspace --lib --bins — 180 uffs-mft lib tests
    pass (was 169; +11 new Frs / ParentFrs regression tests).
  • just lint-pre-push — all 23 gates green (fmt, file-size,
    drift, vet, machete, typos, reuse, cargo-check, lint-ci,
    lint-prod, lint-tests, rustdoc, doc-tests, tests, smoke, deny,
    lint-ci-windows).

Out of scope (follow-up sub-phases)

  • index::types::FileRecord.{frs, base_frs},
    index::types::FILE_NAME.parent_frs,
    index::model::ChildEntry.child_frs, index::* lookup APIs — 5d.2.
  • Query / search surfaces — 5d.3.
  • USN / wire DTOs (UsnRecord, FileChange, broker-protocol rows) — 5d.4.

Refs #191

…(Phase 4 sub-phase 5d.1)

First slice of the FRS migration along the playbook's data-flow split
(5d.1 parse → 5d.2 index → 5d.3 query → 5d.4 wire).  Introduces a
typed parent-vs-child distinction so the historic `(parent_frs: u64,
own_frs: u64)` swap hazard becomes a compile error, and the documented
sentinels (`Frs::ZERO`, `Frs::ROOT`, `ParentFrs::ZERO`,
`ParentFrs::ROOT`) replace the open-coded `0` / `5` magic numbers at
construction sites.

## Newtypes

* `uffs_mft::frs::Frs(u64)` — typed FRS value with `ZERO` (the `$MFT`
  self-record / "null reference") and `ROOT` (FRS 5 — `.` root) plus
  `is_zero` / `is_root` predicates and the standard `new` / `raw` /
  `Display` surface.  Derived `Copy + Eq + Ord + Hash + Default`.
* `uffs_mft::frs::ParentFrs(Frs)` — composed-over-`Frs` newtype for
  parent-directory references.  Provides `ParentFrs::of(Frs)` and
  `ParentFrs::as_frs()` so the parent-vs-child boundary becomes
  explicit at every promotion / demotion site.

Both newtypes are `#[repr(transparent)]` so they preserve the raw
`u64` bit pattern byte-identically for wire-format round-trip.

## Migrated parse-layer fields

* `ntfs::NameInfo.parent_frs: u64` → `ParentFrs`
* `ntfs::NameInfo.source_frs: u64` → `Frs`
* `parse::ParsedRecord.frs / parent_frs / base_frs` → `Frs` /
  `ParentFrs` / `Frs`
* `parse::ExtensionAttributes.base_frs / extension_frs` → `Frs` / `Frs`
* `parse::name_tracker::PrimaryNameTracker.parent_frs: u64` →
  `ParentFrs` (mirrors the migrated `NameInfo`).

## Construction sites (parse internals)

`parse/attribute_helpers.rs::parse_file_name_full`,
`parse/full.rs::parse_record_full`,
`parse/placeholders.rs::create_placeholder_record`,
`parse/forensic.rs` (corrupt-record path) and
`parse/forensic/{base,extension}.rs` now lift kernel-decoded `u64`
values into `Frs::new(...)` / `ParentFrs::new(...)` at the on-disk →
typed boundary, with citation comments at each lift point.  The
`parse_record* (data, frs: u64)` public input signatures stay `u64`
because FRS values are derived arithmetically from chunk offsets in
the read loop; the typed surface is the *output* contract.

`parse/merger.rs` calls `.raw()` at the four `frs_to_usize` /
`TryFrom<u64>` index-arithmetic boundaries.

## Boundary down-casts at consumers (5d.2 will erase these)

* `parse/columns.rs::push_record{,_expanded}` — `ParsedColumns`
  Polars-bound `Vec<u64>` columns downcast at the push site.
* `reader/dataframe_build.rs` (4 sites) — same Polars-bound `Vec<u64>`
  pattern.
* `index/builder.rs::from_parsed_records` — single typed→raw boundary
  at loop entry (`parsed_frs`, `parsed_parent_frs`, `parsed_base_frs`
  locals) with a doc-comment citing the 5d.2 follow-up; per-name
  `.raw()` at the inner parent-walker site.
* `uffs-diag/src/bin/dump_mft_records.rs` — wraps the CLI-parsed `u64`
  argument with `Frs::new(...)` at the merged-record lookup.

## Wire format unchanged

Pinned by:

* `frs::tests::frs_raw_roundtrip_preserves_u64_exactly` /
  `parent_frs_raw_roundtrip_preserves_u64_exactly` — any `u64`
  survives `Frs::new` → `.raw()` byte-identically (covers `0`, `1`,
  `5`, `42`, `1 << 47`, `u64::MAX`).
* `frs::tests::frs_sentinels_match_literal_values` /
  `parent_frs_sentinels_match_literal_values` — `ZERO` / `ROOT` match
  the documented `0` / `5` literals exactly.
* `frs::tests::parent_frs_of_and_as_frs_roundtrip` — promotion /
  demotion across the `Frs` ↔ `ParentFrs` boundary is bit-identical.
* Updated `parse::tests::*` assertions — every existing parse
  regression test now compares against `Frs::new(...)` /
  `Frs::ROOT` / `ParentFrs::ROOT` etc., preserving the prior
  numeric expectations.

## Verification

* `cargo check --workspace --all-targets` ✅
* `cargo xwin check --target x86_64-pc-windows-msvc --workspace
  --all-targets` ✅
* `cargo test --workspace --lib --bins` — 180 uffs-mft lib tests pass
  (was 169; +11 new `Frs` / `ParentFrs` regression tests).
* `just lint-pre-push` — all 23 gates green (fmt, file-size, drift,
  vet, machete, typos, reuse, cargo-check, lint-ci, lint-prod,
  lint-tests, rustdoc, doc-tests, tests, smoke, deny,
  lint-ci-windows).

## Out of scope (follow-up sub-phases)

* `index::types::FileRecord.{frs, base_frs}`,
  `index::types::FILE_NAME.parent_frs`,
  `index::model::ChildEntry.child_frs`, `index::*` lookup APIs
  (`find`, `get_or_create`, `frs_to_idx_opt`, ...) — **5d.2**.
* Query / search surfaces (`uffs-core::index_search::result.frs`,
  ...) — **5d.3**.
* USN / wire DTOs (`uffs_mft::usn::UsnRecord.{frs, parent_frs}`,
  `uffs_mft::usn::FileChange.{frs, parent_frs}`, broker-protocol
  result rows) — **5d.4**.

Refs #191
@githubrobbi githubrobbi enabled auto-merge (squash) May 16, 2026 12:33
@githubrobbi githubrobbi merged commit ffa81b0 into main May 16, 2026
20 checks passed
@githubrobbi githubrobbi deleted the refactor/phase-4-5d1-frs-newtype-parse branch May 16, 2026 12:47
githubrobbi added a commit that referenced this pull request May 16, 2026
….4) (#261)

* refactor(uffs-mft): migrate index layer to Frs / ParentFrs newtypes — Phase 4 sub-phase 5d.2

Second slice of the FRS migration along the playbook's data-flow split
(5d.1 parse → 5d.2 index → 5d.3 query → 5d.4 wire).  Pushes the typed
`Frs` / `ParentFrs` newtypes introduced in #259 across the index
layer's storage types, public methods, and every cross-crate consumer
so the parent-vs-child distinction is now type-checked end-to-end from
the on-disk parse boundary through every record-construction,
record-lookup, and record-mutation site.

## Migrated index storage fields

* `index::types::FileRecord.frs: u64` → `Frs`
* `index::types::FileRecord.base_frs: u64` → `Frs`
* `index::types::LinkInfo.parent_frs: u64` → `ParentFrs`

All three remain `#[repr(transparent)]` over `u64`, so on-disk layout
stays byte-identical (Pod / Zeroable derives unchanged; `bytemuck`
contract preserved for memcpy serialise / deserialise).

## Migrated public method signatures

`index/base.rs` (the primary lookup surface):

* `FileRecord::new(frs: u64)` → `FileRecord::new(frs: Frs)`
* `FileRecord::new_unified(frs: u64)` → `(frs: Frs)`
* `MftIndex::get_or_create(&mut self, frs: u64)` → `(frs: Frs)`
* `MftIndex::get_or_create_unified(&mut self, frs: u64)` → `(frs: Frs)`
* `MftIndex::ensure_record(&mut self, frs: u64)` → `(frs: Frs)`
* `MftIndex::find(&self, frs: u64)` → `(frs: Frs)`
* `MftIndex::frs_to_idx_opt(&self, frs: u64)` → `(frs: Frs)`

Other index methods:

* `MftIndex::build_path(&self, frs: u64)` → `(frs: Frs)` (paths.rs)
* `MftIndex::add_child_entry(parent_frs: u64, child_frs: u64, name_index)`
  → `(parent_frs: ParentFrs, child_frs: Frs, name_index)` (child_order.rs)
* `PathResolver::is_valid(&self, frs: u64)` → `(frs: Frs)`
* `PathResolver::is_illegal(&self, frs: u64)` → `(frs: Frs)`
* `IllegalRefs::is_valid(&self, index: &MftIndex, frs: u64)` → `(frs: Frs)`
* `path_resolver::PathCache::get(&self, frs: u64)` → `(frs: Frs)`

## Construction-site lifts (raw → typed)

Every place inside `parse/`, `io/parser/`, `index/{builder,merge,model,
fragment,base,child_order,paths,tree,storage}.rs` and the index
helpers in `parse/index_helpers.rs` that previously stored a raw `u64`
FRS now lifts at the on-disk → typed boundary, with a citation comment
naming the upstream source (parser-emitted `Frs` value, kernel-decoded
`u64` chunk offset, or sentinel literal).

The `ROOT_FRS: u64 = 5` constant stays a raw `u64` (DTO surface for
external consumers — wire format, fixtures); internal call sites lift
via `ROOT_FRS.into()` / `Into::into(ROOT_FRS)` at the typed
boundary.  `Frs::ROOT` and `ParentFrs::ROOT` remain the preferred
construction path for new code per the 5d.1 contract.

## Cross-crate caller updates

* `uffs-core::compact` — `build_compact_index` walks records via the
  typed `record.frs` and lifts at every `frs_to_idx`-indexing site;
  the closure that resolves a parent FRS to a compact-record index is
  now factored out into `resolve_parent_compact_idx(index, parent_frs:
  ParentFrs, own_frs: Frs)` so the typed signature is enforced at
  every call site (own↔parent swap = compile error) AND so
  `build_compact_index` stays under `clippy::too_many_lines`.
* `uffs-core::aggregate::duplicates` — Pod-fixture builders use
  `(100 + i).into()` / `ROOT_FRS.into()` at construction.
* `uffs-core::index_search::result` + cross-crate test fixtures
  (`uffs-core::compact_tests`, `uffs-core::index_search::tests`,
  `uffs-core::search::*::tests`, `uffs-core::aggregate::integration_tests`)
  now construct typed `Frs` / `ParentFrs` values.
* `uffs-daemon::index::tests::build_test_drive*` — three synthetic
  drive builders updated to the typed surface (rebuilt from scratch
  with the new newtypes).
* `uffs-diag::bin::dump_mft_records` — diagnostic binary lifts at the
  CLI-input boundary.

## Pod-layout test endianness fix

`crates/uffs-mft/src/frs.rs` — the `frs_is_pod_zeroable_with_u64_layout`
and `parent_frs_is_pod_zeroable_with_u64_layout` regression tests
previously compared `bytemuck::bytes_of(&Frs::new(…))` against
`u64::to_ne_bytes(…)`.  Clippy's `host_endian_bytes` lint forbids
`to_ne_bytes` because it leaks host-endian semantics; switched to
`bytemuck::bytes_of(&raw)` which is endianness-agnostic by
construction (we only assert layout parity, not a specific endianness).

## Test-fixture migration

All `index/tests_*.rs` (`tests_core`, `tests_children`, `tests_extensions`,
`tests_helpers`, `tests_merge`, `tests_perf`, `tests_tree`, `tests_ads`),
`parse/direct_index_extension_tests`, and the cross-crate fixtures
listed above lift at construction.  Where a test owns a typed
`child_frs: Frs` and constructs a `ChildInfo { child_frs, … }`, the
shorthand expands to `child_frs: child_frs.into()` to bridge raw u64
test-locals into the typed field; conversely, where the field is
already typed and the helper takes `Frs` (e.g. `frs_to_idx_opt`), the
redundant `.into()` is dropped to satisfy `clippy::useless_conversion`.

Three `: ParentFrs` annotations on `let no_entry_parent = ParentFrs::new(…)`
in `index/{builder,child_order,paths}.rs` are dropped per
`clippy::redundant_type_annotations` (type is clearly inferable from the
constructor).

## Verification

* `cargo fmt --all --check` — clean
* `cargo clippy --workspace --all-targets --all-features --no-deps -- -D warnings` — clean
* `just lint-prod` (pedantic + nursery + cargo, libs/bins) — clean
* `just lint-tests` (pedantic + nursery + cargo, tests, unwrap/expect allowed) — clean
* `cargo nextest run --workspace --all-features` — 1793 / 1793 pass (14 skipped, all pre-existing)
* `cargo nextest run --workspace --profile pre-push-smoke` — 1608 / 1608 pass
* `cargo test --doc --workspace --all-features` (with `RUSTDOCFLAGS=-Dwarnings`) — clean
* `RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --all-features --no-deps` — clean
* `cargo deny check` — clean
* `just lint-ci-windows` (cross-platform Windows clippy via cargo-xwin) — clean
* `just lint-pre-push` — all 26 gates green in 101 s

## Behavior preservation

No DTO / wire-format / on-disk layout changes.  `Frs` and `ParentFrs`
are `#[repr(transparent)]` over `u64`; `bytemuck::bytes_of(&Frs::new(x))`
== `bytemuck::bytes_of(&x)` (regression test enforces this in
`frs.rs`).  The `MftStats::max_frs: u64` DTO field stays raw; the
internal max-tracking loop in `index/base.rs::compute_stats` now lifts
to `record.frs.raw()` once per record at the comparison boundary.

Refs #191 (FRS newtype migration umbrella).

* refactor(uffs-mft): migrate journal/USN layer to Frs / ParentFrs newtypes — Phase 4 sub-phase 5d.3

Third slice of the FRS migration along the playbook's data-flow split
(5d.1 parse → 5d.2 index → **5d.3 query/journal** → 5d.4 wire).
Pushes the typed `Frs` / `ParentFrs` newtypes introduced in #259
through the USN-journal DTO surface and the surgical-patch pipeline
that consumes it, so the kernel-decoded → typed lift sits at exactly
one point (`usn::windows::read_usn_journal`) and every downstream
consumer (`apply_usn_deletes`, `apply_usn_patch`, the daemon's
`PatchSink` / `IndexManager` glue, every test fixture in the journal
cluster) operates on typed values.

## Migrated DTO fields

* `uffs_mft::usn::UsnRecord.frs: u64` → `Frs`
* `uffs_mft::usn::UsnRecord.parent_frs: u64` → `ParentFrs`
* `uffs_mft::usn::FileChange.frs: u64` → `Frs`
* `uffs_mft::usn::FileChange.parent_frs: u64` → `ParentFrs`

Both DTOs are `#[derive(Debug, Clone[, Default])]` only — no serde, no
Pod, no on-disk / on-wire layout (the wire-side path goes through
`compact_storage` / `aggregate_wire`, not these in-process DTOs).
`Frs` and `ParentFrs` are `#[repr(transparent)]` over `u64` so any
future memcpy serialiser stays bit-identical.

## Migrated function signature

* `uffs_mft::usn::aggregate_changes(records: &[UsnRecord])` —
  return type `HashMap<u64, FileChange>` → `HashMap<Frs, FileChange>`.

`Frs` already derives `Hash + Eq + Copy + Default` (per 5d.1) so the
hash-map key swap is bit-identical to the raw `u64` key it replaces.
Every existing caller (`uffs_daemon::cache::journal_loop::sources`
chains `.into_values().collect()` so the key type is dropped at the
boundary) compiles unchanged.

## Construction-site lifts (raw → typed)

* `uffs_mft::usn::windows::read_usn_journal` — the single on-disk →
  typed boundary.  NTFS file references are 64-bit values whose low 48
  bits encode the FRS (the high 16 bits are the sequence number,
  which downstream consumers don't need).  The mask is unchanged;
  the lift `Frs::new(masked) / ParentFrs::new(masked)` now happens
  inline at the `all_records.push(UsnRecord { … })` site, with a
  citation comment naming the parser-boundary contract.

## Typed → raw demotions (single-point lift)

* `uffs_mft::index::MftIndex::apply_usn_deletes` — `change.frs.raw()`
  lifted once per change because (a) the `frs_to_idx` lookup table is
  `Vec<u32>` keyed by `usize`, and (b) the returned `frs_to_read:
  Vec<u64>` feeds the kernel-loop arithmetic input of
  `read_targeted_frs_records(&[u64])` (which stays raw per the 5d.1
  parser-boundary policy).  Single lift, citation-commented.
* `uffs_core::compact_loader::apply_usn_patch` — three lift sites at
  the `frs_to_compact` CSR-lookup boundary (one for `change.frs`, two
  for `change.parent_frs` on the create and rename branches).  Each
  documents that the CSR table is `Vec<u32>` indexed by `usize` so
  the demotion is a single `.raw()` call per change.

## Test-fixture migration

* `uffs_core::compact_loader_tests` — 20 `FileChange { frs: N, … }`
  and `parent_frs: N` literals lifted via `N_u64.into()`.  No test
  semantics changed; the integer literals stay in the source for
  readability and the lift happens at the field-construction
  boundary.
* `uffs_daemon::cache::journal_sink` (test module) — `make_change(frs:
  u64)` now lifts via `frs.into()` at the single construction site,
  and the three snapshot helpers (`pending_frs_for_letter`,
  `c_changes.iter().map(…)`, `d_changes.iter().map(…)`) demote via
  `change.frs.raw()` so assertion literals stay as integer arrays
  (`[100, 101]`, `[1, 2, 3, 4]`, etc.).  Two new doc-comment
  paragraphs name the construction / snapshot boundaries.
* `uffs_daemon::cache::journal_loop::tests` — `one_change(frs: u64)`
  helper lifts via `frs.into()` at construction.
* `uffs_daemon::cache::shard::tests` — two `FileChange { frs: 10, …
  }` fixtures lifted via `10_u64.into()`.

## Boundary policy preserved

These typed fields stay **raw `u64`** by deliberate design — they
either represent kernel-loop arithmetic inputs or DTO surface
contracts that are documented as wire/columnar parity points:

* `MftError::RecordRead { frs: u64 }` and `MftError::InvalidRecord(u64)`
  — constructed inside `io/readers/basic.rs` from the raw `frs: u64`
  parameter that the kernel-loop arithmetic derives from chunk offsets
  (per 5d.1 boundary policy).
* `index::MftStats::max_frs: u64` — DTO surface (per 5d.2 commit
  message; the internal max-tracking loop lifts to `record.frs.raw()`
  at the comparison boundary).
* `index_search::result::SearchResult.frs / .parent_frs: u64` —
  consumer-facing DTO with documented "wire-format / DataFrame parity"
  semantics; deferred to 5d.4 (wire) or a deliberate non-migration.
* `path_resolver::fast::FastPathResolver` — every public method takes a
  `polars::DataFrame` with `frs: u64` columns or a single `frs: u64`
  parameter that comes from a polars column; the polars column IS the
  wire boundary by definition.  Migration deferred.
* `io/parser/*.rs::parse_record_to_index(_, frs: u64, _)` and
  friends — kernel-loop arithmetic input (per 5d.1 contract).
* `read_targeted_frs_records(_, _, frs_list: &[u64])` — same.

## Verification

* `cargo fmt --all -- --check` — clean
* `cargo check --workspace --all-targets --all-features` — clean
* `cargo clippy --workspace --all-targets --all-features --no-deps -- -D warnings` — clean
* `cargo nextest run --workspace --all-features --no-fail-fast` — 1793 / 1793 pass (14 skipped, pre-existing)
* `just lint-pre-push` — all 23 active gates green in 77 s
  (`fmt`, `file-size`, `gates/hooks/workflow/fast/manifest-drift`,
   `commit-subjects`, `vet`, `vet-audit-discipline`, `machete`, `typos`,
   `reuse`, `cargo-check`, `lint-ci`, `lint-prod`, `lint-tests`,
   `rustdoc`, `doc-tests`, `tests`, `smoke`, `deny`, `lint-ci-windows`)

## Behavior preservation

No on-disk, on-wire, or columnar layout changes.  `UsnRecord` and
`FileChange` are in-process DTOs without serde / Pod derives;
swapping their FRS fields from `u64` to `Frs` / `ParentFrs` is a
zero-cost source-level rename because both newtypes are
`#[repr(transparent)]` over `u64`.  `aggregate_changes`'s hash-map
key type swap is bit-identical because `Frs` forwards `Hash` and `Eq`
to the underlying `u64`.

Refs #191 (FRS newtype migration umbrella).

* refactor(uffs-core): migrate SearchResult to Frs / ParentFrs newtypes — Phase 4 sub-phase 5d.4

Fourth and final slice of the FRS migration along the playbook's
data-flow split (5d.1 parse → 5d.2 index → 5d.3 query/journal →
**5d.4 wire**).  Promotes the consumer-facing search-result DTO to
typed FRS values and formalises the deliberate raw-`u64`
wire-boundary policy at the two remaining polars / DataFrame edges
with module-level doc comments.

## Migrated DTO fields

* `uffs_core::index_search::result::SearchResult.frs: u64` → `Frs`
* `uffs_core::index_search::result::SearchResult.parent_frs: u64`
  → `ParentFrs`

Both newtypes are `#[repr(transparent)]` over `u64` and derive
`Copy + Clone + PartialEq + Eq + PartialOrd + Ord + Hash + Default`
plus `bytemuck::Pod + Zeroable`, so the field swap is bit-identical
in memory and transparent to every existing usage: `Display`
forwards to the inner `u64`, tuple `sort()` / `assert_eq!` keep
working, and any caller that ultimately demands a raw `u64` (CSV,
JSON, polars columnar output) does a `.raw()` or `u64::from(...)`
at its own final-wire site.

## Construction-site simplifications

* `SearchResult::from_record` and `SearchResult::from_expanded` no
  longer perform the `.raw()` demotion at the construction
  boundary.  The typed [`FileRecord.frs`] / [`NameInfo.parent_frs`]
  values (typed since 5d.1 / 5d.2) flow through unchanged into the
  typed `SearchResult` fields — one fewer lift in the hot expansion
  path, and the boundary citation comment now reads "typed FRS
  values flow through unchanged" instead of the previous
  "demote at this construction boundary only" wording.

## Formalised wire-boundary policy (deliberate raw `u64`)

Two module-level doc comments document the surfaces that remain
deliberately raw because the polars column / columnar staging is
**itself** the FRS wire boundary by Phase-4 doctrine.  These were
already raw-by-design — this commit elevates the inline policy
comments to module docs so the contract is discoverable from the
crate docs and not just from individual lift sites.

* `uffs_core::path_resolver::fast` — every public method on
  `FastPathResolver` takes either a `uffs_polars::DataFrame` with
  `frs: u64` / `parent_frs: u64` columns or a single `frs: u64`
  parameter read out of one of those columns.  The internal
  `FastEntry.parent_frs`, `FastPathResolver::path_cache` key, and
  `FastPathResolver::max_frs` field all stay raw `u64`: they live
  one row-loop downstream of the polars lift, and lifting them
  into the typed domain only to immediately demote on the
  lookup-by-`u64` path would add ceremony without type-safety
  win.  Typed callers demote via `frs.raw()` at the `resolve` /
  `resolve_cached` boundary.

* `uffs_mft::parse::columns` — the `ParsedColumns.frs: Vec<u64>` /
  `parent_frs: Vec<u64>` staging buffers feed
  `reader::dataframe_build` and ultimately become
  `polars::Series::new("frs", _)`.  The canonical lift site is
  `ParsedColumns::push_record`, which does
  `self.frs.push(record.frs.raw())` — single demotion, one
  citation comment.

## Non-migrated surfaces (audit closure)

Phase 5d.4 also closes the audit on the wire layer by confirming
the following surfaces have no FRS sites that need touching:

* `uffs_client::protocol::aggregate_wire` — the JSON-serialisable
  aggregation-wire family (`AggregateSpecWire`, `BucketWire`,
  `StatsWire`, `DrilldownWire`, `SampleRowWire`,
  `AggregateResultWire`) carries no FRS fields.  The playbook's
  step-5 note about "add `impl From<Frs> for u64` only at the
  `aggregate_wire` boundary" is moot — there is no boundary
  there because the wire schema doesn't transmit FRS.
* `uffs_broker_protocol` — broker control protocol, no FRS.
* `uffs_core::compact_storage` — already memcpy-serialised via
  `bytemuck::Pod` over `Frs` / `ParentFrs`; on-disk layout is
  bit-identical to the pre-newtype representation.

## Verification

* `cargo fmt --all -- --check` — clean
* `cargo check --workspace --all-targets --all-features` — clean
* `cargo clippy --workspace --all-targets --all-features --no-deps -- -D warnings` — clean
* `RUSTDOCFLAGS='-Dwarnings -Drustdoc::broken_intra_doc_links' \
   cargo doc -p uffs-core -p uffs-mft --all-features --no-deps` — clean
* `cargo nextest run --workspace --all-features --no-fail-fast` —
  1793 / 1793 pass (14 skipped, pre-existing)
* `just lint-pre-push` — all 23 active gates green in 103 s

## Behavior preservation

Zero on-disk, on-wire, or columnar layout changes:

* `SearchResult` was never serde-derived; it's a process-local DTO
  consumed by the search dispatch, the daemon's response builders,
  and the index_search test suite.  Field-type rename is a
  source-level refactor only.
* `Frs` and `ParentFrs` are `#[repr(transparent)]` over `u64` and
  forward `Display`, `Hash`, `Eq`, `PartialOrd`, `Ord`.  The only
  test in this crate that reads `result.frs` collects it into a
  tuple, sorts the tuple vec, and `assert_eq!`s against another
  tuple vec built the same way — both sides switch to `Frs` in
  lockstep, comparison stays bit-identical to the prior `u64`
  version.
* The two doc-comment-only changes (`path_resolver::fast`,
  `parse::columns`) add no code; they elevate the inline
  boundary-citation comments already present at individual lift
  sites to module-level discoverability.

## Phase 5d closure

This commit closes the FRS-newtype migration arc:

    5d.1 parse   — Frs, ParentFrs newtypes + parse-layer plumbing  (#259)
    5d.2 index   — MftIndex, FileRecord, LinkInfo, ChildInfo       (64be6ce)
    5d.3 journal — UsnRecord, FileChange, USN apply pipeline       (fb822a9)
    5d.4 wire    — SearchResult + formalised raw-u64 boundary docs (this commit)

Every typed-FRS site now has a single documented lift point at the
parser / polars boundary, and every remaining raw-`u64` site is
either kernel-loop arithmetic input or a polars / DataFrame column
that is the wire boundary itself.

Refs #191 (FRS newtype migration umbrella).
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