Files
Jyotisha/frontend/src/lib/rectification-agentic/core/candidate-contrast-packet.ts
T
Jesse_ChenandCursor ca6252ecb6
Independent Staging Quality Gate / validate (push) Successful in 9m30s
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(rectification): ask from the scored probe catalog, not snapshot leftovers
Empty snapshot candidates were starving remaining D24 splits, so the
TypeScript follow-up chain asked the low-gain Python career probe.
Read paths now share one inference+engine catalog and yield a stale
low-gain distinguish card to the current winner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 09:48:57 +08:00

739 lines
26 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 { d9StyleLabel, d10StyleLabel } from "../v9/varga-type-tables.ts";
import {
completeStyleOptions,
isRenderableProbe,
rankDiscriminatorScore,
} from "../v9/probe-question-contract.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;
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;
}>[];
}>;
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)[] {
const changes = transitions
.filter((item) => item.layer === layer)
.filter((item) => /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(item.at))
.sort((left, right) => clockMinutes(left.at) - clockMinutes(right.at));
return groups.map((group) => {
const time = group[0];
if (!time) return null;
const crossed = changes.filter((item) => clockMinutes(item.at) <= clockMinutes(time));
if (crossed.length === 0) return changes[0]?.from_sign ?? null;
return crossed[crossed.length - 1]?.to_sign ?? changes[0]?.from_sign ?? null;
});
}
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 || 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 (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(`${item.domain}.${year}`);
}
}
return [...keys];
}
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(item.domain);
}
return [...domains];
}
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: {
candidateSetVersion: string;
calculationResultId?: string | null;
engineProbes?: readonly EngineContrastProbe[];
vargaDifferences?: readonly VargaDifference[];
remainingSplits?: readonly RemainingVargaSplit[];
candidateTimes?: readonly string[];
transitions?: readonly WindowScanTransition[];
askedKeys?: readonly string[];
volunteeredDomains?: readonly string[];
}): CandidateContrastPacket {
const asked = new Set(input.askedKeys ?? []);
const fromEngine = (input.engineProbes ?? []).flatMap((probe) => {
const built = probeFromEngine(probe, input.candidateSetVersion, input.calculationResultId ?? null);
if (!built) return [];
const eventKey = built.domain && built.year ? `${built.domain}.${built.year}` : null;
const isStructured = built.choiceKind === "varga_style" || built.semanticKey.startsWith("varga.");
if (
asked.has(built.semanticKey)
|| asked.has(built.candidateSplitHash)
|| asked.has(built.probeId)
|| (!isStructured && eventKey && asked.has(eventKey))
) {
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 = vargaProbe(
remainingSplits,
input.candidateSetVersion,
input.calculationResultId ?? null,
new Set([...asked, ...presentKeys]),
);
const probes = [...fromEngine, ...(fromVarga ? [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 selectDiscriminatorProbe(
packet: CandidateContrastPacket | null | undefined,
options?: { askedKeys?: readonly string[]; topCandidateTimes?: readonly string[] },
): CandidateDiscriminatorProbe | null {
const asked = new Set(options?.askedKeys ?? []);
const ranked = (packet?.probes ?? []).flatMap((probe) => {
const completed = withCompletedContrastOptions(probe);
if (!completed) return [];
const ids = [...new Set(completed.expectedOutcomes.flatMap((row) => [
...row.supportsCandidateIds,
...row.conflictsCandidateIds,
]))];
if (!isRenderableProbe({
informationGain: completed.informationGain,
candidateIds: ids,
expectedOutcomeCount: completed.expectedOutcomes.length,
choiceKind: completed.choiceKind,
styleOptions: completed.styleOptions,
})) return [];
const askedAlready = asked.has(completed.semanticKey)
|| asked.has(completed.candidateSplitHash)
|| asked.has(completed.probeId);
return [{
probe: completed,
score: rankDiscriminatorScore({
informationGain: completed.informationGain,
asked: askedAlready,
candidateIds: ids,
topCandidateTimes: options?.topCandidateTimes,
}),
}];
}).sort((left, right) => right.score - left.score || right.probe.informationGain - left.probe.informationGain);
return ranked[0]?.probe ?? null;
}
function withCompletedContrastOptions(
probe: CandidateDiscriminatorProbe,
): CandidateDiscriminatorProbe | null {
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) return null;
const outcomes = withUnsureOutcome(probe.expectedOutcomes);
return {
...probe,
choiceKind,
expectedOutcomes: outcomes,
styleOptions: styleOptions.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 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;
const fromKey = match?.[2]
?.split(/[|/]/)
.map((item) => item.trim())
.filter((item) => item && !/^\d{1,2}:\d{2}$/.test(item))
?? [];
const fromOutcomes = probe.expectedOutcomes.flatMap((row) => {
const token = row.outcomeId.replace(/^supports_/, "").trim();
return token && !/^\d{1,2}:\d{2}$/.test(token) ? [token] : [];
});
const signs = (fromKey.length >= 2 ? fromKey : fromOutcomes).slice(0, 3);
if (!layer || 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";
if (key.startsWith("varga.d9") || key.startsWith("varga.d10")) return "varga_style";
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;
}
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 [];
if (probe.informationGain <= 0) return [];
const candidateIds = [...new Set(probe.expectedOutcomes.flatMap((row) => [
...row.supportsCandidateIds,
...row.conflictsCandidateIds,
]))];
if (candidateIds.length < 2) return [];
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 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";
}
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 = probe.candidate_split_hash
? (probe.candidate_split_hash.includes(candidateSetVersion)
? probe.candidate_split_hash
: `${candidateSetVersion}:${probe.candidate_split_hash}`)
: `${candidateSetVersion}:${semanticKey}`;
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;
}
function vargaProbe(
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 null;
return vargaProbeFromRemaining(remaining, candidateSetVersion, calculationResultId);
}
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 | null {
if (split.entropy <= 0) return null;
const allMinutes = split.groups.flat();
const choiceKind = remainingChoiceKind(split.layer);
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 = `varga.${split.layer}.${split.groups.map((group) => group.join("|")).join("/")}`;
const layerLabel = split.layer.toUpperCase();
const domain = remainingDomain(split.layer);
const styleOptions = remainingStyleOptions(split, choiceKind);
return {
probeId: `contrast:${semanticKey}`,
candidateSetVersion,
question: remainingQuestion(split.layer, 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) return undefined;
return uniquifyStyleLabels(completed.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,
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;
}