Files
Jyotisha/frontend/src/lib/rectification-agentic/core/candidate-contrast-packet.ts
T
Jesse_ChenandCursor 14d199e694
Independent Staging Quality Gate / validate (push) Successful in 12m12s
Independent Staging Quality Gate / publish (push) Successful in 24m21s
fix(rectification): compare health and occupation aliases through one merge (BUG-672)
Skipped or already-reported health must not be asked again as health_pressure, and occupation declined as other must still close the occupation line.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 23:08:55 +08:00

995 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Chart differences that can actually eliminate candidates.
* Window-scan prose in the final report is not a substitute for this packet.
*/
import type { AnswerClass, ConflictProbe } from "./types.ts";
import { signFromTransitions } from "./sign-from-transitions.ts";
import {
D9_TYPE_TABLE,
D10_TYPE_TABLE,
d9StyleLabel,
d10StyleLabel,
signKey,
} from "../v9/varga-type-tables.ts";
import {
completeStyleOptions,
informationGainAmongActive,
isRenderableProbe,
type DroppedProbe,
rankDiscriminatorScore,
askedKeysForSameYearDedup,
sameYearProbeAsked,
SEMANTIC_YEAR_KEY,
} from "../v9/probe-question-contract.ts";
import { probeBelowAdultFloor } from "../v9/adult-floor.ts";
import { canonicalCollectDomain, sameCollectDomain } from "../v9/domain-alias.ts";
export type ContrastChoiceKind = "existence" | "varga_style" | "event_quality";
export type ContrastStyleOption = Readonly<{
label: string;
answerClass: AnswerClass;
sign?: string;
}>;
export type ContrastExpectedOutcome = Readonly<{
outcomeId: string;
supportsCandidateIds: readonly string[];
conflictsCandidateIds: readonly string[];
}>;
export type ContrastSourceFeature = Readonly<{
technique: string;
calculationResultId: string | null;
}>;
export type CandidateDiscriminatorProbe = Readonly<{
probeId: string;
candidateSetVersion: string;
question: string;
authoringHint?: string;
expectedOutcomes: readonly ContrastExpectedOutcome[];
candidateSplitHash: string;
informationGain: number;
sourceFeatures: readonly ContrastSourceFeature[];
domain: string | null;
year: number | null;
semanticKey: string;
choiceKind?: ContrastChoiceKind;
styleOptions?: readonly ContrastStyleOption[];
}>;
export type VargaDifference = Readonly<{
layer: string;
signs: readonly string[];
}>;
export type WindowScanTransition = Readonly<{
layer: string;
at: string;
from_sign?: string;
to_sign?: string;
}>;
export type RemainingVargaSplit = Readonly<{
layer: string;
groups: readonly (readonly string[])[];
signs: readonly (string | null)[];
entropy: number;
}>;
export type CandidateContrastPacket = Readonly<{
candidateSetVersion: string;
probes: readonly CandidateDiscriminatorProbe[];
vargaDifferences: readonly VargaDifference[];
}>;
export type EngineContrastProbe = Readonly<{
semantic_key?: string;
candidate_split_hash?: string;
domain?: string;
year?: number;
question?: string;
user_meaning?: string;
information_gain?: number;
expected_outcomes?: readonly Readonly<{
answer_class?: string;
outcomeId?: string;
supports?: readonly string[];
supportsCandidateIds?: readonly string[];
conflicts?: readonly string[];
conflictsCandidateIds?: readonly string[];
}>[];
left_time?: string;
right_time?: string;
choice_kind?: ContrastChoiceKind;
style_options?: readonly Readonly<{
label?: string;
answer_class?: string;
sign?: string;
}>[];
target_evidence_id?: string;
}>;
const REMAINING_LAYERS = ["d24", "d5", "d10", "d9", "d4", "d7", "d12", "d2", "d11", "d30"] as const;
const VOLUNTEER_LAYER_DOMAIN: Readonly<Record<string, string>> = {
d2: "finance",
d11: "finance",
d30: "health_pressure",
};
const DUTY_ANSWERED_RE = /技术执行|算法|分析|数据处理|系统维护|组织型|第三个|照顾、家庭|台前|带人|公开担责|程序员|前端|工程师|开发/;
const EXAM_QUALITY_RE = /失利|失常|复读|没考好|考砸|发挥不好|发挥失常|发挥异常|压力很大/;
const RELOCATION_RE = /搬家|离乡|迁居|长期异地|离开家/;
const FAMILY_RE = /家人|父母|子女|兄弟|亲戚/;
const FINANCE_RE = /收入|资产|财务|欠债|投资/;
const HEALTH_RE = /健康|住院|手术|事故|持续压力/;
const LIVE_EVIDENCE = new Set(["confirmed", "draft", "pending_confirmation"]);
const ANSWER_CLASSES: ReadonlySet<string> = new Set(["yes", "weak_yes", "no", "unsure"]);
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[],
volunteeredDomains: readonly string[] = [],
): readonly RemainingVargaSplit[] {
if (candidateTimes.length < 2) return [];
const volunteered = new Set(volunteeredDomains);
const rows: RemainingVargaSplit[] = [];
for (const layer of REMAINING_LAYERS) {
const volunteerDomain = VOLUNTEER_LAYER_DOMAIN[layer];
if (volunteerDomain && !volunteered.has(volunteerDomain)) continue;
const groups = remainingLayerGroups(candidateTimes, transitions, layer);
if (groups.length < 2) continue;
rows.push({
layer,
groups,
signs: remainingGroupSigns(groups, transitions, layer),
entropy: groupEntropy(groups),
});
}
return rows.sort((left, right) => (
right.entropy - left.entropy
|| right.groups.length - left.groups.length
|| left.layer.localeCompare(right.layer)
));
}
function groupEntropy(groups: readonly (readonly string[])[]): number {
const sizes = groups.map((group) => group.length);
const total = sizes.reduce((sum, size) => sum + size, 0);
if (total <= 0) return 0;
return -sizes.reduce((sum, size) => (
size > 0 ? sum + (size / total) * Math.log2(size / total) : sum
), 0);
}
function remainingGroupSigns(
groups: readonly (readonly string[])[],
transitions: readonly WindowScanTransition[],
layer: string,
): readonly (string | null)[] {
return groups.map((group) => {
const time = group[0];
if (!time) return null;
return signFromTransitions(transitions, layer, time);
});
}
export function mentionedVargaKeysFromLedgerEvidence(
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 || 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");
const family = item.domain === "family" || FAMILY_RE.test(summary);
if (family) {
keys.add("varga.d7");
keys.add("varga.d12");
}
if (item.domain === "finance" || FINANCE_RE.test(summary)) {
keys.add("varga.d2");
keys.add("varga.d11");
}
if (sameCollectDomain(item.domain, "health_pressure") || HEALTH_RE.test(summary)) keys.add("varga.d30");
}
return [...keys];
}
export function askedEventProbeKeysFromLedgerEvidence(
evidence: readonly Readonly<{
status?: string | null;
domain?: string | null;
occurredFrom?: string | null;
occurredTo?: string | null;
}>[],
): string[] {
const keys = new Set<string>();
for (const item of evidence) {
if (item.status && !LIVE_EVIDENCE.has(item.status)) continue;
if (!item.domain) continue;
for (const date of [item.occurredFrom, item.occurredTo]) {
const year = date?.match(/^(\d{4})/)?.[1];
if (year) keys.add(`${canonicalCollectDomain(item.domain)}.${year}`);
}
}
return [...keys];
}
export function datedDomainsFromEvidence(
evidence: readonly Readonly<{
status?: string | null;
domain?: string | null;
occurredFrom?: string | null;
occurredTo?: string | null;
}>[],
): string[] {
const domains = new Set<string>();
for (const item of evidence) {
if (item.status && !LIVE_EVIDENCE.has(item.status)) continue;
if (!item.domain) continue;
const dated = [item.occurredFrom, item.occurredTo].some((value) => /^\d{4}/.test(value ?? ""));
if (dated) domains.add(canonicalCollectDomain(item.domain));
}
return [...domains];
}
export function volunteeredDomainsFromEvidence(
evidence: readonly Readonly<{
status?: string | null;
domain?: string | null;
}>[],
): string[] {
const domains = new Set<string>();
for (const item of evidence) {
if (item.status && !LIVE_EVIDENCE.has(item.status)) continue;
if (item.domain) domains.add(canonicalCollectDomain(item.domain));
}
return [...domains];
}
export function askedKeysFromLedgerEvidence(
evidence: readonly Readonly<{
status?: string | null;
domain?: string | null;
eventKind?: string | null;
summary?: string | null;
}>[],
): string[] {
return mentionedVargaKeysFromLedgerEvidence(evidence);
}
export function askedKeysFromOccupationEvidence(
evidence: readonly Readonly<{
status?: string | null;
domain?: string | null;
eventKind?: string | null;
summary?: string | null;
}>[],
): string[] {
return mentionedVargaKeysFromLedgerEvidence(evidence);
}
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[];
mentionedKeys?: readonly string[];
volunteeredDomains?: readonly string[];
providedDomains?: readonly string[];
}): CandidateContrastPacket {
const asked = new Set(input.askedKeys ?? []);
// mentionedKeys are ranking-only; passing them here must not skip remaining varga probes.
const provided = new Set((input.providedDomains ?? []).map((item) => canonicalCollectDomain(item)));
const fromEngine = (input.engineProbes ?? []).flatMap((probe) => {
const built = probeFromEngine(probe, input.candidateSetVersion, input.calculationResultId ?? null);
if (!built) return [];
const eventKey = built.domain && built.year
? `${canonicalCollectDomain(built.domain)}.${built.year}`
: null;
const isStructured = isStructuredDiscriminator(built);
if (
asked.has(built.semanticKey)
|| asked.has(built.candidateSplitHash)
|| asked.has(built.probeId)
|| (!isStructured && eventKey && asked.has(eventKey))
|| (!isStructured && !(built.year && built.year > 0) && built.domain && provided.has(canonicalCollectDomain(built.domain)))
) {
return [];
}
return [built];
});
const remainingSplits = input.remainingSplits
?? remainingVargaSplits(
input.candidateTimes ?? [],
input.transitions ?? [],
input.volunteeredDomains ?? [],
);
const vargaDifferences = vargaDifferencesForPacket({
remainingSplits,
windowDifferences: input.vargaDifferences ?? [],
candidateTimes: input.candidateTimes ?? [],
transitions: input.transitions ?? [],
});
const presentKeys = new Set(fromEngine.map((item) => item.semanticKey));
const fromVarga = vargaProbes(
remainingSplits,
input.candidateSetVersion,
input.calculationResultId ?? null,
new Set([...asked, ...presentKeys]),
);
const probes = [...fromEngine, ...fromVarga]
.sort((left, right) => right.informationGain - left.informationGain);
return {
candidateSetVersion: input.candidateSetVersion,
probes,
vargaDifferences,
};
}
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 vargaLayerCovered(keys: ReadonlySet<string>, layer: string): boolean {
if (keys.has(`varga.${layer}`) || keys.has(layer)) return true;
for (const key of keys) {
if (key.startsWith(`varga.${layer}.`)) return true;
}
return false;
}
export function vargaLayerFromSemanticKey(key: string): string | null {
return key.match(/^varga\.(d\d+)/)?.[1] ?? null;
}
function probeDomainYear(probe: Pick<CandidateDiscriminatorProbe, "domain" | "year" | "semanticKey">): {
domain: string;
year: number;
} | null {
if (probe.domain && probe.year && probe.year > 0) {
return { domain: probe.domain, year: probe.year };
}
const match = probe.semanticKey.match(SEMANTIC_YEAR_KEY);
if (!match) return null;
return { domain: match[1], year: Number(match[2]) };
}
export function inspectDiscriminatorProbes(
packet: CandidateContrastPacket | null | undefined,
options?: {
askedKeys?: readonly string[];
mentionedKeys?: readonly string[];
topCandidateTimes?: readonly string[];
birthDate?: string | null;
},
): {
selected: CandidateDiscriminatorProbe | null;
dropped: DroppedProbe[];
} {
const asked = new Set(options?.askedKeys ?? []);
const mentioned = new Set(options?.mentionedKeys ?? []);
const dropped: DroppedProbe[] = [];
const ranked = (packet?.probes ?? []).flatMap((probe) => {
const completed = withCompletedContrastOptions(probe);
if (!completed.ok) {
dropped.push({
semantic_key: probe.semanticKey,
information_gain: probe.informationGain,
reason: completed.reason,
});
return [];
}
const ids = [...new Set(completed.probe.expectedOutcomes.flatMap((row) => [
...row.supportsCandidateIds,
...row.conflictsCandidateIds,
]))];
const renderable = isRenderableProbe({
informationGain: completed.probe.informationGain,
candidateIds: ids,
expectedOutcomeCount: completed.probe.expectedOutcomes.length,
choiceKind: completed.probe.choiceKind,
styleOptions: completed.probe.styleOptions,
year: completed.probe.year,
});
if (!renderable.ok) {
dropped.push({
semantic_key: completed.probe.semanticKey,
information_gain: completed.probe.informationGain,
reason: renderable.reason,
});
return [];
}
const top = options?.topCandidateTimes ?? [];
const activeGain = top.length >= 2
? informationGainAmongActive(completed.probe.expectedOutcomes, top)
: { splits: true, informationGain: completed.probe.informationGain };
if (!activeGain.splits) {
dropped.push({
semantic_key: completed.probe.semanticKey,
information_gain: completed.probe.informationGain,
reason: "no_split_among_active",
});
return [];
}
const domainYear = probeDomainYear(completed.probe);
const yearAsked = askedKeysForSameYearDedup(asked, completed.probe);
if (domainYear && sameYearProbeAsked(yearAsked, domainYear.domain, domainYear.year)) {
dropped.push({
semantic_key: completed.probe.semanticKey,
information_gain: completed.probe.informationGain,
reason: "same_year_asked",
});
return [];
}
if (domainYear && probeBelowAdultFloor(domainYear, options?.birthDate)) {
dropped.push({
semantic_key: completed.probe.semanticKey,
information_gain: completed.probe.informationGain,
reason: "below_adult_floor",
});
return [];
}
const layer = vargaLayerFromSemanticKey(completed.probe.semanticKey);
const askedAlready = asked.has(completed.probe.semanticKey)
|| asked.has(completed.probe.candidateSplitHash)
|| asked.has(completed.probe.probeId)
|| (layer ? vargaLayerCovered(mentioned, layer) : false);
return [{
probe: completed.probe,
score: rankDiscriminatorScore({
informationGain: activeGain.informationGain,
asked: askedAlready,
candidateIds: ids,
topCandidateTimes: options?.topCandidateTimes,
}),
}];
}).sort((left, right) => right.score - left.score || right.probe.informationGain - left.probe.informationGain);
return { selected: ranked[0]?.probe ?? null, dropped };
}
export function selectDiscriminatorProbe(
packet: CandidateContrastPacket | null | undefined,
options?: {
askedKeys?: readonly string[];
mentionedKeys?: readonly string[];
topCandidateTimes?: readonly string[];
birthDate?: string | null;
},
): CandidateDiscriminatorProbe | null {
return inspectDiscriminatorProbes(packet, options).selected;
}
export function withCompletedContrastOptions(
probe: CandidateDiscriminatorProbe,
): { ok: true; probe: CandidateDiscriminatorProbe } | { ok: false; reason: DroppedProbe["reason"] } {
const mapped = probe.styleOptions?.map((item) => ({
label: item.label,
answer_class: item.answerClass,
...(item.sign ? { sign: item.sign } : {}),
})) ?? [];
const incoming = mapped.length >= 2 ? mapped : [...mapped, ...inferredVargaStyleIncoming(probe)];
const choiceKind = effectiveContrastChoiceKind({
...probe,
styleOptions: incoming.map((item) => ({
label: item.label,
answerClass: item.answer_class as AnswerClass,
...(item.sign ? { sign: item.sign } : {}),
})),
});
const styleOptions = completeStyleOptions({
choiceKind,
styleOptions: incoming,
});
if (!styleOptions.ok) return { ok: false, reason: styleOptions.reason };
const outcomes = withUnsureOutcome(probe.expectedOutcomes);
return {
ok: true,
probe: {
...probe,
choiceKind,
expectedOutcomes: outcomes,
styleOptions: styleOptions.options.map((item) => ({
label: item.label,
answerClass: item.answer_class,
...(item.sign ? { sign: item.sign } : {}),
})),
},
};
}
function inferredVargaStyleIncoming(
probe: CandidateDiscriminatorProbe,
): Array<{ label: string; answer_class: AnswerClass; sign?: string }> {
const parsed = signsFromVargaProbe(probe);
if (!parsed) return [];
const labelFor = parsed.layer === "d9" ? d9StyleLabel : d10StyleLabel;
const classes = ["yes", "weak_yes", "no"] as const;
return parsed.signs.slice(0, 3).flatMap((sign, index) => {
const label = labelFor(sign);
const answerClass = classes[index];
if (!label || !answerClass) return [];
return [{ label, answer_class: answerClass, sign }];
});
}
function knownVargaSigns(layer: "d9" | "d10", tokens: readonly string[]): string[] {
const table = layer === "d9" ? D9_TYPE_TABLE : D10_TYPE_TABLE;
return tokens.flatMap((token) => {
const key = signKey(token);
return table[key] ? [key] : [];
});
}
function signsFromVargaProbe(
probe: CandidateDiscriminatorProbe,
): { layer: "d9" | "d10"; signs: string[] } | null {
const match = probe.semanticKey.match(/^varga\.(d9|d10)\.(.+)$/);
const layer = match?.[1] === "d9" || match?.[1] === "d10" ? match[1] : null;
if (!layer) return null;
const fromKey = knownVargaSigns(
layer,
match?.[2]
?.split(/[|/]/)
.map((item) => item.trim())
.filter((item) => item && !/^\d{1,2}:\d{2}$/.test(item))
?? [],
);
const fromOutcomes = knownVargaSigns(
layer,
probe.expectedOutcomes.flatMap((row) => {
const token = row.outcomeId.replace(/^supports_/, "").trim();
return token && !ANSWER_CLASSES.has(token) && !/^\d{1,2}:\d{2}$/.test(token) ? [token] : [];
}),
);
const signs = (fromKey.length >= 2 ? fromKey : fromOutcomes).slice(0, 3);
if (signs.length < 2) return null;
return { layer, signs };
}
function effectiveContrastChoiceKind(probe: CandidateDiscriminatorProbe): ContrastChoiceKind {
const key = probe.semanticKey;
if (probe.choiceKind === "varga_style" && (probe.styleOptions?.length ?? 0) < 2) {
if (key.startsWith("varga.d24") || key.startsWith("varga.d5")) return "event_quality";
return "existence";
}
if (probe.choiceKind === "varga_style" || probe.choiceKind === "event_quality" || probe.choiceKind === "existence") {
return probe.choiceKind;
}
if (key.startsWith("varga.d24") || key.startsWith("varga.d5")) return "event_quality";
if (key.startsWith("varga.d9") || key.startsWith("varga.d10")) return "varga_style";
return "existence";
}
function withUnsureOutcome(
outcomes: readonly ContrastExpectedOutcome[],
): readonly ContrastExpectedOutcome[] {
const rows = [...outcomes];
if (!rows.some((row) => row.outcomeId === "unsure")) {
rows.push({ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] });
}
if (!rows.some((row) => row.outcomeId === "no") && rows.some((row) => row.outcomeId === "weak_yes")) {
rows.push({ outcomeId: "no", supportsCandidateIds: [], conflictsCandidateIds: [] });
}
return rows;
}
function conflictStyleOptions(
probe: CandidateDiscriminatorProbe,
): ConflictProbe["style_options"] {
const rows = probe.styleOptions?.flatMap((item) => {
const label = item.label.trim();
if (!label) return [];
return [{
label,
answer_class: item.answerClass,
...(item.sign ? { sign: item.sign } : {}),
}];
}) ?? [];
return rows.length > 0 ? rows : undefined;
}
export function conflictProbesFromContrast(
packet: CandidateContrastPacket | null | undefined,
): ConflictProbe[] {
return (packet?.probes ?? []).flatMap((probe) => {
if (!probe.semanticKey.startsWith("varga.")) return [];
const completed = withCompletedContrastOptions(probe);
const working = completed.ok ? completed.probe : probe;
const outcomes = working.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 [];
if (working.informationGain <= 0) return [];
const candidateIds = [...new Set(working.expectedOutcomes.flatMap((row) => [
...row.supportsCandidateIds,
...row.conflictsCandidateIds,
]))];
if (candidateIds.length < 2) return [];
const choiceKind = completed.ok
? (working.choiceKind ?? effectiveContrastChoiceKind(working))
: effectiveContrastChoiceKind(probe);
const styleOptions = conflictStyleOptions(working);
return [{
id: working.probeId,
semantic_key: working.semanticKey,
candidate_split_hash: working.candidateSplitHash,
domain: working.domain ?? "career",
year: working.year ?? 0,
question: working.question,
candidate_ids: candidateIds,
expected_outcomes: outcomes,
information_gain: working.informationGain,
source: "varga_contrast",
...(choiceKind === "varga_style"
|| choiceKind === "event_quality"
|| choiceKind === "existence"
? { choice_kind: choiceKind }
: {}),
...(styleOptions ? { style_options: styleOptions } : {}),
}];
});
}
function inferredContrastChoiceKind(
semanticKey: string,
explicit?: ContrastChoiceKind,
): ContrastChoiceKind {
if (explicit === "varga_style" || explicit === "event_quality" || explicit === "existence") {
return explicit;
}
const layer = semanticKey.match(/^varga\.(d\d+)/)?.[1];
if (layer === "d9" || layer === "d10") return "varga_style";
if (layer === "d24" || layer === "d5") return "event_quality";
return "existence";
}
/**
* Split hashes are `<candidate set id>:<semantic key>` for varga contrasts and
* `<candidate set id>:<opaque engine hash>` for dated event probes. A stored
* probe can carry the set id of an earlier scoring round; re-prefixing such a
* hash would stack prefixes and make the persisted focus schema unmatchable, so
* a stale set prefix is re-minted against the current set instead.
*/
export function splitHashForCandidateSet(
rawHash: string | null | undefined,
semanticKey: string,
candidateSetVersion: string,
): string {
const scoped = `${candidateSetVersion}:${semanticKey}`;
const hash = rawHash?.trim() ?? "";
if (!hash) return scoped;
if (hash === scoped) return hash;
if (hash.endsWith(`:${semanticKey}`)) return scoped;
if (hash.startsWith(`${candidateSetVersion}:`)) return hash;
return `${candidateSetVersion}:${hash}`;
}
/**
* Two hashes describe the same split when they are equal, or when both are
* `<some candidate set id>:<the same semantic key>`. The semantic key of a
* varga contrast already encodes its candidate groups, so only the set prefix
* can differ, and a stale prefix must not hide an otherwise valid card.
*/
export function isSameCandidateSplit(
left: string | null | undefined,
right: string | null | undefined,
semanticKey: string | null | undefined,
): boolean {
const a = left?.trim() ?? "";
const b = right?.trim() ?? "";
if (a === b) return true;
const key = semanticKey?.trim() ?? "";
if (!key || !a || !b) return false;
return a.endsWith(`:${key}`) && b.endsWith(`:${key}`);
}
function probeFromEngine(
probe: EngineContrastProbe,
candidateSetVersion: string,
calculationResultId: string | null,
): CandidateDiscriminatorProbe | null {
const outcomes = (probe.expected_outcomes ?? []).flatMap((row) => {
const outcomeId = typeof row.answer_class === "string" && row.answer_class.trim()
? row.answer_class
: typeof row.outcomeId === "string" ? row.outcomeId : "";
const supports = row.supports ?? row.supportsCandidateIds ?? [];
const conflicts = row.conflicts ?? row.conflictsCandidateIds ?? [];
if (!outcomeId) return [];
return [{ outcomeId, supportsCandidateIds: supports, conflictsCandidateIds: conflicts }];
});
if (outcomes.length < 2) return null;
if ((probe.information_gain ?? 0) <= 0) return null;
const semanticKey = probe.semantic_key ?? `${probe.domain ?? "career"}.${probe.year ?? "contrast"}`;
const split = splitHashForCandidateSet(
probe.candidate_split_hash,
semanticKey,
candidateSetVersion,
);
const question = probe.question ?? probe.user_meaning ?? "";
if (!question.trim()) return null;
const candidateIds = [...new Set(outcomes.flatMap((row) => [
...row.supportsCandidateIds,
...row.conflictsCandidateIds,
]))];
if (candidateIds.length < 2) return null;
return {
probeId: `contrast:${semanticKey}:${split}`,
candidateSetVersion,
question,
expectedOutcomes: outcomes,
candidateSplitHash: split,
informationGain: probe.information_gain ?? 0,
sourceFeatures: [{ technique: probe.domain ?? "event_probe", calculationResultId }],
domain: probe.domain ?? null,
year: probe.year ?? null,
semanticKey,
choiceKind: inferredContrastChoiceKind(semanticKey, probe.choice_kind),
styleOptions: styleOptionsFromEngine(probe.style_options),
};
}
function styleOptionsFromEngine(
rows: EngineContrastProbe["style_options"],
): readonly ContrastStyleOption[] | undefined {
if (!rows?.length) return undefined;
const parsed = rows.flatMap((row) => {
const label = typeof row.label === "string" ? row.label.trim() : "";
const answerClass = typeof row.answer_class === "string" && ANSWER_CLASSES.has(row.answer_class)
? row.answer_class as AnswerClass
: null;
if (!label || !answerClass) return [];
return [{
label,
answerClass,
...(typeof row.sign === "string" && row.sign.trim() ? { sign: row.sign.trim() } : {}),
}];
});
return parsed.length > 0 ? parsed : undefined;
}
export function isStructuredDiscriminator(probe: Pick<CandidateDiscriminatorProbe, "choiceKind" | "semanticKey">): boolean {
return probe.choiceKind === "varga_style"
|| probe.choiceKind === "event_quality"
|| probe.semanticKey.startsWith("varga.");
}
const CLOCK_TOKEN = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
export function vargaSignPartitionKey(
layer: string,
signs: readonly (string | null | undefined)[],
): string {
const parts = signs.flatMap((item) => {
const raw = typeof item === "string" ? item.trim() : "";
if (!raw || CLOCK_TOKEN.test(raw)) return [];
return [signKey(raw)];
});
if (parts.length >= 2) return `varga.${layer}.${parts.join("|")}`;
return `varga.${layer}.unsigned`;
}
function vargaProbes(
remainingSplits: readonly RemainingVargaSplit[],
candidateSetVersion: string,
calculationResultId: string | null,
asked: ReadonlySet<string>,
): CandidateDiscriminatorProbe[] {
return remainingSplits.flatMap((item) => {
if (vargaLayerAsked(asked, item.layer)) return [];
const probe = vargaProbeFromRemaining(item, candidateSetVersion, calculationResultId);
return probe ? [probe] : [];
});
}
function vargaLayerAsked(asked: ReadonlySet<string>, layer: string): boolean {
return vargaLayerCovered(asked, layer);
}
function vargaProbeFromRemaining(
split: RemainingVargaSplit,
candidateSetVersion: string,
calculationResultId: string | null,
): CandidateDiscriminatorProbe | null {
if (split.entropy <= 0) return null;
const allMinutes = split.groups.flat();
let choiceKind = remainingChoiceKind(split.layer);
let styleOptions = remainingStyleOptions(split, choiceKind);
if (choiceKind === "varga_style" && (styleOptions?.length ?? 0) < 2) {
choiceKind = "existence";
styleOptions = remainingStyleOptions(split, choiceKind);
}
const outcomes = remainingOutcomes(split.groups, allMinutes, choiceKind);
const ids = new Set(outcomes.flatMap((row) => [...row.supportsCandidateIds, ...row.conflictsCandidateIds]));
if (outcomes.length < 2 || ids.size < 2) return null;
const semanticKey = vargaSignPartitionKey(split.layer, split.signs);
const layerLabel = split.layer.toUpperCase();
const domain = remainingDomain(split.layer);
return {
probeId: `contrast:${semanticKey}`,
candidateSetVersion,
question: remainingQuestion(split.layer),
authoringHint: remainingAuthoringHint(layerLabel),
expectedOutcomes: outcomes,
candidateSplitHash: `${candidateSetVersion}:${semanticKey}`,
informationGain: split.entropy,
sourceFeatures: [{ technique: layerLabel, calculationResultId }],
domain,
year: null,
semanticKey,
choiceKind,
...(styleOptions ? { styleOptions } : {}),
};
}
function remainingChoiceKind(layer: string): ContrastChoiceKind {
if (layer === "d10" || layer === "d9") return "varga_style";
if (layer === "d24" || layer === "d5") return "event_quality";
return "existence";
}
function remainingStyleOptions(
split: RemainingVargaSplit,
kind: ContrastChoiceKind,
): readonly ContrastStyleOption[] | undefined {
const incoming = kind === "varga_style"
? split.groups.slice(0, 3).flatMap((group, index) => {
const sign = split.signs[index];
if (!sign) return [];
const label = split.layer === "d10" ? d10StyleLabel(sign) : d9StyleLabel(sign);
const classes = ["yes", "weak_yes", "no"] as const;
return [{ label, answer_class: classes[index] ?? "unsure", sign }];
})
: [];
const completed = completeStyleOptions({ choiceKind: kind, styleOptions: incoming });
if (!completed.ok) return undefined;
return uniquifyStyleLabels(completed.options.map((item) => ({
label: item.label,
answerClass: item.answer_class,
...(item.sign ? { sign: item.sign } : {}),
})));
}
function uniquifyStyleLabels(
options: readonly ContrastStyleOption[],
): readonly ContrastStyleOption[] {
const seen = new Set<string>();
return options.map((option) => {
let label = option.label;
if (seen.has(label) && option.sign) label = `${label}(${option.sign})`;
seen.add(label);
return { ...option, label };
});
}
function remainingDomain(layer: string): string {
if (layer === "d24" || layer === "d5") return "education";
if (layer === "d10") return "career";
if (layer === "d4") return "relocation";
if (layer === "d7" || layer === "d12") return "family";
if (layer === "d2" || layer === "d11") return "finance";
if (layer === "d30") return "health_pressure";
return "relationship";
}
function remainingQuestion(layer: string): string {
if (layer === "d10") return "平时做事,你更接近下面哪一种?";
if (layer === "d9") return "亲密关系里,你更接近哪一种相处方式?";
if (layer === "d24" || layer === "d5") return "有没有学业或考试发挥明显失常、压力特别大的时候?";
if (layer === "d4") return "有没有搬家或长期住到外地?";
if (layer === "d7" || layer === "d12") return "家里有没有结婚、添丁或住院这类事?";
if (layer === "d2" || layer === "d11") return "有没有收入明显变化、大笔支出或欠债?";
if (layer === "d30") return "有没有生病、受伤或压力特别大的时候?";
return "有没有下面这类事发生过?";
}
function remainingAuthoringHint(layerLabel: string): string {
return `引擎给出的区分机会绑定 ${layerLabel}。按 Opportunity 的时间范围、领域和 expected_outcomes 改写成自然语言,不得发明年份、事件事实或候选映射,不得改写时间范围。`;
}
function remainingOutcomes(
groups: readonly (readonly string[])[],
allMinutes: readonly string[],
kind: ContrastChoiceKind,
): ContrastExpectedOutcome[] {
let rows: ContrastExpectedOutcome[];
if (kind === "varga_style" && groups.length === 2) {
rows = [
{ outcomeId: "yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
{ outcomeId: "weak_yes", supportsCandidateIds: groups[1], conflictsCandidateIds: groups[0] },
{ outcomeId: "no", supportsCandidateIds: [], conflictsCandidateIds: [] },
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
];
} else if (groups.length === 2) {
rows = [
{ 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] },
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
];
} else {
const classes = ["yes", "weak_yes", "no"] as const;
rows = groups.slice(0, 3).map((group, index) => ({
outcomeId: classes[index] ?? `group_${index}`,
supportsCandidateIds: group,
conflictsCandidateIds: allMinutes.filter((time) => !group.includes(time)),
}));
if (!rows.some((row) => row.outcomeId === "unsure")) {
rows.push({ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] });
}
}
return rows;
}