/** * User-visible rectification copy. Tone lives here; decision gates do not. * * See frontend/docs/VOICE.md. Semantic constraints in comments must survive * rewrites: question-slot ownership, representative-minute boundary, and * no unique-minute claim. */ import { PROBE_EXPLAIN_COPY } from "./v9/probe-explain.ts"; import { STEP_STATE_COPY } from "./v9/step-state.ts"; import { REPORTED_TIME_LABEL, boardDeclaredTimeLine, reportedTimeOffsetCopy, SWITCH_TO_RECTIFIED_LABEL, switchToRectifiedConfirmCopy, } from "./birth-time-provenance.ts"; export const REPRESENTATIVE_MINUTE_DISCLAIMER = "这只是代表性候选,不是已确认的唯一出生分钟。"; /** Phrases that keep the unique-minute boundary in delivery/adopt turns. */ export const BOUNDARY_SEMANTIC_PHRASES = [ "不是已确认的唯一出生分钟", "不是已经确认的唯一出生分钟", ] as const; export type CollectDomain = | "relationship" | "career" | "family" | "occupation" | "education" | "relocation" | "finance" | "health_pressure" | "other"; export const GENERIC_COLLECT_QUESTION = "先说你最容易想起的一两件,年月大概就行。"; /** One-time suffix on the first dated-collect stem. Not a chase-more turn. */ export const FIRST_DATED_COLLECT_INVITE = "想到别的也可以一起说。"; export function withFirstDatedCollectInvite(stem: string): string { const spoken = stem.trim(); if (!spoken || spoken.includes(FIRST_DATED_COLLECT_INVITE)) return spoken; return `${spoken}${FIRST_DATED_COLLECT_INVITE}`; } /** Six everyday examples named in the opening body. Not years. */ export const OPENING_COLLECT_DOMAINS = [ "上大学", "第一份工作", "搬到别的城市", "谈恋爱或结婚", "家里添丁或长辈住院", "生病受伤", ] as const; export function openingSpokenBody(range: readonly [string, string] | null): string { const window = formatClockRange(range) ?? "当前这段"; return [ `眼下按 ${window} 来核对,用你记得的经历对照大运和盘面变化。`, "最后给区间和代表分钟,不给精确到秒。", `先说你最容易想起的一两件,比如${OPENING_COLLECT_DOMAINS.join("、")},年月大概就行。`, ].join(""); } const OPENING_YEAR = /(?:19|20)\d{2}/; const OPENING_SENTENCE = /(?<=[。!?])/; export function isAcceptableOpeningBody(body: string): boolean { const spoken = body.trim(); if (!spoken || OPENING_YEAR.test(spoken)) return false; const sentences = spoken.split(OPENING_SENTENCE).map((part) => part.trim()).filter(Boolean); if (sentences.length === 0 || sentences.length > 4) return false; const hits = OPENING_COLLECT_DOMAINS.filter((domain) => spoken.includes(domain)).length; return hits >= 5; } /** Everyday words a collect spoken prompt must mention for its server domain. */ export const COLLECT_DOMAIN_KEYWORDS: Readonly> = { education: ["上学", "大学", "毕业", "考试", "学业"], finance: ["收入", "买房", "贷款", "欠债", "钱"], relocation: ["搬家", "住", "外地"], health_pressure: ["生病", "受伤", "住院", "压力"], family: ["家里", "父母", "家人", "添丁"], career: ["工作", "入职", "换工作"], relationship: ["交往", "分手", "结婚", "感情"], occupation: ["工作", "做什么", "职业"], }; export const USER_COLLECT_QUESTION: Readonly> = { relationship: "感情这边,还记得哪年认真在一起、分开,或结婚吗?有年份就很好。", career: "工作上呢,还记得哪年入职、换工作,或职责一下子变重吗?", family: "家里如果有结婚、添丁或住院这类事,记得大概哪年就行。", occupation: "你平时主要做什么工作?", education: "上学这边,还记得哪年升学、转学或大考吗?", relocation: "有没有哪年搬家,或开始长期住在外地?", finance: "钱的方面,还记得哪年收入明显变过、有过大笔支出,或欠过债吗?", health_pressure: "身体或压力这边,还记得哪年生病、受伤,或特别难熬的一段时间吗?", }; export function collectQuestionForDomain( domain: string, _probeYear?: number | null, ): string | undefined { return USER_COLLECT_QUESTION[domain]; } /** Second phrasing when that domain's collect focus was already established and not declined. */ export const USER_COLLECT_QUESTION_RETRY: Readonly> = { relationship: "回到感情这边——刚才说的工作我记下了,哪年认真在一起或分开还记得吗?", career: "回到工作这边——刚才那件我记下了,哪年入职或换工作还记得吗?", family: "再问一次家里:结婚、添丁或住院,大概哪年?", occupation: "你平时主要做什么工作?刚才还没说到这块。", education: "回到上学这边——哪年升学、转学或大考,还记得吗?", relocation: "搬家或开始长期住外地,大概是哪年?", finance: "钱的方面再对一下:哪年收入明显变过、有过大笔支出,或欠过债?", health_pressure: "身体或压力这边再问一次:哪年生病、受伤,或特别难熬?", }; /** Count=2 → 「这两分钟」; >2 → 「这几个候选」; unknown / 0 / 1 → omit. */ export function rangeDeliveryCollectClosedSubject(candidateCount?: number): string | null { if (candidateCount === 2) return "这两分钟"; if (typeof candidateCount === "number" && Number.isFinite(candidateCount) && candidateCount > 2) { return "这几个候选"; } return null; } export function rangeDeliveryCollectClosed(candidateCount?: number): string { const subject = rangeDeliveryCollectClosedSubject(candidateCount); return subject ? `${subject}按现有信息分不开。` : "按现有信息分不开。"; } export const RECTIFICATION_USER_COPY = { genericCollectQuestion: GENERIC_COLLECT_QUESTION, collectQuestionByDomain: USER_COLLECT_QUESTION, collectQuestionRetryByDomain: USER_COLLECT_QUESTION_RETRY, choicePrompt: "直接点下面的选项就行,打字回答也一样算数。", unclearFocusReply: "我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。", classifierUnavailableReply: "这边没接上,把刚才那句再发一次就行。", declaredWindowLockedReply: "搜索范围是开始时按你的资料定的,校正过程中不改。想按别的时间段重来,请先到资料里改出生时间,再新建一次校正。", questionUpdated: "这一问刚换成新的,刷新后再答就行。", adoptCue: "我按你说的经历认真分析过了,下面是这次的结果。", hostNarrationFallback: "我按现有材料继续往下收。", collectHandoff: "接下来我们继续。", evidenceNotRecorded: "这件我还没记上。请再说一次大概年月和发生的事。", collectDeclinedAck: "记下了,这条按没有发生过记。", collectSkippedAck: "记下了,这题先放着,后面换个问法再问一次。", firstDatedCollectInvite: FIRST_DATED_COLLECT_INVITE, uncertaintyStop: "前面几道题你多半选了\"说不好\",再问下去也分不开,先停在这里。", tiedFirstStop: "几个候选打成平手,问题已经分不开它们。", probePoolExhaustedStop: "能分开候选的问题已经问完,先按现有材料给你结果。", postAdoptVerifyDone: "前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。", insufficientEventsGate: "再来一件记得大概年月的事就能开始筛。", insufficientDomainsGate: "再来一件不是同一类的、记得大概年月的事就能开始筛。", lowDateQualityGate: "两件事的日期还没对清。", noCandidatesGate: "当前还排不出可比较的候选时间。", forceMinuteAfterSubBlocks: "时段分不开,直接按分钟比。", compareFailedRetry: "候选比较这次没跑成,下一句话时会自动再试。", rescoreSkipped: "这次没有重新比较,稍后再比一次。", lastSuccessfulCompareRange: "这是按上一次成功比较给出的范围。", deferredCareerWindow: "下一次事业变动的预测窗口留在采用后的核对阶段。", rangeDeliveryTitle: "目前范围", rangeDeliveryMoreLikeThis: "更像这个", rangeDeliveryRelativeLikelihood: "相对可能性", rangeDeliveryNoWindow: "未来一年没有明显的时段", rangeDeliveryNotCompared: "这一分钟还没对照", rangeDeliveryAdopting: "正在采用…", rangeDeliveryAdopted: "已采用", rangeDeliveryTieBreakEntry: "再答两道参考题微调排序", rangeDeliveryTieBreakAck: "只微调排序,不改目前范围。", rangeDeliveryTieBreakUsed: "这两分钟按现有信息分不开,参考题已经用过。", rangeDeliveryAdoptLocked: "先答完上面这道,再选时间。", verificationReportSummary: "查看验证报告", } as const; /** Top-two relative-likelihood gap, in percentage points, that counts as a tie on the range card. */ export const RANGE_DELIVERY_TIE_PERCENT = 3; export function rangeDeliveryTopTwoTied( columns: readonly { probability_percent: number }[], ): boolean { if (columns.length < 2) return false; return Math.abs(columns[0].probability_percent - columns[1].probability_percent) <= RANGE_DELIVERY_TIE_PERCENT; } export const RANGE_DELIVERY_DOMAIN_LABEL: Readonly> = { education: "学业", career: "事业", relationship: "感情", family: "家人", relocation: "搬家", finance: "财务", health_pressure: "健康", occupation: "职业", other: "其他", }; export function rangeDeliveryEventCopy(count: number): string { return `对照了 ${count} 件经历`; } export function RANGE_DELIVERY_MORE_MINUTES(count: number): string { return `还有 ${count} 个分钟可能性更低,已并入范围`; } function spokenYearMonth(yearMonth: string): string | null { const match = /^(\d{4})(?:-(\d{2}))?/.exec(yearMonth.trim()); if (!match) return null; const year = Number(match[1]); const month = match[2] ? Number(match[2]) : null; if (!Number.isFinite(year)) return null; return month && month >= 1 && month <= 12 ? `${year} 年 ${month} 月` : `${year} 年`; } export type RangeDeliveryFitCounts = Readonly<{ strong: number; medium: number; weak: number; total: number; worst: Readonly<{ year_month: string; domain: string }> | null; }>; export function rangeDeliveryFitLine(fit: RangeDeliveryFitCounts | null): string { if (!fit) return RECTIFICATION_USER_COPY.rangeDeliveryNotCompared; const total = fit.total >= 0 ? fit.total : fit.strong + fit.medium + fit.weak; const matched = fit.strong + fit.medium; const worstMonth = fit.worst ? spokenYearMonth(fit.worst.year_month) : null; const worstDomain = fit.worst ? RANGE_DELIVERY_DOMAIN_LABEL[fit.worst.domain] ?? fit.worst.domain : ""; const worst = worstMonth && worstDomain ? `${worstMonth}那段${worstDomain}` : null; return worst ? `${total} 件经历里 ${matched} 件对得上,最不合的是 ${worst}` : `${total} 件经历里 ${matched} 件对得上`; } export function rangeDeliveryWindowLine( windows: readonly { domain: string; from: string; to: string }[] | null, ): string { if (windows == null) return RECTIFICATION_USER_COPY.rangeDeliveryNotCompared; const next = windows[0]; if (!next) return RECTIFICATION_USER_COPY.rangeDeliveryNoWindow; const month = spokenYearMonth(next.from) ?? next.from; const domain = RANGE_DELIVERY_DOMAIN_LABEL[next.domain] ?? next.domain; return `下一个值得留意的时段:${month}前后(${domain})`; } export function sharedTraitLine(line: string, columnCount: number): string { const countLabel = columnCount === 2 ? "两个时间" : columnCount === 3 ? "三个时间" : `${columnCount} 个时间`; const career = /^事业盘落在([^,]+),通常表现为(.+)$/.exec(line); if (career) return `${countLabel}的事业盘都在${career[1]}:${career[2]}`; const relationship = /^关系盘落在([^,]+),通常表现为(.+)$/.exec(line); if (relationship) return `${countLabel}的关系盘都在${relationship[1]}:${relationship[2]}`; return line; } export function postAdoptVerifyDoneCopy( time: string | null | undefined, hadVerifyQuestions: boolean, ): string { if (hadVerifyQuestions) return RECTIFICATION_USER_COPY.postAdoptVerifyDone; const clock = time?.trim().slice(0, 5) ?? ""; return clock ? `已采用 ${clock},新建对话即按这个时间排盘。` : "已采用当前时间,新建对话即按这个时间排盘。"; } export function withCompareFailedRetryNotice(body: string): string { const notice = RECTIFICATION_USER_COPY.compareFailedRetry; const spoken = body.trim(); if (!spoken) return notice; if (spoken.includes(notice)) return spoken; return `${spoken}\n\n${notice}`; } export function stripRangeProgressClaims(body: string): string { const spoken = body.trim(); if (!spoken) return ""; const kept = spoken .split(/(?<=[。!?])/) .map((part) => part.trim()) .filter((part) => part && !/收窄|范围从/.test(part)); return kept.join(""); } export function withRescoreSkippedNotice(body: string): string { const notice = RECTIFICATION_USER_COPY.rescoreSkipped; const spoken = stripRangeProgressClaims(body); if (spoken.includes("这次没有重新比较")) return spoken; if (!spoken) return `记下了。${notice}`; const prefix = /[。!?]$/.test(spoken) ? spoken : `${spoken}。`; if (spoken.includes("记下了")) return `${prefix}${notice}`; return `${prefix}记下了。${notice}`; } export function withLastSuccessfulCompareNotice(body: string): string { const notice = RECTIFICATION_USER_COPY.lastSuccessfulCompareRange; const spoken = body.trim(); if (!spoken) return notice; if (spoken.includes(notice)) return spoken; return `${spoken}\n\n${notice}`; } export const ACCEPTANCE_GATE_COPY: Readonly> = { insufficient_events: RECTIFICATION_USER_COPY.insufficientEventsGate, insufficient_dated_events: RECTIFICATION_USER_COPY.insufficientEventsGate, insufficient_domain_diversity: RECTIFICATION_USER_COPY.insufficientDomainsGate, insufficient_domains: RECTIFICATION_USER_COPY.insufficientDomainsGate, low_date_quality: RECTIFICATION_USER_COPY.lowDateQualityGate, no_candidates: RECTIFICATION_USER_COPY.noCandidatesGate, }; export function acceptanceGateNarration( reasons: readonly string[] | null | undefined, missingEventCount?: number | null, ): string | null { const tokens = (reasons ?? []).map((item) => item.trim()).filter(Boolean); if ( (tokens.includes("insufficient_events") || tokens.includes("insufficient_dated_events")) ) { return RECTIFICATION_USER_COPY.insufficientEventsGate; } for (const reason of tokens) { const copy = ACCEPTANCE_GATE_COPY[reason]; if (copy) return copy; } return null; } export type RangeNarrationVariant = "delivery" | "intermediate"; export type RangeNarrationInput = { credibleRange?: readonly [string, string] | null; representativeTime?: string | null; openingRange?: readonly [string, string] | null; variant?: RangeNarrationVariant; stopReason?: string | null; /** Extra stop sentence after BUG-503 prefixes; used when the probe pool is exhausted. */ stopExplain?: string | null; eventCount?: number | null; fitPercent?: number | null; }; function clockMinutes(value: string): number | null { const match = /^(\d{2}):(\d{2})$/.exec(value.trim()); if (!match) return null; return Number(match[1]) * 60 + Number(match[2]); } export function rangeWidthMinutes(start: string, end: string): number | null { const from = clockMinutes(start); const to = clockMinutes(end); if (from == null || to == null) return null; const width = to >= from ? to - from : to + 24 * 60 - from; return width > 0 ? width : null; } export function formatClockRange(range: readonly [string, string] | null | undefined): string | null { if (!range?.[0] || !range[1]) return null; return range[0] === range[1] ? range[0] : `${range[0]}–${range[1]}`; } export function rangeChangedAfterEvidence( from: readonly [string, string] | null | undefined, to: readonly [string, string] | null | undefined, ): string | null { const fromText = formatClockRange(from); const toText = formatClockRange(to); if (!fromText || !toText || fromText === toText) return null; return `范围从 ${fromText} 变为 ${toText}。`; } export function withRangeChangedAfterEvidence( body: string, from: readonly [string, string] | null | undefined, to: readonly [string, string] | null | undefined, ): string { const notice = rangeChangedAfterEvidence(from, to); if (!notice) return body; const spoken = body.trim(); if (!spoken) return notice; if (spoken.includes(notice)) return spoken; const prefix = /[。!?]$/.test(spoken) ? spoken : `${spoken}。`; return `${prefix}${notice}`; } export function openingRangeFromCandidateRange( range: { start_time?: string | null; end_time?: string | null } | null | undefined, ): readonly [string, string] | null { const start = range?.start_time?.trim().slice(0, 5) || ""; const end = range?.end_time?.trim().slice(0, 5) || ""; return start && end ? [start, end] : null; } export function containsBoundarySemantics(text: string): boolean { return BOUNDARY_SEMANTIC_PHRASES.some((phrase) => text.includes(phrase)); } export function stopReasonPrefix(reason: string | null | undefined): string | null { if (reason === "user_uncertainty_too_high") return RECTIFICATION_USER_COPY.uncertaintyStop; if (reason === "tied_first") return RECTIFICATION_USER_COPY.tiedFirstStop; if (reason === "probe_pool_exhausted") return RECTIFICATION_USER_COPY.probePoolExhaustedStop; return null; } function progressClause( openingRange: readonly [string, string] | null | undefined, rangeText: string, currentRange: readonly [string, string], ): string | null { if (!openingRange) return null; const openingWidth = rangeWidthMinutes(openingRange[0], openingRange[1]); const currentWidth = rangeWidthMinutes(currentRange[0], currentRange[1]); if (!openingWidth || !currentWidth || currentWidth >= openingWidth) return null; return `已经从最初的 ${openingWidth} 分钟收到 ${rangeText} 这 ${currentWidth} 分钟`; } export function nonConvergingRangeNarration( input: RangeNarrationInput = {}, boundaryCopy = REPRESENTATIVE_MINUTE_DISCLAIMER, ): string { const range = input.credibleRange ?? null; const representative = input.representativeTime?.trim() || null; const rangeText = formatClockRange(range); const variant = input.variant ?? "delivery"; const progress = range && rangeText ? progressClause(input.openingRange, rangeText, range) : null; const reason = variant === "delivery" ? stopReasonPrefix(input.stopReason) : null; const explain = variant === "delivery" ? (input.stopExplain?.trim() || null) : null; const prefix = [reason, explain].filter(Boolean).join(""); if (variant === "intermediate") { if (rangeText && representative) { return progress ? `${progress}。眼下更像 ${representative}。` : `范围已经收到 ${rangeText} 附近,眼下更像 ${representative}。`; } if (rangeText) { return progress ? `${progress}。` : `范围已经收到 ${rangeText} 附近。`; } if (representative) return `眼下更像 ${representative}。`; return "当前几个候选还分不开。"; } if (rangeText && representative) { const collapsed = Boolean(range && range[0] === range[1]); if (collapsed) { const head = progress ?? `眼下更站得住的是 ${representative}`; const body = `${head}。${boundaryCopy}`; return prefix ? `${prefix}${body}` : body; } const head = progress ?? `更站得住的范围是 ${rangeText},代表分钟 ${representative}`; const representativeClause = progress ? `。代表分钟是 ${representative}。` : `。`; const body = `${head}${representativeClause}${boundaryCopy}`; return prefix ? `${prefix}${body}` : body; } if (rangeText) { const head = progress ?? `更站得住的范围是 ${rangeText}`; const body = `${head}。${boundaryCopy}`; return prefix ? `${prefix}${body}` : body; } if (representative) { const body = `当前代表分钟 ${representative}。${boundaryCopy}`; return prefix ? `${prefix}${body}` : body; } const body = `当前几个候选还分不开。${boundaryCopy}`; return prefix ? `${prefix}${body}` : body; } export function deliveryTurnNarration(input: RangeNarrationInput = {}): string { const rangeText = formatClockRange(input.credibleRange ?? null); const sentence1 = rangeText ? `目前范围 ${rangeText}。` : "当前几个候选还分不开。"; const count = typeof input.eventCount === "number" && input.eventCount >= 0 ? input.eventCount : 0; const percent = typeof input.fitPercent === "number" && Number.isFinite(input.fitPercent) ? Math.round(input.fitPercent) : null; const sentence2 = percent != null ? `对照了 ${count} 件经历,事件吻合率 ${percent}%。` : `对照了 ${count} 件经历。`; return `${sentence1}${sentence2}${REPRESENTATIVE_MINUTE_DISCLAIMER}`; } export function deliveryAdoptNarration(input: RangeNarrationInput): string { return deliveryTurnNarration({ ...input, variant: "delivery" }); } const INSTRUCTION_TONE_SENTENCE = /不得|请写成/; const ENGINE_MEANING_REPLACEMENTS: ReadonlyArray = [ [/冲突时不能按更高把握收口。?/g, "两边看法还不一致,先不往更窄处收。"], [/下面是本轮实际执行的技法,不能当作唯一分钟确认。?/g, ""], [/不能确认唯一分钟或声称已完成精确分钟校准。?/g, "还不能把某一分钟当成已确认。"], [/缺少该验证时不能确认唯一分钟。?/g, "所以还不能把某一分钟当成已确认。"], [/不能确认唯一分钟。/g, "还不能把某一分钟当成已确认。"], ]; /** * Consumer-side layer: engine `user_meaning` stays as model/audit material. * User-visible surfaces must run this (or an equivalent display_copy field). */ export function engineMeaningToDisplayCopy(meaning: string | null | undefined): string { if (!meaning) return ""; const replaced = ENGINE_MEANING_REPLACEMENTS.reduce( (text, [pattern, next]) => text.replace(pattern, next), meaning, ); return replaced .split(/(?<=[。!?;;])\s*/) .filter((sentence) => sentence.trim() && !INSTRUCTION_TONE_SENTENCE.test(sentence)) .join("") .replace(/\s+/g, " ") .trim(); } export function listUserVisibleCopy(): string[] { const values: string[] = [ REPRESENTATIVE_MINUTE_DISCLAIMER, GENERIC_COLLECT_QUESTION, RECTIFICATION_USER_COPY.choicePrompt, RECTIFICATION_USER_COPY.unclearFocusReply, RECTIFICATION_USER_COPY.classifierUnavailableReply, RECTIFICATION_USER_COPY.declaredWindowLockedReply, RECTIFICATION_USER_COPY.questionUpdated, RECTIFICATION_USER_COPY.adoptCue, RECTIFICATION_USER_COPY.hostNarrationFallback, RECTIFICATION_USER_COPY.collectHandoff, RECTIFICATION_USER_COPY.evidenceNotRecorded, RECTIFICATION_USER_COPY.collectDeclinedAck, RECTIFICATION_USER_COPY.collectSkippedAck, FIRST_DATED_COLLECT_INVITE, GENERIC_COLLECT_QUESTION, openingSpokenBody(["04:45", "05:15"]), RECTIFICATION_USER_COPY.uncertaintyStop, RECTIFICATION_USER_COPY.tiedFirstStop, RECTIFICATION_USER_COPY.probePoolExhaustedStop, RECTIFICATION_USER_COPY.insufficientEventsGate, RECTIFICATION_USER_COPY.insufficientDomainsGate, RECTIFICATION_USER_COPY.lowDateQualityGate, RECTIFICATION_USER_COPY.noCandidatesGate, RECTIFICATION_USER_COPY.forceMinuteAfterSubBlocks, RECTIFICATION_USER_COPY.postAdoptVerifyDone, RECTIFICATION_USER_COPY.compareFailedRetry, RECTIFICATION_USER_COPY.rescoreSkipped, RECTIFICATION_USER_COPY.lastSuccessfulCompareRange, RECTIFICATION_USER_COPY.deferredCareerWindow, RECTIFICATION_USER_COPY.rangeDeliveryTitle, RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis, RECTIFICATION_USER_COPY.rangeDeliveryRelativeLikelihood, RECTIFICATION_USER_COPY.rangeDeliveryNoWindow, RECTIFICATION_USER_COPY.rangeDeliveryNotCompared, RECTIFICATION_USER_COPY.rangeDeliveryAdopting, RECTIFICATION_USER_COPY.rangeDeliveryAdopted, RECTIFICATION_USER_COPY.rangeDeliveryTieBreakEntry, RECTIFICATION_USER_COPY.rangeDeliveryTieBreakAck, RECTIFICATION_USER_COPY.rangeDeliveryTieBreakUsed, RECTIFICATION_USER_COPY.rangeDeliveryAdoptLocked, rangeDeliveryCollectClosed(2), rangeDeliveryCollectClosed(4), rangeDeliveryCollectClosed(), RECTIFICATION_USER_COPY.verificationReportSummary, postAdoptVerifyDoneCopy("04:53", false), rangeChangedAfterEvidence(["04:50", "04:57"], ["04:47", "05:15"]) ?? "", RANGE_DELIVERY_MORE_MINUTES(2), rangeDeliveryEventCopy(3), rangeDeliveryFitLine({ strong: 4, medium: 3, weak: 1, total: 8, worst: { year_month: "2024-05", domain: "relationship" }, }), rangeDeliveryWindowLine([{ domain: "career", from: "2027-03", to: "2027-03" }]), sharedTraitLine("事业盘落在巨蟹座,通常表现为做事以照顾人为主,在意团队里的感受", 3), deliveryTurnNarration({ credibleRange: ["04:51", "04:59"], representativeTime: "04:53", eventCount: 7, fitPercent: 80, }), "关系盘落在天秤座,通常表现为", "事业盘落在巨蟹座,通常表现为", "刚才那个日子是查过记录,还是凭记忆?", PROBE_EXPLAIN_COPY.unsureImpact, PROBE_EXPLAIN_COPY.splitGroups, `${PROBE_EXPLAIN_COPY.vargaWhyPrefix} D9 ${PROBE_EXPLAIN_COPY.vargaWhySuffix}`, STEP_STATE_COPY.collect.headline, STEP_STATE_COPY.collect.reason, STEP_STATE_COPY.collect.next, STEP_STATE_COPY.discriminate.headline, STEP_STATE_COPY.discriminate.reason, STEP_STATE_COPY.discriminate.next, STEP_STATE_COPY.deliver.headline, STEP_STATE_COPY.deliver.reason, STEP_STATE_COPY.deliver.next, STEP_STATE_COPY.deliverExhausted.reason, STEP_STATE_COPY.deliverUncertain.reason, STEP_STATE_COPY.postAdopt.headline, STEP_STATE_COPY.postAdopt.reason, STEP_STATE_COPY.postAdopt.next, "这不是已确认的唯一出生分钟。", "这只是粗看。", "的判断是稳定的", "会随分钟变,看盘时按范围读。", "没有整段都稳定的主题", "没有会随分钟变的主题。", "事业方向", "婚恋(D9)", "性格底色", ...Object.values(USER_COLLECT_QUESTION), ...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", }) ?? "", SWITCH_TO_RECTIFIED_LABEL, switchToRectifiedConfirmCopy("05:12", "04:53"), ]; return values; } export function accidentCaseHardcodedTurns(input: { openingRange?: readonly [string, string] | null; credibleRange: readonly [string, string]; representativeTime: string; }): { openingCollect: string; discriminatorRedirect: string; unclearFocusReply: string; staleQuestion: string; intermediateRange: string; financeCollect: string; deliveryAdopt: string; } { const shared = { credibleRange: input.credibleRange, representativeTime: input.representativeTime, openingRange: input.openingRange ?? null, }; return { openingCollect: GENERIC_COLLECT_QUESTION, discriminatorRedirect: RECTIFICATION_USER_COPY.choicePrompt, unclearFocusReply: RECTIFICATION_USER_COPY.unclearFocusReply, staleQuestion: RECTIFICATION_USER_COPY.questionUpdated, intermediateRange: nonConvergingRangeNarration({ ...shared, variant: "intermediate" }), financeCollect: USER_COLLECT_QUESTION.finance, deliveryAdopt: deliveryAdoptNarration(shared), }; }