fix(correlations): date the earlier-engine note from the instance's first new run

The note for records whose findings predate the seasonally adjusted
engine compared against a fixed calendar date, so findings an instance
kept writing under the earlier engine until its deploy did not count.
The first discovery sync of the new engine now stamps
AppSettings.correlationSeasonalEngineSince before it writes; without a
stamp every stored discovery pattern counts as an earlier finding.

A restore from a portable export reset each pattern's createdAt to the
restore day and dropped the note for good. createdAt now rides every
export, and a file without it falls back to lastComputedAt.
This commit is contained in:
Marc-André Bombeck committed 2026-10-09 10:44:17 +02:00
1 parent 587ea35d34
commit 0ea8d929d2
5 files changed
+250 -31

No files matched your search

+11 -4
View File
@@ -506,10 +506,15 @@ export async function buildProfileBackupSection(
...(disasterRecovery
? {
id: pattern.id,
createdAt: pattern.createdAt.toISOString(),
updatedAt: pattern.updatedAt.toISOString(),
}
: {}),
// Carried in every export, not only DR: when a finding was first made
// is part of what it means. The correlation surface reads it to tell a
// finding of the earlier engine from one of the seasonally adjusted
// engine, and a restore that reset it to the restore day would drop
// that note for good.
createdAt: pattern.createdAt.toISOString(),
canonicalKey: pattern.canonicalKey,
family: pattern.family,
factorKey: pattern.factorKey,
@@ -945,9 +950,11 @@ export async function restoreProfileData(
dismissedEvidenceHash: pattern.dismissedEvidenceHash,
dismissedEffectSize: pattern.dismissedEffectSize,
dismissedSampleSize: pattern.dismissedSampleSize,
...(pattern.createdAt
? { createdAt: new Date(pattern.createdAt) }
: {}),
// A file from before portable exports carried `createdAt` falls back
// to the last computation, the latest instant the finding certainly
// existed: still earlier than this restore, which is what keeps a
// finding of the earlier engine counted as one.
createdAt: new Date(pattern.createdAt ?? pattern.lastComputedAt),
...(pattern.updatedAt
? { updatedAt: new Date(pattern.updatedAt) }
: {}),
+68 -16
View File
@@ -1,6 +1,7 @@
import { createHash } from "node:crypto";
import { prisma } from "@/lib/db";
import { getEvent } from "@/lib/logging/context";
export const PATTERN_FAMILIES = {
discoveryRetrospective: "DISCOVERY_RETROSPECTIVE",
@@ -140,6 +141,12 @@ export async function syncAcceptedPatterns(args: {
}));
const keys = prepared.map((item) => item.canonicalKey);
// The earlier-engine cutoff has to exist before the new engine writes its
// first discovery pattern, or that pattern would count as an earlier one.
if (DISCOVERY_FAMILIES.includes(args.family)) {
await stampSeasonalEngineStart();
}
return prisma.$transaction(async (tx) => {
const existing = await tx.correlationPattern.findMany({
where: { userId: args.userId, canonicalKey: { in: keys } },
@@ -252,33 +259,78 @@ export function decisionForEvidence(
}
/**
* The day the discovery engine started correlating seasonally adjusted
* residuals with an effective-sample-size p-value (v1.42). Before it, two
* series that merely shared the season or a long trend surfaced as a
* finding, so an account with history usually sees fewer pairs afterwards.
* The discovery engine correlates seasonally adjusted residuals with an
* effective-sample-size p-value since v1.42. Before it, two series that
* merely shared the season or a long trend surfaced as a finding, so an
* account with history usually sees fewer pairs afterwards.
*
* Where that change took effect is an instance fact, not a calendar date: an
* instance keeps writing earlier-engine findings until it runs the new build.
* The first discovery sync of the new engine stamps
* `AppSettings.correlationSeasonalEngineSince`, before it writes anything, and
* the cutoff below reads that stamp. Stamped once per instance; the process
* remembers it so later syncs skip the write.
*/
export const SEASONAL_ADJUSTMENT_SINCE = new Date("2026-10-08T00:00:00.000Z");
const DISCOVERY_FAMILIES: readonly PatternFamily[] = [
PATTERN_FAMILIES.discoveryRetrospective,
PATTERN_FAMILIES.discoveryRecent,
];
let seasonalEngineStamped = false;
/**
* Record that the seasonally adjusted engine is running here, unless already
* recorded. Best effort: a failed stamp leaves the marker unset, which counts
* every pattern as an earlier finding (the note shows once more than it
* should), never the reverse.
*/
export async function stampSeasonalEngineStart(
now: Date = new Date(),
): Promise<void> {
if (seasonalEngineStamped) return;
try {
await prisma.appSettings.upsert({
where: { id: "singleton" },
create: { id: "singleton", correlationSeasonalEngineSince: now },
update: {},
});
await prisma.appSettings.updateMany({
where: { id: "singleton", correlationSeasonalEngineSince: null },
data: { correlationSeasonalEngineSince: now },
});
seasonalEngineStamped = true;
} catch {
getEvent()?.addWarning("correlation seasonal engine stamp failed");
}
}
/** Test seam: forget that this process already stamped the marker. */
export function resetSeasonalEngineStampForTests(): void {
seasonalEngineStamped = false;
}
/**
* Whether the record held a discovery finding under the earlier engine: a
* stored discovery pattern first written before {@link SEASONAL_ADJUSTMENT_SINCE}.
* A dismissed pattern counts, it was a finding. An account created after the
* change can never qualify, so the one-time explanation on the correlation
* surface reaches only the people whose list actually changed.
* stored discovery pattern created before the instance's seasonal-engine
* stamp, or any stored discovery pattern while there is no stamp yet. A
* dismissed pattern counts, it was a finding. An account whose first finding
* came from the new engine never qualifies, so the one-time explanation on
* the correlation surface reaches only the people whose list actually
* changed.
*/
export async function hadFindingsBeforeSeasonalAdjustment(
userId: string,
): Promise<boolean> {
const settings = await prisma.appSettings.findUnique({
where: { id: "singleton" },
select: { correlationSeasonalEngineSince: true },
});
const since = settings?.correlationSeasonalEngineSince ?? null;
const row = await prisma.correlationPattern.findFirst({
where: {
userId,
family: {
in: [
PATTERN_FAMILIES.discoveryRetrospective,
PATTERN_FAMILIES.discoveryRecent,
],
},
createdAt: { lt: SEASONAL_ADJUSTMENT_SINCE },
family: { in: [...DISCOVERY_FAMILIES] },
...(since ? { createdAt: { lt: since } } : {}),
},
select: { id: true },
});