Files
Jyotisha/frontend/src/lib/rectification-agentic/core/candidate-contrast-packet.ts
T
Jesse_ChenandCursor 7718317d69
Independent Staging Quality Gate / validate (push) Successful in 11m41s
Independent Staging Quality Gate / publish (push) Successful in 15m44s
fix(web): keep career quality off the gate and let the agent write stems
Exam-quality cards may still jump ahead of adoption, but career years stay on method rotation. Server stamps only period and family; spoken questions remain model-authored.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 12:39:05 +08:00

565 lines
20 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";
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;
supports?: readonly string[];
conflicts?: 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 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 [];
if (asked.has(built.semanticKey) || asked.has(built.candidateSplitHash) || asked.has(built.probeId)) {
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 fromVarga = vargaProbe(
remainingSplits,
input.candidateSetVersion,
input.calculationResultId ?? null,
asked,
);
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,
): CandidateDiscriminatorProbe | null {
const ranked = (packet?.probes ?? []).filter((probe) => probe.expectedOutcomes.length >= 2);
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,
calculationResultId: string | null,
): CandidateDiscriminatorProbe | null {
const outcomes = (probe.expected_outcomes ?? []).flatMap((row) => {
const outcomeId = typeof row.answer_class === "string" ? row.answer_class : "";
const supports = row.supports ?? [];
const conflicts = row.conflicts ?? [];
if (!outcomeId) return [];
return [{ outcomeId, supportsCandidateIds: supports, conflictsCandidateIds: conflicts }];
});
if (outcomes.length < 2 && probe.left_time && probe.right_time && probe.left_time !== probe.right_time) {
outcomes.push(
{ outcomeId: "yes", supportsCandidateIds: [probe.left_time], conflictsCandidateIds: [probe.right_time] },
{ outcomeId: "no", supportsCandidateIds: [probe.right_time], conflictsCandidateIds: [probe.left_time] },
);
}
if (outcomes.length < 2) return null;
const semanticKey = probe.semantic_key ?? `${probe.domain ?? "career"}.${probe.year ?? "contrast"}`;
const split = probe.candidate_split_hash ?? semanticKey;
const question = probe.question ?? probe.user_meaning ?? "";
if (!question.trim()) 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: 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 {
const allMinutes = split.groups.flat();
const choiceKind = remainingChoiceKind(split.layer);
const outcomes = remainingOutcomes(split.groups, allMinutes, choiceKind);
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);
const question = remainingQuestion(split.layer, layerLabel, styleOptions);
return {
probeId: `contrast:${semanticKey}`,
candidateSetVersion,
question,
expectedOutcomes: outcomes,
candidateSplitHash: 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 {
if (kind !== "varga_style") return undefined;
const classes = ["yes", "weak_yes", "no"] as const;
const options = 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);
return [{ label, answerClass: classes[index] ?? "unsure", sign }];
});
return options.length >= 2 ? uniquifyStyleLabels(options) : undefined;
}
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,
styleOptions?: readonly ContrastStyleOption[],
): string {
if (layer === "d24" || layer === "d5") {
return "当前几个候选在学业盘上还分得开。请核对一段还没用进评分的学业前事。";
}
if (layer === "d10") {
return styleOptions?.length
? "当前几个候选在事业盘上还分得开。长期工作更接近哪一类?"
: "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事:长期更接近照顾或家庭,还是台前带人,还是技术执行或分析?";
}
if (layer === "d9") {
return "当前几个候选在关系盘上还分得开。这段关系更接近哪一种相处?";
}
if (layer === "d4") {
return "当前几个候选在居所盘上还分得开。请核对一段还没用进评分的搬家或离乡:那几年有没有明显搬家、离乡或长期异地?";
}
if (layer === "d7" || layer === "d12") {
return "当前几个候选在家人盘上还分得开。那几年有没有家人相关的明显变化?";
}
if (layer === "d2" || layer === "d11") {
return "当前几个候选在财帛盘上还分得开。那几年有没有收入、资产或财务明显变化?";
}
if (layer === "d30") {
return "当前几个候选在健康盘上还分得开。那几年有没有健康、事故或持续压力明显变化?";
}
return `当前几个候选在关系盘上还分得开。请核对一段还没用进评分的感情前事,用来对照 ${layerLabel} 差异。`;
}
function remainingOutcomes(
groups: readonly (readonly string[])[],
allMinutes: readonly string[],
kind: ContrastChoiceKind,
): ContrastExpectedOutcome[] {
if (kind === "varga_style" && groups.length === 2) {
return [
{ outcomeId: "yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
{ outcomeId: "weak_yes", supportsCandidateIds: groups[1], conflictsCandidateIds: groups[0] },
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
];
}
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)),
}));
}