fix(web): stop rectification interview when remaining minutes no longer split
Coverage-complete ties stayed in discrimination because whole-window D9/D24 follow-ups were treated as probes, and restated dates inserted duplicate evidence. Skip encoded remaining layers, ask leftover D4 or offer a provisional range, and dedupe dated rows by kind and date. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -340,5 +340,6 @@ function phaseFor(status: ResultStatus, fallback: RectificationPhase): Rectifica
|
||||
if (status === "converged" || status === "credible_range") return "completed";
|
||||
if (status === "max_rounds_reached" || status === "validation_failed") return "stopped";
|
||||
if (status === "insufficient_evidence") return "event_collection";
|
||||
if (status === "discriminating") return "discrimination";
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@@ -67,29 +67,13 @@ export type EngineContrastProbe = Readonly<{
|
||||
right_time?: string;
|
||||
}>;
|
||||
|
||||
const D10_PREDICTIONS: Readonly<Record<string, string>> = {
|
||||
白羊: "某一阶段身份独立、承担主导职责",
|
||||
金牛: "稳定技术或事务执行、在组织内慢慢积累",
|
||||
双子: "沟通、写作或频繁切换任务",
|
||||
巨蟹: "照顾、家庭或情感劳动更重的职责",
|
||||
狮子: "站到台前、带人、或公开担责",
|
||||
处女: "稳定技术执行、组织内分析或服务",
|
||||
天秤: "跨领域合作、工作环境或合作模式变化",
|
||||
天蝎: "研究、转化、或把一件事做深",
|
||||
射手: "跨领域、教学或更开阔的工作环境",
|
||||
摩羯: "管理职责、长期结构、延迟回报",
|
||||
水瓶: "独立技术路线、非传统协作",
|
||||
双鱼: "服务、艺术或界限更模糊的工作",
|
||||
};
|
||||
|
||||
const REMAINING_LAYER_ORDER = ["d24", "d5", "d10", "d9"] as const;
|
||||
const DUTY_ANSWERED_RE = /技术执行|算法|分析|数据处理|系统维护|组织型|第三个|照顾、家庭|台前|带人|公开担责/;
|
||||
const REMAINING_LAYER_ORDER = ["d24", "d5", "d10", "d9", "d4"] as const;
|
||||
const DUTY_ANSWERED_RE = /技术执行|算法|分析|数据处理|系统维护|组织型|第三个|照顾、家庭|台前|带人|公开担责|程序员|前端|工程师|开发/;
|
||||
const EXAM_QUALITY_RE = /失利|失常|复读|没考好|考砸|发挥不好|发挥失常|压力很大/;
|
||||
const RELOCATION_RE = /搬家|离乡|迁居|长期异地|离开家/;
|
||||
const LIVE_EVIDENCE = new Set(["confirmed", "draft", "pending_confirmation"]);
|
||||
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;
|
||||
@@ -136,19 +120,39 @@ export function remainingVargaSplits(
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function askedKeysFromOccupationEvidence(
|
||||
export function askedKeysFromLedgerEvidence(
|
||||
evidence: readonly Readonly<{
|
||||
status?: string | null;
|
||||
domain?: string | null;
|
||||
eventKind?: string | null;
|
||||
summary?: string | null;
|
||||
}>[],
|
||||
): string[] {
|
||||
const keys = new Set<string>();
|
||||
for (const item of evidence) {
|
||||
if (item.status && !LIVE_EVIDENCE.has(item.status)) continue;
|
||||
const summary = item.summary ?? "";
|
||||
const occupation = item.domain === "occupation" || item.eventKind === "occupation_note";
|
||||
if (!occupation) continue;
|
||||
if (DUTY_ANSWERED_RE.test(item.summary ?? "")) return ["varga.d10"];
|
||||
if (occupation || DUTY_ANSWERED_RE.test(summary)) keys.add("varga.d10");
|
||||
if (item.domain === "education" && EXAM_QUALITY_RE.test(summary)) {
|
||||
keys.add("varga.d24");
|
||||
keys.add("varga.d5");
|
||||
}
|
||||
const relocation = item.domain === "relocation" || item.eventKind === "home_change";
|
||||
if (relocation || RELOCATION_RE.test(summary)) keys.add("varga.d4");
|
||||
}
|
||||
return [];
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
export function askedKeysFromOccupationEvidence(
|
||||
evidence: readonly Readonly<{
|
||||
status?: string | null;
|
||||
domain?: string | null;
|
||||
eventKind?: string | null;
|
||||
summary?: string | null;
|
||||
}>[],
|
||||
): string[] {
|
||||
return askedKeysFromLedgerEvidence(evidence);
|
||||
}
|
||||
|
||||
export function buildCandidateContrastPacket(input: {
|
||||
@@ -179,7 +183,6 @@ export function buildCandidateContrastPacket(input: {
|
||||
transitions: input.transitions ?? [],
|
||||
});
|
||||
const fromVarga = vargaProbe(
|
||||
vargaDifferences,
|
||||
remainingSplits,
|
||||
input.candidateSetVersion,
|
||||
input.calculationResultId ?? null,
|
||||
@@ -293,48 +296,14 @@ 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 && !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;
|
||||
const predictions = chosen.signs.map((sign) => D10_PREDICTIONS[signKey(sign)] ?? `${sign} 这一段更常见的前事`);
|
||||
const outcomes: ContrastExpectedOutcome[] = chosen.signs.map((sign, index) => ({
|
||||
outcomeId: `supports_${signKey(sign)}`,
|
||||
supportsCandidateIds: [sign],
|
||||
conflictsCandidateIds: chosen.signs.filter((_, other) => other !== index),
|
||||
}));
|
||||
const layerLabel = chosen.layer.toUpperCase();
|
||||
const question = chosen.layer === "d10"
|
||||
? `当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事:更接近${predictions.join(",还是")}?`
|
||||
: `当前几个候选在关系盘上还分得开。请核对一段还没用进评分的感情前事,用来对照 ${layerLabel} 差异。`;
|
||||
return {
|
||||
probeId: `contrast:${semanticKey}`,
|
||||
candidateSetVersion,
|
||||
question,
|
||||
expectedOutcomes: outcomes,
|
||||
candidateSplitHash: semanticKey,
|
||||
informationGain: 0.12,
|
||||
sourceFeatures: [{ technique: layerLabel, calculationResultId }],
|
||||
domain: chosen.layer === "d10" ? "career" : "relationship",
|
||||
year: null,
|
||||
semanticKey,
|
||||
};
|
||||
if (!remaining) return null;
|
||||
return vargaProbeFromRemaining(remaining, candidateSetVersion, calculationResultId);
|
||||
}
|
||||
|
||||
function vargaLayerAsked(asked: ReadonlySet<string>, layer: string): boolean {
|
||||
@@ -354,12 +323,8 @@ function vargaProbeFromRemaining(
|
||||
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} 差异。`;
|
||||
const domain = remainingDomain(split.layer);
|
||||
const question = remainingQuestion(split.layer, layerLabel);
|
||||
return {
|
||||
probeId: `contrast:${semanticKey}`,
|
||||
candidateSetVersion,
|
||||
@@ -368,12 +333,32 @@ function vargaProbeFromRemaining(
|
||||
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",
|
||||
domain,
|
||||
year: null,
|
||||
semanticKey,
|
||||
};
|
||||
}
|
||||
|
||||
function remainingDomain(layer: string): string {
|
||||
if (layer === "d24" || layer === "d5") return "education";
|
||||
if (layer === "d10") return "career";
|
||||
if (layer === "d4") return "relocation";
|
||||
return "relationship";
|
||||
}
|
||||
|
||||
function remainingQuestion(layer: string, layerLabel: string): string {
|
||||
if (layer === "d24" || layer === "d5") {
|
||||
return "当前几个候选在学业盘上还分得开。请核对一段还没用进评分的学业前事:那次高考或重要考试有没有发挥明显失常、压力很大?";
|
||||
}
|
||||
if (layer === "d10") {
|
||||
return "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事:长期更接近照顾或家庭,还是台前带人,还是技术执行或分析?";
|
||||
}
|
||||
if (layer === "d4") {
|
||||
return "当前几个候选在居所盘上还分得开。请核对一段还没用进评分的搬家或离乡:那几年有没有明显搬家、离乡或长期异地?";
|
||||
}
|
||||
return `当前几个候选在关系盘上还分得开。请核对一段还没用进评分的感情前事,用来对照 ${layerLabel} 差异。`;
|
||||
}
|
||||
|
||||
function remainingOutcomes(
|
||||
groups: readonly (readonly string[])[],
|
||||
allMinutes: readonly string[],
|
||||
|
||||
Reference in New Issue
Block a user