feat(consult): shared affliction-first reading, lists for every domain, health renamed
- T1 (BUG-1160): 「通用读法 · 先看受冲」 opens every turn's methodology once. - T2 (BUG-1161): Chinese lists for parents, children, education, migration, family, annual, general, health; health replaces the English route in the web consultation only. - T3 (BUG-1162): career / marriage / wealth each gain an affliction line. - T4 (BUG-1163): health label 健康, prompt asks about the constitution first. - Single-domain annual / timing turns keep the pre-brief method (CONSULT_READING_BUDGET_BLOCKED): they would exceed the 12,000-character assertions, which this brief may not change. 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:
co-authored by
Claude Opus 5.5
parent
509c88c436
commit
401796509a
@@ -0,0 +1,167 @@
|
||||
// TASK-consult-affliction-reading-20261001 T1–T4 (BUG-1160~1163): every
|
||||
// consultation turn opens with the shared "afflictions before dignities"
|
||||
// reading once; every domain but timing carries a local consultation list;
|
||||
// career / marriage / wealth gain an affliction line; health is named 健康 and
|
||||
// asks about the constitution first. Checklists may only cite field names the
|
||||
// evidence card really carries (hard red line 3), checked against the cards
|
||||
// built from the real-engine golden fixture.
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
CONDENSED_GENDER_LINES,
|
||||
CONDENSED_SHARED_READING_LINES,
|
||||
CONDENSED_SHARED_READING_TITLE,
|
||||
CONSULT_BRIEF_CHECKLIST_SOURCE,
|
||||
CONSULTATION_CONDENSED_CHECKLISTS,
|
||||
} from "../src/lib/consultation-condensed-checklist.ts";
|
||||
import {
|
||||
CONSULT_READING_BUDGET_BLOCKED,
|
||||
consultationMethodologyForDomains,
|
||||
} from "../src/lib/consultation-methodology.ts";
|
||||
import {
|
||||
consultationDomainDefinition,
|
||||
consultationDomainIds,
|
||||
type ConsultationDomain,
|
||||
} from "../src/lib/consultation-domain-registry.ts";
|
||||
import { buildEvidenceCard } from "../src/lib/consultation-evidence-card.ts";
|
||||
import { createConsultationPlan } from "../src/lib/consultation-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 titles = (domains: ConsultationDomain[]) => consultationMethodologyForDomains(domains)!.sections.map((section) => section.title);
|
||||
|
||||
test("T1: the shared reading is the first section, once per turn, whatever the plan", () => {
|
||||
for (const domain of consultationDomainIds.filter((id) => !CONSULT_READING_BUDGET_BLOCKED.has(id))) {
|
||||
const list = titles([domain]);
|
||||
assert.equal(list[0], CONDENSED_SHARED_READING_TITLE, domain);
|
||||
assert.equal(list.filter((title) => title === CONDENSED_SHARED_READING_TITLE).length, 1, domain);
|
||||
}
|
||||
for (const plan of [["parents", "children"], ["career", "health", "marriage"], ["annual", "parents"], ["timing", "career"]] as ConsultationDomain[][]) {
|
||||
const list = titles(plan);
|
||||
assert.equal(list[0], CONDENSED_SHARED_READING_TITLE, plan.join("+"));
|
||||
assert.equal(list.filter((title) => title === CONDENSED_SHARED_READING_TITLE).length, 1, plan.join("+"));
|
||||
}
|
||||
const text = consultationMethodologyForDomains(["parents"])!.sections[0]!.text;
|
||||
assert.ok(text.startsWith("1. 先看受冲,再看旺弱。"));
|
||||
assert.match(text, /两个及以上受冲信号/);
|
||||
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);
|
||||
});
|
||||
|
||||
test("T1/T2 budget blocker is pinned: a single annual or timing turn keeps its pre-brief method", () => {
|
||||
// BLOCKED on the 12,000-character ceiling (see CONSULT_READING_BUDGET_BLOCKED); this
|
||||
// test documents the gap so lifting it is a deliberate change.
|
||||
assert.deepEqual([...CONSULT_READING_BUDGET_BLOCKED].sort(), ["annual", "timing"]);
|
||||
assert.deepEqual(titles(["annual"]), ["Shared mandatory baseline"]);
|
||||
assert.deepEqual(consultationMethodologyForDomains(["annual"])!.domains_without_strict_checklist, ["annual"]);
|
||||
assert.deepEqual(titles(["timing"]), ["Shared mandatory baseline", "时运 · event-timing-strict"]);
|
||||
});
|
||||
|
||||
test("T2: every domain section, in order, and no domain without a list (annual pending the budget)", () => {
|
||||
const expected: Record<ConsultationDomain, string> = {
|
||||
career: "事业 · career-timing-strict · 普通对话精简清单",
|
||||
marriage: "关系 · relationship-timing-strict · 普通对话精简清单",
|
||||
wealth: "财富 · finance-timing-strict · 普通对话精简清单",
|
||||
health: "健康 · consult-health · 普通对话精简清单",
|
||||
education: "学习 · consult-education · 普通对话精简清单",
|
||||
migration: "迁居 · consult-migration · 普通对话精简清单",
|
||||
family: "家庭 · consult-family · 普通对话精简清单",
|
||||
parents: "父母 · consult-parents · 普通对话精简清单",
|
||||
children: "子女 · consult-children · 普通对话精简清单",
|
||||
annual: "",
|
||||
timing: "时运 · event-timing-strict",
|
||||
general: "综合 · consult-general · 普通对话精简清单",
|
||||
};
|
||||
for (const domain of consultationDomainIds) {
|
||||
const methodology = consultationMethodologyForDomains([domain])!;
|
||||
if (CONSULT_READING_BUDGET_BLOCKED.has(domain)) continue;
|
||||
assert.deepEqual(methodology.sections.map((section) => section.title), [
|
||||
CONDENSED_SHARED_READING_TITLE, "Shared mandatory baseline", expected[domain],
|
||||
], domain);
|
||||
assert.deepEqual(methodology.domains_without_strict_checklist, [], domain);
|
||||
}
|
||||
const mixed = consultationMethodologyForDomains(["parents", "children", "education", "migration", "family", "general"])!;
|
||||
assert.deepEqual(mixed.domains_without_strict_checklist, []);
|
||||
for (const domain of ["parents", "children", "education", "migration", "family", "general", "health"] as const) {
|
||||
const section = consultationMethodologyForDomains([domain])!.sections[2]!;
|
||||
assert.equal(section.source, CONSULT_BRIEF_CHECKLIST_SOURCE, domain);
|
||||
for (const line of CONSULTATION_CONDENSED_CHECKLISTS[domain]!) assert.ok(section.text.includes(line), `${domain}: ${line.slice(0, 20)}`);
|
||||
}
|
||||
const parents = CONSULTATION_CONDENSED_CHECKLISTS.parents!.join("\n");
|
||||
assert.match(parents, /不得写成「有分量」「站得住」/);
|
||||
assert.match(parents, /照顾你长大的可能不止一位/);
|
||||
assert.match(CONSULTATION_CONDENSED_CHECKLISTS.health!.join("\n"), /这不是医疗意见/);
|
||||
assert.match(CONSULTATION_CONDENSED_CHECKLISTS.children!.join("\n"), /孩子性别不写/);
|
||||
});
|
||||
|
||||
test("T3: career, marriage and wealth each carry their affliction line", () => {
|
||||
assert.ok(CONSULTATION_CONDENSED_CHECKLISTS.career!.some((line) => line.startsWith("AL 落第几宫要说出来")));
|
||||
assert.ok(CONSULTATION_CONDENSED_CHECKLISTS.marriage!.some((line) => line.includes("感情可能不止一段 / 可能有断续") && line.includes("不预设用户现在有伴侣或单身")));
|
||||
assert.ok(CONSULTATION_CONDENSED_CHECKLISTS.wealth!.some((line) => line.startsWith("8 宫也管版税、继承、意外之财")));
|
||||
for (const domain of ["career", "marriage", "wealth"] as const) {
|
||||
const lines = CONSULTATION_CONDENSED_CHECKLISTS[domain]!;
|
||||
assert.ok(lines.length >= 5 && lines.length <= 8, domain);
|
||||
}
|
||||
});
|
||||
|
||||
function cardKeys(): Set<string> {
|
||||
const keys = new Set<string>();
|
||||
const walk = (value: unknown) => {
|
||||
if (Array.isArray(value)) value.forEach(walk);
|
||||
else if (value && typeof value === "object") {
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
keys.add(key);
|
||||
walk(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const chart of golden.charts) {
|
||||
for (const domain of consultationDomainIds) {
|
||||
const workflow = domain === "annual" ? chart.annual_workflow : domain === "timing" ? chart.timing_workflow : chart.workflow;
|
||||
const context = toAgentConsultationContext(consultationWorkflowResponseSchema.parse(structuredClone(workflow)));
|
||||
const packet = toModelOutput(context, createConsultationPlan({ userIntent: "q", theme: domain }), domain);
|
||||
walk(buildEvidenceCard([{ domain, packet, context }]));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
test("hard red line 3: every field name the lists cite is a key on a real card", () => {
|
||||
const keys = cardKeys();
|
||||
const texts = [
|
||||
...CONDENSED_SHARED_READING_LINES,
|
||||
...Object.values(CONSULTATION_CONDENSED_CHECKLISTS).flat(),
|
||||
...Object.values(CONDENSED_GENDER_LINES),
|
||||
].join("\n");
|
||||
// Lower-case identifiers in these Chinese texts are field names; "blocked" is the
|
||||
// only prose word (KP 精确宫头仍 blocked). Static text scan, not intent detection.
|
||||
const prose = new Set(["blocked"]);
|
||||
const cited = new Set([...texts.matchAll(/(?<![A-Za-z0-9_])[a-z][a-z0-9_]*/g)].map((match) => match[0]).filter((word) => !prose.has(word)));
|
||||
for (const required of ["conjunct_with", "aspected_by", "combust", "lord_rows", "significator_rows", "saturn_from_moon_house", "hits", "functional"]) {
|
||||
assert.ok(cited.has(required), `the lists cite ${required}`);
|
||||
}
|
||||
for (const word of cited) assert.ok(keys.has(word), `cited field ${word} is on a card`);
|
||||
});
|
||||
|
||||
test("T4: health is 健康 and asks about the constitution first; its boundary and cap are unchanged", () => {
|
||||
const health = consultationDomainDefinition("health");
|
||||
assert.equal(health.label, "健康");
|
||||
assert.equal(health.prompt, "我的身体底子怎样,哪些方面要多留意?");
|
||||
assert.ok(health.aliases.includes("身心压力"));
|
||||
assert.equal(health.claimBoundary, "wellbeing_pressure_patterns_not_medical_diagnosis");
|
||||
assert.equal(health.confidenceCap, "low");
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import test from "node:test";
|
||||
|
||||
import {
|
||||
CONDENSED_CHECKLIST_SOURCE,
|
||||
CONSULT_BRIEF_CHECKLIST_SOURCE,
|
||||
CONSULTATION_CONDENSED_CHECKLISTS,
|
||||
} from "../src/lib/consultation-condensed-checklist.ts";
|
||||
import { consultationMethodologyForDomains, sharedConsultationMethodMarkdown } from "../src/lib/consultation-methodology.ts";
|
||||
@@ -75,10 +76,19 @@ async function writerPrompt(domain: "career" | "marriage" | "wealth") {
|
||||
}
|
||||
|
||||
test("each condensed list is 5–8 lines", () => {
|
||||
for (const [domain, lines] of Object.entries(CONSULTATION_CONDENSED_CHECKLISTS)) {
|
||||
assert.ok(lines!.length >= 5 && lines!.length <= 8, `${domain}: ${lines!.length} lines`);
|
||||
// 原值:全部清单 5–8 行,且只有 career / marriage / wealth 三份;
|
||||
// 新值:这三份仍 5–8 行;另有 TASK-consult-affliction-reading-20261001 定稿的 8 份(2–6 行,按任务书原文,不凑行数),
|
||||
// 只有 timing 没有本地清单;原因:该任务书 T2 / 决策 D5 补齐清单,定稿文本不得改意思。
|
||||
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`);
|
||||
}
|
||||
assert.deepEqual(Object.keys(CONSULTATION_CONDENSED_CHECKLISTS).sort(), ["career", "marriage", "wealth"]);
|
||||
for (const [domain, lines] of Object.entries(CONSULTATION_CONDENSED_CHECKLISTS)) {
|
||||
assert.ok(lines!.length >= 2 && lines!.length <= 8, `${domain}: ${lines!.length} lines`);
|
||||
}
|
||||
assert.deepEqual(Object.keys(CONSULTATION_CONDENSED_CHECKLISTS).sort(), [
|
||||
"annual", "career", "children", "education", "family", "general", "health", "marriage", "migration", "parents", "wealth",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the writer's prompt carries the condensed list with the three layers and the forbidden statements, not the long checklist", async () => {
|
||||
@@ -114,8 +124,12 @@ test("other routes still quote the live skill, and the bound system method is un
|
||||
const eventTiming = timing.sections.find((section) => section.title.includes("event-timing-strict"));
|
||||
assert.ok(eventTiming);
|
||||
assert.match(eventTiming.source, /references\/strict-workflow-router\.md$/);
|
||||
// 原值:health 段标题含 health-timing-strict、来源是 strict-workflow-router.md;
|
||||
// 新值:普通对话的 health 段是 consult-health 中文清单,来源是任务书,不再发路由英文段;
|
||||
// 原因:TASK-consult-affliction-reading-20261001 决策 D4/D5(报告面仍用路由,不受影响)。
|
||||
const health = consultationMethodologyForDomains(["health"])!;
|
||||
assert.match(health.sections.find((section) => section.title.includes("health-timing-strict"))!.source, /strict-workflow-router\.md$/);
|
||||
assert.equal(health.sections.some((section) => section.title.includes("health-timing-strict")), false);
|
||||
assert.equal(health.sections.find((section) => section.title.includes("consult-health"))!.source, CONSULT_BRIEF_CHECKLIST_SOURCE);
|
||||
for (const domain of consultationDomainIds.filter((id) => !(id in CONSULTATION_CONDENSED_CHECKLISTS))) {
|
||||
const methodology = consultationMethodologyForDomains([domain])!;
|
||||
assert.equal(methodology.sections.some((section) => section.source === CONDENSED_CHECKLIST_SOURCE), false, domain);
|
||||
|
||||
@@ -100,13 +100,18 @@ test("a stored session theme can never be parents or children (chat_sessions.the
|
||||
assert.equal(chatSessionCreateSchema.safeParse({ ...base, theme: "children" }).success, false);
|
||||
});
|
||||
|
||||
test("methodology names no strict checklist for parents or children instead of substituting one", () => {
|
||||
test("methodology gives parents and children their own consultation lists after the shared reading", () => {
|
||||
const methodology = consultationMethodologyForDomains(["parents"]);
|
||||
assert.ok(methodology);
|
||||
assert.deepEqual(methodology.domains_without_strict_checklist, ["parents"]);
|
||||
assert.equal(methodology.sections[0]?.title, "Shared mandatory baseline");
|
||||
assert.match(methodology.sections[0]?.text ?? "", /D7 for children, D12 for family/);
|
||||
assert.deepEqual(consultationMethodologyForDomains(["children"])?.domains_without_strict_checklist, ["children"]);
|
||||
// 原值:domains_without_strict_checklist = ["parents"] / ["children"],sections[0] 是 Shared mandatory baseline;
|
||||
// 新值:两者为 [],sections[0] 是「通用读法 · 先看受冲」,baseline 顺延到 sections[1];
|
||||
// 原因:TASK-consult-affliction-reading-20261001 T1(通用读法每轮最前)与决策 D6(父母、子女有本产品清单)。
|
||||
assert.deepEqual(methodology.domains_without_strict_checklist, []);
|
||||
assert.equal(methodology.sections[0]?.title, "通用读法 · 先看受冲");
|
||||
assert.equal(methodology.sections[1]?.title, "Shared mandatory baseline");
|
||||
assert.match(methodology.sections[1]?.text ?? "", /D7 for children, D12 for family/);
|
||||
assert.ok(methodology.sections.some((section) => section.title.includes("consult-parents")));
|
||||
assert.deepEqual(consultationMethodologyForDomains(["children"])?.domains_without_strict_checklist, []);
|
||||
});
|
||||
|
||||
test("receipts and the thinking plan show 父母 / 子女", () => {
|
||||
|
||||
@@ -79,7 +79,9 @@ test("general guided topics keep user-centered prompts and the same claim bounda
|
||||
"请帮我梳理目前的事业方向和下一步重点。",
|
||||
"请帮我看看我在关系中容易重复什么模式。",
|
||||
"请帮我分析目前的财富重点、风险和更稳妥的选择。",
|
||||
"请从非医疗诊断的角度,帮我看看近期的身心压力和调整重点。",
|
||||
// 原值「请从非医疗诊断的角度,帮我看看近期的身心压力和调整重点。」→ 新值如下;
|
||||
// 原因:TASK-consult-affliction-reading-20261001 D4 健康改口径,先讲体质底子。
|
||||
"请从非医疗诊断的角度,帮我看看我的身体底子怎样,哪些方面要多留意。",
|
||||
"请帮我看看我更适合怎样学习,以及如何安排下一步。",
|
||||
"请帮我分析现阶段是否适合迁居、置业或考虑海外发展。",
|
||||
"请帮我看看家庭关系中最需要处理的问题和责任边界。",
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
METHODOLOGY_SECTION_MAX_CHARS,
|
||||
METHODOLOGY_TOTAL_MAX_CHARS,
|
||||
} from "../src/lib/consultation-methodology.ts";
|
||||
import { CONDENSED_CHECKLIST_SOURCE } from "../src/lib/consultation-condensed-checklist.ts";
|
||||
import {
|
||||
CONDENSED_CHECKLIST_SOURCE,
|
||||
CONDENSED_SHARED_READING_TITLE,
|
||||
CONSULT_BRIEF_CHECKLIST_SOURCE,
|
||||
} from "../src/lib/consultation-condensed-checklist.ts";
|
||||
import { consultationDomainIds } from "../src/lib/consultation-domain-registry.ts";
|
||||
import { resolveLiveJyotishSkill } from "../src/lib/skill-package-registry.ts";
|
||||
|
||||
@@ -37,16 +41,25 @@ test("the route's own strict checklist reaches the answer, not just the package
|
||||
)).length,
|
||||
0,
|
||||
);
|
||||
const domainSections = methodology.sections.filter((section) => !section.title.includes("Shared mandatory baseline"));
|
||||
// 原值:只排除 Shared mandatory baseline;新值:同时排除每轮一次的「通用读法 · 先看受冲」;
|
||||
// 原因:TASK-consult-affliction-reading-20261001 T1 把通用读法放在 sections 最前面,它不是领域清单。
|
||||
const domainSections = methodology.sections.filter((section) => (
|
||||
!section.title.includes("Shared mandatory baseline") && section.title !== CONDENSED_SHARED_READING_TITLE
|
||||
));
|
||||
// Former: 2 (strict route + event judgment). New: 1 (condensed list). Reason: as above.
|
||||
assert.equal(domainSections.length, 1);
|
||||
assert.deepEqual(methodology.domains_without_strict_checklist, []);
|
||||
});
|
||||
|
||||
test("a route the skill declares no checklist for is reported, not filled in with another route's", () => {
|
||||
test("a route the skill declares no checklist for gets this product's own list, never another route's", () => {
|
||||
const methodology = consultationMethodologyForDomains(["education"]);
|
||||
assert.ok(methodology);
|
||||
assert.deepEqual(methodology.domains_without_strict_checklist, ["education"]);
|
||||
// 原值:["education"](路由没声明清单就报缺、不替代);新值:[],学业发本产品自己的中文清单;
|
||||
// 原因:TASK-consult-affliction-reading-20261001 决策 D6 对普通对话推翻「报缺不替代」。
|
||||
assert.deepEqual(methodology.domains_without_strict_checklist, []);
|
||||
const education = methodology.sections.find((section) => section.title.includes("consult-education"));
|
||||
assert.ok(education);
|
||||
assert.equal(education.source, CONSULT_BRIEF_CHECKLIST_SOURCE);
|
||||
assert.ok(
|
||||
!methodology.sections.some((section) => section.title.includes("timing-strict") || section.title.includes("verification-strict")),
|
||||
"no strict section may be substituted for a route that has none",
|
||||
@@ -63,12 +76,17 @@ test("a route the skill declares no checklist for is reported, not filled in wit
|
||||
);
|
||||
});
|
||||
|
||||
test("health uses the skill's health-timing-strict checklist", () => {
|
||||
test("health uses the consultation's Chinese health list in place of health-timing-strict", () => {
|
||||
const methodology = consultationMethodologyForDomains(["health"]);
|
||||
assert.ok(methodology);
|
||||
assert.deepEqual(methodology.domains_without_strict_checklist, []);
|
||||
const health = methodology.sections.find((section) => section.title.includes("health-timing-strict"));
|
||||
// 原值:标题含 health-timing-strict(路由英文清单);新值:consult-health 中文清单,来源是任务书;
|
||||
// 原因:TASK-consult-affliction-reading-20261001 决策 D4/D5,普通对话健康改口径(报告面不变)。
|
||||
assert.equal(methodology.sections.some((section) => section.title.includes("health-timing-strict")), false);
|
||||
const health = methodology.sections.find((section) => section.title.includes("consult-health"));
|
||||
assert.ok(health);
|
||||
assert.equal(health.source, CONSULT_BRIEF_CHECKLIST_SOURCE);
|
||||
assert.match(health.text, /体质底子/);
|
||||
assert.match(health.text, /D6/);
|
||||
assert.match(health.text, /D30/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user