mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(insights): persist correlation patterns
This commit is contained in:
1 parent
8ece6f39cc
commit
666a85d311
53 files changed
+2129
-55
No files matched your search
@@ -7,9 +7,9 @@
|
||||
* so the wire contract stays single-source. The response shapes are declared
|
||||
* here.
|
||||
*
|
||||
* Custom metrics are a SEPARATE generic store from the closed measurement
|
||||
* system: not synced, not in FHIR, not in AI insights — log + chart only. All
|
||||
* fields are plaintext.
|
||||
* Custom metrics are a separate generic store from the closed measurement
|
||||
* system. A definition can explicitly opt into the bounded daily correlation
|
||||
* behaviour-channel scan. All fields are plaintext.
|
||||
*/
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import { z } from "zod/v4";
|
||||
@@ -26,13 +26,13 @@ import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
|
||||
createCustomMetricSchema.meta({
|
||||
id: "CreateCustomMetricRequest",
|
||||
description:
|
||||
"Define a user-scoped custom metric ONCE: free-text `name` + `unit`, an optional target window (`targetLow` / `targetHigh`; when both present `targetLow` must not exceed `targetHigh`), optional display `decimals`, and an optional `description`. The name is unique per user. Logging a value later just picks this metric — its unit is snapshotted onto the value at write time. Isolated from the closed measurement system: not synced, not in FHIR, not in insights.",
|
||||
"Define a user-scoped custom metric ONCE: free-text `name` + `unit`, an optional target window (`targetLow` / `targetHigh`; when both present `targetLow` must not exceed `targetHigh`), optional display `decimals`, an optional `description`, and explicit `correlationEnabled` opt-in. The name is unique per user. Logging a value later snapshots its unit.",
|
||||
});
|
||||
|
||||
updateCustomMetricSchema.meta({
|
||||
id: "UpdateCustomMetricRequest",
|
||||
description:
|
||||
"Partial edit of a custom metric. An omitted key leaves the column untouched; an explicit `null` on a target bound / `decimals` / `description` clears it. A rename that collides with another of the caller's live metrics is rejected 409.",
|
||||
"Partial edit of a custom metric. An omitted key leaves the column untouched; an explicit `null` on a target bound / `decimals` / `description` clears it. `correlationEnabled` explicitly controls use as a bounded behaviour channel. A conflicting rename is rejected 409.",
|
||||
});
|
||||
|
||||
createCustomMetricEntrySchema.meta({
|
||||
@@ -64,6 +64,7 @@ const customMetricRow = z
|
||||
targetHigh: z.number().nullable(),
|
||||
decimals: z.number().nullable(),
|
||||
description: z.string().nullable(),
|
||||
correlationEnabled: z.boolean(),
|
||||
latest: latestValue,
|
||||
entryCount: z.number(),
|
||||
createdAt: z.string(),
|
||||
|
||||
@@ -24,6 +24,9 @@ import {
|
||||
derivedBatchQuery,
|
||||
derivedBatchResponse,
|
||||
correlationDiscoveryResponse,
|
||||
correlationPatternListResponse,
|
||||
updateCorrelationPatternRequest,
|
||||
updateCorrelationPatternResponse,
|
||||
glp1PlateauResponse,
|
||||
insightStatusQuery,
|
||||
insightStatusResponse,
|
||||
@@ -511,4 +514,59 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/insights/patterns": {
|
||||
get: {
|
||||
tags: ["Insights"],
|
||||
summary: "List current correlation patterns",
|
||||
description:
|
||||
"Returns the authenticated account's currently accepted persisted correlation identities, evidence, and dismissal timestamps.",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Current persisted correlation patterns.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
correlationPatternListResponse,
|
||||
"CorrelationPatternListResponseEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/insights/patterns/{id}": {
|
||||
patch: {
|
||||
tags: ["Insights"],
|
||||
summary: "Update correlation pattern dismissal",
|
||||
description:
|
||||
"Dismisses or restores one current account-owned pattern. A dismissal remains effective until the accepted evidence changes materially.",
|
||||
requestParams: { path: z.object({ id: z.string() }) },
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": { schema: updateCorrelationPatternRequest },
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Updated dismissal decision.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: dataEnvelope(
|
||||
updateCorrelationPatternResponse,
|
||||
"UpdateCorrelationPatternResponseEnvelope",
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
"404": {
|
||||
description: "No current pattern with that id for this account.",
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -281,6 +281,14 @@ export const discoveredCorrelation = z
|
||||
.describe(
|
||||
"Outcome channel (lag target), e.g. SLEEP_DURATION, HEART_RATE_VARIABILITY.",
|
||||
),
|
||||
behaviourLabel: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Display label for a dynamic behaviour channel."),
|
||||
outcomeLabel: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Display label for a dynamic outcome channel."),
|
||||
n: z
|
||||
.number()
|
||||
.int()
|
||||
@@ -308,9 +316,57 @@ export const discoveredCorrelation = z
|
||||
.describe(
|
||||
"v1.22 — true for an emerging recent-window pair: fewer days, hedged as provisional rather than established.",
|
||||
),
|
||||
patternId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Account-scoped persisted identity for dismissal."),
|
||||
canonicalKey: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Stable factor + outcome + lag identity."),
|
||||
dismissed: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Server decision to suppress unchanged evidence."),
|
||||
})
|
||||
.meta({ id: "DiscoveredCorrelation" });
|
||||
|
||||
export const correlationPattern = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
canonicalKey: z.string(),
|
||||
family: z.string(),
|
||||
factorKey: z.string(),
|
||||
outcomeKey: z.string(),
|
||||
lagDays: z.number().int(),
|
||||
sampleSize: z.number().int(),
|
||||
effectSize: z.number(),
|
||||
pValue: z.number(),
|
||||
qValue: z.number().nullable(),
|
||||
evidenceHash: z.string(),
|
||||
lastComputedAt: z.string(),
|
||||
dismissedAt: z.string().nullable(),
|
||||
})
|
||||
.meta({ id: "CorrelationPattern" });
|
||||
|
||||
export const correlationPatternListResponse = z
|
||||
.object({ patterns: z.array(correlationPattern) })
|
||||
.meta({ id: "CorrelationPatternListResponse" });
|
||||
|
||||
export const updateCorrelationPatternRequest = z
|
||||
.strictObject({ dismissed: z.boolean() })
|
||||
.meta({ id: "UpdateCorrelationPatternRequest" });
|
||||
|
||||
export const updateCorrelationPatternResponse = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
canonicalKey: z.string(),
|
||||
dismissed: z.boolean(),
|
||||
dismissedAt: z.string().nullable(),
|
||||
evidenceHash: z.string(),
|
||||
})
|
||||
.meta({ id: "UpdateCorrelationPatternResponse" });
|
||||
|
||||
// v1.22 — one labs ↔ outcome association (point-vs-window over sparse draws).
|
||||
export const discoveredLabCorrelation = z
|
||||
.object({
|
||||
|
||||
Reference in new issue
Block a user