feat(consult): split family into parents and children domains (engine still family)
parents (D12, 4/9 houses, Sun/Moon) and children (D7, 5th house, Jupiter, PK) are plan/card domains with Chinese labels and the brief's aliases. They send the family route contract to the engine and are rejected as a stored session theme, so neither Python nor the chat_sessions.theme CHECK changes. Methodology reports no strict checklist for them; must-use layers follow the plan domain. Tests with 原值/新值/原因 where assertions changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
75a1844cdf
commit
5c65834dda
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { consultationDomainIds } from "../src/lib/consultation-domain-registry.ts";
|
||||
import { consultationDomainIds, consultationEngineDomainIds } from "../src/lib/consultation-domain-registry.ts";
|
||||
|
||||
const migration = readFileSync(
|
||||
new URL("../supabase/migrations/20260814050000_consultation_domain_registry.sql", import.meta.url),
|
||||
@@ -16,7 +16,14 @@ function themeConstraintValues(sql: string) {
|
||||
}
|
||||
|
||||
test("chat_sessions persists exactly the canonical consultation domains", () => {
|
||||
assert.deepEqual(themeConstraintValues(migration), [...consultationDomainIds]);
|
||||
// 原值:CHECK 值 == consultationDomainIds。
|
||||
// 新值:CHECK 值 == consultationEngineDomainIds(会话主题 / 引擎领域);parents、children 只在数据卡里区分,不落库。
|
||||
// 原因:TASK-consult-evidence-card-20260927 D2,不改数据库。
|
||||
assert.deepEqual(themeConstraintValues(migration), [...consultationEngineDomainIds]);
|
||||
assert.deepEqual(
|
||||
consultationDomainIds.filter((domain) => !(consultationEngineDomainIds as readonly string[]).includes(domain)),
|
||||
["parents", "children"],
|
||||
);
|
||||
});
|
||||
|
||||
test("chat_sessions does not persist aliases or independent product routes", () => {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// TASK-consult-evidence-card-20260927 T2 (D2): 「家庭」拆成「父母」「子女」。
|
||||
//
|
||||
// parents / children are consultation domains the model can pick and the card
|
||||
// can tell apart, while the engine keeps calculating them as `family`: the
|
||||
// engine validates the route contract exactly (consultation_plan_contract.py),
|
||||
// and chat_sessions.theme has a CHECK on the original ten themes.
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
consultationDomainDefinition,
|
||||
consultationDomainPlanValues,
|
||||
consultationEngineDomain,
|
||||
normalizeConsultationDomain,
|
||||
validateConsultationDomainPlan,
|
||||
} from "../src/lib/consultation-domain-registry.ts";
|
||||
import { projectConsultationWorkflowRequest } from "../src/lib/consultation-workflow-request.ts";
|
||||
import { createConsultationPlan } from "../src/lib/consultation-plan.ts";
|
||||
import { consultationMethodologyForDomains } from "../src/lib/consultation-methodology.ts";
|
||||
import { natalConsultationThinkingPlan } from "../src/lib/consultation-thinking-plan.ts";
|
||||
import { workflowReceiptSchema } from "../src/lib/consultation-agent-events.ts";
|
||||
import { chatSessionCreateSchema } from "../src/lib/chat-session-write-contract.ts";
|
||||
import {
|
||||
consultationWorkflowResponseSchema,
|
||||
toAgentConsultationContext,
|
||||
toModelOutput,
|
||||
} from "../src/mastra/consultation-workflow.ts";
|
||||
import {
|
||||
canonicalDomainPlan,
|
||||
createConsultationRuntimeState,
|
||||
createConsultationTools,
|
||||
} from "../src/mastra/consultation-tools.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: Array<{ id: string; workflow: Json }> };
|
||||
const jobsWorkflow = golden.charts.find((chart) => chart.id === "steve_jobs")!.workflow;
|
||||
|
||||
test("parents and children are registered with Chinese labels and run as family on the engine", () => {
|
||||
const parents = consultationDomainDefinition("parents");
|
||||
const children = consultationDomainDefinition("children");
|
||||
assert.equal(parents.label, "父母");
|
||||
assert.equal(children.label, "子女");
|
||||
for (const definition of [parents, children]) {
|
||||
assert.equal(definition.strictWorkflowRoute, "family");
|
||||
assert.deepEqual([...definition.workflowThemes], ["family"]);
|
||||
}
|
||||
assert.equal(consultationEngineDomain("parents"), "family");
|
||||
assert.equal(consultationEngineDomain("children"), "family");
|
||||
assert.equal(consultationEngineDomain("career"), "career");
|
||||
assert.ok(parents.requiredLayers.includes("D12"));
|
||||
assert.ok(children.requiredLayers.includes("D7"));
|
||||
});
|
||||
|
||||
test("the brief's aliases resolve to parents and children; the household question stays family", () => {
|
||||
for (const alias of ["父母", "父亲", "母亲", "爸爸", "妈妈", "爸妈", "parents"]) {
|
||||
assert.equal(normalizeConsultationDomain(alias), "parents", alias);
|
||||
}
|
||||
for (const alias of ["子女", "孩子", "儿子", "女儿", "children"]) {
|
||||
assert.equal(normalizeConsultationDomain(alias), "children", alias);
|
||||
}
|
||||
assert.equal(normalizeConsultationDomain("家庭"), "family");
|
||||
assert.equal(normalizeConsultationDomain("family"), "family");
|
||||
// The model-facing schema enumerates the new ids and aliases.
|
||||
for (const value of ["parents", "children", "父母", "孩子", "family", "家庭"]) {
|
||||
assert.ok(consultationDomainPlanValues.includes(value), value);
|
||||
}
|
||||
});
|
||||
|
||||
test("mixed plans keep the two-domain rule: children with education, parents deduplicated", () => {
|
||||
// 「孩子学业」:the model lists both domains; the server canonicalizes aliases.
|
||||
assert.deepEqual(validateConsultationDomainPlan(["孩子", "学习"]), ["children", "education"]);
|
||||
assert.deepEqual(validateConsultationDomainPlan(["爸妈", "父母", "parents"]), ["parents"]);
|
||||
assert.deepEqual(validateConsultationDomainPlan(["父母", "子女", "家庭"]), ["parents", "children", "family"]);
|
||||
assert.deepEqual(canonicalDomainPlan({ domains: ["父母"] }, {}), ["parents"]);
|
||||
});
|
||||
|
||||
test("the engine request for parents and children is exactly the family route contract", () => {
|
||||
const familyPlan = createConsultationPlan({ userIntent: "q", theme: "family" });
|
||||
const family = projectConsultationWorkflowRequest("q", "family", familyPlan);
|
||||
for (const domain of ["parents", "children"] as const) {
|
||||
const plan = createConsultationPlan({ userIntent: "q", theme: domain });
|
||||
assert.deepEqual([...plan.requestedDomains], ["family"], domain);
|
||||
const request = projectConsultationWorkflowRequest("q", domain, plan);
|
||||
assert.deepEqual(request.themes, family.themes, domain);
|
||||
assert.equal(request.strictWorkflowRoute, "family", domain);
|
||||
assert.deepEqual(request.requiredLayers, family.requiredLayers, domain);
|
||||
assert.equal(request.claimBoundary, family.claimBoundary, domain);
|
||||
assert.deepEqual(request.requested_domains, family.requested_domains, domain);
|
||||
}
|
||||
});
|
||||
|
||||
test("a stored session theme can never be parents or children (chat_sessions.theme CHECK)", () => {
|
||||
const base = { id: "00000000-0000-4000-8000-000000000001", title: "t", model_id: "m", messages: [], session_type: "consultation", rectification_case_id: null };
|
||||
assert.equal(chatSessionCreateSchema.safeParse({ ...base, theme: "family" }).success, true);
|
||||
assert.equal(chatSessionCreateSchema.safeParse({ ...base, theme: "parents" }).success, false);
|
||||
assert.equal(chatSessionCreateSchema.safeParse({ ...base, theme: "children" }).success, false);
|
||||
});
|
||||
|
||||
test("methodology names no strict checklist for parents or children instead of substituting one", () => {
|
||||
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"]);
|
||||
});
|
||||
|
||||
test("receipts and the thinking plan show 父母 / 子女", () => {
|
||||
assert.equal(workflowReceiptSchema.safeParse({
|
||||
route: "multi-domain", status: "ready", preciseTiming: "allowed", missingLayers: [], domains: ["parents", "children"],
|
||||
}).success, true);
|
||||
const labels = natalConsultationThinkingPlan({ domains: ["parents", "children"] })
|
||||
.flatMap((section) => section.steps.map((step) => step.label));
|
||||
assert.ok(labels.some((label) => label.startsWith("父母:")));
|
||||
assert.ok(labels.some((label) => label.startsWith("子女:")));
|
||||
});
|
||||
|
||||
test("must-use layers follow the plan's domain, not the engine's family route", () => {
|
||||
const context = toAgentConsultationContext(consultationWorkflowResponseSchema.parse(structuredClone(jobsWorkflow)));
|
||||
assert.equal(context.evidence_contract.route, "family");
|
||||
const packet = toModelOutput(context, createConsultationPlan({ userIntent: "q", theme: "parents" }), "parents");
|
||||
const mustUse = packet.evidence_contract.must_use_layers as string[];
|
||||
assert.ok(mustUse.includes("Formal Vargas D1–D60"));
|
||||
assert.ok(mustUse.includes("Narayana Dasha"));
|
||||
});
|
||||
|
||||
test("the calculation tool runs a parents plan as family and reports parents in the receipt", async () => {
|
||||
const calls: Array<{ theme: string; requested: readonly string[] }> = [];
|
||||
const state = createConsultationRuntimeState();
|
||||
const serverChart = {
|
||||
name: "测试",
|
||||
toolInput: { year: 1955, month: 2, day: 24, hour: 19, minute: 15, city: "San Francisco", lat: 37.77, lon: -122.42, tz: -8, ayanamsa: "raman" },
|
||||
truth: { birthTimeSource: "reported" },
|
||||
};
|
||||
const tools = createConsultationTools({
|
||||
userId: "u", sessionId: "s", requestId: "r-parents", consultationMode: "verified_chart",
|
||||
serverChart: serverChart as never, state,
|
||||
runWorkflow: async (input, options) => {
|
||||
calls.push({ theme: input.theme, requested: options?.plan?.requestedDomains ?? [] });
|
||||
return structuredClone(jobsWorkflow) as never;
|
||||
},
|
||||
});
|
||||
await tools["run-jyotish-consultation"].execute!(
|
||||
{ question: "我和父母关系如何", domains: ["父母"] } as never,
|
||||
{ writer: { custom: async () => {} } } as never,
|
||||
);
|
||||
assert.deepEqual(calls, [{ theme: "parents", requested: ["family"] }]);
|
||||
assert.deepEqual(state.workflowReceipt?.domains, ["parents"]);
|
||||
assert.equal(state.workflowReceipt?.route, "family");
|
||||
});
|
||||
@@ -14,7 +14,11 @@ import {
|
||||
generalGuidedJyotishTopics,
|
||||
} from "../src/lib/guided-jyotish-topics.ts";
|
||||
|
||||
const expected = ["career", "marriage", "wealth", "health", "education", "migration", "family", "annual", "timing", "general"];
|
||||
// 原值:十个领域,无 parents / children。
|
||||
// 新值:family 后面加 parents、children(数据卡子领域,引擎侧按 family 计算)。
|
||||
// 原因:TASK-consult-evidence-card-20260927 D2。会话主题(chat_sessions.theme CHECK)仍是原十个,见 sessionThemes。
|
||||
const expected = ["career", "marriage", "wealth", "health", "education", "migration", "family", "parents", "children", "annual", "timing", "general"];
|
||||
const sessionThemes = ["career", "marriage", "wealth", "health", "education", "migration", "family", "annual", "timing", "general"];
|
||||
|
||||
test("domain registry is the complete allowlist used by UI and workflow input", () => {
|
||||
assert.deepEqual([...consultationDomainIds], expected);
|
||||
@@ -42,7 +46,8 @@ test("server domain plans are allowlisted, ordered and deduplicated", () => {
|
||||
test("default guided topics derive every canonical domain with evidence and claim boundaries", () => {
|
||||
// Moved from starter-questions.test.ts when homepage theme cards were removed.
|
||||
const guidedTopicsSource = readFileSync(new URL("../src/lib/guided-jyotish-topics.ts", import.meta.url), "utf8");
|
||||
assert.equal(consultationDomainIds.length, 10);
|
||||
// 原值:assert.equal(consultationDomainIds.length, 10);新值:12;原因:D2 新增 parents / children。
|
||||
assert.equal(consultationDomainIds.length, 12);
|
||||
assert.deepEqual(consultationDomainRegistry.map((domain) => domain.id), [...consultationDomainIds]);
|
||||
assert.match(guidedTopicsSource, /consultationDomainRegistry\.map/);
|
||||
assert.deepEqual(defaultGuidedJyotishTopics.map((topic) => topic.id), [...consultationDomainIds]);
|
||||
@@ -78,6 +83,9 @@ test("general guided topics keep user-centered prompts and the same claim bounda
|
||||
"请帮我看看我更适合怎样学习,以及如何安排下一步。",
|
||||
"请帮我分析现阶段是否适合迁居、置业或考虑海外发展。",
|
||||
"请帮我看看家庭关系中最需要处理的问题和责任边界。",
|
||||
// 新增两行(D2):父母、子女的无出生分钟版问法。
|
||||
"请帮我看看我和父母之间最需要处理的问题。",
|
||||
"请帮我看看我和孩子相处时最需要留意的地方。",
|
||||
"请帮我看看未来一年的主要趋势、机会和需要留意的阶段。",
|
||||
"请帮我看看目前适合推进什么,哪些事情需要再等等。",
|
||||
"请结合我的情况,帮我找出现在最值得优先处理的三个问题。",
|
||||
@@ -97,7 +105,14 @@ test("general guided topics keep user-centered prompts and the same claim bounda
|
||||
});
|
||||
|
||||
test("session persistence accepts every canonical domain and rejects product routes", () => {
|
||||
for (const theme of expected) {
|
||||
// 原值:遍历 expected(全部领域)都能落库。
|
||||
// 新值:遍历 sessionThemes(chat_sessions.theme CHECK 的十个)能落库;parents / children 不能落库。
|
||||
// 原因:D2 的两个子领域只作用于数据卡,不改数据库;写入口先挡住,免得撞 CHECK 变成 500。
|
||||
for (const theme of ["parents", "children"]) {
|
||||
const parsed = chatSessionWriteSchema.safeParse({ title: "Test", theme, model_id: "test", messages: [], session_type: "consultation", rectification_case_id: null, updated_at: "2026-08-14T00:00:00.000Z" });
|
||||
assert.equal(parsed.success, false, theme);
|
||||
}
|
||||
for (const theme of sessionThemes) {
|
||||
const parsed = chatSessionWriteSchema.safeParse({ title: "Test", theme, model_id: "test", messages: [], session_type: "consultation", rectification_case_id: null, updated_at: "2026-08-14T00:00:00.000Z" });
|
||||
assert.equal(parsed.success, true, theme);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,10 @@ test("consultation plans are server-owned and bounded", () => {
|
||||
assert.match(tools, /plan: domainPlan/);
|
||||
assert.match(workflow, /packet_version: "consultation-evidence-packet-v2"/);
|
||||
assert.match(workflow, /claim_cards/);
|
||||
assert.match(tools, /modelOutput: toModelOutput\(agentContext, domainPlan\)/);
|
||||
// 原值:/modelOutput: toModelOutput\(agentContext, domainPlan\)/
|
||||
// 新值:多传执行领域 domain(parents / children 的必用层按计划领域取,引擎路由是 family)。
|
||||
// 原因:TASK-consult-evidence-card-20260927 T2。
|
||||
assert.match(tools, /modelOutput: toModelOutput\(agentContext, domainPlan, domain\)/);
|
||||
assert.match(tools, /modelOutput: toModelOutput[\s\S]*ctx\.state\.consultationToolCompleted = true/);
|
||||
assert.match(plan, /precisionBoundary/);
|
||||
assert.match(plan, /consultationPlanMaxCreditCost = 1/);
|
||||
|
||||
Reference in New Issue
Block a user