Problem
The peer "gap" (the ±% shown next to a metric = its distance from the peer/department median) has the wrong sign for lower-is-better metrics. It should be + when the value is above the median and − when below. Instead the frontend flips the sign by higher_is_better, so for the 18 lower-is-better metrics (Meeting Hours, PR Cycle Time, PR Size, MTTR, Task Dev Time, …) a below-median value shows + and an above-median value shows −. IC and Team dashboards both. The value and median are correct — only the derived sign is wrong.
Reproduce
- IC dashboard → a lower-is-better metric where the person is below the department median (e.g. Meeting Hours — fewer hours is better).
- Read the peer gap.
Expected: below median → −%. Actual: below median → +%.
Real example (Engineering / Meeting Hours, median ≈ 2.69 h): member at 2.45 h (below) shows +9%; member at 2.74 h (above) shows −2%.
Root cause
Gap is higher_is_better ? (value − median) : −(value − median) — the second branch negates the sign for every lower-is-better metric. The intended convention is direction-agnostic: gap = (value − median) / |median| everywhere, with good/bad color driven by higher_is_better (not by the sign).
Examples
Sign must track value vs median only. Direction changes the color, never the sign.
| Metric |
Dir. |
Value |
Median |
vs median |
Current (wrong) |
Correct |
Color |
| PRs Merged |
higher-better |
20 |
10 |
above |
+100% |
+100% ✓ |
green (good) |
| PRs Merged |
higher-better |
5 |
10 |
below |
−50% |
−50% ✓ |
red (bad) |
| Meeting Hours |
lower-better |
2.45 |
2.69 |
below |
+9% ✗ |
−9% |
green (good) |
| Meeting Hours |
lower-better |
2.74 |
2.69 |
above |
−2% ✗ |
+2% |
red (bad) |
| PR Cycle Time |
lower-better |
30 h |
20 h |
above |
−50% ✗ |
+50% |
red (bad) |
Higher-better metrics already read correctly; every lower-better metric is inverted. Note the last rows: a − gap can be good (below median on meeting hours) and a + gap can be bad (above median on cycle time) — which is exactly why the sign must come from value−median and the color from direction, kept separate.
Fix checklist
Sign shown to the user (the visible bug):
Sign used as a "badness" ranking key — re-express via higher_is_better so the new sign doesn't break the pick/sort:
Single source of truth:
Problem
The peer "gap" (the
±%shown next to a metric = its distance from the peer/department median) has the wrong sign for lower-is-better metrics. It should be+when the value is above the median and−when below. Instead the frontend flips the sign byhigher_is_better, so for the 18 lower-is-better metrics (Meeting Hours, PR Cycle Time, PR Size, MTTR, Task Dev Time, …) a below-median value shows+and an above-median value shows−. IC and Team dashboards both. The value and median are correct — only the derived sign is wrong.Reproduce
Expected: below median →
−%. Actual: below median →+%.Real example (Engineering / Meeting Hours, median ≈ 2.69 h): member at 2.45 h (below) shows +9%; member at 2.74 h (above) shows −2%.
Root cause
Gap is
higher_is_better ? (value − median) : −(value − median)— the second branch negates the sign for every lower-is-better metric. The intended convention is direction-agnostic:gap = (value − median) / |median|everywhere, with good/bad color driven byhigher_is_better(not by the sign).Examples
Sign must track value vs median only. Direction changes the color, never the sign.
+100%+100%✓−50%−50%✓+9%✗−9%−2%✗+2%−50%✗+50%Higher-better metrics already read correctly; every lower-better metric is inverted. Note the last rows: a
−gap can be good (below median on meeting hours) and a+gap can be bad (above median on cycle time) — which is exactly why the sign must come from value−median and the color from direction, kept separate.Fix checklist
Sign shown to the user (the visible bug):
gap = higherIsBetter ? raw : -rawgapDelta/gapPctflip; rendered at L144-L152 (L298, L410). (Also feeds the AI panel via ai-personal-panel.tsx#L256.)Sign used as a "badness" ranking key — re-express via
higher_is_betterso the new sign doesn't break the pick/sort:gap, then worst-metric pick at L351relGap(the opposite flip), sorted at L70Single source of truth:
relativeGap()in lib/peers.ts#L134-L140 — currentlyhigher_is_better ? (median − value) : (value − median)(opposite of the counters/heatmap flip → the app disagrees with itself). Make this the one direction-agnostic definition and route every site above through it.