import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; const voice = readFileSync(new URL("../src/mastra/product-voice.ts", import.meta.url), "utf8"); const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8"); const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8"); const binding = readFileSync(new URL("../src/mastra/skill-binding.ts", import.meta.url), "utf8"); const workflow = readFileSync(new URL("../src/mastra/consultation-workflow.ts", import.meta.url), "utf8"); test("product voice uses a spoken opener then the skill Level 2 report skeleton", () => { assert.match(voice, /VISIBLE VOICE/); assert.match(voice, /natalSpokenReportContract/); assert.match(voice, /governs METHOD, TECHNIQUE INVOCATION, TRUTH BOUNDARIES/); assert.match(voice, /Raman six-step house judgment/); assert.match(voice, /Yoga table/); assert.match(voice, /Do not write 统一参数与原始结构 or 技法审计表 into the body/); assert.match(voice, /governed_support_only_candidate_ready/); // 原值: assert.match(voice, /Skip an executed layer the question needs/) // 新值: 「不许为了短而漏掉问题需要的卡上事实」;写答案的模型只读数据卡 // 原因: TASK-consult-evidence-card-20260927 D1/T4(推翻「用尽每个已执行层」的口径) assert.match(voice, /Skip a card fact the question needs/); assert.doesNotMatch(voice, /Skip an executed layer/); // 原值: assert.match(voice, /你更适合把已经积累的专业能力做成长期事业/) —— 它是 Good 例。 // 新值: 同一句改断言成 Bad 例(下方 voice-opener-shape 测试里锁住它在 Bad 段)。 // 原因: 产品判定这句像顾问报告——没有反差、没说谁在推谁在修、没有可扮演的象。 // 原值: assert.match(voice, /3–6 heading-free sentences/) // 新值: assert.match(voice, /open with the OPENER SHAPE above, heading-free/) // 原因: 开场从「3–6 句」改成固定形状,句数不再是约束。 // 原值(2): assert.match(voice, /open with the OPENER SHAPE above, heading-free/) // 新值(2): 合同改为跟随 ANSWER SHAPE,并引用唯一的形状摘要 NATAL_ANSWER_SHAPE_SUMMARY // 原因(2): TASK-consult-plain-answer-20261001 D1/D7(BUG-1132):四步开场形状下线,形状只定义一处 assert.match(voice, /follow the ANSWER SHAPE above\. \$\{NATAL_ANSWER_SHAPE_SUMMARY\}/); // 原值: assert.match(voice, /one conclusion sentence, then 2–3 short point-sentences/) // 新值: assert.match(voice, /别去应 X 的象,去扮演 Y 的象/) // 原因: 旧形状(结论+要点+下一步)被反差→谁推谁修→扮演象→短行动取代。 // 原值(2): assert.match(voice, /别去应 X 的象,去扮演 Y 的象/); assert.match(voice, /Spoken-layer total ≤ 400 Chinese characters/) // 新值(2): 两句都不得再出现;改锁「不设字数上限,讲清楚为准」 // 原因(2): TASK-consult-plain-answer-20261001 D1/D8:扮演象修辞下线,产品取消字数上限 assert.doesNotMatch(voice, /扮演|≤ 400|Spoken-layer total/); assert.match(voice, /不设字数上限,讲清楚为准/); assert.match(voice, /骨架不可省略,但必须以直接回应开场/); assert.match(mastra, /productConversationVoice/); assert.match(mastra, /natalSpokenReportContract/); assert.match(mastra, /skill Level 2 report skeleton/); assert.match(mastra, /Do not paste the Technique Audit Table into the answer body/); assert.match(binding, /Follow this method and its truth boundaries/); // 原值: assert.match(binding, /present its Level 2 report template in the chat body after a 3-6 sentence spoken reply/) // 新值: Level 2 模板是回答形状的依据层,形状引用 NATAL_ANSWER_SHAPE_SUMMARY // 原因: TASK-consult-plain-answer-20261001 D2/D7:先答、按对象分段,Level 2 内容落在「盘上依据」 assert.match(binding, /present its Level 2 report template as the evidence of the product's answer shape, not as the shape itself\. \$\{NATAL_ANSWER_SHAPE_SUMMARY\}/); assert.match(mastra, /The bound skill method is this product's answering contract/); assert.match(mastra, /Do not use a restricted technique/); // 原值: assert.match(mastra, /must_use_layers is the executed shortlist/) // 新值: 数据卡是这次回答的清单;卡外数据只能用补取工具取一次 // 原因: TASK-consult-evidence-card-20260927 D1/D3/T4,must_use_layers 与审计表不再进模型上下文 assert.match(mastra, /The evidence card is the shortlist for this answer/); assert.match(mastra, /call read-consultation-evidence once, before writing any answer text/); assert.doesNotMatch(mastra, /must_use_layers is the executed shortlist|Use every executed layer/); assert.match(mastra, /do not invent it from model knowledge/); assert.match(mastra, /search_period is the searched observation window/); // 原值: assert.match(voice, /## 先回答你的问题/) // 新值: 正文只有三个标题,「先回答你的问题」不再出现在合同里;开场不带行动、行动只在最后一节、首轮 ≤ 900 字 // 原因: TASK-consult-answer-the-question-20260927 D8 / BUG-1073:开场就是回答,同一判断不再说三遍 assert.doesNotMatch(voice, /## 先回答你的问题/); // 原值: assert.match(voice, /## 盘里支持这个判断的地方, then ## 时间怎么看, then ## 这周可以做的一件事/) // 新值: 按对象分段之后才是三个固定标题,标题取自 REPORT_HEADING(「盘里支持这个判断的地方」改名「盘上依据」),前两个可省 // 原因: TASK-consult-plain-answer-20261001 D2/D7(BUG-1132) assert.match(voice, /One section per person or sub-question the user named \(妈妈 and 爸爸 are two sections, never one\)/); assert.match(voice, /## \$\{REPORT_HEADING\.support\}[\s\S]*Omit the section when there is nothing new to cite[\s\S]*## \$\{REPORT_HEADING\.timing\}[\s\S]*## \$\{REPORT_HEADING\.action\}/); // 原值: assert.match(voice, /The opener carries no action list/) // 新值: 「开场不写行动」写在共享 ANSWER SHAPE 第 1 步与形状摘要里 // 原因: TASK-consult-plain-answer-20261001 D2:合同不再复述开场细节,引用唯一定义 assert.match(voice, /开场不写标题、不写行动/); assert.match(voice, /Say each thing once \(D8\)/); // 原值: assert.match(voice, /First-turn total ≤ 900 Chinese characters/) // 新值: 没有字数上限,写到讲清楚为止 // 原因: TASK-consult-plain-answer-20261001 D8:产品取消字数上限(推翻 BUG-1073 的 900 字,保留「只说一遍」) assert.doesNotMatch(voice, /900/); assert.match(voice, /There is no length cap: write until it is clear, and stop there/); // 原值: /cite only the fields that support this judgment, and only what the opener did not already say/ // 新值: 「只写开场与对象段都没引过的」 // 原因: TASK-consult-plain-answer-20261001 D2:对象段已经在括号里给过依据 assert.match(voice, /cite only the fields that support this judgment, and only what the opener and the sections did not already cite/); assert.match(voice, /One to three actions for this week, here and nowhere else/); assert.doesNotMatch(voice, /One natal career\/wealth\/marriage\/family question still uses the opener-plus-skeleton above/); assert.match(voice, /a follow-up turn follows the shape instruction carried in the user turn/); // 原值: assert.match(mastra, /each thing said once, first-turn total ≤ 900 characters/) // 新值: 指令引用 NATAL_ANSWER_SHAPE_SUMMARY(摘要里写「无字数上限、只说一遍」),不再含 900 // 原因: TASK-consult-plain-answer-20261001 D7/D8 assert.match(mastra, /Write in Simplified Chinese\. \$\{NATAL_ANSWER_SHAPE_SUMMARY\}/); assert.doesNotMatch(mastra, /≤ 900|≤ 400|反差|扮演/); assert.match(mastra, /The checklist is a list of things to check, not a list of paragraphs to write/); // 原值: assert.match(binding, /no action list in the opener/) // 新值: 同一句改由引用的 NATAL_ANSWER_SHAPE_SUMMARY 携带(见下方 shape-summary 测试) // 原因: TASK-consult-plain-answer-20261001 D7:形状只定义一处 assert.doesNotMatch(binding, /反差|扮演|≤ 400/); // 原值: assert.match(binding, /the actions, once/) // 新值: assert.match(binding, /the actions appear once/) // 原因: TASK-consult-plain-answer-20261001 D7:骨架句改写,语义不变 assert.match(binding, /the actions appear once/); assert.match(mastra, /consultationSpokenHeadingRule\("natal"\)/); assert.match(workflow, /template: "skill_level_2"/); assert.match(workflow, /raman_six_step/); assert.doesNotMatch(mastra, /Name the domains you did cover, say plainly that the remaining ones were not calculated/); assert.doesNotMatch(mastra, /2-5 short paragraphs/); }); test("general and window get voice without the natal Level 2 skeleton", () => { // 原值: 另切 onboardingInstructions,断言它没有 natal 骨架。 // 新值: onboarding agent 已删除;general 与 window 仍无骨架。 // 原因: 建议问题生成接口下线。 const generalStart = mastra.indexOf("const generalJyotishInstructions"); const windowStart = mastra.indexOf("const windowJyotishInstructions"); const natalStart = mastra.indexOf("const jyotishInstructions"); const general = mastra.slice(generalStart, mastra.indexOf("const dailyStarlanguageInstructions")); const window = mastra.slice(windowStart, mastra.indexOf("export function getWindowJyotishAgent")); const natal = mastra.slice(natalStart, mastra.indexOf("export function getJyotishAgent")); assert.match(natal, /natalSpokenReportContract/); // 新增: natal 也必须带 productConversationVoice —— 开场形状对三种模式共用。 assert.match(natal, /productConversationVoice/); assert.match(general, /productConversationVoice/); assert.doesNotMatch(general, /natalSpokenReportContract/); assert.match(window, /productConversationVoice/); const exemption = "只是打招呼、道谢、告别时,回一句话,不要套开场形状"; const sharedVoice = voice.slice(0, voice.indexOf("export const natalSpokenReportContract")); for (const instructions of [natal, general, window]) { // Expand only the shared voice literal; no runtime Skill filesystem is needed. assert.ok(instructions.replace("${productConversationVoice}", sharedVoice).includes(exemption)); } assert.doesNotMatch(window, /natalSpokenReportContract/); assert.doesNotMatch(mastra, /const onboardingInstructions/); }); test("consult user turn no longer dumps tool JSON that the model would parrot", () => { const chartBranch = route.slice(route.indexOf("const toolInput = consultationInputSchema.parse")); // 原值: /先用 3–6 句口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/ // 新值: /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/ // 原因: 用户回合文案与新开场形状对齐,约束从句数改成字数加形状。 // 原值(3): /先用不超过 400 字口语直接回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/ 与 /别去应 X 的象,去扮演 Y 的象/ // 新值(3): 用户回合引用 natalAnswerShapeBody(),路由源码里不再有字数上限和扮演象 // 原因(3): TASK-consult-plain-answer-20261001 D1/D7/D8 assert.match(chartBranch, /natalAnswerShapeBody\(\)[\s\S]*?resolvedQuestion\.modelQuestion/); assert.doesNotMatch(route, /扮演|不超过 400 字|不超过 900 字/); assert.doesNotMatch(chartBranch, /JSON\.stringify\(toolInput\)/); assert.doesNotMatch(chartBranch, /经过服务端校验的工具参数/); assert.match(route, /文末技法审计表/); assert.match(chartBranch, /await runConsultationWorkflow\(toolInput/); }); // 原值: test "the opener is a shape — 反差, 谁推谁修, 扮演哪个象, then at most three actions"(锁 OPENER SHAPE 四步、≤ 20 字行动、降级路线「第 3 步照给」) // 新值: 同位置改锁 ANSWER SHAPE:先答、按对象分段、人话自检与禁止清单、空宫规则、无字数上限、降级路线两条 // 原因: TASK-consult-plain-answer-20261001 D1–D4/D8(BUG-1132/1134):产品推翻 09-17 四步开场形状 test("the answer is a shape — conclusion first, one section per person, plain words checked, no length cap (BUG-1132)", () => { assert.match(voice, /ANSWER SHAPE \(natal \/ general \/ declared-window 三种模式共用\)/); assert.doesNotMatch(voice, /OPENER SHAPE|表面 A,底下 B|谁在推|命名成一个格局|外松内紧/); assert.match(voice, /不设字数上限,讲清楚为准。同一件事只说一遍/); assert.match(voice, /开场一到三句人话,第一句就是结论/); assert.match(voice, /按对象分段:问题里点到几个对象/); assert.match(voice, /问「父母」时妈妈和爸爸分开写,不揉成一段。只有一个对象、一个子问题时不拆段/); assert.match(voice, /然后在括号里给出撑住这段判断的盘上依据/); assert.match(voice, /人话自检:把每句话括号里的内容删掉,一个完全不懂占星的人仍然能看懂它在说什么/); for (const banned of ["自造格局名、四字概括", "把大运、行运写成拟人的角色", "把星名当形容词的主人", "括号外每段最多一个占星术语"]) { assert.ok(voice.includes(banned), banned); } assert.match(voice, /推断边界:空宫不单独下结论——要说这个宫,就说它的宫主落在哪、什么状态/); assert.match(voice, /一到三条破折号短句/); // 降级路线:申报时段与无出生分钟都不编月份;形状照给。 assert.match(voice, /申报时段(declared birth window):依据只取窗口内稳定的那一层[\s\S]*?也不写月份。/); assert.match(voice, /无出生分钟(no birth minute):时间只讲公开日历这一天的大势,不点个人大运,不写月份/); assert.doesNotMatch(voice, /≤ 20 Chinese characters|≤ 400|≤ 900/); }); test("one shape definition: contract, thinking plan, user turn and agent prompts share the headings and summary (D7)", async () => { const { NATAL_ANSWER_SHAPE_SUMMARY, REPORT_HEADING, consultationSpokenHeadingRule, natalAnswerShapeBody } = await import("../src/lib/consultation-thinking-plan.ts"); const { natalSpokenReportContract } = await import("../src/mastra/product-voice.ts"); assert.equal(REPORT_HEADING.support, "盘上依据"); for (const heading of Object.values(REPORT_HEADING)) { assert.ok(NATAL_ANSWER_SHAPE_SUMMARY.includes(`## ${heading}`), heading); assert.ok(natalSpokenReportContract.includes(`## ${heading}`), heading); assert.ok(natalAnswerShapeBody().includes(`## ${heading}`), heading); } assert.ok(natalSpokenReportContract.includes(NATAL_ANSWER_SHAPE_SUMMARY)); assert.ok(consultationSpokenHeadingRule("natal").includes(NATAL_ANSWER_SHAPE_SUMMARY)); assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /There is no length cap/); assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /no conclusion from an empty house alone/); for (const text of [NATAL_ANSWER_SHAPE_SUMMARY, natalSpokenReportContract, natalAnswerShapeBody(), consultationSpokenHeadingRule("natal")]) { assert.doesNotMatch(text, /扮演|谁在推|命名成一个格局|外松内紧|≤ 400|≤ 900|不超过 \d+ 字/); } }); test("persona is the sharp-but-reliable colleague, and hope never becomes a platitude", () => { assert.match(voice, /把对面的人当一个人认真对待/); assert.match(voice, /嘴有点毒但靠谱的同事/); assert.match(voice, /毒只对处境,不对人/); // 原值: assert.match(voice, /这句我按『……』理解了,不对你纠正我/) // 新值: 该规则删除;只有两种实质不同读法时才写「我按 A 答」,单义问题直接答、不改写 // 原因: TASK-consult-answer-the-question-20260927 D4 / BUG-1070:这条规则让模型先改写问题再答(「是不是 X」→「你是否需要 Y」) assert.doesNotMatch(voice, /这句我按『……』理解了/); assert.match(voice, /只有当这句话有两种实质不同的读法时,才写一句「我按 A 答」/); assert.match(voice, /单义的问题直接答,不改写它/); assert.match(voice, /HOPE DISCIPLINE/); assert.match(voice, /answer_policy\.can_answer_precise_timing 允许精确应期时/); // 原值: assert.match(voice, /不是每个人都有转机,但每个人都有能扮演的象/) // 新值: assert.match(voice, /不是每个人都有转机,但每个人这段时间都有能做的事/) // 原因: TASK-consult-plain-answer-20261001 D5:希望纪律保留,删「扮演哪个象」说法 assert.match(voice, /不是每个人都有转机,但每个人这段时间都有能做的事/); assert.match(voice, /每次都要说清这段时间能做什么/); // 空话必须留在禁令里,但不得出现在 Good 例里。 assert.match(voice, /Never write 一切都会好, 相信自己, 加油, 你值得更好的, or 宇宙自有安排。?/); assert.match(voice, /No 亲爱的, no 感谢你的信任, no emoji/); }); // 原值: test "the pinned Good example carries the new shape; the old consultant-report line is now Bad"(锁「外松内紧」定稿样例、扮演土星的守、顾问报告 Bad 的理由「没有反差…没有可扮演的象」) // 新值: 锁父母题按对象分段的 Good、单对象不拆段的 Good、格局名/大运拟人的 Bad;顾问报告 Bad 保留、理由改成「没有盘上依据」 // 原因: TASK-consult-plain-answer-20261001 D1/D2/T1(BUG-1132):09-17 定稿样例随形状一起下线 test("the pinned Good examples split by person and stay plain; the coined-pattern opener is Bad", () => { // 原值: indexOf("Good (父母题首轮,按对象分段;盘上数据是示意)") / indexOf("Good (单对象事业题首轮,不拆段;盘上数据是示意)") // 新值: 两个标签改为「括号里是依据的占位,只演示口气与结构,不是读法」,事业例再加「不预设读者在上班、做哪类工作」 // 原因: TASK-consult-no-presupposition-and-backtest-20261001 D3/D7(BUG-1165/1168):示例不得携带读法规则与职业形态 const parentsStart = voice.indexOf("Good (父母题首轮,按对象分段;括号里是依据的占位,只演示口气与结构,不是读法)"); const careerStart = voice.indexOf("Good (单对象事业题首轮,不拆段;括号里是依据的占位,只演示口气与结构,不是读法;不预设读者在上班、做哪类工作)"); const badStart = voice.indexOf("Bad (多对象题,改写示意)"); assert.ok(parentsStart > -1 && careerStart > parentsStart && badStart > careerStart); const parents = voice.slice(parentsStart, careerStart); for (const heading of ["## 你和妈妈", "## 你和爸爸", "## 他们怎么对你", "## 时间怎么看", "## 这周可以做的一件事"]) { assert.ok(parents.includes(heading), heading); } // 原值: assert.match(parents, /(4 宫母亲宫的宫主水星落在 10 宫事业宫;代表母亲的月亮也在 10 宫)/) // 新值: 括号只放占位(4 宫主落 10 宫;月亮在 10 宫);爸爸段演示「受冲先说、用可能、段末请确认」;不再有「分量重」「问爸爸的意见」 // 原因: TASK-consult-no-presupposition-and-backtest-20261001 D1/D3(BUG-1164/1165):旧例预设父亲在场,回测里缺席的父亲被照抄成「话少、分量重」 // 原值(3): /(4 宫主落 10 宫;月亮在 10 宫)/ // 新值(3): /(母亲这条线没有受冲;4 宫主落 10 宫;月亮在 10 宫)/ // 原因(3): 第五轮回测前修(BUG-1182):梦露盘 4 宫主正落 10 宫且母亲受冲两个以上,模型照抄「上心、前途」;示例只在没有受冲时演示「近、上心」 assert.match(parents, /(母亲这条线没有受冲;4 宫主落 10 宫;月亮在 10 宫)/); // 原值(2): /他可能不常在你身边[\s\S]*如果实际情况不是这样,告诉我,我按真实情况重看。/ // 新值(2): 爸爸段写「有距离的迹象」+ 范围 + 「实际是哪一种,告诉我一句」,不断定在不在身边 // 原因(2): 产品 2026-10-02 改口径(名人回测对照组误报 1→15;盘分不出父母在不在身边) assert.match(parents, /爸爸这条线有距离的迹象[\s\S]*现实里可能是他不常在你身边,也可能人在、只是话少、关系紧。实际是哪一种,告诉我一句,我按真实情况再看。/); assert.doesNotMatch(parents, /分量重|标杆|爸爸的意见|靠做事/); const career = voice.slice(careerStart, badStart); assert.doesNotMatch(career, /## 你和/); assert.match(career, /## 盘上依据/); for (const good of [parents, career]) { assert.doesNotMatch(good, /一切都会好|相信自己|加油|宇宙自有安排|扮演|格局/); // No coined 外X内Y pattern names and no empty-house verdicts in the pinned examples. assert.doesNotMatch(good, /「外.内.」|空宫/); } const bad = voice.slice(badStart, voice.indexOf("\n", badStart)); assert.match(bad, /自造格局名、大运写成角色、星名当形容词,爸妈揉成一段/); // 原值: /Good \(追问轮,用户说「你在说什么鬼」\): 说直接点:这份工作还值得留/ // 新值: /…说直接点:现在这条路还值得走/ // 原因: TASK-consult-no-presupposition-and-backtest-20261001 D7(BUG-1168):示例不预设读者在上班 assert.match(voice, /Good \(追问轮,用户说「你在说什么鬼」\): 说直接点:现在这条路还值得走/); // 原值: startsWith("Bad: 你更适合把已经积累的专业能力做成长期事业") // 新值: startsWith("Bad: 你更适合长期做一件事"),理由不变;全篇示例不再有「专业能力」「升职」「做深」「现有工作」 // 原因: TASK-consult-no-presupposition-and-backtest-20261001 D7/T2b(BUG-1168):Bad 例的职业形态词被模型反向吸收 assert.doesNotMatch(voice, /专业能力|升职|做深|现有工作|这份工作/); const badLine = voice.split("\n").find((line) => line.startsWith("Bad: 你更适合长期做一件事")); assert.ok(badLine, "旧 Good 例必须改挂在 Bad 上"); assert.match(badLine!, /像顾问报告:只有结论,没有盘上依据/); }); test("the voice answers the sentence asked: no motive guessing, yes/no first, contrast on the chart (BUG-1070)", () => { const forbidden = ["你其实想问的是", "你真正在意的是", "你要的是", "你是否需要", "你问这个是因为", "你更想知道"]; const discipline = voice.slice(voice.indexOf("不揣测(回答对象是用户问的那句话)"), voice.indexOf("Five principles")); assert.ok(discipline.length > 0, "the no-guessing block sits in the shared voice before the principles"); for (const phrase of forbidden) assert.ok(discipline.includes(`「${phrase}」`), phrase); assert.match(discipline, /任何替用户或当事人陈述动机、需求、感受的句子/); assert.match(discipline, /不得写「她问的是结果,你要的是被听」/); assert.match(discipline, /是非题(是不是 \/ 会不会 \/ 能不能 \/ 有没有 \/ ……吗):第一句就是「是」「不是」或「看情况:……」/); // 原值(2): /A 和 B 都必须是盘上的结构——…/ 与 /反差写那个人在你盘上对应的宫位与代表星,不写那个人的心思/ // 新值(2): 反差形状下线;「问别人时写宫位与代表星,不写心思」移到对象段规则里 // 原因(2): TASK-consult-plain-answer-20261001 D1(BUG-1070 的目的保留) assert.match(voice, /问的是别人时,写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思/); // Actions stay in the opener only where there is no body skeleton. // 原值(2): /本命解读里这三条不写在开场,只写在正文最后一节「这周可以做的一件事」/、/…行动就写在开场结尾/ // 新值(2): /本命解读里只写在最后一节「这周可以做的一件事」,出现一次/、/…行动就写在回答结尾/ // 原因(2): TASK-consult-plain-answer-20261001 D2:开场之后还有对象段,「开场结尾」改「回答结尾」 assert.match(voice, /本命解读里只写在最后一节「这周可以做的一件事」,出现一次/); assert.match(voice, /无出生分钟与申报时段没有正文骨架,行动就写在回答结尾/); // Yes/no Good and Bad pair, fictional chart. assert.match(voice, /Good \(是非题,用户问「我妈是不是不太在意我」/); // 原值: assert.match(voice, /不是。4 宫主水星逆行落 10 宫/) // 新值: 「不是。(4 宫主落 10 宫)」;示例与提示词里不再有「逆行」,已婚预设的婚姻 Good 例删除 // 原因: TASK-consult-no-presupposition-and-backtest-20261001 D2/D3(BUG-1165):「逆行」被照抄成「反复、打回来」 // 原值(2): /不是。(4 宫主落 10 宫)她把心力放在你的前途上/ // 新值(2): /不是。(母亲这条线没有受冲;4 宫主落 10 宫)她把心力放在你的前途上/ // 原因(2): BUG-1182:受冲两个以上的母亲被照抄成「心力在你前途上」;示例括号写明这一读法只在没有受冲时成立 assert.match(voice, /不是。(母亲这条线没有受冲;4 宫主落 10 宫)她把心力放在你的前途上/); assert.doesNotMatch(voice, /逆行|已经在一起的关系/); assert.match(voice, /Bad \(同一问题\): 你其实要的是被听见/); // 原值: "…wants, needs, or feels; state where the chart puts them"(Never 列表里第二句读成也被禁止) // 新值: 两句拆开,第二句是替代做法 原因: Claude 验收改措辞 assert.match(voice, /State what the user or the person asked about wants, needs, or feels\. Say where the chart puts them instead/); // The three-mode shared voice still carries all of it (general and window expand the same literal). const sharedVoice = voice.slice(0, voice.indexOf("export const natalSpokenReportContract")); assert.ok(sharedVoice.includes("不揣测(回答对象是用户问的那句话)")); // BUG-1134: the empty-house rule is also in the natal Never list. assert.match(voice, /Draw a conclusion from an empty house alone\. Say where its lord sits and in what state instead/); }); test("the voice pins Chinese-only terms and one spelling per thing (BUG-1148)", () => { assert.match(voice, /名词只用中文,同一个东西全篇一个叫法/); for (const term of ["克图", "凯图", "中运", "sade sati", "vargottama", "rupas"]) { assert.ok(voice.includes(term), `the voice names ${term} as a spelling not to use`); } assert.match(voice, /力量只说强、中、弱/); });