Engine by_time ledgers now include the full candidate set so posterior columns can look up fit and windows. Delivery turns keep up to three sentences after idle persist; the card subtracts repeated traits and folds the board chart. Co-authored-by: Cursor <cursoragent@cursor.com>
422 lines
16 KiB
TypeScript
422 lines
16 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
import { createElement } from "react";
|
||
import { renderToStaticMarkup } from "react-dom/server";
|
||
|
||
import { INFERENCE_ALGORITHM_VERSION, type InferenceCandidate, type InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
|
||
import {
|
||
buildRangeDelivery,
|
||
d10DivergenceLabel,
|
||
d9DivergenceLabel,
|
||
} from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
|
||
import { D10_TYPE_TABLE, D9_TYPE_TABLE } from "../src/lib/rectification-agentic/v9/varga-type-tables.ts";
|
||
import {
|
||
RECTIFICATION_USER_COPY,
|
||
REPRESENTATIVE_MINUTE_DISCLAIMER,
|
||
deliveryTurnNarration,
|
||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||
import { appendPostAdoptProspectiveWindows } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
|
||
import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx";
|
||
import type { RectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
|
||
|
||
const ID_A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1";
|
||
const ID_B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2";
|
||
|
||
function cand(
|
||
time: string,
|
||
range: readonly [string, string],
|
||
posterior = 20,
|
||
): InferenceCandidate {
|
||
return {
|
||
id: time,
|
||
time,
|
||
cluster_range: range,
|
||
prior_score: posterior,
|
||
posterior_score: posterior,
|
||
probability: 0.5,
|
||
status: "active",
|
||
rank: 1,
|
||
strong_conflict_count: 0,
|
||
};
|
||
}
|
||
|
||
function inference(overrides: Partial<InferenceState> = {}): InferenceState {
|
||
return {
|
||
algorithm_version: INFERENCE_ALGORITHM_VERSION,
|
||
candidate_set_id: "04:48-04:59:04:50,04:56",
|
||
revision: 1,
|
||
phase: "discrimination",
|
||
result_status: "credible_range",
|
||
range_start: "04:48",
|
||
range_end: "04:59",
|
||
candidates: [
|
||
cand("04:50", ["04:48", "04:53"]),
|
||
cand("04:56", ["04:54", "04:59"]),
|
||
],
|
||
events: [
|
||
{ id: "e1", domain: "career", year: 2018, precision: "month", usage: "training" },
|
||
{ id: "e2", domain: "education", year: 2016, precision: "year", usage: "training" },
|
||
],
|
||
probes: [],
|
||
answered_probes: [],
|
||
rounds: [],
|
||
entropy: 1,
|
||
representative_time: "04:50",
|
||
credible_range: ["04:48", "04:59"],
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function d9Probe() {
|
||
return {
|
||
id: "probe:varga.d9",
|
||
semantic_key: "varga.d9.天秤座/天蝎座",
|
||
candidate_split_hash: "varga.d9",
|
||
domain: "relationship",
|
||
year: 0,
|
||
question: "相处更像哪一种?",
|
||
candidate_ids: ["04:50", "04:56"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes" as const, supports: ["04:50"], conflicts: ["04:56"] },
|
||
{ answer_class: "weak_yes" as const, supports: ["04:56"], conflicts: ["04:50"] },
|
||
],
|
||
information_gain: 1,
|
||
source: "varga_contrast",
|
||
choice_kind: "varga_style" as const,
|
||
style_options: [
|
||
{ sign: "天秤座", label: D9_TYPE_TABLE.天秤座.userChoice, answer_class: "yes" as const },
|
||
{ sign: "天蝎座", label: D9_TYPE_TABLE.天蝎座.userChoice, answer_class: "weak_yes" as const },
|
||
],
|
||
};
|
||
}
|
||
|
||
function d10Probe() {
|
||
return {
|
||
id: "probe:varga.d10",
|
||
semantic_key: "varga.d10.巨蟹座/狮子座",
|
||
candidate_split_hash: "varga.d10",
|
||
domain: "career",
|
||
year: 0,
|
||
question: "做事更像哪一种?",
|
||
candidate_ids: ["04:50", "04:56"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes" as const, supports: ["04:50"], conflicts: ["04:56"] },
|
||
{ answer_class: "weak_yes" as const, supports: ["04:56"], conflicts: ["04:50"] },
|
||
],
|
||
information_gain: 1,
|
||
source: "varga_contrast",
|
||
choice_kind: "varga_style" as const,
|
||
style_options: [
|
||
{ sign: "巨蟹座", label: D10_TYPE_TABLE.巨蟹座.userChoice, answer_class: "yes" as const },
|
||
{ sign: "狮子座", label: D10_TYPE_TABLE.狮子座.userChoice, answer_class: "weak_yes" as const },
|
||
],
|
||
};
|
||
}
|
||
|
||
const publicCandidates = [
|
||
{ candidateId: ID_A, time: "04:50" },
|
||
{ candidateId: ID_B, time: "04:56" },
|
||
];
|
||
|
||
function deliveryResult(
|
||
delivery: ReturnType<typeof buildRangeDelivery>,
|
||
selectedTime: string | null = null,
|
||
): RectificationCandidateResult {
|
||
return {
|
||
resultId: "result-1",
|
||
candidates: delivery.columns.map((column, index) => ({
|
||
candidateId: column.candidate_id,
|
||
rank: index + 1,
|
||
time: column.time,
|
||
relativeSupport: 10,
|
||
tiedMinuteCount: 1,
|
||
})),
|
||
overallConfidence: "medium",
|
||
selectionAllowed: true,
|
||
canAdopt: true,
|
||
confirmationAllowed: false,
|
||
decisionReceipt: null,
|
||
representativeTime: delivery.representative_time,
|
||
selectedTime,
|
||
selectionKind: selectedTime ? "accepted" : null,
|
||
houseTable: null,
|
||
houseTablesByTime: {},
|
||
natalRecast: null,
|
||
techniqueAudit: [],
|
||
windowTransitions: [],
|
||
eventDashaLedger: [],
|
||
dashaAgreement: null,
|
||
lagnaContrast: null,
|
||
nakshatraBoundary: null,
|
||
precisionStage: null,
|
||
oosBlindPrompts: [],
|
||
confirmationGate: { confirmation_allowed: false } as RectificationCandidateResult["confirmationGate"],
|
||
validated: false,
|
||
completionStatus: null,
|
||
sessionOutcome: "adopt_representative",
|
||
credibleRange: delivery.range,
|
||
rangeDelivery: delivery,
|
||
verificationReportMarkdown: delivery.verification_markdown,
|
||
};
|
||
}
|
||
|
||
test("delivery columns rank by posterior and write D9/D10 on every column", () => {
|
||
// 原值: rows 代表分钟第一、差异句来自类型表、相同则空
|
||
// 新值: 至多三列,按后验排序,每列写全 D9/D10,不预标排盘用
|
||
// 原因: BUG-597 决策 1–2
|
||
const delivery = buildRangeDelivery({
|
||
inference: inference({
|
||
probes: [d9Probe(), d10Probe()],
|
||
candidates: [
|
||
{ ...cand("04:50", ["04:48", "04:53"]), probability: 0.55, posterior_score: 30 },
|
||
{ ...cand("04:56", ["04:54", "04:59"]), probability: 0.35, posterior_score: 20 },
|
||
],
|
||
}),
|
||
publicCandidates,
|
||
credibleRange: ["04:48", "04:59"],
|
||
representativeTime: "04:50",
|
||
});
|
||
assert.equal(delivery.columns[0]?.time, "04:50");
|
||
assert.equal(delivery.columns[0]?.probability_percent, 55);
|
||
assert.ok(delivery.columns[0]?.traits.d9.includes(d9DivergenceLabel("天秤座") ?? "missing"));
|
||
assert.ok(delivery.columns[0]?.traits.d10.includes(d10DivergenceLabel("巨蟹座") ?? "missing"));
|
||
assert.equal(delivery.columns[1]?.time, "04:56");
|
||
assert.ok(delivery.columns[1]?.traits.d9.includes(d9DivergenceLabel("天蝎座") ?? "missing"));
|
||
assert.ok(delivery.columns[1]?.traits.d10.includes(d10DivergenceLabel("狮子座") ?? "missing"));
|
||
assert.equal(delivery.event_count, 2);
|
||
assert.equal(delivery.boundary, REPRESENTATIVE_MINUTE_DISCLAIMER);
|
||
assert.equal(delivery.representative_candidate_id, ID_A);
|
||
assert.equal(delivery.more_count, 0);
|
||
});
|
||
|
||
test("same D9/D10 lifts the shared sentences to the card top", () => {
|
||
const sharedD9 = {
|
||
...d9Probe(),
|
||
candidate_ids: ["04:50", "04:51"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes" as const, supports: ["04:50", "04:51"], conflicts: [] },
|
||
{ answer_class: "weak_yes" as const, supports: [], conflicts: ["04:50", "04:51"] },
|
||
],
|
||
};
|
||
const sharedD10 = {
|
||
...d10Probe(),
|
||
candidate_ids: ["04:50", "04:51"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes" as const, supports: ["04:50", "04:51"], conflicts: [] },
|
||
{ answer_class: "weak_yes" as const, supports: [], conflicts: ["04:50", "04:51"] },
|
||
],
|
||
};
|
||
const delivery = buildRangeDelivery({
|
||
inference: inference({
|
||
candidates: [
|
||
{ ...cand("04:50", ["04:48", "04:53"]), probability: 0.6 },
|
||
{ ...cand("04:51", ["04:48", "04:53"]), probability: 0.4 },
|
||
],
|
||
probes: [sharedD9, sharedD10],
|
||
representative_time: "04:50",
|
||
}),
|
||
publicCandidates: [
|
||
{ candidateId: ID_A, time: "04:50" },
|
||
{ candidateId: ID_B, time: "04:51" },
|
||
],
|
||
credibleRange: ["04:50", "04:51"],
|
||
representativeTime: "04:50",
|
||
});
|
||
assert.equal(delivery.columns.length, 2);
|
||
assert.ok(delivery.shared_traits.some((line) => line.includes("两个时间的关系盘都在")));
|
||
assert.ok(delivery.shared_traits.some((line) => line.includes("两个时间的事业盘都在")));
|
||
assert.equal(delivery.columns[0]?.traits.d9, "");
|
||
assert.equal(delivery.columns[1]?.traits.d9, "");
|
||
assert.equal(delivery.columns[0]?.traits.d10, "");
|
||
assert.equal(delivery.columns[1]?.traits.d10, "");
|
||
});
|
||
|
||
test("range-delivery DOM is one button per row and adopted is aria-pressed", () => {
|
||
const delivery = buildRangeDelivery({
|
||
inference: inference({ probes: [d9Probe()] }),
|
||
publicCandidates,
|
||
credibleRange: ["04:48", "04:59"],
|
||
representativeTime: "04:50",
|
||
verificationMarkdown: "| 方法 | 结果 |\n| --- | --- |\n| 1 | 吻合 |",
|
||
});
|
||
const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, {
|
||
result: deliveryResult(delivery, "04:50"),
|
||
acceptingCandidateId: null,
|
||
readonly: false,
|
||
onAccept: () => undefined,
|
||
}));
|
||
assert.equal([...html.matchAll(/<button\b/g)].length, delivery.columns.length);
|
||
assert.match(html, /aria-pressed="true"/);
|
||
assert.match(html, /查看验证报告/);
|
||
assert.match(html, /更像这个|已采用/);
|
||
assert.doesNotMatch(html, / open[=> ]/);
|
||
assert.doesNotMatch(html, /按这个范围用/);
|
||
assert.doesNotMatch(html, /再补一件经历/);
|
||
assert.doesNotMatch(html, /都不像/);
|
||
assert.doesNotMatch(html, /说不好/);
|
||
assert.doesNotMatch(html, /排盘用/);
|
||
const source = readFileSync(new URL("../src/components/rectification-range-delivery.tsx", import.meta.url), "utf8");
|
||
assert.match(source, /onAccept\(column\.candidate_id\)/);
|
||
assert.match(source, /aria-pressed=\{adopted\}/);
|
||
});
|
||
|
||
test("the range-delivery component does not render relative support or the old two actions", () => {
|
||
const source = readFileSync(new URL("../src/components/rectification-range-delivery.tsx", import.meta.url), "utf8");
|
||
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
|
||
assert.doesNotMatch(source, /相对支持度/);
|
||
assert.doesNotMatch(source, /当前推荐/);
|
||
assert.doesNotMatch(source, /采用此时间/);
|
||
assert.doesNotMatch(source, /排盘用/);
|
||
// 原值: 不含 rangeDeliveryAdopt / rangeDeliveryContinue;`Adopt[^e]` 会误伤 rangeDeliveryAdopting
|
||
// 新值: 只禁旧标识符单词本身
|
||
// 原因: BUG-595 决策 1
|
||
assert.doesNotMatch(source, /\brangeDeliveryAdopt\b|\brangeDeliveryContinue\b/);
|
||
assert.doesNotMatch(chat, /相对支持度/);
|
||
assert.doesNotMatch(chat, /RectificationCandidateCards/);
|
||
assert.match(chat, /<RectificationRangeDelivery/);
|
||
assert.match(source, /rangeDeliveryAdopting/);
|
||
assert.match(source, /rangeDeliveryMoreLikeThis/);
|
||
assert.match(source, /RectificationVerificationReport/);
|
||
});
|
||
|
||
test("chat pins the card on the offering message without a dismissed-range continue hint", () => {
|
||
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
|
||
assert.match(chat, /showSelectionCards && candidateResult && message\.renderKey === selectionCardMessageKey/);
|
||
assert.doesNotMatch(chat, /dismissedRangeResultId/);
|
||
assert.doesNotMatch(chat, /RangeDeliveryContinueHint/);
|
||
assert.doesNotMatch(chat, /rectification-composer-meta/);
|
||
});
|
||
|
||
const CAREER_WINDOW_PROBE = {
|
||
candidate_label: "A",
|
||
domain: "career",
|
||
window_label: "2027 年 3 月附近",
|
||
user_meaning: "候选 A 预测下一次事业变动更可能在 2027 年 3 月附近。这是预测窗口,不是承诺;下次发生时回来补一条,可进一步分辨。",
|
||
used_for_scoring: false,
|
||
};
|
||
|
||
test("post-adopt first-round narration appends prospective windows when present", () => {
|
||
const base = "2013 年前后,钱上有没有明显变化?";
|
||
const withWindows = appendPostAdoptProspectiveWindows(base, {
|
||
prospective_probes: [CAREER_WINDOW_PROBE],
|
||
});
|
||
assert.match(withWindows, /预测下一次事业变动/);
|
||
assert.match(withWindows, /2013 年前后/);
|
||
const without = appendPostAdoptProspectiveWindows(base, { prospective_probes: [] });
|
||
assert.equal(without, base);
|
||
const idle = readFileSync(
|
||
new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const start = idle.indexOf("export async function persistNextInterviewIfIdle");
|
||
const slice = idle.slice(start, start + 1800);
|
||
assert.match(slice, /appendPostAdoptProspectiveWindows/);
|
||
assert.match(slice, /dossier\.case\.acceptedTime/);
|
||
});
|
||
|
||
test("delivery turn narration is three sentences without eight-method tables", () => {
|
||
const text = deliveryTurnNarration({
|
||
credibleRange: ["04:51", "04:59"],
|
||
representativeTime: "04:53",
|
||
eventCount: 7,
|
||
fitPercent: 80,
|
||
});
|
||
const sentences = text.split(/(?<=。)/).filter(Boolean);
|
||
assert.equal(sentences.length, 3);
|
||
assert.doesNotMatch(text, /方法1|Technique Audit/);
|
||
});
|
||
|
||
function ledgerRow(
|
||
match: "strong" | "medium" | "weak" | "none",
|
||
yearMonth: string,
|
||
domain: string,
|
||
) {
|
||
return {
|
||
summary: `${domain} ${yearMonth}`,
|
||
match,
|
||
match_label: match,
|
||
user_meaning: `${domain} ${yearMonth}`,
|
||
gochara: null,
|
||
domain,
|
||
year_month: yearMonth,
|
||
};
|
||
}
|
||
|
||
test("posterior top three still get fit when engine score order differs", () => {
|
||
const ID_C = "cccccccc-cccc-4ccc-8ccc-ccccccccccc3";
|
||
const delivery = buildRangeDelivery({
|
||
inference: inference({
|
||
candidates: [
|
||
{ ...cand("04:49", ["04:47", "04:53"]), probability: 0.51, posterior_score: 51 },
|
||
{ ...cand("04:47", ["04:47", "04:53"]), probability: 0.24, posterior_score: 24 },
|
||
{ ...cand("04:53", ["04:47", "04:53"]), probability: 0.24, posterior_score: 24 },
|
||
],
|
||
representative_time: "04:49",
|
||
range_start: "04:47",
|
||
range_end: "04:53",
|
||
credible_range: ["04:47", "04:53"],
|
||
}),
|
||
publicCandidates: [
|
||
{ candidateId: ID_A, time: "04:49" },
|
||
{ candidateId: ID_B, time: "04:47" },
|
||
{ candidateId: ID_C, time: "04:53" },
|
||
],
|
||
credibleRange: ["04:47", "04:53"],
|
||
representativeTime: "04:49",
|
||
eventDashaLedgerByTime: {
|
||
"04:47": [ledgerRow("strong", "2016-09", "education"), ledgerRow("none", "2024-05", "relationship")],
|
||
"04:49": [ledgerRow("strong", "2016-09", "education"), ledgerRow("medium", "2018-03", "career")],
|
||
"04:53": [ledgerRow("weak", "2016-09", "education"), ledgerRow("none", "2024-05", "relationship")],
|
||
},
|
||
prospectiveWindowsByTime: {
|
||
"04:47": [{ domain: "career", from: "2027-03", to: "2027-03" }],
|
||
"04:49": [{ domain: "career", from: "2027-03", to: "2027-03" }],
|
||
"04:53": [],
|
||
},
|
||
});
|
||
assert.equal(delivery.columns.map((column) => column.time).join(","), "04:49,04:47,04:53");
|
||
assert.ok(delivery.columns.every((column) => column.fit != null));
|
||
assert.ok(delivery.columns.every((column) => column.windows != null));
|
||
assert.match(delivery.columns[0]?.fit_line ?? "", /2 件经历里 2 件对得上/);
|
||
assert.match(delivery.columns[1]?.fit_line ?? "", /最不合的是 2024 年 5 月那段感情/);
|
||
assert.equal(delivery.columns[2]?.window_line, RECTIFICATION_USER_COPY.rangeDeliveryNoWindow);
|
||
});
|
||
|
||
test("missing by_time keys stay null and render 还没对照, never 0 · 0 · 0", () => {
|
||
const delivery = buildRangeDelivery({
|
||
inference: inference({
|
||
probes: [d9Probe()],
|
||
candidates: [
|
||
{ ...cand("04:50", ["04:48", "04:59"]), probability: 0.6 },
|
||
{ ...cand("04:56", ["04:48", "04:59"]), probability: 0.4 },
|
||
],
|
||
}),
|
||
publicCandidates,
|
||
credibleRange: ["04:48", "04:59"],
|
||
representativeTime: "04:50",
|
||
eventDashaLedgerByTime: {
|
||
"04:50": [ledgerRow("strong", "2016-09", "education")],
|
||
},
|
||
prospectiveWindowsByTime: {
|
||
"04:50": [{ domain: "career", from: "2027-03", to: "2027-03" }],
|
||
},
|
||
});
|
||
assert.ok(delivery.columns[0]?.fit != null);
|
||
assert.equal(delivery.columns[1]?.fit, null);
|
||
assert.equal(delivery.columns[1]?.windows, null);
|
||
assert.equal(delivery.columns[1]?.fit_line, RECTIFICATION_USER_COPY.rangeDeliveryNotCompared);
|
||
const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, {
|
||
result: deliveryResult(delivery),
|
||
acceptingCandidateId: null,
|
||
readonly: false,
|
||
onAccept: () => undefined,
|
||
}));
|
||
assert.match(html, /这一分钟还没对照/);
|
||
assert.doesNotMatch(html, /0 · 0 · 0/);
|
||
assert.doesNotMatch(html, /强相关/);
|
||
assert.doesNotMatch(html, /<h3/);
|
||
assert.doesNotMatch(html, /经历对照/);
|
||
assert.doesNotMatch(html, /往后 12 个月/);
|
||
});
|