mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Maintainer-voice sweep across `src/`. ~190 references to the maintainer by first name in code comments and JSDoc neutralised to "the maintainer", "the live tenant", "users", or "the user" depending on context. Three sed passes covering ~50 distinct patterns (`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.). Test fixtures (`displayName: "Marc B."`, `username: "marc"`, `<HeroStrip userName="Marc">`) kept as opaque test data per the brief. CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a German bullet with its English translation by historical design — the in-app copy is English-default and the user docs are English, so the mixed-language style was a holdover from the early-marathon notes. A one-off node script extracted the `_..._` English paragraph from each bilingual bullet and rewrote the entry as that text. ~44 bullets reduced; one Docs bullet that lost a leading word in the regex fixed by hand. CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md` stays in place for multi-agent compatibility (the conventions doc is shared with Codex / Cursor / etc.). Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete; v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20 items the marathon landed; `.planning/phase-W4-report.md` documents the per-commit decisions, test deltas, and the 23 items deferred to v1.4.23 with reasoning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
3.1 KiB
TypeScript
77 lines
3.1 KiB
TypeScript
/**
|
||
* Window-trend helpers for the health-chart `showTrend` overlay.
|
||
*
|
||
* Two complementary deltas:
|
||
*
|
||
* - `weeklyDelta` — first vs last regression-line value, normalised
|
||
* to a per-week rate. Stable for short windows
|
||
* (≤ 90 days) where the user is asking "what is
|
||
* this metric doing this week / month".
|
||
* - `splitHalfDelta` — mean of the second half minus mean of the first
|
||
* half of the visible series. Stays meaningful
|
||
* for long ("All") ranges where the per-week rate
|
||
* rounds to ±0 in the 1-decimal formatter.
|
||
*
|
||
* v1.4.16 Fix A8b — the maintainer reported the chart trend overlay reads
|
||
* "+0.0 kg/Woche" on the All filter, even when his weight clearly
|
||
* shifted across years. Root cause: `weeklyDelta = (last − first) / days
|
||
* × 7` is mathematically correct but visually rounds to zero whenever
|
||
* the slope-per-day is small enough that 7× still falls below the
|
||
* formatter's display precision. The split-half delta side-steps the
|
||
* rounding entirely because it doesn't divide by the window width.
|
||
*
|
||
* Pure & deterministic — pinned by unit test.
|
||
*/
|
||
|
||
export interface WindowTrendInput {
|
||
/** Sorted ascending by timestamp. */
|
||
rawValues: number[];
|
||
/** Sorted ascending by timestamp. Must align 1:1 with `rawValues`. */
|
||
trendValues: number[];
|
||
/** Days between the first and last point of the visible series. */
|
||
windowDays: number;
|
||
}
|
||
|
||
export interface WindowTrendResult {
|
||
/** Linear-trend per-week delta. Always present when ≥ 2 points. */
|
||
weeklyDelta: number;
|
||
/**
|
||
* First-half-mean vs second-half-mean delta of the raw (not trend)
|
||
* values. Reported as `null` for short windows or when one of the
|
||
* halves is empty (n < 2 split into two halves).
|
||
*/
|
||
splitHalfDelta: number | null;
|
||
}
|
||
|
||
/** Default threshold above which the split-half delta is computed. */
|
||
export const SPLIT_HALF_THRESHOLD_DAYS = 90;
|
||
|
||
export function computeWindowTrend(
|
||
input: WindowTrendInput,
|
||
splitHalfThresholdDays = SPLIT_HALF_THRESHOLD_DAYS,
|
||
): WindowTrendResult | null {
|
||
if (input.rawValues.length < 2) return null;
|
||
if (input.trendValues.length !== input.rawValues.length) return null;
|
||
if (!Number.isFinite(input.windowDays) || input.windowDays <= 0) return null;
|
||
|
||
const firstTrend = input.trendValues[0];
|
||
const lastTrend = input.trendValues[input.trendValues.length - 1];
|
||
const weeklyDelta = ((lastTrend - firstTrend) / input.windowDays) * 7;
|
||
|
||
let splitHalfDelta: number | null = null;
|
||
if (input.windowDays >= splitHalfThresholdDays) {
|
||
const mid = Math.floor(input.rawValues.length / 2);
|
||
const firstHalf = input.rawValues.slice(0, mid);
|
||
const secondHalf = input.rawValues.slice(mid);
|
||
if (firstHalf.length > 0 && secondHalf.length > 0) {
|
||
const meanFirst =
|
||
firstHalf.reduce((sum, value) => sum + value, 0) / firstHalf.length;
|
||
const meanSecond =
|
||
secondHalf.reduce((sum, value) => sum + value, 0) / secondHalf.length;
|
||
splitHalfDelta = meanSecond - meanFirst;
|
||
}
|
||
}
|
||
|
||
return { weeklyDelta, splitHalfDelta };
|
||
}
|