fix(rectification): remove static interview fallbacks
Independent Staging Quality Gate / validate (push) Successful in 10m50s
Independent Staging Quality Gate / publish (push) Successful in 13m29s

This commit is contained in:
Jesse_Chen
2026-08-27 16:40:47 +08:00
parent 5f855c610c
commit f3327235ea
43 changed files with 1798 additions and 509 deletions
@@ -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,