/**
* ref-diff — compare a repo's ref-VIEW (the replicated autobase source of
* truth) against its bare repo on DISK, and name the disagreements.
*
* Pure: no I/O, no network. The `repo-refs` handler supplies both maps.
*
* WHY THIS EXISTS. Until 2026-08-08 nothing in pear-git-v3 could show the view's
* refs at all — `pgh repo branches` goes to `syncServer.getBranches()`, which
* reads DISK. That invisibility produced a concrete failure: a `release` ref was
* deleted on the origin and believed to have propagated fleet-wide, when for 3 of
* 5 repos the deletion silently never entered the view (`gipPublishRef` returns
* without a word when the tracker is not writable, or when the view never carried
* the ref) — while `ref-update-observed` still answered `ok: true`.
*
* The four states are the whole point; each one means something operationally:
*
* in-sync view and disk agree. Nothing to do.
* divergent both have it, at different oids. The view wins: RefReconciler
* rolls disk BACK to the view's oid (that is how a locally-accepted
* but globally-non-FF push gets undone).
* viewOnly the view carries it, disk does not. RefReconciler will materialize
* it — so deleting it from disk alone does NOT stick.
* diskOnly disk carries it, the view does not. THE RESURRECTION HAZARD:
* ref-publish-reconciler reads this as "disk is ahead" and
* re-announces, which undoes a deletion made elsewhere. It is also
* what an out-of-band ref (never propagated) looks like, so the two
* cases are indistinguishable from this side alone — which is
* exactly the ambiguity that follows from deleting a key rather than
* tombstoning it.
*/
/** @typedef {{ ref: string, view: string|null, disk: string|null, state: string }} RefRow */
export const IN_SYNC = 'in-sync'
export const DIVERGENT = 'divergent'
export const VIEW_ONLY = 'viewOnly'
export const DISK_ONLY = 'diskOnly'
/**
* @param {Map<string,string>|Iterable<[string,string]>} viewRefs ref -> oid
* @param {Map<string,string>|Iterable<[string,string]>} diskRefs ref -> oid
* @returns {RefRow[]} sorted by ref name
*/
export function classifyRefs (viewRefs, diskRefs) {
const view = viewRefs instanceof Map ? viewRefs : new Map(viewRefs || [])
const disk = diskRefs instanceof Map ? diskRefs : new Map(diskRefs || [])
const rows = []
for (const ref of new Set([...view.keys(), ...disk.keys()])) {
const v = view.get(ref) || null
const d = disk.get(ref) || null
let state
if (v && d) state = v === d ? IN_SYNC : DIVERGENT
else if (v) state = VIEW_ONLY
else state = DISK_ONLY
rows.push({ ref, view: v, disk: d, state })
}
return rows.sort((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0))
}
/**
* Rows worth an operator's attention — everything except in-sync.
* @param {RefRow[]} rows
*/
export function refDisagreements (rows) {
return rows.filter((r) => r.state !== IN_SYNC)
}