fix(rectification): label declared birth time as record or estimate (BUG-690)
Hospital records, family estimates and period-only windows now keep distinct copy on the board and range card. Scoring still uses the reported clock as the search-window centre. Hospital minutes outside the current range are stated with the offset and no preference.
This commit is contained in:
@@ -355,3 +355,16 @@ test("range delivery copy bans 这次给出 / 最终 and titles 目前范围", (
|
||||
assert.match(pool, /SPLIT_ENDPOINT_PHRASE/);
|
||||
assert.doesNotMatch(pool, /能把 04:51 和 05:06 分开/);
|
||||
});
|
||||
|
||||
test("declared-time copy splits hospital records from family estimates", () => {
|
||||
const visible = listUserVisibleCopy().join("\n");
|
||||
assert.match(visible, /你的出生记录时间/);
|
||||
assert.match(visible, /你填的大概时间/);
|
||||
assert.match(visible, /你给的时间段/);
|
||||
assert.match(visible, /与你填的大概时间 05:00 相差 7 分钟/);
|
||||
assert.match(visible, /出生记录时间 05:12。目前范围不含这一分钟,相差 5 分钟/);
|
||||
const approximate = listUserVisibleCopy().filter((item) => item.includes("大概时间") || item.includes("时间段"));
|
||||
for (const item of approximate) {
|
||||
assert.doesNotMatch(item, /你的出生时间/);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import {
|
||||
FORBIDDEN_DECLARED_BIRTH_TIME_PHRASE,
|
||||
boardDeclaredTimeLine,
|
||||
normalizeBirthTimeSource,
|
||||
reportedTimeOffsetCopy,
|
||||
} from "../src/lib/rectification-agentic/birth-time-provenance.ts";
|
||||
import { publicDecisionFields } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||||
import { INFERENCE_ALGORITHM_VERSION, type InferenceCandidate, type InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
|
||||
import { decideFromDossier, type DecisionDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||||
import { buildRangeDelivery } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
|
||||
import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx";
|
||||
import type { RectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
|
||||
import { RECTIFICATION_USER_COPY } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
|
||||
function dossier(source?: string | null): DecisionDossier {
|
||||
return {
|
||||
evidence: [],
|
||||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||||
latestResult: null,
|
||||
case: { acceptedTime: null, ...(source === undefined ? {} : { birthTimeSource: source }) },
|
||||
};
|
||||
}
|
||||
|
||||
function cand(time: string, probability: number): InferenceCandidate {
|
||||
return {
|
||||
id: time,
|
||||
time,
|
||||
cluster_range: ["04:48", "05:07"],
|
||||
prior_score: probability * 100,
|
||||
posterior_score: probability * 100,
|
||||
probability,
|
||||
status: "active",
|
||||
rank: 1,
|
||||
strong_conflict_count: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function inference(): InferenceState {
|
||||
return {
|
||||
algorithm_version: INFERENCE_ALGORITHM_VERSION,
|
||||
candidate_set_id: "set",
|
||||
revision: 1,
|
||||
phase: "discrimination",
|
||||
result_status: "credible_range",
|
||||
range_start: "04:48",
|
||||
range_end: "05:07",
|
||||
candidates: [cand("04:53", 0.4), cand("04:50", 0.35)],
|
||||
events: [],
|
||||
probes: [],
|
||||
answered_probes: [],
|
||||
rounds: [],
|
||||
entropy: 1,
|
||||
representative_time: "04:53",
|
||||
credible_range: ["04:48", "05:07"],
|
||||
};
|
||||
}
|
||||
|
||||
function deliveryResult(delivery: ReturnType<typeof buildRangeDelivery>): RectificationCandidateResult {
|
||||
return {
|
||||
resultId: "result-provenance",
|
||||
candidates: delivery.columns.map((column, index) => ({
|
||||
candidateId: column.candidate_id,
|
||||
rank: index + 1,
|
||||
time: column.time,
|
||||
relativeSupport: column.probability_percent,
|
||||
tiedMinuteCount: 1,
|
||||
})),
|
||||
overallConfidence: "medium",
|
||||
selectionAllowed: true,
|
||||
canAdopt: true,
|
||||
confirmationAllowed: false,
|
||||
decisionReceipt: null,
|
||||
representativeTime: delivery.representative_time,
|
||||
selectedTime: null,
|
||||
selectionKind: null,
|
||||
houseTable: null,
|
||||
houseTablesByTime: {},
|
||||
natalRecast: null,
|
||||
techniqueAudit: [],
|
||||
windowTransitions: [],
|
||||
eventDashaLedger: [],
|
||||
dashaAgreement: null,
|
||||
lagnaContrast: null,
|
||||
nakshatraBoundary: null,
|
||||
precisionStage: null,
|
||||
oosBlindPrompts: [],
|
||||
confirmationGate: { confirmation_allowed: false } as RectificationCandidateResult["confirmationGate"],
|
||||
validated: false,
|
||||
completionStatus: null,
|
||||
sessionOutcome: "adopt_representative",
|
||||
credibleRange: delivery.range,
|
||||
rangeDelivery: delivery,
|
||||
verificationReportMarkdown: delivery.verification_markdown,
|
||||
};
|
||||
}
|
||||
|
||||
test("GET and decideFromDossier expose hospital / approximate / period_only, missing becomes approximate", () => {
|
||||
assert.equal(normalizeBirthTimeSource("hospital_record"), "hospital_record");
|
||||
assert.equal(normalizeBirthTimeSource("approximate"), "approximate");
|
||||
assert.equal(normalizeBirthTimeSource("period_only"), "period_only");
|
||||
assert.equal(normalizeBirthTimeSource("family_exact"), "approximate");
|
||||
assert.equal(normalizeBirthTimeSource(null), "approximate");
|
||||
assert.equal(normalizeBirthTimeSource(undefined), "approximate");
|
||||
assert.equal(publicDecisionFields(decideFromDossier(dossier("hospital_record"))).birth_time_source, "hospital_record");
|
||||
assert.equal(publicDecisionFields(decideFromDossier(dossier("approximate"))).birth_time_source, "approximate");
|
||||
assert.equal(publicDecisionFields(decideFromDossier(dossier("period_only"))).birth_time_source, "period_only");
|
||||
assert.equal(publicDecisionFields(decideFromDossier(dossier())).birth_time_source, "approximate");
|
||||
const response = readFileSync(
|
||||
new URL("../src/lib/rectification-agentic/v9/case-dossier-response.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(response, /birth_time_source: birthTimeSource/);
|
||||
});
|
||||
|
||||
test("approximate and period_only copy never says 你的出生时间", () => {
|
||||
const approximate = [
|
||||
boardDeclaredTimeLine("05:00", "approximate"),
|
||||
reportedTimeOffsetCopy({
|
||||
source: "approximate",
|
||||
reportedTime: "05:00",
|
||||
range: ["04:48", "05:07"],
|
||||
representativeTime: "04:53",
|
||||
}) ?? "",
|
||||
];
|
||||
const period = [
|
||||
boardDeclaredTimeLine("05:00", "period_only"),
|
||||
reportedTimeOffsetCopy({
|
||||
source: "period_only",
|
||||
reportedTime: "05:00",
|
||||
range: ["04:48", "05:07"],
|
||||
representativeTime: "04:53",
|
||||
}) ?? "",
|
||||
];
|
||||
for (const line of [...approximate, ...period]) {
|
||||
assert.doesNotMatch(line, FORBIDDEN_DECLARED_BIRTH_TIME_PHRASE);
|
||||
}
|
||||
assert.match(approximate.join("\n"), /你填的大概时间/);
|
||||
assert.match(period.join("\n"), /你给的时间段/);
|
||||
});
|
||||
|
||||
test("hospital_record copy names the record and states the offset without picking a side", () => {
|
||||
const inside = reportedTimeOffsetCopy({
|
||||
source: "hospital_record",
|
||||
reportedTime: "05:00",
|
||||
range: ["04:48", "05:07"],
|
||||
representativeTime: "04:53",
|
||||
});
|
||||
assert.match(inside ?? "", /出生记录时间 05:00,落在目前范围内/);
|
||||
const outside = reportedTimeOffsetCopy({
|
||||
source: "hospital_record",
|
||||
reportedTime: "05:12",
|
||||
range: ["04:48", "05:07"],
|
||||
representativeTime: "04:53",
|
||||
});
|
||||
assert.match(outside ?? "", /出生记录时间 05:12/);
|
||||
assert.match(outside ?? "", /相差 5 分钟/);
|
||||
assert.doesNotMatch(outside ?? "", /以记录为准|以证据为准|应该信/);
|
||||
const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, {
|
||||
result: deliveryResult(buildRangeDelivery({
|
||||
inference: inference(),
|
||||
publicCandidates: [
|
||||
{ candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", time: "04:53" },
|
||||
{ candidateId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2", time: "04:50" },
|
||||
],
|
||||
credibleRange: ["04:48", "05:07"],
|
||||
representativeTime: "04:53",
|
||||
reportedBirthTime: "05:12",
|
||||
birthTimeSource: "hospital_record",
|
||||
})),
|
||||
acceptingCandidateId: null,
|
||||
readonly: false,
|
||||
onAccept: () => undefined,
|
||||
}));
|
||||
assert.match(html, /出生记录时间 05:12/);
|
||||
assert.match(html, /相差 5 分钟/);
|
||||
assert.ok(html.includes(RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis));
|
||||
});
|
||||
@@ -129,6 +129,8 @@ test("peek copy stays a control label, not a second dump of the house table", ()
|
||||
const empty = rectificationBoardPeekCopy(null);
|
||||
assert.equal(empty.title, "当前盘面");
|
||||
assert.equal(empty.detail, "补充经历后会在这里更新");
|
||||
assert.equal(rectificationBoardPeekCopy(null, "05:10").detail, "大概 05:10");
|
||||
assert.equal(rectificationBoardPeekCopy(null, "05:10", "hospital_record").detail, "出生记录 05:10");
|
||||
|
||||
const filled = rectificationBoardPeekCopy(snapshot({ time: "04:48", lagna: "金牛座" }));
|
||||
assert.equal(filled.title, "当前盘面");
|
||||
|
||||
@@ -150,8 +150,8 @@ test("the choice card confirms the tap and the board's first state shows the dec
|
||||
assert.match(choiceCard, /stop_label !== CHOICE_STOP_LABEL/);
|
||||
// BUG-509
|
||||
assert.match(board, /declaredTime: string \| null;/);
|
||||
assert.match(board, /rectificationBoardEmptyCopy\(declaredTime\)/);
|
||||
assert.match(board, /rectificationBoardPeekCopy\(result, declaredTime\)/);
|
||||
assert.match(board, /rectificationBoardEmptyCopy\(declaredTime, birthTimeSource\)/);
|
||||
assert.match(board, /rectificationBoardPeekCopy\(result, declaredTime, birthTimeSource\)/);
|
||||
assert.doesNotMatch(board, /补充经历后,这里会显示当前本命宫位和换升时刻/);
|
||||
assert.match(chat, /\$\{candidateResult \? "" : " is-board-empty"\}/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-board-empty \{\s*grid-template-columns: minmax\(0, 1fr\) minmax\(16rem, 18rem\);/);
|
||||
|
||||
@@ -222,9 +222,16 @@ test("live-row labels follow the action that started the turn", () => {
|
||||
});
|
||||
|
||||
test("board copy before any candidate shows the declared minute, never invented data", () => {
|
||||
// 原值: 「填报出生时间 05:10」
|
||||
// 新值: 缺来源时按大概时间;医院记录用「出生记录时间」
|
||||
// 原因: BUG-690 推算值不得称作「你的出生时间」
|
||||
assert.deepEqual(rectificationBoardEmptyCopy("05:10"), {
|
||||
clock: "05:10",
|
||||
lines: ["填报出生时间 05:10", "回答几个问题后,这里会显示宫位随时间的变化。"],
|
||||
lines: ["你填的大概时间 05:10", "回答几个问题后,这里会显示宫位随时间的变化。"],
|
||||
});
|
||||
assert.deepEqual(rectificationBoardEmptyCopy("05:10", "hospital_record"), {
|
||||
clock: "05:10",
|
||||
lines: ["出生记录时间 05:10", "回答几个问题后,这里会显示宫位随时间的变化。"],
|
||||
});
|
||||
assert.deepEqual(rectificationBoardEmptyCopy(null), {
|
||||
clock: null,
|
||||
|
||||
Reference in New Issue
Block a user