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:
@@ -3396,6 +3396,12 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
font-size: var(--type-body-md);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.rectification-range-delivery__provenance {
|
||||
margin: 0;
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-caption);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.rectification-range-delivery__boundary {
|
||||
margin: 0;
|
||||
color: var(--color-ink-secondary);
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
persistedQuestionSurface,
|
||||
interviewStopReasonFromSnapshot,
|
||||
interviewSessionOutcomeFromSnapshot,
|
||||
birthTimeSourceFromSnapshot,
|
||||
type RectificationCaseSnapshotPayload,
|
||||
} from "@/lib/rectification-surface-state";
|
||||
import { RectificationTimeline } from "@/components/rectification-timeline";
|
||||
@@ -394,6 +395,7 @@ type CaseSnapshotState = Readonly<{
|
||||
stage: RectificationTimelineStage | null;
|
||||
interviewStopReason: string | null;
|
||||
interviewSessionOutcome: string | null;
|
||||
birthTimeSource: ReturnType<typeof birthTimeSourceFromSnapshot>;
|
||||
}>;
|
||||
|
||||
function nextUserActionIdFromSnapshot(payload: RectificationCaseSnapshotPayload | null): string | null {
|
||||
@@ -419,6 +421,7 @@ function caseSnapshotState(payload: RectificationCaseSnapshotPayload | null): Ca
|
||||
stage: caseStageFromSnapshot(payload.case?.stage),
|
||||
interviewStopReason: interviewStopReasonFromSnapshot(payload),
|
||||
interviewSessionOutcome: interviewSessionOutcomeFromSnapshot(payload),
|
||||
birthTimeSource: birthTimeSourceFromSnapshot(payload),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -464,6 +467,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const [nextUserActionId, setNextUserActionId] = useState<string | null>(() => caseSnapshotState(initialSnapshot)?.nextUserActionId ?? null);
|
||||
const [interviewStopReason, setInterviewStopReason] = useState<string | null>(() => caseSnapshotState(initialSnapshot)?.interviewStopReason ?? null);
|
||||
const [interviewSessionOutcome, setInterviewSessionOutcome] = useState<string | null>(() => caseSnapshotState(initialSnapshot)?.interviewSessionOutcome ?? null);
|
||||
const [birthTimeSource, setBirthTimeSource] = useState(() => caseSnapshotState(initialSnapshot)?.birthTimeSource ?? birthTimeSourceFromSnapshot(initialSnapshot));
|
||||
const [caseSnapshotLoaded, setCaseSnapshotLoaded] = useState(initialSnapshot !== null);
|
||||
const [questionRetryAttempts, setQuestionRetryAttempts] = useState(0);
|
||||
const [questionRepairAttempts, setQuestionRepairAttempts] = useState(0);
|
||||
@@ -637,6 +641,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
setNextUserActionId(nextActionId || null);
|
||||
setInterviewStopReason(interviewStopReasonFromSnapshot(snapshot));
|
||||
setInterviewSessionOutcome(interviewSessionOutcomeFromSnapshot(snapshot));
|
||||
setBirthTimeSource(birthTimeSourceFromSnapshot(snapshot));
|
||||
setCaseSnapshotLoaded(true);
|
||||
if (nextQuestion || nextChoice || acceptedTime || confirmedTime) {
|
||||
setQuestionRepairAttempts(0);
|
||||
@@ -1683,6 +1688,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
<RectificationBoardPeek
|
||||
result={candidateResult}
|
||||
declaredTime={declaredTime}
|
||||
birthTimeSource={birthTimeSource}
|
||||
expanded={boardOpen}
|
||||
boardId={boardId}
|
||||
onOpen={toggleBoard}
|
||||
@@ -1988,6 +1994,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
<RectificationBoard
|
||||
result={candidateResult}
|
||||
declaredTime={declaredTime}
|
||||
birthTimeSource={birthTimeSource}
|
||||
savedStatus={savedStatus}
|
||||
diff={boardDiff}
|
||||
compact={compactBoard}
|
||||
|
||||
@@ -39,6 +39,7 @@ function LayerChips({ layers }: Readonly<{ layers: readonly WindowScanLayer[] }>
|
||||
function RectificationBoardBody({
|
||||
result,
|
||||
declaredTime,
|
||||
birthTimeSource,
|
||||
savedStatus,
|
||||
diff,
|
||||
compact,
|
||||
@@ -47,6 +48,7 @@ function RectificationBoardBody({
|
||||
}: Readonly<{
|
||||
result: RectificationCandidateResult | null;
|
||||
declaredTime: string | null;
|
||||
birthTimeSource?: string | null;
|
||||
savedStatus: "accepted" | "confirmed" | null;
|
||||
diff: RectificationBoardDiff;
|
||||
compact: boolean;
|
||||
@@ -55,7 +57,7 @@ function RectificationBoardBody({
|
||||
}>) {
|
||||
const table = result ? workingRectificationHouseTable(result) : null;
|
||||
const workingTime = workingRectificationTime(result);
|
||||
const emptyCopy = rectificationBoardEmptyCopy(declaredTime);
|
||||
const emptyCopy = rectificationBoardEmptyCopy(declaredTime, birthTimeSource);
|
||||
const grouped = result ? groupWindowTransitions(result.windowTransitions) : [];
|
||||
const scoringMinutes = grouped.filter((row) => row.scoringLayers.length > 0);
|
||||
const displayMinutes = grouped.filter((row) => row.displayLayers.length > 0);
|
||||
@@ -220,6 +222,7 @@ function RectificationBoardBody({
|
||||
export function RectificationBoard({
|
||||
result,
|
||||
declaredTime,
|
||||
birthTimeSource,
|
||||
savedStatus,
|
||||
diff,
|
||||
compact,
|
||||
@@ -230,6 +233,7 @@ export function RectificationBoard({
|
||||
}: Readonly<{
|
||||
result: RectificationCandidateResult | null;
|
||||
declaredTime: string | null;
|
||||
birthTimeSource?: string | null;
|
||||
savedStatus: "accepted" | "confirmed" | null;
|
||||
diff: RectificationBoardDiff;
|
||||
compact: boolean;
|
||||
@@ -265,6 +269,7 @@ export function RectificationBoard({
|
||||
<RectificationBoardBody
|
||||
result={result}
|
||||
declaredTime={declaredTime}
|
||||
birthTimeSource={birthTimeSource}
|
||||
savedStatus={savedStatus}
|
||||
diff={diff}
|
||||
compact={compact}
|
||||
@@ -287,17 +292,19 @@ export function RectificationBoard({
|
||||
export function RectificationBoardPeek({
|
||||
result,
|
||||
declaredTime = null,
|
||||
birthTimeSource,
|
||||
expanded,
|
||||
boardId,
|
||||
onOpen,
|
||||
}: Readonly<{
|
||||
result: RectificationCandidateResult | null;
|
||||
declaredTime?: string | null;
|
||||
birthTimeSource?: string | null;
|
||||
expanded: boolean;
|
||||
boardId: string;
|
||||
onOpen: () => void;
|
||||
}>) {
|
||||
const peek = rectificationBoardPeekCopy(result, declaredTime);
|
||||
const peek = rectificationBoardPeekCopy(result, declaredTime, birthTimeSource);
|
||||
return (
|
||||
<button
|
||||
className="rectification-board-peek"
|
||||
|
||||
@@ -65,6 +65,9 @@ export function RectificationRangeDelivery({
|
||||
{RECTIFICATION_USER_COPY.rangeDeliveryCollectClosed}
|
||||
</p>
|
||||
) : null}
|
||||
{delivery?.provenance_line ? (
|
||||
<p className="rectification-range-delivery__provenance">{delivery.provenance_line}</p>
|
||||
) : null}
|
||||
{delivery?.tie_break_available && onTieBreak && !readonly ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* User-facing labels for the declared birth-time source.
|
||||
* Scoring still uses reportedBirthTime as the search-window centre.
|
||||
*/
|
||||
|
||||
export const PROVENANCE_SOURCES = ["hospital_record", "approximate", "period_only"] as const;
|
||||
|
||||
export type ProvenanceSource = (typeof PROVENANCE_SOURCES)[number];
|
||||
|
||||
const CLOCK = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
||||
|
||||
export function normalizeBirthTimeSource(value: string | null | undefined): ProvenanceSource {
|
||||
if (value === "hospital_record") return "hospital_record";
|
||||
if (value === "period_only") return "period_only";
|
||||
return "approximate";
|
||||
}
|
||||
|
||||
export const REPORTED_TIME_LABEL: Readonly<Record<ProvenanceSource, string>> = {
|
||||
hospital_record: "你的出生记录时间",
|
||||
approximate: "你填的大概时间",
|
||||
period_only: "你给的时间段",
|
||||
};
|
||||
|
||||
/** Approximate / period_only copy must not match this. Hospital uses 出生记录时间. */
|
||||
export const FORBIDDEN_DECLARED_BIRTH_TIME_PHRASE = /你的出生时间/;
|
||||
|
||||
export function reportedTimeLabel(source: string | null | undefined): string {
|
||||
return REPORTED_TIME_LABEL[normalizeBirthTimeSource(source)];
|
||||
}
|
||||
|
||||
export function boardDeclaredTimeLine(time: string, source?: string | null): string {
|
||||
const kind = normalizeBirthTimeSource(source);
|
||||
if (kind === "hospital_record") return `出生记录时间 ${time}`;
|
||||
if (kind === "period_only") return `你给的时间段 ${time}`;
|
||||
return `你填的大概时间 ${time}`;
|
||||
}
|
||||
|
||||
export function boardDeclaredTimePeek(time: string, source?: string | null): string {
|
||||
const kind = normalizeBirthTimeSource(source);
|
||||
if (kind === "hospital_record") return `出生记录 ${time}`;
|
||||
if (kind === "period_only") return `时间段 ${time}`;
|
||||
return `大概 ${time}`;
|
||||
}
|
||||
|
||||
function clockMinutes(value: string): number | null {
|
||||
const clock = value.trim().slice(0, 5);
|
||||
if (!CLOCK.test(clock)) return null;
|
||||
const hours = Number(clock.slice(0, 2));
|
||||
const minutes = Number(clock.slice(3, 5));
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
export function minutesBetweenClocks(left: string, right: string): number | null {
|
||||
const start = clockMinutes(left);
|
||||
const end = clockMinutes(right);
|
||||
if (start == null || end == null) return null;
|
||||
return Math.abs(start - end);
|
||||
}
|
||||
|
||||
function clockInRange(clock: string, range: readonly [string, string]): boolean {
|
||||
const time = clockMinutes(clock);
|
||||
const start = clockMinutes(range[0]);
|
||||
const end = clockMinutes(range[1]);
|
||||
if (time == null || start == null || end == null) return false;
|
||||
return time >= Math.min(start, end) && time <= Math.max(start, end);
|
||||
}
|
||||
|
||||
function offsetMinutes(reported: string, range: readonly [string, string] | null, representative: string | null): number | null {
|
||||
if (representative) {
|
||||
const direct = minutesBetweenClocks(reported, representative);
|
||||
if (direct != null) return direct;
|
||||
}
|
||||
if (!range) return null;
|
||||
if (clockInRange(reported, range)) return 0;
|
||||
const toStart = minutesBetweenClocks(reported, range[0]);
|
||||
const toEnd = minutesBetweenClocks(reported, range[1]);
|
||||
if (toStart == null) return toEnd;
|
||||
if (toEnd == null) return toStart;
|
||||
return Math.min(toStart, toEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* How the declared clock sits next to the current range. Null when there is
|
||||
* no comparable minute (typical for period_only).
|
||||
*/
|
||||
export function reportedTimeOffsetCopy(input: {
|
||||
source?: string | null;
|
||||
reportedTime?: string | null;
|
||||
range?: readonly [string, string] | null;
|
||||
representativeTime?: string | null;
|
||||
}): string | null {
|
||||
const reported = (input.reportedTime ?? "").trim().slice(0, 5);
|
||||
if (!CLOCK.test(reported)) return null;
|
||||
const source = normalizeBirthTimeSource(input.source);
|
||||
const offset = offsetMinutes(reported, input.range ?? null, input.representativeTime ?? null);
|
||||
if (offset == null) return null;
|
||||
if (source === "hospital_record") {
|
||||
const inside = input.range ? clockInRange(reported, input.range) : offset === 0;
|
||||
if (inside) return `出生记录时间 ${reported},落在目前范围内`;
|
||||
const edge = input.range
|
||||
? Math.min(
|
||||
minutesBetweenClocks(reported, input.range[0]) ?? Number.POSITIVE_INFINITY,
|
||||
minutesBetweenClocks(reported, input.range[1]) ?? Number.POSITIVE_INFINITY,
|
||||
)
|
||||
: offset;
|
||||
const gap = Number.isFinite(edge) ? edge : offset;
|
||||
return `出生记录时间 ${reported}。目前范围不含这一分钟,相差 ${gap} 分钟`;
|
||||
}
|
||||
if (source === "period_only") {
|
||||
return `与你给的时间段参照 ${reported} 相差 ${offset} 分钟`;
|
||||
}
|
||||
return `与你填的大概时间 ${reported} 相差 ${offset} 分钟`;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
REPRESENTATIVE_MINUTE_DISCLAIMER,
|
||||
nonConvergingRangeNarration,
|
||||
} from "../user-copy.ts";
|
||||
import { normalizeBirthTimeSource, type ProvenanceSource } from "../birth-time-provenance.ts";
|
||||
|
||||
export { REPRESENTATIVE_MINUTE_DISCLAIMER, nonConvergingRangeNarration };
|
||||
|
||||
@@ -199,6 +200,8 @@ export type RectificationDecision = Readonly<{
|
||||
terminationCopy?: string | null;
|
||||
/** Close lead with unused D9/D10 style questions: ask those before delivering. */
|
||||
heldForTieBreak?: boolean;
|
||||
/** Declared-time source for copy. Missing means approximate. */
|
||||
birthTimeSource?: "hospital_record" | "approximate" | "period_only";
|
||||
}>;
|
||||
|
||||
export type DecideRectificationInput = Readonly<{
|
||||
@@ -930,6 +933,7 @@ export function publicNextAction(decision: RectificationDecision): Readonly<{
|
||||
representative_time: string | null;
|
||||
credible_range: readonly [string, string] | null;
|
||||
stop_reason: EvidenceStopReason | null;
|
||||
birth_time_source?: ProvenanceSource;
|
||||
}> {
|
||||
return {
|
||||
type: decision.nextAction,
|
||||
@@ -945,6 +949,7 @@ export function publicNextAction(decision: RectificationDecision): Readonly<{
|
||||
representative_time: decision.representativeTime,
|
||||
credible_range: decision.credibleRange,
|
||||
stop_reason: decision.stopReason ?? null,
|
||||
birth_time_source: normalizeBirthTimeSource(decision.birthTimeSource),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
import { PROBE_EXPLAIN_COPY } from "./v9/probe-explain.ts";
|
||||
import { STEP_STATE_COPY } from "./v9/step-state.ts";
|
||||
import {
|
||||
REPORTED_TIME_LABEL,
|
||||
boardDeclaredTimeLine,
|
||||
reportedTimeOffsetCopy,
|
||||
} from "./birth-time-provenance.ts";
|
||||
|
||||
export const REPRESENTATIVE_MINUTE_DISCLAIMER = "这只是代表性候选,不是已确认的唯一出生分钟。";
|
||||
|
||||
@@ -606,6 +611,23 @@ export function listUserVisibleCopy(): string[] {
|
||||
...Object.values(USER_COLLECT_QUESTION_RETRY),
|
||||
"还有吗?比如第一份工作、搬到别的城市、谈恋爱或分手、家里添丁或长辈住院。",
|
||||
"再说一件带年月的事",
|
||||
REPORTED_TIME_LABEL.hospital_record,
|
||||
REPORTED_TIME_LABEL.approximate,
|
||||
REPORTED_TIME_LABEL.period_only,
|
||||
boardDeclaredTimeLine("05:00", "hospital_record"),
|
||||
boardDeclaredTimeLine("05:00", "approximate"),
|
||||
reportedTimeOffsetCopy({
|
||||
source: "approximate",
|
||||
reportedTime: "05:00",
|
||||
range: ["04:48", "05:07"],
|
||||
representativeTime: "04:53",
|
||||
}) ?? "",
|
||||
reportedTimeOffsetCopy({
|
||||
source: "hospital_record",
|
||||
reportedTime: "05:12",
|
||||
range: ["04:48", "05:07"],
|
||||
representativeTime: "04:53",
|
||||
}) ?? "",
|
||||
];
|
||||
return values;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { rangeDeliveryForSnapshot } from "@/lib/rectification-agentic/v9/diverge
|
||||
import { rectificationFollowupCatalog } from "@/lib/rectification-agentic/v9/decision-from-dossier";
|
||||
import { tieBreakGateInput, tieBreakPersonalityAvailable } from "@/lib/rectification-agentic/v9/method-followup";
|
||||
import { latestResultToolProjection } from "@/mastra/rectification-v9-tools";
|
||||
import { normalizeBirthTimeSource } from "@/lib/rectification-agentic/birth-time-provenance";
|
||||
|
||||
export function dossierResponse(
|
||||
dossier: V9CaseDossier,
|
||||
@@ -31,6 +32,7 @@ export function dossierResponse(
|
||||
const listed = options.listed ?? { focuses: [] as const, available: true };
|
||||
const turnsWithQuestions = attachQuestionsToTurns(dossier.turns, listed.focuses);
|
||||
const fields = publicDecisionFields(decision);
|
||||
const birthTimeSource = normalizeBirthTimeSource(dossier.case.birthTimeSource);
|
||||
const overlaid = dossier.latestResult
|
||||
? overlayPublicDecision(dossier.latestResult, decision)
|
||||
: fields;
|
||||
@@ -55,6 +57,7 @@ export function dossierResponse(
|
||||
completed_at: dossier.case.completedAt,
|
||||
closed_reason: dossier.case.closedReason,
|
||||
last_activity_at: dossier.case.lastActivityAt,
|
||||
birth_time_source: birthTimeSource,
|
||||
case_revision: previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null)?.revision ?? 0,
|
||||
},
|
||||
turns: turns.map((turn) => ({
|
||||
@@ -75,6 +78,7 @@ export function dossierResponse(
|
||||
: null,
|
||||
interview: {
|
||||
...fields,
|
||||
birth_time_source: birthTimeSource,
|
||||
collection_progress: collectionProgressFromReceipt(dossier.latestResult?.decisionReceipt ?? null),
|
||||
},
|
||||
current_question: projectCurrentQuestion(dossier.conversationSummary.activeFocus),
|
||||
@@ -121,6 +125,8 @@ function publicLatestResult(
|
||||
key.includes("varga.d9") || key.includes("varga.d10")
|
||||
)),
|
||||
stillTied: decision.separation.lead <= 1 && decision.separation.ranked.length >= 2,
|
||||
reportedBirthTime: dossier.case.reportedBirthTime,
|
||||
birthTimeSource: dossier.case.birthTimeSource,
|
||||
});
|
||||
const withDelivery = {
|
||||
...projected,
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
import { evidenceLedgerFingerprint } from "./tool-service";
|
||||
import { followupCaseArgs, blockScanDeclinedForFingerprint } from "./block-scan.ts";
|
||||
import { openingRangeFromCandidateRange } from "../user-copy.ts";
|
||||
import { normalizeBirthTimeSource } from "../birth-time-provenance.ts";
|
||||
import {
|
||||
representativeNearWindowEdge,
|
||||
} from "./search-window.ts";
|
||||
@@ -926,6 +927,7 @@ export function decideFromDossier(
|
||||
...narrowingExhaustion(dossier, inference, options, catalog),
|
||||
}),
|
||||
droppedProbes: mergeDroppedProbes(gated.dropped, nakshatra.dropped),
|
||||
birthTimeSource: normalizeBirthTimeSource(dossier.case.birthTimeSource),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -971,30 +973,33 @@ export function decideAfterInferenceChange(input: {
|
||||
if (!input.state) {
|
||||
const evidenceStops = evidenceStopInputs(input.dossier.evidence);
|
||||
const caseStage = input.dossier.case.stage === "block_scan" ? "block_scan" : "minute";
|
||||
return decideRectification({
|
||||
methodCoverageAll: blockingMethodsCovered(collecting.methods),
|
||||
trainingGateOpen: caseStage === "block_scan"
|
||||
? evidenceStops.datedEventCount >= MIN_STANDALONE_DATED_EVENTS
|
||||
&& evidenceStops.datedDomainCount >= MIN_STANDALONE_DATED_DOMAINS
|
||||
: trainingScoreableGate(input.dossier.evidence).open,
|
||||
candidateScores: [],
|
||||
userStopped: input.userStopped,
|
||||
snapshotCurrent,
|
||||
engineCeiling: engineCapabilityCeilingFromReceipt(input.dossier.latestResult?.decisionReceipt ?? null),
|
||||
...decisionBudgetFromInference(null),
|
||||
...evidenceStops,
|
||||
caseStage: input.dossier.case.stage === "block_scan" ? "block_scan" : "minute",
|
||||
blockScanDeclined: blockScanDeclinedForFingerprint(
|
||||
input.dossier.case.blockScan,
|
||||
evidenceLedgerFingerprint(input.dossier.evidence as never),
|
||||
),
|
||||
windowWidenSuggested: windowWidenSuggestedFromDossier(
|
||||
input.dossier,
|
||||
evidenceLedgerFingerprint(input.dossier.evidence as never),
|
||||
),
|
||||
openingCandidateRange: openingRangeFromCandidateRange(input.dossier.case.candidateRange),
|
||||
...tieBreakHold,
|
||||
});
|
||||
return {
|
||||
...decideRectification({
|
||||
methodCoverageAll: blockingMethodsCovered(collecting.methods),
|
||||
trainingGateOpen: caseStage === "block_scan"
|
||||
? evidenceStops.datedEventCount >= MIN_STANDALONE_DATED_EVENTS
|
||||
&& evidenceStops.datedDomainCount >= MIN_STANDALONE_DATED_DOMAINS
|
||||
: trainingScoreableGate(input.dossier.evidence).open,
|
||||
candidateScores: [],
|
||||
userStopped: input.userStopped,
|
||||
snapshotCurrent,
|
||||
engineCeiling: engineCapabilityCeilingFromReceipt(input.dossier.latestResult?.decisionReceipt ?? null),
|
||||
...decisionBudgetFromInference(null),
|
||||
...evidenceStops,
|
||||
caseStage: input.dossier.case.stage === "block_scan" ? "block_scan" : "minute",
|
||||
blockScanDeclined: blockScanDeclinedForFingerprint(
|
||||
input.dossier.case.blockScan,
|
||||
evidenceLedgerFingerprint(input.dossier.evidence as never),
|
||||
),
|
||||
windowWidenSuggested: windowWidenSuggestedFromDossier(
|
||||
input.dossier,
|
||||
evidenceLedgerFingerprint(input.dossier.evidence as never),
|
||||
),
|
||||
openingCandidateRange: openingRangeFromCandidateRange(input.dossier.case.candidateRange),
|
||||
...tieBreakHold,
|
||||
}),
|
||||
birthTimeSource: normalizeBirthTimeSource(input.dossier.case.birthTimeSource),
|
||||
};
|
||||
}
|
||||
const training = input.state.events.filter((item) => item.usage === "training");
|
||||
const trainingDomains = new Set(training.map((item) => item.domain));
|
||||
@@ -1075,6 +1080,7 @@ export function decideAfterInferenceChange(input: {
|
||||
...narrowingExhaustion(input.dossier, input.state, undefined, catalog),
|
||||
}),
|
||||
droppedProbes: mergeDroppedProbes(gated.dropped, nakshatra.dropped),
|
||||
birthTimeSource: normalizeBirthTimeSource(input.dossier.case.birthTimeSource),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
rangeDeliveryWindowLine,
|
||||
sharedTraitLine,
|
||||
} from "../user-copy.ts";
|
||||
import { reportedTimeOffsetCopy } from "../birth-time-provenance.ts";
|
||||
import { previousInferenceFromReceipt } from "./inference-adapter.ts";
|
||||
import {
|
||||
rangeNarrowHint,
|
||||
@@ -84,6 +85,7 @@ export type RangeDeliveryProjection = Readonly<{
|
||||
narrow_hint: string | null;
|
||||
tie_break_available: boolean;
|
||||
tie_break_note: string | null;
|
||||
provenance_line: string | null;
|
||||
}>;
|
||||
|
||||
export type PublicCandidateClock = Readonly<{
|
||||
@@ -338,6 +340,8 @@ export function buildRangeDelivery(input: {
|
||||
tieBreakAvailable?: boolean;
|
||||
tieBreakUsed?: boolean;
|
||||
stillTied?: boolean;
|
||||
reportedBirthTime?: string | null;
|
||||
birthTimeSource?: string | null;
|
||||
}): RangeDeliveryProjection {
|
||||
const inference = input.inference;
|
||||
const range = input.credibleRange
|
||||
@@ -428,6 +432,12 @@ export function buildRangeDelivery(input: {
|
||||
&& input.stillTied === true
|
||||
? RECTIFICATION_USER_COPY.rangeDeliveryTieBreakUsed
|
||||
: null,
|
||||
provenance_line: reportedTimeOffsetCopy({
|
||||
source: input.birthTimeSource,
|
||||
reportedTime: input.reportedBirthTime,
|
||||
range: normalizedRange,
|
||||
representativeTime,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -488,6 +498,10 @@ export function rangeDeliveryForSnapshot(snapshot: {
|
||||
tieBreakAvailable?: boolean;
|
||||
tieBreakUsed?: boolean;
|
||||
stillTied?: boolean;
|
||||
reportedBirthTime?: string | null;
|
||||
reported_birth_time?: string | null;
|
||||
birthTimeSource?: string | null;
|
||||
birth_time_source?: string | null;
|
||||
} | null | undefined): RangeDeliveryProjection {
|
||||
const receipt = snapshot?.decisionReceipt ?? snapshot?.decision_receipt ?? null;
|
||||
const inference = previousInferenceFromReceipt(receipt);
|
||||
@@ -520,6 +534,8 @@ export function rangeDeliveryForSnapshot(snapshot: {
|
||||
tieBreakAvailable: snapshot?.accepted !== true && snapshot?.tieBreakAvailable === true,
|
||||
tieBreakUsed: snapshot?.tieBreakUsed === true,
|
||||
stillTied: snapshot?.stillTied === true,
|
||||
reportedBirthTime: snapshot?.reportedBirthTime ?? snapshot?.reported_birth_time,
|
||||
birthTimeSource: snapshot?.birthTimeSource ?? snapshot?.birth_time_source,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -667,5 +683,10 @@ export function parseRangeDelivery(value: unknown): RangeDeliveryProjection | nu
|
||||
: typeof row.tieBreakNote === "string" && row.tieBreakNote.trim()
|
||||
? row.tieBreakNote.trim()
|
||||
: null,
|
||||
provenance_line: typeof row.provenance_line === "string" && row.provenance_line.trim()
|
||||
? row.provenance_line.trim()
|
||||
: typeof row.provenanceLine === "string" && row.provenanceLine.trim()
|
||||
? row.provenanceLine.trim()
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type RectificationCandidateResult,
|
||||
type RectificationHouseTable,
|
||||
} from "./rectification-candidate-result";
|
||||
import { boardDeclaredTimePeek } from "./rectification-agentic/birth-time-provenance";
|
||||
|
||||
export const RECTIFICATION_BOARD_SPLIT_MIN_PX = 768;
|
||||
|
||||
@@ -163,6 +164,7 @@ export function diffRectificationBoard(
|
||||
export function rectificationBoardPeekCopy(
|
||||
result: RectificationCandidateResult | null,
|
||||
declaredTime: string | null = null,
|
||||
source?: string | null,
|
||||
): Readonly<{
|
||||
title: string;
|
||||
detail: string | null;
|
||||
@@ -171,7 +173,10 @@ export function rectificationBoardPeekCopy(
|
||||
if (!table) {
|
||||
// Before any candidate exists the board still has a time to show: the one
|
||||
// the reader declared. Without one, say what fills it later.
|
||||
return { title: "当前盘面", detail: declaredTime ? `填报 ${declaredTime}` : "补充经历后会在这里更新" };
|
||||
return {
|
||||
title: "当前盘面",
|
||||
detail: declaredTime ? boardDeclaredTimePeek(declaredTime, source) : "补充经历后会在这里更新",
|
||||
};
|
||||
}
|
||||
return { title: "当前盘面", detail: `${table.time} · 上升${table.lagna}` };
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
import type { PersistedRectificationTurn } from "../components/conversational-birth-time-rectification.tsx";
|
||||
import { BOOTSTRAP_PREPARE_TIMEOUT_MS } from "./home-bootstrap.ts";
|
||||
import { sessionOutcomeAllowsDelivery } from "./rectification-agentic/core/rectification-decision.ts";
|
||||
import {
|
||||
boardDeclaredTimeLine,
|
||||
normalizeBirthTimeSource,
|
||||
type ProvenanceSource,
|
||||
} from "./rectification-agentic/birth-time-provenance.ts";
|
||||
|
||||
/**
|
||||
* Upper bound on hydrating a Case (turns + snapshot) after `/cases/open`
|
||||
@@ -82,6 +87,7 @@ export type RectificationCaseSnapshotPayload = Readonly<{
|
||||
*/
|
||||
candidate_range?: unknown;
|
||||
stage?: unknown;
|
||||
birth_time_source?: unknown;
|
||||
}>;
|
||||
turns?: unknown;
|
||||
}>;
|
||||
@@ -469,7 +475,10 @@ export function rectificationAdoptingLabel(time: string): string {
|
||||
}
|
||||
|
||||
/** Board copy when no candidate result exists yet (task 1.3). */
|
||||
export function rectificationBoardEmptyCopy(declaredTime: string | null): Readonly<{
|
||||
export function rectificationBoardEmptyCopy(
|
||||
declaredTime: string | null,
|
||||
source?: string | null,
|
||||
): Readonly<{
|
||||
clock: string | null;
|
||||
lines: readonly string[];
|
||||
}> {
|
||||
@@ -478,10 +487,15 @@ export function rectificationBoardEmptyCopy(declaredTime: string | null): Readon
|
||||
}
|
||||
return {
|
||||
clock: declaredTime,
|
||||
lines: [`填报出生时间 ${declaredTime}`, "回答几个问题后,这里会显示宫位随时间的变化。"],
|
||||
lines: [boardDeclaredTimeLine(declaredTime, source), "回答几个问题后,这里会显示宫位随时间的变化。"],
|
||||
};
|
||||
}
|
||||
|
||||
export function birthTimeSourceFromSnapshot(payload: RectificationCaseSnapshotPayload | null | undefined): ProvenanceSource {
|
||||
const raw = payload?.case?.birth_time_source;
|
||||
return normalizeBirthTimeSource(typeof raw === "string" ? raw : null);
|
||||
}
|
||||
|
||||
/** The declared birth minute from the profile, or null when only a period was given. */
|
||||
export function declaredBirthTime(profile: Readonly<{ time?: string; reportedTime?: string }>): string | null {
|
||||
const candidate = (profile.reportedTime || profile.time || "").trim();
|
||||
|
||||
Reference in New Issue
Block a user