Dated-choice exhaustion is not convergence. Refresh probes from remaining active candidates, then ask a targeted collect, then deliver. Skill 10.0.24. Co-authored-by: Cursor <cursoragent@cursor.com>
334 lines
12 KiB
TypeScript
334 lines
12 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.24");
|
||
});
|
||
|
||
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 defers D9", () => {
|
||
const plan = personalityPlan({ eventProbes: [] });
|
||
// 原值: next_followup 是 D9 varga_style,dropped 不含 yearless_deferred
|
||
// 新值: D9 记 yearless_deferred,next 不再是性格卡
|
||
// 原因: BUG-651 带年月池空即交付,性格题不得挡在结果前面
|
||
assert.notEqual(plan.next_followup?.semantic_key, D9_STYLE.semanticKey);
|
||
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("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 \| 性格自评,只作排序参考,不参与淘汰 \|/);
|
||
});
|