OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 commits / 9de49ea

0.1.251: an exact tie in the new ordering key was still per-host
Found in production within the hour 0.1.250 shipped. lastRefUpdate agreed to the MILLISECOND across both backends — proc-nodes and blender-bridge both read 1788551868967, because one batch publish stamps one timestamp on every ref it touches — and with no tiebreak the comparator returned 0, so Array.sort left them in INPUT order. That input is sync.repos, a Map in readdir order, which is per-host. 134 of 135 positions matched; those two were swapped. The replicated key is what made ties COMMON. Filesystem mtimes were near-unique by accident, so the missing tiebreak never showed; a batch publish is not an accident. Break on name, which is unique per daemon, so the order is total and host-independent. Verified live after deploying 0.1.250 to both backends: lastRefUpdate byte- identical across tabertron and stortron for every repo, and the page order differing at exactly this one tied pair. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9de49ea43a6fdc0027e7dec16e53fe7c948f812d
by plankatron <fleeky@gmail.com> · 2026-09-05T19:55:54.000Z

Changed files (3) +51 -2

lib/web/pages/repos.js +10 -1
……@@ -160,7 +160,16 @@ export function sortReposByRecency (repos) {
160160 const aHas = repoHasHistory(a)
161161 const bHas = repoHasHistory(b)
162162 if (aHas !== bHas) return aHas ? -1 : 1
163 return repoRecency(b) - repoRecency(a)
163 const byRecency = repoRecency(b) - repoRecency(a)
164 if (byRecency !== 0) return byRecency
165 // TOTAL ORDER, or the backends still disagree. An exact tie left the
166 // comparator returning 0, so Array.sort kept INPUT order — and the input is
167 // sync.repos, a Map in readdir order, which is per-host. Ties are common
168 // now rather than accidental: one batch publish stamps one timestamp on
169 // every ref it touches, so proc-nodes and blender-bridge both read
170 // 1788551868967 and were the only 2 of 135 positions that disagreed across
171 // the two backends the hour this key shipped. Names are unique per daemon.
172 return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
164173 })
165174}
166175
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pear-git-v3",
3 "version": "0.1.250",
3 "version": "0.1.251",
44 "description": "P2P git forge — multi-writer issue/org/release/peer-mirror daemon. Built bottom-up after the failed v2.",
55 "type": "module",
66 "main": "index.js",
test/repo-ref-activity.test.js +40 -0
……@@ -163,3 +163,43 @@ test('a corrupt or absent cache file is not fatal', async (t) => {
163163 recordRefActivity('alpha', 1)
164164 t.is(getRefActivity('alpha'), 1, 'still usable after a bad load')
165165})
166
167test('an EXACT tie orders identically on both backends', async (t) => {
168 // Found in production the hour 0.1.250 shipped. lastRefUpdate agreed to the
169 // millisecond across tabertron and stortron (1788551868967 for both
170 // proc-nodes and blender-bridge — one batch publish stamps one timestamp),
171 // and with no tiebreak the comparator returned 0, so Array.sort left them in
172 // INPUT order. That input is sync.repos, a Map in readdir order, which is
173 // per-host: 134 of 135 positions matched and those two were swapped.
174 //
175 // The replicated key made ties COMMON — mtimes were near-unique by accident,
176 // a batch publish is not. Names are unique per daemon, so tie-breaking on
177 // name makes the order total and host-independent.
178 const tied = (order) => order.map((name) => ({ name, branches: 1, lastRefUpdate: 1788551868967 }))
179
180 t.alike(
181 sortReposByRecency(tied(['proc-nodes', 'blender-bridge'])).map((r) => r.name),
182 sortReposByRecency(tied(['blender-bridge', 'proc-nodes'])).map((r) => r.name),
183 'the same tied set orders the same way whatever order it arrives in'
184 )
185 t.alike(sortReposByRecency(tied(['proc-nodes', 'blender-bridge'])).map((r) => r.name),
186 ['blender-bridge', 'proc-nodes'], 'tie broken by name')
187
188 // The tie is only reached when the recency is equal — it must never outrank
189 // the timestamp itself.
190 t.alike(
191 sortReposByRecency([
192 { name: 'aaa', branches: 1, lastRefUpdate: 1 },
193 { name: 'zzz', branches: 1, lastRefUpdate: 2 }
194 ]).map((r) => r.name),
195 ['zzz', 'aaa'],
196 'a newer repo still wins over an alphabetically earlier one'
197 )
198
199 // Repos with NO signal at all (both keys absent) tie at 0 — same rule.
200 t.alike(
201 sortReposByRecency([{ name: 'b', branches: 1 }, { name: 'a', branches: 1 }]).map((r) => r.name),
202 ['a', 'b'],
203 'repos with no signal at all are also deterministic'
204 )
205})