Product 2026-10-07: the answer voice becomes a knowing, gentle older sister, matching the homepage greeting, and may read feelings: after the answer, one sentence, with 可能 / 多半, never a guess stated as fact or a question rewritten into a need; another person's feelings need chart evidence and only on an unafflicted line (afflicted lines keep the range rule, BUG-1182). Overturns the feelings part of BUG-1070. Also removes rules that contradicted the answer shape: 'restate the same constraint', the rectification 'say progress' line, 'answer concisely', and 'every time say what to do' (now only when things are hard). Full suite 4969 / fail 24, identical to 52d14e8f; tsc 0; lint 0 errors; build keeps / Static, first-load gzip unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
91 lines
6.1 KiB
TypeScript
91 lines
6.1 KiB
TypeScript
// PROGRESS-consult-upstream-functional-v2-20261002: the evidence cards carry the
|
|
// upstream functional profile v2 (BUG-1158), and the round-4 backtest residue is
|
|
// closed in the single-definition rules (BUG-1176 career placements, BUG-1182
|
|
// parents portrait / second ask, BUG-1165 retrograde). Expected functional roles
|
|
// come from the real engine golden (scripts/research/capture_consult_evidence_card_golden.py).
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import { buildEvidenceCard } from "../src/lib/consultation-evidence-card.ts";
|
|
import type { ConsultationDomain } from "../src/lib/consultation-domain-registry.ts";
|
|
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
|
|
import { CONDENSED_SHARED_READING_LINES } from "../src/lib/consultation-condensed-checklist.ts";
|
|
import { AFFLICTION_RANGE_RULE, CAREER_FIELD_ASK_RULE } from "../src/lib/consultation-thinking-plan.ts";
|
|
import {
|
|
consultationWorkflowResponseSchema,
|
|
toAgentConsultationContext,
|
|
toModelOutput,
|
|
} from "../src/mastra/consultation-workflow.ts";
|
|
import { productConversationVoice } from "../src/mastra/product-voice.ts";
|
|
|
|
type Json = Record<string, unknown>;
|
|
const golden = JSON.parse(readFileSync(
|
|
new URL("./fixtures/consult-evidence-card-golden.json", import.meta.url),
|
|
"utf8",
|
|
)) as { charts: { id: string; workflow: Json }[] };
|
|
const backtest = JSON.parse(readFileSync(
|
|
new URL("./fixtures/consult-biography-backtest-golden.json", import.meta.url),
|
|
"utf8",
|
|
)) as { figures: { id: string; shared_chart: { modules: { functional_benefic_malefic: Json } } }[] };
|
|
|
|
function card(workflow: Json, domain: ConsultationDomain) {
|
|
const context = toAgentConsultationContext(consultationWorkflowResponseSchema.parse(structuredClone(workflow)));
|
|
const packet = toModelOutput(context, createConsultationPlan({ userIntent: "q", theme: domain }), domain);
|
|
return buildEvidenceCard([{ domain, packet, context }]);
|
|
}
|
|
|
|
test("goldens carry functional profile v2 from the real engine (BUG-1158)", () => {
|
|
const layers = [
|
|
...golden.charts.map((chart) => (chart.workflow.chart as Json).modules as Json).map((modules) => modules.functional_benefic_malefic as Json),
|
|
...backtest.figures.map((figure) => figure.shared_chart.modules.functional_benefic_malefic),
|
|
];
|
|
assert.equal(layers.length, 12);
|
|
for (const layer of layers) assert.equal(layer.profile, "bphs_ch34_general_with_sign_exceptions_v2");
|
|
// Scorpio (Taylor): explicit exceptions Mars mixed, Sun yoga-supporting; Venus (7/12) no longer malefic by the 12th alone.
|
|
const taylor = card(golden.charts.find((chart) => chart.id === "elizabeth_taylor")!.workflow, "career");
|
|
const functional = taylor.base.natal.functional_benefic_malefic as Json;
|
|
assert.deepEqual(functional.functional_benefics, ["Jupiter", "Moon", "Sun"]);
|
|
assert.deepEqual(functional.functional_malefics, ["Mercury", "Saturn"]);
|
|
assert.deepEqual(functional.functional_neutrals, ["Mars", "Venus"]);
|
|
assert.deepEqual(functional.yogakarakas, []);
|
|
// Virgo (Jobs): Jupiter challenging by exception, Sun (sole 12th lord) neutral.
|
|
const jobs = card(golden.charts.find((chart) => chart.id === "steve_jobs")!.workflow, "parents");
|
|
const jobsFunctional = jobs.base.natal.functional_benefic_malefic as Json;
|
|
assert.deepEqual(jobsFunctional.functional_malefics, ["Jupiter", "Mars", "Moon"]);
|
|
assert.deepEqual(jobsFunctional.functional_neutrals, ["Sun"]);
|
|
});
|
|
|
|
test("career: weak placements of A10 / AL / Sun / 10th lord are resistance, not a behind-the-scenes verdict (BUG-1176)", () => {
|
|
for (const phrase of [
|
|
"A10、AL、太阳、10 宫主落 4、6、8、12 宫只说这条线的阻力或代价",
|
|
"不翻成「不在台前」「不在台面上」「不靠曝光」「幕后」「不是站台式」「往深处做」",
|
|
"也不把落宫翻成行业",
|
|
]) assert.ok(CAREER_FIELD_ASK_RULE.includes(phrase), phrase);
|
|
// Still the only ask of the answer.
|
|
assert.ok(CAREER_FIELD_ASK_RULE.includes("这一句就是全篇唯一一处请用户说实际情况"));
|
|
});
|
|
|
|
test("afflicted person: no portrait of what they do, no presupposing action, one ask (BUG-1182)", () => {
|
|
for (const phrase of [
|
|
"不拿宫主落哪一宫、宫里有吉星去描写这个人对你做什么",
|
|
"「在管」「在安排」「心思在你的前途上」「照顾是实的」都预设人在身边",
|
|
"这一方的行动建议也不写给他打电话、去问他",
|
|
"问过之后别的段落不再另问",
|
|
]) assert.ok(AFFLICTION_RANGE_RULE.includes(phrase), phrase);
|
|
// The examples only show "near / cares about your future" for an unafflicted mother line.
|
|
// 原值: /盘上有证据、且这条线没有受冲时可以写「4 宫主落 10 宫,母亲的注意力在你的前途上」(受冲两个及以上按受冲规则写,不写这一句)/
|
|
// 新值: 读心第 3 条:暖的一面只在没有受冲时写,受冲两个及以上只给范围、不写暖心定论
|
|
// 原因: 2026-10-07 产品定知心姐姐口吻、放开读心(BUG-1255,推翻 BUG-1070 不揣测的读心部分);受冲口径(BUG-1182)不变
|
|
assert.match(productConversationVoice, /只在这条线没有受冲时写暖的那一面/);
|
|
assert.match(productConversationVoice, /受冲两个及以上按受冲规则只给范围(例:「他心里可能有距离,也可能只是不会表达。」),不写「其实很在意你」这类暖心定论/);
|
|
assert.match(productConversationVoice, /她离你近,对你的事上心,你们之间来往多。(母亲这条线没有受冲;4 宫主落 10 宫;月亮在 10 宫)/);
|
|
assert.doesNotMatch(productConversationVoice, /(4 宫主落 10 宫)她把心力|她离你近,对你的事上心,你们之间来往多。(4 宫主落 10 宫/);
|
|
});
|
|
|
|
test("retrograde is never the reason for 'drags on' either (BUG-1165 recurrence)", () => {
|
|
const line = CONDENSED_SHARED_READING_LINES.find((text) => text.includes("逆行不等于"));
|
|
assert.ok(line);
|
|
assert.match(line, /逆行不等于「反复、打回来」,也不等于「拖得久、拖出来」,推论里不拿逆行当理由(依据里可以列)/);
|
|
});
|