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 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-17 09:56:13 +03:00
co-authored by Claude Sonnet 5
parent a590242382
commit 6827c1fe20
+16 -13
View File
@@ -2188,26 +2188,29 @@ func dedupePSUs(items []ReanimatorPSU) []ReanimatorPSU {
// string from the source. Some collectors (e.g. BEE-SP) report every PSU // 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 // at slot "0". A shared slot value prevents Reanimator from telling the
// PSUs apart by installed position, so they land in "other components" // PSUs apart by installed position, so they land in "other components"
// instead of being tracked as PSU installations. Reassigning stable 0-based // instead of being tracked as PSU installations. Within each colliding
// positions by encounter order is the best available disambiguation without // group, positions are assigned by ascending serial number rather than
// inventing a serial or other identity data we don't have. // 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 { func disambiguatePSUSlots(items []ReanimatorPSU) []ReanimatorPSU {
if len(items) < 2 { if len(items) < 2 {
return items return items
} }
counts := make(map[string]int, len(items)) groups := make(map[string][]int, len(items))
for _, it := range items { for i, it := range items {
counts[strings.ToLower(strings.TrimSpace(it.Slot))]++ key := strings.ToLower(strings.TrimSpace(it.Slot))
groups[key] = append(groups[key], i)
} }
next := make(map[string]int, len(items)) for _, idxs := range groups {
for i := range items { if len(idxs) < 2 {
key := strings.ToLower(strings.TrimSpace(items[i].Slot))
if counts[key] < 2 {
continue continue
} }
idx := next[key] sort.Slice(idxs, func(a, b int) bool {
next[key]++ return strings.ToLower(items[idxs[a]].SerialNumber) < strings.ToLower(items[idxs[b]].SerialNumber)
items[i].Slot = strconv.Itoa(idx) })
for pos, idx := range idxs {
items[idx].Slot = strconv.Itoa(pos)
}
} }
return items return items
} }