- Voice: contrast limited to chart structures; drop 「这句我按『……』理解了」; forbid motive/need sentences; yes/no answered first (BUG-1070) - Follow-up turn (session already holds an answer): ≤200-char direct answer, no skeleton; decided from stored history, no intent regex; tool still runs each turn (BUG-1071) - Thinking bar first section 「先回答你问的这件事」 (BUG-1072) - First turn: opener without actions, body headings 盘里支持 / 时间怎么看 / 这周可以做的一件事 only, actions once, ≤900 chars (BUG-1073, D8) - Checklist is a list to check, not paragraphs to write (T3) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
181 lines
9.1 KiB
TypeScript
181 lines
9.1 KiB
TypeScript
// TASK-consult-evidence-card-v2-20260927 T4: the web consultation hands the
|
||
// answer model a condensed checklist for career, marriage and wealth (must-see
|
||
// items, the three layers, forbidden statements, the gender-unknown rule), and
|
||
// nothing else changes: the router file, the event-judgment files, the bound
|
||
// system method and every other route are as before.
|
||
//
|
||
// Real `getJyotishAgent` + prompt-recording fake model; the calculation is the
|
||
// golden engine capture of public AA charts (AGENTS §7.4).
|
||
import assert from "node:assert/strict";
|
||
import { readFileSync, readdirSync } from "node:fs";
|
||
import { join } from "node:path";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
CONDENSED_CHECKLIST_SOURCE,
|
||
CONSULTATION_CONDENSED_CHECKLISTS,
|
||
} from "../src/lib/consultation-condensed-checklist.ts";
|
||
import { consultationMethodologyForDomains, sharedConsultationMethodMarkdown } from "../src/lib/consultation-methodology.ts";
|
||
import { consultationDomainIds, type ConsultationDomain } from "../src/lib/consultation-domain-registry.ts";
|
||
import { REPORT_HEADING } from "../src/lib/consultation-thinking-plan.ts";
|
||
import { createConsultationRuntimeState, createConsultationTools } from "../src/mastra/consultation-tools.ts";
|
||
import { firstPromptWithToolResult, pieces, runNatalAgent, type Turn } from "./consult-natal-agent-test-support.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 QUESTIONS: Record<"career" | "marriage" | "wealth", string> = {
|
||
career: "未来一年,事业该关注什么?",
|
||
marriage: "今年婚恋怎么样?",
|
||
wealth: "我的财富增长方式是什么?",
|
||
};
|
||
|
||
// 原值: 夹具里有 `## ${REPORT_HEADING.question}` 一节
|
||
// 新值: 该节删除,夹具从口语开场直接到 `## ${REPORT_HEADING.support}`
|
||
// 原因: TASK-consult-answer-the-question-20260927 D8 / BUG-1073:开场就是回答,正文不再从「先回答你的问题」重来
|
||
const ANSWER = [
|
||
"表面是一个人在扛,底下是一群人在推,这叫「借力成事」。",
|
||
"",
|
||
`## ${REPORT_HEADING.support}`,
|
||
"十宫主在九分盘里站得住。",
|
||
"",
|
||
`## ${REPORT_HEADING.timing}`,
|
||
"先看接下来这段子运。",
|
||
"",
|
||
`## ${REPORT_HEADING.action}`,
|
||
"- **这周**:把要谈的事写下来。",
|
||
"",
|
||
].join("\n");
|
||
|
||
function toolResultText(prompt: unknown[] | undefined) {
|
||
return JSON.stringify((prompt ?? []).filter((message) => {
|
||
const value = message as { role?: string; content?: unknown };
|
||
return value.role === "tool" && JSON.stringify(value.content).includes("run-jyotish-consultation");
|
||
}));
|
||
}
|
||
|
||
async function writerPrompt(domain: "career" | "marriage" | "wealth") {
|
||
const calc: Turn = {
|
||
parts: [{ tool: { name: "run-jyotish-consultation", input: { question: QUESTIONS[domain], domains: [domain] } } }],
|
||
finish: "tool-calls",
|
||
};
|
||
const result = await runNatalAgent([calc, { parts: pieces(ANSWER), finish: "stop" }], {
|
||
workflow: golden.charts[0]!.workflow,
|
||
theme: domain,
|
||
question: QUESTIONS[domain],
|
||
});
|
||
assert.deepEqual(result.terminal.map((event) => event.type), ["run.completed"]);
|
||
const writer = firstPromptWithToolResult(result.prompts, "run-jyotish-consultation");
|
||
assert.ok(writer > 0);
|
||
return toolResultText(result.prompts[writer]);
|
||
}
|
||
|
||
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`);
|
||
}
|
||
assert.deepEqual(Object.keys(CONSULTATION_CONDENSED_CHECKLISTS).sort(), ["career", "marriage", "wealth"]);
|
||
});
|
||
|
||
test("the writer's prompt carries the condensed list with the three layers and the forbidden statements, not the long checklist", async () => {
|
||
const required: Record<"career" | "marriage" | "wealth", string[]> = {
|
||
career: ["接触", "结构性机会", "公开落地", "事业必成", "d9 段", "KP 精确宫头仍 blocked"],
|
||
marriage: [
|
||
"心动接触", "关系成对", "社会法律落地",
|
||
"5 宫主大运 ≠ 法律婚", "金星过本命月亮 ≠ 心动月", "Punarphoo 不得写成结婚", "土星回到本命月亮是观察项,不是结婚",
|
||
"性别未知:金星与木星都要看", "夫星", "Vivah Saham",
|
||
],
|
||
wealth: ["8 宫", "12 宫", "挣钱机会", "到账变现", "发财", "D10"],
|
||
};
|
||
const longChecklist = [
|
||
"Opportunity contact", "Mandatory modules", "Career timing output", "Relationship timing output",
|
||
"Marriage Event Adjudicator", "Wealth Event Adjudicator", "Career Event Adjudicator", "Route Freeze",
|
||
"Evidence Ledger Roles",
|
||
];
|
||
for (const domain of ["career", "marriage", "wealth"] as const) {
|
||
const prompt = await writerPrompt(domain);
|
||
assert.ok(prompt.includes(CONDENSED_CHECKLIST_SOURCE), `${domain}: condensed source`);
|
||
for (const line of CONSULTATION_CONDENSED_CHECKLISTS[domain]!) {
|
||
assert.ok(prompt.includes(line.slice(0, 24)), `${domain}: ${line.slice(0, 24)}`);
|
||
}
|
||
for (const phrase of required[domain]) assert.ok(prompt.includes(phrase), `${domain}: ${phrase}`);
|
||
for (const phrase of longChecklist) assert.equal(prompt.includes(phrase), false, `${domain}: long checklist text "${phrase}"`);
|
||
// The shared baseline still travels with the result.
|
||
assert.ok(prompt.includes("Shared mandatory baseline"), `${domain}: shared baseline`);
|
||
}
|
||
});
|
||
|
||
test("other routes still quote the live skill, and the bound system method is unchanged", () => {
|
||
const timing = consultationMethodologyForDomains(["timing"])!;
|
||
const eventTiming = timing.sections.find((section) => section.title.includes("event-timing-strict"));
|
||
assert.ok(eventTiming);
|
||
assert.match(eventTiming.source, /references\/strict-workflow-router\.md$/);
|
||
const health = consultationMethodologyForDomains(["health"])!;
|
||
assert.match(health.sections.find((section) => section.title.includes("health-timing-strict"))!.source, /strict-workflow-router\.md$/);
|
||
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);
|
||
}
|
||
const shared = sharedConsultationMethodMarkdown();
|
||
assert.match(shared, /Full-Spectrum Invocation Contract/);
|
||
assert.match(shared, /Event Judgment Skeleton|event judgment/i);
|
||
});
|
||
|
||
test("only the consultation tool builds the methodology: reports and other surfaces never read it", () => {
|
||
const root = new URL("../src/", import.meta.url).pathname;
|
||
const users: string[] = [];
|
||
const walk = (dir: string) => {
|
||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||
const path = join(dir, entry.name);
|
||
if (entry.isDirectory()) walk(path);
|
||
else if (/\.(ts|tsx)$/.test(entry.name) && readFileSync(path, "utf8").includes("consultationMethodologyForDomains")) {
|
||
users.push(path.slice(root.length));
|
||
}
|
||
}
|
||
};
|
||
walk(root);
|
||
assert.deepEqual(users.sort(), ["lib/consultation-methodology.ts", "mastra/consultation-tools.ts"]);
|
||
const condensedUsers: string[] = [];
|
||
const walkCondensed = (dir: string) => {
|
||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||
const path = join(dir, entry.name);
|
||
if (entry.isDirectory()) walkCondensed(path);
|
||
else if (/\.(ts|tsx)$/.test(entry.name) && readFileSync(path, "utf8").includes("consultation-condensed-checklist")) {
|
||
condensedUsers.push(path.slice(root.length));
|
||
}
|
||
}
|
||
};
|
||
walkCondensed(root);
|
||
assert.deepEqual(condensedUsers, ["lib/consultation-methodology.ts"]);
|
||
});
|
||
|
||
test("size: every single-domain turn stays within 12,000 characters, contract and checklist included", async () => {
|
||
const serverChart = {
|
||
name: "public",
|
||
toolInput: { year: 2000, month: 1, day: 1, hour: 0, minute: 0, city: "fixture", lat: 0, lon: 0, tz: 0, ayanamsa: "raman" },
|
||
truth: { birthTimeSource: "reported" },
|
||
};
|
||
for (const chart of golden.charts) {
|
||
for (const [workflow, domain] of [
|
||
...consultationDomainIds.filter((id) => id !== "annual" && id !== "timing").map((id) => [chart.workflow, id] as const),
|
||
[chart.annual_workflow, "annual"] as const,
|
||
[chart.timing_workflow, "timing"] as const,
|
||
] as Array<readonly [Json, ConsultationDomain]>) {
|
||
const state = createConsultationRuntimeState();
|
||
const tools = createConsultationTools({
|
||
userId: "u", sessionId: "s", requestId: `size-${chart.id}-${domain}`, consultationMode: "verified_chart",
|
||
serverChart: serverChart as never, state,
|
||
runWorkflow: async () => structuredClone(workflow) as never,
|
||
});
|
||
const view = await tools["run-jyotish-consultation"].execute!({ question: "q", domains: [domain] } as never, { writer: { custom: async () => {} } } as never);
|
||
const total = JSON.stringify(view).length;
|
||
assert.equal(state.modelVisibleChars, total);
|
||
assert.ok(total <= 12_000, `${chart.id}/${domain}: ${total}`);
|
||
}
|
||
}
|
||
});
|