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 -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.`;