fix(consult): blank-line paragraphs, drop the H2 contract line, remove the assistant avatar
Independent Staging Quality Gate / validate (push) Failing after 18m5s
Independent Staging Quality Gate / publish (push) Skipped

Acceptance fix for 28113fa0 (TASK-consult-conversational-answer-fix-20261006):
- F1: chat-answer-detail no longer pins 'Every H2 must start its own line'
  (first turns carry no H2), assertion change noted in three columns.
- F2: Good examples separate paragraphs with a blank line and the shape says
  so; .message p is pre-wrap, so a single newline only wraps.
- Product request: remove the assistant avatar on every surface (consult,
  rectification, onboarding); --assistant-content-inset back to 0px.

Full suite 4941 / fail 24, identical to baseline 86cafe68 minus a timing flake;
build keeps / Static; first-load gzip -0.1%.

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 15:58:40 +08:00
co-authored by Claude Opus 5.5
parent 5a589a9058
commit da226db1d4
14 changed files with 82 additions and 49 deletions
+2
View File
@@ -5,6 +5,8 @@
- 问事业、财运、父母这类首轮,以前会按固定小标题往下写,最后再交一份「这周可以做的一件事」。现在首轮是几段人话:先给结论,再写最关键的依据,相关时带上时间。问到几个人就分段说,段首点名,不写小标题(BUG-1244)。
- 行动不再每轮都要。盘上确实指向一件相关的具体事情时,顺口说一句;用户问该怎么办时,那一轮可以多说几条。提示词里的示范删掉了会被逐字照抄的生活建议(BUG-1245)。
- 同一件事仍只说一遍。追问仍是直接回答,讲清楚为准。思考栏不再出现「这周可以做什么」;没有小标题的回答,写作行是「组织回答」。
- 示范和指令写明段落之间空一行;以前单个换行在界面上只是折行,几段话会挤成一整块。
- 助手回答旁边的圆形头像去掉了(对话、生时校正、首次引导三处都去),回答从栏边开始,追问按钮和校正时间轴跟着对齐正文。
- Skill 版本不变;不改数据库;不改模型、答题时钟和计费。
## 2026-10-05 — 普通对话不再白等校正闸的外部快照,用量页能看分段时间(未上线)
+2 -2
View File
@@ -16683,8 +16683,8 @@
- 现象:首轮按「开场 → 按对象的小标题 → 依据节 → 时间节 → 行动节」往下写。行动清单放在固定的最后一节。问到几个人时,段首是二级标题。申报时段还会要求先写「先回答你的问题」这个标题。
- 根因:2026-10-01 的形状(BUG-1132)把「按对象分段」做成了二级标题,并保留三个固定节。2026-09-27 的「行动只在最后一节出现一次」(BUG-1073 的标题部分)仍要求交那一节。模型按标题填空,读起来是汇报。
- 修复:形状只在 `consultation-thinking-plan.ts` 定义一处。首轮不写二级标题;问到几个人就分段落,段首点名。行动不强制,盘上有具体指向才顺口一句,用户问怎么办时那一轮可以给几条。首轮通常三到六段,不设硬截断。追问仍讲清楚为准;换领域或要求完整看时回到同样的段落形状。思考计划去掉行动节。正文没有二级标题时,思考节在流式中不保持进行中,结算后一起完成。写作行走「组织回答」。历史消息里已经写出的标题仍能拆开。`REPORT_HEADING` 只留在拆旧消息和思考节的 heading 字段里,不再拼进提示词。
- 验证:合同测试见进度记录。本机没有模型凭据,四道离线题没有真跑。真机八步见 `docs/testing/consult-conversational-answer-20261006.md`。staging 尚未部署这次改动。
- 防复发:`consultation-thinking-plan.test.ts`、`consultation-voice-contract.test.ts`、`consultation-run-timeline.test.ts` 锁住「不写 ## 节名」「段首点名」「行动不强制」「三到六段」,以及无标题首轮的思考节与写作行。改断言写了原值 / 新值 / 原因。
- 验证:合同测试见进度记录。本机没有模型凭据,四道离线题没有真跑。真机八步见 `docs/testing/consult-conversational-answer-20261006.md`。staging 尚未部署这次改动。 验收复核(Claude,Linux + Node 22.14,2026-10-06):首版 28113fa0 全量失败名单比基线 86cafe68 多一条 `chat-answer-detail` 合同测试(锁「Every H2 must start its own line」),执行方在 Windows 上没跑到;修复后全量 4941 项 fail 24,名单与基线逐条一致(基线另一条 70 s 计时测试是并发抖动,单独跑 3/3 通过);`next build` 后 `/` 仍 Static,首屏 gzip 567,642 → 567,007 B(-0.1%)。
- 防复发:`consultation-thinking-plan.test.ts`、`consultation-voice-contract.test.ts`、`consultation-run-timeline.test.ts` 锁住「不写 ## 节名」「段首点名」「行动不强制」「三到六段」,以及无标题首轮的思考节与写作行。改断言写了原值 / 新值 / 原因。 验收补修:`chat-answer-detail.test.ts` 改为不再要求 H2 那句;`consultation-voice-contract.test.ts` 新增「示范段落空一行、指令写明段落之间空一行」——聊天正文 `.message p` 是 pre-wrap,单个换行只折行、没有段间距,去掉标题后几段话会挤成一块。
- 相关记录:BUG-1073(同一件事只说一遍,保留)、BUG-1132(10-01 的按对象标题与三个固定节,本记录推翻其标题部分)。
- 复发自:无。这是产品改口径,不是 1073 / 1132 的同一缺陷复发。
- 修复版本:待发布
@@ -72,3 +72,28 @@
## 构建
`npx next build --webpack`(Next.js 16.3.1):webpack 编译成功,接着的 TypeScript 检查也通过。收集页面数据时失败,原因与测试里的技能链接相同:`SkillPackageRegistryError`,无法为 `skills/jyotish-vedic-astrology/SKILL.md` 创建符号链接(EPERM)。路由表没有打出来。`/` 是否仍为 Static、首屏 gzip 相对基线的变化,本机没有数。没有改用 Turbopack:工作树的 `node_modules` 是联接,Turbopack 会直接拒绝。
## 验收修复(Claude 直接执行,2026-10-06)
产品要求直接修复,并顺带去掉助手回答旁的头像。修复单 `TASK-consult-conversational-answer-fix-20261006.md`。
漏掉的原因:执行方在 Windows 上全量测试有 145 条因技能符号链接 EPERM 整文件加载失败,`chat-answer-detail.test.ts` 混在里面,进度记录也没有和基线逐条比对。
| 项 | 做了什么 |
| --- | --- |
| F1 | `chat-answer-detail.test.ts`:原值要求 `Every H2 must start its own line` / 新值不再要求 / 原因 首轮不写 ## |
| F2 | 父母题、事业题 Good 示范段落之间空一行;`NATAL_ANSWER_SHAPE_SUMMARY`、`natalAnswerShapeBody()`、`ANSWER SHAPE` 写明段落之间空一行(单换行在界面上不分段)。英文摘要里「One person or one matter stays one stretch of prose」改成「is not split by person」,避免单对象题被读成只写一段。新增合同测试一条 |
| 头像 | 删 `AgentAvatar`(对话、生时校正、首次引导三处)与 `.agent-avatar` 样式;`--assistant-content-inset` 两处回到 `0px`,`.message-assistant` 的 gap 删掉。推翻 09-16 `071317f7`「加回头像」。两条既有断言改三栏:`rectification-timeline-20260909.test.ts`、`rectification-agentic-entry.test.ts`(原值 `calc(32px + var(--space-3))` / 新值 `0px` / 原因 产品 10-06 要求去头像)。新增「助手消息没有头像」测试一条 |
验证(Linux + Node 22.14,干净 `npm ci`):
| 项 | 基线 86cafe68 | 修复后 |
| --- | --- | --- |
| `tsc --noEmit` | — | 0 错 |
| `npm run lint` | — | 0 error / 126 warning |
| `npm test` | 4936 项 / fail 25 | 4941 项 / fail 24,名单与基线逐条一致(基线多出的 70 s 计时测试是并发抖动,单跑 3/3 通过) |
| `next build` | `/` ○ Static | `/` ○ Static |
| 首屏 gzip | 567,642 B | 567,007 B(-0.1%) |
环境缺口:无模型凭据,没有真跑新提示词;无浏览器,去头像后的版面没有截图核对,列入真机清单第 9、10 步。
@@ -12,3 +12,6 @@
| 6 | 新开对话,打「你好」 | 仍是一句寒暄 |
| 7 | 用没填出生分钟的人物档案问事业 | 不点个人大运,不写月份,也没有标题 |
| 8 | 盯第 1 步回答上方的进度栏,直到写完 | 首轮不出现「这周可以做什么」。结束后没有卡住的转圈项 |
| 9 | 看第 1、2 步的回答 | 段落之间有空隙,不是一整块挤在一起的文字 |
| 10 | 看普通对话、生时校正、首次引导三处的助手回答 | 回答左边没有圆形头像;回答文字、下面的追问按钮、校正页顶部时间轴左边对齐 |
+2 -2
View File
@@ -370,7 +370,7 @@ The birth-time rectification session is the consultation transcript plus a house
|---|---|---|
| `opening` | live row “正在读取你的出生资料,准备第一个问题…”, then tool labels | enabled (typing queues), stop visible |
| `empty` | “这段校正还没有开始。” and one primary action “开始提问” | enabled |
| `question-live` | the asking message carries the embedded card or the spoken stem. An active spoken or choice focus without `asked_turn_id` hangs on the last assistant message, same avatar column. A deterministic reply streams “ack + stem” so the stem is readable before the card arrives; once the snapshot attaches the card, the bubble drops that sentence on the live transcript as well as on refresh (BUG-1045) | enabled, placeholder points at the card |
| `question-live` | the asking message carries the embedded card or the spoken stem. An active spoken or choice focus without `asked_turn_id` hangs on the last assistant message, same text column. A deterministic reply streams “ack + stem” so the stem is readable before the card arrives; once the snapshot attaches the card, the bubble drops that sentence on the live transcript as well as on refresh (BUG-1045) | enabled, placeholder points at the card |
| `question-gap`, persisted question | the snapshot's current question hangs on the latest settled assistant message (the client does this too, not only GET), and any other copy of the same `focus_id` comes off: one focus, one card (BUG-1046). The host question row (`data-testid="persisted-question"`) appears only when no assistant message can carry the prompt: there is none, or the latest one already carries a different question | enabled, placeholder “请回答上面的问题…” |
| `question-live`, after a failed tap | the tapped option is taken back (question active again, nothing selected), the card remounts tappable, the Case is re-read, and the error line is “这次没提交上,请再点一次。” — never a second copy of the card below (BUG-1046) | enabled |
| `question-gap`, retries left | one timeline live row “正在准备下一个问题…”, refetching on a 2s timer up to two retries | enabled |
@@ -479,7 +479,7 @@ The birth-time rectification session is the consultation transcript plus a house
### Message
- **Variants:** assistant editorial text on canvas; user text on warm card surface; streaming; error. Streaming uses a timeline of completed steps plus the current step; the thinking body expands while streaming, collapses when answer text appears, and is stored with the assistant message.
- **Identity:** neither role carries an avatar. The assistant reply is plain editorial text starting at the column edge; the user's turn is a right-aligned tinted bubble. The 32px Jyotisha logo used to sit beside every assistant message — at one avatar per turn it became the most repeated element in a long transcript, and it pushed the reply 44px in from the column the user reads down. `--assistant-content-inset` is `0px` now, so the follow-up chips and the run timeline line up with the reply text itself rather than with a mark that is gone.
- **Identity:** neither role carries an avatar. The assistant reply is plain editorial text starting at the column edge; the user's turn is a right-aligned tinted bubble. The 32px Jyotisha logo used to sit beside every assistant message — at one avatar per turn it became the most repeated element in a long transcript, and it pushed the reply 44px in from the column the user reads down. `--assistant-content-inset` is `0px` now, so the follow-up chips and the run timeline line up with the reply text itself rather than with a mark that is gone. History: removed 2026-09-16 (R3), restored the same day after a real-device review because rectification narration had no visual owner (071317f7), removed again on 2026-10-06 at the product owner's request — on every surface, rectification and onboarding included. The rectification timeline bar and candidate cards use the same `0px` inset.
- **Typography:** assistant body `--type-body-md` (16px) with serif subheadings; user body 14px.
- **Tables:** three-column technique audit tables keep 状态 on one line. Below 768px they stack each row as title + status, then the note, instead of squeezing 已执行 into a vertical glyph column.
- **Follow-up:** the latest settled consultation answer may offer two or three grounded next questions under that answer. Clicking one sends it in the current session. The composer never hosts suggestion chips. If the answer does not support a grounded continuation, nothing is shown.
+8 -25
View File
@@ -483,7 +483,7 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.birth-time-assessment-progress strong { margin-top: var(--space-2); color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 500; }
.birth-time-assessment-progress > span { font-size: var(--type-body-sm); line-height: 1.55; text-wrap: pretty; word-break: auto-phrase; }
.message-assistant { align-items: flex-start; justify-content: flex-start; gap: var(--space-3); }
.message-assistant { align-items: flex-start; justify-content: flex-start; }
.message-user { justify-content: flex-end; }
.message-user .message-content { display: flex; flex-direction: column; align-items: flex-end; }
.message-assistant .message-content { width: auto; max-width: none; flex: 1; }
@@ -1243,9 +1243,10 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
.message-list {
/* Follow-up chips and the run timeline line up with the assistant's text,
which starts past the avatar and its gap. */
--assistant-content-inset: calc(32px + var(--space-3));
/* The assistant reply starts at the column edge (no avatar since
2026-10-06), so follow-up chips and the run timeline line up with the
text itself. */
--assistant-content-inset: 0px;
margin: 0 auto;
width: min(900px, 100%);
padding: var(--space-8) var(--space-8) var(--space-16);
@@ -1258,8 +1259,6 @@ section[popover]:has(> [data-sonner-toaster]), section[popover][aria-label^="页
color: var(--color-ink-secondary);
}
.message { display: flex; padding: var(--space-2) 0; }
/* Restored after real-device review; see chat-message-row.tsx for why. */
.agent-avatar { width: 32px; height: 32px; display: block; flex: 0 0 32px; margin-top: var(--space-2); border-radius: 50%; background: var(--color-canvas) url("/jyotish-logo.png") center / contain no-repeat; box-shadow: 0 0 0 1px var(--ring-hairline); }
.message-content { min-width: 0; max-width: min(80%, 680px); }
.message-bubble { overflow: hidden; border: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-lg); background: var(--color-canvas-muted); }
.message-assistant .message-bubble { border-radius: 0; background: transparent; padding: var(--space-3) 0; }
@@ -2581,18 +2580,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-top: var(--space-8);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant {
gap: var(--space-4);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant .agent-avatar {
width: 30px;
height: 30px;
flex-basis: 30px;
margin-top: 2px;
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-border) 82%, transparent);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant .message-bubble {
padding: 0 0 var(--space-2);
@@ -2676,10 +2663,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-top: var(--space-6);
}
.conversation:not(.is-empty):not(.is-rectification) .message-assistant {
gap: var(--space-3);
}
.conversation:not(.is-empty):not(.is-rectification) .message-user .message-content {
max-width: 88%;
@@ -2888,9 +2871,9 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
The pending state keeps the same box for the same reason. */
.rectification-workspace__chat {
--rectification-timeline-height: 64px;
/* Same token as `.message-list`: avatar 32px + gap. The bar sits outside the
transcript, so it cannot inherit that custom property from there. */
--assistant-content-inset: calc(32px + var(--space-3));
/* Same token as `.message-list`. The bar sits outside the transcript, so it
cannot inherit that custom property from there. */
--assistant-content-inset: 0px;
}
.rectification-timeline {
height: var(--rectification-timeline-height);
@@ -31,14 +31,6 @@ function motionPreferred() {
if (motionPreferred()) prefetchOnIdle(loadGsap);
/* Restored after real-device review: R3 removed it on the grounds that it was
the most repeated element in a long transcript, but without it the assistant
turn has no visual owner — in the rectification surface especially, where
narration, a question and its options all stack as plain text. */
export function AgentAvatar() {
return <span className="agent-avatar" aria-hidden="true" />;
}
export const ChatMessageRow = memo(function ChatMessageRow({
message,
showActivity = message.state !== "settled",
@@ -138,7 +130,6 @@ export const ChatMessageRow = memo(function ChatMessageRow({
className={`message message-${message.role}`}
aria-label={message.role === "assistant" ? assistantLabel : "你"}
>
{message.role === "assistant" && <AgentAvatar />}
<div className="message-content">
<div className="message-bubble">
{message.role === "assistant" ? (
@@ -1,7 +1,6 @@
"use client";
import { ChatMessageContent } from "@/components/chat-message-content";
import { AgentAvatar } from "@/components/chat-message-row";
import { protectOnboardingPhrases } from "@/lib/onboarding-copy";
import type { Message } from "@/lib/home-types";
@@ -10,7 +9,6 @@ export function OnboardingChatMessage({ role, text, streaming = false, length =
const protectedVisibleText = protectOnboardingPhrases(visibleText);
return (
<article className={`message message-${role} onboarding-message${phraseSafe ? " is-phrase-safe" : ""}`} aria-label={role === "assistant" ? "Jyotisha" : "你"}>
{role === "assistant" && <AgentAvatar />}
<div className="message-content">
<div className="message-bubble">
{role === "assistant" ? (
@@ -26,7 +26,7 @@ export const REPORT_HEADING = {
* 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 stays one stretch of prose. 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. 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 = "先回答你的问题";
@@ -429,7 +429,7 @@ export function natalAnswerShapeBody(): string {
`${NATAL_SECTION_RULE}每段的判断后在括号里给撑住它的盘上依据,每段最多一处括号。`,
"行动不强制:只有盘上确实指向一件和问题相关的具体事情时,才在话里顺口说一句,不列清单、不放固定位置、全篇最多一处;用户问「我该怎么办」或「怎么做」时,那一轮可以给几条。",
"首轮通常三到六段:讲清结论、最关键的一两条依据,相关时带上时间;不为显得周全把各方面写满。不设硬截断。同一件事只说一遍。",
"正文写成段落。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用加粗标签加一句的格式。",
"正文写成段落,段落之间空一行(单个换行在界面上不分段)。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用加粗标签加一句的格式。",
"首轮正文不写 ##,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。",
"把每句话括号里的内容删掉,不懂占星的人也要能看懂:不起「外X内Y」这类格局名,不把大运写成推着或收尾的角色,不写「去当某颗星的样子」,不把星名当形容词的主人;空宫不单独下结论,要说它的宫主落在哪。名词只用中文、全篇一个叫法(罗睺、计都;大运、子运、小运),不写英文或梵文音译(sade sati、vargottama、rupas 之类),力量只说强、中、弱。",
"不要写「先回答你的问题」这个标题,不要写「统一参数与原始结构」,不要写技法审计表,不要写思考过程清单,不要复述内部 JSON 字段。",
+7 -1
View File
@@ -27,7 +27,7 @@ ANSWER SHAPE (natal / general / declared-window 三种模式共用)
2. 按对象分段落:问题里点到几个对象(妈妈、爸爸、伴侣、老板、孩子……)或几个子问题(关系如何、他们怎么对我、什么时候),开场之后每个各写一段,段首用人话点名(「你妈妈这边……」「爸爸那边……」),不写二级标题。问「父母」时妈妈和爸爸分开写,不揉成一段。只有一个对象、一个子问题时不拆段。
3. ${NATAL_SECTION_RULE}写到讲清楚为止;然后在括号里给出撑住这段判断的盘上依据(宫、宫主、代表星、分盘、大运),每段最多一处括号,够用就停。问的是别人时,写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思。
4. 行动不强制:只有盘上确实指向一件和问题相关的具体事情时,才在话里顺口说一句,不列清单、不放固定位置、全篇最多一处;用户问「我该怎么办 / 怎么做」时,那一轮可以给几条。无出生分钟与申报时段同样不交固定行动清单。
并列的点默认写成段落。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用「加粗标签 + 一句」的格式。首轮正文不写 ##,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。
段落之间空一行(单个换行在界面上不分段)。并列的点默认写成段落。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用「加粗标签 + 一句」的格式。首轮正文不写 ##,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。
人话自检:把每句话括号里的内容删掉,一个完全不懂占星的人仍然能看懂它在说什么;做不到就重写这句。禁止:
- 自造格局名、四字概括(「外X内Y」这一类);
- 把大运、行运写成拟人的角色(「推这一段的是某某大运」「修形式的是某某子运」「交给某某去收尾」),以及「去当某颗星的某种样子」「别去应某颗星的某个象」这类角色修辞;
@@ -41,14 +41,20 @@ ANSWER SHAPE (natal / general / declared-window 三种模式共用)
Good (父母题首轮,分段落、段首点名;括号里是依据的占位,只演示口气与结构,不是读法):
你和妈妈这条线近、来往多;爸爸这条线盘上有距离的迹象。
你妈妈这边,她离你近,对你的事上心,你们之间来往多。(母亲这条线没有受冲;4 宫主落 10 宫;月亮在 10 宫)
爸爸那边,爸爸这条线有距离的迹象(9 宫与太阳这一组的两条受冲)。现实里可能是他不常在你身边,也可能人在、只是话少、关系紧。实际是哪一种,告诉我一句,我按真实情况再看。
两边给你的不一样:妈妈这一头来往多,爸爸这一头要看实际情况再说。
现在这十年(月亮大运,到 2034 年 2 月)和妈妈的来往会比以前多。
Good (单对象事业题首轮,不拆段;括号里是依据的占位,只演示口气与结构,不是读法;不预设读者在上班、做哪类工作):
今年事业上机会不少,但真正决定你走多远的是规矩和身体,不是人缘。
机会和帮你的人今年都在;同时事情多、要求严,身体也容易被拖累。(木星行运 10 宫;土星行运 6 宫)
9 月木星换宫之前,机会多、事情也不稳。
Bad (多对象题,改写示意): 你和父母是「外热内冷」:推着这段关系的是金星大运,土星子运负责收尾;别去应火星的「硬」,去当金星的「和」。(自造格局名、大运写成角色、星名当形容词,爸妈揉成一段;括号外没有一句不懂占星的人能看懂。)
+13 -2
View File
@@ -33,8 +33,10 @@ test("the model is told the answer renders inline, so it stops writing for a fol
const voice = readFileSync(new URL("../src/mastra/product-voice.ts", import.meta.url), "utf8");
assert.match(voice, /renders the whole answer inline/);
assert.doesNotMatch(voice, /folds everything from the first H2/);
// 截图里 `## 适合推进 / 需要避开` 接在上一句后面,被当成字面量渲染。
assert.match(voice, /Every H2 must start its own line/);
// 原值: assert.match(voice, /Every H2 must start its own line/)
// 新值: 不再要求(首轮正文不写 ##,这句没有对象了)
// 原因: TASK-consult-conversational-answer-20261006 D1(BUG-1244);修复单 F1
assert.doesNotMatch(voice, /Every H2 must start its own line/);
});
test("ChatMessageContent still splits spoken from report and keeps the audit fold", () => {
@@ -60,3 +62,12 @@ test("copy still sends the original message text, not the spoken layer", () => {
test("onboarding typewriter messages still go through ChatMessageContent", () => {
assert.match(onboardingSource, /<ChatMessageContent text=\{protectedVisibleText\} \/>/);
});
test("assistant turns carry no avatar; the reply starts at the column edge (2026-10-06)", () => {
const row = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8");
const onboarding = readFileSync(new URL("../src/components/onboarding-chat-message.tsx", import.meta.url), "utf8");
for (const source of [row, onboarding, globalStyles]) {
assert.doesNotMatch(source, /AgentAvatar|agent-avatar/);
}
assert.doesNotMatch(globalStyles, /--assistant-content-inset: calc\(32px/);
});
@@ -363,6 +363,19 @@ test("pinned examples carry no copyable action and no weekly heading (BUG-1245)"
assert.doesNotMatch(`${goodParents}\n${goodCareer}`, /^- /m);
});
test("example paragraphs are blank-line separated and the shape says so (BUG-1244 fix F2)", async () => {
const { NATAL_ANSWER_SHAPE_SUMMARY, natalAnswerShapeBody } = await import("../src/lib/consultation-thinking-plan.ts");
// 聊天正文 `.message p` 是 pre-wrap:单个换行只折行,段间距只给空一行分开的段落。
const goodParents = voice.slice(voice.indexOf("Good (父母题首轮"), voice.indexOf("Good (单对象事业题首轮"));
const goodCareer = voice.slice(voice.indexOf("Good (单对象事业题首轮"), voice.indexOf("Bad (多对象题"));
assert.match(goodParents, /\n\n你妈妈这边/);
assert.match(goodParents, /\n\n爸爸那边/);
assert.match(goodCareer, /\n\n机会和帮你的人/);
assert.match(voice, /段落之间空一行/);
assert.match(natalAnswerShapeBody(), /段落之间空一行/);
assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /Separate paragraphs with a blank line/);
});
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"]) {
@@ -522,7 +522,8 @@ test("candidate state renders from the snapshot API and never from sentinels", (
assert.match(styles, /\.rectification-snapshot \{/);
assert.match(styles, /\.rectification-house-table \{[^}]*border:\s*1px solid var\(--color-border\)/);
assert.match(styles, /\.rectification-house-table \{[^}]*background:\s*var\(--color-canvas-soft\)/);
assert.match(styles, /\.message-list \{[\s\S]*?--assistant-content-inset: calc\(32px \+ var\(--space-3\)\)/);
// 原值: calc(32px + var(--space-3)) / 新值: 0px / 原因: 2026-10-06 产品要求去掉 agent 头像,正文从栏边起
assert.match(styles, /\.message-list \{[\s\S]*?--assistant-content-inset: 0px/);
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*width: calc\(100% - var\(--assistant-content-inset\)\)/);
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/);
assert.match(styles, /\.rectification-workspace \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) minmax\(18rem, 22\.5rem\)/);
@@ -452,10 +452,10 @@ test("the chat grid has three rows and the timeline row is a fixed height", () =
// stick-to-bottom, since it sits outside the observed scroll container.
assert.doesNotMatch(barRule, /min-height|height:\s*auto/);
// Inner padding matches the assistant column, not the chat-panel edge.
// 原值 `0px` / 新值 `calc(32px + var(--space-3))`
// / 原因:产品在真机走查后要求把 agent 头像加回来(推翻 R3 的 D7);
// 原值 `calc(32px + var(--space-3))` / 新值 `0px`
// / 原因:2026-10-06 产品要求去掉 agent 头像(推翻 071317f7 加回头像);
// 这条缩进跟着头像走,时间轴仍与助手正文左边界对齐,规则本身没变。
assert.match(chatRule, /--assistant-content-inset: calc\(32px \+ var\(--space-3\)\);/);
assert.match(chatRule, /--assistant-content-inset: 0px;/);
assert.match(barRule, /padding: 0 calc\(var\(--space-8\) \+ var\(--assistant-content-inset\)\);/);
});