fix(web): restore rectification discriminator cards and stop-offer path
Coverage-complete ties never persisted A/B/C/D because contrast probes were stamped with an answered education quality probe, remaining minutes were asked as window D10 signs, and 「没有了」 missed the stop pattern. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -239,6 +239,7 @@ export function answersFromEvidence(
|
||||
events: readonly EngineEventInput[],
|
||||
): ProbeAnswer[] {
|
||||
return probes.flatMap((probe) => {
|
||||
if (probe.source === "known_event_quality" || probe.source === "varga_contrast") return [];
|
||||
if (!events.some((item) => item.domain === probe.domain && item.year === probe.year)) return [];
|
||||
return [{
|
||||
probe_id: probe.id,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Window-scan prose in the final report is not a substitute for this packet.
|
||||
*/
|
||||
|
||||
import type { AnswerClass, ConflictProbe } from "./types.ts";
|
||||
|
||||
export type ContrastExpectedOutcome = Readonly<{
|
||||
outcomeId: string;
|
||||
supportsCandidateIds: readonly string[];
|
||||
@@ -32,6 +34,16 @@ export type VargaDifference = Readonly<{
|
||||
signs: readonly string[];
|
||||
}>;
|
||||
|
||||
export type WindowScanTransition = Readonly<{
|
||||
layer: string;
|
||||
at: string;
|
||||
}>;
|
||||
|
||||
export type RemainingVargaSplit = Readonly<{
|
||||
layer: string;
|
||||
groups: readonly (readonly string[])[];
|
||||
}>;
|
||||
|
||||
export type CandidateContrastPacket = Readonly<{
|
||||
candidateSetVersion: string;
|
||||
probes: readonly CandidateDiscriminatorProbe[];
|
||||
@@ -70,15 +82,83 @@ const D10_PREDICTIONS: Readonly<Record<string, string>> = {
|
||||
双鱼: "服务、艺术或界限更模糊的工作",
|
||||
};
|
||||
|
||||
const REMAINING_LAYER_ORDER = ["d24", "d5", "d10", "d9"] as const;
|
||||
const DUTY_ANSWERED_RE = /技术执行|算法|分析|数据处理|系统维护|组织型|第三个|照顾、家庭|台前|带人|公开担责/;
|
||||
const ANSWER_CLASSES: ReadonlySet<string> = new Set(["yes", "weak_yes", "no", "unsure"]);
|
||||
|
||||
function signKey(value: string): string {
|
||||
return value.replace(/座$/, "").trim();
|
||||
}
|
||||
|
||||
function clockMinutes(time: string): number {
|
||||
const [hour, minute] = time.split(":").map(Number);
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
export function remainingLayerGroups(
|
||||
candidateTimes: readonly string[],
|
||||
transitions: readonly WindowScanTransition[],
|
||||
layer: string,
|
||||
): readonly (readonly string[])[] {
|
||||
const changes = transitions
|
||||
.filter((item) => item.layer === layer)
|
||||
.map((item) => item.at)
|
||||
.filter((at) => /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(at))
|
||||
.sort((left, right) => clockMinutes(left) - clockMinutes(right));
|
||||
const groups = new Map<number, string[]>();
|
||||
for (const time of candidateTimes) {
|
||||
if (!/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(time)) continue;
|
||||
const point = clockMinutes(time);
|
||||
let index = 0;
|
||||
for (const at of changes) {
|
||||
if (point >= clockMinutes(at)) index += 1;
|
||||
}
|
||||
const row = groups.get(index) ?? [];
|
||||
row.push(time);
|
||||
groups.set(index, row);
|
||||
}
|
||||
return [...groups.entries()]
|
||||
.sort((left, right) => left[0] - right[0])
|
||||
.map(([, times]) => times);
|
||||
}
|
||||
|
||||
export function remainingVargaSplits(
|
||||
candidateTimes: readonly string[],
|
||||
transitions: readonly WindowScanTransition[],
|
||||
): readonly RemainingVargaSplit[] {
|
||||
if (candidateTimes.length < 2) return [];
|
||||
const rows: RemainingVargaSplit[] = [];
|
||||
for (const layer of REMAINING_LAYER_ORDER) {
|
||||
const groups = remainingLayerGroups(candidateTimes, transitions, layer);
|
||||
if (groups.length < 2) continue;
|
||||
rows.push({ layer, groups });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function askedKeysFromOccupationEvidence(
|
||||
evidence: readonly Readonly<{
|
||||
domain?: string | null;
|
||||
eventKind?: string | null;
|
||||
summary?: string | null;
|
||||
}>[],
|
||||
): string[] {
|
||||
for (const item of evidence) {
|
||||
const occupation = item.domain === "occupation" || item.eventKind === "occupation_note";
|
||||
if (!occupation) continue;
|
||||
if (DUTY_ANSWERED_RE.test(item.summary ?? "")) return ["varga.d10"];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function buildCandidateContrastPacket(input: {
|
||||
candidateSetVersion: string;
|
||||
calculationResultId?: string | null;
|
||||
engineProbes?: readonly EngineContrastProbe[];
|
||||
vargaDifferences?: readonly VargaDifference[];
|
||||
remainingSplits?: readonly RemainingVargaSplit[];
|
||||
candidateTimes?: readonly string[];
|
||||
transitions?: readonly WindowScanTransition[];
|
||||
askedKeys?: readonly string[];
|
||||
}): CandidateContrastPacket {
|
||||
const asked = new Set(input.askedKeys ?? []);
|
||||
@@ -90,8 +170,21 @@ export function buildCandidateContrastPacket(input: {
|
||||
}
|
||||
return [built];
|
||||
});
|
||||
const vargaDifferences = input.vargaDifferences ?? [];
|
||||
const fromVarga = vargaProbe(vargaDifferences, input.candidateSetVersion, input.calculationResultId ?? null, asked);
|
||||
const remainingSplits = input.remainingSplits
|
||||
?? remainingVargaSplits(input.candidateTimes ?? [], input.transitions ?? []);
|
||||
const vargaDifferences = vargaDifferencesForPacket({
|
||||
remainingSplits,
|
||||
windowDifferences: input.vargaDifferences ?? [],
|
||||
candidateTimes: input.candidateTimes ?? [],
|
||||
transitions: input.transitions ?? [],
|
||||
});
|
||||
const fromVarga = vargaProbe(
|
||||
vargaDifferences,
|
||||
remainingSplits,
|
||||
input.candidateSetVersion,
|
||||
input.calculationResultId ?? null,
|
||||
asked,
|
||||
);
|
||||
const probes = [...fromEngine, ...(fromVarga ? [fromVarga] : [])]
|
||||
.sort((left, right) => right.informationGain - left.informationGain);
|
||||
return {
|
||||
@@ -101,6 +194,24 @@ export function buildCandidateContrastPacket(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function vargaDifferencesForPacket(input: {
|
||||
remainingSplits: readonly RemainingVargaSplit[];
|
||||
windowDifferences: readonly VargaDifference[];
|
||||
candidateTimes: readonly string[];
|
||||
transitions: readonly WindowScanTransition[];
|
||||
}): readonly VargaDifference[] {
|
||||
if (input.remainingSplits.length > 0) {
|
||||
return input.remainingSplits.map((split) => ({
|
||||
layer: split.layer,
|
||||
signs: split.groups.map((group) => group.join("|")),
|
||||
}));
|
||||
}
|
||||
if (input.candidateTimes.length >= 2 && input.transitions.length > 0) {
|
||||
return [];
|
||||
}
|
||||
return input.windowDifferences;
|
||||
}
|
||||
|
||||
export function selectDiscriminatorProbe(
|
||||
packet: CandidateContrastPacket | null | undefined,
|
||||
): CandidateDiscriminatorProbe | null {
|
||||
@@ -108,6 +219,42 @@ export function selectDiscriminatorProbe(
|
||||
return ranked[0] ?? null;
|
||||
}
|
||||
|
||||
export function conflictProbesFromContrast(
|
||||
packet: CandidateContrastPacket | null | undefined,
|
||||
): ConflictProbe[] {
|
||||
return (packet?.probes ?? []).flatMap((probe) => {
|
||||
if (!probe.semanticKey.startsWith("varga.")) return [];
|
||||
const outcomes = probe.expectedOutcomes.flatMap((row, index) => {
|
||||
const answer = ANSWER_CLASSES.has(row.outcomeId)
|
||||
? row.outcomeId as AnswerClass
|
||||
: (["yes", "weak_yes", "no"][index] as AnswerClass | undefined);
|
||||
if (!answer) return [];
|
||||
return [{
|
||||
answer_class: answer,
|
||||
supports: row.supportsCandidateIds,
|
||||
conflicts: row.conflictsCandidateIds,
|
||||
}];
|
||||
});
|
||||
if (outcomes.length < 2) return [];
|
||||
const candidateIds = [...new Set(probe.expectedOutcomes.flatMap((row) => [
|
||||
...row.supportsCandidateIds,
|
||||
...row.conflictsCandidateIds,
|
||||
]))];
|
||||
return [{
|
||||
id: probe.probeId,
|
||||
semantic_key: probe.semanticKey,
|
||||
candidate_split_hash: probe.candidateSplitHash,
|
||||
domain: probe.domain ?? "career",
|
||||
year: probe.year ?? 0,
|
||||
question: probe.question,
|
||||
candidate_ids: candidateIds,
|
||||
expected_outcomes: outcomes,
|
||||
information_gain: probe.informationGain,
|
||||
source: "varga_contrast",
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function probeFromEngine(
|
||||
probe: EngineContrastProbe,
|
||||
candidateSetVersion: string,
|
||||
@@ -147,13 +294,22 @@ function probeFromEngine(
|
||||
|
||||
function vargaProbe(
|
||||
differences: readonly VargaDifference[],
|
||||
remainingSplits: readonly RemainingVargaSplit[],
|
||||
candidateSetVersion: string,
|
||||
calculationResultId: string | null,
|
||||
asked: ReadonlySet<string>,
|
||||
): CandidateDiscriminatorProbe | null {
|
||||
const remaining = remainingSplits.find((item) => !vargaLayerAsked(asked, item.layer));
|
||||
if (remaining) {
|
||||
return vargaProbeFromRemaining(remaining, candidateSetVersion, calculationResultId);
|
||||
}
|
||||
const d10 = differences.find((item) => item.layer === "d10" && item.signs.length >= 2);
|
||||
const d9 = differences.find((item) => item.layer === "d9" && item.signs.length >= 2);
|
||||
const chosen = d10 ?? d9;
|
||||
const chosen = d10 && !vargaLayerAsked(asked, "d10")
|
||||
? d10
|
||||
: d9 && !vargaLayerAsked(asked, "d9")
|
||||
? d9
|
||||
: null;
|
||||
if (!chosen) return null;
|
||||
const semanticKey = `varga.${chosen.layer}.${chosen.signs.join("|")}`;
|
||||
if (asked.has(semanticKey)) return null;
|
||||
@@ -180,3 +336,59 @@ function vargaProbe(
|
||||
semanticKey,
|
||||
};
|
||||
}
|
||||
|
||||
function vargaLayerAsked(asked: ReadonlySet<string>, layer: string): boolean {
|
||||
if (asked.has(`varga.${layer}`)) return true;
|
||||
for (const key of asked) {
|
||||
if (key === layer || key.startsWith(`varga.${layer}.`)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function vargaProbeFromRemaining(
|
||||
split: RemainingVargaSplit,
|
||||
candidateSetVersion: string,
|
||||
calculationResultId: string | null,
|
||||
): CandidateDiscriminatorProbe {
|
||||
const allMinutes = split.groups.flat();
|
||||
const outcomes = remainingOutcomes(split.groups, allMinutes);
|
||||
const semanticKey = `varga.${split.layer}.${split.groups.map((group) => group.join("|")).join("/")}`;
|
||||
const layerLabel = split.layer.toUpperCase();
|
||||
const education = split.layer === "d24" || split.layer === "d5";
|
||||
const question = education
|
||||
? "当前几个候选在学业盘上还分得开。请核对一段还没用进评分的学业前事:那次高考或重要考试有没有发挥明显失常、压力很大?"
|
||||
: split.layer === "d10"
|
||||
? "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事:长期更接近照顾或家庭,还是台前带人,还是技术执行或分析?"
|
||||
: `当前几个候选在关系盘上还分得开。请核对一段还没用进评分的感情前事,用来对照 ${layerLabel} 差异。`;
|
||||
return {
|
||||
probeId: `contrast:${semanticKey}`,
|
||||
candidateSetVersion,
|
||||
question,
|
||||
expectedOutcomes: outcomes,
|
||||
candidateSplitHash: semanticKey,
|
||||
informationGain: split.layer === "d24" || split.layer === "d5" ? 0.16 : 0.12,
|
||||
sourceFeatures: [{ technique: layerLabel, calculationResultId }],
|
||||
domain: education ? "education" : split.layer === "d10" ? "career" : "relationship",
|
||||
year: null,
|
||||
semanticKey,
|
||||
};
|
||||
}
|
||||
|
||||
function remainingOutcomes(
|
||||
groups: readonly (readonly string[])[],
|
||||
allMinutes: readonly string[],
|
||||
): ContrastExpectedOutcome[] {
|
||||
if (groups.length === 2) {
|
||||
return [
|
||||
{ outcomeId: "yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
|
||||
{ outcomeId: "weak_yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
|
||||
{ outcomeId: "no", supportsCandidateIds: groups[1], conflictsCandidateIds: groups[0] },
|
||||
];
|
||||
}
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
return groups.slice(0, 3).map((group, index) => ({
|
||||
outcomeId: classes[index] ?? `group_${index}`,
|
||||
supportsCandidateIds: group,
|
||||
conflictsCandidateIds: allMinutes.filter((time) => !group.includes(time)),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { isDuplicateProbe } from "../core/duplicate-probes.ts";
|
||||
import { probeFromEngine } from "../core/probes-from-engine.ts";
|
||||
import { selectHighestGainProbe } from "../core/select-probe.ts";
|
||||
import { askedKeysFromOccupationEvidence } from "../core/candidate-contrast-packet.ts";
|
||||
import type { AnswerClass, ConflictProbe, InferenceState } from "../core/types.ts";
|
||||
import {
|
||||
isHoldoutVerificationQuote,
|
||||
@@ -47,6 +48,20 @@ export function askedProbeKeysFromReceipt(
|
||||
return keys;
|
||||
}
|
||||
|
||||
export function askedDiscriminatorKeys(
|
||||
receipt: Readonly<Record<string, unknown>> | null | undefined,
|
||||
evidence: readonly Readonly<{
|
||||
domain?: string | null;
|
||||
eventKind?: string | null;
|
||||
summary?: string | null;
|
||||
}>[] = [],
|
||||
): string[] {
|
||||
return [
|
||||
...askedProbeKeysFromReceipt(receipt),
|
||||
...askedKeysFromOccupationEvidence(evidence),
|
||||
];
|
||||
}
|
||||
|
||||
export { previousInferenceFromReceipt } from "../core/compose-receipt.ts";
|
||||
|
||||
export function compactInferenceProjection(state: InferenceState | null | undefined): Record<string, unknown> | null {
|
||||
@@ -93,6 +108,7 @@ export function buildCaseInferenceState(input: {
|
||||
datePrecision: string;
|
||||
}>[];
|
||||
probes: readonly DiscriminatingEventProbe[];
|
||||
extraProbes?: readonly ConflictProbe[];
|
||||
previous?: InferenceState | null;
|
||||
transitionTimes?: readonly string[];
|
||||
eventLedger?: Readonly<Record<string, Readonly<Record<string, number>>>>;
|
||||
@@ -103,7 +119,10 @@ export function buildCaseInferenceState(input: {
|
||||
year: yearFrom(item.occurredFrom),
|
||||
precision: asPrecision(item.datePrecision),
|
||||
}));
|
||||
const probes = input.probes.map(probeFromEngine);
|
||||
const probes = [
|
||||
...input.probes.map(probeFromEngine),
|
||||
...(input.extraProbes ?? []),
|
||||
];
|
||||
return buildInferenceState({
|
||||
range_start: input.range.start_time,
|
||||
range_end: input.range.end_time,
|
||||
@@ -175,10 +194,15 @@ export function matchProbeForChoice(
|
||||
const splitHash = asText(row?.candidate_split_hash);
|
||||
const probes = state.probes;
|
||||
if (probeId) {
|
||||
return probes.find((item) => item.id === probeId) ?? null;
|
||||
const found = probes.find((item) => (
|
||||
item.id === probeId
|
||||
|| item.semantic_key === probeId
|
||||
|| probeId === `probe:${item.semantic_key}`
|
||||
));
|
||||
if (found) return found;
|
||||
}
|
||||
if (semanticKey) {
|
||||
const found = probes.find((item) => item.semantic_key === semanticKey);
|
||||
const found = probes.find((item) => item.semantic_key === semanticKey || item.id === semanticKey);
|
||||
if (found) return found;
|
||||
}
|
||||
if (splitHash) {
|
||||
@@ -191,20 +215,65 @@ export function matchProbeForChoice(
|
||||
return selectHighestGainProbe(unanswered.length > 0 ? unanswered : probes, state.answered_probes);
|
||||
}
|
||||
|
||||
function probeMatchesPreferred(
|
||||
probe: ConflictProbe,
|
||||
preferred: { semantic_key?: string | null; candidate_split_hash?: string | null; probe_id?: string | null },
|
||||
): boolean {
|
||||
const semanticKey = preferred.semantic_key?.trim() || null;
|
||||
const splitHash = preferred.candidate_split_hash?.trim() || null;
|
||||
const probeId = preferred.probe_id?.trim() || null;
|
||||
if (probeId && (probe.id === probeId || probe.semantic_key === probeId)) return true;
|
||||
if (semanticKey && (probe.semantic_key === semanticKey || probe.id === semanticKey)) return true;
|
||||
if (splitHash && probe.candidate_split_hash === splitHash) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function stampChoiceSchemaWithProbe(
|
||||
schema: Readonly<Record<string, unknown>>,
|
||||
state: InferenceState | null,
|
||||
questionId: string,
|
||||
preferred?: {
|
||||
semantic_key?: string | null;
|
||||
candidate_split_hash?: string | null;
|
||||
probe_id?: string | null;
|
||||
},
|
||||
): Record<string, unknown> {
|
||||
if (!hasChoiceSchema(schema) || !state) return { ...schema };
|
||||
const next = selectHighestGainProbe(state.probes, state.answered_probes);
|
||||
if (!next) return { ...schema };
|
||||
if (!hasChoiceSchema(schema)) return { ...schema };
|
||||
const scoring = schema.scoring === false || questionId.endsWith(":holdout") ? false : true;
|
||||
const preferredKey = preferred?.semantic_key?.trim() || asText(schema.semantic_key);
|
||||
const preferredSplit = preferred?.candidate_split_hash?.trim() || asText(schema.candidate_split_hash);
|
||||
const preferredId = preferred?.probe_id?.trim() || asText(schema.probe_id);
|
||||
const matched = state?.probes.find((probe) => probeMatchesPreferred(probe, {
|
||||
semantic_key: preferredKey,
|
||||
candidate_split_hash: preferredSplit,
|
||||
probe_id: preferredId,
|
||||
})) ?? null;
|
||||
if (preferredKey && !matched) {
|
||||
return {
|
||||
...schema,
|
||||
probe_id: preferredId ?? `probe:${preferredKey}`,
|
||||
semantic_key: preferredKey,
|
||||
candidate_split_hash: preferredSplit ?? preferredKey,
|
||||
scoring,
|
||||
};
|
||||
}
|
||||
const next = matched ?? (state ? selectHighestGainProbe(state.probes, state.answered_probes) : null);
|
||||
if (!next) {
|
||||
if (!preferredKey) return { ...schema };
|
||||
return {
|
||||
...schema,
|
||||
probe_id: preferredId ?? `probe:${preferredKey}`,
|
||||
semantic_key: preferredKey,
|
||||
candidate_split_hash: preferredSplit ?? preferredKey,
|
||||
scoring,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...schema,
|
||||
probe_id: next.id,
|
||||
semantic_key: next.semantic_key,
|
||||
candidate_split_hash: next.candidate_split_hash,
|
||||
scoring: schema.scoring === false || questionId.endsWith(":holdout") ? false : true,
|
||||
scoring,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -246,7 +315,7 @@ export function applyChoiceWithoutEvidence(
|
||||
const lastAnsweredId = state.answered_probes.at(-1)?.probe_id ?? null;
|
||||
const answerClass = classifyChoiceAnswer(choiceKey);
|
||||
if (
|
||||
(submittedProbeId && submittedProbeId !== openProbeId && submittedProbeId !== lastAnsweredId)
|
||||
(submittedProbeId && submittedProbeId !== openProbeId && submittedProbeId !== lastAnsweredId && submittedProbeId !== probe.id && submittedProbeId !== probe.semantic_key && submittedProbeId !== `probe:${probe.semantic_key}`)
|
||||
|| (openProbeId && probe.id !== openProbeId && probe.id !== lastAnsweredId)
|
||||
) {
|
||||
return { applied: false, reason: "stale_probe", state, answerClass, probeId: probe.id };
|
||||
|
||||
@@ -5,8 +5,13 @@
|
||||
* (candidates already diverge or holdout) and the Agent wrote choice copy.
|
||||
*/
|
||||
|
||||
import { projectRectificationChoiceCard } from "./method-followup";
|
||||
import { previousInferenceFromReceipt } from "./inference-adapter";
|
||||
import {
|
||||
askedKeysFromOccupationEvidence,
|
||||
buildCandidateContrastPacket,
|
||||
} from "../core/candidate-contrast-packet.ts";
|
||||
import { evaluateCandidateSeparation } from "../core/candidate-separation.ts";
|
||||
import { askedProbeKeysFromReceipt, previousInferenceFromReceipt } from "./inference-adapter";
|
||||
import { latestUserStoppedCollecting, projectRectificationChoiceCard } from "./method-followup";
|
||||
import { refinementFromDecisionReceipt } from "./refinement-packet";
|
||||
import {
|
||||
internalObservationsFromWindowScan,
|
||||
@@ -14,6 +19,20 @@ import {
|
||||
} from "./varga-observations";
|
||||
import type { RectificationChoiceCard } from "./choice-card";
|
||||
|
||||
function candidateScoresFromDossier(latest: {
|
||||
decisionReceipt: Readonly<Record<string, unknown>> | null;
|
||||
candidates?: readonly Readonly<{ time: string; relativeSupport?: number; posterior_score?: number }>[];
|
||||
} | null) {
|
||||
const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null);
|
||||
if (inference && inference.candidates.length > 0) {
|
||||
return inference.candidates.map((item) => ({ time: item.time, score: item.posterior_score }));
|
||||
}
|
||||
return (latest?.candidates ?? []).map((item) => ({
|
||||
time: item.time,
|
||||
score: item.relativeSupport ?? item.posterior_score ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
export function choiceCardFromCaseDossier(dossier: {
|
||||
evidence: readonly Readonly<{
|
||||
status: string;
|
||||
@@ -35,17 +54,42 @@ export function choiceCardFromCaseDossier(dossier: {
|
||||
declinedSkippedTopics: readonly Readonly<Record<string, unknown>>[];
|
||||
};
|
||||
latestResult: {
|
||||
resultId?: string;
|
||||
decisionReceipt: Readonly<Record<string, unknown>> | null;
|
||||
selectionAllowed?: boolean;
|
||||
candidates?: readonly Readonly<{ time: string; relativeSupport?: number }>[];
|
||||
} | null;
|
||||
case: {
|
||||
acceptedTime: string | null;
|
||||
};
|
||||
turns?: readonly Readonly<{ role: string; text: string | null }>[];
|
||||
}): RectificationChoiceCard | null {
|
||||
const windowScan = windowScanFromDecisionReceipt(dossier.latestResult?.decisionReceipt ?? null);
|
||||
const observations = internalObservationsFromWindowScan(windowScan);
|
||||
const refinement = refinementFromDecisionReceipt(dossier.latestResult?.decisionReceipt ?? null);
|
||||
const inference = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
|
||||
const candidateScores = candidateScoresFromDossier(dossier.latestResult);
|
||||
const askedProbeKeys = [
|
||||
...askedProbeKeysFromReceipt(dossier.latestResult?.decisionReceipt),
|
||||
...askedKeysFromOccupationEvidence(dossier.evidence),
|
||||
];
|
||||
const contrastPacket = buildCandidateContrastPacket({
|
||||
candidateSetVersion: inference?.candidate_set_id ?? dossier.latestResult?.resultId ?? "none",
|
||||
calculationResultId: dossier.latestResult?.resultId ?? null,
|
||||
engineProbes: refinement.discriminating_event_probes,
|
||||
vargaDifferences: [
|
||||
...(windowScan?.d9_candidates_differ && windowScan.d9_sign_names.length >= 2
|
||||
? [{ layer: "d9", signs: windowScan.d9_sign_names }]
|
||||
: []),
|
||||
...(windowScan?.d10_candidates_differ && windowScan.d10_sign_names.length >= 2
|
||||
? [{ layer: "d10", signs: windowScan.d10_sign_names }]
|
||||
: []),
|
||||
],
|
||||
candidateTimes: candidateScores.map((item) => item.time),
|
||||
transitions: windowScan?.transitions ?? [],
|
||||
askedKeys: askedProbeKeys,
|
||||
});
|
||||
const userStopped = latestUserStoppedCollecting(dossier.turns ?? []);
|
||||
return projectRectificationChoiceCard({
|
||||
evidence: dossier.evidence,
|
||||
activeFocus: dossier.conversationSummary.activeFocus,
|
||||
@@ -56,9 +100,14 @@ export function choiceCardFromCaseDossier(dossier: {
|
||||
nakshatraBoundary: refinement.nakshatra_boundary,
|
||||
oosBlindPrompts: refinement.oos_blind_prompts,
|
||||
eventProbes: refinement.discriminating_event_probes,
|
||||
askedProbeKeys,
|
||||
accepted: Boolean(dossier.case.acceptedTime),
|
||||
selectionAllowed: dossier.latestResult?.selectionAllowed === true,
|
||||
proposeAllowed: dossier.latestResult?.decisionReceipt?.propose_allowed === true,
|
||||
caseRevision: inference?.revision ?? 0,
|
||||
contrastPacket,
|
||||
candidateScores,
|
||||
userStopped,
|
||||
candidatesSeparated: evaluateCandidateSeparation(candidateScores).sufficient,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
type HoldoutValidationStatus,
|
||||
} from "../core/decide-next-action.ts";
|
||||
import {
|
||||
askedKeysFromOccupationEvidence,
|
||||
selectDiscriminatorProbe,
|
||||
type CandidateContrastPacket,
|
||||
type CandidateDiscriminatorProbe,
|
||||
@@ -239,6 +240,15 @@ const PROBE_METHOD_ID = {
|
||||
health_pressure: "d30_health",
|
||||
} as const;
|
||||
|
||||
function contrastFollowupDomain(
|
||||
domain: string | null,
|
||||
): keyof typeof REVERSE_VERIFY_THEME {
|
||||
if (domain && domain in REVERSE_VERIFY_THEME) {
|
||||
return domain as keyof typeof REVERSE_VERIFY_THEME;
|
||||
}
|
||||
return "career";
|
||||
}
|
||||
|
||||
const CONFLICT_PROBE_SOURCES = new Set<string>([
|
||||
"dasha_boundary",
|
||||
"dasha_activation",
|
||||
@@ -342,7 +352,7 @@ function action(
|
||||
return { id, user_meaning };
|
||||
}
|
||||
|
||||
const USER_STOP_PATTERN = /暂时想不到了|没有更多|先这样/;
|
||||
const USER_STOP_PATTERN = /暂时想不到了|没有更多|没有其它|没有其他|想不起来了|先这样|没有了|没了/;
|
||||
|
||||
export function latestUserStoppedCollecting(
|
||||
turns: readonly Readonly<{ role: string; text: string | null }>[],
|
||||
@@ -676,7 +686,10 @@ export function buildMethodFollowupPlan(input: {
|
||||
|
||||
let next: MethodFollowup | null = null;
|
||||
const stage = input.precisionStage ?? null;
|
||||
const askedKeys = new Set(input.askedProbeKeys ?? []);
|
||||
const askedKeys = new Set([
|
||||
...(input.askedProbeKeys ?? []),
|
||||
...askedKeysFromOccupationEvidence(input.evidence),
|
||||
]);
|
||||
const conflictProbe = dashaCovered
|
||||
? remainingConflictProbes(input.eventProbes, input.evidence, declined, askedKeys)[0] ?? null
|
||||
: null;
|
||||
@@ -768,16 +781,16 @@ export function buildMethodFollowupPlan(input: {
|
||||
source: "method_coverage",
|
||||
});
|
||||
} else if (contrastProbe && !candidatesSeparated) {
|
||||
const domain = contrastProbe.domain === "relationship" ? "relationship" : "career";
|
||||
const domain = contrastFollowupDomain(contrastProbe.domain);
|
||||
next = makeFollowup({
|
||||
method_id: domain === "relationship" ? "d9_relationship" : "d10_career",
|
||||
method_id: PROBE_METHOD_ID[domain],
|
||||
intent: "distinguish_candidates",
|
||||
ask_theme: domain === "relationship" ? "relationship_style" : "career_style",
|
||||
ask_theme: REVERSE_VERIFY_THEME[domain],
|
||||
domain,
|
||||
kind_hint: domain === "relationship" ? "relationship_change" : "career_change",
|
||||
kind_hint: REVERSE_VERIFY_KIND[domain],
|
||||
user_prompt_hint: agentHint(
|
||||
contrastProbe.question,
|
||||
domain === "relationship" ? "D9" : "D10",
|
||||
REVERSE_VERIFY_VARGA[domain],
|
||||
"按候选盘面差异核对前事,不要问两套盘哪个更像。",
|
||||
),
|
||||
source: "event_probe",
|
||||
|
||||
@@ -55,6 +55,7 @@ function expectedAnswerSchemaFor(
|
||||
frame: RectificationChoiceFrame,
|
||||
questionId: string,
|
||||
decisionReceipt: Readonly<Record<string, unknown>> | null | undefined,
|
||||
followup: MethodFollowup,
|
||||
): Record<string, unknown> | null {
|
||||
const copy = serverOwnedChoiceCopy(frame);
|
||||
if (!copy) return null;
|
||||
@@ -66,11 +67,17 @@ function expectedAnswerSchemaFor(
|
||||
option_c: copy.option_c,
|
||||
option_d: copy.option_d,
|
||||
},
|
||||
semantic_key: followup.semantic_key ?? null,
|
||||
candidate_split_hash: followup.candidate_split_hash ?? null,
|
||||
};
|
||||
return stampChoiceSchemaWithProbe(
|
||||
schema,
|
||||
previousInferenceFromReceipt(decisionReceipt ?? null),
|
||||
questionId,
|
||||
{
|
||||
semantic_key: followup.semantic_key,
|
||||
candidate_split_hash: followup.candidate_split_hash,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,7 +99,7 @@ export async function persistServerOwnedFocus(input: {
|
||||
return { status: skip, focus: input.activeFocus, questionId: null, prompt: null };
|
||||
}
|
||||
const questionId = stableFollowupQuestionId(followup);
|
||||
const schema = expectedAnswerSchemaFor(frame, questionId, input.decisionReceipt);
|
||||
const schema = expectedAnswerSchemaFor(frame, questionId, input.decisionReceipt, followup);
|
||||
if (!schema?.choice) {
|
||||
return { status: "skipped", focus: input.activeFocus, questionId, prompt: null };
|
||||
}
|
||||
@@ -117,14 +124,6 @@ export async function persistServerOwnedFocus(input: {
|
||||
) {
|
||||
return { status: "already_open", focus: active, questionId: active.questionId, prompt };
|
||||
}
|
||||
if (followup.source === "event_probe" && !schemaProbeId(schema)) {
|
||||
return {
|
||||
status: "probe_already_answered",
|
||||
focus: input.activeFocus,
|
||||
questionId,
|
||||
prompt: null,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await setV10ConversationFocus(input.accounting, input.userId, input.caseId, {
|
||||
questionId,
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
} from "../../rectification-activity-labels.ts";
|
||||
|
||||
const CJK_RE = /[\u4e00-\u9fff]/;
|
||||
const INTERNAL_TOKEN_RE = /\b(?:datePrecision|occurredFrom|occurredTo|proposedKind|education_start|missing_evidence|SKILL\.md|rectification-[a-z0-9-]+|focusId|evidenceId|display_date_label|occupation_note|method_followup_plan|open_question|next_action|next_user_action|not_separated|propose_allowed|selection_allowed|information_gain|event_probe|session_outcome|unique_minute_path|confirmation_allowed|collect_method_evidence|candidate_contrast|deferred_followup)\b/;
|
||||
const PROCESS_ZH_RE = /skill\s*规则|不得猜补|让我(?:调用|记录|batch|提交|继续|用)|我(?:决定|倾向|batch|需要用|需要继续|继续收集|继续访谈|自然地|用自然语言)|权衡:|内部矛盾|思维链|调用 batch|批量工具|写入(?:这些)?证据|datePrecision|occurredFrom|occurredTo|方法覆盖|还不能出牌|不得出牌|本轮对照了|不可分宽度|重新计算了候选|带评分日期|当前还应继续收集|根据 method_followup/;
|
||||
const INTERNAL_TOKEN_RE = /\b(?:datePrecision|occurredFrom|occurredTo|proposedKind|education_start|missing_evidence|SKILL\.md|rectification-[a-z0-9-]+|focusId|evidenceId|display_date_label|occupation_note|method_followup_plan|open_question|next_action|next_user_action|not_separated|propose_allowed|selection_allowed|information_gain|event_probe|session_outcome|unique_minute_path|confirmation_allowed|collect_method_evidence|candidate_contrast(?:_packet)?|choice_frame|deferred_followup)\b/;
|
||||
const PROCESS_ZH_RE = /skill\s*规则|不得猜补|让我(?:调用|记录|batch|提交|继续|用)|我(?:决定|倾向|batch|需要用|需要继续|继续收集|继续访谈|自然地|用自然语言)|权衡:|内部矛盾|思维链|调用 batch|批量工具|写入(?:这些)?证据|datePrecision|occurredFrom|occurredTo|方法覆盖|方法资料已齐|还不能出牌|不得出牌|不得\s*offer|本轮对照了|这意味着|服务器给了|第.{0,4}条边界|不可分宽度|重新计算了候选|带评分日期|当前还应继续收集|根据 method_followup/;
|
||||
const THIRD_PERSON_USER_RE = /^用户|用户(?:在上|提到|先(?:说|提到)|说|自己|的核心|想表达|原话|的最终|对年份|提供了)/;
|
||||
|
||||
const ACTIVITY_ECHO_LABELS = [
|
||||
|
||||
Reference in New Issue
Block a user