fsroot.Path.Rel() returns OS-native separators, making serialized documents and checksums platform-dependent #377
Description
fsroot.Path.Rel() returns OS-native separators, so the same logical path serializes as sub/file.txt on Linux and sub\file.txt on Windows. Since Path is a serialized type, that makes documents and their checksums platform-dependent.
Evidence
From the first three-platform CI run (PR #375, commit f8b28c82, job test (windows-latest)), six failures of this shape:
root_test.go: Rel() = "sub\\file.txt", want "sub/file.txt"
root_test.go: Rel() = "b\\c.txt", want "b/c.txt"
Mechanism
Path.Rel() returns the stored rel field (pkg/fsroot/root.go:544). That field is built with OS-native helpers:
pkg/fsroot/root.go:687—rel := assert.Must(filepath.Rel(rootName, path))pkg/fsroot/root.go:692—rel: filepath.Clean(path)
filepath.Rel and filepath.Clean both yield \ on Windows.
Why this is a defect rather than a test expectation
Path round-trips through serialization — pkg/fsroot/root.go:624 and pkg/fsroot/root.go:659 both assign p.rel = decoded.Rel when decoding. So rel is not merely an in-memory convenience; it is written into documents and read back.
The consequence is that a graph, trace, or receipt produced on Windows differs byte-for-byte from the identical logical result produced on Linux, and therefore checksums differ. This repository already treats that as unacceptable elsewhere and specifies its directory-digest Merkle root as forward-slash, NUL-delimited, and sorted precisely so the digest is platform-stable. Path.Rel() violates the same principle.
The six failing assertions are asserting the correct thing. The product is what is wrong.
Likely shape of the fix
Normalize to slash form at construction — filepath.ToSlash on the results at root.go:687 and root.go:692 — so rel is canonical regardless of host, converting back with filepath.FromSlash only where an OS call requires it. abs may legitimately stay OS-native since it is a local handle rather than a serialized identity; that distinction needs confirming rather than assuming, along with an audit of every consumer of Rel() and every existing serialized document.
Found by the platform test matrix (#373) on its first run. Classified bucket 1 — a genuine platform defect — under that plan's phase 2 triage.
Activity
fix(test): give the devlore-test binary its .exe suffix on Windows (#378
docs: the canonical-Rel plan and the corrected Windows baseline (#391)
fix(file): treat rooted patterns as absolute in Find's scope check; f…
fix(windows): slash-form logical paths and unix-scoped ownership tests (