Finding #9: resolve included-file roots for Clean Stale Archives; conservative on any unproven attribution#17
Merged
Conversation
"Clean Stale Archives" collected only each profile's direct `root =` values, so a profile whose roots come from an included file contributed nothing: its live archive matched no profile, appeared as a local orphan, and could start CHECKED for deletion. Add pure `ProfileRootResolver` that resolves a profile's effective roots (and rootaliases) by following include/source recursively, replicating upstream prefs.ml semantics: - `include f`: fileInUnisonDir(f); exact file if it exists else `f.prf`; missing is fatal. `source f`: literal fileInUnisonDir(f), never `.prf`; missing fatal. `include?`/`source?`: missing file silently skipped. All tokens resolve relative to the Unison directory, including nested includes. - Conservative reliability: a required missing/unreadable file, a cycle, a malformed/`.raw` line (which Unison itself rejects), or exceeding the traversal bound makes resolution unreliable. - Cycle detection (recursion stack) + hard bounds (maxFiles/maxDepth) guarantee termination that upstream lacks. CleanStaleArchivesWindowController.profiles() now feeds resolved roots to the matcher and folds resolution.reliable into attributionReliable, so an archive whose roots live in an included file is attributed correctly and an unreliably-resolved profile's archives stay uncertain (never preselected). Tests: 18 ProfileRootResolver cases with scratch fixtures (direct/include/ source/exact-file/optional-missing/required-missing/nested/diamond/cycle/ subdir/escaped-space/malformed/rootalias) plus an injected reader for unreadable and unbounded-chain cases. No real archives touched. Pure-logic coverage only — not field-proven through the live Clean Stale Archives window. No C/bridge/vendor change; blob a57f5c4e, vendor/ and patches/ untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Safety-correction pass for PR #17 (included-root attribution). 1. Existing-but-unreadable target is no longer treated as absent, even when OPTIONAL. `include?`/`source?` skip only a proven-`.missing` file; an unreadable one is `.unreadable` -> unreliable. Unison reads profiles as bytes, so a file we fail to decode as UTF-8 may still carry roots we can't see; treating it as absent would misattribute an archive as an orphan. 2. filesystemRead classification made precise. A directory, or a non-regular object (FIFO/device/socket), is `.unreadable` and NOT `.missing`: its existence must suppress the `.prf` fallback for `include`, and a FIFO/device is classified via stat WITHOUT being opened (an open could block indefinitely). A leading UTF-8 BOM is stripped so the first line never mis-parses as `.raw`. 3. Enumeration failure propagates GLOBAL uncertainty. When the Unison directory can't be enumerated, the controller no longer returns an empty profile set (which would mark every archive a certain orphan and preselect the local-only ones for deletion). Every row becomes uncertain and none are preselected. 4. Uncertainty is surfaced. A new orange banner (pure, tested `attributionWarning`) explains an unreadable directory and names profiles with unresolved/unreadable includes; the per-row tooltip now lists the includes case too. 5. Tests: optional invalid-UTF-8 -> unreliable; exact-path directory alongside a valid <name>.prf -> no fallback + unreliable; FIFO -> unreadable without hanging; missing/regular classification; BOM parity; global-uncertainty forces no destructive preselect; warning-text content + no em-dash. Full suite 580 tests, 0 failures (1 pre-existing env skip). Debug build + Release compile both succeed. Swift-only; vendored blob a57f5c4e and vendor/ patches/ untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-root-attribution
bcourbage
marked this pull request as ready for review
July 20, 2026 19:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Finding #9 — "Clean Stale Archives" now attributes an archive correctly when a profile's roots come from an included file, and stays conservative (never preselecting a destructive cleanup) whenever attribution can't be proven.
Problem
Profile collection read only the direct
root =lines. A profile whose roots live in aninclude/sourcefile therefore looked rootless, so an archive it actively uses could be classified as a local orphan and start checked for deletion.Design
ProfileRootResolver(pure) resolves a profile's effective roots recursively throughinclude/source/include?/source?, replicating upstreamprefs.mlsemantics: resolved relative to the Unison directory;includeappends.prfonly when the exact file is absent;sourcenever appends;?variants skip a proven-absent file. It is conservative — any ambiguity it cannot prove past marks the resolutionreliable == false: a missing required target, an unreadable/unparseable target, a cycle, a malformed/.rawline Unison would itself reject, or exceeding a hard traversal bound (cycle detection +maxFiles/maxDepthguarantee termination that upstream lacks).CleanStaleArchivesWindowControllerfeeds the resolved roots to the matcher and foldsreliableintoattributionReliable, so an unreliably-resolved profile's archives stay uncertain and never start checked.Safety properties
include?/source?— it marks the resolution unreliable. Only a proven-.missingoptional is skipped.filesystemRead. A directory, or a non-regular object (FIFO/device/socket), classifies as.unreadable(not.missing): forincludeits existence correctly suppresses the.prffallback, and a FIFO/device is classified viastatwithout being opened (no blocking). A leading UTF-8 BOM is stripped so the first line never mis-parses.attributionWarning) explains an unreadable directory and names profiles with unresolved/unreadable includes; the per-row tooltip lists the includes case too.Coverage
Pure-logic tests: direct/nested/diamond/cyclic includes; required-vs-optional missing;
sourceno-.prf; relative-to-Unison-dir + escaped names; malformed.raw; rootalias surfaced; bounded traversal; optional invalid-UTF-8 → unreliable; exact-path directory alongside a valid<name>.prf→ no fallback + unreliable; FIFO →.unreadablewithout hanging; directory/missing/regular classification; BOM parity; global uncertainty forces no destructive preselect; warning-text content (no em-dash). Full suite green.Scope note
The enumeration-failure → no-preselect invariant is proven at the unit level via the pure
rowUncertain/defaultsToCheckedcomposition andattributionWarning; the controller's privateprofileScan()/window wiring is a straight-line application of those. End-to-end window behavior against a live.unisondirectory remains manual (no UI harness).Untouched
Swift + tests only.
vendor/,patches/, and the vendored blob (a57f5c4e) are untouched.