311 lines
9.0 KiB
TypeScript
311 lines
9.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
parseBirthTimeProfile,
|
|
parseCandidateResult,
|
|
parseRectificationQuestionnaire,
|
|
parseRectificationScoring,
|
|
} from "../src/lib/birth-time-journey-adapters.ts";
|
|
|
|
const coordinates = {
|
|
latitude: 31.2304,
|
|
longitude: 121.4737,
|
|
timezone_offset: 8,
|
|
} as const;
|
|
|
|
test("birth time profile adapter parses an exact hospital declaration", () => {
|
|
const assessment = parseBirthTimeProfile({
|
|
birth_date: "1993-04-17",
|
|
reported_birth_time: "08:16:00",
|
|
birth_time_source: "hospital_record",
|
|
uncertainty_before_minutes: 2,
|
|
uncertainty_after_minutes: 2,
|
|
...coordinates,
|
|
});
|
|
|
|
assert.equal(assessment.source, "hospital_record");
|
|
if (assessment.source === "hospital_record") {
|
|
assert.equal(assessment.reportedTime, "08:16");
|
|
assert.equal(assessment.location.lon, 121.4737);
|
|
}
|
|
});
|
|
|
|
test("birth time profile adapter normalizes PostgreSQL date values", () => {
|
|
const assessment = parseBirthTimeProfile({
|
|
birth_date: new Date("1993-04-17T00:00:00.000Z"),
|
|
reported_birth_time: "08:16:00",
|
|
birth_time_source: "hospital_record",
|
|
uncertainty_before_minutes: 2,
|
|
uncertainty_after_minutes: 2,
|
|
...coordinates,
|
|
});
|
|
|
|
assert.equal(assessment.date, "1993-04-17");
|
|
});
|
|
|
|
test("birth time profile adapter parses a period without inventing a time", () => {
|
|
const assessment = parseBirthTimeProfile({
|
|
birth_date: "1993-04-17",
|
|
reported_birth_time: null,
|
|
birth_time_source: "period_only",
|
|
birth_time_period: "evening",
|
|
...coordinates,
|
|
});
|
|
|
|
assert.equal(assessment.source, "period_only");
|
|
assert.equal("reportedTime" in assessment, false);
|
|
});
|
|
|
|
test("birth time profile adapter rejects missing location coordinates", () => {
|
|
assert.throws(() => parseBirthTimeProfile({
|
|
birth_date: "1993-04-17",
|
|
reported_birth_time: "08:16:00",
|
|
birth_time_source: "hospital_record",
|
|
uncertainty_before_minutes: 2,
|
|
uncertainty_after_minutes: 2,
|
|
}));
|
|
});
|
|
|
|
test("rectification adapter normalizes Python questionnaire samples and options", () => {
|
|
const questionnaire = parseRectificationQuestionnaire({
|
|
questions: [{
|
|
id: "education_environment_shift",
|
|
prompt: "是否有明显学业变化?",
|
|
options: [
|
|
{ key: "A", label: "明确有" },
|
|
{ key: "D", label: "不记得" },
|
|
],
|
|
}],
|
|
candidate_scan: {
|
|
samples: [{
|
|
ascendant: { sign: "Cancer" },
|
|
varga_lagna: {
|
|
D9: { sign: "Leo" },
|
|
D10: { sign: "Virgo" },
|
|
},
|
|
}],
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(questionnaire.questions[0]?.options, [
|
|
{ key: "A", label: "明确有" },
|
|
{ key: "D", label: "不记得" },
|
|
]);
|
|
assert.deepEqual(questionnaire.samples[0], {
|
|
ascendantSign: "Cancer",
|
|
d4Sign: null,
|
|
d9Sign: "Leo",
|
|
d10Sign: "Virgo",
|
|
d24Sign: null,
|
|
d30Sign: null,
|
|
a7Sign: null,
|
|
ulSign: null,
|
|
a10Sign: null,
|
|
});
|
|
});
|
|
|
|
test("rectification adapter preserves real engine sample times and named Varga keys", () => {
|
|
const questionnaire = parseRectificationQuestionnaire({
|
|
questions: [],
|
|
candidate_scan: {
|
|
samples: [{
|
|
time: "1997-08-08 06:00",
|
|
ascendant: { sign: "Cancer" },
|
|
varga_lagna: {
|
|
D2_Hora: { sign: "Cancer" },
|
|
D4_Turyamsa: { sign: "Aries" },
|
|
D9_Navamsa: { sign: "Aquarius" },
|
|
D10_Dasamsa: { sign: "Scorpio" },
|
|
D11_Rudramsa: { sign: "Libra" },
|
|
D24_Siddhamsa: { sign: "Pisces" },
|
|
D30_Trimsamsa: { sign: "Pisces" },
|
|
},
|
|
arudha: {
|
|
A7: { sign: "Scorpio" },
|
|
UL: { sign: "Virgo" },
|
|
A10: { sign: "Capricorn" },
|
|
},
|
|
}],
|
|
},
|
|
});
|
|
|
|
const rawScan = questionnaire.raw.candidate_scan as {
|
|
samples: Array<{ time?: unknown }>;
|
|
};
|
|
assert.equal(rawScan.samples[0]?.time, "1997-08-08 06:00");
|
|
assert.deepEqual(questionnaire.samples[0], {
|
|
ascendantSign: "Cancer",
|
|
d2Sign: "Cancer",
|
|
d4Sign: "Aries",
|
|
d9Sign: "Aquarius",
|
|
d10Sign: "Scorpio",
|
|
d11Sign: "Libra",
|
|
d24Sign: "Pisces",
|
|
d30Sign: "Pisces",
|
|
a7Sign: "Scorpio",
|
|
ulSign: "Virgo",
|
|
a10Sign: "Capricorn",
|
|
});
|
|
});
|
|
|
|
test("rectification adapter rejects a malformed Python questionnaire", () => {
|
|
assert.throws(() => parseRectificationQuestionnaire({
|
|
questions: [{ id: "missing_prompt" }],
|
|
candidate_scan: { samples: [] },
|
|
}));
|
|
});
|
|
|
|
test("rectification adapter normalizes all evidence-domain Varga signs", () => {
|
|
const questionnaire = parseRectificationQuestionnaire({
|
|
questions: [],
|
|
candidate_scan: {
|
|
samples: [{
|
|
ascendant: { sign: "Cancer" },
|
|
varga_lagna: {
|
|
D4: { sign: "Aries" },
|
|
D9: { sign: "Leo" },
|
|
D10: { sign: "Virgo" },
|
|
D24: { sign: "Gemini" },
|
|
D30: { sign: "Pisces" },
|
|
},
|
|
}],
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(questionnaire.samples[0], {
|
|
ascendantSign: "Cancer",
|
|
d4Sign: "Aries",
|
|
d9Sign: "Leo",
|
|
d10Sign: "Virgo",
|
|
d24Sign: "Gemini",
|
|
d30Sign: "Pisces",
|
|
a7Sign: null,
|
|
ulSign: null,
|
|
a10Sign: null,
|
|
});
|
|
});
|
|
|
|
test("rectification adapter preserves scoring maps needed after the third answer", () => {
|
|
const questionnaire = parseRectificationQuestionnaire({
|
|
questions: [{
|
|
id: "education_environment_shift",
|
|
prompt: "是否有明显学业变化?",
|
|
round: 1,
|
|
options: [{ key: "A", label: "明确有" }],
|
|
scoring_map: {
|
|
A: { cluster: "early_candidate_cluster", points: 3 },
|
|
},
|
|
}],
|
|
candidate_scan: { samples: [] },
|
|
});
|
|
|
|
assert.deepEqual(questionnaire.raw.questions, [{
|
|
id: "education_environment_shift",
|
|
prompt: "是否有明显学业变化?",
|
|
round: 1,
|
|
options: [{ key: "A", label: "明确有" }],
|
|
scoring_map: {
|
|
A: { cluster: "early_candidate_cluster", points: 3 },
|
|
},
|
|
}]);
|
|
});
|
|
|
|
test("rectification adapter normalizes scoring without elevating confidence", () => {
|
|
const scoring = parseRectificationScoring({
|
|
answered_count: 3,
|
|
candidate_cluster_rankings: [
|
|
{ cluster: "middle_candidate_cluster", score: 5 },
|
|
],
|
|
next_round: 2,
|
|
next_round_questions: [{
|
|
id: "health_crisis_or_low_period",
|
|
prompt: "是否有明显健康或低谷阶段?",
|
|
options: [{ key: "A", label: "明确有" }],
|
|
}],
|
|
});
|
|
|
|
assert.equal(scoring.answeredCount, 3);
|
|
assert.deepEqual(scoring.candidateClusterRankings, [
|
|
{ cluster: "middle_candidate_cluster", score: 5 },
|
|
]);
|
|
assert.equal(scoring.nextRound, 2);
|
|
assert.deepEqual(scoring.nextRoundQuestions, [{
|
|
id: "health_crisis_or_low_period",
|
|
prompt: "是否有明显健康或低谷阶段?",
|
|
options: [{ key: "A", label: "明确有" }],
|
|
}]);
|
|
});
|
|
|
|
test("rectification adapter normalizes an event-scored candidate result", () => {
|
|
const result = parseCandidateResult({
|
|
result_id: "1d8ee348-61a3-433d-8907-ff6d281b9992",
|
|
confidence: "high",
|
|
can_apply: true,
|
|
winning_segment: {
|
|
start_time: "14:22",
|
|
end_time: "14:26",
|
|
representative_time: "14:24",
|
|
width_minutes: 5,
|
|
},
|
|
event_count: 4,
|
|
domain_count: 3,
|
|
top_score: 16,
|
|
second_score: 10,
|
|
margin_percent: 37.5,
|
|
reasons: [],
|
|
evidence: [{
|
|
event_id: "5cb071d6-6d99-46be-85dc-a9bf59ef6ac5",
|
|
domain: "career",
|
|
candidate_time: "14:24",
|
|
rule_ids: ["vim_md_domain_house"],
|
|
points: 4,
|
|
legacy_server_metadata: { source: "existing-engine" },
|
|
}],
|
|
algorithm_version: "birth-time-event-scoring-v1",
|
|
technique_contract: {
|
|
calculation_status: "evaluated",
|
|
used_divisional_charts: ["D10"],
|
|
used_arudha: [],
|
|
dasha_tracks: ["vimshottari"],
|
|
missing_layers: [],
|
|
auxiliary_layers: [],
|
|
hard_blockers: ["neighbor_stability"],
|
|
confirmation_allowed: false,
|
|
decision: "continue_rectification",
|
|
gates: {
|
|
neighbor_stability: {
|
|
status: "diagnostic_fail",
|
|
reason: "diagnostic_only_unique_lead_at_plus_minus_1_2_5_minutes",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.equal(result.resultId, "1d8ee348-61a3-433d-8907-ff6d281b9992");
|
|
assert.equal(result.winningSegment?.representativeTime, "14:24");
|
|
assert.equal(result.canApply, false, "an old or incomplete engine receipt cannot open minute confirmation");
|
|
assert.deepEqual(result.evidence[0]?.ruleIds, ["vim_md_domain_house"]);
|
|
assert.equal(result.techniqueReceipt?.gates?.neighbor_stability?.status, "fail");
|
|
assert.equal("legacy_server_metadata" in (result.evidence[0] ?? {}), false);
|
|
});
|
|
|
|
test("candidate compatibility result accepts event counts beyond the former safety cap", () => {
|
|
const lowCandidate = {
|
|
result_id: "1d8ee348-61a3-433d-8907-ff6d281b9992",
|
|
confidence: "low",
|
|
can_apply: false,
|
|
winning_segment: null,
|
|
event_count: 10,
|
|
domain_count: 5,
|
|
top_score: 0,
|
|
second_score: 0,
|
|
margin_percent: 0,
|
|
reasons: ["safety_cap"],
|
|
evidence: [],
|
|
algorithm_version: "birth-time-choice-scoring-v2",
|
|
} as const;
|
|
|
|
assert.equal(parseCandidateResult(lowCandidate).eventCount, 10);
|
|
assert.equal(parseCandidateResult({ ...lowCandidate, event_count: 11 }).eventCount, 11);
|
|
});
|