医院记录那一分钟落在校正区间外时,交付卡以前只说一句「相差 N 分钟」:
没说默认按哪个时间排盘,采用按钮也仍写中性的「更像这个」,用户看不出
点下去会把之后的排盘换成另一分钟。
产品负责人 2026-09-14 拍板「记录优先,分歧如实呈现」:
- 冲突文案补满三层——默认仍按出生记录时间排盘、经历指向另一段时间相差
N 分钟、两条路都可以走。
- 冲突态采用按钮改「改用校正结果」,按钮下按列写明
「选它之后,排盘会从出生记录时间 hh:mm 换成 hh:mm」。
- 新增 FORBIDDEN_RECORD_VERDICT_PHRASE 锁住 D4:不得宣布记录不准,
也不得宣布校正结果无效。
只改文案层与采用入口措辞。打分、判据、采用 RPC、置信度与确认门控、
数据库一律未动;不新增入口、不加确认弹窗;记录落在范围内或来源是
「大概时间 / 时间段」时卡片与今天完全一致。Skill 未 bump,只补
references §6.1。
验收:tsc 0 错;lint 0 error;npm test 3335 项 fail 31,失败清单与基线
37e6c519 逐条一致(全部是无 Docker / 无外网的既有环境缺口);next build
通过且 / 仍 ○ Static;首屏 chunks gzip +269 B(+0.019%)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
191 lines
7.4 KiB
TypeScript
191 lines
7.4 KiB
TypeScript
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,
|
|
SWITCH_TO_RECTIFIED_LABEL,
|
|
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))
|
|
// 新值: 记录落在范围外时按钮写 SWITCH_TO_RECTIFIED_LABEL(「改用校正结果」)
|
|
// 原因: BUG-691 决策 D3——记录与范围冲突时采用入口要说明自己换掉的是什么;
|
|
// 05:12 对 04:48–05:07 正是冲突态。非冲突态仍走「更像这个」,
|
|
// 由 rectification-record-conflict-copy-20260916.test.ts 逐态锁住。
|
|
assert.ok(html.includes(SWITCH_TO_RECTIFIED_LABEL));
|
|
assert.equal(html.includes(RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis), false);
|
|
});
|