Task 8: The decision function — three-way truth table #8
Labels
No labels
data-integrity
engine
platform
procurement
remote
scaffold
ui
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/cairn-desktop#8
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Depends on Task 7. This is the heart of the engine.
Goal
A pure function that, given last-known / local / remote state for one path, returns the operation to perform. No I/O, no mutation — so every row of the truth table is a unit test.
Files
internal/sync/ops.go,internal/sync/decide.go,internal/sync/decide_test.goProduces
last == nilmeans we have never synced this path.The truth table
OpUploadOpDownloadOpUpdateStateOpConflictOpNoneOpUploadOpDownloadOpUpdateStateOpConflictOpDeleteRemoteOpDeleteLocalOpDownload— never deleteOpUpload— never deleteOpUpdateState(forget it)"local changed" means
local.Hash != last.ContentHash. "remote changed" meansremote.ETag != last.RemoteETag.The two bold rows are the ones that protect data: a delete racing a modification must never destroy the surviving copy.
The rule that prevents data loss
OrdersemanticsCreates and downloads parent-first (shallowest
Depthfirst); deletes child-first (deepest first); all non-deletes before deletes. Applying a create before its parent, or a delete before its children, is how sync engines corrupt trees. Usesort.SliceStable.Steps
ops.gothendecide.go.git commit -s -m "feat(sync): three-way decision function and operation ordering"Acceptance criteria
Decideperforms no I/O and mutates nothing.Done
What was built
ops.go:Op+ 9 constants,String(),Operation{Op,Path,From},Order()— copies input thensort.SliceStable(non-deletes parent-first, deletes child-first)decide.go:Side,sameContent(verbatim),knownEqual,Decide(last, local, remote, path...)— a pure function, no I/O, no mutationdecide_test.go: 14 named subtests, one per truth-table row, plusTestDecideNeverDeletesOnUnknownHash(verbatim) and an addedTestDecideUnknownValuesNeverMatchknownEqualtreats""as never matching (rather than literal!=), which is what AC2 and "never delete on ambiguity" require — the issue's literal wording is self-contradictory here and this is the safe resolutionTests
go vet ./...andgo test -count=1 ./...green across all 4 packagesAcceptance criteria
t.Runsubtests, issue's row orderOpDeleteLocal/OpDeleteRemoteboth gated onknownEqual; verbatim test plusTestDecideUnknownValuesNeverMatchDecideperforms no I/O and mutates nothing — met:Sideby value,lastonly read,decide.goimports onlyinternal/state, every subtest asserts*lastunchangedRulings
Ordersorts a copy of its inputCo-Authored-BytrailerOrdertests independently re-run againstops.go(not just traced) — all passDeferred
OpDeleteRemoteas ETag-gated (notremote.Hash-gated), for Plan 2's ETag-first comparisonknownEqualreads an empty recorded value as permanently "changed" (safe, but could affect #12's "second sync = zero ops" AC)Decide(nil, absent, absent)→OpNoneand extra variadicpathargs are unpinned but harmlessImplemented and reviewed by Claude (subagent-driven), landed on main after review and green CI.