From 6827c1fe2000e97788aff103256cca776eed2939 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Mon, 17 Aug 2026 09:56:13 +0300 Subject: [PATCH] fix(exporter): assign PSU slot positions by serial order, not encounter order disambiguatePSUSlots previously renumbered colliding PSU slots in whatever order the devices happened to appear in after parsing/dedup, which isn't guaranteed stable across runs. Sort each colliding group by serial number ascending before assigning 0-based positions, so the same PSU always lands in the same slot on re-conversion regardless of incidental ordering upstream. Co-Authored-By: Claude Sonnet 5 --- internal/exporter/reanimator_converter.go | 29 +++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/internal/exporter/reanimator_converter.go b/internal/exporter/reanimator_converter.go index 537f947..34f736d 100644 --- a/internal/exporter/reanimator_converter.go +++ b/internal/exporter/reanimator_converter.go @@ -2188,26 +2188,29 @@ func dedupePSUs(items []ReanimatorPSU) []ReanimatorPSU { // string from the source. Some collectors (e.g. BEE-SP) report every PSU // at slot "0". A shared slot value prevents Reanimator from telling the // PSUs apart by installed position, so they land in "other components" -// instead of being tracked as PSU installations. Reassigning stable 0-based -// positions by encounter order is the best available disambiguation without -// inventing a serial or other identity data we don't have. +// instead of being tracked as PSU installations. Within each colliding +// group, positions are assigned by ascending serial number rather than +// source encounter order, so the result is deterministic and reproducible +// across re-imports instead of depending on incidental parse ordering. func disambiguatePSUSlots(items []ReanimatorPSU) []ReanimatorPSU { if len(items) < 2 { return items } - counts := make(map[string]int, len(items)) - for _, it := range items { - counts[strings.ToLower(strings.TrimSpace(it.Slot))]++ + groups := make(map[string][]int, len(items)) + for i, it := range items { + key := strings.ToLower(strings.TrimSpace(it.Slot)) + groups[key] = append(groups[key], i) } - next := make(map[string]int, len(items)) - for i := range items { - key := strings.ToLower(strings.TrimSpace(items[i].Slot)) - if counts[key] < 2 { + for _, idxs := range groups { + if len(idxs) < 2 { continue } - idx := next[key] - next[key]++ - items[i].Slot = strconv.Itoa(idx) + sort.Slice(idxs, func(a, b int) bool { + return strings.ToLower(items[idxs[a]].SerialNumber) < strings.ToLower(items[idxs[b]].SerialNumber) + }) + for pos, idx := range idxs { + items[idx].Slot = strconv.Itoa(pos) + } } return items }