Ask finance and health like other domains, put year-cued collect questions first, and let the classifier distinguish decline vs skip. Skill 10.0.22. Co-authored-by: Cursor <cursoragent@cursor.com>
329 lines
11 KiB
TypeScript
329 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts";
|
|
import { applyAnswerToState, buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
|
import { asInferenceState } from "../src/lib/rectification-agentic/core/compose-receipt.ts";
|
|
import {
|
|
PROBE_WEIGHT,
|
|
SCORE_DELTA,
|
|
STRONG_CONFLICT_ELIMINATION_COUNT,
|
|
type ConflictProbe,
|
|
} from "../src/lib/rectification-agentic/core/types.ts";
|
|
import type { CandidateDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
|
import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
|
import { buildSkillVerificationPacket } from "../src/lib/rectification-agentic/v9/skill-verification-report.ts";
|
|
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
|
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
|
|
|
|
const TIMES = ["05:00", "05:10"] as const;
|
|
|
|
function dated(
|
|
id: string,
|
|
domain: string,
|
|
eventKind: string,
|
|
occurredFrom: string,
|
|
) {
|
|
return {
|
|
id,
|
|
status: "confirmed" as const,
|
|
domain,
|
|
datePrecision: "month" as const,
|
|
occurredFrom,
|
|
occurredTo: null,
|
|
eventKind,
|
|
};
|
|
}
|
|
|
|
const LEDGER = [
|
|
dated("e-edu", "education", "education_start", "2016-09-01"),
|
|
dated("e-rel", "relationship", "relationship_start", "2024-05-01"),
|
|
dated("e-family", "family", "family_event", "2016-05-01"),
|
|
dated("e-reloc", "relocation", "home_change", "2022-10-01"),
|
|
];
|
|
|
|
const D9_STYLE: CandidateDiscriminatorProbe = {
|
|
probeId: "contrast:varga.d9.巨蟹座/狮子座",
|
|
candidateSetVersion: "05:00-05:10",
|
|
question: "亲密关系里更接近下面哪一种相处方式?",
|
|
expectedOutcomes: [
|
|
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:10"] },
|
|
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:10"], conflictsCandidateIds: ["05:00"] },
|
|
],
|
|
candidateSplitHash: "varga.d9.巨蟹座/狮子座",
|
|
informationGain: 1.4,
|
|
sourceFeatures: [{ technique: "D9", calculationResultId: null }],
|
|
domain: "relationship",
|
|
year: null,
|
|
semanticKey: "varga.d9.巨蟹座/狮子座",
|
|
choiceKind: "varga_style",
|
|
styleOptions: [
|
|
{ label: "相处里更在意照顾对方的感受", answerClass: "yes", sign: "巨蟹座" },
|
|
{ label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes", sign: "狮子座" },
|
|
],
|
|
};
|
|
|
|
const CAREER_2023_05: DiscriminatingEventProbe = {
|
|
year: 2023,
|
|
year_label: "2023 年 5 月前后",
|
|
month: 5,
|
|
domain: "career",
|
|
event_family: "入职或职责变化",
|
|
source: "dasha_boundary",
|
|
tracks: ["vimshottari", "narayana"],
|
|
tracks_agree: true,
|
|
unique_minute_claim: false,
|
|
user_meaning: "2023 年 5 月前后有没有入职或换工作?",
|
|
role: "distinguish",
|
|
phase: "candidate_discriminator",
|
|
information_gain: 0.9,
|
|
semantic_key: "career.2023.05.dasha_boundary",
|
|
candidate_split_hash: "career.2023.05",
|
|
candidate_ids: [...TIMES],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
|
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
|
{ answer_class: "unsure", supports: [], conflicts: [] },
|
|
],
|
|
style_options: [
|
|
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
|
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
|
{ label: "明确没有发生", answer_class: "no" },
|
|
{ label: "这段记不清楚", answer_class: "unsure" },
|
|
],
|
|
choice_kind: "existence",
|
|
};
|
|
|
|
function personalityPlan(extra: Partial<Parameters<typeof buildMethodFollowupPlan>[0]> = {}) {
|
|
return buildMethodFollowupPlan({
|
|
evidence: LEDGER,
|
|
sessionOutcome: "discriminate_candidates",
|
|
candidatesSeparated: false,
|
|
topCandidateTimes: [...TIMES],
|
|
contrastPacket: {
|
|
candidateSetVersion: "05:00-05:10",
|
|
vargaDifferences: [],
|
|
probes: [D9_STYLE],
|
|
},
|
|
...extra,
|
|
});
|
|
}
|
|
|
|
function clockProbe(input: {
|
|
id: string;
|
|
semanticKey: string;
|
|
domain: string;
|
|
year: number;
|
|
source: string;
|
|
choiceKind?: ConflictProbe["choice_kind"];
|
|
yesSupports: readonly string[];
|
|
yesConflicts: readonly string[];
|
|
}): ConflictProbe {
|
|
return {
|
|
id: input.id,
|
|
semantic_key: input.semanticKey,
|
|
candidate_split_hash: `${input.semanticKey}:${input.yesSupports.join(",")}`,
|
|
domain: input.domain,
|
|
year: input.year,
|
|
question: input.semanticKey,
|
|
candidate_ids: [...input.yesSupports, ...input.yesConflicts],
|
|
expected_outcomes: [
|
|
{ answer_class: "yes", supports: input.yesSupports, conflicts: input.yesConflicts },
|
|
{ answer_class: "weak_yes", supports: input.yesConflicts, conflicts: input.yesSupports },
|
|
{ answer_class: "no", supports: [], conflicts: [] },
|
|
{ answer_class: "unsure", supports: [], conflicts: [] },
|
|
],
|
|
information_gain: 0.4,
|
|
source: input.source,
|
|
...(input.choiceKind ? { choice_kind: input.choiceKind } : {}),
|
|
};
|
|
}
|
|
|
|
const D9_PROBE = clockProbe({
|
|
id: "probe:varga.d9",
|
|
semanticKey: "varga.d9.巨蟹座/狮子座",
|
|
domain: "relationship",
|
|
year: 0,
|
|
source: "varga_contrast",
|
|
choiceKind: "varga_style",
|
|
yesSupports: ["05:00"],
|
|
yesConflicts: ["05:10"],
|
|
});
|
|
|
|
test("SCORE_DELTA stays ±2/±1 and yearless weight is half", () => {
|
|
assert.deepEqual(SCORE_DELTA, {
|
|
support: 2,
|
|
weak_support: 1,
|
|
neutral: 0,
|
|
weak_conflict: -1,
|
|
conflict: -2,
|
|
});
|
|
assert.equal(PROBE_WEIGHT.dated, 1);
|
|
assert.equal(PROBE_WEIGHT.yearless, 0.5);
|
|
assert.equal(STRONG_CONFLICT_ELIMINATION_COUNT, 3);
|
|
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
|
});
|
|
|
|
test("D9 answer B moves scores by ±1 and does not count toward elimination", () => {
|
|
const applied = applyProbeOutcome(
|
|
{ "05:00": 20, "05:10": 20 },
|
|
D9_PROBE,
|
|
"weak_yes",
|
|
);
|
|
assert.equal(applied.kind, "tie_break");
|
|
assert.equal(applied.deltas["05:10"], 1);
|
|
assert.equal(applied.deltas["05:00"], -1);
|
|
assert.equal(applied.strong_conflict_counts["05:00"], 0);
|
|
assert.equal(applied.strong_conflict_counts["05:10"], 0);
|
|
assert.deepEqual(applied.eliminated_ids, []);
|
|
});
|
|
|
|
test("three yearless conflicts never eliminate; three dated conflicts still do", () => {
|
|
const yearless = [1, 2, 3].map((index) => clockProbe({
|
|
id: `probe:varga.d9.${index}`,
|
|
semanticKey: `varga.d9.style.${index}`,
|
|
domain: "relationship",
|
|
year: 0,
|
|
source: "varga_contrast",
|
|
choiceKind: "varga_style",
|
|
yesSupports: ["05:00"],
|
|
yesConflicts: ["05:10"],
|
|
}));
|
|
let scores: Record<string, number> = { "05:00": 20, "05:10": 20 };
|
|
let counts: Record<string, number> = { "05:00": 0, "05:10": 0 };
|
|
let eliminated = new Set<string>();
|
|
for (const probe of yearless) {
|
|
const applied = applyProbeOutcome(scores, probe, "yes", {
|
|
eliminatedIds: eliminated,
|
|
strongConflictCounts: counts,
|
|
});
|
|
scores = { ...applied.scores };
|
|
counts = { ...applied.strong_conflict_counts };
|
|
eliminated = new Set(applied.eliminated_ids);
|
|
}
|
|
assert.equal(counts["05:10"], 0);
|
|
assert.equal(eliminated.has("05:10"), false);
|
|
|
|
const dated = [1, 2, 3].map((index) => clockProbe({
|
|
id: `probe:career.${2019 + index}`,
|
|
semanticKey: `career.${2019 + index}.dasha_boundary`,
|
|
domain: "career",
|
|
year: 2019 + index,
|
|
source: "dasha_boundary",
|
|
yesSupports: ["05:00"],
|
|
yesConflicts: ["05:10"],
|
|
}));
|
|
for (const probe of dated) {
|
|
const applied = applyProbeOutcome(scores, probe, "yes", {
|
|
eliminatedIds: eliminated,
|
|
strongConflictCounts: counts,
|
|
});
|
|
scores = { ...applied.scores };
|
|
counts = { ...applied.strong_conflict_counts };
|
|
eliminated = new Set(applied.eliminated_ids);
|
|
assert.equal(applied.kind, "informative");
|
|
assert.equal(applied.deltas["05:00"], 2);
|
|
assert.equal(applied.deltas["05:10"], -2);
|
|
}
|
|
assert.ok(counts["05:10"] >= STRONG_CONFLICT_ELIMINATION_COUNT);
|
|
assert.equal(eliminated.has("05:10"), true);
|
|
});
|
|
|
|
test("nakshatra_boundary is yearless: ±1 and no conflict count", () => {
|
|
const probe = clockProbe({
|
|
id: "probe:nakshatra",
|
|
semanticKey: "nakshatra.boundary.a/b",
|
|
domain: "other",
|
|
year: 0,
|
|
source: "nakshatra_boundary",
|
|
choiceKind: "varga_style",
|
|
yesSupports: ["05:00"],
|
|
yesConflicts: ["05:10"],
|
|
});
|
|
const applied = applyProbeOutcome({ "05:00": 20, "05:10": 20 }, probe, "yes");
|
|
assert.equal(applied.kind, "tie_break");
|
|
assert.equal(applied.deltas["05:00"], 1);
|
|
assert.equal(applied.deltas["05:10"], -1);
|
|
assert.equal(applied.strong_conflict_counts["05:10"], 0);
|
|
});
|
|
|
|
test("an askable 2023.05 career probe keeps D9 deferred", () => {
|
|
const plan = personalityPlan({ eventProbes: [CAREER_2023_05] });
|
|
assert.equal(plan.next_followup?.semantic_key, CAREER_2023_05.semantic_key);
|
|
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
|
|
assert.equal(
|
|
plan.dropped_probes.some((item) => (
|
|
item.semantic_key === D9_STYLE.semanticKey && item.reason === "yearless_deferred"
|
|
)),
|
|
true,
|
|
JSON.stringify(plan.dropped_probes),
|
|
);
|
|
});
|
|
|
|
test("dated distinguish empty and two unseparated candidates asks D9", () => {
|
|
const plan = personalityPlan({ eventProbes: [] });
|
|
assert.equal(plan.next_followup?.semantic_key, D9_STYLE.semanticKey);
|
|
assert.equal(plan.next_followup?.choice_kind, "varga_style");
|
|
assert.ok(plan.next_followup?.choice_frame);
|
|
assert.equal(
|
|
plan.dropped_probes.some((item) => item.reason === "yearless_deferred"),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("a sole remaining candidate defers D9 as yearless_deferred", () => {
|
|
const plan = personalityPlan({
|
|
eventProbes: [],
|
|
topCandidateTimes: ["05:00"],
|
|
});
|
|
assert.notEqual(plan.next_followup?.semantic_key, D9_STYLE.semanticKey);
|
|
assert.equal(
|
|
plan.dropped_probes.some((item) => (
|
|
item.semantic_key === D9_STYLE.semanticKey && item.reason === "yearless_deferred"
|
|
)),
|
|
true,
|
|
JSON.stringify(plan.dropped_probes),
|
|
);
|
|
});
|
|
|
|
test("already-separated candidates do not ask D9", () => {
|
|
const plan = personalityPlan({
|
|
eventProbes: [],
|
|
candidatesSeparated: true,
|
|
});
|
|
assert.notEqual(plan.next_followup?.semantic_key, D9_STYLE.semanticKey);
|
|
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
|
|
});
|
|
|
|
test("asInferenceState round-trips a varga_style tie_break round", () => {
|
|
const before = buildInferenceState({
|
|
range_start: "05:00",
|
|
range_end: "05:10",
|
|
candidates: [
|
|
{ id: "05:00", time: "05:00", relative_support: 20 },
|
|
{ id: "05:10", time: "05:10", relative_support: 18 },
|
|
],
|
|
events: [{ id: "e-rel", domain: "relationship", year: 2024, precision: "year" }],
|
|
probes: [D9_PROBE],
|
|
});
|
|
const after = applyAnswerToState(before, D9_PROBE.id, "weak_yes");
|
|
assert.equal(after.rounds.at(-1)?.kind, "tie_break");
|
|
const loaded = asInferenceState(JSON.parse(JSON.stringify(after)));
|
|
assert.ok(loaded);
|
|
assert.equal(loaded.rounds.at(-1)?.kind, "tie_break");
|
|
assert.equal(loaded.answered_probes.length, 1);
|
|
});
|
|
|
|
test("verification report marks D9/D10 and nakshatra as reference", () => {
|
|
const packet = buildSkillVerificationPacket({
|
|
representativeTime: "05:00",
|
|
widthMinutes: 10,
|
|
candidates: [
|
|
{ time: "05:00", rank: 1, relativeSupport: 20 },
|
|
{ time: "05:10", rank: 2, relativeSupport: 18 },
|
|
],
|
|
});
|
|
assert.match(packet.markdown, /\| D9 \/ D10 类型对照 \| reference \| 性格自评,只作排序参考,不参与淘汰 \|/);
|
|
assert.match(packet.markdown, /\| 月宿边界 \| reference \| 性格自评,只作排序参考,不参与淘汰 \|/);
|
|
});
|