fix(analyzer): deterministic ordering -- stable sorts with total-order tie-breaks
The bug
The analyzer produced different output on identical input across runs. Several output-affecting orderings lacked a total order:
sort.Sliceis not stable.- Two percentile sorts (normalizer, influence) tie-broke only on the primary key, then assigned percentile rank as the sorted index, so tied users landed at different ranks.
- Two hand-rolled sorts (summary top-gaps, team archetypes) had no tie-break.
connections.jsonwas emitted straight from map iteration (randomized) with no sort at all.
Tied elements permuted run to run, cascading into percentile ranks, archetype distributions, team gaps, and the full connection edge list. This polluted the momentum/trend feature (which diffs runs) and made results irreproducible.
Proof on main: analyze the seed-42 fixture twice and diff the outputs (ignoring the collected_at timestamp) differs by 55,238 lines, dominated by connections.json (53,794 lines).
The fix
Give each output-affecting ordering a unique tie-break key so output is byte-identical on identical input:
| File | Ordering | Tie-break added |
|---|---|---|
normalizer.go |
sortEntries (percentile) |
user_id |
influence.go |
sortIntEntries (percentile) |
user_id |
summary.go |
sortTopGaps (map-built, truncated to top N) |
(strong_domain, weak_domain) |
teams.go |
computeTeamArchetypes (map-built) |
archetype name |
connections.go |
edge list (never sorted) | sort by (source_id, target_id, type) |
This is a determinism/correctness fix only. No weights, thresholds, or scoring math changed. Tie-aware ranking (equal raw scores sharing a rank) is intentionally out of scope.
Proof
Post-fix, analyze run twice on the same seed-42 fixture, same binary:
$ diff -r run1 run2The only difference is the collected_at wall-clock metadata timestamp (legitimately reflects each run's execution time, not the analysis). Every analysis data file (profiles, archetypes, gaps, summary, teams, connections) is byte-identical:
archetypes.json: IDENTICAL
connections.json: IDENTICAL
gaps.json: IDENTICAL
profiles.json: IDENTICAL
summary.json: IDENTICAL
teams.json: IDENTICALTests
CGO_ENABLED=1 go test -race ./... passes. No test expected values changed -- the existing analyzer tests pass unchanged under the new deterministic tie-breaks (verified stable across 5 repeated runs). go vet clean. golangci-lint 0 issues. Total coverage 66.9% (floor 65%); analyzer package 94.4%.