mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
chore(source-priority): observable parse failures + frozen resolved blob
Two reconcile fixes on parseSourcePriority(), one observability and one
invariant guard:
- Emit a wide-event tag when the persisted JSON fails the Zod schema
so a future schema-tightening regression that silently nukes a
user's saved ladder surfaces in ops dashboards. annotate() is a
no-op outside a request context, so static settings-page renders
and unit tests stay side-effect-free.
- Deep-freeze the resolved blob. The top-level flat keys share their
backing arrays with `metricPriority` via the spread, so a caller
mutating `resolved.metricPriority.weight = […]` would silently
desync the two views. Freezing every ladder + the containers + the
resolved root surfaces the mutation at runtime in strict mode.
This commit is contained in:
1 parent
71745b4c63
commit
2bb49ae764
1 file changed
+38
-2
@@ -34,6 +34,7 @@
|
||||
*/
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
measurementSourceEnum,
|
||||
measurementTypeEnum,
|
||||
@@ -272,7 +273,24 @@ export function parseSourcePriority(raw: unknown): ResolvedSourcePriority {
|
||||
if (raw == null) return buildResolved({}, {}, {});
|
||||
|
||||
const parsed = sourcePrioritySchema.safeParse(raw);
|
||||
if (!parsed.success) return buildResolved({}, {}, {});
|
||||
if (!parsed.success) {
|
||||
// v1.4.25 W10 reconcile (Sr-M2) — emit a wide-event tag so a future
|
||||
// schema-tightening regression that silently nukes a user's saved
|
||||
// ladder surfaces in ops dashboards rather than as a "why did my
|
||||
// priorities reset?" user report. `annotate()` is a no-op outside a
|
||||
// request context, so the static settings-page render and tests
|
||||
// remain side-effect-free.
|
||||
annotate({
|
||||
meta: {
|
||||
sourcePriority: {
|
||||
parse: "failed",
|
||||
issueCount: parsed.error.issues.length,
|
||||
firstIssuePath: parsed.error.issues[0]?.path.join(".") ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
return buildResolved({}, {}, {});
|
||||
}
|
||||
|
||||
// Pull the W5e flat per-metric ladder off the top level. Drop the
|
||||
// two W8c container keys so the rest of the object is just the flat
|
||||
@@ -296,11 +314,29 @@ function buildResolved(
|
||||
...flat,
|
||||
...nested,
|
||||
};
|
||||
return {
|
||||
const resolved: ResolvedSourcePriority = {
|
||||
...merged,
|
||||
metricPriority: merged,
|
||||
deviceTypePriority,
|
||||
};
|
||||
// v1.4.25 W10 reconcile (Code-M1) — deep-freeze the resolved blob so
|
||||
// a caller who mutates `resolved.metricPriority.weight = …` after
|
||||
// parseSourcePriority() returns trips at runtime instead of silently
|
||||
// desyncing the two views (the top-level flat keys share their
|
||||
// backing object with `metricPriority` via the spread above, but
|
||||
// anything written through the alias post-freeze raises in strict
|
||||
// mode and surfaces in dev). Freezing the leaves keeps the contract
|
||||
// immutable end-to-end without copying the source ladders.
|
||||
for (const ladder of Object.values(merged)) {
|
||||
Object.freeze(ladder);
|
||||
}
|
||||
for (const ladder of Object.values(deviceTypePriority)) {
|
||||
if (ladder) Object.freeze(ladder);
|
||||
}
|
||||
Object.freeze(merged);
|
||||
Object.freeze(deviceTypePriority);
|
||||
Object.freeze(resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user