Files
Jyotisha/frontend/src/lib/rectification-agentic/user-copy.ts
T
jesse-ux cfb41daf3d
Independent Staging Quality Gate / validate (push) Failing after 6m28s
Independent Staging Quality Gate / publish (push) Skipped
feat(rectification): 出卡加精度门槛,补经历改成系统点名
宽度超过 10 分钟或头名并列时不再出交付卡,改为按大运边界逐条问、
用类型芯片和年/月选择器录入。跳过的线换问法再问一次;答「这类事
都没有过」的不再问。用户说「没有了」仍立刻给目前范围。Skill 10.0.27。

BUG-740~743
2026-09-16 18:35:27 +08:00

671 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<Record<string, readonly string[]>> = {
education: ["上学", "大学", "毕业", "考试", "学业"],
finance: ["收入", "买房", "贷款", "欠债", "钱"],
relocation: ["搬家", "住", "外地"],
health_pressure: ["生病", "受伤", "住院", "压力"],
family: ["家里", "父母", "家人", "添丁"],
career: ["工作", "入职", "换工作"],
relationship: ["交往", "分手", "结婚", "感情"],
occupation: ["工作", "做什么", "职业"],
};
export const USER_COLLECT_QUESTION: Readonly<Record<string, string>> = {
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<Record<string, string>> = {
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<Record<string, string>> = {
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<Record<string, string>> = {
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<readonly [RegExp, string]> = [
[/冲突时不能按更高把握收口。?/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),
};
}