fix(rectification): remove static interview fallbacks
This commit is contained in:
@@ -20,6 +20,7 @@ export const RECTIFICATION_TOOL_DONE_LABELS: Readonly<Record<PublicRectification
|
||||
"rectification-offer-candidates": "生成候选建议",
|
||||
"rectification-accept-candidate": "采用候选时间",
|
||||
"rectification-confirm-birth-time": "确认校正时间",
|
||||
"rectification-stop-and-review": "暂停证据收集",
|
||||
"rectification-close-case": "完成校正记录",
|
||||
};
|
||||
|
||||
@@ -45,6 +46,7 @@ export const RECTIFICATION_TOOL_PROGRESS_LABELS: Readonly<Record<PublicRectifica
|
||||
"rectification-offer-candidates": "正在生成候选建议…",
|
||||
"rectification-accept-candidate": "正在采用候选时间…",
|
||||
"rectification-confirm-birth-time": "正在确认校正时间…",
|
||||
"rectification-stop-and-review": "正在暂停证据收集…",
|
||||
"rectification-close-case": "正在完成校正记录…",
|
||||
};
|
||||
|
||||
@@ -83,6 +85,7 @@ export function rectificationToolActivityPhase(tool: PublicRectificationTool): P
|
||||
if (
|
||||
tool === "rectification-accept-candidate"
|
||||
|| tool === "rectification-confirm-birth-time"
|
||||
|| tool === "rectification-stop-and-review"
|
||||
|| tool === "rectification-close-case"
|
||||
) {
|
||||
return "answer-composition";
|
||||
|
||||
@@ -89,4 +89,4 @@ export function evidenceWritesAllowed(
|
||||
export const MAX_RESUMABLE_CASES_PER_USER = 1;
|
||||
|
||||
export const RECTIFICATION_SKILL_NAME = "jyotish-birth-time-rectification";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.12";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.13";
|
||||
|
||||
@@ -91,25 +91,6 @@ export type ChoiceCardEvidence = Readonly<{
|
||||
occurredTo: string | null;
|
||||
}>;
|
||||
|
||||
const PRIMARY_C = "没有明显发生";
|
||||
const SECONDARY_D = "不记得 / 不确定";
|
||||
const OPTION_A = "是,大概就在那段时间";
|
||||
const OPTION_B = "有类似,但年份不对或不够重大";
|
||||
const STYLE_NEITHER = "两边都不像";
|
||||
const QUALITY_A = "有,失常或压力很大";
|
||||
const QUALITY_B = "有压力,但不算失常";
|
||||
const QUALITY_C = "没有明显失常";
|
||||
|
||||
const AGE_BAND: Record<string, { lo: number; hi: number; family: string; varga: string | null }> = {
|
||||
education: { lo: 16, hi: 18, family: "升学、高考、转学或学习环境变化", varga: "D5 / D24" },
|
||||
relocation: { lo: 18, hi: 24, family: "搬家、离乡或长期异地", varga: "D4" },
|
||||
relationship: { lo: 21, hi: 26, family: "认真关系进入、结束或关系观明显转变", varga: "D9" },
|
||||
career: { lo: 22, hi: 30, family: "入职、升职或职责明显加重", varga: "D10" },
|
||||
family: { lo: 18, hi: 30, family: "家人相关的明显变化", varga: "D12 / D7 / D3" },
|
||||
finance: { lo: 22, hi: 32, family: "收入、资产或财务明显变化", varga: "D2 / D11" },
|
||||
health_pressure: { lo: 16, hi: 40, family: "健康、事故或持续压力明显变化", varga: "D30" },
|
||||
};
|
||||
|
||||
const THEME_DOMAIN: Record<string, string> = {
|
||||
relationship_style: "relationship",
|
||||
career_style: "career",
|
||||
@@ -178,27 +159,15 @@ function pickProbe(
|
||||
return pool[0] ?? probes[0] ?? null;
|
||||
}
|
||||
|
||||
function ageBandPeriod(birthDate: string | null | undefined, domain: string | null): string {
|
||||
const year = yearFrom(birthDate ?? null);
|
||||
const band = domain ? AGE_BAND[domain] : null;
|
||||
if (year && band) return `${year + Math.floor((band.lo + band.hi) / 2)} 年前后`;
|
||||
return "那段时间";
|
||||
}
|
||||
|
||||
function periodFor(
|
||||
evidence: readonly ChoiceCardEvidence[] | undefined,
|
||||
domain: string | null,
|
||||
probes: readonly DiscriminatingEventProbe[] | undefined,
|
||||
birthDate?: string | null,
|
||||
_birthDate?: string | null,
|
||||
followup?: ChoiceCardFollowup,
|
||||
): string {
|
||||
const probe = pickProbe(probes, domain, followup);
|
||||
if (probe) return probe.year_label;
|
||||
if (domain && (evidence ?? []).some((item) => item.domain === domain && isConfirmedDated(item))) {
|
||||
return lifePeriodLabel(evidence ?? [], domain);
|
||||
}
|
||||
const band = ageBandPeriod(birthDate, domain);
|
||||
if (band !== "那段时间") return band;
|
||||
return lifePeriodLabel(evidence ?? [], domain);
|
||||
}
|
||||
|
||||
@@ -216,142 +185,47 @@ function eventLockPrompt(period: string, family: string): string {
|
||||
return `${period} · ${family}`.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function eventHypothesis(
|
||||
period: string,
|
||||
family: string,
|
||||
function withStyleOptionLabels(
|
||||
prompt: string,
|
||||
why: string,
|
||||
varga: string | null,
|
||||
): Hypothesis {
|
||||
styleOptions: readonly EventProbeStyleOption[],
|
||||
): Hypothesis | null {
|
||||
const labels = new Map<string, string>();
|
||||
for (const option of styleOptions) {
|
||||
const label = clippedCopy(option.label, 4, 80);
|
||||
if (!label || labels.has(option.answer_class)) return null;
|
||||
labels.set(option.answer_class, label);
|
||||
}
|
||||
const values = ["yes", "weak_yes", "no", "unsure"].map((answerClass) => labels.get(answerClass));
|
||||
if (values.some((value) => !value) || new Set(values).size !== values.length) return null;
|
||||
return {
|
||||
prompt: eventLockPrompt(period, family),
|
||||
prompt,
|
||||
why,
|
||||
varga,
|
||||
a: OPTION_A,
|
||||
b: OPTION_B,
|
||||
neither: PRIMARY_C,
|
||||
unsure: SECONDARY_D,
|
||||
};
|
||||
}
|
||||
|
||||
function withStyleOptionLabels(
|
||||
hypothesis: Hypothesis,
|
||||
styleOptions: readonly EventProbeStyleOption[],
|
||||
): Hypothesis {
|
||||
const label = (answerClass: string, fallback: string) => clippedCopy(
|
||||
styleOptions.find((item) => item.answer_class === answerClass)?.label,
|
||||
4,
|
||||
80,
|
||||
) ?? fallback;
|
||||
return {
|
||||
...hypothesis,
|
||||
a: label("yes", hypothesis.a),
|
||||
b: label("weak_yes", hypothesis.b),
|
||||
neither: label("no", hypothesis.neither),
|
||||
unsure: label("unsure", hypothesis.unsure),
|
||||
a: values[0]!,
|
||||
b: values[1]!,
|
||||
neither: values[2]!,
|
||||
unsure: values[3]!,
|
||||
};
|
||||
}
|
||||
|
||||
function hypothesisFor(
|
||||
followup: ChoiceCardFollowup,
|
||||
observations: readonly InternalVargaObservation[] | undefined,
|
||||
_observations: readonly InternalVargaObservation[] | undefined,
|
||||
evidence: readonly ChoiceCardEvidence[] | undefined,
|
||||
probes?: readonly DiscriminatingEventProbe[],
|
||||
birthDate?: string | null,
|
||||
): Hypothesis {
|
||||
const theme = followup.ask_theme;
|
||||
if (theme === "occupation") {
|
||||
return {
|
||||
prompt: "长期工作更像哪一类?",
|
||||
why: "职业说明独立于带日期的事业事件,对照本命第 10 宫和 D10。",
|
||||
varga: "D1-H10 + D10",
|
||||
a: "长期偏对外、领导或经营",
|
||||
b: "长期偏研究、技术或幕后转化",
|
||||
neither: "都不是,或职业经常变",
|
||||
unsure: SECONDARY_D,
|
||||
};
|
||||
}
|
||||
if (theme === "horary") {
|
||||
return {
|
||||
prompt: "有没有第一次认真问起这件事的时间?",
|
||||
why: "占问只作观察,不计分,也不挡给出时间卡。",
|
||||
varga: "占问观察盘",
|
||||
a: "记得第一次认真问起的大概时间",
|
||||
b: "有问起,但时间很模糊",
|
||||
neither: "没有专门问起过",
|
||||
unsure: SECONDARY_D,
|
||||
};
|
||||
}
|
||||
if (theme === "nakshatra_trait") {
|
||||
return {
|
||||
prompt: "近年处事方式更像哪一组?",
|
||||
why: "升点靠近两段日常节奏的交界。只用来偏置时间窗,不能确认唯一分钟。",
|
||||
varga: null,
|
||||
a: "更干脆、外放、说做就做",
|
||||
b: "更慢热、内收、反复权衡",
|
||||
neither: "都不像,或两边都有",
|
||||
unsure: SECONDARY_D,
|
||||
};
|
||||
}
|
||||
if (theme === "active_focus") {
|
||||
const domain = followup.domain;
|
||||
const probe = pickProbe(probes, domain, followup);
|
||||
const period = probe?.year_label ?? lifePeriodLabel(evidence ?? [], domain);
|
||||
return withStyleOptionLabels(eventHypothesis(
|
||||
period,
|
||||
probe?.event_family ?? "刚才那件待确认的经历",
|
||||
probe?.user_meaning ?? "先承接当前问题,不要另开领域清单。题干自己写。",
|
||||
probe ? AGE_BAND[probe.domain]?.varga ?? null : null,
|
||||
), followup.style_options ?? probe?.style_options ?? []);
|
||||
}
|
||||
): Hypothesis | null {
|
||||
const domain = followupDomain(followup);
|
||||
const probe = pickProbe(probes, domain, followup);
|
||||
const kind = followup.choice_kind ?? probe?.choice_kind ?? "existence";
|
||||
const styleOptions = followup.style_options ?? probe?.style_options ?? [];
|
||||
const family = probe?.event_family
|
||||
?? (domain ? AGE_BAND[domain]?.family : null)
|
||||
?? "带大概年份的经历";
|
||||
const varga = probe
|
||||
? AGE_BAND[probe.domain]?.varga ?? null
|
||||
: domain
|
||||
? AGE_BAND[domain]?.varga ?? null
|
||||
: "本命 Dasha + 行运";
|
||||
if (!probe?.event_family?.trim()) return null;
|
||||
const period = periodFor(evidence, domain, probes, birthDate, followup);
|
||||
const reverse = followup.method_id === "reverse_verify";
|
||||
const holdout = theme === "oos_blind";
|
||||
const why = reverse
|
||||
? "按当前采用时间核对一件前事。对得上写入账本并重算;对不上可以改选其他候选。不确认唯一分钟。题干自己写,年份不得发明。"
|
||||
: holdout
|
||||
? "这是采用后的盘外核对,答案不会改候选分数。题干自己写。"
|
||||
: probe?.user_meaning
|
||||
?? "用一件带年份的具体生平分开还在比的时间窗。题干自己写,年份不得发明。";
|
||||
void observations;
|
||||
if (kind === "varga_style") {
|
||||
const career = domain === "career" || followup.ask_theme === "career_style";
|
||||
return withStyleOptionLabels({
|
||||
prompt: career ? "长期工作更接近哪一类?" : "这段关系更接近哪一种相处?",
|
||||
why,
|
||||
varga,
|
||||
a: OPTION_A,
|
||||
b: OPTION_B,
|
||||
neither: STYLE_NEITHER,
|
||||
unsure: SECONDARY_D,
|
||||
}, styleOptions);
|
||||
}
|
||||
if (kind === "event_quality") {
|
||||
const exam = (probe?.domain ?? domain) === "education"
|
||||
|| /高考|考试发挥|发挥明显失常/.test(probe?.event_family ?? family);
|
||||
const hypothesis = eventHypothesis(
|
||||
period,
|
||||
family,
|
||||
why,
|
||||
exam ? varga ?? "D5 / D24" : varga,
|
||||
);
|
||||
return withStyleOptionLabels(
|
||||
exam ? { ...hypothesis, a: QUALITY_A, b: QUALITY_B, neither: QUALITY_C } : hypothesis,
|
||||
styleOptions,
|
||||
);
|
||||
}
|
||||
return withStyleOptionLabels(eventHypothesis(period, family, why, varga), styleOptions);
|
||||
const prompt = eventLockPrompt(period, probe.event_family);
|
||||
const why = probe.user_meaning?.trim() || followup.user_prompt_hint.trim();
|
||||
if (!why) return null;
|
||||
return withStyleOptionLabels(prompt, why, null, styleOptions);
|
||||
}
|
||||
|
||||
export function buildChoiceFrame(
|
||||
@@ -363,7 +237,7 @@ export function buildChoiceFrame(
|
||||
birthDate?: string | null;
|
||||
scoring?: boolean;
|
||||
} = {},
|
||||
): RectificationChoiceFrame {
|
||||
): RectificationChoiceFrame | null {
|
||||
const scoring = input.scoring !== false;
|
||||
const hypothesis = hypothesisFor(
|
||||
followup,
|
||||
@@ -372,6 +246,7 @@ export function buildChoiceFrame(
|
||||
input.probes,
|
||||
input.birthDate,
|
||||
);
|
||||
if (!hypothesis) return null;
|
||||
const domain = followupDomain(followup);
|
||||
return {
|
||||
question_id: `${followup.method_id}:${followup.ask_theme}:${scoring ? "score" : "holdout"}`,
|
||||
|
||||
@@ -38,12 +38,11 @@ export type ConfirmationGateBlocker = Readonly<{
|
||||
top_1_rate?: number;
|
||||
confirmation_coverage_rate?: number;
|
||||
sealed_benchmark_id?: string;
|
||||
failure_code?: string;
|
||||
}>;
|
||||
|
||||
export type UniqueMinutePath = "closed_at_representative" | "awaiting_user_consent";
|
||||
|
||||
export const UNIQUE_MINUTE_CLOSED_COPY = "本会话以代表性时间收口,不确认唯一分钟";
|
||||
|
||||
export function uniqueMinutePath(confirmationAllowed: boolean): UniqueMinutePath {
|
||||
return confirmationAllowed ? "awaiting_user_consent" : "closed_at_representative";
|
||||
}
|
||||
@@ -81,7 +80,7 @@ export function sessionOutcomeView(kind: SessionOutcomeKind): SessionOutcome {
|
||||
if (kind === "adopt_representative") {
|
||||
return {
|
||||
kind,
|
||||
user_meaning: `这次校正的收口是采用代表性时间作当前排盘。${UNIQUE_MINUTE_CLOSED_COPY}。`,
|
||||
user_meaning: "可以采用代表性候选作当前排盘;不得把它描述为已确认的唯一出生分钟。",
|
||||
};
|
||||
}
|
||||
if (kind === "discriminate_candidates") {
|
||||
@@ -123,7 +122,7 @@ export function sessionOutcomeView(kind: SessionOutcomeKind): SessionOutcome {
|
||||
if (kind === "completed_with_range") {
|
||||
return {
|
||||
kind,
|
||||
user_meaning: "无法可信地区分唯一分钟。本会话以可信区间和代表性工作时间收口,不确认唯一分钟。",
|
||||
user_meaning: "当前证据只能支持可信区间和代表性工作时间,不能确认唯一分钟。",
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -169,18 +168,29 @@ export function readVedastroMinuteSensitiveStatus(
|
||||
return "not_evaluated";
|
||||
}
|
||||
|
||||
function readVedastroFailureCode(
|
||||
decisionReceipt: Readonly<Record<string, unknown>> | null | undefined,
|
||||
): string | null {
|
||||
const gates = asRecord(decisionReceipt?.gates);
|
||||
const exact = asRecord(gates?.exact_confirmation);
|
||||
const validation = asRecord(exact?.vedastro_event_validation);
|
||||
const failure = asRecord(validation?.failure);
|
||||
return typeof failure?.code === "string" && failure.code.trim() ? failure.code.trim() : null;
|
||||
}
|
||||
|
||||
function vedastroPassed(status: string): boolean {
|
||||
return status === "passed";
|
||||
}
|
||||
|
||||
function vedastroUserMeaning(status: string): string {
|
||||
function vedastroUserMeaning(status: string, failureCode: string | null): string {
|
||||
if (status === "not_evaluated") {
|
||||
return "官方分钟敏感校验尚未跑通。未调用不等于失败,但缺这一层不能写确认。";
|
||||
return "分钟敏感校验尚未执行;缺少该验证时不能确认唯一分钟。";
|
||||
}
|
||||
if (status === "passed") {
|
||||
return "官方分钟敏感校验已通过。";
|
||||
return "分钟敏感校验已通过。";
|
||||
}
|
||||
return "官方分钟敏感校验未能区分相邻分钟,不能写确认。";
|
||||
if (failureCode) return `分钟敏感校验执行失败(${failureCode}),不能确认唯一分钟。`;
|
||||
return "分钟敏感校验未能区分相邻分钟,不能确认唯一分钟。";
|
||||
}
|
||||
|
||||
function adjacentPassed(candidates: readonly GateCandidate[], widthMinutes: number): boolean {
|
||||
@@ -210,6 +220,7 @@ export function buildConfirmationGate(input: {
|
||||
}): ConfirmationGate {
|
||||
const width = indistinguishableWidthMinutes(input.candidates);
|
||||
const vedastroStatus = readVedastroMinuteSensitiveStatus(input.decisionReceipt);
|
||||
const vedastroFailureCode = readVedastroFailureCode(input.decisionReceipt);
|
||||
const adjacentOk = adjacentPassed(input.candidates, width);
|
||||
const holdoutOk = holdoutPassed();
|
||||
const confirmationAllowed = input.engineConfirmationAllowed
|
||||
@@ -224,7 +235,8 @@ export function buildConfirmationGate(input: {
|
||||
{
|
||||
id: "vedastro_minute_sensitive",
|
||||
status: vedastroStatus,
|
||||
user_meaning: vedastroUserMeaning(vedastroStatus),
|
||||
...(vedastroFailureCode ? { failure_code: vedastroFailureCode } : {}),
|
||||
user_meaning: vedastroUserMeaning(vedastroStatus, vedastroFailureCode),
|
||||
},
|
||||
{
|
||||
id: "adjacent_minutes_indistinguishable",
|
||||
@@ -245,7 +257,7 @@ export function buildConfirmationGate(input: {
|
||||
sealed_benchmark_id: SEALED_MINUTE_HOLDOUT.sealed_benchmark_id,
|
||||
user_meaning: holdoutOk
|
||||
? "公开密封 holdout 已达发布门槛。"
|
||||
: `公开密封 holdout 未达发布门槛。${UNIQUE_MINUTE_CLOSED_COPY},也不能声称已校准到精确分钟。`,
|
||||
: "公开密封 holdout 未达发布门槛,不能确认唯一分钟或声称已完成精确分钟校准。",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
import {
|
||||
blockingMethodsCovered,
|
||||
buildMethodFollowupPlan,
|
||||
latestUserStoppedCollecting,
|
||||
} from "./method-followup";
|
||||
import { buildConfirmationGate } from "./confirmation-gate";
|
||||
import {
|
||||
MIN_ACCEPTANCE_DOMAINS,
|
||||
MIN_ACCEPTANCE_EVENTS,
|
||||
@@ -60,6 +60,7 @@ export type DecisionDossier = Readonly<{
|
||||
resultId?: string;
|
||||
decisionReceipt: Readonly<Record<string, unknown>> | null;
|
||||
selectionAllowed?: boolean;
|
||||
confirmationAllowed?: boolean;
|
||||
candidates?: readonly Readonly<{
|
||||
candidateId?: string;
|
||||
time: string;
|
||||
@@ -74,6 +75,7 @@ export type DecisionDossier = Readonly<{
|
||||
} | null;
|
||||
case: {
|
||||
acceptedTime: string | null;
|
||||
status?: string;
|
||||
};
|
||||
turns?: readonly Readonly<{ role: string; text: string | null }>[];
|
||||
}>;
|
||||
@@ -168,11 +170,21 @@ export function decideFromDossier(
|
||||
const storedFingerprint = dossier.latestResult?.evidenceLedgerFingerprint ?? "";
|
||||
const currentFingerprint = options?.currentEvidenceFingerprint ?? storedFingerprint;
|
||||
const snapshotCurrent = !storedFingerprint || storedFingerprint === currentFingerprint;
|
||||
const latest = dossier.latestResult;
|
||||
const confirmationGate = buildConfirmationGate({
|
||||
engineConfirmationAllowed: latest?.confirmationAllowed === true,
|
||||
candidates: (latest?.candidates ?? []).map((candidate) => ({
|
||||
time: candidate.time,
|
||||
rank: candidate.rank ?? Number.MAX_SAFE_INTEGER,
|
||||
tiedMinuteCount: candidate.tiedMinuteCount ?? Number.MAX_SAFE_INTEGER,
|
||||
})),
|
||||
decisionReceipt: latest?.decisionReceipt ?? null,
|
||||
});
|
||||
return decideRectification({
|
||||
methodCoverageAll: blockingMethodsCovered(collecting.methods),
|
||||
trainingGateOpen: trainingGate.open,
|
||||
confirmationAllowed: false,
|
||||
userStopped: latestUserStoppedCollecting(dossier.turns ?? []),
|
||||
confirmationAllowed: confirmationGate.confirmation_allowed,
|
||||
userStopped: dossier.case.status === "paused",
|
||||
snapshotCurrent,
|
||||
candidateScores: candidateScoresFromDossier(dossier.latestResult),
|
||||
discriminatorProbe: selectDiscriminatorProbe(contrastPacketFromDossier(dossier)),
|
||||
|
||||
@@ -398,9 +398,12 @@ function engineRequestBody(input: {
|
||||
|
||||
export type V9VedastroValidateResult = Readonly<{
|
||||
status: "passed" | "failed" | "not_evaluated";
|
||||
canConfirmExactMinute: false;
|
||||
canConfirmExactMinute: boolean;
|
||||
minuteSensitiveStatus: "passed" | "failed" | "not_evaluated";
|
||||
searchEventsSupportsLocalWinner: boolean;
|
||||
failure: Readonly<{
|
||||
code: "timeout" | "engine_request_failed" | "engine_invalid_response" | "unknown";
|
||||
}> | null;
|
||||
raw: Readonly<Record<string, unknown>>;
|
||||
}>;
|
||||
|
||||
@@ -420,12 +423,19 @@ export function mergeVedastroValidateIntoReceipt(
|
||||
exact.vedastro_event_validation = {
|
||||
status: validation.status,
|
||||
search_events_primary_supports_local_winner: validation.searchEventsSupportsLocalWinner,
|
||||
can_confirm_exact_minute: validation.canConfirmExactMinute,
|
||||
failure: validation.failure,
|
||||
};
|
||||
const minuteSensitiveStatus = validation.status === "passed"
|
||||
? validation.minuteSensitiveStatus
|
||||
: validation.status === "failed"
|
||||
? "failed"
|
||||
: "not_evaluated";
|
||||
if (
|
||||
(validation.minuteSensitiveStatus === "passed" || validation.minuteSensitiveStatus === "failed")
|
||||
(minuteSensitiveStatus === "passed" || minuteSensitiveStatus === "failed")
|
||||
&& (exact.external_validation_status === "not_evaluated" || exact.external_validation_status == null)
|
||||
) {
|
||||
exact.external_validation_status = validation.minuteSensitiveStatus;
|
||||
exact.external_validation_status = minuteSensitiveStatus;
|
||||
}
|
||||
gates.exact_confirmation = exact;
|
||||
next.gates = gates;
|
||||
@@ -444,6 +454,7 @@ export async function runV9VedastroValidate(input: {
|
||||
canConfirmExactMinute: false,
|
||||
minuteSensitiveStatus: "not_evaluated",
|
||||
searchEventsSupportsLocalWinner: false,
|
||||
failure: null,
|
||||
raw: { status: "not_evaluated", can_confirm_exact_minute: false },
|
||||
});
|
||||
if (input.candidateTimes[0] === input.candidateTimes[1]) return unevaluated();
|
||||
@@ -455,15 +466,38 @@ export async function runV9VedastroValidate(input: {
|
||||
);
|
||||
const eventValidation = record(data.event_validation);
|
||||
const minuteValidation = record(data.minute_sensitive_validation);
|
||||
const status = vedastroValidateStatus(data.status);
|
||||
const minuteSensitiveStatus = status === "passed"
|
||||
? vedastroValidateStatus(minuteValidation?.status)
|
||||
: status === "failed"
|
||||
? "failed"
|
||||
: "not_evaluated";
|
||||
return {
|
||||
status: vedastroValidateStatus(data.status),
|
||||
canConfirmExactMinute: false,
|
||||
minuteSensitiveStatus: vedastroValidateStatus(minuteValidation?.status),
|
||||
status,
|
||||
canConfirmExactMinute: data.can_confirm_exact_minute === true
|
||||
&& status === "passed"
|
||||
&& minuteSensitiveStatus === "passed",
|
||||
minuteSensitiveStatus,
|
||||
searchEventsSupportsLocalWinner: eventValidation?.search_events_primary_supports_local_winner === true,
|
||||
failure: null,
|
||||
raw: data,
|
||||
};
|
||||
} catch {
|
||||
return unevaluated();
|
||||
} catch (error) {
|
||||
const code = error instanceof RectificationEngineError
|
||||
? error.code === "engine_invalid_response"
|
||||
? "engine_invalid_response"
|
||||
: "engine_request_failed"
|
||||
: error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError")
|
||||
? "timeout"
|
||||
: "unknown";
|
||||
return {
|
||||
status: "failed",
|
||||
canConfirmExactMinute: false,
|
||||
minuteSensitiveStatus: "failed",
|
||||
searchEventsSupportsLocalWinner: false,
|
||||
failure: { code },
|
||||
raw: { status: "failed", failure: { code }, can_confirm_exact_minute: false },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
decideFromDossier,
|
||||
} from "./decision-from-dossier";
|
||||
import { evidenceLedgerFingerprint } from "./tool-service";
|
||||
import { latestUserStoppedCollecting, projectRectificationChoiceCard } from "./method-followup";
|
||||
import { projectRectificationChoiceCard } from "./method-followup";
|
||||
import { refinementFromDecisionReceipt } from "./refinement-packet";
|
||||
import {
|
||||
internalObservationsFromWindowScan,
|
||||
@@ -46,11 +46,18 @@ export function choiceCardFromCaseDossier(dossier: {
|
||||
resultId?: string;
|
||||
decisionReceipt: Readonly<Record<string, unknown>> | null;
|
||||
selectionAllowed?: boolean;
|
||||
candidates?: readonly Readonly<{ time: string; relativeSupport?: number }>[];
|
||||
confirmationAllowed?: boolean;
|
||||
candidates?: readonly Readonly<{
|
||||
time: string;
|
||||
rank?: number;
|
||||
tiedMinuteCount?: number;
|
||||
relativeSupport?: number;
|
||||
}>[];
|
||||
evidenceLedgerFingerprint?: string | null;
|
||||
} | null;
|
||||
case: {
|
||||
acceptedTime: string | null;
|
||||
status?: string;
|
||||
};
|
||||
turns?: readonly Readonly<{ role: string; text: string | null }>[];
|
||||
}): RectificationChoiceCard | null {
|
||||
@@ -92,13 +99,14 @@ export function choiceCardFromCaseDossier(dossier: {
|
||||
accepted: Boolean(dossier.case.acceptedTime),
|
||||
selectionAllowed: decision.selectionAllowed,
|
||||
proposeAllowed: decision.proposeAllowed,
|
||||
confirmationAllowed: decision.canConfirmExactMinute,
|
||||
caseRevision: inference?.revision ?? 0,
|
||||
contrastPacket,
|
||||
candidateScores: decision.separation.ranked.map((item) => ({
|
||||
time: item.time,
|
||||
score: item.score,
|
||||
})),
|
||||
userStopped: latestUserStoppedCollecting(dossier.turns ?? []),
|
||||
userStopped: dossier.case.status === "paused",
|
||||
latestAssistantText,
|
||||
candidatesSeparated: decision.separation.sufficient,
|
||||
holdoutValidation: decision.holdoutValidation,
|
||||
|
||||
@@ -428,22 +428,6 @@ function action(
|
||||
return { id, user_meaning };
|
||||
}
|
||||
|
||||
const USER_STOP_PATTERN = /结束校正|不想继续|直接给结果|就到这里/;
|
||||
const USER_STOP_NEGATION_PATTERN = /(?:不是|并非|不要|别|还没|未).{0,8}(?:结束校正|不想继续|直接给结果|就到这里)/;
|
||||
|
||||
export function latestUserStoppedCollecting(
|
||||
turns: readonly Readonly<{ role: string; text: string | null }>[],
|
||||
): boolean {
|
||||
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
||||
const turn = turns[index];
|
||||
if (turn.role !== "user") continue;
|
||||
const text = turn.text?.trim() ?? "";
|
||||
if (!text) continue;
|
||||
return USER_STOP_PATTERN.test(text) && !USER_STOP_NEGATION_PATTERN.test(text);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isOfferBlockingFollowup(
|
||||
followup: MethodFollowup | null,
|
||||
methods?: readonly MethodCoverage[],
|
||||
@@ -578,7 +562,7 @@ export function buildNextUserAction(input: {
|
||||
}
|
||||
const adopt = action(
|
||||
"adopt_representative",
|
||||
"本轮已有代表性候选时间。说明本会话以代表性时间收口,不确认唯一分钟,请用户采用下方时间卡片;采用后才用该时间看盘。不要只说记下了以后再说。",
|
||||
"已有可采用的代表性候选时间。请用户从下方时间卡片选择;采用后再用该时间看盘。不得把代表性候选说成已确认的唯一出生分钟。",
|
||||
);
|
||||
const provisional = action(
|
||||
"offer_provisional_range",
|
||||
@@ -1250,6 +1234,7 @@ export function projectRectificationChoiceCard(
|
||||
input: Parameters<typeof buildMethodFollowupPlan>[0] & {
|
||||
selectionAllowed?: boolean;
|
||||
proposeAllowed?: boolean;
|
||||
confirmationAllowed?: boolean;
|
||||
userStopped?: boolean;
|
||||
candidateScores?: readonly Readonly<{ time: string; score: number }>[];
|
||||
caseRevision?: number | null;
|
||||
@@ -1260,7 +1245,7 @@ export function projectRectificationChoiceCard(
|
||||
const sessionOutcome = conversationalSessionOutcome({
|
||||
selectionAllowed: input.selectionAllowed === true,
|
||||
proposeAllowed: input.proposeAllowed === true,
|
||||
confirmationAllowed: false,
|
||||
confirmationAllowed: input.confirmationAllowed === true,
|
||||
nextFollowup: plan.next_followup,
|
||||
methods: plan.methods,
|
||||
userStopped: input.userStopped,
|
||||
|
||||
@@ -48,6 +48,7 @@ export const PUBLIC_RECTIFICATION_TOOLS = [
|
||||
"rectification-offer-candidates",
|
||||
"rectification-accept-candidate",
|
||||
"rectification-confirm-birth-time",
|
||||
"rectification-stop-and-review",
|
||||
"rectification-close-case",
|
||||
] as const;
|
||||
|
||||
@@ -194,6 +195,7 @@ const TOOL_ACTIVITY: Readonly<Partial<Record<PublicRectificationTool, PublicRect
|
||||
"rectification-offer-candidates": "preparing_result",
|
||||
"rectification-accept-candidate": "preparing_result",
|
||||
"rectification-confirm-birth-time": "preparing_result",
|
||||
"rectification-stop-and-review": "preparing_result",
|
||||
"rectification-close-case": "preparing_result",
|
||||
};
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export function buildSkillVerificationReport(input: SkillVerificationReportInput
|
||||
|
||||
return [
|
||||
"## 生时纠正验证报告(skill 八法)",
|
||||
"这是当前窗的相对拟合,标签是 `candidate_range_not_birth_time_truth`。采用把代表性时间写入当前排盘;本会话以代表性时间收口,不是已确认唯一出生分钟。",
|
||||
"这是当前窗的相对拟合,标签是 `candidate_range_not_birth_time_truth`。采用只会把代表性时间写入当前排盘,不等于确认唯一出生分钟。",
|
||||
"",
|
||||
"### 筛选",
|
||||
tied
|
||||
|
||||
@@ -92,6 +92,7 @@ const TOOL_PHASE_ON_RESULT: Readonly<Record<PublicRectificationTool, PublicRecti
|
||||
"rectification-offer-candidates": "candidates.updated",
|
||||
"rectification-accept-candidate": "candidate.accepted",
|
||||
"rectification-confirm-birth-time": "birth_time.confirmed",
|
||||
"rectification-stop-and-review": "intent.classified",
|
||||
// Closing a Case has no truthful existing semantic phase. The tool.activity
|
||||
// event remains visible, while completion is still owned by the runner.
|
||||
"rectification-close-case": null,
|
||||
|
||||
@@ -1274,6 +1274,36 @@ export type V9PersistedCandidate = Readonly<{
|
||||
executionLedger: readonly Readonly<Record<string, unknown>>[];
|
||||
}>;
|
||||
|
||||
export async function refreshV9VedastroValidation(
|
||||
accounting: AccountingClient,
|
||||
userId: string,
|
||||
caseId: string,
|
||||
input: {
|
||||
resultId: string;
|
||||
evidenceFingerprint: string;
|
||||
rangeFingerprint: string;
|
||||
validation: Readonly<Record<string, unknown>>;
|
||||
minuteSensitiveStatus: "passed" | "failed" | "not_evaluated";
|
||||
},
|
||||
): Promise<Readonly<Record<string, unknown>>> {
|
||||
const row = await rpc<Record<string, unknown>>(
|
||||
accounting,
|
||||
"refresh_agentic_rectification_vedastro_validation",
|
||||
{
|
||||
p_user_id: userId,
|
||||
p_case_id: caseId,
|
||||
p_result_id: input.resultId,
|
||||
p_evidence_ledger_fingerprint: input.evidenceFingerprint,
|
||||
p_candidate_range_fingerprint: input.rangeFingerprint,
|
||||
p_validation: input.validation,
|
||||
p_minute_sensitive_status: input.minuteSensitiveStatus,
|
||||
},
|
||||
);
|
||||
const receipt = rowObject(row.decision_receipt);
|
||||
if (!receipt) throw new RectificationToolServiceError("invalid_candidate_result");
|
||||
return receipt;
|
||||
}
|
||||
|
||||
export async function persistV9Candidate(
|
||||
accounting: AccountingClient,
|
||||
userId: string,
|
||||
|
||||
Reference in New Issue
Block a user