fix(consult): plain wrapping for answer prose; stop reading the analysis checklist aloud
Independent Staging Quality Gate / publish (push) Canceled after 0s
Independent Staging Quality Gate / validate (push) Canceled after 12m33s

BUG-1252: answer prose drops text-wrap: pretty. WebKit (every iPhone browser)
applies it to the whole paragraph, so lines came out evenly short with a blank
strip on the right and every streamed character re-broke the lines above.

BUG-1253: the condensed checklists and shared reading open with an
analysis-only note; PLAIN_SPEECH_RULE (one definition) tells the writer not to
quote layer names, signal counts, rules or house-counting, caps evidence in
brackets at two, and gives everyday words for card fields with no Chinese name
(no coined names like 合力星). Marriage layers become everyday wording; career,
wealth and health say not to announce layers.

Full suite 4966 / fail 24, identical to baseline d2c22ca2 (4962 / 24); build
keeps / Static; first-load gzip unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-06 20:19:50 +08:00
co-authored by Claude Opus 5.5
parent d2c22ca206
commit 171a3f25ac
16 changed files with 219 additions and 20 deletions
+6
View File
@@ -1,5 +1,11 @@
# 印度占星 Skill 更新日志
## 2026-10-06 — 回答在手机上铺满一行、输出不抖;感情等回答不再念分析清单(未上线)
- iPhone 上回答每行都短一截、右边留一条空白,输出时前面的字跟着跳。原因是正文用了「让每行长短整齐」的排版,苹果内核会对整段生效、每来一个字重排一次。正文改成普通折行(BUG-1252)。
- 感情回答里出现「心动这一层」「成对居住的那一层」「从它数起的第 6 位」「合力星」这类话:模型把内部分析清单原样说了出来。清单开头写明只用于分析;婚恋三件事换成生活说法;给没有中文叫法的字段配白话对照,不许自造译名;括号依据最多两条(BUG-1253)。
- Skill 版本不变;不改数据库;不改模型、答题时钟和计费。
## 2026-10-06 — 普通对话首轮改成几段聊天,不再交汇报骨架(未上线)
- 问事业、财运、父母这类首轮,以前会按固定小标题往下写,最后再交一份「这周可以做的一件事」。现在首轮是几段人话:先给结论,再写最关键的依据,相关时带上时间。问到几个人就分段说,段首点名,不写小标题(BUG-1244)。
+29
View File
@@ -16749,3 +16749,32 @@
- 相关记录:BUG-1165、BUG-1168、BUG-1182(示范被照抄的前几轮)、BUG-1244。
- 复发自:BUG-1165 的「示范被当成规则照抄」。旧测试锁的是读法和职业形态,没有锁行动句,所以这次换了一类句子仍能抄出去。
- 修复版本:staging `e62d0a32`(含 `da226db1`;2026-10-06 部署核对:health gitCommit=e62d0a32,`/api/account` 401,`/login` 200;`da226db1` 自身门禁 run 1742 测试步失败,日志需 token 未读到,后续 run 1743 同代码通过)。真机清单未走,状态保持 fixed-pending-verify
## BUG-1252 | iPhone 上回答右侧一条空白,输出时正文抖动
- 状态:fixed-pending-verify(分支 `codex/consult-readable-20261006`;真机确认前不标 resolved)
- 首次发现 / 最近更新:2026-10-06 / 2026-10-06
- 来源:产品 iPhone 真机截图(staging,普通对话婚恋题)。
- 影响面:所有对话里的助手回答正文(普通咨询、生时校正旁白);首次引导气泡另有规则,未动。
- 现象:每行在约 19–20 个汉字处断开、连词也拆开,右侧留出固定空白;流式输出时已显示的行跟着换行位置变化,看起来在跳。
- 触发条件:iOS 上任何浏览器(均为 WebKit),回答段落较长时更明显(10-06 改为大段落后加重)。
- 根因:`globals.css` 的 `.message p, .message-markdown` 带 `text-wrap: pretty`(2026-07 起)。WebKit 对整段做优化,把各行调成接近等长,代价是每行短于可用宽度;每追加一个字就重算整段折行。Chrome 只调最后几行,所以桌面不明显。用本机 Chrome 按手机宽度渲染真实样式确认容器占满整行(排除宽度上限);WebKit 测试浏览器缺系统库未能起,未在 WebKit 内亲眼复现。
- 修复:答案正文去掉 `text-wrap: pretty` 与 `word-break: auto-phrase`(后者 WebKit 不支持),普通折行。
- 验证:`tests/consult-plain-speech-20261006.test.ts` 锁定该规则不含 `text-wrap`,且其他回答正文选择器不再加回 `pretty`。全量 4966 项 fail 24,与基线 d2c22ca2(4962 / 24)逐条一致;build 后 `/` Static,首屏 gzip 567,175 → 567,175 B。
- 防复发:同上测试;`frontend/DESIGN.md` 字体段写明答案正文例外。
- 相关记录:BUG-1244(首轮改大段落,放大了此现象)。
- 修复版本:待发布
## BUG-1253 | 情感等回答把内部分析清单原样念给用户,看不懂
- 状态:fixed-pending-verify(分支 `codex/consult-readable-20261006`;无模型凭据,真机复核前不标 resolved)
- 首次发现 / 最近更新:2026-10-06 / 2026-10-06
- 来源:同 BUG-1252 截图。
- 影响面:普通咨询各领域回答的措辞;工具结果里的精简清单、通用读法;首轮与追问轮指令。不改证据卡、工具、打分。
- 现象:「先说心动这一层」「再看成对居住的那一层」「宫主火星落在从它数起的第 6 位」「这条只说这颗星自己有劲,不说明事情就顺」「合力星」;括号里一次列四条信号。
- 根因:①婚恋精简清单写「分三层说:①心动接触 ②关系成对 ③社会法律落地」,是给模型分步判断的,模型把层名当成说法;②通用读法的受冲条件「宫主落在从该宫数起的第 6、8、12 宫」、规则句「旺弱只说明这颗星自己有没有力气」被搬进正文;③证据卡字段 `yogakarakas` 只有英文名,名词规则又禁音译,模型自造「合力星」;④`AFFLICTION_RANGE_RULE` 要求「括号里写是哪几条」,与「每段最多一处括号、一两条依据」冲突,模型选了列全。清单比「人话自检」更具体,压过了它。
- 修复:`consultation-thinking-plan.ts` 新增 `CHECKLIST_ANALYSIS_ONLY_NOTE`(清单开头一行:分析用,不写进回答)与 `PLAIN_SPEECH_RULE`(清单只用来想;括号最多两条;没有日常叫法的字段白话对照,不自造译名),进首轮、追问、ANSWER SHAPE 与英文形状摘要;婚恋三层改为「遇到喜欢的人、谈恋爱 / 定下来、长期在一起 / 结婚、领证」并写「不报层名和编号」,事业、财运、健康同样注明;受冲规则括号改为「只写最能说明问题的一两条,用白话,不列全」。
- 验证:新增 `consult-plain-speech-20261006.test.ts` 4 条;改断言 4 处(三栏写在测试里)。无模型凭据,没有真跑新提示词,真机复核见 `docs/testing/consult-readable-20261006.md`。
- 防复发:测试锁清单不含旧层名(心动接触 / 关系成对 / 社会法律落地 / 分三层说 / 压力窗 / 恢复窗)、每份清单以用途说明开头、白话对照与括号上限存在。
- 相关记录:BUG-1148(名词只用中文;本次补上没有中文叫法时怎么说)、BUG-1244、BUG-1160~1163(通用读法与精简清单)。
- 修复版本:待发布
@@ -0,0 +1,34 @@
# PROGRESS:回答铺满一行、输出不抖、不念分析清单 — 2026-10-06
产品看真机截图后要求直接执行(无任务书,诊断见本记录与 BUG-1252 / BUG-1253)。基线 `origin/staging` `d2c22ca2`,分支 `codex/consult-readable-20261006`,Claude 实现并自验。
## 做了什么
| 项 | 改动 |
| --- | --- |
| BUG-1252 | `globals.css`:`.message p, .message-markdown` 去掉 `text-wrap: pretty`、`word-break: auto-phrase`。首次引导气泡的 `.onboarding-message .message-markdown p` 规则保留(短句、有词组保护逻辑,不在本次范围) |
| BUG-1253 | `consultation-thinking-plan.ts` 新增 `CHECKLIST_ANALYSIS_ONLY_NOTE`、`PLAIN_SPEECH_RULE`,进首轮正文指令、追问指令、`product-voice.ts` ANSWER SHAPE 与英文形状摘要;`natalSpokenReportContract` 的 Never 加一条不复述清单;`AFFLICTION_RANGE_RULE` 括号改为一两条白话 |
| BUG-1253 | `consultation-condensed-checklist.ts`:通用读法与每份领域清单开头一行用途说明;婚恋「冻结判定对象」与三层改生活说法;事业、财运、健康三层注明不报层名(健康「压力窗 / 恢复窗」改为「压力集中的时段 / 恢复得好的时段」) |
## 断言三栏
| 位置 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `consult-affliction-reading-20261001` T1 | 通用读法以「1. 先看受冲」开头 | 先是用途说明一行,再「1. 先看受冲」 | BUG-1253 |
| `consult-gender-card-20260927` | 清单行数 = 条数 | 条数 + 1 | 同上 |
| `consult-condensed-checklist-20260927` | 婚恋必含「心动接触 / 关系成对 / 社会法律落地」 | 必含三件事的生活说法与「不报层名和编号」 | 同上 |
| `consult-no-presupposition-20261001` | 受冲规则「括号里写是哪几条」 | 「括号里只写最能说明问题的一两条,用白话,不列全」 | 同上 |
## 验证(Linux + Node 22.14)
| 项 | 基线 d2c22ca2 | 本分支 |
| --- | --- | --- |
| `tsc --noEmit` | — | 0 错 |
| `npm run lint` | — | 0 error / 126 warning |
| `npm test` | 4962 项 / fail 24 | 4966 项 / fail 24,失败名单逐条一致(无 Docker 的数据库 / 部署类) |
| `next build` | `/` ○ Static,首屏 gzip 567,175 B | `/` ○ Static,567,175 B |
## 环境缺口
- WebKit:本机 Playwright WebKit 缺 `libevent` 等系统库起不来,没有在 WebKit 内亲眼复现与复核;Chrome 按手机宽度渲染确认容器占满整行。真机清单第 1、2、7 步。
- 模型:无模型凭据,新提示词没有真跑;真机清单第 3~6 步。
+1
View File
@@ -426,3 +426,4 @@
| `TASK-rectification-delivery-dup-adopt-20261006.md` | `PROGRESS-rectification-delivery-dup-adopt-20261006.md` | **校正交付一次两段回答 + 点「改用…盘解读」被拒 `adoption_not_allowed`**(10-06 真机):收尾补位按文字子串防重,遇采用旁白 Agent 改写版失效(BUG-1153 复发);accept/GET 调 `decideFromDossier` 不带出生日期,未成年探针复活致判定与出卡时相反(BUG-598/680 同族);卡片按钮不看公开 can_adopt、拒绝码直出英文。不放宽采用门,判定入参统一 + 结构防重 + 按钮判据 + 中文提示(BUG-1241~1243) | **已验收,待推 staging**(Claude 10-06:首版 `b977f482` 验收 T1/T3 过、T2 未过;修复单 F1 由 Claude 直接执行——防重改为「上一条包含交付句」,真实答题路径测试修前红修后绿;F2 疑似 BUG-1246 复核不成立已撤回、留守护测试。全量 npm 失败名与基线逐名相同、tsc 0、lint 0 error;PG17 重放未做,见 PROGRESS) | 分支 `codex/rectification-delivery-dup-adopt-20261006`;修复单 `TASK-rectification-delivery-dup-adopt-fix-20261006.md` |
| `TASK-consult-conversational-answer-20261006.md` | `PROGRESS-consult-conversational-answer-20261006.md` | **普通对话首轮去汇报骨架,改成聊天**(10-06 产品反馈「像机器在汇报」):首轮取消全部 `##`(三个固定节 + 按对象标题,多人改分段落点名)、行动不强制(盘上有具体指向才顺口一句,问「怎么办」再展开)、首轮三到六段;示范删可抄的行动句(实测被逐字照抄);思考栏去「这周可以做什么」。推翻 10-01 D2 与 D8 首轮部分(BUG-1244~1245) | 已验收 | 28113fa0 验收未过 → 修复单 `TASK-consult-conversational-answer-fix-20261006.md` 由 Claude 直接执行(含去掉助手头像),复验通过后推 staging;真机清单 10 步待产品 |
| `TASK-rectification-in-chat-step1-20261006.md` | `PROGRESS-rectification-in-chat-step1-20261006.md` | **生时校正并入聊天 · 第一步**:删首页「生时校正」按钮与文字口令;普通对话里用户说不准时由 agent 分清读法 / 时间问题,时间问题出提议卡(本人、开关、价格服务端校验,同会话最多主动提一次);点卡带来源打开校正;采用 / 未采用 / 手动返回都回到原对话,分隔线 + 旧回答标「按校正前的时间」+ 点了才发的「按新时间重新看」;模型历史标注旧时间。须等 dup-adopt 单合入后开工;第二步(卡片嵌进同一对话)另立单(BUG-1247~1251) | 待领取 | — |
| —(直接执行,无任务书) | `PROGRESS-consult-readable-20261006.md` | **回答铺满一行、输出不抖、不念分析清单**(10-06 真机截图):答案正文去 `text-wrap: pretty`(WebKit 整段等长 + 逐字重排,BUG-1252);清单开头写明只用于分析、婚恋三层改生活说法、字段白话对照、括号最多两条(BUG-1253) | 已验收 | Claude 直接执行;真机清单 `docs/testing/consult-readable-20261006.md` 待产品 |
+13
View File
@@ -0,0 +1,13 @@
# 真机清单 · 回答铺满一行、输出不抖、不念分析清单(2026-10-06)
对应 BUG-1252、BUG-1253。在 staging 上用 iPhone 走(任意浏览器都行),deepseek-v4-flash 走一遍,再换一个模型走一遍。回答原文不记进仓库。
| 步 | 怎么做 | 通过标准 |
| --- | --- | --- |
| 1 | 新开对话问「我和我的伴侣能走下去吗」,看回答 | 每行基本写到右边,右侧没有一条固定空白 |
| 2 | 第 1 步回答生成时盯着已出现的几行 | 前面已经显示的字不跳动,新字只往后接 |
| 3 | 读第 1 步的回答 | 没有「心动这一层」「成对」「落地」这类层名;没有「从它数起第几宫/第几位」;没有「合力星」或其他自造名词;没有「这条只说明……不说明……」这类讲规则的话 |
| 4 | 看括号 | 每个括号最多两条依据,读得懂大意 |
| 5 | 新开对话问「我今年财运怎么样」「我的事业接下来怎么走」 | 同第 3、4 步 |
| 6 | 问「我身体怎么样」 | 没有「压力窗」「恢复窗」这类词 |
| 7 | 桌面浏览器再看一次第 1 步 | 排版正常,没有变挤或变稀 |
+1 -1
View File
@@ -285,7 +285,7 @@ is read through an external store so a change in one tab reaches the others.
| `--type-caption` | `13px` | 500 | 1.4 | 0 | Labels and metadata |
| `--type-overline` | `12px` | 500 | 1.4 | `1.5px` | Eyebrows and badges |
Display headings use `--font-display` (self-hosted Jyotisha Serif SC, sans fallback) at weight 500; the `--type-display-*` rows above list 400 from the earlier serif era, but every display rule now carries 500 or 600 (BUG-737), which the SemiBold face covers. Body copy never drops below 14px; 12–13px is reserved for short labels and metadata. No product UI text is smaller than `--type-overline` (12px). Product UI uses three font weights: 400 (display and body), 500 (UI titles, labels, buttons), and 600 (emphasis only). CJK text uses `text-wrap: pretty`; display text uses `text-wrap: balance`.
Display headings use `--font-display` (self-hosted Jyotisha Serif SC, sans fallback) at weight 500; the `--type-display-*` rows above list 400 from the earlier serif era, but every display rule now carries 500 or 600 (BUG-737), which the SemiBold face covers. Body copy never drops below 14px; 12–13px is reserved for short labels and metadata. No product UI text is smaller than `--type-overline` (12px). Product UI uses three font weights: 400 (display and body), 500 (UI titles, labels, buttons), and 600 (emphasis only). CJK text uses `text-wrap: pretty`, **except answer prose** (`.message p`, `.message-markdown`), which wraps plainly since 2026-10-06 (BUG-1252): WebKit — every iPhone browser — applies `pretty` to the whole paragraph, so answer lines came out evenly short with a blank strip on the right, and each streamed character re-broke the lines above it. The first-run onboarding bubbles keep their own phrase-protected rule. Display text uses `text-wrap: balance`.
One documented exception: the thinking text inside a timeline step (`.consultation-run-timeline__thinking`) and the fallback thinking trace (`.message-thinking-body`) render at 13px. They are working notes shown on request inside a collapsed row, not reading copy; the answer itself never inherits that size.
+8
View File
@@ -22,6 +22,14 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
首页开场语下面可以有一行今日趋势(今日星语卡片的 trend,每日生成);还没生成时写「今天的星语还没写出来。」,没有出生分钟时不写。入口按钮下面平时不写字,只在用户需要动手时写一句:有没做完的校正写「上次那次校正还没完成,可以在历史对话里接着做。」;当前人物不是本人写「生时校正暂时只支持本人。」。不再写「不确定出生时间时,用记得住的经历一步步缩小范围」「上次已经校正完,可以拿最新资料再来一次」,也不要用「·」把两句不相干的提示拼成一行。
## 分析清单不说给用户(2026-10-06,BUG-1253)
工具结果里的清单(受冲信号编号与条数、「分三件事看」的层名、「旺弱不说明顺不顺」这类规则、「从某宫数起第几宫」)是让模型判断用的,回答里只说判断结果。真机婚恋回答出现过「先说心动这一层」「再看成对居住的那一层」「宫主火星落在从它数起的第 6 位」「这条只说这颗星自己有劲,不说明事情就顺」,以及给 yogakaraka 自造的「合力星」——读者一句都看不懂。
- 婚恋三件事的说法:遇到喜欢的人、谈恋爱 / 定下来、长期在一起 / 结婚、领证;只说用户问到的那件,不报层名。
- 括号里的依据最多两条,挑最能撑住这句话的,用白话写。
- 没有日常中文叫法的字段按 `PLAIN_SPEECH_RULE`(`consultation-thinking-plan.ts`,只定义这一处)的对照说:功能凶星 → 对你这个上升来说偏添麻烦的星;yogakaraka → 不提或「这张盘里最帮你的那颗星」,不自造译名;DK → 代表伴侣的星;落陷 → 力气弱……对照里没有的名词就不提。
## 内容审核的三句话(2026-09-30,合规轮)
- 用户输入被拦:「这个话题我们不能讨论,换个问题试试。」——一句,不说教、不解释规则、不说「违规」。
+6 -1
View File
@@ -1615,7 +1615,12 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.rectification-analysis-content li span { min-width: 0; color: var(--color-ink-secondary); }
.rectification-analysis-content li small { flex: 0 0 auto; color: var(--color-ink-muted); }
.rectification-analysis-content p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.55; }
.message p, .message-markdown { color: var(--color-ink-strong); font-size: var(--type-body-md); line-height: 1.65; text-wrap: pretty; word-break: auto-phrase; }
/* No `text-wrap: pretty` on answer prose (BUG-1252). WebKit (every iPhone
browser) applies it to the whole paragraph: lines come out evenly short with
a blank strip on the right, and every streamed character re-breaks the
lines above it, so a growing answer jitters. Plain wrapping fills the line
and only ever adds to the last one. */
.message p, .message-markdown { color: var(--color-ink-strong); font-size: var(--type-body-md); line-height: 1.65; }
.message-evidence-status { margin: var(--space-3) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--color-border); color: var(--color-ink-tertiary); font-size: var(--type-body-sm); line-height: 1.5; }
.message-user p { line-height: 1.55; color: var(--color-ink); font-size: var(--type-body-sm); }
.message-markdown h2, .message-markdown h3 { margin: 24px 0 10px; color: var(--color-ink); font-family: var(--font-display); font-weight: 500; letter-spacing: -.3px; }
@@ -1,6 +1,6 @@
import type { ConsultationDomain } from "./consultation-domain-registry.ts";
import type { ProfileGender } from "./profile-gender.ts";
import { AFFLICTION_RANGE_RULE, CAREER_FIELD_ASK_RULE } from "./consultation-thinking-plan.ts";
import { AFFLICTION_RANGE_RULE, CAREER_FIELD_ASK_RULE, CHECKLIST_ANALYSIS_ONLY_NOTE } from "./consultation-thinking-plan.ts";
/**
* Condensed consultation checklists (TASK-consult-evidence-card-v2-20260927
@@ -69,7 +69,7 @@ export const CONDENSED_SHARED_READING_LINES: readonly string[] = [
/** The shared reading as one numbered block. */
export function sharedConsultationReading(): string {
return CONDENSED_SHARED_READING_LINES.map((line, index) => `${index + 1}. ${line}`).join("\n");
return [CHECKLIST_ANALYSIS_ONLY_NOTE, ...CONDENSED_SHARED_READING_LINES.map((line, index) => `${index + 1}. ${line}`)].join("\n");
}
/**
@@ -84,15 +84,15 @@ export const CONSULTATION_CONDENSED_CHECKLISTS: Readonly<Partial<Record<Consulta
CAREER_FIELD_ASK_RULE,
"必看:D1 10 宫与 10 宫主、AmK;卡上 d9 段确认 10 宫主与 AmK 的旺弱、Vargottama、D1/D9 反转;D10 上升与 10 宫;AL 与 A10;10 宫 SAV 与木星、土星所在宫的 SAV。",
"时间:Vimshottari(MD/AD/PD)与 Narayana 双轨同向才谈应期,看大运主与 10 宫、10 宫主、D10 的关系;每颗星先按功能吉凶定性。",
"分三层说,不得合成一句「事业机会」:①机会出现(消息、邀约、初步接洽)②成形(合同、合作、长期项目)③公开落地(发布、到账、被看见、名声)。",
"分三件事看(回答里用生活说法,不报层名和编号),不得合成一句「事业机会」:①机会出现(消息、邀约、初步接洽)②成形(合同、合作、长期项目)③公开落地(发布、到账、被看见、名声)。",
"禁写:本命承诺弱时,不得因一段大运或一次行运断言「事业必成」;不得把接触窗写成落地窗。",
"AL 落第几宫要说出来(说明名声和外界印象这条线有没有力量,不据此断定公众型或幕后型);10 宫主、AmK 的受冲按通用读法。",
"卡外按需补取一次:Karakamsha、宫主链、D1→D9→D10 联动、Argala、Chara 大运;KP 精确宫头仍 blocked,只作参考、不作依据。",
],
marriage: [
"第一句先冻结判定对象:心动接触 / 关系成对(契约)/ 社会法律落地;对象不清就先说清楚再往下。",
"先弄清问的是哪一件:遇到喜欢的人、谈恋爱 / 定下来、长期在一起 / 结婚、领证;问得不清就先问清楚再往下。",
"必看:D1 7 宫与 7 宫主、5 宫与 5 宫主、金星、木星、DK、UL;卡上 d9 段(D9 上升、金木与 DK 的旺弱);昼夜盘;Vivah Saham 的度数与落宫。",
"分三层说:①心动接触:5 宫、5 宫主、Punarphoo 观察 ②关系成对:7 宫主、DK、UL ③社会法律落地:D9 + UL + Vivah Saham + 双大运同向。",
"分三件事看,回答里只说用户问到的那件、用这里的生活说法,不报层名和编号:①遇到喜欢的人、谈恋爱:5 宫、5 宫主、Punarphoo 观察 ②定下来、长期在一起:7 宫主、DK、UL ③结婚、领证:D9 + UL + Vivah Saham + 双大运同向。",
"时间:Vimshottari 与 Narayana 双轨;双重过运(木星、土星同时激活 7 宫 / 7 宫主 / DK / UL)只是激活窗,不是事件日。",
CONDENSED_GENDER_LINES.unknown,
"7 宫主或金星与罗睺、计都同宫,火星照 7 宫,UL 第 2 宫有凶星,伴侣代表星落 6、8、12:受冲两个及以上就写「感情这条线上有波折的迹象」,再给范围(可能是来得晚、有分合,也可能是同一段关系里摩擦多),不断定有几段;不预设用户现在有伴侣或单身。",
@@ -102,7 +102,7 @@ export const CONSULTATION_CONDENSED_CHECKLISTS: Readonly<Partial<Record<Consulta
wealth: [
"必看:D1 2、11、5、9 宫及宫主,8 宫(共享资源、突然得失)与 12 宫(支出、外流);木星、金星、水星;D2、D11;2、11 宫 SAV 与最强、最弱星座;卡上 d9 段确认 2 宫主、11 宫主的旺弱。",
"时间:2、11、5、9 宫主(收入来自工作时加 10 宫主)大运是否激活,Vimshottari 与 Narayana 双轨同向;每颗星先按功能吉凶定性。",
"分三层说:①挣钱机会 ②收入或资产真实增长 ③到账变现;不得合成一句「财运好」。",
"分三件事看(回答里用生活说法,不报层名和编号):①挣钱机会 ②收入或资产真实增长 ③到账变现;不得合成一句「财运好」。",
"禁写:本命承诺弱时,不得因一次行运或一段大运断言「发财」;不给投资建议、不保证收益。",
"8 宫也管版税、继承、意外之财,不只读成纠纷;2、11 宫主与 9 宫主同宫或互照是财富组合。",
"卡外按需补取一次:收入来自工作时补取 D10;Argala、KP 到账类(KP 精确宫头仍 blocked,只作参考、不作依据)。",
@@ -155,7 +155,7 @@ export const CONSULTATION_CONDENSED_CHECKLISTS: Readonly<Partial<Record<Consulta
"先讲体质底子:命宫、命主、月亮、太阳的强弱与受冲。受冲两个及以上写「身体这条线有压力的迹象」并给范围(可能是容易累、恢复慢,也可能是某些阶段压力集中),不写「底子不差」,不诊断。",
"必看:6 宫(疾病)、8 宫(慢性、手术)、12 宫(住院)及宫主;土星、火星;D6、D8、D30 上命主、6 宫主、8 宫主落第几宫、和谁同宫。",
"宫位对应的身体部位只作提示(如 4 宫胸、心、肺),不写成诊断。",
"分三层:一生底子;压力窗(当前大运子运激活 1、6、8、12 宫主);恢复窗。",
"分三件事看(回答里用生活说法,不报层名):一生的身体底子;压力集中的时段(当前大运子运激活 1、6、8、12 宫主);恢复得好的时段。",
"禁写:诊断、病名、治疗建议、手术或死亡的断言;保留「这不是医疗意见」。",
],
};
@@ -172,8 +172,10 @@ export function condensedConsultationChecklist(
const lines = CONSULTATION_CONDENSED_CHECKLISTS[domain];
if (!lines) return null;
const genderLine = options.gender ? CONDENSED_GENDER_LINES[options.gender] : CONDENSED_GENDER_LINES.unknown;
return lines
.map((line) => (line === CONDENSED_GENDER_LINES.unknown ? genderLine : line))
.map((line, index) => `${index + 1}. ${line}`)
.join("\n");
return [
CHECKLIST_ANALYSIS_ONLY_NOTE,
...lines
.map((line) => (line === CONDENSED_GENDER_LINES.unknown ? genderLine : line))
.map((line, index) => `${index + 1}. ${line}`),
].join("\n");
}
+17 -2
View File
@@ -20,13 +20,26 @@ export const REPORT_HEADING = {
action: "这周可以做的一件事",
} as const;
/**
* The domain checklists and the shared reading are written for the model's
* reasoning (layers, numbered signals, rules about what strength does not
* mean). A 10-06 real-device marriage answer quoted them to the reader —
* 「心动这一层」「宫主落在从它数起的第 6 位」「这条只说这颗星自己有劲」 — and coined
* 「合力星」 for a card field that only has an English name (BUG-1253). One
* statement here: what the checklists are for, and the plain words for the
* card fields that have no everyday Chinese name.
*/
export const CHECKLIST_ANALYSIS_ONLY_NOTE = "(分析用:本清单的层名、编号、计数和规则原句只决定你怎么判断,不写进回答,也不对用户复述;说给用户的是判断结果,用生活里的话。)";
export const PLAIN_SPEECH_RULE = "分析清单只用来想,不用来说:工具结果里的清单(受冲信号的编号与条数、「分三件事看」的层名、「旺弱不说明顺不顺」这类规则原句、「从某宫数起第几宫」这类数法)只决定你怎么判断,不写进回答,也不对用户解释规则;说给用户的是判断的结果,用生活里的话。括号里的依据最多两条,挑最能撑住这句话的,用白话写,不把信号列全。盘上字段没有日常中文叫法时按这张对照说,对照里没有的名词就不提:功能吉星 → 对你这个上升来说帮得上忙的星;功能凶星 → 对你这个上升来说偏添麻烦的星;yogakaraka → 不提,或说这张盘里最帮你的那颗星,不自造译名;DK → 代表伴侣的星;UL、Vivah Saham、Punarphoo → 不提名字,说婚姻这条线、结婚的时机;AmK → 代表事业的星;MK、PiK、PK → 代表母亲、父亲、孩子的星;AL、A10 → 外人眼里的你、事业在外人眼里的样子;SAV、Shadbala → 只说强、中、弱;被照(aspected_by)→ 某星照着;燃烧(combust)→ 被太阳压住;落陷 → 力气弱;Narayana 大运 → 不提名字,说两套算法都指向这段时间。";
/**
* The one statement of the first-turn natal answer shape (TASK-consult-
* conversational-answer-20261006 D1–D3/D5/D6): every English prompt that names
* the shape quotes this, and the Chinese user-turn instruction below says the
* same thing.
*/
export const NATAL_ANSWER_SHAPE_SUMMARY = `On the first natal answer of a session, write prose paragraphs and no Markdown headings. The first sentence is the conclusion. When the question names more than one person or sub-question, give each one its own paragraph and name them in the first clause (你妈妈这边……, 爸爸那边……); never merge them and never title the paragraphs. One person or one matter is not split by person. Separate paragraphs with a blank line; a single line break does not start a new paragraph on screen. Cite chart evidence in parentheses, at most once per paragraph, and only the one or two facts that hold the judgment. An action is not required: mention one only when the chart points at a concrete thing tied to this question, weave that one sentence into the prose, never as a list and never in a fixed place, and at most once in the whole answer; when the user asks what to do, that turn may give several. The first answer is usually three to six paragraphs — the conclusion, the one or two strongest grounds, and timing when it bears — and does not fill every topic to look complete. There is no hard cutoff. Say each thing once. Do not write a Markdown heading, and do not write section names such as 这周可以做, 盘上依据, or 时间怎么看, and do not bold a short phrase to fake a heading. Default to paragraphs; use a list only when the user asked for one or there are three or more candidate time windows, and never as a bold label plus one clause. Every sentence must still make sense to a reader who knows no astrology once its parentheses are removed: no coined pattern names, no periods written as people who push or tidy up, and no conclusion from an empty house alone. Terms are Chinese only and one name per thing (罗睺, 计都; 大运 → 子运 → 小运); no English or Sanskrit transliterations such as sade sati, vargottama or rupas, and strength is said as 强 / 中 / 弱.`;
export const NATAL_ANSWER_SHAPE_SUMMARY = `On the first natal answer of a session, write prose paragraphs and no Markdown headings. The first sentence is the conclusion. When the question names more than one person or sub-question, give each one its own paragraph and name them in the first clause (你妈妈这边……, 爸爸那边……); never merge them and never title the paragraphs. One person or one matter is not split by person. The tool's checklists are for reasoning only: never quote their layer names, signal counts, numbered rules or house-counting, and say card fields in the plain words of the 白话对照 rule. Separate paragraphs with a blank line; a single line break does not start a new paragraph on screen. Cite chart evidence in parentheses, at most once per paragraph, and only the one or two facts that hold the judgment. An action is not required: mention one only when the chart points at a concrete thing tied to this question, weave that one sentence into the prose, never as a list and never in a fixed place, and at most once in the whole answer; when the user asks what to do, that turn may give several. The first answer is usually three to six paragraphs — the conclusion, the one or two strongest grounds, and timing when it bears — and does not fill every topic to look complete. There is no hard cutoff. Say each thing once. Do not write a Markdown heading, and do not write section names such as 这周可以做, 盘上依据, or 时间怎么看, and do not bold a short phrase to fake a heading. Default to paragraphs; use a list only when the user asked for one or there are three or more candidate time windows, and never as a bold label plus one clause. Every sentence must still make sense to a reader who knows no astrology once its parentheses are removed: no coined pattern names, no periods written as people who push or tidy up, and no conclusion from an empty house alone. Terms are Chinese only and one name per thing (罗睺, 计都; 大运 → 子运 → 小运); no English or Sanskrit transliterations such as sade sati, vargottama or rupas, and strength is said as 强 / 中 / 弱.`;
/** The declared-window reply still opens its stable-layer section with this heading. */
export const WINDOW_OPEN_HEADING = "先回答你的问题";
@@ -386,7 +399,7 @@ const ANSWER_FROM_EVIDENCE = "拿到本轮计算结果后直接写给用户的
* "managing / arranging / close" from the 4th lord's house or a benefic in
* the 4th, and two asks stacked; the rule now names both.
*/
export const AFFLICTION_RANGE_RULE = "同一个人或同一件事有两个及以上受冲信号:先说这条线有压力或距离的迹象(括号里写是哪几条),再说现实里可能是什么,给一个范围(例:「现实里可能是分开、离得远,也可能只是关系紧、话少」);不替用户断定他自己更清楚的事实(父母在不在身边、几段婚姻或感情、有没有病),也不反过来写「有分量」「站得住」「不是缺位」「底子不差」「关系稳」「能走下去」;不拿宫主落哪一宫、宫里有吉星去描写这个人对你做什么(「在管」「在安排」「心思在你的前途上」「照顾是实的」都预设人在身边,宫主落宫和吉星只说这条线另有支撑的迹象),这一方的行动建议也不写给他打电话、去问他;这一段末尾用一句话请用户说说实际情况,全篇这类请求最多一处,问过之后别的段落不再另问。";
export const AFFLICTION_RANGE_RULE = "同一个人或同一件事有两个及以上受冲信号:先说这条线有压力或距离的迹象(括号里只写最能说明问题的一两条,用白话,不列全),再说现实里可能是什么,给一个范围(例:「现实里可能是分开、离得远,也可能只是关系紧、话少」);不替用户断定他自己更清楚的事实(父母在不在身边、几段婚姻或感情、有没有病),也不反过来写「有分量」「站得住」「不是缺位」「底子不差」「关系稳」「能走下去」;不拿宫主落哪一宫、宫里有吉星去描写这个人对你做什么(「在管」「在安排」「心思在你的前途上」「照顾是实的」都预设人在身边,宫主落宫和吉星只说这条线另有支撑的迹象),这一方的行动建议也不写给他打电话、去问他;这一段末尾用一句话请用户说说实际情况,全篇这类请求最多一处,问过之后别的段落不再另问。";
/**
* Career answers (product decision 2026-10-02 on TASK-consult-career-yoga-
@@ -431,6 +444,7 @@ export function natalAnswerShapeBody(): string {
"首轮通常三到六段:讲清结论、最关键的一两条依据,相关时带上时间;不为显得周全把各方面写满。不设硬截断。同一件事只说一遍。",
"正文写成段落,段落之间空一行(单个换行在界面上不分段)。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用加粗标签加一句的格式。",
"首轮正文不写 ##,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。",
PLAIN_SPEECH_RULE,
"把每句话括号里的内容删掉,不懂占星的人也要能看懂:不起「外X内Y」这类格局名,不把大运写成推着或收尾的角色,不写「去当某颗星的样子」,不把星名当形容词的主人;空宫不单独下结论,要说它的宫主落在哪。名词只用中文、全篇一个叫法(罗睺、计都;大运、子运、小运),不写英文或梵文音译(sade sati、vargottama、rupas 之类),力量只说强、中、弱。",
"不要写「先回答你的问题」这个标题,不要写「统一参数与原始结构」,不要写技法审计表,不要写思考过程清单,不要复述内部 JSON 字段。",
].join("");
@@ -455,6 +469,7 @@ export function followUpAnswerShapeInstruction(): string {
return [
ANSWER_FROM_EVIDENCE,
"这是追问轮:本会话前面已经写过完整解读。第一句就回答用户这句话(是非题先答「是」「不是」或「看情况:……」,再给盘上的依据),讲清楚为准,不限字数;只引与这句话直接相关的盘面事实,够说明就停;用人话说,盘上依据放括号里;名词只用中文、不写英文或梵文音译;不写二级标题,不写行动清单(用户问了才给);不重述上一轮已经写过的结构;不解释用户为什么问,不替用户说他想要什么。",
PLAIN_SPEECH_RULE,
FOLLOW_UP_CORRECTION_RULE,
CAREER_FOLLOW_UP_RULE,
"只有用户明确换了领域或要求完整看一下,才回到首轮形状:几段人话,多人就分段落、段首点名,不写二级标题。",
+3 -1
View File
@@ -1,4 +1,4 @@
import { NATAL_ANSWER_SHAPE_SUMMARY, NATAL_SECTION_RULE } from "../lib/consultation-thinking-plan.ts";
import { NATAL_ANSWER_SHAPE_SUMMARY, NATAL_SECTION_RULE, PLAIN_SPEECH_RULE } from "../lib/consultation-thinking-plan.ts";
export const productConversationVoice = `VISIBLE VOICE
PERSONA: 把对面的人当一个人认真对待——不是工单,不是要被安抚的情绪,不是一张等着填完的表。你不是只会点头的工具人,更像他身边一个行动力很强、嘴有点毒但靠谱的同事:给你一个问题,你自己找路、自己补信息、把脏活干完,把结论丢回他面前。
@@ -35,6 +35,7 @@ ANSWER SHAPE (natal / general / declared-window 三种模式共用)
- 括号外每段最多一个占星术语,第一次出现时用白话套住(例:「现在这十年(月亮大运)」「4 宫(母亲宫)」)。
推断边界:空宫不单独下结论——要说这个宫,就说它的宫主落在哪、什么状态。只用卡上和补取到的事实。
名词只用中文,同一个东西全篇一个叫法:罗睺、计都(不写克图、凯图、Rahu、Ketu);大运 → 子运 → 小运(不写中运、副运、小段);九分盘(D9)、事业分盘(D10)这类分盘第一次出现时带一句白话。不写英文或梵文音译(sade sati、vargottama、rupas、yoga 名的拉丁拼写等):「土星经过你月亮附近的那几年」代替 sade sati,「这颗星在本命盘和九分盘落在同一个星座」代替 vargottama;力量只说强、中、弱,不写 rupas 数值。
${PLAIN_SPEECH_RULE}
降级路线(形状照给,只换依据):
- 申报时段(declared birth window):依据只取窗口内稳定的那一层——行星落座、公开日历;上升、宫位、大运、分盘不作依据,也不写月份。
- 无出生分钟(no birth minute):时间只讲公开日历这一天的大势,不点个人大运,不写月份;能做的事取自公开日历,不取自个人盘。
@@ -94,5 +95,6 @@ Never:
- State what the user or the person asked about wants, needs, or feels. Say where the chart puts them instead.
- Draw a conclusion from an empty house alone. Say where its lord sits and in what state instead.
- Coin a name for the pattern, write a period as a person who pushes or tidies up, or make a planet's name the owner of an adjective.
- Quote the tool's checklists to the reader: layer names (心动 / 成对 / 落地), signal counts, numbered rules, house-counting, or a rule about what strength does not mean. Those decide the judgment; the reader gets the judgment in everyday words.
The first natal career/wealth/marriage/family answer of a session uses the paragraph shape above; a follow-up turn follows the shape instruction carried in the user turn (answer the sentence directly, no headings, no restart of the earlier reading) unless the user changes domain or asks for the full reading, which returns to the same paragraph shape.`;
@@ -17,6 +17,7 @@ import {
CONSULTATION_CONDENSED_CHECKLISTS,
} from "../src/lib/consultation-condensed-checklist.ts";
import { consultationMethodologyForDomains } from "../src/lib/consultation-methodology.ts";
import { CHECKLIST_ANALYSIS_ONLY_NOTE } from "../src/lib/consultation-thinking-plan.ts";
import {
consultationDomainDefinition,
consultationDomainIds,
@@ -54,7 +55,10 @@ test("T1: the shared reading is the first section, once per turn, whatever the p
assert.equal(list.filter((title) => title === CONDENSED_SHARED_READING_TITLE).length, 1, plan.join("+"));
}
const text = consultationMethodologyForDomains(["parents"])!.sections[0]!.text;
assert.ok(text.startsWith("1. 先看受冲,再看旺弱。"));
// 原值: assert.ok(text.startsWith("1. 先看受冲,再看旺弱。"))
// 新值: 先是一行「分析用,不写进回答」说明,第二行才是 1. 先看受冲
// 原因: BUG-1253:清单的层名、计数、规则原句被原样写给用户,清单开头加一行用途说明
assert.ok(text.startsWith(`${CHECKLIST_ANALYSIS_ONLY_NOTE}\n1. 先看受冲,再看旺弱。`));
assert.match(text, /两个及以上受冲信号/);
assert.match(text, /逆行不等于「反复、打回来」/);
// The line added on top of the brief's six: only hits are yogas this chart has.
@@ -101,7 +101,10 @@ test("the writer's prompt carries the condensed list with the three layers and t
// 原因(2): 产品 2026-10-02「行业不猜,问用户」,删掉「先讲一生事业形态:公众型还是幕后型」(BUG-1176)
career: ["接触", "机会出现", "成形", "公开落地", "不替用户断定他做哪一行", "事业必成", "d9 段", "KP 精确宫头仍 blocked"],
marriage: [
"心动接触", "关系成对", "社会法律落地",
// 原值: "心动接触", "关系成对", "社会法律落地"
// 新值: 三件事用生活说法,并写明回答里不报层名
// 原因: BUG-1253:真机婚恋回答原样写出「心动这一层」「成对居住的那一层」
"遇到喜欢的人、谈恋爱", "定下来、长期在一起", "结婚、领证", "不报层名和编号",
"5 宫主大运 ≠ 法律婚", "金星过本命月亮 ≠ 心动月", "Punarphoo 不得写成结婚", "土星回到本命月亮是观察项,不是结婚",
"性别未知:金星与木星都要看", "夫星", "Vivah Saham",
],
@@ -140,7 +140,10 @@ test("the checklist swaps the gender-unknown line only when gender is filled", (
const filled = condensedConsultationChecklist("marriage", { gender })!;
assert.equal(filled.includes("性别未知"), false, gender);
assert.ok(filled.includes(CONDENSED_GENDER_LINES[gender]), gender);
assert.equal(filled.split("\n").length, CONSULTATION_CONDENSED_CHECKLISTS.marriage!.length, "same line count");
// 原值: 行数 = 清单条数
// 新值: 行数 = 清单条数 + 1(开头一行「分析用,不写进回答」说明)
// 原因: BUG-1253:清单内容被原样写给用户
assert.equal(filled.split("\n").length, CONSULTATION_CONDENSED_CHECKLISTS.marriage!.length + 1, "same line count plus the analysis-only note");
// router §4: gender and day/night first; the core stack stays gender-neutral.
assert.match(CONDENSED_GENDER_LINES[gender], /昼夜盘/);
assert.match(CONDENSED_GENDER_LINES[gender], /7 宫、7 宫主、D9、UL、DK/);
@@ -27,7 +27,10 @@ const OLD_SECTION_RULE = /这个人是什么样、你们怎么相处、事情会
// 原值(2): 上一行定稿句,止于「……全篇这类请求最多一处。」
// 新值(2): 加「不拿宫主落宫、宫里吉星描写这个人对你做什么……行动建议不写给他打电话、去问他」与「问过之后别的段落不再另问」
// 原因(2): 第四轮回测梦露 ×2、琵雅芙 ×1 受冲两个以上的母亲仍写「在管、在安排、近」,3 份全篇两处请求(BUG-1182)
const AFFLICTION_RANGE_TEXT = "同一个人或同一件事有两个及以上受冲信号:先说这条线有压力或距离的迹象(括号里写是哪几条),再说现实里可能是什么,给一个范围(例:「现实里可能是分开、离得远,也可能只是关系紧、话少」);不替用户断定他自己更清楚的事实(父母在不在身边、几段婚姻或感情、有没有病),也不反过来写「有分量」「站得住」「不是缺位」「底子不差」「关系稳」「能走下去」;不拿宫主落哪一宫、宫里有吉星去描写这个人对你做什么(「在管」「在安排」「心思在你的前途上」「照顾是实的」都预设人在身边,宫主落宫和吉星只说这条线另有支撑的迹象),这一方的行动建议也不写给他打电话、去问他;这一段末尾用一句话请用户说说实际情况,全篇这类请求最多一处,问过之后别的段落不再另问。";
// 原值(3): 「先说这条线有压力或距离的迹象(括号里写是哪几条)」
// 新值(3): 「(括号里只写最能说明问题的一两条,用白话,不列全)」
// 原因(3): BUG-1253:「写是哪几条」让模型在括号里列全四五条信号,和「每段最多一处括号、一两条依据」冲突
const AFFLICTION_RANGE_TEXT = "同一个人或同一件事有两个及以上受冲信号:先说这条线有压力或距离的迹象(括号里只写最能说明问题的一两条,用白话,不列全),再说现实里可能是什么,给一个范围(例:「现实里可能是分开、离得远,也可能只是关系紧、话少」);不替用户断定他自己更清楚的事实(父母在不在身边、几段婚姻或感情、有没有病),也不反过来写「有分量」「站得住」「不是缺位」「底子不差」「关系稳」「能走下去」;不拿宫主落哪一宫、宫里有吉星去描写这个人对你做什么(「在管」「在安排」「心思在你的前途上」「照顾是实的」都预设人在身边,宫主落宫和吉星只说这条线另有支撑的迹象),这一方的行动建议也不写给他打电话、去问他;这一段末尾用一句话请用户说说实际情况,全篇这类请求最多一处,问过之后别的段落不再另问。";
const SECTION_RULE_TEXT = `每段先说盘上最强的一两个信号说明了什么;盘上没有依据的方面不写,不用日常场景凑段落。${AFFLICTION_RANGE_TEXT}请用户说实际情况的那句例:「实际是哪一种,告诉我一句,我按真实情况再看。」不预设有没有伴侣、是否已婚、是否在上班、做哪类工作、孩子性别、父母是否在身边;性别未知时不用「他」「她」称伴侣和孩子。`;
function source(relative: string) {
@@ -0,0 +1,71 @@
// BUG-1252 / BUG-1253 (2026-10-06 real-device review). 1252: answer prose on
// iPhone left a blank strip on the right and jittered while streaming — the
// prose carried `text-wrap: pretty`, which WebKit applies to the whole
// paragraph. 1253: a marriage answer quoted the analysis checklist to the
// reader (layer names, house-counting, a strength rule) and coined 「合力星」.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
CONSULTATION_CONDENSED_CHECKLISTS,
condensedConsultationChecklist,
sharedConsultationReading,
} from "../src/lib/consultation-condensed-checklist.ts";
import {
AFFLICTION_RANGE_RULE,
CHECKLIST_ANALYSIS_ONLY_NOTE,
NATAL_ANSWER_SHAPE_SUMMARY,
PLAIN_SPEECH_RULE,
followUpAnswerShapeInstruction,
natalAnswerShapeBody,
} from "../src/lib/consultation-thinking-plan.ts";
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const voice = readFileSync(new URL("../src/mastra/product-voice.ts", import.meta.url), "utf8");
test("answer prose wraps plainly: no text-wrap: pretty on .message p / .message-markdown (BUG-1252)", () => {
const rule = /\.message p, \.message-markdown \{([^}]*)\}/.exec(css);
assert.ok(rule, "the shared answer prose rule exists");
assert.doesNotMatch(rule[1]!, /text-wrap/);
assert.doesNotMatch(rule[1]!, /auto-phrase/);
// No other rule puts it back on consultation answer prose. The first-run
// onboarding bubbles keep their own phrase-protected short copy (out of scope).
const offenders = [...css.matchAll(/([^{}\n]*\.(?:message-markdown|message-answer|consultation-report-analysis)[^{}]*)\{[^}]*text-wrap:\s*pretty/g)]
.map((match) => match[1]!.trim())
.filter((selector) => !selector.startsWith(".onboarding-message"));
assert.deepEqual(offenders, []);
});
test("the checklists say they are for analysis, and the shape says how to speak (BUG-1253)", () => {
assert.ok(sharedConsultationReading().startsWith(CHECKLIST_ANALYSIS_ONLY_NOTE));
for (const domain of Object.keys(CONSULTATION_CONDENSED_CHECKLISTS) as (keyof typeof CONSULTATION_CONDENSED_CHECKLISTS)[]) {
assert.ok(condensedConsultationChecklist(domain)!.startsWith(CHECKLIST_ANALYSIS_ONLY_NOTE), domain);
}
for (const text of [natalAnswerShapeBody(), followUpAnswerShapeInstruction()]) {
assert.ok(text.includes(PLAIN_SPEECH_RULE));
}
assert.match(voice, /\$\{PLAIN_SPEECH_RULE\}/);
assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /checklists are for reasoning only/);
});
test("layer names are everyday words and never announced (BUG-1253)", () => {
const all = Object.values(CONSULTATION_CONDENSED_CHECKLISTS).flat().join("\n");
for (const label of ["心动接触", "关系成对", "社会法律落地", "分三层说", "压力窗", "恢复窗"]) {
assert.equal(all.includes(label), false, label);
}
const marriage = CONSULTATION_CONDENSED_CHECKLISTS.marriage!.join("\n");
for (const plain of ["遇到喜欢的人、谈恋爱", "定下来、长期在一起", "结婚、领证", "不报层名和编号"]) {
assert.ok(marriage.includes(plain), plain);
}
});
test("card fields without an everyday Chinese name have one, and the brackets stay short (BUG-1253)", () => {
for (const pair of ["功能凶星 → 对你这个上升来说偏添麻烦的星", "yogakaraka → 不提", "不自造译名", "DK → 代表伴侣的星", "落陷 → 力气弱"]) {
assert.ok(PLAIN_SPEECH_RULE.includes(pair), pair);
}
assert.match(PLAIN_SPEECH_RULE, /括号里的依据最多两条/);
assert.match(PLAIN_SPEECH_RULE, /「从某宫数起第几宫」/);
assert.equal(AFFLICTION_RANGE_RULE.includes("括号里写是哪几条"), false);
assert.match(AFFLICTION_RANGE_RULE, /括号里只写最能说明问题的一两条/);
});