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.Slice is 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.json was 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 run2

The 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:       IDENTICAL

Tests

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%.

Merge request reports

Loading