fix(web): rank remaining-minute probes by split and match choice kind
Choice cards used a hardcoded domain menu and always asked existence. Rank scoring layers by remaining-minute entropy, keep finance and health volunteer-only, and ask D9/D10 style or exam quality so taps match outcomes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -251,13 +251,27 @@ export function answersFromEvidence(
|
||||
});
|
||||
}
|
||||
|
||||
export function classifyChoiceAnswer(key: string): AnswerClass {
|
||||
export function classifyChoiceAnswer(key: string, schema?: unknown): AnswerClass {
|
||||
if (key === "A") return "yes";
|
||||
if (key === "B") return "weak_yes";
|
||||
if (key === "C") return "no";
|
||||
if (key === "C") {
|
||||
if (schemaMapsCToUnsure(schema)) return "unsure";
|
||||
return "no";
|
||||
}
|
||||
return "unsure";
|
||||
}
|
||||
|
||||
function schemaMapsCToUnsure(schema: unknown): boolean {
|
||||
if (!schema || typeof schema !== "object") return false;
|
||||
const row = schema as Record<string, unknown>;
|
||||
if (row.choice_kind !== "varga_style") return false;
|
||||
const choice = row.choice && typeof row.choice === "object" && !Array.isArray(row.choice)
|
||||
? row.choice as Record<string, unknown>
|
||||
: row;
|
||||
const optionC = typeof choice.option_c === "string" ? choice.option_c : "";
|
||||
return optionC.includes("都不像");
|
||||
}
|
||||
|
||||
function rebuildWithAnswers(state: InferenceState, incoming: readonly ProbeAnswer[]): InferenceState {
|
||||
return buildInferenceState({
|
||||
range_start: state.range_start,
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
*/
|
||||
|
||||
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;
|
||||
@@ -27,6 +36,8 @@ export type CandidateDiscriminatorProbe = Readonly<{
|
||||
domain: string | null;
|
||||
year: number | null;
|
||||
semanticKey: string;
|
||||
choiceKind?: ContrastChoiceKind;
|
||||
styleOptions?: readonly ContrastStyleOption[];
|
||||
}>;
|
||||
|
||||
export type VargaDifference = Readonly<{
|
||||
@@ -37,11 +48,15 @@ export type VargaDifference = Readonly<{
|
||||
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<{
|
||||
@@ -65,12 +80,26 @@ export type EngineContrastProbe = Readonly<{
|
||||
}>[];
|
||||
left_time?: string;
|
||||
right_time?: string;
|
||||
choice_kind?: ContrastChoiceKind;
|
||||
style_options?: readonly Readonly<{
|
||||
label?: string;
|
||||
answer_class?: string;
|
||||
sign?: string;
|
||||
}>[];
|
||||
}>;
|
||||
|
||||
const REMAINING_LAYER_ORDER = ["d24", "d5", "d10", "d9", "d4"] as const;
|
||||
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"]);
|
||||
|
||||
@@ -109,15 +138,55 @@ export function remainingLayerGroups(
|
||||
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_LAYER_ORDER) {
|
||||
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 });
|
||||
rows.push({
|
||||
layer,
|
||||
groups,
|
||||
signs: remainingGroupSigns(groups, transitions, layer),
|
||||
entropy: groupEntropy(groups),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
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(
|
||||
@@ -140,10 +209,34 @@ export function askedKeysFromLedgerEvidence(
|
||||
}
|
||||
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;
|
||||
@@ -164,6 +257,7 @@ export function buildCandidateContrastPacket(input: {
|
||||
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) => {
|
||||
@@ -175,7 +269,11 @@ export function buildCandidateContrastPacket(input: {
|
||||
return [built];
|
||||
});
|
||||
const remainingSplits = input.remainingSplits
|
||||
?? remainingVargaSplits(input.candidateTimes ?? [], input.transitions ?? []);
|
||||
?? remainingVargaSplits(
|
||||
input.candidateTimes ?? [],
|
||||
input.transitions ?? [],
|
||||
input.volunteeredDomains ?? [],
|
||||
);
|
||||
const vargaDifferences = vargaDifferencesForPacket({
|
||||
remainingSplits,
|
||||
windowDifferences: input.vargaDifferences ?? [],
|
||||
@@ -292,9 +390,30 @@ function probeFromEngine(
|
||||
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,
|
||||
@@ -320,49 +439,115 @@ function vargaProbeFromRemaining(
|
||||
calculationResultId: string | null,
|
||||
): CandidateDiscriminatorProbe {
|
||||
const allMinutes = split.groups.flat();
|
||||
const outcomes = remainingOutcomes(split.groups, allMinutes);
|
||||
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 question = remainingQuestion(split.layer, layerLabel);
|
||||
const styleOptions = remainingStyleOptions(split, choiceKind);
|
||||
const question = remainingQuestion(split.layer, layerLabel, styleOptions);
|
||||
return {
|
||||
probeId: `contrast:${semanticKey}`,
|
||||
candidateSetVersion,
|
||||
question,
|
||||
expectedOutcomes: outcomes,
|
||||
candidateSplitHash: semanticKey,
|
||||
informationGain: split.layer === "d24" || split.layer === "d5" ? 0.16 : 0.12,
|
||||
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): string {
|
||||
function remainingQuestion(
|
||||
layer: string,
|
||||
layerLabel: string,
|
||||
styleOptions?: readonly ContrastStyleOption[],
|
||||
): string {
|
||||
if (layer === "d24" || layer === "d5") {
|
||||
return "当前几个候选在学业盘上还分得开。请核对一段还没用进评分的学业前事:那次高考或重要考试有没有发挥明显失常、压力很大?";
|
||||
}
|
||||
if (layer === "d10") {
|
||||
return "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事:长期更接近照顾或家庭,还是台前带人,还是技术执行或分析?";
|
||||
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] },
|
||||
|
||||
@@ -119,7 +119,7 @@ export async function applyRectificationChoice(
|
||||
optionId,
|
||||
scoring,
|
||||
appliedInference: false,
|
||||
answerClass: outcomeIdForOption(optionId),
|
||||
answerClass: outcomeIdForOption(optionId, schema),
|
||||
sourceQuote: optionQuoteFromSchema(schema, optionId),
|
||||
year: null,
|
||||
expectedRevision: command.expectedRevision,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { AnswerClass } from "../core/types";
|
||||
import { classifyChoiceAnswer } from "../core/build-state";
|
||||
import type { ChoiceKey, RectificationChoiceCard } from "./choice-card";
|
||||
|
||||
export const CHOICE_ACTION = "answer_choice" as const;
|
||||
@@ -30,8 +31,8 @@ export type StructuredProbeDerivedContext = Readonly<{
|
||||
answerClass: AnswerClass | null;
|
||||
}>;
|
||||
|
||||
export function outcomeIdForOption(optionId: ChoiceKey): AnswerClass {
|
||||
return OPTION_ANSWER_CLASS[optionId];
|
||||
export function outcomeIdForOption(optionId: ChoiceKey, schema?: unknown): AnswerClass {
|
||||
return classifyChoiceAnswer(optionId, schema);
|
||||
}
|
||||
|
||||
export function focusStatusForOption(optionId: ChoiceOptionId): "resolved" | "declined" | "skipped" {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* option copy, and never parses A/B/C/D out of assistant prose.
|
||||
*/
|
||||
|
||||
import type { DiscriminatingEventProbe } from "./refinement-packet";
|
||||
import type { DiscriminatingEventProbe, EventProbeChoiceKind, EventProbeStyleOption } from "./refinement-packet";
|
||||
import type { InternalVargaObservation } from "./varga-observations";
|
||||
|
||||
export const CHOICE_MODE = "A/B/C/D";
|
||||
@@ -39,6 +39,7 @@ export type RectificationChoiceFrame = Readonly<{
|
||||
stop_label: string;
|
||||
stop_message: string;
|
||||
scoring: boolean;
|
||||
choice_kind?: EventProbeChoiceKind;
|
||||
}>;
|
||||
|
||||
export type AgentChoiceCopy = Readonly<{
|
||||
@@ -63,6 +64,7 @@ export type RectificationChoiceCard = Readonly<{
|
||||
probe_id: string | null;
|
||||
case_revision: number | null;
|
||||
focus_id: string | null;
|
||||
choice_kind?: EventProbeChoiceKind;
|
||||
}>;
|
||||
|
||||
export type ChoiceCardFollowup = Readonly<{
|
||||
@@ -70,6 +72,8 @@ export type ChoiceCardFollowup = Readonly<{
|
||||
ask_theme: string;
|
||||
domain: string | null;
|
||||
user_prompt_hint: string;
|
||||
choice_kind?: EventProbeChoiceKind;
|
||||
style_options?: readonly EventProbeStyleOption[];
|
||||
}>;
|
||||
|
||||
export type ChoiceCardEvidence = Readonly<{
|
||||
@@ -84,6 +88,10 @@ const PRIMARY_C = "没有明显发生";
|
||||
const SECONDARY_D = "不记得 / 不确定";
|
||||
const OPTION_A = "是,大概就在那段时间";
|
||||
const OPTION_B = "有类似,但年份不对或不够重大";
|
||||
const STYLE_NEITHER = "两边都不像";
|
||||
const QUALITY_A = "有,失常或压力很大";
|
||||
const QUALITY_B = "有压力,但不算失常";
|
||||
const QUALITY_C = "没有明显失常";
|
||||
|
||||
const AGE_BAND: Record<string, { lo: number; hi: number; family: string; varga: string | null }> = {
|
||||
education: { lo: 16, hi: 18, family: "升学、高考、转学或学习环境变化", varga: "D5 / D24" },
|
||||
@@ -253,6 +261,8 @@ function hypothesisFor(
|
||||
}
|
||||
const domain = followupDomain(followup);
|
||||
const probe = pickProbe(probes, domain);
|
||||
const kind = followup.choice_kind ?? probe?.choice_kind ?? "existence";
|
||||
const styleOptions = followup.style_options ?? probe?.style_options ?? [];
|
||||
const family = probe?.event_family
|
||||
?? (domain ? AGE_BAND[domain]?.family : null)
|
||||
?? "带大概年份的经历";
|
||||
@@ -271,6 +281,40 @@ function hypothesisFor(
|
||||
: probe?.user_meaning
|
||||
?? "用一件带年份的具体生平分开还在比的时间窗。题干自己写,年份不得发明。";
|
||||
void observations;
|
||||
if (kind === "varga_style" && styleOptions.length >= 2) {
|
||||
const career = domain === "career" || followup.ask_theme === "career_style";
|
||||
if (styleOptions.length >= 3) {
|
||||
return {
|
||||
prompt: career ? "长期工作更接近哪一类?" : "这段关系更接近哪一种相处?",
|
||||
why,
|
||||
varga,
|
||||
a: styleOptions[0].label,
|
||||
b: styleOptions[1].label,
|
||||
neither: styleOptions[2].label,
|
||||
};
|
||||
}
|
||||
return {
|
||||
prompt: career ? "长期工作更接近哪一类?" : "这段关系更接近哪一种相处?",
|
||||
why,
|
||||
varga,
|
||||
a: styleOptions[0].label,
|
||||
b: styleOptions[1].label,
|
||||
neither: STYLE_NEITHER,
|
||||
};
|
||||
}
|
||||
if (kind === "event_quality") {
|
||||
const dated = probe?.year_label && probe.year_label !== "那段时间";
|
||||
return {
|
||||
prompt: dated
|
||||
? `${probe.year_label},那次高考或重要考试有没有发挥明显失常、压力很大?`
|
||||
: "那次高考或重要考试有没有发挥明显失常、压力很大?",
|
||||
why,
|
||||
varga: varga ?? "D5 / D24",
|
||||
a: QUALITY_A,
|
||||
b: QUALITY_B,
|
||||
neither: QUALITY_C,
|
||||
};
|
||||
}
|
||||
return eventHypothesis(period, family, why, varga);
|
||||
}
|
||||
|
||||
@@ -308,9 +352,19 @@ export function buildChoiceFrame(
|
||||
stop_label: CHOICE_STOP_LABEL,
|
||||
stop_message: CHOICE_STOP_MESSAGE,
|
||||
scoring,
|
||||
choice_kind: hypothesisKind(followup, input.probes),
|
||||
};
|
||||
}
|
||||
|
||||
function hypothesisKind(
|
||||
followup: ChoiceCardFollowup,
|
||||
probes?: readonly DiscriminatingEventProbe[],
|
||||
): EventProbeChoiceKind {
|
||||
return followup.choice_kind
|
||||
?? pickProbe(probes, followupDomain(followup))?.choice_kind
|
||||
?? "existence";
|
||||
}
|
||||
|
||||
function clippedCopy(value: unknown, min: number, max: number): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const text = value.trim().replace(/\s+/g, " ");
|
||||
@@ -391,6 +445,7 @@ export function mergeChoiceCard(
|
||||
probe_id: meta.probe_id ?? null,
|
||||
case_revision: meta.case_revision ?? null,
|
||||
focus_id: meta.focus_id ?? null,
|
||||
choice_kind: frame.choice_kind,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -454,5 +509,8 @@ export function parseRectificationChoiceCard(value: unknown): RectificationChoic
|
||||
? row.case_revision
|
||||
: null,
|
||||
focus_id: typeof row.focus_id === "string" && row.focus_id.trim() ? row.focus_id : null,
|
||||
...(row.choice_kind === "existence" || row.choice_kind === "varga_style" || row.choice_kind === "event_quality"
|
||||
? { choice_kind: row.choice_kind }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ export function applyChoiceWithoutEvidence(
|
||||
}
|
||||
const openProbeId = nextProbe(state)?.id ?? null;
|
||||
const lastAnsweredId = state.answered_probes.at(-1)?.probe_id ?? null;
|
||||
const answerClass = classifyChoiceAnswer(choiceKey);
|
||||
const answerClass = classifyChoiceAnswer(choiceKey, input.schema);
|
||||
if (
|
||||
(submittedProbeId && submittedProbeId !== openProbeId && submittedProbeId !== lastAnsweredId && submittedProbeId !== probe.id && submittedProbeId !== probe.semantic_key && submittedProbeId !== `probe:${probe.semantic_key}`)
|
||||
|| (openProbeId && probe.id !== openProbeId && probe.id !== lastAnsweredId)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {
|
||||
askedKeysFromLedgerEvidence,
|
||||
buildCandidateContrastPacket,
|
||||
volunteeredDomainsFromEvidence,
|
||||
} from "../core/candidate-contrast-packet.ts";
|
||||
import { evaluateCandidateSeparation } from "../core/candidate-separation.ts";
|
||||
import { askedProbeKeysFromReceipt, previousInferenceFromReceipt } from "./inference-adapter";
|
||||
@@ -88,6 +89,7 @@ export function choiceCardFromCaseDossier(dossier: {
|
||||
candidateTimes: candidateScores.map((item) => item.time),
|
||||
transitions: windowScan?.transitions ?? [],
|
||||
askedKeys: askedProbeKeys,
|
||||
volunteeredDomains: volunteeredDomainsFromEvidence(dossier.evidence),
|
||||
});
|
||||
const userStopped = latestUserStoppedCollecting(dossier.turns ?? []);
|
||||
return projectRectificationChoiceCard({
|
||||
|
||||
@@ -94,6 +94,12 @@ export type MethodFollowup = Readonly<{
|
||||
semantic_key?: string;
|
||||
candidate_split_hash?: string;
|
||||
probe_year?: number;
|
||||
choice_kind?: "existence" | "varga_style" | "event_quality";
|
||||
style_options?: readonly Readonly<{
|
||||
label: string;
|
||||
answer_class: string;
|
||||
sign?: string;
|
||||
}>[];
|
||||
}>;
|
||||
|
||||
export type MethodFollowupPlan = Readonly<{
|
||||
@@ -470,6 +476,12 @@ function discriminatorFromFollowup(followup: MethodFollowup | null): CandidateDi
|
||||
domain: followup.domain,
|
||||
year: followup.probe_year ?? null,
|
||||
semanticKey: followup.semantic_key ?? followup.method_id,
|
||||
choiceKind: followup.choice_kind,
|
||||
styleOptions: followup.style_options?.map((item) => ({
|
||||
label: item.label,
|
||||
answerClass: item.answer_class as "yes" | "weak_yes" | "no" | "unsure",
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -785,6 +797,8 @@ export function buildMethodFollowupPlan(input: {
|
||||
semantic_key: conflictProbe.semantic_key ?? `${conflictProbe.domain}.${conflictProbe.year}`,
|
||||
candidate_split_hash: conflictProbe.candidate_split_hash,
|
||||
probe_year: conflictProbe.year,
|
||||
choice_kind: conflictProbe.choice_kind ?? "existence",
|
||||
style_options: conflictProbe.style_options,
|
||||
}, true, true);
|
||||
} else if (!relationshipCovered && !declined.has("relationship")) {
|
||||
next = makeFollowup({
|
||||
@@ -860,6 +874,12 @@ export function buildMethodFollowupPlan(input: {
|
||||
semantic_key: contrastProbe.semanticKey,
|
||||
candidate_split_hash: contrastProbe.candidateSplitHash,
|
||||
probe_year: contrastProbe.year ?? undefined,
|
||||
choice_kind: contrastProbe.choiceKind ?? "existence",
|
||||
style_options: contrastProbe.styleOptions?.map((item) => ({
|
||||
label: item.label,
|
||||
answer_class: item.answerClass,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})),
|
||||
}, true, true);
|
||||
} else if (stage === "lagna_frame") {
|
||||
next = makeFollowup({
|
||||
|
||||
@@ -92,6 +92,8 @@ export type WindowScanTransition = Readonly<{
|
||||
layer: WindowScanLayer;
|
||||
at: string;
|
||||
user_meaning: string;
|
||||
from_sign?: string;
|
||||
to_sign?: string;
|
||||
}>;
|
||||
|
||||
export type EventDashaLedgerRow = Readonly<{
|
||||
@@ -179,6 +181,14 @@ export type EventProbeSource =
|
||||
|
||||
export type EventProbeRole = "distinguish" | "reverse_verify";
|
||||
|
||||
export type EventProbeChoiceKind = "existence" | "varga_style" | "event_quality";
|
||||
|
||||
export type EventProbeStyleOption = Readonly<{
|
||||
label: string;
|
||||
answer_class: string;
|
||||
sign?: string;
|
||||
}>;
|
||||
|
||||
export type DiscriminatingEventProbe = Readonly<{
|
||||
year: number;
|
||||
year_label: string;
|
||||
@@ -200,6 +210,8 @@ export type DiscriminatingEventProbe = Readonly<{
|
||||
}>[];
|
||||
left_time?: string;
|
||||
right_time?: string;
|
||||
choice_kind?: EventProbeChoiceKind;
|
||||
style_options?: readonly EventProbeStyleOption[];
|
||||
}>;
|
||||
|
||||
function asRecord(value: unknown): Readonly<Record<string, unknown>> | null {
|
||||
@@ -262,6 +274,8 @@ export function parseWindowScanTransitions(value: unknown): readonly WindowScanT
|
||||
layer,
|
||||
at,
|
||||
user_meaning: `${LAYER_LABEL[layer]} 在 ${at} 发生变化`,
|
||||
...(asText(row.from_sign, 12) ? { from_sign: asText(row.from_sign, 12)! } : {}),
|
||||
...(asText(row.to_sign, 12) ? { to_sign: asText(row.to_sign, 12)! } : {}),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
@@ -474,6 +488,22 @@ export function parseDiscriminatingEventProbes(value: unknown): readonly Discrim
|
||||
...(asText(row.candidate_split_hash, 120) ? { candidate_split_hash: asText(row.candidate_split_hash, 120)! } : {}),
|
||||
...(asTime(row.left_time) ? { left_time: asTime(row.left_time)! } : {}),
|
||||
...(asTime(row.right_time) ? { right_time: asTime(row.right_time)! } : {}),
|
||||
...(row.choice_kind === "existence" || row.choice_kind === "varga_style" || row.choice_kind === "event_quality"
|
||||
? { choice_kind: row.choice_kind }
|
||||
: {}),
|
||||
...(Array.isArray(row.style_options) ? {
|
||||
style_options: row.style_options.flatMap((item) => {
|
||||
const option = asRecord(item);
|
||||
const label = asText(option?.label, 80);
|
||||
const answer = typeof option?.answer_class === "string" ? option.answer_class : "";
|
||||
if (!option || !label || !answer) return [];
|
||||
return [{
|
||||
label,
|
||||
answer_class: answer,
|
||||
...(asText(option.sign, 12) ? { sign: asText(option.sign, 12)! } : {}),
|
||||
}];
|
||||
}),
|
||||
} : {}),
|
||||
...(Array.isArray(row.expected_outcomes) ? {
|
||||
expected_outcomes: row.expected_outcomes.flatMap((item) => {
|
||||
const outcome = asRecord(item);
|
||||
|
||||
@@ -69,6 +69,7 @@ function expectedAnswerSchemaFor(
|
||||
},
|
||||
semantic_key: followup.semantic_key ?? null,
|
||||
candidate_split_hash: followup.candidate_split_hash ?? null,
|
||||
choice_kind: frame.choice_kind ?? followup.choice_kind ?? "existence",
|
||||
};
|
||||
return stampChoiceSchemaWithProbe(
|
||||
schema,
|
||||
|
||||
@@ -9,42 +9,7 @@
|
||||
import type { EventDashaLedgerRow } from "./refinement-packet";
|
||||
import type { WindowScan } from "./varga-observations";
|
||||
import type { CandidateSeparation } from "../core/candidate-separation";
|
||||
|
||||
const D9_TYPE_TABLE: Readonly<Record<string, { trait: string; spouse: string; marriage: string }>> = {
|
||||
白羊座: { trait: "主动、热情、冲动", spouse: "独立、有活力", marriage: "早期婚姻、激情型" },
|
||||
金牛座: { trait: "稳定、务实、占有欲强", spouse: "稳重、有经济基础", marriage: "晚婚、稳定型" },
|
||||
双子座: { trait: "沟通、多变、好奇心强", spouse: "聪明、善交流", marriage: "多次恋爱、友谊型" },
|
||||
巨蟹座: { trait: "情感丰富、家庭导向", spouse: "温柔、顾家", marriage: "家庭型" },
|
||||
狮子座: { trait: "骄傲、戏剧性、领导欲", spouse: "有魅力、有地位", marriage: "戏剧性" },
|
||||
处女座: { trait: "完美主义、挑剔、服务型", spouse: "细致、有技能", marriage: "晚婚、服务型" },
|
||||
天秤座: { trait: "和谐、美感、合作", spouse: "优雅、有艺术气质", marriage: "美满、合作型" },
|
||||
天蝎座: { trait: "深刻、占有欲、转化", spouse: "神秘、有深度", marriage: "深刻、转化型" },
|
||||
射手座: { trait: "自由、哲学、冒险", spouse: "开放、有学识", marriage: "自由型、精神伴侣" },
|
||||
摩羯座: { trait: "务实、责任、延迟", spouse: "成熟、有事业", marriage: "晚婚、责任型" },
|
||||
水瓶座: { trait: "独立、非传统、友谊", spouse: "独特、有理想", marriage: "非传统、友谊型" },
|
||||
双鱼座: { trait: "浪漫、牺牲、灵性", spouse: "灵性、有艺术天赋", marriage: "灵性、牺牲型" },
|
||||
};
|
||||
|
||||
const D10_TYPE_TABLE: Readonly<Record<string, { trait: string; occupation: string; style: string }>> = {
|
||||
白羊座: { trait: "领导、创业、竞争", occupation: "创业者、运动员、军人", style: "主动、竞争" },
|
||||
金牛座: { trait: "稳定、财富、艺术", occupation: "金融、艺术、农业", style: "稳定、务实" },
|
||||
双子座: { trait: "沟通、写作、教育", occupation: "教师、作家、销售", style: "多变、沟通" },
|
||||
巨蟹座: { trait: "照顾、家庭、情感", occupation: "护理、餐饮、房地产", style: "照顾、情感" },
|
||||
狮子座: { trait: "领导、表演、创意", occupation: "管理、娱乐、政治", style: "领导、表演" },
|
||||
处女座: { trait: "服务、分析、健康", occupation: "医疗、分析、服务", style: "细致、服务" },
|
||||
天秤座: { trait: "合作、美学、法律", occupation: "法律、艺术、咨询", style: "合作、和谐" },
|
||||
天蝎座: { trait: "研究、转化、危机", occupation: "研究、心理学、危机管理", style: "深度、转化" },
|
||||
射手座: { trait: "教育、哲学、国际", occupation: "教育、出版、国际事务", style: "自由、哲学" },
|
||||
摩羯座: { trait: "管理、责任、延迟", occupation: "管理、政府、建筑", style: "务实、责任" },
|
||||
水瓶座: { trait: "创新、科技、人道", occupation: "科技、人道主义、创新", style: "创新、独立" },
|
||||
双鱼座: { trait: "灵性、艺术、服务", occupation: "艺术、灵性、医疗", style: "灵性、服务" },
|
||||
};
|
||||
|
||||
function signKey(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.endsWith("座")) return trimmed;
|
||||
return `${trimmed}座`;
|
||||
}
|
||||
import { D9_TYPE_TABLE, D10_TYPE_TABLE, signKey } from "./varga-type-tables";
|
||||
|
||||
function typeRow(sign: string, table: "d9" | "d10"): string {
|
||||
const key = signKey(sign);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* D9/D10 type tables for rectification contrast, not fate promises.
|
||||
*/
|
||||
|
||||
export type D9TypeRow = Readonly<{ trait: string; spouse: string; marriage: string }>;
|
||||
export type D10TypeRow = Readonly<{ trait: string; occupation: string; style: string }>;
|
||||
|
||||
export const D9_TYPE_TABLE: Readonly<Record<string, D9TypeRow>> = {
|
||||
白羊座: { trait: "主动、热情、冲动", spouse: "独立、有活力", marriage: "早期婚姻、激情型" },
|
||||
金牛座: { trait: "稳定、务实、占有欲强", spouse: "稳重、有经济基础", marriage: "晚婚、稳定型" },
|
||||
双子座: { trait: "沟通、多变、好奇心强", spouse: "聪明、善交流", marriage: "多次恋爱、友谊型" },
|
||||
巨蟹座: { trait: "情感丰富、家庭导向", spouse: "温柔、顾家", marriage: "家庭型" },
|
||||
狮子座: { trait: "骄傲、戏剧性、领导欲", spouse: "有魅力、有地位", marriage: "戏剧性" },
|
||||
处女座: { trait: "完美主义、挑剔、服务型", spouse: "细致、有技能", marriage: "晚婚、服务型" },
|
||||
天秤座: { trait: "和谐、美感、合作", spouse: "优雅、有艺术气质", marriage: "美满、合作型" },
|
||||
天蝎座: { trait: "深刻、占有欲、转化", spouse: "神秘、有深度", marriage: "深刻、转化型" },
|
||||
射手座: { trait: "自由、哲学、冒险", spouse: "开放、有学识", marriage: "自由型、精神伴侣" },
|
||||
摩羯座: { trait: "务实、责任、延迟", spouse: "成熟、有事业", marriage: "晚婚、责任型" },
|
||||
水瓶座: { trait: "独立、非传统、友谊", spouse: "独特、有理想", marriage: "非传统、友谊型" },
|
||||
双鱼座: { trait: "浪漫、牺牲、灵性", spouse: "灵性、有艺术天赋", marriage: "灵性、牺牲型" },
|
||||
};
|
||||
|
||||
export const D10_TYPE_TABLE: Readonly<Record<string, D10TypeRow>> = {
|
||||
白羊座: { trait: "领导、创业、竞争", occupation: "创业者、运动员、军人", style: "主动、竞争" },
|
||||
金牛座: { trait: "稳定、财富、艺术", occupation: "金融、艺术、农业", style: "稳定、务实" },
|
||||
双子座: { trait: "沟通、写作、教育", occupation: "教师、作家、销售", style: "多变、沟通" },
|
||||
巨蟹座: { trait: "照顾、家庭、情感", occupation: "护理、餐饮、房地产", style: "照顾、情感" },
|
||||
狮子座: { trait: "领导、表演、创意", occupation: "管理、娱乐、政治", style: "领导、表演" },
|
||||
处女座: { trait: "服务、分析、健康", occupation: "医疗、分析、服务", style: "细致、服务" },
|
||||
天秤座: { trait: "合作、美学、法律", occupation: "法律、艺术、咨询", style: "合作、和谐" },
|
||||
天蝎座: { trait: "研究、转化、危机", occupation: "研究、心理学、危机管理", style: "深度、转化" },
|
||||
射手座: { trait: "教育、哲学、国际", occupation: "教育、出版、国际事务", style: "自由、哲学" },
|
||||
摩羯座: { trait: "管理、责任、延迟", occupation: "管理、政府、建筑", style: "务实、责任" },
|
||||
水瓶座: { trait: "创新、科技、人道", occupation: "科技、人道主义、创新", style: "创新、独立" },
|
||||
双鱼座: { trait: "灵性、艺术、服务", occupation: "艺术、灵性、医疗", style: "灵性、服务" },
|
||||
};
|
||||
|
||||
export function signKey(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
return trimmed.endsWith("座") ? trimmed : `${trimmed}座`;
|
||||
}
|
||||
|
||||
export function d9StyleLabel(sign: string): string {
|
||||
const row = D9_TYPE_TABLE[signKey(sign)];
|
||||
return row?.trait ?? `${signKey(sign)}相处`;
|
||||
}
|
||||
|
||||
export function d10StyleLabel(sign: string): string {
|
||||
const row = D10_TYPE_TABLE[signKey(sign)];
|
||||
return row?.style ?? `${signKey(sign)}职责`;
|
||||
}
|
||||
@@ -85,6 +85,7 @@ import {
|
||||
buildCandidateContrastPacket,
|
||||
conflictProbesFromContrast,
|
||||
selectDiscriminatorProbe,
|
||||
volunteeredDomainsFromEvidence,
|
||||
} from "@/lib/rectification-agentic/core/candidate-contrast-packet";
|
||||
import { evaluateCandidateSeparation } from "@/lib/rectification-agentic/core/candidate-separation";
|
||||
import { offerSessionKinds } from "@/lib/rectification-agentic/core/decide-next-action";
|
||||
@@ -205,6 +206,7 @@ function contrastPacketFromLatest(
|
||||
candidateTimes,
|
||||
transitions: windowScan?.transitions ?? [],
|
||||
askedKeys: askedDiscriminatorKeys(latest?.decisionReceipt, evidence),
|
||||
volunteeredDomains: volunteeredDomainsFromEvidence(evidence),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -879,6 +881,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
candidateTimes: score.candidates.map((item) => item.time),
|
||||
transitions: windowScan?.transitions ?? [],
|
||||
askedKeys: askedDiscriminatorKeys(dossier.latestResult?.decisionReceipt, parsed.evidence),
|
||||
volunteeredDomains: volunteeredDomainsFromEvidence(parsed.evidence),
|
||||
});
|
||||
const inference = buildCaseInferenceState({
|
||||
range: parsed.case.candidateRange,
|
||||
|
||||
Reference in New Issue
Block a user