Files
Jyotisha/frontend/tests/rectification-public-range-gate-v6.test.ts
T

121 lines
4.6 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { candidateSnapshotSchema, type EvidenceDomain, type Robustness } from "../src/lib/rectification-v4/contracts.ts";
import { evaluateDecisionGate } from "../src/lib/rectification-v4/decision-gate.ts";
import { classifyMissingTechniqueLayers } from "../src/lib/rectification-v4/technique-layer-policy.ts";
const cluster = {
rank: 1,
startTime: "05:13",
endTime: "05:17",
representativeTime: "05:15",
widthMinutes: 5,
peakScore: 10,
scoreMass: 40,
} as const;
const robustness: Robustness = {
neighborSupportMinutes: 4,
leaveOneOutRetentionRate: 1,
leaveOneDomainOutRetentionRate: 1,
dateSensitivityRetentionRate: 1,
calculationSpecHashMatched: true,
};
const domains = ["education", "relocation", "career"] as const satisfies readonly EvidenceDomain[];
function gate(overrides: Partial<Parameters<typeof evaluateDecisionGate>[0]> = {}) {
return evaluateDecisionGate({
clusters: [cluster],
robustness,
scoreableEventCount: 6,
scoreableDomains: domains,
missingTechniqueLayers: [],
...overrides,
});
}
test("single-domain dependence blocks the public candidate range", () => {
const result = gate({ robustness: { ...robustness, leaveOneDomainOutRetentionRate: 0.79 } });
assert.equal(result.canAcceptRange, false);
assert.ok(result.reasons.includes("leave_one_domain_out_not_stable"));
assert.equal(result.canConfirmExactMinute, false);
});
test("KP cusps alone do not block the public candidate range", () => {
const classified = classifyMissingTechniqueLayers(["KP_cusps"], domains);
assert.deepEqual(classified.optional, ["KP_cusps"]);
assert.equal(gate({ missingTechniqueLayers: ["KP_cusps"] }).canAcceptRange, true);
});
test("D60 remains reference-only and does not block the public candidate range", () => {
const classified = classifyMissingTechniqueLayers(["D60"], domains);
assert.deepEqual(classified.referenceOnly, ["D60"]);
assert.equal(gate({ missingTechniqueLayers: ["D60"] }).canAcceptRange, true);
});
test("missing D10 blocks when career evidence is active", () => {
const result = gate({ missingTechniqueLayers: ["D10"] });
assert.equal(result.canAcceptRange, false);
assert.ok(result.reasons.includes("missing_required_layer:D10"));
});
test("missing D10 does not block without career evidence", () => {
const scoreableDomains = ["education", "relocation", "relationship"] as const;
assert.equal(gate({ scoreableDomains, missingTechniqueLayers: ["D10"] }).canAcceptRange, true);
});
test("unclassified missing layers fail closed", () => {
const result = gate({ missingTechniqueLayers: ["future_unknown_layer"] });
assert.equal(result.canAcceptRange, false);
assert.ok(result.reasons.includes("missing_unclassified_layer:future_unknown_layer"));
});
function snapshotInput(overrides: Record<string, unknown> = {}) {
return {
id: "00000000-0000-4000-8000-000000000001",
caseId: "00000000-0000-4000-8000-000000000002",
caseVersion: 1,
evidenceSetHash: "e".repeat(64),
calculationSpecHash: "c".repeat(64),
algorithmVersion: "rectification-v5-matrix-scoring-1",
candidates: [{ time: "05:15", score: 10, supportingEventIds: [], conflictingEventIds: [] }],
clusters: [cluster],
canConfirmExactMinute: false,
canAcceptRange: true,
gateReasons: [],
createdAt: "2026-07-29T00:00:00.000Z",
...overrides,
};
}
test("pre-matrix historical snapshots remain readable", () => {
const parsed = candidateSnapshotSchema.parse(snapshotInput({
algorithmVersion: "rectification-v4-range-scoring-1",
robustness,
}));
assert.equal(parsed.algorithmVersion, "rectification-v4-range-scoring-1");
});
test("legacy snapshots without domain retention still parse without retroactive rejection", () => {
const parsed = candidateSnapshotSchema.parse(snapshotInput({
robustness: {
neighborSupportMinutes: 4,
leaveOneOutRetentionRate: 1,
dateSensitivityRetentionRate: 1,
calculationSpecHashMatched: true,
},
}));
assert.equal(parsed.robustness.leaveOneDomainOutRetentionRate, 0.8);
assert.equal(parsed.canAcceptRange, true);
assert.ok(!parsed.gateReasons.includes("leave_one_domain_out_not_stable"));
});
test("snapshots with explicit low domain retention are normalized to rejected", () => {
const parsed = candidateSnapshotSchema.parse(snapshotInput({
robustness: { ...robustness, leaveOneDomainOutRetentionRate: 0.79 },
}));
assert.equal(parsed.robustness.leaveOneDomainOutRetentionRate, 0.79);
assert.equal(parsed.canAcceptRange, false);
assert.ok(parsed.gateReasons.includes("leave_one_domain_out_not_stable"));
});