mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
refactor(insights): move the feature window arithmetic to a leaf module so signals-of-day no longer imports the extractor
This commit is contained in:
1 parent
daaa44fd82
commit
17e0f47411
4 files changed
+65
-55
No files matched your search
@@ -128,7 +128,7 @@ const TS_READERS: Record<string, Entry> = {
|
||||
/applyPayloadBudget\(\s*pulsePoints\s*,\s*\{[^}]*type\s*:\s*"PULSE"/,
|
||||
},
|
||||
"lib/insights/signals-of-day.ts": {
|
||||
via: "lib/insights/features.ts",
|
||||
via: "lib/insights/feature-window.ts",
|
||||
passes: /byType\(\s*"PULSE"\s*\)[\s\S]{0,80}type\s*:\s*"PULSE"/,
|
||||
},
|
||||
"lib/ai/coach/snapshot-blocks/core-metrics-block.ts": {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* The window arithmetic the feature extractor and the signals-of-day builder
|
||||
* share: a reading list as trend points, a mean over a days-ago window, and
|
||||
* a spread rounded to the payload's one decimal. A leaf module, so the
|
||||
* signals-of-day builder does not import the extractor that re-exports it.
|
||||
*/
|
||||
import type { DataPoint } from "@/lib/analytics/trends";
|
||||
import { readingsMean } from "@/lib/measurements/day-mean";
|
||||
import { usesHourlyMeanDay } from "@/lib/measurements/day-statistic";
|
||||
import { standardDeviation, type VarianceKind } from "@/lib/stats/spread";
|
||||
|
||||
/**
|
||||
* A spread rounded to the payload's one decimal, `null` below two values: a
|
||||
* single reading has no spread to report, not a spread of zero.
|
||||
*/
|
||||
export function roundedSpread(
|
||||
values: readonly number[],
|
||||
kind: VarianceKind,
|
||||
): number | null {
|
||||
if (values.length < 2) return null;
|
||||
const sd = standardDeviation(values, kind);
|
||||
return sd === null ? null : Math.round(sd * 10) / 10;
|
||||
}
|
||||
|
||||
export function toDataPoints(
|
||||
records: Array<{ value: number; measuredAt: Date }>,
|
||||
): DataPoint[] {
|
||||
return records.map((r) => ({ date: r.measuredAt, value: r.value }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute average of values within a time window (days ago from now).
|
||||
*
|
||||
* `day` names the type and the zone its days are read in. For a type whose
|
||||
* day is the mean of its hours' means (pulse, see `day-mean.ts`) the window
|
||||
* is then the mean of its day values; every other type keeps the plain mean.
|
||||
*/
|
||||
export function avgInWindow(
|
||||
records: Array<{ value: number; measuredAt: Date }>,
|
||||
now: number,
|
||||
fromDaysAgo: number,
|
||||
toDaysAgo: number = 0,
|
||||
day?: { type: string; tz: string },
|
||||
): number | null {
|
||||
const fromMs = now - fromDaysAgo * 24 * 60 * 60 * 1000;
|
||||
const toMs = now - toDaysAgo * 24 * 60 * 60 * 1000;
|
||||
const filtered = records.filter((r) => {
|
||||
const t = r.measuredAt.getTime();
|
||||
return t >= fromMs && t <= toMs;
|
||||
});
|
||||
if (filtered.length === 0) return null;
|
||||
if (day && usesHourlyMeanDay(day.type)) {
|
||||
const mean = readingsMean(day.type, filtered, day.tz);
|
||||
return mean === null ? null : Math.round(mean * 100) / 100;
|
||||
}
|
||||
const sum = filtered.reduce((s, r) => s + r.value, 0);
|
||||
return Math.round((sum / filtered.length) * 100) / 100;
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
} from "@/lib/tz/format";
|
||||
import { readMoodDailyPoints } from "@/lib/insights/mood-daily-points";
|
||||
import { readingsMean } from "@/lib/measurements/day-mean";
|
||||
import { usesHourlyMeanDay } from "@/lib/measurements/day-statistic";
|
||||
import { isCurrentForTodayClaim } from "@/lib/insights/measurement-freshness";
|
||||
import { computeMoodDimensionSeries } from "@/lib/insights/mood-dimension-series";
|
||||
import { computeContextMoodComparison } from "@/lib/insights/mood-context-crosstab";
|
||||
@@ -35,11 +34,12 @@ import {
|
||||
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
|
||||
import { getBpTargets } from "@/lib/analytics/bp-targets";
|
||||
import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target";
|
||||
import { READING_SPREAD } from "@/lib/stats/spread";
|
||||
import {
|
||||
READING_SPREAD,
|
||||
standardDeviation,
|
||||
type VarianceKind,
|
||||
} from "@/lib/stats/spread";
|
||||
avgInWindow,
|
||||
roundedSpread,
|
||||
toDataPoints,
|
||||
} from "@/lib/insights/feature-window";
|
||||
import {
|
||||
pairByTimestamp,
|
||||
significantPearsonCorrelation,
|
||||
@@ -900,25 +900,6 @@ export class FeaturesPayloadTooLargeError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A spread rounded to the payload's one decimal, `null` below two values: a
|
||||
* single reading has no spread to report, not a spread of zero.
|
||||
*/
|
||||
export function roundedSpread(
|
||||
values: readonly number[],
|
||||
kind: VarianceKind,
|
||||
): number | null {
|
||||
if (values.length < 2) return null;
|
||||
const sd = standardDeviation(values, kind);
|
||||
return sd === null ? null : Math.round(sd * 10) / 10;
|
||||
}
|
||||
|
||||
export function toDataPoints(
|
||||
records: Array<{ value: number; measuredAt: Date }>,
|
||||
): DataPoint[] {
|
||||
return records.map((r) => ({ date: r.measuredAt, value: r.value }));
|
||||
}
|
||||
|
||||
function computeCoverage(
|
||||
records: Array<{ measuredAt: Date }>,
|
||||
now: number,
|
||||
@@ -948,35 +929,6 @@ function computeCoverage(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute average of values within a time window (days ago from now).
|
||||
*
|
||||
* `day` names the type and the zone its days are read in. For a type whose
|
||||
* day is the mean of its hours' means (pulse, see `day-mean.ts`) the window
|
||||
* is then the mean of its day values; every other type keeps the plain mean.
|
||||
*/
|
||||
export function avgInWindow(
|
||||
records: Array<{ value: number; measuredAt: Date }>,
|
||||
now: number,
|
||||
fromDaysAgo: number,
|
||||
toDaysAgo: number = 0,
|
||||
day?: { type: string; tz: string },
|
||||
): number | null {
|
||||
const fromMs = now - fromDaysAgo * 24 * 60 * 60 * 1000;
|
||||
const toMs = now - toDaysAgo * 24 * 60 * 60 * 1000;
|
||||
const filtered = records.filter((r) => {
|
||||
const t = r.measuredAt.getTime();
|
||||
return t >= fromMs && t <= toMs;
|
||||
});
|
||||
if (filtered.length === 0) return null;
|
||||
if (day && usesHourlyMeanDay(day.type)) {
|
||||
const mean = readingsMean(day.type, filtered, day.tz);
|
||||
return mean === null ? null : Math.round(mean * 100) / 100;
|
||||
}
|
||||
const sum = filtered.reduce((s, r) => s + r.value, 0);
|
||||
return Math.round((sum / filtered.length) * 100) / 100;
|
||||
}
|
||||
|
||||
// ─── v1.22 — cross-signal integration blocks (briefing-scoped) ────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
avgInWindow,
|
||||
roundedSpread,
|
||||
toDataPoints,
|
||||
} from "@/lib/insights/features";
|
||||
} from "@/lib/insights/feature-window";
|
||||
import {
|
||||
SAME_HOURS_MIN_DAYS,
|
||||
SAME_HOURS_TYPES,
|
||||
|
||||
Reference in new issue
Block a user