From e635c40224e2cb1d1240ce10942f80fe56b12ac8 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 19 Aug 2026 15:29:04 +0800 Subject: [PATCH] fix(consult): fold the technique audit out of the spoken answer Keep comparative tables in chat, but hide the long audit behind a collapsed control so the reply stays readable. Co-authored-by: Cursor --- SKILL.md | 8 +- docs/BUG_HISTORY.md | 16 ++ frontend/src/app/api/consult/route.ts | 8 +- frontend/src/app/globals.css | 134 +++++++++++- .../src/components/chat-markdown-view.tsx | 2 + .../src/components/chat-message-content.tsx | 34 ++- frontend/src/components/chat-message-row.tsx | 7 +- .../components/technique-audit-disclosure.tsx | 45 ++++ frontend/src/lib/consultation-agent-events.ts | 12 ++ .../src/lib/consultation-technique-audit.ts | 203 ++++++++++++++++++ frontend/src/mastra/consultation-tools.ts | 7 +- frontend/src/mastra/index.ts | 4 +- frontend/src/mastra/product-voice.ts | 8 +- .../chat-bundle-splitting-contract.test.ts | 4 +- .../consultation-technique-audit.test.ts | 112 ++++++++++ .../tests/consultation-voice-contract.test.ts | 5 +- frontend/tests/evidence-audit-panel.test.ts | 7 +- frontend/tests/skill-binding.test.ts | 2 +- 18 files changed, 590 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/technique-audit-disclosure.tsx create mode 100644 frontend/src/lib/consultation-technique-audit.ts create mode 100644 frontend/tests/consultation-technique-audit.test.ts diff --git a/SKILL.md b/SKILL.md index 5b424af5..ca53a703 100644 --- a/SKILL.md +++ b/SKILL.md @@ -15,7 +15,7 @@ description: "印度占星(Jyotish)商业解盘与推运系统。核心能 3. `references/oracle/commercial_skill_truth_overlay.v1.json`:商业声明和受限技法的最终覆盖层。 4. 服务端 `consumer_context.answer_policy`:当前请求可回答范围的最终合同。 -若研究快照、商业覆盖层和服务端回执冲突,以商业覆盖层和服务端回执为准。真实计算只能来自服务端工具,模型不得重算或发明行星位置。候选出生时间不得写成 confirmed;`blocked`、参数敏感、外部验证未闭环和多体系冲突必须原样保留。网页对话在口语回答末尾给出 Technique Audit Table(已执行 / 阻塞 / 不适用);正式个人报告仍按 `executive_summary -> thematic_narrative -> evidence_appendix` 排列,审计表位于附录。医疗、法律、投资、安全关键结论及确定性死亡/诊断/妊娠预测均禁止。 +若研究快照、商业覆盖层和服务端回执冲突,以商业覆盖层和服务端回执为准。真实计算只能来自服务端工具,模型不得重算或发明行星位置。候选出生时间不得写成 confirmed;`blocked`、参数敏感、外部验证未闭环和多体系冲突必须原样保留。网页对话的技法审计表由界面折叠展示(已执行 / 阻塞 / 不适用),口语回答不要再把该表写进正文;正式个人报告仍按 `executive_summary -> thematic_narrative -> evidence_appendix` 排列,审计表位于附录。医疗、法律、投资、安全关键结论及确定性死亡/诊断/妊娠预测均禁止。 > **版本**:v6.9.14 | **详细变更**:`CHANGELOG.md` > **对标状态**:中文用户端与技法覆盖领先;D1/D9/AV/Chara 等有守门,Dasha/Shadbala 外部 oracle 扩充仍在进行。 @@ -46,7 +46,7 @@ KP/Muhurta/Gochara/Sahams/Sphuta/Tajika等高阶分支必须按 skill truth over - 领域关联:事业至少 `D10 + A10`,财富至少 `D2 / D11`,婚恋至少 `D9 + UL`。 - 西洋层:有本命出生资料时,自动运行已实现的热带本命及 timing(行运、回归、次限、日弧、converse、中点、月回归、duration scan、parans)。未实现或失败标 `blocked`,不适用标 `not_applicable`。 - 特殊体系:KP、Tajika、Saham、Gulika/Mandi、Panchanga、Muhurta、Prashna、Sphuta、合婚等,按问题关联调用或解释 `blocked / not_applicable`。商业上 KP/Muhurta/Tajika 不得作为确定性结论。 -- Technique Audit Table 对每项使用 `executed / blocked / not_applicable`(可见回答译为 已执行 / 阻塞 / 不适用)。未闭环技法仍展示状态与边界,不得写成确定事件。 +- Technique Audit Table 对每项使用 `executed / blocked / not_applicable`(界面折叠展示,译为 已执行 / 阻塞 / 不适用)。口语正文不要再贴该表。未闭环技法仍展示状态与边界,不得写成确定事件。 ### P0/P1 观察层(不得升格为已验证真值) @@ -163,7 +163,7 @@ adapter available 解释为已完成 VedAstro、PyJHora/JHora 或 jyotishganit r 6. **阶段五**:应期输出(五层验证→时间窗口→Actionable Output+案例检索) 7. **阶段六**:补救措施(可选) 8. **阶段七**:现代措辞包装 -9. **阶段八**:输出 Technique Audit Table,逐项声明已调用/未调用/部分可用/缺失模块及其对置信度的影响。 +9. **阶段八**:产出 Technique Audit Table,逐项声明已调用/未调用/部分可用/缺失模块及其对置信度的影响。网页由界面折叠展示,不要写入口语正文。 ### ⚙️ 事件判定骨架(总入口) @@ -249,7 +249,7 @@ adapter available 解释为已完成 VedAstro、PyJHora/JHora 或 jyotishganit r 核心要求: 1. 先判断问题类型,再自动选择 `career-timing-strict` / `relationship-timing-strict` / `wealth-timing-strict` / `event-timing-strict` / `event-verification-strict`。 2. 用户不需要知道 Chara Dasha、A10、Argala、Shadbala、Ashtakavarga 等技法名称;AI 必须按问题类型自动调用。 -3. 输出末尾必须给出 Technique Audit Table,说明每项高级技法是否调用、结果是什么、缺失会如何降低置信度。 +3. 必须产出 Technique Audit Table,说明每项高级技法是否调用、结果是什么、缺失会如何降低置信度。网页由界面折叠展示,口语正文不要再贴该表。 4. 不得把未实现或未调用的技法静默省略;A10/Karma Pada、Pushkara、Vargottama、Dasha Sandhi 已进入 full-reading 输出;Bhava Chalit 与 Sudarshana Chakra 已进入 complete,可正常纳入 Technique Audit Table。 ### MEVG 强制外部验证门控(v4.2.0+) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 8f92ba30..632f8b07 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -4458,3 +4458,19 @@ - 相关记录:BUG-120、BUG-292、BUG-294、BUG-296 - 复发自:无 - 修复版本:待提交 + +## BUG-298 | 咨询回答把技法审计表铺进正文,列表和表格也缺少对话样式 + +- 状态:resolved +- 首次发现:2026-08-19 +- 最近更新:2026-08-19 +- 影响面:普通咨询消息渲染、`VISIBLE VOICE`、live `jyotish-vedic-astrology` SKILL.md、Agent 回执 +- 用户现象:个人盘回答后,比较表和技法审计表像未排版的管道符墙;末尾约四十行技法表压过口语正文。 +- 触发条件:有出生分钟的个人盘咨询,模型按旧合同把 `| 技法 | 状态 | 说明 |` 写进 `answer.delta`。 +- 根因:可见语音要求口语末尾贴完整 Markdown 技法审计表;`.markdown-table` 没有样式;列表项里的段落仍吃正文边距。BUG-011 只禁止内部 `EvidenceAuditPanel`,没有另做用户可读的折叠表。 +- 修复:口语不再粘贴审计表。服务端把已交付行放进回执,界面用默认收起的 `本轮技法` 折叠块展示。历史回答若仍带该 Markdown 表,则从正文拆出再折叠。比较表与列表改用对话字号、边距和横向滚动。不恢复内部证据面板、英文状态码或 raw receipt。 +- 验证:`frontend/tests/consultation-technique-audit.test.ts`、`consultation-voice-contract`、`skill-binding`、`evidence-audit-panel`、`chat-bundle-splitting-contract`。 +- 防复发:聊天消息不得再挂载 `EvidenceAuditPanel`;口语合同不得要求把技法审计表写进正文;折叠块默认不得带 `open`。 +- 相关记录:BUG-011、BUG-287 +- 复发自:BUG-011 +- 修复版本:待提交 diff --git a/frontend/src/app/api/consult/route.ts b/frontend/src/app/api/consult/route.ts index 95981139..72f3d9e5 100644 --- a/frontend/src/app/api/consult/route.ts +++ b/frontend/src/app/api/consult/route.ts @@ -641,7 +641,7 @@ export async function POST(request: Request) { ? generalDailyContext ? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。" : "当前是用户明确选择的无出生分钟一般咨询。不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。" - : "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答;回答末尾附上技法审计表,不要复述内部字段。", + : "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答;不要把技法审计表写进正文,不要复述内部字段。", generalDailyContextPrompt(generalDailyContext), resolvedQuestion.modelQuestion, ].filter(Boolean).join("\n"), @@ -691,6 +691,9 @@ export async function POST(request: Request) { stepBudget: consultationStepBudgetReceipt(state), workflow: workflowReceipt, techniqueTruth: generalDailyContext ? "public-panchanga-only" : "not-applicable", + ...(state.techniqueAuditTable?.length + ? { techniqueAuditTable: state.techniqueAuditTable } + : {}), }); return streamAgentResponse({ runId: requestId, @@ -774,6 +777,9 @@ export async function POST(request: Request) { stepBudget: consultationStepBudgetReceipt(state), workflow: state.workflowReceipt ?? workflowReceipt, techniqueTruth: state.techniqueTruth ?? "unknown", + ...(state.techniqueAuditTable?.length + ? { techniqueAuditTable: state.techniqueAuditTable } + : {}), }); return streamAgentResponse({ runId: requestId, diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index a048385c..afe88b96 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -195,8 +195,46 @@ button:disabled { cursor: default; opacity: .45; } .raw-evidence-receipt summary { padding: var(--space-1) 0; color: var(--color-ink-tertiary); font-size: 12px; } .raw-evidence-receipt pre { max-height: 180px; overflow: auto; margin: var(--space-2) 0 0; padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink-secondary); font-size: 11px; line-height: 1.5; } .message-markdown p { margin: 0 0 14px; } -.message-markdown ul, .message-markdown ol { margin: 10px 0 16px; padding-left: 24px; } -.message-markdown li { margin: 6px 0; } +.message-markdown ul, .message-markdown ol { margin: 8px 0 18px; padding-inline-start: 1.15em; } +.message-markdown li { margin: 0; padding-block: 4px; padding-inline-start: 4px; } +.message-markdown li::marker { color: var(--color-ink-tertiary); } +.message-markdown li > p, +.message-markdown th > p, +.message-markdown td > p { margin: 0; } +.message-markdown li > p + p { margin-top: 6px; } +.message-answer { min-width: 0; } +.markdown-table { + max-width: 100%; + margin: 14px 0 18px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} +.markdown-table table { + width: 100%; + min-width: 0; + border-collapse: collapse; + font-size: var(--type-body-sm); + line-height: 1.55; +} +.markdown-table th, +.markdown-table td { + padding: 8px 10px 8px 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent); + vertical-align: top; + text-align: start; + overflow-wrap: anywhere; +} +.markdown-table thead th { + color: var(--color-ink-secondary); + font-size: var(--type-overline); + font-weight: 650; + letter-spacing: .02em; +} +.markdown-table tbody th { + font-weight: 550; +} +.markdown-table tr:last-child th, +.markdown-table tr:last-child td { border-bottom: 0; } .agent-activity-status { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-body-sm); line-height: 1.5; } .agent-activity-status canvas { flex: 0 0 auto; } .agent-activity-status__text { @@ -223,7 +261,7 @@ button:disabled { cursor: default; opacity: .45; } from { background-position: 120% 0; } to { background-position: -120% 0; } } -.agent-activity-status + .message-markdown { margin-top: var(--space-2); } +.agent-activity-status + .message-answer { margin-top: var(--space-2); } .composer textarea::placeholder { color: var(--color-ink-tertiary); } .composer button svg { width: 19px; height: 19px; } .dialog-close svg { width: 19px; height: 19px; } @@ -720,6 +758,86 @@ button:disabled { cursor: default; opacity: .45; } .message-markdown strong { color: var(--color-ink); font-weight: 500; } .message-markdown a { text-underline-offset: 3px; color: var(--color-action); } .message-markdown code { padding: 2px 5px; font-size: .9em; border-radius: var(--radius-xs); background: var(--color-canvas-muted); font-family: var(--font-mono); } +.technique-audit { + margin-top: var(--space-5); + padding-top: var(--space-3); + border-top: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent); +} +.technique-audit > summary { + width: fit-content; + display: flex; + align-items: center; + gap: var(--space-2); + min-height: 32px; + margin-inline: calc(var(--space-1) * -1); + padding: 0 var(--space-2); + border-radius: var(--radius-sm); + color: var(--color-ink-tertiary); + cursor: pointer; + list-style: none; + font-size: var(--type-caption); + font-weight: 500; +} +.technique-audit > summary::-webkit-details-marker { display: none; } +.technique-audit > summary::before { + width: 6px; + height: 6px; + border-inline-end: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + content: ""; + transform: rotate(-45deg); + transition: transform 120ms ease-out; +} +.technique-audit[open] > summary::before { transform: rotate(45deg) translate(-1px, -1px); } +.technique-audit > summary:hover { background: var(--color-canvas-muted); color: var(--color-ink-secondary); } +.technique-audit > summary:focus-visible { + outline: 2px solid color-mix(in srgb, var(--color-focus) 52%, transparent); + outline-offset: 1px; +} +.technique-audit > summary small { + color: var(--color-ink-tertiary); + font-size: inherit; + font-variant-numeric: tabular-nums; +} +.technique-audit-panel { + max-width: 100%; + margin-top: var(--space-2); + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} +.technique-audit-panel table { + width: 100%; + border-collapse: collapse; + font-size: var(--type-caption); + line-height: 1.5; +} +.technique-audit-panel th, +.technique-audit-panel td { + padding: 7px 10px 7px 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent); + vertical-align: top; + text-align: start; + overflow-wrap: anywhere; +} +.technique-audit-panel thead th { + color: var(--color-ink-tertiary); + font-size: 11px; + font-weight: 650; + letter-spacing: .04em; +} +.technique-audit-panel tbody th { + width: 28%; + color: var(--color-ink); + font-weight: 550; +} +.technique-audit-panel td[data-status] { + width: 4.5em; + color: var(--color-ink-secondary); + white-space: nowrap; +} +.technique-audit-panel td:last-child { color: var(--color-ink-secondary); } +.technique-audit-panel tr:last-child th, +.technique-audit-panel tr:last-child td { border-bottom: 0; } .error-message { margin: 12px 0 0; padding: 12px 14px; border-left: 3px solid var(--color-danger); background: var(--color-danger-muted); color: var(--color-danger); font-size: 13px; line-height: 1.6; border-color: var(--color-danger); border-radius: 0 var(--radius-md) var(--radius-md) 0; } .composer-wrap { position: sticky; bottom: 0; z-index: 2; min-width: 0; border-top: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: var(--space-3) var(--space-6) var(--space-4); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); } @@ -1490,6 +1608,15 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class margin-bottom: var(--space-4); } +.conversation:not(.is-empty):not(.is-rectification) .message-markdown ul, +.conversation:not(.is-empty):not(.is-rectification) .message-markdown ol { + margin: 4px 0 var(--space-5); +} + +.conversation:not(.is-empty):not(.is-rectification) .markdown-table { + margin: var(--space-5) 0; +} + .conversation:not(.is-empty):not(.is-rectification) .message-markdown h2, .conversation:not(.is-empty):not(.is-rectification) .message-markdown h3 { margin: var(--space-8) 0 var(--space-3); @@ -2168,4 +2295,5 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class -webkit-text-fill-color: currentColor; } .rectification-activity-receipt__toggle svg { transition: none; } + .technique-audit > summary::before { transition: none; } } diff --git a/frontend/src/components/chat-markdown-view.tsx b/frontend/src/components/chat-markdown-view.tsx index cf1b3f74..de24c227 100644 --- a/frontend/src/components/chat-markdown-view.tsx +++ b/frontend/src/components/chat-markdown-view.tsx @@ -14,6 +14,8 @@ const markdownComponents: Components = { {children}
), + ul: ({ children }) => , + ol: ({ children }) =>
    {children}
, }; export function renderChatMarkdown(text: string) { diff --git a/frontend/src/components/chat-message-content.tsx b/frontend/src/components/chat-message-content.tsx index 86f82ebc..85df70f0 100644 --- a/frontend/src/components/chat-message-content.tsx +++ b/frontend/src/components/chat-message-content.tsx @@ -4,6 +4,12 @@ import { useEffect, useState, type ReactNode } from "react"; import { prefetchOnIdle } from "@/components/chat-chunk-prefetch"; import { plainParagraphs } from "@/components/chat-message-paragraphs"; +import { TechniqueAuditDisclosure } from "@/components/technique-audit-disclosure"; +import type { TechniqueAuditRow } from "@/lib/consultation-agent-events"; +import { + resolveTechniqueAuditRows, + splitSpokenAnswerAndTechniqueAudit, +} from "@/lib/consultation-technique-audit"; type MarkdownRenderer = (text: string) => ReactNode; @@ -35,16 +41,30 @@ function useMarkdownRenderer() { return renderer; } -export function ChatMessageContent({ text }: { text: string }) { +export function ChatMessageContent({ + text, + auditRows, +}: { + text: string; + auditRows?: readonly TechniqueAuditRow[]; +}) { const renderMarkdown = useMarkdownRenderer(); + const split = splitSpokenAnswerAndTechniqueAudit(text); + const rows = resolveTechniqueAuditRows(split, auditRows); + const spoken = split.spoken; return ( -
- {renderMarkdown - ? renderMarkdown(text) - : (plainParagraphs(text) ?? []).map((paragraph, index) => ( -

{paragraph}

- ))} +
+ {spoken ? ( +
+ {renderMarkdown + ? renderMarkdown(spoken) + : (plainParagraphs(spoken) ?? []).map((paragraph, index) => ( +

{paragraph}

+ ))} +
+ ) : null} + {rows.length > 0 ? : null}
); } diff --git a/frontend/src/components/chat-message-row.tsx b/frontend/src/components/chat-message-row.tsx index 79a9f6e1..e2ae90ff 100644 --- a/frontend/src/components/chat-message-row.tsx +++ b/frontend/src/components/chat-message-row.tsx @@ -97,7 +97,12 @@ export function ChatMessageRow({ {showActivity && ( )} - {message.text && } + {message.text && ( + + )} ) :

{message.text}

}
diff --git a/frontend/src/components/technique-audit-disclosure.tsx b/frontend/src/components/technique-audit-disclosure.tsx new file mode 100644 index 00000000..6e0ee531 --- /dev/null +++ b/frontend/src/components/technique-audit-disclosure.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { + displayTechniqueAuditLabel, + displayTechniqueAuditNote, + techniqueAuditStatusLabel, +} from "@/lib/consultation-technique-audit"; +import type { TechniqueAuditRow } from "@/lib/consultation-agent-events"; + +export function TechniqueAuditDisclosure({ + rows, +}: { + readonly rows: readonly TechniqueAuditRow[]; +}) { + if (rows.length === 0) return null; + return ( +
+ + 本轮技法 + {rows.length} 项 + +
+ + + + + + + + + + + {rows.map((row, index) => ( + + + + + + ))} + +
本轮技法审计
技法状态说明
{displayTechniqueAuditLabel(row.technique)}{techniqueAuditStatusLabel(row.status)}{displayTechniqueAuditNote(row.note)}
+
+
+ ); +} diff --git a/frontend/src/lib/consultation-agent-events.ts b/frontend/src/lib/consultation-agent-events.ts index 0aab2e41..ce5a52ee 100644 --- a/frontend/src/lib/consultation-agent-events.ts +++ b/frontend/src/lib/consultation-agent-events.ts @@ -29,6 +29,17 @@ export const workflowReceiptSchema: z.ZodType = z.object({ omittedDomains: z.array(consultationDomainSchema).min(1).max(6).optional(), }).strict(); +export const techniqueAuditStatusSchema = z.enum(["executed", "blocked", "not_applicable"]); +export type TechniqueAuditStatus = z.infer; + +export const techniqueAuditRowSchema = z.object({ + technique: z.string().trim().min(1).max(160), + status: techniqueAuditStatusSchema, + note: z.string().max(320).optional(), +}).strict(); +export type TechniqueAuditRow = z.infer; + + const executionStepSchema = z.object({ sequence: z.number().int().min(1).max(32), kind: z.enum(["skill", "tool", "validation"]), @@ -64,6 +75,7 @@ export const agentExecutionReceiptSchema = z.object({ stepBudget: stepBudgetSchema.optional(), workflow: workflowReceiptSchema, techniqueTruth: z.string().max(120).optional(), + techniqueAuditTable: z.array(techniqueAuditRowSchema).max(80).optional(), }).strict(); export type AgentExecutionReceipt = z.infer; diff --git a/frontend/src/lib/consultation-technique-audit.ts b/frontend/src/lib/consultation-technique-audit.ts new file mode 100644 index 00000000..0370884b --- /dev/null +++ b/frontend/src/lib/consultation-technique-audit.ts @@ -0,0 +1,203 @@ +import type { TechniqueAuditRow, TechniqueAuditStatus } from "./consultation-agent-events.ts"; + +export type SpokenAnswerAndTechniqueAudit = Readonly<{ + spoken: string; + rows: readonly TechniqueAuditRow[]; + complete: boolean; +}>; + +const AUDIT_HEADER = /^\|\s*技法\s*\|\s*状态\s*\|\s*说明\s*\|?\s*$/; +const AUDIT_SEPARATOR = /^\|?(?:\s*:?-{2,}:?\s*\|){2,}\s*:?-{2,}:?\s*\|?\s*$/; +const MACHINE_KEY = /^[a-z][a-z0-9_]{2,}$/i; + +const STATUS_LABELS: Readonly> = { + executed: "已执行", + blocked: "阻塞", + not_applicable: "不适用", +}; + +const TECHNIQUE_LABELS: Readonly> = { + "VedAstro Cloud State": "VedAstro 云状态", + "VedAstro Raw Archive Manifest": "VedAstro 原始档案清单", + "External Engine Cross-Validation": "外部引擎交叉验证", + "Western Cross-Validation": "西洋交叉验证", + "Cross-System Arbitration": "跨系统仲裁", + "Evidence Packet": "证据包", + "Blind Technical Mode": "盲技术模式", + "MEVG / Global Web Evidence": "MEVG / 全网证据", + "Real Case Calibration": "真实案例校准", + "Timing Precision Gate": "时间精度门", + "Functional Benefic/Malefic": "功能吉凶星", + "Formal Vargas D1–D60": "正式分盘 D1–D60", + "Research D-N through D60": "研究型 D-N 至 D60", + "Extended Vargas D81/D108/D144": "扩展分盘 D81/D108/D144", + "Arudha / UL / A10": "Arudha / UL / A10", + "Narayana Dasha": "Narayana Dasha", + "Vimshottari sub-periods": "Vimshottari 小运", + Ashtakavarga: "Ashtakavarga", + "KP cusps": "KP 宫头", + "Gulika / Mandi": "Gulika / Mandi", + "Shadbala components": "Shadbala 分量", + "Ashtakavarga Kakshya": "Ashtakavarga Kakshya", + "Western natal (tropical)": "西洋本命(回归黄道)", + "Prashna chart": "问卦盘(Prashna)", + "Ashtakoota matching": "合盘 Ashtakoota", + "Adhana / Niseka": "Adhana / Niseka", + "Muhurta election": "择时 Muhurta", +}; + +const WESTERN_LAYER_LABELS: Readonly> = { + transits: "行运", + solar_return: "太阳回归盘", + secondary_progressions: "次限", + solar_arc_directions: "太阳弧", + converse_secondary_progressions: "逆行次限", + converse_solar_arc_directions: "逆行太阳弧", + midpoints: "中点", + lunar_return: "月亮回归盘", + transit_duration_scan: "行运时长扫描", + parans: "parans", +}; + +const NOTE_LABELS: Readonly> = { + confidence_capped_without_verified_official_cloud: "未经核验的官方云证据,置信度封顶", + official_raw_archive_not_auditable_for_this_run: "官方原始档案本轮不可审计", + official_raw_archive_is_auditable: "官方原始档案可审计", + official_cloud_evidence_available: "官方云证据可用", + claims_capped_until_pyjhora_jhora_jyotishganit_are_invoked_for_this_run: "结论上限受外部引擎调用限制", + three_engine_runtime_closure_available: "三引擎运行时已闭环", + complete_packet_required_for_high_confidence: "高置信度需完整证据包", + supports_high_confidence: "支持高置信度", + normal_runtime_mode: "常规运行模式", + prevents_conversation_feedback_leakage: "防止会话反馈泄漏", + caps_claims_until_global_web_evidence_runs: "全局网页证据未运行,声明受限", + partial_reference_only_until_outcome_replay: "仅部分参考,未做结果回放", + supports_calibration: "支持校准", + blocks_false_precision_until_control_date_rankings_pass: "控制日期排名未通过前禁止伪精度", + high_rigor_claims_blocked_until_functional_nature_layer_is_present: "功能属性与自然属性冲突时降置信", + "observation for A10/UL; commercial claims stay non-deterministic": "仅观察层,商业断语保持非确定", + "component parity remains partial": "分量对齐仍为部分", + "KP remains non-deterministic in this product": "本产品内 KP 非确定性", + observation_only: "仅观察层", + "Chesta/Sthana/Dig/Drik/Kala/Naisargika as components; absolute Virupa truth is not closed": "绝对 Virupa 真值未闭环", + "observation_only; not verified day/month timing": "仅观察层,不作已核验日期", + "this run is a natal consultation": "本轮为本命咨询", + horary_path: "问卦路径", + "requires a partner chart": "需要伴侣盘", + "research_only; not a natal consultation layer": "仅研究用途", + "not an electional request": "非择时请求", + layer_not_materialized: "本层未物化", +}; + +export function techniqueAuditStatusLabel(status: TechniqueAuditStatus): string { + return STATUS_LABELS[status]; +} + +export function displayTechniqueAuditLabel(technique: string): string { + const mapped = TECHNIQUE_LABELS[technique]; + if (mapped) return mapped; + const western = /^Western (.+)$/i.exec(technique); + if (!western?.[1]) return technique; + const layer = western[1].trim(); + const layerLabel = WESTERN_LAYER_LABELS[layer] ?? layer.replaceAll("_", " "); + return `西洋${layerLabel}`; +} + +export function displayTechniqueAuditNote(note: string | undefined): string { + const trimmed = note?.trim() ?? ""; + if (!trimmed) return ""; + const mapped = NOTE_LABELS[trimmed]; + if (mapped) return mapped; + const formal = /^(\d+)\/20 named traditional charts/i.exec(trimmed); + if (formal) return `${formal[1]}/20 传统命名分盘`; + const research = /^(\d+) generic D-N charts/i.exec(trimmed); + if (research) return `${research[1]} 张通用分盘,非传统命名`; + const extended = /^(\d+) super-divisional charts/i.exec(trimmed); + if (extended) return `${extended[1]} 张超细分盘`; + if (MACHINE_KEY.test(trimmed) || trimmed.includes("_")) return ""; + return trimmed; +} + +export function parseTechniqueAuditStatus(value: string): TechniqueAuditStatus { + const normalized = value.trim().toLowerCase(); + if ( + value.trim() === "已执行" + || ["used", "executed", "complete", "official_verified", "pass", "ready", "ok", "partial"].includes(normalized) + ) { + return "executed"; + } + if ( + value.trim() === "不适用" + || ["not_applicable", "n/a", "na", "not applicable"].includes(normalized) + ) { + return "not_applicable"; + } + return "blocked"; +} + +export function normalizeTechniqueAuditRows(value: unknown): TechniqueAuditRow[] | undefined { + if (!Array.isArray(value)) return undefined; + const rows: TechniqueAuditRow[] = []; + for (const item of value.slice(0, 80)) { + if (!item || typeof item !== "object" || Array.isArray(item)) continue; + const record = item as Record; + const technique = String(record.technique ?? record.name ?? "").trim(); + if (!technique) continue; + const noteValue = record.note ?? record.boundary ?? record.effect_on_confidence; + const note = typeof noteValue === "string" ? noteValue.trim().slice(0, 320) : ""; + rows.push({ + technique: technique.slice(0, 160), + status: parseTechniqueAuditStatus(String(record.status ?? "")), + ...(note ? { note } : {}), + }); + } + return rows.length > 0 ? rows : undefined; +} + +export function splitSpokenAnswerAndTechniqueAudit(text: string): SpokenAnswerAndTechniqueAudit { + const lines = text.replace(/\r\n/g, "\n").split("\n"); + let headerIndex = -1; + for (let index = 0; index < lines.length; index += 1) { + if (AUDIT_HEADER.test(lines[index].trim())) headerIndex = index; + } + if (headerIndex < 0) { + return { spoken: text.replace(/\s+$/, ""), rows: [], complete: true }; + } + + const spoken = lines.slice(0, headerIndex).join("\n").replace(/\s+$/, ""); + const tableLines = lines.slice(headerIndex).map((line) => line.trim()); + const separator = tableLines[1] && AUDIT_SEPARATOR.test(tableLines[1]) ? tableLines[1] : null; + if (!separator) { + return { spoken, rows: [], complete: false }; + } + + const rows: TechniqueAuditRow[] = []; + for (const line of tableLines.slice(2)) { + if (!line) break; + if (!line.startsWith("|")) break; + const cells = splitGfmRow(line); + if (cells.length < 3) return { spoken, rows, complete: false }; + const technique = cells[0]?.trim() ?? ""; + if (!technique) continue; + const note = cells.slice(2).join(" | ").trim().slice(0, 320); + rows.push({ + technique: technique.slice(0, 160), + status: parseTechniqueAuditStatus(cells[1] ?? ""), + ...(note ? { note } : {}), + }); + } + + return { spoken, rows, complete: rows.length > 0 }; +} + +export function resolveTechniqueAuditRows( + split: SpokenAnswerAndTechniqueAudit, + delivered?: readonly TechniqueAuditRow[], +): readonly TechniqueAuditRow[] { + if (split.complete && split.rows.length > 0) return split.rows; + return delivered && delivered.length > 0 ? delivered : []; +} + +function splitGfmRow(line: string): string[] { + return line.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim()); +} diff --git a/frontend/src/mastra/consultation-tools.ts b/frontend/src/mastra/consultation-tools.ts index c2c7a616..9a2adb7d 100644 --- a/frontend/src/mastra/consultation-tools.ts +++ b/frontend/src/mastra/consultation-tools.ts @@ -10,7 +10,8 @@ import { applyBirthTimeModeToWorkflowContext, type ConsultationBirthTimeMode } f import { consultationMethodologyForDomains } from "../lib/consultation-methodology.ts"; import type { ServerChartConsultation } from "../lib/consultation-route-service.ts"; import { createConsultationPlan, type ConsultationPlan } from "../lib/consultation-plan.ts"; -import type { WorkflowReceipt } from "../lib/consultation-agent-events.ts"; +import type { TechniqueAuditRow, WorkflowReceipt } from "../lib/consultation-agent-events.ts"; +import { normalizeTechniqueAuditRows } from "../lib/consultation-technique-audit.ts"; import type { AgentModelFinishReason } from "../lib/agent-observability.ts"; import { consultationEvidencePacketSchema, @@ -106,6 +107,7 @@ export type ConsultationRuntimeState = { consultationToolDurationMs?: number; workflowReceipt?: WorkflowReceipt; techniqueTruth?: string; + techniqueAuditTable?: TechniqueAuditRow[]; steps: ConsultationRuntimeStep[]; stepBudget: ConsultationStepBudget; stepsTruncated: boolean; @@ -579,6 +581,9 @@ export function createConsultationTools(ctx: ConsultationAgentContext) { appendConsultationRuntimeStep(ctx.state, { kind: "tool", name: "run-jyotish-consultation", status: "completed", durationMs: ctx.state.consultationToolDurationMs }); const modelContext = toModelDomainPlanContext(executions, omittedDomains); ctx.state.methodologySectionCount = modelContext.methodology?.sections.length ?? 0; + ctx.state.techniqueAuditTable = normalizeTechniqueAuditRows( + modelContext.evidence_contract?.technique_audit_table, + ); return modelContext; } catch (error) { ctx.state.consultationToolDurationMs = now() - startedAt; diff --git a/frontend/src/mastra/index.ts b/frontend/src/mastra/index.ts index b86d887d..fccdae20 100644 --- a/frontend/src/mastra/index.ts +++ b/frontend/src/mastra/index.ts @@ -28,7 +28,7 @@ Treat the server-provided current time as authoritative for words such as today, Treat the tool result's top-level status and evidence_contract as the authoritative answer policy: - When status is ready and evidence_contract.answer_policy.can_answer_direction is true, answer the user's actual question directly. Do not begin with infrastructure or confidence disclaimers. - An unavailable optional provider or external cross-check is not a calculation failure. Never call it an internal error. -- Do not mention VedAstro, snapshot, fallback, gateway, archive, provider, MEVG, or calibration unless the user explicitly asks about methodology, or the missing layer materially blocks the exact claim they requested. The required Technique Audit Table may list those rows by their delivered labels without discussing provider internals. +- Do not mention VedAstro, snapshot, fallback, gateway, archive, provider, MEVG, or calibration unless the user explicitly asks about methodology, or the missing layer materially blocks the exact claim they requested. The collapsed Technique Audit Table may list those rows by their delivered labels without discussing provider internals. When reference_transparency is present: - Present candidate_windows and exact_triggers when relevant, but describe exact_triggers as technical trigger points, never guaranteed events. @@ -44,7 +44,7 @@ When reference_transparency is present: - Only say the chart calculation failed when evidence_contract.hard_blockers is non-empty. - Never claim D2, D11, D9, D10, A10, UL, Narayana Dasha, a delivered Varga, or a delivered Western layer is missing when it appears in evidence_contract.available_layers, evidence_contract.varga_spectrum, evidence_contract.western_spectrum, chart, or local_layers. - evidence_contract.technique_audit_table is the invocation record for this run. Use every executed layer that is relevant to the question. Status executed means the server ran it; blocked means it could not; not_applicable means it does not apply. Never treat an omitted row as permission to invent the technique, and never silently skip an executed layer the question needs. -- The visible answer must end with that Technique Audit Table in spoken Chinese labels (已执行 / 阻塞 / 不适用). This is the one table VISIBLE VOICE requires; do not also dump JSON. +- Do not paste that Technique Audit Table into the spoken answer. The product UI renders it collapsed from the delivered rows. This is the one audit surface; do not also dump JSON. - local_layers.dasha_sub_periods carries the antardasha boundaries inside the running mahadasha. When it is present, use those boundaries and never say sub-periods were not calculated; when it is absent, say so once instead of implying the calculation broke. - Treat evidence_contract.answer_policy as a hard output contract. When can_answer_precise_timing is false, provide only direction or structure and do not state a month, date, or guaranteed timing outcome. - Treat answer_policy.deterministic_claims_forbidden_for as a hard prohibition. Do not use a restricted technique to make a deterministic conclusion. reference_only, partial, blocked, research_only_blocked, and partial_registry_only are commercial claim boundaries, not validated capabilities. diff --git a/frontend/src/mastra/product-voice.ts b/frontend/src/mastra/product-voice.ts index 3a6709e9..5cb4fc34 100644 --- a/frontend/src/mastra/product-voice.ts +++ b/frontend/src/mastra/product-voice.ts @@ -5,16 +5,16 @@ Write spoken Simplified Chinese, addressing the reader as 你. - Answer the user's actual question in the first sentence. - Then use the delivered evidence the way the local skill does: unified parameters and raw structure first (degrees, houses, vargas, Dasha boundaries, Shadbala/AV, functional benefic/malefic, Western layers), then parallel systems, conflicts, and only then a conditional synthesis. - Gloss a technical term once in plain Chinese (婚姻分盘(D9), 当前大运) before using the everyday phrase. -- Prefer paragraphs. Use Markdown to compare concrete options, to show raw structure that would otherwise be a wall of numbers, and for the required Technique Audit Table at the end. -- If a limitation materially changes the claim, put it in one short closing sentence before the table; otherwise omit it. +- Prefer paragraphs. Use Markdown to compare concrete options or to show raw structure that would otherwise be a wall of numbers. Do not paste the Technique Audit Table into the spoken answer; the product UI renders those delivered rows in a collapsed control. +- If a limitation materially changes the claim, put it in one short closing sentence; otherwise omit it. -After the spoken answer, include exactly one compact Markdown Technique Audit Table with columns 技法 / 状态 / 说明. Copy rows from the delivered technique_audit_table. Translate status to 已执行 / 阻塞 / 不适用. Do not invent, omit, or upgrade a row. Do not dump JSON keys or YAML. +Do not copy the delivered technique_audit_table into the spoken answer. The product UI renders it collapsed from the server-delivered rows. Still obey every row: do not invent, omit, or upgrade a technique. Translate nothing into JSON or YAML. Status labels in that UI are 已执行 / 阻塞 / 不适用. Never do these in the visible answer: - Quote JSON keys, English status codes, or field names (core_status, blocked, ready, consumer_context, missing_layers, omitted_domains). - Say 根据系统计算, 根据当前计算结果, 综合来看, 需要指出的是, 当前状态为, 本轮未完整计算, or 从星盘来看 as an opener. - Restate the user's question, pad to a template length, or use a fixed 首先 / 其次 / 最后 skeleton. -- Dump YAML, JSON, or a second inventory besides that one table. +- Dump YAML, JSON, or a Technique Audit Table in the spoken answer. - Skip an executed layer the question needs in order to keep the answer short. Bad: 根据当前计算结果,事业方向总体偏稳定,但副运细节与行运触发未在本轮完整计算。 diff --git a/frontend/tests/chat-bundle-splitting-contract.test.ts b/frontend/tests/chat-bundle-splitting-contract.test.ts index 36ff39c4..aa334bdb 100644 --- a/frontend/tests/chat-bundle-splitting-contract.test.ts +++ b/frontend/tests/chat-bundle-splitting-contract.test.ts @@ -45,8 +45,10 @@ test("the pre-markdown fallback renders prose but never raw markdown", () => { // And: one stable wrapper serves both paths, so no styling or caret is lost on upgrade. assert.equal(contentSource.match(/className="message-markdown"/g)?.length, 1); + assert.match(contentSource, /className="message-answer"/); + assert.match(contentSource, /TechniqueAuditDisclosure/); assert.match(contentSource, /

\{paragraph\}<\/p>/); - assert.match(contentSource, /plainParagraphs\(text\) \?\? \[\]/); + assert.match(contentSource, /plainParagraphs\(spoken\) \?\? \[\]/); }); test("gsap loads on demand while keeping the reduced-motion gate", () => { diff --git a/frontend/tests/consultation-technique-audit.test.ts b/frontend/tests/consultation-technique-audit.test.ts new file mode 100644 index 00000000..8c6a8f07 --- /dev/null +++ b/frontend/tests/consultation-technique-audit.test.ts @@ -0,0 +1,112 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +import { agentExecutionReceiptSchema } from "../src/lib/consultation-agent-events.ts"; +import { + displayTechniqueAuditLabel, + displayTechniqueAuditNote, + normalizeTechniqueAuditRows, + resolveTechniqueAuditRows, + splitSpokenAnswerAndTechniqueAudit, + techniqueAuditStatusLabel, +} from "../src/lib/consultation-technique-audit.ts"; + +const disclosureSource = readFileSync(new URL("../src/components/technique-audit-disclosure.tsx", import.meta.url), "utf8"); +const contentSource = readFileSync(new URL("../src/components/chat-message-content.tsx", import.meta.url), "utf8"); +const toolsSource = readFileSync(new URL("../src/mastra/consultation-tools.ts", import.meta.url), "utf8"); +const routeSource = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8"); +const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); + +const spoken = [ + "今天适合推进沟通,不适合拍板承诺。", + "", + "| 适合推进 | 需要避开 |", + "|---|---|", + "| 汇报、提案、写作 | 冲动承诺、夸大其词 |", + "", + "给一个现在就能做的行动。", +].join("\n"); + +const auditTable = [ + "| 技法 | 状态 | 说明 |", + "|---|---|---|", + "| VedAstro 云状态 | 阻塞 | 未经核验的官方云证据,置信度封顶 |", + "| 功能吉凶星 | 已执行 | 功能属性与自然属性冲突时降置信 |", + "| 问卦盘(Prashna) | 不适用 | 本轮为本命咨询 |", +].join("\n"); + +test("a trailing technique audit table is split out of the spoken answer", () => { + const split = splitSpokenAnswerAndTechniqueAudit(`${spoken}\n\n${auditTable}`); + assert.equal(split.complete, true); + assert.equal(split.spoken, spoken); + assert.equal(split.spoken.includes("适合推进"), true); + assert.equal(split.spoken.includes("技法"), false); + assert.deepEqual(split.rows, [ + { technique: "VedAstro 云状态", status: "blocked", note: "未经核验的官方云证据,置信度封顶" }, + { technique: "功能吉凶星", status: "executed", note: "功能属性与自然属性冲突时降置信" }, + { technique: "问卦盘(Prashna)", status: "not_applicable", note: "本轮为本命咨询" }, + ]); +}); + +test("an incomplete streaming audit table is hidden rather than shown as pipes", () => { + const split = splitSpokenAnswerAndTechniqueAudit(`${spoken}\n\n| 技法 | 状态 | 说明 |\n|---|---|---|\n| VedAstro 云状态 | 阻塞 |`); + assert.equal(split.complete, false); + assert.equal(split.spoken, spoken); + assert.deepEqual(split.rows, []); +}); + +test("delivered receipt rows fill the folded table when the spoken answer has no dump", () => { + const delivered = normalizeTechniqueAuditRows([ + { technique: "VedAstro Cloud State", status: "blocked", boundary: "confidence_capped_without_verified_official_cloud" }, + { technique: "Formal Vargas D1–D60", status: "executed", boundary: "20/20 named traditional charts; remaining D-N are separate" }, + { technique: "Western transits", status: "used", note: "layer_not_materialized" }, + ]); + assert.deepEqual(delivered, [ + { technique: "VedAstro Cloud State", status: "blocked", note: "confidence_capped_without_verified_official_cloud" }, + { technique: "Formal Vargas D1–D60", status: "executed", note: "20/20 named traditional charts; remaining D-N are separate" }, + { technique: "Western transits", status: "executed", note: "layer_not_materialized" }, + ]); + assert.equal(displayTechniqueAuditLabel("VedAstro Cloud State"), "VedAstro 云状态"); + assert.equal(displayTechniqueAuditLabel("Western transits"), "西洋行运"); + assert.equal(displayTechniqueAuditNote("confidence_capped_without_verified_official_cloud"), "未经核验的官方云证据,置信度封顶"); + assert.equal(displayTechniqueAuditNote("20/20 named traditional charts; remaining D-N are separate"), "20/20 传统命名分盘"); + assert.equal(displayTechniqueAuditNote("layer_not_materialized"), "本层未物化"); + assert.equal(displayTechniqueAuditNote("unmapped_machine_key"), ""); + assert.equal(techniqueAuditStatusLabel("blocked"), "阻塞"); + + const split = splitSpokenAnswerAndTechniqueAudit(spoken); + assert.deepEqual(resolveTechniqueAuditRows(split, delivered), delivered); +}); + +test("the public receipt may carry the audit rows and still rejects internal diagnostics", () => { + const receipt = agentExecutionReceiptSchema.parse({ + runId: "run", + runtime: "mastra-agentic", + skill: { name: "jyotish-vedic-astrology", loaded: true, referenceReads: 0, methodologySections: 0 }, + steps: [{ sequence: 1, kind: "skill", name: "jyotish-vedic-astrology", status: "completed" }], + workflow: { route: "timing", status: "ready", preciseTiming: "allowed", missingLayers: [] }, + techniqueAuditTable: [ + { technique: "Narayana Dasha", status: "executed", note: "本轮无独立当前读数" }, + ], + }); + assert.equal(receipt.techniqueAuditTable?.[0]?.technique, "Narayana Dasha"); + assert.throws(() => agentExecutionReceiptSchema.parse({ + ...receipt, + techniqueAuditTable: [{ technique: "Narayana Dasha", status: "used" }], + })); +}); + +test("the folded audit control stays closed and never remounts the internal panel", () => { + assert.match(disclosureSource, /

/); + assert.doesNotMatch(disclosureSource, /]*\sopen/); + assert.doesNotMatch(disclosureSource, /EvidenceAuditPanel/); + assert.doesNotMatch(disclosureSource, /techniqueTruth|workflowReceipt|rawReceipt/); + assert.match(contentSource, /splitSpokenAnswerAndTechniqueAudit/); + assert.match(contentSource, /TechniqueAuditDisclosure/); + assert.match(toolsSource, /ctx\.state\.techniqueAuditTable = normalizeTechniqueAuditRows/); + assert.match(routeSource, /techniqueAuditTable: state\.techniqueAuditTable/); + assert.match(globalStyles, /\.markdown-table/); + assert.match(globalStyles, /\.technique-audit >/); + assert.match(globalStyles, /\.markdown-table th,/); +}); diff --git a/frontend/tests/consultation-voice-contract.test.ts b/frontend/tests/consultation-voice-contract.test.ts index c0f6bcf4..744ba92e 100644 --- a/frontend/tests/consultation-voice-contract.test.ts +++ b/frontend/tests/consultation-voice-contract.test.ts @@ -12,7 +12,7 @@ test("product voice keeps skill technique density without dumping JSON", () => { assert.match(voice, /governs METHOD, TECHNIQUE INVOCATION, and TRUTH BOUNDARIES/); assert.match(voice, /must not drop techniques, raw structures, or audit rows/); assert.match(voice, /unified parameters and raw structure first/); - assert.match(voice, /required Technique Audit Table/); + assert.match(voice, /Do not paste the Technique Audit Table/); assert.match(voice, /已执行 \/ 阻塞 \/ 不适用/); assert.match(voice, /Answer the user's actual question in the first sentence/); assert.match(voice, /本轮未完整计算/); @@ -26,6 +26,7 @@ test("product voice keeps skill technique density without dumping JSON", () => { assert.match(binding, /never quote it, reveal it, or present its report template as the chat format/); 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, /Do not paste that Technique Audit Table into the spoken answer/); assert.match(mastra, /Do not announce a skipped-domain inventory/); 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/); @@ -37,6 +38,6 @@ test("consult user turn no longer dumps tool JSON that the model would parrot", assert.match(chartBranch, /请直接用口语回答下面的问题[\s\S]*?resolvedQuestion\.modelQuestion/); assert.doesNotMatch(chartBranch, /JSON\.stringify\(toolInput\)/); assert.doesNotMatch(chartBranch, /经过服务端校验的工具参数/); - assert.match(route, /请用口语直接回答;回答末尾附上技法审计表,不要复述内部字段/); + assert.match(route, /请用口语直接回答;不要把技法审计表写进正文,不要复述内部字段/); assert.match(chartBranch, /await runConsultationWorkflow\(toolInput/); }); diff --git a/frontend/tests/evidence-audit-panel.test.ts b/frontend/tests/evidence-audit-panel.test.ts index 46ef1ee7..fe8436fe 100644 --- a/frontend/tests/evidence-audit-panel.test.ts +++ b/frontend/tests/evidence-audit-panel.test.ts @@ -3,14 +3,17 @@ import { readFileSync } from "node:fs"; import test from "node:test"; const rowSource = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8"); +const contentSource = readFileSync(new URL("../src/components/chat-message-content.tsx", import.meta.url), "utf8"); const panelSource = readFileSync(new URL("../src/components/evidence-audit-panel.tsx", import.meta.url), "utf8"); const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); -test("assistant messages do not expose the internal Technique Audit Table", () => { +test("assistant messages fold the user-facing Technique Audit Table and never remount the internal panel", () => { assert.doesNotMatch(rowSource, /EvidenceAuditPanel/); assert.doesNotMatch(rowSource, /claimStatus=\{message\.techniqueTruth\}/); assert.doesNotMatch(rowSource, /workflowReceipt=\{message\.workflowReceipt\}/); + assert.match(contentSource, /TechniqueAuditDisclosure/); + assert.match(rowSource, /auditRows=\{message\.agentExecutionReceipt\?\.techniqueAuditTable\}/); assert.match(panelSource, /Technique Audit Table/); assert.match(panelSource, /Workflow route:/); assert.match(panelSource, /Precise timing:/); @@ -25,6 +28,8 @@ test("assistant messages do not expose the internal Technique Audit Table", () = assert.match(panelSource, /查看非敏感 raw receipt/); assert.match(globalStyles, /\.evidence-audit-panel/); assert.match(globalStyles, /\.raw-evidence-receipt/); + assert.match(globalStyles, /\.technique-audit >/); + assert.match(globalStyles, /\.markdown-table/); }); test("consult responses persist workflow receipt headers for evidence rendering", () => { diff --git a/frontend/tests/skill-binding.test.ts b/frontend/tests/skill-binding.test.ts index cafda5bf..5a9ec3b4 100644 --- a/frontend/tests/skill-binding.test.ts +++ b/frontend/tests/skill-binding.test.ts @@ -60,7 +60,7 @@ test("the bound method is the runtime excerpt, not the maintainer manual", () => assert.ok(body.includes(heading), heading); assert.ok(bound.includes(heading), heading); } - for (const phrase of ["全谱系真实调用", "P0/P1 观察层", "网页对话在口语回答末尾给出 Technique Audit Table", "静态分析10步"]) { + for (const phrase of ["全谱系真实调用", "P0/P1 观察层", "网页对话的技法审计表由界面折叠展示", "静态分析10步"]) { assert.ok(bound.includes(phrase), phrase); } for (const heading of ["施工判断原则", "冲顶路线", "37大子命令", "验证与错题体系", "开源复用边界冻结"]) {