fix: make rectification follow-up actionable

This commit is contained in:
Jesse_Chen
2026-07-21 22:53:56 +08:00
parent bfc6870614
commit f4ac6ce75e
11 changed files with 296 additions and 29 deletions
@@ -458,6 +458,21 @@ function boundaryDistance(range: { readonly startTime: string; readonly endTime:
return Math.max(0, Math.min(value - start, end - value));
}
async function rectificationPacketStage<Value>(
stage: "score_events" | "scan" | "merge_scans" | "candidate_differences" | "time_links" | "technical_packet",
operation: () => Value | Promise<Value>,
): Promise<Value> {
try {
return await operation();
} catch (error) {
const errorKind = error instanceof Error ? error.name : typeof error;
const status = error !== null && typeof error === "object" && "status" in error
&& typeof error.status === "number" ? error.status : null;
console.error(`[birth-time-conversation-packet] stage=${stage} error=${errorKind}${status === null ? "" : ` status=${status}`}`);
throw error;
}
}
export async function buildProductionConversationalRectificationPacket(
engine: BirthTimeJourneyEngine,
input: ConversationalRectificationPacketBuildInput,
@@ -466,21 +481,23 @@ export async function buildProductionConversationalRectificationPacket(
if (place.latitude === undefined || place.longitude === undefined) {
throw new ConversationalRectificationError("profile_incomplete");
}
const latitude = place.latitude;
const longitude = place.longitude;
const baseRange = currentRange(input);
const events = scoreableLifeEvents(
input.evidence as readonly LifeEventEvidence[],
input.declaredBirthInput.birthDate,
);
const eventScore: CandidateResult | null = events.length >= 3
? await engine.scoreEvents({
? await rectificationPacketStage("score_events", () => engine.scoreEvents({
birthDate: input.declaredBirthInput.birthDate,
startTime: baseRange.startTime,
endTime: baseRange.endTime,
lat: place.latitude,
lon: place.longitude,
lat: latitude,
lon: longitude,
tz: place.timezoneOffset,
events,
})
}))
: null;
const selectedRange = !input.preserveCandidateRange && eventScore?.winningSegment
? { startTime: eventScore.winningSegment.startTime, endTime: eventScore.winningSegment.endTime }
@@ -488,25 +505,28 @@ export async function buildProductionConversationalRectificationPacket(
const questionnaires: RectificationQuestionnaire[] = [];
for (const scanRange of boundedScanRanges(selectedRange)) {
const scanPoint = scanCoordinates(scanRange);
const { questionnaire } = await engine.scan({
const { questionnaire } = await rectificationPacketStage("scan", () => engine.scan({
birthTime: `${input.declaredBirthInput.birthDate} ${scanPoint.centerTime}`,
uncertaintyMinutes: scanPoint.uncertaintyMinutes,
lat: place.latitude,
lon: place.longitude,
lat: latitude,
lon: longitude,
tz: place.timezoneOffset,
ayanamsa: "lahiri",
});
}));
questionnaires.push(questionnaire);
}
const questionnaire = mergeQuestionnaireScans(questionnaires, selectedRange);
const candidateDifferences = await engine.buildDifferencePacket({
const questionnaire = await rectificationPacketStage(
"merge_scans",
() => mergeQuestionnaireScans(questionnaires, selectedRange),
);
const candidateDifferences = await rectificationPacketStage("candidate_differences", () => engine.buildDifferencePacket({
caseId: input.caseId,
asOfDate: input.asOfDate,
birthDate: input.declaredBirthInput.birthDate,
startTime: selectedRange.startTime,
endTime: selectedRange.endTime,
lat: place.latitude,
lon: place.longitude,
lat: latitude,
lon: longitude,
tz: place.timezoneOffset,
evidence: [],
events,
@@ -515,18 +535,22 @@ export async function buildProductionConversationalRectificationPacket(
partitionFingerprints: [],
recentRanges: [],
candidateModel: null,
});
}));
const calculationVersion = eventScore
? `${candidateDifferences.packet.scoringVersion}+${eventScore.algorithmVersion}`
: candidateDifferences.packet.scoringVersion;
const metadata = layerMetadata(questionnaire, calculationVersion);
const timeLinkedScanSamples = await rectificationPacketStage(
"time_links",
() => sampleTimes(questionnaire),
);
const representative = eventScore?.winningSegment?.representativeTime
?? scanCoordinates(selectedRange).centerTime;
const { buildRectificationTechnicalPacket } = await import(
"../../../lib/conversational-rectification/technical-packet.ts"
);
return {
packet: buildRectificationTechnicalPacket({
packet: await rectificationPacketStage("technical_packet", () => buildRectificationTechnicalPacket({
scan: questionnaire,
candidateDifferences,
eventScore: input.preserveCandidateRange && eventScore
@@ -537,11 +561,11 @@ export async function buildProductionConversationalRectificationPacket(
calculationVersion,
availableLayers: metadata.availableLayers,
layerReferences: metadata.layerReferences,
timeLinkedScanSamples: sampleTimes(questionnaire),
timeLinkedScanSamples,
boundaryDistanceMinutes: boundaryDistance(selectedRange, representative),
futureWindows: [],
},
}),
})),
resultId: eventScore?.resultId ?? null,
};
}
+13 -1
View File
@@ -459,8 +459,17 @@ button:disabled { cursor: default; opacity: .45; }
.conversational-domain-picker { display: grid; gap: var(--space-3); margin: 0; padding: 0; border: 0; }
.conversational-domain-picker .conversational-domain-question { margin-bottom: var(--space-2); color: var(--color-ink); font-size: var(--type-body-md); font-weight: 600; line-height: 1.5; }
.conversational-domain-picker > div { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--space-2); }
.conversational-domain-picker button { padding: var(--space-2) var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; text-align: left; }
.conversational-domain-picker button { display: grid; gap: var(--space-1); padding: var(--space-2) var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); cursor: pointer; text-align: left; }
.conversational-domain-picker button > span { font-size: var(--type-body-sm); font-weight: 600; }
.conversational-domain-picker button > small { color: var(--color-ink-secondary); font-size: var(--type-overline); font-weight: 400; }
.conversational-domain-picker button[aria-pressed="true"] { border-color: var(--color-action); background: var(--color-action-soft); color: var(--color-action); }
.conversational-domain-picker button[aria-label$="下一步建议"] { border-color: color-mix(in srgb, var(--color-action) 52%, var(--color-border)); background: var(--color-action-soft); }
.conversational-answer-pending { min-width: 0; display: grid; grid-template-columns: 40px minmax(0, 1fr); align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid color-mix(in srgb, var(--color-action) 24%, var(--color-border)); border-radius: var(--radius-lg); background: var(--color-action-soft); }
.conversational-answer-pending .app-loading-symbol { width: 40px; height: 40px; }
.conversational-answer-pending .app-loading-mark { width: 22px; height: 22px; }
.conversational-answer-pending > div:last-child { min-width: 0; display: grid; gap: var(--space-1); }
.conversational-answer-pending strong { color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 600; }
.conversational-answer-pending span { color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.5; }
.conversational-composer { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); }
.conversational-composer label { display: grid; gap: var(--space-1); font-size: var(--type-body-sm); font-weight: 600; }
.conversational-composer label span { color: var(--color-ink-secondary); font-size: var(--type-caption); font-weight: 400; line-height: 1.5; }
@@ -486,6 +495,9 @@ button:disabled { cursor: default; opacity: .45; }
.conversational-candidate dt { color: var(--color-ink-tertiary); font-size: var(--type-overline); }
.conversational-candidate dd { min-width: 0; margin: 0; color: var(--color-ink); font-size: var(--type-body-sm); overflow-wrap: anywhere; }
.conversational-candidate time { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.conversational-candidate-progress { min-width: 0; display: grid; gap: var(--space-1); padding-top: var(--space-3); border-top: 1px solid var(--color-border); }
.conversational-candidate-progress strong { color: var(--color-ink); font-size: var(--type-body-sm); font-weight: 600; }
.conversational-candidate-progress span { color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.55; text-wrap: pretty; }
.conversational-confirmation { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); background: var(--color-action-soft); }
.conversational-status { min-width: 0; min-height: 1px; }
.conversational-original-question { background: var(--color-success-muted); }
+8 -4
View File
@@ -1953,12 +1953,16 @@ export default function Home() {
&& session.rectificationCaseId === null)
?? null
: null;
const rectificationSession = sourceSession.sessionType === "birth_time_rectification"
const canReuseSourceRectificationSession = action === "resume"
&& sourceSession.sessionType === "birth_time_rectification"
&& account.rectificationCase !== null
&& (sourceSession.rectificationCaseId === account.rectificationCase.caseId
|| sourceSession.rectificationCaseId === null);
const rectificationSession = canReuseSourceRectificationSession
? sourceSession
: resumableSession ?? createSession(modelCatalog.defaultModelId, "birth_time_rectification");
const reusingRectificationSession = rectificationSession !== sourceSession
? resumableSession !== null
: sourceSession.sessionType === "birth_time_rectification";
const reusingRectificationSession = canReuseSourceRectificationSession
|| resumableSession !== null;
const requestedQuestion = pendingConsultationQuestion
?? (reusingRectificationSession
? null