fix(rectification): refresh remaining probes and targeted collect before delivering range (BUG-653/654)
Dated-choice exhaustion is not convergence. Refresh probes from remaining active candidates, then ask a targeted collect, then deliver. Skill 10.0.24. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -176,6 +176,8 @@ function publicLatestResult(
|
||||
credibleRange: projected.credible_range ?? decision.credibleRange,
|
||||
credible_range: projected.credible_range ?? decision.credibleRange,
|
||||
skill_verification_report: toolProjection.skill_verification_report,
|
||||
evidence: dossier.evidence,
|
||||
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
|
||||
});
|
||||
const withDelivery = {
|
||||
...projected,
|
||||
|
||||
@@ -3329,6 +3329,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.rectification-range-delivery__likelihood,
|
||||
.rectification-range-delivery__block p,
|
||||
.rectification-range-delivery__more,
|
||||
.rectification-range-delivery__narrow,
|
||||
.rectification-range-delivery__shared,
|
||||
.rectification-range-delivery__column > p {
|
||||
margin: 0;
|
||||
|
||||
@@ -32,7 +32,7 @@ export function RectificationRangeDelivery({
|
||||
const columns = delivery?.columns ?? [];
|
||||
const title = range
|
||||
? eventCount > 0
|
||||
? `${RECTIFICATION_USER_COPY.rangeDeliveryTitle} ${range[0]}–${range[1]} · ${rangeDeliveryEventCopy(eventCount)}`
|
||||
? `${RECTIFICATION_USER_COPY.rangeDeliveryTitle} ${range[0]}–${range[1]}(${rangeDeliveryEventCopy(eventCount)})`
|
||||
: `${RECTIFICATION_USER_COPY.rangeDeliveryTitle} ${range[0]}–${range[1]}`
|
||||
: RECTIFICATION_USER_COPY.rangeDeliveryTitle;
|
||||
const markdown = delivery?.verification_markdown
|
||||
@@ -44,6 +44,9 @@ export function RectificationRangeDelivery({
|
||||
<div className="rectification-candidates-heading">
|
||||
<strong>{title}</strong>
|
||||
</div>
|
||||
{delivery?.narrow_hint ? (
|
||||
<p className="rectification-range-delivery__narrow">{delivery.narrow_hint}</p>
|
||||
) : null}
|
||||
{sharedTraits.length > 0 ? (
|
||||
<ul className="rectification-range-delivery__shared">
|
||||
{sharedTraits.map((line) => (
|
||||
|
||||
@@ -190,6 +190,10 @@ export function buildInferenceState(input: {
|
||||
representative_time: top?.time ?? null,
|
||||
credible_range: unionStillValidRange(candidates),
|
||||
holdout_passed: holdoutPassed,
|
||||
...(typeof previous?.refresh_count === "number" ? { refresh_count: previous.refresh_count } : {}),
|
||||
...(typeof previous?.refresh_answer_count === "number"
|
||||
? { refresh_answer_count: previous.refresh_answer_count }
|
||||
: {}),
|
||||
...(transitions ? { transitions } : {}),
|
||||
};
|
||||
const decision = evaluateConvergence({ ...draft, holdout_passed: holdoutPassed });
|
||||
|
||||
@@ -141,7 +141,7 @@ export function publicCanAdopt(decision: Pick<RectificationDecision, "canAdopt"
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivery copy (`这次给出的范围` / representative-candidate close) is only
|
||||
* Delivery copy (`目前范围` / representative-candidate close) is only
|
||||
* allowed when the public adopt flag is on, or the session is explicitly
|
||||
* offering a selectable range. Internal `canAdopt` during collect is not enough.
|
||||
*/
|
||||
@@ -210,6 +210,8 @@ export type DecideRectificationInput = Readonly<{
|
||||
caseStage?: "minute" | "block_scan";
|
||||
blockScanDeclined?: boolean;
|
||||
windowWidenSuggested?: boolean;
|
||||
refreshExhausted?: boolean;
|
||||
targetedCollectExhausted?: boolean;
|
||||
}>;
|
||||
|
||||
function classifyStop(
|
||||
@@ -318,16 +320,25 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
if (coverageBlocks) {
|
||||
const engineOffers = input.engineCeiling.acceptanceAllowed
|
||||
|| input.engineCeiling.proposeAllowed;
|
||||
const narrowingOpen = stillNeedNarrowing(input);
|
||||
if (
|
||||
stopClass?.kind !== "keep_collecting"
|
||||
&& input.trainingGateOpen !== false
|
||||
&& separation.ranked.length > 0
|
||||
&& !probe
|
||||
&& engineOffers
|
||||
&& !narrowingOpen
|
||||
) {
|
||||
return offerRangeWithoutAdopt(separation, holdout, range, capability);
|
||||
}
|
||||
return collect(separation, holdout, range, probe, capability, stopReason);
|
||||
return collect(
|
||||
separation,
|
||||
holdout,
|
||||
range,
|
||||
probe,
|
||||
narrowingOpen ? waitToNarrowCapability(capability) : capability,
|
||||
stopReason,
|
||||
);
|
||||
}
|
||||
if (stopClass?.kind === "keep_collecting") {
|
||||
return collect(separation, holdout, range, probe, capability, stopClass.reason);
|
||||
@@ -343,7 +354,13 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
return holdoutValidation(separation, range, capability);
|
||||
}
|
||||
// coverageBlocks already collected when the training gate is closed.
|
||||
// An open leftover collect must not block S3 delivery (BUG-651).
|
||||
// Dated-pool empty is not delivery until refresh and targeted collect are
|
||||
// exhausted (BUG-654). Personality still does not occupy this slot.
|
||||
// Omitted flags mean the helper/unit path: do not wait. Production
|
||||
// decideFromDossier always passes explicit booleans.
|
||||
if (stillNeedNarrowing(input)) {
|
||||
return collect(separation, holdout, range, probe, waitToNarrowCapability(capability), stopReason);
|
||||
}
|
||||
if (stopClass?.kind === "exhausted") {
|
||||
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
|
||||
}
|
||||
@@ -506,6 +523,20 @@ function askWindowWiden(
|
||||
};
|
||||
}
|
||||
|
||||
function stillNeedNarrowing(input: DecideRectificationInput): boolean {
|
||||
return input.refreshExhausted === false || input.targetedCollectExhausted === false;
|
||||
}
|
||||
|
||||
function waitToNarrowCapability(capability: DeliveryCapability): DeliveryCapability {
|
||||
return {
|
||||
...capability,
|
||||
canAdopt: false,
|
||||
selectionAllowed: false,
|
||||
proposeAllowed: false,
|
||||
canConfirmExactMinute: false,
|
||||
};
|
||||
}
|
||||
|
||||
function collect(
|
||||
separation: CandidateSeparation,
|
||||
holdout: HoldoutValidationStatus,
|
||||
|
||||
@@ -143,6 +143,8 @@ export type InferenceState = Readonly<{
|
||||
representative_time: string | null;
|
||||
credible_range: readonly [string, string] | null;
|
||||
holdout_passed?: boolean | null;
|
||||
refresh_count?: number;
|
||||
refresh_answer_count?: number;
|
||||
transitions?: readonly Readonly<{
|
||||
layer: string;
|
||||
at: string;
|
||||
|
||||
@@ -139,7 +139,7 @@ export const RECTIFICATION_USER_COPY = {
|
||||
compareFailedRetry: "候选比较这次没跑成,下一句话时会自动再试。",
|
||||
lastSuccessfulCompareRange: "这是按上一次成功比较给出的范围。",
|
||||
deferredCareerWindow: "下一次事业变动的预测窗口留在采用后的核对阶段。",
|
||||
rangeDeliveryTitle: "这次给出的范围",
|
||||
rangeDeliveryTitle: "目前范围",
|
||||
rangeDeliveryMoreLikeThis: "更像这个",
|
||||
rangeDeliveryRelativeLikelihood: "相对可能性",
|
||||
rangeDeliveryNoWindow: "未来一年没有明显的时段",
|
||||
@@ -417,7 +417,7 @@ export function nonConvergingRangeNarration(
|
||||
export function deliveryTurnNarration(input: RangeNarrationInput = {}): string {
|
||||
const rangeText = formatClockRange(input.credibleRange ?? null);
|
||||
const sentence1 = rangeText
|
||||
? `这次给出的范围 ${rangeText}。`
|
||||
? `目前范围 ${rangeText}。`
|
||||
: "当前几个候选还分不开。";
|
||||
const count = typeof input.eventCount === "number" && input.eventCount >= 0 ? input.eventCount : 0;
|
||||
const percent = typeof input.fitPercent === "number" && Number.isFinite(input.fitPercent)
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
RECTIFICATION_USER_COPY,
|
||||
withLastSuccessfulCompareNotice,
|
||||
} from "../user-copy.ts";
|
||||
import { moreCollectHint, preciseGapNarration } from "./collection-question-pool.ts";
|
||||
import { moreCollectHint, preciseGapNarration, rangeNarrowHint } from "./collection-question-pool.ts";
|
||||
import {
|
||||
applyChoiceWithoutEvidence,
|
||||
previousInferenceFromReceipt,
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
planWithDateReliability,
|
||||
spokenCollectFallbackFollowup,
|
||||
spokenFollowupForUser,
|
||||
targetedCollectFollowup,
|
||||
ledgerHasConfirmedDatedEvent,
|
||||
type MethodCoverage,
|
||||
type MethodFollowup,
|
||||
@@ -97,6 +98,7 @@ import { mutateCaseForBlockChoice, mutateCaseForWidenWindow, rescoreStaleMinuteS
|
||||
import type { SessionOutcomeKind } from "./confirmation-gate";
|
||||
import { prospectiveWindowsNarration, refinementFromDecisionReceipt } from "./refinement-packet";
|
||||
import { projectCurrentQuestion } from "./turn-decision";
|
||||
import { refreshDatedDiscriminatorPoolIfNeeded } from "./refresh-discriminator-probes.ts";
|
||||
|
||||
const EXHAUSTION_DELIVERY_ACTIONS = new Set([
|
||||
"offer_provisional_range",
|
||||
@@ -127,7 +129,8 @@ export function looksLikeTerminalNoteNarration(text: string | null | undefined):
|
||||
|| trimmed.includes("范围还能再收一截")
|
||||
|| /还差(?: \d+ 件)?带月份的经历/.test(trimmed)
|
||||
|| trimmed.includes("两件事的日期还没对清")
|
||||
|| trimmed.includes("这次给出的范围");
|
||||
|| trimmed.includes("这次给出的范围")
|
||||
|| trimmed.includes("目前范围");
|
||||
}
|
||||
|
||||
function terminalNoteHostPresent(input: {
|
||||
@@ -308,9 +311,12 @@ function adoptHostNarration(input: {
|
||||
eventCount: datedEventCount(inference),
|
||||
fitPercent: fit?.percent ?? null,
|
||||
});
|
||||
const hint = moreCollectHint(
|
||||
const catalog = rectificationFollowupCatalog(input.dossier.latestResult, input.dossier.evidence);
|
||||
const hint = rangeNarrowHint(
|
||||
catalog.remainingLayers,
|
||||
input.dossier.evidence,
|
||||
input.dossier.conversationSummary.declinedSkippedTopics,
|
||||
catalog.remainingSplitTimes,
|
||||
);
|
||||
if (!hint || delivered.includes(hint)) return delivered;
|
||||
return `${delivered} ${hint}`.replace(/\s+/g, " ").trim();
|
||||
@@ -790,6 +796,7 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
birthDate?: string | null;
|
||||
askedTurnId?: string | null;
|
||||
narrateAdopt?: AdoptNarrationWriter;
|
||||
skipRefresh?: boolean;
|
||||
}): Promise<{
|
||||
hostNarration: string;
|
||||
choiceReady: boolean;
|
||||
@@ -799,59 +806,70 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
followup?: MethodFollowup | null;
|
||||
terminalNote?: boolean;
|
||||
}> {
|
||||
const latest = input.dossier.latestResult
|
||||
? {
|
||||
...input.dossier.latestResult,
|
||||
decisionReceipt: {
|
||||
...(input.dossier.latestResult.decisionReceipt ?? {}),
|
||||
...(input.decisionState ? { inference_state: input.decisionState } : {}),
|
||||
},
|
||||
}
|
||||
: {
|
||||
decisionReceipt: input.decisionState ? { inference_state: input.decisionState } : null,
|
||||
};
|
||||
const birthDate = input.birthDate ?? null;
|
||||
const liveDossier = dossierWithCurrentInference(input.dossier, input.decisionState);
|
||||
const decision = input.decision ?? decideAfterInferenceChange({
|
||||
dossier: input.dossier,
|
||||
state: input.decisionState,
|
||||
let liveDossier = dossierWithCurrentInference(input.dossier, input.decisionState);
|
||||
let decisionState = input.decisionState;
|
||||
let decision = input.decision ?? decideAfterInferenceChange({
|
||||
dossier: liveDossier,
|
||||
state: decisionState,
|
||||
userStopped: false,
|
||||
birthDate,
|
||||
});
|
||||
const catalog = rectificationFollowupCatalog(latest, input.dossier.evidence);
|
||||
const sessionOutcome = typeof input.nextAction.session_outcome === "string"
|
||||
? input.nextAction.session_outcome as SessionOutcomeKind
|
||||
if (!input.skipRefresh) {
|
||||
const refreshed = await refreshDatedDiscriminatorPoolIfNeeded({
|
||||
accounting: input.accounting,
|
||||
userId: input.userId,
|
||||
caseId: input.caseId,
|
||||
dossier: liveDossier,
|
||||
state: decisionState,
|
||||
hasDatedProbe: Boolean(decision.probe),
|
||||
});
|
||||
liveDossier = refreshed.dossier;
|
||||
decisionState = refreshed.state;
|
||||
if (refreshed.refreshed) {
|
||||
decision = decideAfterInferenceChange({
|
||||
dossier: liveDossier,
|
||||
state: decisionState,
|
||||
userStopped: false,
|
||||
birthDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
const catalog = rectificationFollowupCatalog(liveDossier.latestResult, liveDossier.evidence);
|
||||
const nextAction = publicNextAction(decision);
|
||||
const sessionOutcome = typeof nextAction.session_outcome === "string"
|
||||
? nextAction.session_outcome as SessionOutcomeKind
|
||||
: "collect_evidence";
|
||||
const plan = planWithDateReliability(buildMethodFollowupPlan({
|
||||
evidence: input.dossier.evidence,
|
||||
evidence: liveDossier.evidence,
|
||||
activeFocus: null,
|
||||
declinedTopics: input.dossier.conversationSummary.declinedSkippedTopics,
|
||||
closedCollectFocuses: input.dossier.conversationSummary.declinedSkippedTopics,
|
||||
declinedTopics: liveDossier.conversationSummary.declinedSkippedTopics,
|
||||
closedCollectFocuses: liveDossier.conversationSummary.declinedSkippedTopics,
|
||||
sessionOutcome,
|
||||
...catalog,
|
||||
birthDate,
|
||||
accepted: Boolean(input.dossier.case.acceptedTime),
|
||||
candidatesSeparated: input.nextAction.type !== "ask_candidate_discriminator"
|
||||
&& input.nextAction.type !== "ask_holdout_validation",
|
||||
accepted: Boolean(liveDossier.case.acceptedTime),
|
||||
candidatesSeparated: nextAction.type !== "ask_candidate_discriminator"
|
||||
&& nextAction.type !== "ask_holdout_validation",
|
||||
...followupCaseArgs({
|
||||
stage: input.dossier.case.stage,
|
||||
blockScan: input.dossier.case.blockScan,
|
||||
reportedBirthTime: input.dossier.case.reportedBirthTime,
|
||||
candidateRange: input.dossier.case.candidateRange,
|
||||
stage: liveDossier.case.stage,
|
||||
blockScan: liveDossier.case.blockScan,
|
||||
reportedBirthTime: liveDossier.case.reportedBirthTime,
|
||||
candidateRange: liveDossier.case.candidateRange,
|
||||
}),
|
||||
}), input.dossier.evidence, input.askedTurnId);
|
||||
}), liveDossier.evidence, input.askedTurnId);
|
||||
const followup = interviewToPersist(plan);
|
||||
if (shouldSkipFollowupPersist({
|
||||
canAdopt: input.nextAction.can_adopt,
|
||||
nextAction: input.nextAction.type,
|
||||
canAdopt: nextAction.can_adopt,
|
||||
nextAction: nextAction.type,
|
||||
followup,
|
||||
methods: plan.methods,
|
||||
accepted: Boolean(input.dossier.case.acceptedTime),
|
||||
accepted: Boolean(liveDossier.case.acceptedTime),
|
||||
stopReason: decision.stopReason ?? null,
|
||||
sessionOutcome: input.nextAction.session_outcome,
|
||||
evidence: input.dossier.evidence,
|
||||
declinedTopics: input.dossier.conversationSummary.declinedSkippedTopics,
|
||||
}) && deliveryNarrationAllowed(decision, input.nextAction.type)) {
|
||||
sessionOutcome: nextAction.session_outcome,
|
||||
evidence: liveDossier.evidence,
|
||||
declinedTopics: liveDossier.conversationSummary.declinedSkippedTopics,
|
||||
}) && deliveryNarrationAllowed(decision, nextAction.type)) {
|
||||
const facts = adoptDeliveryFacts(decision, liveDossier);
|
||||
const fallback = adoptHostNarration({
|
||||
dossier: liveDossier,
|
||||
@@ -882,7 +900,7 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
accounting: input.accounting,
|
||||
userId: input.userId,
|
||||
caseId: input.caseId,
|
||||
decisionReceipt: latest.decisionReceipt,
|
||||
decisionReceipt: liveDossier.latestResult?.decisionReceipt,
|
||||
followup,
|
||||
askedTurnId: input.askedTurnId ?? null,
|
||||
});
|
||||
@@ -914,7 +932,7 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
accounting: input.accounting,
|
||||
userId: input.userId,
|
||||
caseId: input.caseId,
|
||||
decisionReceipt: latest.decisionReceipt,
|
||||
decisionReceipt: liveDossier.latestResult?.decisionReceipt,
|
||||
followup: {
|
||||
...spokenFollowup,
|
||||
user_prompt_hint: spoken,
|
||||
@@ -947,7 +965,7 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
caseId: input.caseId,
|
||||
dossier: liveDossier,
|
||||
decision,
|
||||
decisionReceipt: latest.decisionReceipt,
|
||||
decisionReceipt: liveDossier.latestResult?.decisionReceipt,
|
||||
askedTurnId: input.askedTurnId ?? null,
|
||||
});
|
||||
}
|
||||
@@ -965,7 +983,7 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
caseId: input.caseId,
|
||||
dossier: liveDossier,
|
||||
decision,
|
||||
decisionReceipt: latest.decisionReceipt,
|
||||
decisionReceipt: liveDossier.latestResult?.decisionReceipt,
|
||||
askedTurnId: input.askedTurnId ?? null,
|
||||
});
|
||||
}
|
||||
@@ -984,7 +1002,7 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
caseId: input.caseId,
|
||||
dossier: liveDossier,
|
||||
decision,
|
||||
decisionReceipt: latest.decisionReceipt,
|
||||
decisionReceipt: liveDossier.latestResult?.decisionReceipt,
|
||||
askedTurnId: input.askedTurnId ?? null,
|
||||
});
|
||||
}
|
||||
@@ -1297,10 +1315,36 @@ export async function persistNextInterviewIfIdle(input: {
|
||||
} catch {
|
||||
birthDate = null;
|
||||
}
|
||||
const decision = decideFromDossier(dossier, {
|
||||
let decision = decideFromDossier(dossier, {
|
||||
birthDate,
|
||||
snapshotCurrent: rescored.snapshotCurrent,
|
||||
});
|
||||
const refreshed = await refreshDatedDiscriminatorPoolIfNeeded({
|
||||
accounting: input.accounting,
|
||||
userId: input.userId,
|
||||
caseId: input.caseId,
|
||||
dossier,
|
||||
state: previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null),
|
||||
userStopped: input.userStopped,
|
||||
hasDatedProbe: Boolean(decision.probe),
|
||||
});
|
||||
if (refreshed.refreshed) {
|
||||
const latest = dossier.latestResult;
|
||||
const overlay = refreshed.dossier.latestResult;
|
||||
dossier = {
|
||||
...dossier,
|
||||
latestResult: latest && overlay
|
||||
? {
|
||||
...latest,
|
||||
decisionReceipt: overlay.decisionReceipt ?? latest.decisionReceipt ?? null,
|
||||
}
|
||||
: latest,
|
||||
};
|
||||
decision = decideFromDossier(dossier, {
|
||||
birthDate,
|
||||
snapshotCurrent: rescored.snapshotCurrent,
|
||||
});
|
||||
}
|
||||
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: dossier.evidence,
|
||||
@@ -1416,6 +1460,7 @@ export async function persistNextInterviewIfIdle(input: {
|
||||
birthDate,
|
||||
askedTurnId: input.askedTurnId ?? null,
|
||||
narrateAdopt: input.narrateAdopt,
|
||||
skipRefresh: true,
|
||||
});
|
||||
return finishIdle({
|
||||
persisted: Boolean(nextInterview.hostNarration) || nextInterview.choiceReady,
|
||||
@@ -1452,7 +1497,12 @@ async function persistExhaustionCollect(input: {
|
||||
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
|
||||
answeredProbes: catalog.answeredProbes,
|
||||
eventProbes: catalog.eventProbes,
|
||||
});
|
||||
}) ?? targetedCollectFollowup(
|
||||
catalog.remainingLayers,
|
||||
dossier.evidence,
|
||||
dossier.conversationSummary.declinedSkippedTopics,
|
||||
catalog.remainingSplitTimes,
|
||||
);
|
||||
const trainingGate = trainingScoreableGate(dossier.evidence);
|
||||
const ceiling = engineCapabilityCeilingFromReceipt(receipt);
|
||||
const inference = previousInferenceFromReceipt(receipt);
|
||||
@@ -1592,9 +1642,11 @@ async function persistExhaustionCollect(input: {
|
||||
terminalNote: true,
|
||||
};
|
||||
}
|
||||
const hint = moreCollectHint(
|
||||
const hint = rangeNarrowHint(
|
||||
catalog.remainingLayers,
|
||||
dossier.evidence,
|
||||
dossier.conversationSummary.declinedSkippedTopics,
|
||||
catalog.remainingSplitTimes,
|
||||
);
|
||||
const range = nonConvergingRangeNarration({
|
||||
credibleRange: decision.credibleRange ?? input.decision.credibleRange,
|
||||
|
||||
@@ -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.23";
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.24";
|
||||
|
||||
@@ -18,7 +18,7 @@ export const COLLECT_KIND_ORDER = [
|
||||
|
||||
export type CollectKind = (typeof COLLECT_KIND_ORDER)[number];
|
||||
|
||||
export type CollectionPoolKind = "invite" | "anchor" | "generic";
|
||||
export type CollectionPoolKind = "invite" | "anchor" | "generic" | "targeted";
|
||||
|
||||
export type CollectionEvidence = Readonly<{
|
||||
status: string;
|
||||
@@ -40,6 +40,7 @@ export type CollectionPoolItem = Readonly<{
|
||||
domain: string;
|
||||
targetKind: string | null;
|
||||
year: number | null;
|
||||
examples?: readonly string[];
|
||||
}>;
|
||||
|
||||
const KIND_EXAMPLES: Readonly<Record<CollectKind, string>> = {
|
||||
@@ -451,6 +452,186 @@ export function moreCollectHint(
|
||||
return `如果还记得${exampleText},范围还能再收一截。`;
|
||||
}
|
||||
|
||||
export const REMAINING_LAYER_DOMAIN: Readonly<Record<string, CollectKind>> = {
|
||||
d9: "relationship",
|
||||
d10: "career",
|
||||
d4: "relocation",
|
||||
d5: "education",
|
||||
d24: "education",
|
||||
d7: "family",
|
||||
d12: "family",
|
||||
d2: "finance",
|
||||
d11: "finance",
|
||||
d30: "health_pressure",
|
||||
};
|
||||
|
||||
const TARGETED_EXAMPLES: Readonly<Record<CollectKind, readonly [string, string]>> = {
|
||||
education: ["哪年升学或毕业", "哪年考试发挥明显变过"],
|
||||
career: ["哪年换工作", "哪年岗位性质变过"],
|
||||
relocation: ["哪年搬家", "哪年换城市或出国"],
|
||||
relationship: ["哪年结婚或订婚", "哪年确定长期关系"],
|
||||
family: ["家里哪年添丁", "哪年长辈住院"],
|
||||
finance: ["哪年收入明显变过", "哪年有过大笔进出"],
|
||||
health_pressure: ["哪年住院或手术", "哪年身体明显垮过一截"],
|
||||
};
|
||||
|
||||
const SCAN_LAYER_FLAGS: ReadonlyArray<readonly [string, string]> = [
|
||||
["d9", "d9_candidates_differ"],
|
||||
["d10", "d10_candidates_differ"],
|
||||
["d4", "d4_candidates_differ"],
|
||||
["d5", "d5_candidates_differ"],
|
||||
["d24", "d24_candidates_differ"],
|
||||
["d7", "d7_candidates_differ"],
|
||||
["d12", "d12_candidates_differ"],
|
||||
["d2", "d2_candidates_differ"],
|
||||
["d11", "d11_candidates_differ"],
|
||||
["d30", "d30_candidates_differ"],
|
||||
];
|
||||
|
||||
const CLOCK = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
||||
|
||||
function clockValue(value: string | null | undefined): string | null {
|
||||
const clock = (value ?? "").slice(0, 5);
|
||||
return CLOCK.test(clock) ? clock : null;
|
||||
}
|
||||
|
||||
export function remainingSplitLayers(input: {
|
||||
transitions?: readonly Readonly<{ layer?: string; at?: string }>[];
|
||||
scanFlags?: Readonly<Record<string, unknown>> | null;
|
||||
activeTimes?: readonly string[];
|
||||
}): string[] {
|
||||
const active = [...new Set((input.activeTimes ?? []).map((item) => clockValue(item)).filter((item): item is string => Boolean(item)))].sort();
|
||||
const fromTransitions: string[] = [];
|
||||
for (const row of input.transitions ?? []) {
|
||||
const layer = typeof row.layer === "string" ? row.layer.trim().toLowerCase() : "";
|
||||
if (!layer || !(layer in REMAINING_LAYER_DOMAIN)) continue;
|
||||
const at = clockValue(row.at);
|
||||
if (active.length >= 2 && at && (at < active[0] || at > active[active.length - 1])) continue;
|
||||
if (!fromTransitions.includes(layer)) fromTransitions.push(layer);
|
||||
}
|
||||
if (fromTransitions.length > 0) return fromTransitions;
|
||||
const flags = input.scanFlags ?? {};
|
||||
const fromScan: string[] = [];
|
||||
for (const [layer, flag] of SCAN_LAYER_FLAGS) {
|
||||
if (flags[flag] === true && !fromScan.includes(layer)) fromScan.push(layer);
|
||||
}
|
||||
return fromScan;
|
||||
}
|
||||
|
||||
export function remainingSplitTimes(
|
||||
activeTimes: readonly string[] = [],
|
||||
): readonly [string, string] | null {
|
||||
const clocks = [...new Set(activeTimes.map((item) => clockValue(item)).filter((item): item is string => Boolean(item)))].sort();
|
||||
if (clocks.length < 2) return null;
|
||||
return [clocks[0], clocks[clocks.length - 1]];
|
||||
}
|
||||
|
||||
export function isTargetedCollectDeclined(
|
||||
topics: readonly CollectionTopic[] = [],
|
||||
): boolean {
|
||||
return topics.some((topic) => {
|
||||
const status = topicStatus(topic);
|
||||
if (status !== "declined" && status !== "skipped") return false;
|
||||
const questionId = topicQuestionId(topic);
|
||||
const kind = topicKind(topic);
|
||||
const domain = topicDomain(topic);
|
||||
return questionId.startsWith("collect:targeted:")
|
||||
|| kind.startsWith("targeted:")
|
||||
|| domain === "targeted";
|
||||
});
|
||||
}
|
||||
|
||||
function collectDeclinedKinds(topics: readonly CollectionTopic[]): ReadonlySet<CollectKind> {
|
||||
const declined = new Set<CollectKind>();
|
||||
for (const topic of topics) {
|
||||
const status = topicStatus(topic);
|
||||
if (status !== "declined" && status !== "skipped") continue;
|
||||
const intent = typeof topic.intent === "string" ? topic.intent : "";
|
||||
const questionId = topicQuestionId(topic);
|
||||
const collectIntent = intent === "collect_method_evidence"
|
||||
|| questionId.startsWith("collect:");
|
||||
if (!collectIntent) continue;
|
||||
if (questionId.startsWith("collect:invite:")) continue;
|
||||
if (questionId.startsWith("collect:other:")) continue;
|
||||
const kind = normalizeCollectKind(topicDomain(topic))
|
||||
?? (questionId.startsWith("collect:generic:")
|
||||
? normalizeCollectKind(questionId.split(":")[2] ?? "")
|
||||
: null);
|
||||
if (kind) declined.add(kind);
|
||||
}
|
||||
return declined;
|
||||
}
|
||||
function remainingTargetedDomains(
|
||||
layers: readonly string[],
|
||||
evidence: readonly CollectionEvidence[],
|
||||
declined: ReadonlySet<CollectKind>,
|
||||
): CollectKind[] {
|
||||
const covered = coveredCollectKinds(evidence);
|
||||
const domains: CollectKind[] = [];
|
||||
for (const layer of layers) {
|
||||
const domain = REMAINING_LAYER_DOMAIN[layer];
|
||||
if (!domain || declined.has(domain) || covered.has(domain)) continue;
|
||||
if (!domains.includes(domain)) domains.push(domain);
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
|
||||
export function targetedCollectPool(
|
||||
remainingLayers: readonly string[],
|
||||
evidence: readonly CollectionEvidence[],
|
||||
declinedTopics: readonly CollectionTopic[] = [],
|
||||
splitTimes?: readonly [string, string] | null,
|
||||
): CollectionPoolItem[] {
|
||||
if (isTargetedCollectDeclined(declinedTopics)) return [];
|
||||
const declined = collectDeclinedKinds(declinedTopics);
|
||||
const domains = remainingTargetedDomains(remainingLayers, evidence, declined);
|
||||
if (domains.length === 0) return [];
|
||||
const examples = domains.length === 1
|
||||
? [...TARGETED_EXAMPLES[domains[0]]]
|
||||
: domains.map((domain) => TARGETED_EXAMPLES[domain][0]);
|
||||
const uniqueExamples = [...new Set(examples)];
|
||||
if (uniqueExamples.length < 2) return [];
|
||||
const shown = uniqueExamples.slice(0, Math.max(2, Math.min(domains.length, uniqueExamples.length)));
|
||||
const pair = splitTimes && splitTimes[0] && splitTimes[1]
|
||||
? splitTimes
|
||||
: null;
|
||||
const splitText = pair ? `能把 ${pair[0]} 和 ${pair[1]} 分开` : "还能把剩下的候选分开";
|
||||
const prompt = `还有${Math.min(domains.length, shown.length)}条线${splitText}:${shown.join("、")}。记得哪件说哪件,年月大概就行。`;
|
||||
const primary = domains[0];
|
||||
return [{
|
||||
kind: "targeted",
|
||||
value: 1.5,
|
||||
prompt,
|
||||
key: `collect:targeted:${primary}`,
|
||||
domain: primary,
|
||||
targetKind: `targeted:${primary}`,
|
||||
year: null,
|
||||
examples: shown,
|
||||
}];
|
||||
}
|
||||
|
||||
export function targetedCollectHint(item: CollectionPoolItem | null | undefined): string | null {
|
||||
if (!item) return null;
|
||||
const examples = item.examples?.filter(Boolean) ?? [];
|
||||
if (examples.length >= 2) {
|
||||
return `还能再收窄:如果记得${examples.slice(0, 2).join("、")}`;
|
||||
}
|
||||
const body = item.prompt.replace(/[。??]$/, "");
|
||||
return `还能再收窄:如果记得${body}`;
|
||||
}
|
||||
|
||||
/** Card copy ignores a declined targeted collect so the delivered range still says what would narrow it. */
|
||||
export function rangeNarrowHint(
|
||||
remainingLayers: readonly string[],
|
||||
evidence: readonly CollectionEvidence[],
|
||||
declinedTopics: readonly CollectionTopic[] = [],
|
||||
splitTimes?: readonly [string, string] | null,
|
||||
): string {
|
||||
const item = targetedCollectPool(remainingLayers, evidence, [], splitTimes)[0]
|
||||
?? targetedCollectPool(remainingLayers, evidence, declinedTopics, splitTimes)[0];
|
||||
return targetedCollectHint(item) ?? `还能再收窄:${moreCollectHint(evidence, declinedTopics)}`;
|
||||
}
|
||||
|
||||
export const COLLECT_FLOW_BANNED_PHRASES = [
|
||||
"任何领域",
|
||||
"领域不限",
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
blockingMethodsCovered,
|
||||
buildMethodFollowupPlan,
|
||||
datedMethodCollectOpen,
|
||||
eventFamilyForDiscriminator,
|
||||
exhaustionSpokenCollectFollowup,
|
||||
isRemainingEvidenceCollect,
|
||||
} from "./method-followup";
|
||||
@@ -46,10 +47,16 @@ import {
|
||||
MIN_ACCEPTANCE_EVENTS,
|
||||
trainingScoreableGate,
|
||||
} from "./evidence-model";
|
||||
import { refinementFromDecisionReceipt, type DiscriminatingEventProbe } from "./refinement-packet";
|
||||
import { refinementFromDecisionReceipt, EVENT_PROBE_DOMAINS, type DiscriminatingEventProbe } from "./refinement-packet";
|
||||
import { windowScanFromDecisionReceipt } from "./varga-observations";
|
||||
import type { DroppedProbe } from "./probe-question-contract.ts";
|
||||
import { RECTIFICATION_POLICY } from "../../rectification-policy.ts";
|
||||
import {
|
||||
remainingSplitLayers,
|
||||
remainingSplitTimes,
|
||||
targetedCollectPool,
|
||||
isTargetedCollectDeclined,
|
||||
} from "./collection-question-pool.ts";
|
||||
import { evidenceLedgerFingerprint } from "./tool-service";
|
||||
import { followupCaseArgs, blockScanDeclinedForFingerprint } from "./block-scan.ts";
|
||||
import {
|
||||
@@ -261,6 +268,55 @@ export function contrastPacketFromLatestResult(
|
||||
});
|
||||
}
|
||||
|
||||
function eventProbeFromInference(probe: ConflictProbe): DiscriminatingEventProbe | null {
|
||||
if (probe.year <= 0 || probe.choice_kind === "varga_style" || probe.source === "nakshatra_boundary") {
|
||||
return null;
|
||||
}
|
||||
const domain = EVENT_PROBE_DOMAINS.includes(probe.domain as (typeof EVENT_PROBE_DOMAINS)[number])
|
||||
? probe.domain as (typeof EVENT_PROBE_DOMAINS)[number]
|
||||
: null;
|
||||
if (!domain) return null;
|
||||
return {
|
||||
year: probe.year,
|
||||
year_label: `${probe.year} 年前后`,
|
||||
domain,
|
||||
event_family: eventFamilyForDiscriminator(domain, probe.choice_kind),
|
||||
source: probe.source === "dasha_activation" || probe.source === "dasha_boundary"
|
||||
|| probe.source === "known_event_quality"
|
||||
? probe.source
|
||||
: "dasha_boundary",
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: probe.question,
|
||||
role: "distinguish",
|
||||
information_gain: probe.information_gain,
|
||||
semantic_key: probe.semantic_key,
|
||||
candidate_split_hash: probe.candidate_split_hash,
|
||||
candidate_ids: probe.candidate_ids,
|
||||
expected_outcomes: probe.expected_outcomes,
|
||||
...(probe.choice_kind ? { choice_kind: probe.choice_kind } : {}),
|
||||
...(probe.style_options?.length ? { style_options: probe.style_options } : {}),
|
||||
...(probe.target_evidence_id ? { target_evidence_id: probe.target_evidence_id } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeDiscriminatingEventProbes(
|
||||
...groups: ReadonlyArray<readonly DiscriminatingEventProbe[] | undefined>
|
||||
): DiscriminatingEventProbe[] {
|
||||
const byKey = new Map<string, DiscriminatingEventProbe>();
|
||||
for (const group of groups) {
|
||||
for (const probe of group ?? []) {
|
||||
const key = probe.semantic_key?.trim() ?? `${probe.domain}.${probe.year}`;
|
||||
const current = byKey.get(key);
|
||||
if (!current || (probe.information_gain ?? 0) > (current.information_gain ?? 0)) {
|
||||
byKey.set(key, probe);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...byKey.values()];
|
||||
}
|
||||
|
||||
function mergeEngineProbes(
|
||||
...groups: ReadonlyArray<readonly EngineContrastProbe[] | undefined>
|
||||
): EngineContrastProbe[] {
|
||||
@@ -304,12 +360,27 @@ export function rectificationFollowupCatalog(
|
||||
topCandidateTimes,
|
||||
})
|
||||
: null;
|
||||
const windowScan = windowScanFromDecisionReceipt(receipt);
|
||||
const activeTimes = (inference?.candidates ?? [])
|
||||
.filter((item) => item.status === "active" || item.status === "equivalent" || item.status === "winner")
|
||||
.map((item) => item.time);
|
||||
const remainingLayers = remainingSplitLayers({
|
||||
transitions: inference?.transitions ?? windowScan?.transitions ?? [],
|
||||
scanFlags: windowScan,
|
||||
activeTimes: activeTimes.length ? activeTimes : topCandidateTimes,
|
||||
});
|
||||
const answeredIds = new Set((inference?.answered_probes ?? []).map((item) => item.probe_id));
|
||||
const fromInference = (inference?.probes ?? []).flatMap((probe) => {
|
||||
if (answeredIds.has(probe.id)) return [];
|
||||
const mapped = eventProbeFromInference(probe);
|
||||
return mapped ? [mapped] : [];
|
||||
});
|
||||
return {
|
||||
contrastPacket: contrastPacketFromLatestResult(latest ?? null, evidence),
|
||||
topCandidateTimes,
|
||||
askedProbeKeys: askedKeys,
|
||||
answeredProbes: inference?.answered_probes ?? [],
|
||||
eventProbes: refinement.discriminating_event_probes,
|
||||
eventProbes: mergeDiscriminatingEventProbes(refinement.discriminating_event_probes, fromInference),
|
||||
eventClarificationProbes: refinement.event_clarification_probes,
|
||||
evidenceCollectionProbes: refinement.evidence_collection_probes,
|
||||
precisionStage: refinement.precision_stage?.current ?? null,
|
||||
@@ -318,6 +389,8 @@ export function rectificationFollowupCatalog(
|
||||
holdoutEvents: (inference?.events ?? [])
|
||||
.filter((item) => item.usage === "holdout")
|
||||
.map((item) => ({ domain: item.domain, year: item.year })),
|
||||
remainingLayers,
|
||||
remainingSplitTimes: remainingSplitTimes(activeTimes.length ? activeTimes : topCandidateTimes),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -417,8 +490,35 @@ export type DecideFromDossierOptions = Readonly<{
|
||||
currentEvidenceFingerprint?: string | null;
|
||||
birthDate?: string | null;
|
||||
snapshotCurrent?: boolean;
|
||||
refreshExhausted?: boolean;
|
||||
targetedCollectExhausted?: boolean;
|
||||
}>;
|
||||
|
||||
function narrowingExhaustion(
|
||||
dossier: DecisionDossier,
|
||||
inference: InferenceState | null,
|
||||
options?: Pick<DecideFromDossierOptions, "refreshExhausted" | "targetedCollectExhausted">,
|
||||
catalog?: ReturnType<typeof rectificationFollowupCatalog>,
|
||||
) {
|
||||
const live = catalog ?? rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||||
const declined = dossier.conversationSummary.declinedSkippedTopics;
|
||||
const targeted = targetedCollectPool(
|
||||
live.remainingLayers,
|
||||
dossier.evidence,
|
||||
declined,
|
||||
live.remainingSplitTimes,
|
||||
);
|
||||
const remainingLayers = live.remainingLayers;
|
||||
const refreshed = (inference?.refresh_count ?? 0) >= 1;
|
||||
// No remaining split layers: BUG-651 pool-empty delivery still holds.
|
||||
// Remaining layers without a refresh: wait (BUG-653 accident).
|
||||
return {
|
||||
refreshExhausted: options?.refreshExhausted ?? (refreshed || remainingLayers.length === 0),
|
||||
targetedCollectExhausted: options?.targetedCollectExhausted
|
||||
?? (isTargetedCollectDeclined(declined) || targeted.length === 0),
|
||||
};
|
||||
}
|
||||
|
||||
function userInterviewAnswers(
|
||||
answers: InferenceState["answered_probes"] | undefined,
|
||||
) {
|
||||
@@ -660,6 +760,8 @@ export function decideFromDossier(
|
||||
sessionOutcome: "collect_evidence",
|
||||
answeredProbes: catalog.answeredProbes,
|
||||
eventProbes: catalog.eventProbes,
|
||||
remainingLayers: catalog.remainingLayers,
|
||||
remainingSplitTimes: catalog.remainingSplitTimes,
|
||||
...followupCaseArgs({
|
||||
stage: dossier.case.stage,
|
||||
blockScan: dossier.case.blockScan,
|
||||
@@ -755,6 +857,7 @@ export function decideFromDossier(
|
||||
options?.currentEvidenceFingerprint ?? evidenceLedgerFingerprint(dossier.evidence as never),
|
||||
),
|
||||
windowWidenSuggested,
|
||||
...narrowingExhaustion(dossier, inference, options, catalog),
|
||||
}),
|
||||
droppedProbes: mergeDroppedProbes(gated.dropped, nakshatra.dropped),
|
||||
};
|
||||
@@ -775,6 +878,8 @@ export function decideAfterInferenceChange(input: {
|
||||
sessionOutcome: "collect_evidence",
|
||||
answeredProbes: catalog.answeredProbes,
|
||||
eventProbes: catalog.eventProbes,
|
||||
remainingLayers: catalog.remainingLayers,
|
||||
remainingSplitTimes: catalog.remainingSplitTimes,
|
||||
...followupCaseArgs({
|
||||
stage: input.dossier.case.stage,
|
||||
blockScan: input.dossier.case.blockScan,
|
||||
@@ -894,6 +999,7 @@ export function decideAfterInferenceChange(input: {
|
||||
input.dossier,
|
||||
evidenceLedgerFingerprint(input.dossier.evidence as never),
|
||||
),
|
||||
...narrowingExhaustion(input.dossier, input.state, undefined, catalog),
|
||||
}),
|
||||
droppedProbes: mergeDroppedProbes(gated.dropped, nakshatra.dropped),
|
||||
};
|
||||
|
||||
@@ -18,6 +18,11 @@ import {
|
||||
sharedTraitLine,
|
||||
} from "../user-copy.ts";
|
||||
import { previousInferenceFromReceipt } from "./inference-adapter.ts";
|
||||
import {
|
||||
rangeNarrowHint,
|
||||
remainingSplitLayers,
|
||||
remainingSplitTimes,
|
||||
} from "./collection-question-pool.ts";
|
||||
import {
|
||||
parseEventDashaLedgerByTime,
|
||||
parseProspectiveWindowsByTime,
|
||||
@@ -74,6 +79,7 @@ export type RangeDeliveryProjection = Readonly<{
|
||||
more_count: number;
|
||||
more_label: string | null;
|
||||
verification_markdown: string | null;
|
||||
narrow_hint: string | null;
|
||||
}>;
|
||||
|
||||
export type PublicCandidateClock = Readonly<{
|
||||
@@ -315,6 +321,16 @@ export function buildRangeDelivery(input: {
|
||||
eventDashaLedgerByTime?: Readonly<Record<string, readonly EventDashaLedgerRow[]>>;
|
||||
prospectiveWindowsByTime?: Readonly<Record<string, readonly ProspectiveWindow[]>>;
|
||||
eventDashaLedger?: readonly EventDashaLedgerRow[];
|
||||
evidence?: readonly Readonly<{
|
||||
status: string;
|
||||
domain: string;
|
||||
datePrecision: string;
|
||||
occurredFrom: string | null;
|
||||
occurredTo: string | null;
|
||||
eventKind?: string | null;
|
||||
summary?: string | null;
|
||||
}>[];
|
||||
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
|
||||
}): RangeDeliveryProjection {
|
||||
const inference = input.inference;
|
||||
const range = input.credibleRange
|
||||
@@ -384,6 +400,22 @@ export function buildRangeDelivery(input: {
|
||||
more_count: moreCount,
|
||||
more_label: moreCount > 0 ? RANGE_DELIVERY_MORE_MINUTES(moreCount) : null,
|
||||
verification_markdown: input.verificationMarkdown ?? null,
|
||||
narrow_hint: rangeNarrowHint(
|
||||
remainingSplitLayers({
|
||||
transitions: inference?.transitions ?? windowScan?.transitions ?? [],
|
||||
scanFlags: windowScan,
|
||||
activeTimes: inference?.candidates
|
||||
.filter((item) => item.status !== "eliminated")
|
||||
.map((item) => item.time) ?? clocks.map((item) => item.time),
|
||||
}),
|
||||
input.evidence ?? [],
|
||||
input.declinedTopics ?? [],
|
||||
remainingSplitTimes(
|
||||
inference?.candidates
|
||||
.filter((item) => item.status !== "eliminated")
|
||||
.map((item) => item.time) ?? clocks.map((item) => item.time),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -429,6 +461,16 @@ export function rangeDeliveryForSnapshot(snapshot: {
|
||||
skillVerificationReport?: unknown;
|
||||
event_fit_rate?: unknown;
|
||||
eventFitRate?: unknown;
|
||||
evidence?: readonly Readonly<{
|
||||
status: string;
|
||||
domain: string;
|
||||
datePrecision: string;
|
||||
occurredFrom: string | null;
|
||||
occurredTo: string | null;
|
||||
eventKind?: string | null;
|
||||
summary?: string | null;
|
||||
}>[];
|
||||
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
|
||||
} | null | undefined): RangeDeliveryProjection {
|
||||
const receipt = snapshot?.decisionReceipt ?? snapshot?.decision_receipt ?? null;
|
||||
const inference = previousInferenceFromReceipt(receipt);
|
||||
@@ -453,6 +495,8 @@ export function rangeDeliveryForSnapshot(snapshot: {
|
||||
eventDashaLedgerByTime: refinement.event_dasha_ledger_by_time,
|
||||
prospectiveWindowsByTime: refinement.prospective_windows_by_time,
|
||||
eventDashaLedger: refinement.event_dasha_ledger,
|
||||
evidence: snapshot?.evidence,
|
||||
declinedTopics: snapshot?.declinedTopics,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -589,5 +633,10 @@ export function parseRangeDelivery(value: unknown): RangeDeliveryProjection | nu
|
||||
: moreCount > 0 ? RANGE_DELIVERY_MORE_MINUTES(moreCount) : null,
|
||||
verification_markdown: verificationMarkdownFromUnknown(row.verification_markdown)
|
||||
?? verificationMarkdownFromUnknown(row.verificationMarkdown),
|
||||
narrow_hint: typeof row.narrow_hint === "string" && row.narrow_hint.trim()
|
||||
? row.narrow_hint.trim()
|
||||
: typeof row.narrowHint === "string" && row.narrowHint.trim()
|
||||
? row.narrowHint.trim()
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -519,6 +519,7 @@ export function engineRequestBody(input: {
|
||||
events: readonly V9EngineEvent[];
|
||||
askedProbeKeys?: readonly string[];
|
||||
columnTimes?: readonly string[];
|
||||
refreshProbes?: boolean;
|
||||
}): Record<string, unknown> {
|
||||
const snapshot = input.baselineBirthSnapshot;
|
||||
const birthDate = String(snapshot.birth_date ?? "");
|
||||
@@ -553,6 +554,7 @@ export function engineRequestBody(input: {
|
||||
local_time_status: snapshot.local_time_status,
|
||||
...(askedProbeKeys.length ? { asked_probe_keys: askedProbeKeys } : {}),
|
||||
...(columnTimes.length ? { column_times: columnTimes } : {}),
|
||||
...(input.refreshProbes === true ? { refresh_probes: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -667,6 +669,7 @@ export async function runV9CandidateScore(input: {
|
||||
events: readonly V9EngineEvent[];
|
||||
askedProbeKeys?: readonly string[];
|
||||
columnTimes?: readonly string[];
|
||||
refreshProbes?: boolean;
|
||||
}): Promise<V9EngineScoreResult> {
|
||||
const data = await postEngine("/api/rectification/v5/score", engineRequestBody(input));
|
||||
const candidates = readCandidates(data.candidate_decisions, input.candidateRange);
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
* Remaining dated dasha distinguish probes are asked first. Yearless
|
||||
* D9/D10 style and nakshatra_boundary stay out of the dated pool and
|
||||
* never occupy ask_candidate_discriminator (BUG-629, BUG-651). When
|
||||
* the dated pool is empty after the training gate, deliver the range.
|
||||
* the dated pool is empty after the training gate, refresh remaining
|
||||
* candidates then ask targeted collect before delivering the range.
|
||||
* If holdout is already reserved but training is still short,
|
||||
* keep collecting a dated event instead of discriminating.
|
||||
* Once blocking methods are covered, move into candidate discrimination.
|
||||
@@ -91,6 +92,7 @@ import {
|
||||
import {
|
||||
collectionQuestionPool,
|
||||
isInviteCollectTopic,
|
||||
targetedCollectPool,
|
||||
type CollectionPoolItem,
|
||||
} from "./collection-question-pool.ts";
|
||||
|
||||
@@ -1033,6 +1035,10 @@ function followupEventFamily(domain: string, kind: string): string {
|
||||
return EXISTENCE_EVENT_FAMILY[domain] ?? "这段经历是否发生过";
|
||||
}
|
||||
|
||||
export function eventFamilyForDiscriminator(domain: string, choiceKind?: string): string {
|
||||
return followupEventFamily(domain, choiceKind ?? "existence");
|
||||
}
|
||||
|
||||
function followupOwnedProbe(
|
||||
item: Omit<MethodFollowup, "must_not_label" | "choice_frame">,
|
||||
): DiscriminatingEventProbe | null {
|
||||
@@ -1327,13 +1333,15 @@ export function followupFromPoolItem(item: CollectionPoolItem): MethodFollowup {
|
||||
const theme = domain in REVERSE_VERIFY_THEME
|
||||
? REVERSE_VERIFY_THEME[domain as keyof typeof REVERSE_VERIFY_THEME]
|
||||
: "dated_event";
|
||||
const kindHint = item.kind === "anchor" && item.targetKind && item.year != null
|
||||
? `anchor:${item.targetKind}:${item.year}`
|
||||
: item.kind === "generic"
|
||||
? `generic:${domain}`
|
||||
: domain in REVERSE_VERIFY_KIND
|
||||
? REVERSE_VERIFY_KIND[domain as keyof typeof REVERSE_VERIFY_KIND]
|
||||
: null;
|
||||
const kindHint = item.kind === "targeted"
|
||||
? `targeted:${domain}`
|
||||
: item.kind === "anchor" && item.targetKind && item.year != null
|
||||
? `anchor:${item.targetKind}:${item.year}`
|
||||
: item.kind === "generic"
|
||||
? `generic:${domain}`
|
||||
: domain in REVERSE_VERIFY_KIND
|
||||
? REVERSE_VERIFY_KIND[domain as keyof typeof REVERSE_VERIFY_KIND]
|
||||
: null;
|
||||
return {
|
||||
method_id: methodId,
|
||||
intent: "collect_method_evidence",
|
||||
@@ -1358,6 +1366,16 @@ export function nextCollectionFollowup(
|
||||
return top ? followupFromPoolItem(top) : null;
|
||||
}
|
||||
|
||||
export function targetedCollectFollowup(
|
||||
remainingLayers: readonly string[],
|
||||
evidence: readonly MethodFollowupEvidence[],
|
||||
declinedTopics: readonly Readonly<Record<string, unknown>>[] = [],
|
||||
splitTimes?: readonly [string, string] | null,
|
||||
): MethodFollowup | null {
|
||||
const top = targetedCollectPool(remainingLayers, evidence, declinedTopics, splitTimes)[0];
|
||||
return top ? followupFromPoolItem(top) : null;
|
||||
}
|
||||
|
||||
export function nextDatedCollectFollowup(
|
||||
evidence: readonly MethodFollowupEvidence[],
|
||||
declined: ReadonlySet<string>,
|
||||
@@ -1398,9 +1416,10 @@ export function isRemainingEvidenceCollect(
|
||||
key.startsWith("collect:invite:")
|
||||
|| key.startsWith("collect:anchor:")
|
||||
|| key.startsWith("collect:generic:")
|
||||
|| key.startsWith("collect:targeted:")
|
||||
) return true;
|
||||
const hint = followup.kind_hint ?? "";
|
||||
if (hint === "invite_more" || hint.startsWith("anchor:") || hint.startsWith("generic:")) return true;
|
||||
if (hint === "invite_more" || hint.startsWith("anchor:") || hint.startsWith("generic:") || hint.startsWith("targeted:")) return true;
|
||||
return typeof followup.domain === "string"
|
||||
&& REMAINING_EVIDENCE_COLLECT_DOMAINS.has(followup.domain);
|
||||
}
|
||||
@@ -1936,6 +1955,8 @@ export function buildMethodFollowupPlan(input: {
|
||||
blockScan?: BlockScanPayload | null;
|
||||
reportedTime?: string | null;
|
||||
candidateRange?: { start_time: string; end_time: string } | null;
|
||||
remainingLayers?: readonly string[];
|
||||
remainingSplitTimes?: readonly [string, string] | null;
|
||||
}): MethodFollowupPlan {
|
||||
const makeFollowup = (
|
||||
item: Omit<MethodFollowup, "must_not_label" | "choice_frame">,
|
||||
@@ -2057,6 +2078,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
})
|
||||
: emptyRankedCatalog();
|
||||
const rankedDiscriminators = rankedCatalog.locked;
|
||||
const datedPoolEmpty = rankedDiscriminators.length === 0;
|
||||
const personalityDiscriminators = rankedCatalog.personality;
|
||||
const yearlessDiscriminators = rankedCatalog.yearless;
|
||||
const bestDiscriminator = rankedDiscriminators[0] ?? null;
|
||||
@@ -2501,7 +2523,6 @@ export function buildMethodFollowupPlan(input: {
|
||||
: null;
|
||||
if (!next) {
|
||||
const precisionCard = takeRenderableDistinguish(precisionStageFollowup());
|
||||
const datedPoolEmpty = rankedDiscriminators.length === 0;
|
||||
const datedPrecision = Boolean(
|
||||
precisionCard?.choice_frame && followupLocksDatedPeriod(precisionCard),
|
||||
);
|
||||
@@ -2512,11 +2533,23 @@ export function buildMethodFollowupPlan(input: {
|
||||
} else if (
|
||||
datedPoolEmpty
|
||||
&& meetsAcceptanceEventQuality(input.evidence)
|
||||
&& sessionOutcome === "discriminate_candidates"
|
||||
&& (
|
||||
sessionOutcome === "discriminate_candidates"
|
||||
|| sessionOutcome === "collect_evidence"
|
||||
)
|
||||
&& (next = targetedCollectFollowup(
|
||||
input.remainingLayers ?? [],
|
||||
input.evidence,
|
||||
[
|
||||
...(input.declinedTopics ?? []),
|
||||
...(input.closedCollectFocuses ?? []),
|
||||
],
|
||||
input.remainingSplitTimes,
|
||||
))
|
||||
) {
|
||||
// BUG-651: discriminating with no dated probe must not pick yearless
|
||||
// personality or leftover method collect as the next discriminator.
|
||||
next = null;
|
||||
// BUG-651: no yearless personality as the next discriminator.
|
||||
// BUG-654: after the dated pool is empty, ask targeted collect first.
|
||||
// If the targeted pool is empty, fall through to horary / leftover.
|
||||
} else if ((renderableYearless = firstRenderableYearlessFollowup())) {
|
||||
next = renderableYearless;
|
||||
} else if (
|
||||
@@ -2698,6 +2731,21 @@ export function buildMethodFollowupPlan(input: {
|
||||
});
|
||||
if (leftoverCollect) {
|
||||
next = makeFollowup(leftoverCollect);
|
||||
} else if (
|
||||
meetsAcceptanceEventQuality(input.evidence)
|
||||
&& datedPoolEmpty
|
||||
) {
|
||||
const targeted = targetedCollectFollowup(
|
||||
input.remainingLayers ?? [],
|
||||
input.evidence,
|
||||
[
|
||||
...(input.declinedTopics ?? []),
|
||||
...(input.closedCollectFocuses ?? []),
|
||||
],
|
||||
input.remainingSplitTimes,
|
||||
);
|
||||
if (targeted) next = makeFollowup(targeted);
|
||||
else if (pendingHoldout) next = makeFollowup(pendingHoldout, false);
|
||||
} else if (pendingHoldout) {
|
||||
next = makeFollowup(pendingHoldout, false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Refresh dated discriminator probes from remaining active candidates.
|
||||
*
|
||||
* Choice answers only rescore in TypeScript. The engine probe list is static
|
||||
* until this path runs. It must not change candidate_set_id or the answer
|
||||
* ledger (BUG-587 / BUG-594 / BUG-653).
|
||||
*/
|
||||
|
||||
import { probeFromEngine } from "../core/probes-from-engine.ts";
|
||||
import type { ConflictProbe, InferenceState } from "../core/types.ts";
|
||||
import { askedDiscriminatorKeys, previousInferenceFromReceipt } from "./inference-adapter.ts";
|
||||
import {
|
||||
runV9CandidateScore,
|
||||
toEngineEvents,
|
||||
} from "./engine-client.ts";
|
||||
import { refinementFromDecisionReceipt, type DiscriminatingEventProbe } from "./refinement-packet.ts";
|
||||
import { isTargetedCollectDeclined } from "./collection-question-pool.ts";
|
||||
import { trainingScoreableGate } from "./evidence-model.ts";
|
||||
import {
|
||||
evidenceLedgerFingerprint,
|
||||
inferenceFingerprintForState,
|
||||
loadV9CaseCompute,
|
||||
persistV9InferenceState,
|
||||
scorableEvidence,
|
||||
type AccountingClient,
|
||||
} from "./tool-service.ts";
|
||||
import type { DecisionDossier } from "./decision-from-dossier.ts";
|
||||
|
||||
export const MAX_DISCRIMINATOR_REFRESHES = 2;
|
||||
|
||||
export type RefreshDiscriminatorProbesInput = Readonly<{
|
||||
accounting: AccountingClient;
|
||||
userId: string;
|
||||
caseId: string;
|
||||
dossier: DecisionDossier;
|
||||
state: InferenceState;
|
||||
}>;
|
||||
|
||||
export type RefreshDiscriminatorProbesResult = Readonly<{
|
||||
state: InferenceState;
|
||||
eventProbes: readonly DiscriminatingEventProbe[];
|
||||
candidateSetId: string;
|
||||
refreshCount: number;
|
||||
}>;
|
||||
|
||||
type RefreshImpl = (input: RefreshDiscriminatorProbesInput) => Promise<RefreshDiscriminatorProbesResult>;
|
||||
|
||||
function isRefreshableDatedProbe(probe: DiscriminatingEventProbe): boolean {
|
||||
if ((probe.year ?? 0) <= 0) return false;
|
||||
if (probe.choice_kind === "varga_style") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function mergeEventProbes(
|
||||
existing: readonly DiscriminatingEventProbe[] | undefined,
|
||||
extra: readonly DiscriminatingEventProbe[] = [],
|
||||
): DiscriminatingEventProbe[] {
|
||||
const byKey = new Map<string, DiscriminatingEventProbe>();
|
||||
for (const probe of existing ?? []) {
|
||||
const key = probe.semantic_key?.trim() ?? `${probe.domain}.${probe.year}`;
|
||||
byKey.set(key, probe);
|
||||
}
|
||||
for (const probe of extra) {
|
||||
if (!isRefreshableDatedProbe(probe)) continue;
|
||||
const key = probe.semantic_key?.trim() ?? `${probe.domain}.${probe.year}`;
|
||||
const current = byKey.get(key);
|
||||
if (!current || (probe.information_gain ?? 0) > (current.information_gain ?? 0)) {
|
||||
byKey.set(key, probe);
|
||||
}
|
||||
}
|
||||
return [...byKey.values()];
|
||||
}
|
||||
|
||||
function mergeConflictProbes(
|
||||
existing: readonly ConflictProbe[],
|
||||
incoming: readonly ConflictProbe[],
|
||||
): ConflictProbe[] {
|
||||
const byKey = new Map<string, ConflictProbe>();
|
||||
for (const probe of existing) {
|
||||
byKey.set(probe.semantic_key, probe);
|
||||
}
|
||||
for (const probe of incoming) {
|
||||
if (probe.year <= 0 || probe.choice_kind === "varga_style" || probe.source === "nakshatra_boundary") {
|
||||
continue;
|
||||
}
|
||||
const current = byKey.get(probe.semantic_key);
|
||||
if (!current || probe.information_gain > current.information_gain) {
|
||||
byKey.set(probe.semantic_key, probe);
|
||||
}
|
||||
}
|
||||
return [...byKey.values()];
|
||||
}
|
||||
|
||||
function askedKeysFromState(state: InferenceState, dossier: DecisionDossier): string[] {
|
||||
const fromAnswers = state.answered_probes.flatMap((item) => [
|
||||
item.probe_id,
|
||||
item.semantic_key,
|
||||
item.candidate_split_hash,
|
||||
]);
|
||||
return [...new Set([
|
||||
...fromAnswers,
|
||||
...askedDiscriminatorKeys(dossier.latestResult?.decisionReceipt, dossier.evidence),
|
||||
])];
|
||||
}
|
||||
|
||||
function activeCandidateTimes(state: InferenceState): string[] {
|
||||
return [...new Set(
|
||||
state.candidates
|
||||
.filter((item) => item.status === "active" || item.status === "equivalent" || item.status === "winner")
|
||||
.map((item) => item.time.slice(0, 5)),
|
||||
)];
|
||||
}
|
||||
|
||||
function withRefreshCount(
|
||||
state: InferenceState,
|
||||
refreshCount: number,
|
||||
probes: readonly ConflictProbe[],
|
||||
): InferenceState {
|
||||
return {
|
||||
...state,
|
||||
probes,
|
||||
refresh_count: refreshCount,
|
||||
refresh_answer_count: state.answered_probes.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function defaultRefreshDiscriminatorProbes(
|
||||
input: RefreshDiscriminatorProbesInput,
|
||||
): Promise<RefreshDiscriminatorProbesResult> {
|
||||
const nextCount = (input.state.refresh_count ?? 0) + 1;
|
||||
const empty: RefreshDiscriminatorProbesResult = {
|
||||
state: withRefreshCount(input.state, nextCount, input.state.probes),
|
||||
eventProbes: [],
|
||||
candidateSetId: input.state.candidate_set_id,
|
||||
refreshCount: nextCount,
|
||||
};
|
||||
const times = activeCandidateTimes(input.state);
|
||||
if (times.length < 2) return empty;
|
||||
let compute;
|
||||
try {
|
||||
compute = await loadV9CaseCompute(input.accounting, input.userId, input.caseId);
|
||||
} catch {
|
||||
return empty;
|
||||
}
|
||||
const candidateRange = compute.candidateRange;
|
||||
const scorable = scorableEvidence(input.dossier.evidence as never);
|
||||
const events = toEngineEvents(scorable);
|
||||
if (events.length === 0) return empty;
|
||||
try {
|
||||
const score = await runV9CandidateScore({
|
||||
baselineBirthSnapshot: compute.baselineBirthSnapshot,
|
||||
candidateRange,
|
||||
events,
|
||||
askedProbeKeys: askedKeysFromState(input.state, input.dossier),
|
||||
columnTimes: times,
|
||||
refreshProbes: true,
|
||||
});
|
||||
const eventProbes = mergeEventProbes(
|
||||
refinementFromDecisionReceipt(score.decisionReceipt).discriminating_event_probes,
|
||||
);
|
||||
const incoming = eventProbes.flatMap((probe) => {
|
||||
const mapped = probeFromEngine(probe);
|
||||
return mapped ? [mapped] : [];
|
||||
});
|
||||
return {
|
||||
state: withRefreshCount(
|
||||
input.state,
|
||||
nextCount,
|
||||
mergeConflictProbes(input.state.probes, incoming),
|
||||
),
|
||||
eventProbes,
|
||||
candidateSetId: input.state.candidate_set_id,
|
||||
refreshCount: nextCount,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[rectification-v9] refresh discriminator probes failed case=${input.caseId} reason=${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
return empty;
|
||||
}
|
||||
}
|
||||
|
||||
let refreshImpl: RefreshImpl = defaultRefreshDiscriminatorProbes;
|
||||
|
||||
export function setRefreshDiscriminatorProbesForTests(impl: RefreshImpl | null): void {
|
||||
refreshImpl = impl ?? defaultRefreshDiscriminatorProbes;
|
||||
}
|
||||
|
||||
export function resetRefreshDiscriminatorProbesForTests(): void {
|
||||
refreshImpl = defaultRefreshDiscriminatorProbes;
|
||||
}
|
||||
|
||||
export async function refreshDiscriminatorProbes(
|
||||
input: RefreshDiscriminatorProbesInput,
|
||||
): Promise<RefreshDiscriminatorProbesResult> {
|
||||
return refreshImpl(input);
|
||||
}
|
||||
|
||||
export function applyRefreshedProbesToDossier(
|
||||
dossier: DecisionDossier,
|
||||
state: InferenceState,
|
||||
extraEventProbes: readonly DiscriminatingEventProbe[] = [],
|
||||
): DecisionDossier {
|
||||
const latest = dossier.latestResult;
|
||||
const receipt = latest?.decisionReceipt ?? {};
|
||||
const existing = refinementFromDecisionReceipt(receipt).discriminating_event_probes;
|
||||
return {
|
||||
...dossier,
|
||||
latestResult: {
|
||||
...(latest ?? {}),
|
||||
decisionReceipt: {
|
||||
...receipt,
|
||||
inference_state: state,
|
||||
discriminating_event_probes: mergeEventProbes(existing, extraEventProbes),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function shouldRefreshDatedPool(input: {
|
||||
dossier: DecisionDossier;
|
||||
state: InferenceState | null;
|
||||
userStopped?: boolean;
|
||||
hasDatedProbe: boolean;
|
||||
}): boolean {
|
||||
if (input.userStopped === true) return false;
|
||||
if (input.dossier.case.acceptedTime) return false;
|
||||
if (!input.state) return false;
|
||||
if (input.hasDatedProbe) return false;
|
||||
const refreshCount = input.state.refresh_count ?? 0;
|
||||
if (refreshCount >= MAX_DISCRIMINATOR_REFRESHES) return false;
|
||||
if (
|
||||
refreshCount >= 1
|
||||
&& input.state.answered_probes.length <= (input.state.refresh_answer_count ?? 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!trainingScoreableGate(input.dossier.evidence).open) return false;
|
||||
if (isTargetedCollectDeclined(input.dossier.conversationSummary.declinedSkippedTopics)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function persistRefreshedInference(input: {
|
||||
accounting: AccountingClient;
|
||||
userId: string;
|
||||
caseId: string;
|
||||
dossier: DecisionDossier;
|
||||
previous: InferenceState;
|
||||
next: InferenceState;
|
||||
}): Promise<void> {
|
||||
const last = input.next.answered_probes.at(-1);
|
||||
if (!last) return;
|
||||
const evidenceFp = input.dossier.latestResult?.evidenceLedgerFingerprint
|
||||
?? evidenceLedgerFingerprint(input.dossier.evidence as never);
|
||||
try {
|
||||
await persistV9InferenceState(input.accounting, input.userId, input.caseId, {
|
||||
expectedRevision: input.previous.revision,
|
||||
probeId: last.probe_id,
|
||||
openProbeId: last.probe_id,
|
||||
semanticKey: last.semantic_key,
|
||||
candidateSplitHash: last.candidate_split_hash,
|
||||
answerClass: last.answer_class,
|
||||
rawAnswer: "refresh_probes",
|
||||
inferenceState: input.next as unknown as Record<string, unknown>,
|
||||
posteriorBefore: Object.fromEntries(
|
||||
input.previous.candidates.map((item) => [item.time, item.posterior_score]),
|
||||
),
|
||||
posteriorAfter: Object.fromEntries(
|
||||
input.next.candidates.map((item) => [item.time, item.posterior_score]),
|
||||
),
|
||||
scoreDeltas: {},
|
||||
decisionStateFingerprint: inferenceFingerprintForState(
|
||||
input.caseId,
|
||||
evidenceFp,
|
||||
input.next,
|
||||
),
|
||||
reason: "supersede",
|
||||
idempotencyKey: `refresh_probes:${input.next.candidate_set_id}:${input.next.refresh_count ?? 1}`,
|
||||
candidateSetId: input.next.candidate_set_id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[rectification-v9] persist refreshed probes failed case=${input.caseId} reason=${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshDatedDiscriminatorPoolIfNeeded(input: {
|
||||
accounting: AccountingClient;
|
||||
userId: string;
|
||||
caseId: string;
|
||||
dossier: DecisionDossier;
|
||||
state?: InferenceState | null;
|
||||
userStopped?: boolean;
|
||||
hasDatedProbe: boolean;
|
||||
}): Promise<{
|
||||
dossier: DecisionDossier;
|
||||
state: InferenceState | null;
|
||||
refreshed: boolean;
|
||||
}> {
|
||||
const state = input.state
|
||||
?? previousInferenceFromReceipt(input.dossier.latestResult?.decisionReceipt ?? null);
|
||||
if (!shouldRefreshDatedPool({
|
||||
dossier: input.dossier,
|
||||
state,
|
||||
userStopped: input.userStopped,
|
||||
hasDatedProbe: input.hasDatedProbe,
|
||||
}) || !state) {
|
||||
return { dossier: input.dossier, state, refreshed: false };
|
||||
}
|
||||
const result = await refreshDiscriminatorProbes({
|
||||
accounting: input.accounting,
|
||||
userId: input.userId,
|
||||
caseId: input.caseId,
|
||||
dossier: input.dossier,
|
||||
state,
|
||||
});
|
||||
await persistRefreshedInference({
|
||||
accounting: input.accounting,
|
||||
userId: input.userId,
|
||||
caseId: input.caseId,
|
||||
dossier: input.dossier,
|
||||
previous: state,
|
||||
next: result.state,
|
||||
});
|
||||
return {
|
||||
dossier: applyRefreshedProbesToDossier(input.dossier, result.state, result.eventProbes),
|
||||
state: result.state,
|
||||
refreshed: true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user