Files
Jyotisha/frontend/tests/rectification-occupation-coverage-exit.test.ts
T
Jesse_Chen b43808b016
Independent Staging Quality Gate / validate (push) Successful in 9m10s
Independent Staging Quality Gate / publish (push) Successful in 8m43s
fix(rectification): stop yearless ungrounded varga contrast from minting cards
Only sign-bound varga_style questions may omit a concrete period. Remaining-layer existence and quality probes now drop as yearless_ungrounded_contrast instead of scoring by group order.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 10:55:11 +08:00

483 lines
19 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import {
parseRectificationCandidateResult,
workingRectificationHouseTable,
} from "../src/lib/rectification-candidate-result.ts";
import { decideFromDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import { latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
import { isRenderableChoiceOpenQuestion } from "../src/lib/rectification-agentic/v9/server-focus.ts";
const STYLE_OPTIONS = [
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
{ label: "明确没有发生", answer_class: "no" as const },
{ label: "这段记不清楚", answer_class: "unsure" as const },
];
const TIED = [
{ time: "04:47", score: 12 },
{ time: "04:51", score: 12 },
{ time: "04:53", score: 11 },
{ time: "04:59", score: 11 },
{ time: "05:00", score: 11 },
{ time: "05:07", score: 11 },
{ time: "05:12", score: 11 },
{ time: "05:14", score: 8 },
{ time: "05:15", score: 7 },
];
function dated(
id: string,
domain: string,
eventKind: string,
occurredFrom: string,
extra: {
occurredTo?: string | null;
datePrecision?: "year" | "month" | "day" | "range" | "unknown";
status?: "confirmed" | "draft";
} = {},
) {
return {
id,
status: extra.status ?? ("confirmed" as const),
domain,
datePrecision: extra.datePrecision ?? ("month" as const),
occurredFrom,
occurredTo: extra.occurredTo ?? null,
eventKind,
};
}
/** 19-row ledger from the stalled case: 15 scoreable, 4 domains; occupation never recorded. */
const CASE_EVIDENCE = [
dated("e-edu-start", "education", "education_start", "2016-09-01"),
dated("e-edu-complete", "education", "education_completion", "2020-06-01"),
dated("e-edu-interrupt", "education", "education_interruption", "2021-01-01", {
occurredTo: "2023-07-01",
datePrecision: "range",
}),
dated("e-edu-change", "education", "education_change", "2023-01-01", { datePrecision: "year" }),
dated("e-career-entry-2020", "career", "career_entry", "2020-04-01"),
dated("e-career-exit-2020", "career", "career_exit", "2020-10-01"),
dated("e-career-entry-2024", "career", "career_entry", "2024-04-07", { datePrecision: "day" }),
dated("e-career-exit-2026", "career", "career_exit", "2026-08-11", { datePrecision: "day" }),
dated("e-career-change-2022", "career", "career_change", "2022-12-01"),
dated("e-career-change-2024", "career", "career_change", "2024-01-01", { datePrecision: "year" }),
dated("e-career-range", "career", "career_change", "2020-04-01", {
occurredTo: "2020-10-01",
datePrecision: "range",
}),
dated("e-career-draft-a", "career", "career_change", "", {
datePrecision: "unknown",
status: "draft",
occurredTo: null,
}),
dated("e-career-draft-b", "career", "career_entry", "", {
datePrecision: "unknown",
status: "draft",
occurredTo: null,
}),
dated("e-rel-2024-05", "relationship", "relationship_change", "2024-05-01"),
dated("e-rel-start", "relationship", "relationship_start", "2024-06-01"),
dated("e-rel-end", "relationship", "relationship_end", "2024-08-08", { datePrecision: "day" }),
dated("e-reloc", "relocation", "home_change", "2022-10-01"),
dated("e-family", "family", "family_event", "2016-05-01"),
].map((item) => ({
...item,
occurredFrom: item.occurredFrom || null,
}));
const OCCUPATION_FOCUS = {
intent: "collect_method_evidence" as const,
targetDomain: "occupation",
targetKind: "occupation_note",
questionId: "collect:occupation:collect_method_evidence",
};
function yearlessProbe(layer: string, gain: number, domain: string) {
return {
probeId: `contrast:varga.${layer}.04:47/05:00`,
candidateSetVersion: "04:47-05:15",
question: `当前几个候选在 ${layer} 上还分得开。`,
expectedOutcomes: [
{ outcomeId: "yes" as const, supportsCandidateIds: ["04:47"], conflictsCandidateIds: ["05:00"] },
{ outcomeId: "no" as const, supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["04:47"] },
],
candidateSplitHash: `varga.${layer}.04:47/05:00`,
informationGain: gain,
sourceFeatures: [{ technique: layer.toUpperCase(), calculationResultId: null }],
domain,
year: null as number | null,
semanticKey: `varga.${layer}.04:47/05:00`,
choiceKind: "existence" as const,
styleOptions: STYLE_OPTIONS.map((item) => ({
label: item.label,
answerClass: item.answer_class,
})),
};
}
const YEARLESS_PACKET = {
candidateSetVersion: "04:47-05:15",
vargaDifferences: [] as const,
probes: [
yearlessProbe("d24", 2.5, "education"),
yearlessProbe("d7", 1.22, "family"),
yearlessProbe("d4", 0.99, "relocation"),
yearlessProbe("d5", 0.5, "education"),
],
};
function occupationPlan(
extra: Partial<Parameters<typeof buildMethodFollowupPlan>[0]> = {},
) {
return buildMethodFollowupPlan({
evidence: CASE_EVIDENCE,
contrastPacket: YEARLESS_PACKET,
candidatesSeparated: false,
...extra,
});
}
function twelveHouses(sign: string, occupant?: string) {
return Array.from({ length: 12 }, (_, index) => ({
house: index + 1,
sign,
occupants: index === 0 && occupant ? [occupant] : [],
}));
}
const CANDIDATE_IDS = [
"88888888-8888-4888-8888-888888888881",
"88888888-8888-4888-8888-888888888882",
] as const;
test("skill version stays 10.0.13 for the occupation-coverage exit fix", () => {
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13");
});
test("nineteen-row ledger opens the training gate with four scoreable domains", () => {
const gate = trainingScoreableGate(CASE_EVIDENCE);
assert.equal(gate.open, true);
assert.ok(gate.trainingCount >= 3);
assert.ok(gate.trainingDomainCount >= 2);
assert.ok(CASE_EVIDENCE.length >= 15);
});
test("career answers under an occupation collect focus do not cover occupation before ledger norm", () => {
const plan = occupationPlan();
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
assert.equal(
CASE_EVIDENCE.some((item) => item.domain === "occupation" && item.eventKind === "occupation_note"),
false,
);
});
test("answering occupation collect remaps a career-domain job description onto occupation_note", async () => {
const { applyOccupationCollectLedgerNorm } = await import(
"../src/lib/rectification-agentic/v9/evidence-model.ts"
);
const remapped = applyOccupationCollectLedgerNorm(OCCUPATION_FOCUS, [{
domain: "career",
eventKind: "career_entry" as const,
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
summary: "长期做有机合成",
quote: "有机合成",
subject: "self" as const,
}]);
assert.equal(remapped[0]?.domain, "occupation");
assert.equal(remapped[0]?.eventKind, "occupation_note");
const plan = occupationPlan({
evidence: [
...CASE_EVIDENCE,
{
status: "confirmed" as const,
domain: remapped[0]!.domain,
datePrecision: remapped[0]!.datePrecision,
occurredFrom: remapped[0]!.occurredFrom,
occurredTo: remapped[0]!.occurredTo,
eventKind: remapped[0]!.eventKind,
summary: remapped[0]!.summary,
},
],
contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] },
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.notEqual(plan.next_followup?.method_id, "occupation");
assert.notEqual(plan.next_followup?.ask_theme, "occupation");
});
test("occupation coverage fallback uses a closed occupation collect focus plus career evidence", () => {
const uncovered = occupationPlan({
contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] },
});
assert.equal(uncovered.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
const covered = occupationPlan({
contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] },
closedCollectFocuses: [{
questionId: OCCUPATION_FOCUS.questionId,
targetDomain: "occupation",
intent: "collect_method_evidence",
status: "resolved",
}],
});
assert.equal(covered.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.notEqual(covered.next_followup?.method_id, "occupation");
});
test("career evidence alone still does not cover occupation", () => {
const plan = occupationPlan({
contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] },
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
assert.equal(plan.next_followup?.method_id, "occupation");
});
test("training gate open does not mint yearless existence or quality varga cards", () => {
const plan = occupationPlan();
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /^varga\.d(24|7|4|5)\./);
assert.equal(plan.dropped_probes.some((item) => item.reason === "yearless_ungrounded_contrast"), true);
assert.equal(plan.next_followup?.method_id, "occupation");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.choice_frame, null);
});
test("training gate open still asks a signed yearless D10 style card", () => {
const d10 = {
probeId: "contrast:varga.d10.巨蟹座/狮子座",
candidateSetVersion: "04:47-05:15",
question: "平时做事,你更接近下面哪一种?",
expectedOutcomes: [
{ outcomeId: "yes" as const, supportsCandidateIds: ["04:47"], conflictsCandidateIds: ["05:00"] },
{ outcomeId: "weak_yes" as const, supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["04:47"] },
],
candidateSplitHash: "varga.d10.巨蟹座/狮子座",
informationGain: 1.4,
sourceFeatures: [{ technique: "D10", calculationResultId: null }],
domain: "career",
year: null as number | null,
semanticKey: "varga.d10.巨蟹座/狮子座",
choiceKind: "varga_style" as const,
styleOptions: [
{ label: "做事以照顾人为主,在意团队里的感受", answerClass: "yes" as const, sign: "巨蟹座" },
{ label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes" as const, sign: "狮子座" },
],
};
const plan = occupationPlan({
contrastPacket: {
...YEARLESS_PACKET,
probes: [...YEARLESS_PACKET.probes, d10],
},
});
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
assert.ok(plan.next_followup?.choice_frame);
assert.equal(plan.next_followup?.semantic_key, d10.semanticKey);
assert.equal(plan.next_followup?.choice_kind, "varga_style");
});
test("training gate closed still withholds yearless varga cards", () => {
const short = [
dated("e-edu", "education", "education_start", "2016-09-01"),
dated("e-career", "career", "career_entry", "2020-04-01"),
];
assert.equal(trainingScoreableGate(short).open, false);
const plan = buildMethodFollowupPlan({
evidence: short,
contrastPacket: YEARLESS_PACKET,
candidatesSeparated: false,
});
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /^varga\.d(24|7|4|5)\./);
assert.notEqual(plan.next_followup?.intent, "distinguish_candidates");
});
test("training complete with no renderable distinguish card allows offering a range without adopt", () => {
const blocked = decideRectification({
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: TIED,
discriminatorProbe: null,
engineAcceptAllowed: true,
engineProposeAllowed: true,
});
assert.equal(blocked.canOfferRange, true);
assert.equal(blocked.canAdopt, false);
assert.equal(blocked.canConfirmExactMinute, false);
assert.equal(blocked.nextAction, "offer_provisional_range");
assert.equal(blocked.sessionOutcome, "provisional_range");
const stillClosed = decideRectification({
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: TIED,
discriminatorProbe: null,
engineAcceptAllowed: true,
engineProposeAllowed: true,
});
assert.equal(stillClosed.canOfferRange, false);
assert.equal(stillClosed.canAdopt, false);
assert.equal(stillClosed.nextAction, "ask_fact_collection");
});
test("decideFromDossier offers a range when training is complete and the ask layer has no card", () => {
const decision = decideFromDossier({
evidence: CASE_EVIDENCE,
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
latestResult: {
resultId: "55555555-5555-4555-8555-555555555555",
selectionAllowed: true,
confirmationAllowed: false,
candidates: TIED.map((item, index) => ({
candidateId: `88888888-8888-4888-8888-88888888888${index}`,
time: item.time,
rank: index + 1,
relativeSupport: item.score,
})),
representativeTime: "04:47",
decisionReceipt: {
accept_allowed: true,
propose_allowed: true,
selection_allowed: true,
},
},
case: { acceptedTime: null },
});
assert.equal(decision.canOfferRange, true);
assert.equal(decision.canAdopt, false);
assert.equal(decision.canConfirmExactMinute, false);
});
test("agent route narrates the binary range exit when no renderable distinguish card remains", () => {
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
assert.match(route, /可以先按当前区间看盘,也可以再补一件记得时间的经历/);
const fastPath = route.slice(
route.indexOf('if (action === "message")'),
route.indexOf("const requestTime"),
);
assert.match(fastPath, /DISCRIMINATOR_EXHAUSTED_NARRATION|isNonConvergingRangeOffer|canOfferRange/);
assert.doesNotMatch(fastPath, /USER_STOP_PATTERN|parseChoiceKeyFromUserMessage/);
});
test("public house_table and natal_recast follow the representative minute", () => {
const matched = parseRectificationCandidateResult({
resultId: "11111111-1111-4111-8111-111111111111",
candidates: [
{ candidateId: CANDIDATE_IDS[0], rank: 1, time: "04:47", relativeSupport: 12, tiedMinuteCount: 1 },
{ candidateId: CANDIDATE_IDS[1], rank: 2, time: "05:00", relativeSupport: 11, tiedMinuteCount: 1 },
],
overallConfidence: "low",
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:47",
selectedTime: null,
decisionReceipt: {
house_table: { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") },
house_tables_by_time: {
"04:47": { time: "04:47", lagna: "金牛座", houses: twelveHouses("金牛座", "太阳") },
"05:00": { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") },
},
natal_recast: {
time: "05:00",
lagna: "双子座",
user_meaning: "本命宫位已按 05:00 重算(上升 双子座)。下面是本轮实际执行的技法,不能当作唯一分钟确认。",
unique_minute_claim: false,
confirmation_allowed: false,
},
},
});
assert.equal(matched?.representativeTime, "04:47");
assert.equal(matched?.houseTable?.time, "04:47");
assert.equal(matched?.houseTable?.lagna, "金牛座");
assert.equal(matched?.natalRecast?.time, "04:47");
assert.equal(workingRectificationHouseTable(matched!)?.time, "04:47");
});
test("a mismatched receipt house_table is not emitted with another representative minute", () => {
const mismatched = parseRectificationCandidateResult({
resultId: "11111111-1111-4111-8111-111111111111",
candidates: [
{ candidateId: CANDIDATE_IDS[0], rank: 1, time: "04:47", relativeSupport: 12, tiedMinuteCount: 1 },
{ candidateId: CANDIDATE_IDS[1], rank: 2, time: "05:00", relativeSupport: 11, tiedMinuteCount: 1 },
],
overallConfidence: "low",
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:47",
selectedTime: null,
decisionReceipt: {
house_table: { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") },
natal_recast: {
time: "05:00",
lagna: "双子座",
user_meaning: "本命宫位已按 05:00 重算(上升 双子座)。下面是本轮实际执行的技法,不能当作唯一分钟确认。",
unique_minute_claim: false,
confirmation_allowed: false,
},
},
});
assert.equal(mismatched?.representativeTime, "04:47");
assert.notEqual(mismatched?.houseTable?.time, "05:00");
assert.equal(mismatched?.houseTable, null);
assert.notEqual(mismatched?.natalRecast?.time, "05:00");
});
test("tool projection does not pair a 05:00 house table with a 04:47 representative time", () => {
const decision = decideRectification({
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: TIED,
discriminatorProbe: null,
});
const projection = latestResultToolProjection({
resultId: "55555555-5555-4555-8555-555555555555",
candidates: TIED.map((item, index) => ({
candidateId: `88888888-8888-4888-8888-88888888888${index}`,
time: item.time,
rank: index + 1,
relativeSupport: item.score,
tiedMinuteCount: 1,
})),
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:47",
selectedTime: null,
selectionKind: null,
algorithmVersion: "test",
decisionReceipt: {
house_table: { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") },
natal_recast: {
time: "05:00",
lagna: "双子座",
user_meaning: "本命宫位已按 05:00 重算(上升 双子座)。",
unique_minute_claim: false,
confirmation_allowed: false,
},
},
}, decision);
assert.notEqual(projection.house_table && (projection.house_table as { time?: string }).time, "05:00");
if (projection.representative_time === "04:47" && projection.house_table) {
assert.equal((projection.house_table as { time?: string }).time, "04:47");
}
});
test("collect focus is still not a renderable choice card", () => {
assert.equal(isRenderableChoiceOpenQuestion({
question_id: OCCUPATION_FOCUS.questionId,
prompt: "你长期做什么工作?",
status: "already_open",
kind: "collect_spoken",
intent: "collect_method_evidence",
domain: "occupation",
focus_id: "11111111-1111-4111-8111-111111111111",
probe_id: null,
}), false);
});