feat(consult): career list reads field and public/behind-the-scenes from chart evidence (BUG-1176); opener keeps the affliction rule (BUG-1177)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-10-02 08:32:11 +08:00
co-authored by Claude Opus 5.5
parent c74bfcc06b
commit dd817c8eb8
7 changed files with 114 additions and 7 deletions
@@ -59,7 +59,10 @@ test("T1: the shared reading is the first section, once per turn, whatever the p
assert.match(text, /逆行不等于「反复、打回来」/);
// The line added on top of the brief's six: only hits are yogas this chart has.
assert.match(text, /瑜伽只认 yogas\.hits/);
assert.equal(CONDENSED_SHARED_READING_LINES.length, 7);
// 原值: 7 行
// 新值: 8 行(第 8 行:traditional_yogas 只说明力量,不单独下结论,不算受冲也不抵消受冲)
// 原因: TASK-consult-career-yoga-functional-20261002 T3 / D3 定稿,通用读法加一行(BUG-1175)
assert.equal(CONDENSED_SHARED_READING_LINES.length, 8);
});
// 原值: test "T1/T2 budget blocker is pinned: a single annual or timing turn keeps its pre-brief method"
@@ -126,9 +129,13 @@ test("T3: career, marriage and wealth each carry their affliction line", () => {
assert.ok(CONSULTATION_CONDENSED_CHECKLISTS.marriage!.some((line) => line.includes("感情这条线上有波折的迹象") && line.includes("不断定有几段") && line.includes("不预设用户现在有伴侣或单身")));
assert.ok(!CONSULTATION_CONDENSED_CHECKLISTS.marriage!.join("\n").includes("可能不止一段"));
assert.ok(CONSULTATION_CONDENSED_CHECKLISTS.wealth!.some((line) => line.startsWith("8 宫也管版税、继承、意外之财")));
// 原值: career / marriage / wealth 都是 5–8 行
// 新值: marriage / wealth 仍 5–8 行;career 5–9 行
// 原因: TASK-consult-career-yoga-functional-20261002 T4 定稿给事业清单加两行(行业、公众 / 幕后的依据,BUG-1176),定稿文本不得改意思
for (const domain of ["career", "marriage", "wealth"] as const) {
const lines = CONSULTATION_CONDENSED_CHECKLISTS[domain]!;
assert.ok(lines.length >= 5 && lines.length <= 8, domain);
const max = domain === "career" ? 9 : 8;
assert.ok(lines.length >= 5 && lines.length <= max, domain);
}
});
@@ -0,0 +1,92 @@
// TASK-consult-career-yoga-functional-20261002 T3–T5 (BUG-1175~1177): the
// closed-list traditional yogas reach the general / career / wealth cards with
// a fixed label, the shared reading says how to use them, the career list
// reads field and public/behind-the-scenes from chart evidence, and the opener
// keeps the affliction rule. Expected yoga names are the real engine's own
// layer in the 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,
EVIDENCE_CARD_LABELS,
EVIDENCE_CARD_SPECS,
} 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,
CONSULTATION_CONDENSED_CHECKLISTS,
} from "../src/lib/consultation-condensed-checklist.ts";
import { natalAnswerShapeBody } from "../src/lib/consultation-thinking-plan.ts";
import {
consultationWorkflowResponseSchema,
toAgentConsultationContext,
toModelOutput,
} from "../src/mastra/consultation-workflow.ts";
type Json = Record<string, unknown>;
type GoldenChart = { id: string; workflow: Json; annual_workflow: Json; timing_workflow: Json };
const golden = JSON.parse(readFileSync(
new URL("./fixtures/consult-evidence-card-golden.json", import.meta.url),
"utf8",
)) as { charts: GoldenChart[] };
const chartById = (id: string) => golden.charts.find((chart) => chart.id === id)!;
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 }]);
}
function engineNames(workflow: Json): string[] {
const layers = (workflow.chart as Json).consultation_native_layers as Json;
return ((layers.traditional_yogas as Json).names as string[]);
}
test("traditional yogas: fixed label, engine names, general / career / wealth only", () => {
assert.equal(EVIDENCE_CARD_LABELS.traditionalYogas, "传统格局,本站未验证,只作参考");
const withYogas = (Object.keys(EVIDENCE_CARD_SPECS) as ConsultationDomain[])
.filter((domain) => EVIDENCE_CARD_SPECS[domain].traditionalYogas);
assert.deepEqual(withYogas.sort(), ["career", "general", "wealth"]);
for (const chart of golden.charts) {
const names = engineNames(chart.workflow);
for (const domain of ["general", "career", "wealth"] as const) {
const section = card(chart.workflow, domain).domain_sections[domain]!;
if (names.length) assert.deepEqual(section.traditional_yogas, { label: EVIDENCE_CARD_LABELS.traditionalYogas, names }, `${chart.id}/${domain}`);
else assert.equal("traditional_yogas" in section, false, `${chart.id}/${domain}`);
}
assert.equal("traditional_yogas" in card(chart.workflow, "parents").domain_sections.parents!, false);
}
});
test("Obama carries Sasa and Neecha Bhanga Raja; Jobs and Taylor carry Gajakesari (engine names)", () => {
const general = (id: string) => (card(chartById(id).workflow, "general").domain_sections.general!.traditional_yogas as Json).names as string[];
assert.ok(general("barack_obama").includes("Sasa Yoga"));
assert.ok(general("barack_obama").includes("Neecha Bhanga Raja Yoga"));
assert.ok(general("steve_jobs").includes("Gajakesari Yoga"));
assert.ok(general("elizabeth_taylor").includes("Gajakesari Yoga"));
});
test("no formed closed-list yoga: the field is absent", () => {
const workflow = structuredClone(chartById("barack_obama").workflow);
((workflow.chart as Json).consultation_native_layers as Json).traditional_yogas = { status: "executed", names: [] };
assert.equal("traditional_yogas" in card(workflow, "general").domain_sections.general!, false);
});
test("shared reading: traditional yogas only show strength", () => {
assert.ok(CONDENSED_SHARED_READING_LINES.includes("`traditional_yogas` 只用来说明力量,不单独下结论,不算受冲,也不抵消受冲。"));
});
test("career list: field by planet nature, public only with chart evidence (T4 text)", () => {
const career = CONSULTATION_CONDENSED_CHECKLISTS.career!;
assert.ok(career.includes("行业看星的本性:10 宫里的星、10 宫主、AmK,以及 10 宫主在 D9 的定位星。太阳 = 权力、公职、管理;月亮 = 公众、照护、流动;火星 = 体力、竞技、工程、军警;水星 = 沟通、写作、商业、分析;木星 = 教育、法律、顾问、宗教;金星 = 艺术、表演、娱乐、美;土星 = 劳动、组织、长期工程;罗睺 = 新技术、跨界、海外、大众传播。先说最强的一两颗,不平均罗列。"));
assert.ok(career.includes("公众型还是幕后型要有依据:太阳或 10 宫主强且在角宫、AL 或 A10 落角宫、10 宫或命宫有入旺入庙星、`traditional_yogas` 有大格局,偏公众;10 宫主落 12 宫且无上述信号,才偏幕后。没有依据就不写这一句。不写「出头慢、越往后越稳、靠专业交付」这类人人适用的话,除非能指出具体盘面。"));
});
test("opener keeps the affliction rule (T5 text)", () => {
const body = natalAnswerShapeBody();
assert.ok(body.startsWith("开场不要标题,先用一到三句人话回答问题;开场同样守受冲规则:不替用户断定在不在身边、几段感情、有没有病,不写程度结论,只说哪条线顺、哪条线有压力迹象。不给这件事起名字,开场不写行动清单。"));
assert.equal(body.includes("第一句就是结论"), false);
});
@@ -79,12 +79,16 @@ test("each condensed list is 5–8 lines", () => {
// 原值:全部清单 5–8 行,且只有 career / marriage / wealth 三份;
// 新值:这三份仍 5–8 行;另有 TASK-consult-affliction-reading-20261001 定稿的 8 份(2–6 行,按任务书原文,不凑行数),
// 只有 timing 没有本地清单;原因:该任务书 T2 / 决策 D5 补齐清单,定稿文本不得改意思。
// 原值:career / marriage / wealth 5–8 行;新值:marriage / wealth 5–8 行、career 5–9 行;
// 原因:TASK-consult-career-yoga-functional-20261002 T4 定稿给事业清单加两行(BUG-1176),定稿文本不凑行数。
for (const domain of ["career", "marriage", "wealth"] as const) {
const lines = CONSULTATION_CONDENSED_CHECKLISTS[domain]!;
assert.ok(lines.length >= 5 && lines.length <= 8, `${domain}: ${lines.length} lines`);
const max = domain === "career" ? 9 : 8;
assert.ok(lines.length >= 5 && lines.length <= max, `${domain}: ${lines.length} lines`);
}
for (const [domain, lines] of Object.entries(CONSULTATION_CONDENSED_CHECKLISTS)) {
assert.ok(lines!.length >= 2 && lines!.length <= 8, `${domain}: ${lines!.length} lines`);
const max = domain === "career" ? 9 : 8;
assert.ok(lines!.length >= 2 && lines!.length <= max, `${domain}: ${lines!.length} lines`);
}
assert.deepEqual(Object.keys(CONSULTATION_CONDENSED_CHECKLISTS).sort(), [
"annual", "career", "children", "education", "family", "general", "health", "marriage", "migration", "parents", "wealth",