Retire the 09-17 opener shape (coined pattern name, push/tidy periods, play-a-role) that produced riddles; one shape definition in consultation-thinking-plan; parents card names mother/father roles with MK; empty-house rule; thinking bar closes the opener at any H2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
278 lines
23 KiB
TypeScript
278 lines
23 KiB
TypeScript
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", () => {
|
||
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 宫)/);
|
||
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, /自造格局名、大运写成角色、星名当形容词,爸妈揉成一段/);
|
||
|
||
assert.match(voice, /Good \(追问轮,用户说「你在说什么鬼」\): 说直接点:这份工作还值得留/);
|
||
|
||
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 宫/);
|
||
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/);
|
||
});
|