fix(analyzer): momentum on a fixed reference scale + has_trend_data flag
Two coupled fixes for the momentum and trend construct (methodology section 2.6).
Fixed reference scale
ComputeMomentumScores normalized each user's summed six-domain delta by the run's OWN max absolute delta-sum. "-1.0" meant "most-declining this run," not a fixed amount, so the at-risk and accelerating thresholds selected a roughly fixed fraction of the population regardless of reality (if everyone improved, the least-improved user was still pushed toward -1 and flagged at risk).
Replaced the run-relative max with a fixed reference scale: referenceScale = len(AllDomains) * Momentum.MaxPerDomainDelta = 6 * 1.0 = 6.0 (number of domains times the max plausible per-domain delta of 1.0; each domain score is normalized to [0,1], so the summed delta lies in [-6,6]). Divide by 6 and clamp to [-1,1]. A given set of deltas now yields the same momentum independent of the population. The scale and the at-risk/accelerating thresholds route through the Methodology config (MomentumThresholds.MaxPerDomainDelta added; thresholds were already centralized).
has_trend_data flag
With no previous run (PreviousDir empty, momentum nil) the summary emitted adoption_sentiment "steady", at_risk 0, accelerating 0, which the dashboard rendered as a confident "all steady / 0 at risk" narrative, indistinguishable from "no history." Added has_trend_data bool to the Summary schema, set false when momentum is nil. The frontend now suppresses the momentum narrative and shows "No trend history yet" when the flag is false. JS bundle regenerated via make static.
Validation (unit-test based)
A live momentum before/after needs two timed snapshots; the seeded instance is a single static snapshot with no history, so this is unit-validated, not validated by a live before/after.
- Fixed-scale invariance: a user with the same six per-domain deltas across two populations whose max-delta differs now gets identical momentum (0.2 in both). Under the old run-max it would have been 1.0 vs 0.25.
- has_trend_data both directions: no previous dir gives
has_trend_data=falsewith neutral at-risk/accelerating counts and no trends.json; with a previous dirhas_trend_data=trueand momentum is computed. - Determinism: momentum is stable across repeated computation; double-analyze of the seeded
data/produces byte-identical output (only the wall-clock collected_at metadata differs).
Existing momentum test expected values updated to the new fixed scale, each with a comment recording the old run-max value and the new fixed-scale value.
Results: go vet clean, CGO_ENABLED=1 go test -race ./... pass, golangci-lint 0 issues, total coverage 67.5 percent (analyzer package 93.9 percent).
Do not merge; this is operator-gated like every score-changing change.