fix: preserve rectification evidence flow
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const migration = readFileSync(
|
||||
new URL(
|
||||
"../supabase/migrations/20260721150000_align_conversational_finance_domain.sql",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("durable rectification SQL accepts every application evidence domain", () => {
|
||||
for (const validator of [
|
||||
"conversational_rectification_valid_evidence_request(jsonb)",
|
||||
"conversational_rectification_valid_life_event_evidence(jsonb)",
|
||||
"conversational_rectification_valid_private_candidate(jsonb)",
|
||||
]) {
|
||||
assert.match(migration, new RegExp(validator.replace(/[()]/g, "\\$&")));
|
||||
}
|
||||
assert.match(migration, /'education', 'finance', 'relocation'/);
|
||||
assert.match(migration, /birth_time_rectification_event_evidence_domain_check/);
|
||||
});
|
||||
|
||||
test("durable public recap accepts and validates its optional domain", () => {
|
||||
assert.match(
|
||||
migration,
|
||||
/array\['id', 'summary', 'dateLabel', 'domain', 'isCorrection'\]/,
|
||||
);
|
||||
assert.match(migration, /item \? 'domain'/);
|
||||
assert.match(migration, /item ->> 'domain' not in/);
|
||||
});
|
||||
@@ -594,6 +594,75 @@ test("production packet waits for three supported events and then scores the acc
|
||||
);
|
||||
});
|
||||
|
||||
test("production keeps the prior candidate range when a scored segment loses technical discrimination", async () => {
|
||||
const scanCalls: Array<{ readonly birthTime: string; readonly uncertaintyMinutes: number }> = [];
|
||||
const evidence = [
|
||||
syntheticEvidence(11, "education"),
|
||||
syntheticEvidence(12, "relocation"),
|
||||
syntheticEvidence(13, "career"),
|
||||
];
|
||||
const overNarrowed: CandidateResult = {
|
||||
resultId: "00000000-0000-4000-8000-000000000897",
|
||||
confidence: "high",
|
||||
canApply: true,
|
||||
winningSegment: {
|
||||
startTime: "05:20",
|
||||
endTime: "05:20",
|
||||
representativeTime: "05:20",
|
||||
widthMinutes: 1,
|
||||
},
|
||||
eventCount: 3,
|
||||
domainCount: 3,
|
||||
topScore: 10,
|
||||
secondScore: 1,
|
||||
marginPercent: 90,
|
||||
reasons: ["synthetic over-narrowed segment"],
|
||||
evidence: evidence.map((item) => ({
|
||||
eventId: item.id,
|
||||
domain: item.domain as "career" | "education" | "relocation",
|
||||
candidateTime: "05:20",
|
||||
ruleIds: ["synthetic-rule"],
|
||||
points: 1,
|
||||
})),
|
||||
algorithmVersion: "synthetic-event-score-v1",
|
||||
};
|
||||
|
||||
const built = await buildProductionConversationalRectificationPacket(
|
||||
packetEngine({ scanCalls, scoreResults: [overNarrowed] }),
|
||||
{
|
||||
userId,
|
||||
caseId,
|
||||
asOfDate: "2026-07-21",
|
||||
declaredBirthInput: {
|
||||
source: "approximate",
|
||||
birthDate: "1990-01-01",
|
||||
reportedTime: "05:20",
|
||||
uncertaintyBeforeMinutes: 30,
|
||||
uncertaintyAfterMinutes: 30,
|
||||
birthTimeClue: null,
|
||||
birthplace: packetBirthplace,
|
||||
},
|
||||
privateCandidate: null,
|
||||
evidence,
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(scanCalls, [{
|
||||
birthTime: "1990-01-01 05:20",
|
||||
uncertaintyMinutes: 1,
|
||||
}, {
|
||||
birthTime: "1990-01-01 05:20",
|
||||
uncertaintyMinutes: 30,
|
||||
}]);
|
||||
assert.deepEqual(built.packet.candidate.range, { startTime: "04:50", endTime: "05:50" });
|
||||
assert.equal(built.packet.candidate.status, "pending_validation");
|
||||
assert.equal(built.resultId, null);
|
||||
assert.deepEqual(
|
||||
built.packet.scoredHistoricalEvidence.map((item) => item.evidenceId),
|
||||
evidence.map((item) => item.id),
|
||||
);
|
||||
});
|
||||
|
||||
test("legacy import scores inherited events without silently replacing the inherited candidate range", async () => {
|
||||
const scoreCalls: LifeEvent[][] = [];
|
||||
const inherited = { startTime: "05:10", endTime: "05:50" };
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from "node:test";
|
||||
import {
|
||||
buildRectificationTechnicalPacket,
|
||||
projectRectificationTechnicalPacket,
|
||||
RectificationTechnicalPacketRangeError,
|
||||
} from "../src/lib/conversational-rectification/technical-packet.ts";
|
||||
import type { CandidateResult } from "../src/lib/birth-time-evidence.ts";
|
||||
import type { CandidateDifferenceBuild } from "../src/lib/birth-time-dynamic-choice-internal.ts";
|
||||
@@ -267,7 +268,8 @@ test("does not claim scan-wide 05:10-05:30 differences inside a 05:16-05:24 cand
|
||||
boundaryDistanceMinutes: 4,
|
||||
futureWindows: [],
|
||||
},
|
||||
}), /two time-linked scan samples inside the selected candidate range/);
|
||||
}), (error) => error instanceof RectificationTechnicalPacketRangeError
|
||||
&& error.reason === "insufficient_samples");
|
||||
});
|
||||
|
||||
test("does not describe sparse in-range samples as adjacent-minute switches", () => {
|
||||
@@ -296,7 +298,8 @@ test("does not describe sparse in-range samples as adjacent-minute switches", ()
|
||||
boundaryDistanceMinutes: 4,
|
||||
futureWindows: [],
|
||||
},
|
||||
}), /two time-linked discriminating domains/);
|
||||
}), (error) => error instanceof RectificationTechnicalPacketRangeError
|
||||
&& error.reason === "insufficient_domains");
|
||||
});
|
||||
|
||||
test("uses typed server time links when normalized scan raw metadata omits sample times", () => {
|
||||
|
||||
Reference in New Issue
Block a user