From ece76a2e8e4a1177b4d4e47ea4c242cbe63f3df5 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Mon, 9 Nov 2026 18:39:56 +0800 Subject: [PATCH] wip(#182): one yoga status table drives the contract and the professional voice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - yoga-status-words.ts:新增 yogaStatusContractTable()(遍历 YOGA_STATUS_MAPPING 生成, 不手写文字)与 YOGA_STATUS_WORD_CLASSES(拦截检查用的三张词表 + 括号里照抄状态的词) - references/product_runtime_contract.md:shared-reading 第 7 条与它上面的注释改成 「按 yogas_detail 每行状态 + {{YOGA_STATUS_TABLE}}」,别的行一字未动 - consultation-condensed-checklist.ts:sharedReadingBlock() 的取值里补 YOGA_STATUS_TABLE - product-voice.ts:只把「状态词照卡上原词抄」那一行换成按同一张表写 - consult-professional-mode-20261107.test.ts:按任务书改那一处断言(三栏注释) 未完成(下一轮):consultation-answer-fact-guard.ts 的格局状态检查与 9 组测试。 Tests: consult-professional-mode 10/10、consult-basis-candidate-status-1824 12/12 通过。 Co-Authored-By: Grok (agent loop) --- .../lib/consultation-condensed-checklist.ts | 8 +++- frontend/src/lib/yoga-status-words.ts | 38 +++++++++++++++++++ frontend/src/mastra/product-voice.ts | 3 +- ...consult-professional-mode-20261107.test.ts | 7 +++- references/product_runtime_contract.md | 4 +- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/consultation-condensed-checklist.ts b/frontend/src/lib/consultation-condensed-checklist.ts index 2ccdb7e2..a687766a 100644 --- a/frontend/src/lib/consultation-condensed-checklist.ts +++ b/frontend/src/lib/consultation-condensed-checklist.ts @@ -1,6 +1,7 @@ import type { ConsultationDomain } from "./consultation-domain-registry.ts"; import type { ProfileGender } from "./profile-gender.ts"; import { AFFLICTION_RANGE_RULE, CAREER_FIELD_ASK_RULE, CHECKLIST_ANALYSIS_ONLY_NOTE } from "./consultation-thinking-plan.ts"; +import { yogaStatusContractTable } from "./yoga-status-words.ts"; import { productContractBlock } from "./product-runtime-contract.ts"; /** @@ -68,7 +69,12 @@ export const CONDENSED_SHARED_READING_SOURCE = "consultation shared reading (TAS * items without their numbers. */ function sharedReadingBlock(): string { - return productContractBlock("shared-reading", { AFFLICTION_RANGE_RULE, CHECKLIST_ANALYSIS_ONLY_NOTE }); + return productContractBlock("shared-reading", { + AFFLICTION_RANGE_RULE, + CHECKLIST_ANALYSIS_ONLY_NOTE, + // BUG-1825:格局状态表只从 yoga-status-words.ts 取,合同里不手写。 + YOGA_STATUS_TABLE: yogaStatusContractTable(), + }); } export const CONDENSED_SHARED_READING_LINES: readonly string[] = sharedReadingBlock() .split("\n") diff --git a/frontend/src/lib/yoga-status-words.ts b/frontend/src/lib/yoga-status-words.ts index 304bc207..37ab49ad 100644 --- a/frontend/src/lib/yoga-status-words.ts +++ b/frontend/src/lib/yoga-status-words.ts @@ -74,3 +74,41 @@ export function yogaStatus(row: Rec | undefined, options: { notEvaluated?: boole export function isStructuralObservation(row: Rec): boolean { return row.claim_kind === "structural_observation" || row.status === "structural_observation"; } + + +/** + * 她的表渲染成一行合同文字(合同与 product-voice 都用它,不各写一遍)。 + * 左边是卡上可能出现的原始值(用「 / 」连接),右边是映射表里的文字;文字不手写。 + */ +export function yogaStatusContractTable(): string { + return (Object.keys(YOGA_STATUS_MAPPING) as YogaStatusKey[]) + .map((key) => { + const entry = YOGA_STATUS_MAPPING[key]; + return `${entry.evidence.join(" / ")} →「${entry.text}」`; + }) + .join(";"); +} + +/** 拦截检查用的三张词表(占星师第七轮回复 §5 P0.2 的口径)。 */ +export const YOGA_STATUS_WORD_CLASSES: Readonly<{ + affirm: readonly string[]; + deny: readonly string[]; + exempt: readonly string[]; + conditional: readonly string[]; + literalStatus: readonly string[]; +}> = { + // 肯定:说这条格局已经成立。 + affirm: ["已成立", "已经成立", "确认成立", "已形成", "已经形成", "形成了", "成立"], + // 否定:说这条格局不成立 / 落空。 + deny: ["不成立", "非成立", "未成立", "没有成立", "并不成立", "落空", "不构成", "未形成", "没有形成", "必要条件未满足", "条件未满足"], + // 排除写法:出现时这一处既不算肯定也不算否定。 + exempt: [ + "不作成立", "不作为成立", "不作格局成立", "不当作成立", "不当成成立", "不算成立", "不算作成立", + "不写成成立", "不说成成立", "不能说成立", "未列为成立", "没有列为成立", "不视为成立", + "是否成立", "能否成立", "成立与否", "成立条件", "成立的条件", "成立名单", + ], + // 条件句里出现这些词时,后面的「成立」不算。 + conditional: ["若", "如果", "假如", "一旦", "只有", "才能", "才会"], + // 括号里照抄状态时,这条格局名不检查。 + literalStatus: ["candidate", "computed", "候选", "待核实", "检出", "未确认", "待定"], +}; diff --git a/frontend/src/mastra/product-voice.ts b/frontend/src/mastra/product-voice.ts index bede4222..a94d8d36 100644 --- a/frontend/src/mastra/product-voice.ts +++ b/frontend/src/mastra/product-voice.ts @@ -1,3 +1,4 @@ +import { yogaStatusContractTable } from "../lib/yoga-status-words.ts"; import { DEFAULT_CONSULTATION_ANSWER_MODE, INTERPRETATION_STATUS_RULE, @@ -140,7 +141,7 @@ export const professionalConversationVoice = `VISIBLE VOICE - 数值照卡上和补取结果原样引用,标出处层(D1、D9、卡上 timing 等);卡上没有、补取也没有的,写「未交付」,不从记忆补。 - 受冲信号写实际来源:哪一宫、哪颗星、什么关系(同宫、相照、宫主落 6 / 8 / 12、功能凶星、落陷或燃烧),不报条数,条数只在后台(占星师 2026-11-08 第五轮裁定 6)。 - 点位(A2、A11、AL、UL、Indu Lagna 等)和宫主、行星的星座、宫位、主星,只照卡上那个字段写;同一个点在正文、表格、审计表里只有一个值,不从别的点、宫或 Saham 推出另一个落点。卡上没给的大运层(比如下一个子运)写「未交付」,不拿下一个主运或别的体系顶替。 -- 状态词照卡上原词抄:candidate 写「候选」、computed 写「已计算,未验证」、strength_comparison_required 写「待定」;不升格为「成立」「确认」「命中」,标题里也不写「已成立」。 +- 格局(Yoga)的状态只按卡上 yogas_detail 那一行写,用这张表:${yogaStatusContractTable()};只出现在 yogas.hits、卡上没有状态行的,按 blocked 写并写明缺什么。不从 hits 推出「成立」,没有完整条件检查不写「不成立」「非成立」「落空」。其他状态词照卡上原词抄:candidate 写「候选」、strength_comparison_required 写「待定」;不升格为「成立」「确认」「命中」,标题里也不写「已成立」。 - 日期精度跟出生时间走:卡上 rectification 没有写明出生时间已校正时,大运、子运、细运和行运的日期都是名义计算边界,几分钟的出生误差就能移动几天;第一次写日期时注明「名义计算边界」,结论和行动建议里的时间写到月或旬,不写「某日起」。 - 用户点名的技法、宫位、分盘每项都写到;用户的写法要求照做:不让排除的,不写排除或圈定某一类的句子;要分明显和可能的,按证据等级分。 - 用户纠正你的判断时,先认下,直说原来哪句读偏了,再只说盘上真正对得上的部分,并重新标等级;不为原判断辩护,也不把原因推到用户身上。 diff --git a/frontend/tests/consult-professional-mode-20261107.test.ts b/frontend/tests/consult-professional-mode-20261107.test.ts index b93d7955..7fccb073 100644 --- a/frontend/tests/consult-professional-mode-20261107.test.ts +++ b/frontend/tests/consult-professional-mode-20261107.test.ts @@ -6,6 +6,7 @@ // read"; sub-skills travel with the router; follow-ups and entries never // invite a question the chart cannot answer. import assert from "node:assert/strict"; +import { yogaStatusContractTable } from "../src/lib/yoga-status-words.ts"; import { readFileSync } from "node:fs"; import test from "node:test"; @@ -205,7 +206,11 @@ test("claims come from the card, status words are copied, dates carry the birth- const voice = professionalConversationVoice; // 原值:「再 Yoga(只写 yogas.hits 里成立的,写成立条件……)」;新值:状态照卡上原词写;原因:占星师 2026-11-07 评测意见(candidate 格局被写成「成立」,原句本身就要求写「成立的」)。 assert.doesNotMatch(voice, /yogas\.hits 里成立的/); - assert.match(voice, /状态词照卡上原词抄:candidate 写「候选」、computed 写「已计算,未验证」、strength_comparison_required 写「待定」;不升格为「成立」「确认」「命中」,标题里也不写「已成立」/); + // 原值:/状态词照卡上原词抄:candidate 写「候选」、computed 写「已计算,未验证」、strength_comparison_required 写「待定」;不升格为「成立」「确认」「命中」,标题里也不写「已成立」/ + // 新值:断言新的格局状态行(含映射表整串 + 其他状态词那句原文) + // 原因:BUG-1825:占星师 2026-11-09 第七轮回复 §5 P0.2,格局状态用 yoga-status-words.ts 的同一张映射表 + assert.ok(voice.includes(`- 格局(Yoga)的状态只按卡上 yogas_detail 那一行写,用这张表:${yogaStatusContractTable()};`), voice); + assert.match(voice, /其他状态词照卡上原词抄:candidate 写「候选」、strength_comparison_required 写「待定」;不升格为「成立」「确认」「命中」,标题里也不写「已成立」/); // 原值:无;新值:点位和宫主只照卡上字段写、一个点一个值,卡上没给的大运层写「未交付」;原因:占星师 2026-11-07 评测意见(A11 表格写狮子、正文写金牛/11 宫;三次运行把下一个 Narayana 主运处女写成下一个子运)。 assert.match(voice, /同一个点在正文、表格、审计表里只有一个值/); assert.match(voice, /卡上没给的大运层(比如下一个子运)写「未交付」,不拿下一个主运或别的体系顶替/); diff --git a/references/product_runtime_contract.md b/references/product_runtime_contract.md index fa3a0bd6..7a35873c 100644 --- a/references/product_runtime_contract.md +++ b/references/product_runtime_contract.md @@ -192,8 +192,8 @@ evidence_card.backstage records, for the backend only, that global web evidence 5. 不写没有区分度的推论:逆行不等于「反复、打回来」,也不等于「拖得久、拖出来」,推论里不拿逆行当理由(依据里可以列);宫主不在本宫不等于「往外跑」;某星不在某宫不等于「离得远」;空宫不单独下结论。 6. 不断言死亡、离婚、疾病诊断、流产、不孕。 - -7. 瑜伽只认 yogas.hits:hits 里的才是这张盘成立的格局;只在 names 里、没进 hits 的是待检规则,不得说成本人的格局。 + +7. 格局只按卡上 yogas_detail 每一行的状态说,状态文字用这张表:{{YOGA_STATUS_TABLE}}。yogas.hits 只是检测器匹配到的规则名,不能据此写「成立」;只在 names 里、没进 hits 的是待检规则,不得说成本人的格局;没有完整条件检查,不写「不成立」「非成立」「落空」。 8. `traditional_yogas` 只用来说明力量,不单独下结论,不算受冲,也不抵消受冲。