From 3427b30fd42e9746c65e4f0841791dd7f91d88db Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Tue, 21 Jul 2026 15:04:07 +0800 Subject: [PATCH] feat: add strict workflow taxonomy and claim badges --- frontend/src/app/globals.css | 1 + frontend/src/components/chat-message-row.tsx | 3 +- .../src/components/claim-boundary-badge.tsx | 13 ++++ .../src/lib/consultation-workflow-request.ts | 69 ++++++++++++++++--- frontend/tests/claim-boundary-badge.test.ts | 16 +++++ .../consultation-workflow-contract.test.ts | 8 +++ .../consultation-workflow-request.test.ts | 12 ++-- 7 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/claim-boundary-badge.tsx create mode 100644 frontend/tests/claim-boundary-badge.test.ts diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index dd4ea9c4..d153c602 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -148,6 +148,7 @@ button:disabled { cursor: default; opacity: .45; } .message-markdown { color: var(--color-ink); font-size: 17px; line-height: 1.65; } .message-markdown > *:first-child { margin-top: 0; } .message-markdown > *:last-child { margin-bottom: 0; } +.claim-boundary-badge { width: fit-content; margin: 0 0 var(--space-2); padding: 4px 9px; border: 1px solid color-mix(in srgb, var(--color-action) 24%, transparent); border-radius: 999px; background: var(--color-action-soft); color: var(--color-action-hover); font-size: 12px; line-height: 1.45; } .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; } diff --git a/frontend/src/components/chat-message-row.tsx b/frontend/src/components/chat-message-row.tsx index 5abd93c3..2791a612 100644 --- a/frontend/src/components/chat-message-row.tsx +++ b/frontend/src/components/chat-message-row.tsx @@ -1,4 +1,5 @@ import { ChatMessageContent } from "@/components/chat-message-content"; +import { ClaimBoundaryBadge } from "@/components/claim-boundary-badge"; import type { ChatMessageView } from "@/lib/chat-message-view"; export function AgentAvatar() { @@ -23,7 +24,7 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView {message.role === "assistant" ? ( message.state === "thinking" ?
- : + : <> ) :

{message.text}

} diff --git a/frontend/src/components/claim-boundary-badge.tsx b/frontend/src/components/claim-boundary-badge.tsx new file mode 100644 index 00000000..e9dd5b28 --- /dev/null +++ b/frontend/src/components/claim-boundary-badge.tsx @@ -0,0 +1,13 @@ +const boundaryCopy: Record = { + unknown: "证据边界未知", + partial: "部分证据闭环", + observation_only: "仅观察", + blocked: "证据阻塞", + reference_only: "仅参考", +}; + +export function ClaimBoundaryBadge({ status }: { readonly status?: string }) { + const normalized = status?.trim() || "unknown"; + const label = boundaryCopy[normalized] ?? `证据状态:${normalized}`; + return

{label} · 不把未闭环内容包装成确定预测。

; +} diff --git a/frontend/src/lib/consultation-workflow-request.ts b/frontend/src/lib/consultation-workflow-request.ts index 1f4d08d8..0fe7b16e 100644 --- a/frontend/src/lib/consultation-workflow-request.ts +++ b/frontend/src/lib/consultation-workflow-request.ts @@ -2,15 +2,64 @@ export const consultationThemeValues = ["career", "marriage", "wealth", "timing" export type ConsultationTheme = typeof consultationThemeValues[number]; +export type ConsultationWorkflowRoute = + | "career" + | "marriage" + | "wealth" + | "timing" + | "rectification" + | "prashna" + | "general"; + +type WorkflowProjection = { + question: string; + themes: readonly ("career" | "marriage" | "wealth")[]; + strictWorkflowRoute: ConsultationWorkflowRoute; + requiredLayers: readonly string[]; + claimBoundary: string; +}; + +const routeRequirements: Record & { prefix?: string }> = { + career: { + themes: ["career"], + strictWorkflowRoute: "career", + requiredLayers: ["D1", "D10", "10th house/lord", "A10", "AmK", "Vimshottari", "Narayana", "Transit"], + claimBoundary: "career_direction_and_broad_timing_only", + }, + marriage: { + themes: ["marriage"], + strictWorkflowRoute: "marriage", + requiredLayers: ["D1", "D9", "7th house/lord", "Venus/Jupiter", "DK", "UL", "A7", "Vimshottari", "Narayana", "Transit"], + claimBoundary: "relationship_pattern_and_broad_window_only", + }, + wealth: { + themes: ["wealth"], + strictWorkflowRoute: "wealth", + requiredLayers: ["D1", "D2", "D11", "2nd/11th/9th/5th houses", "Wealth Yogas", "Ashtakavarga", "Dasha"], + claimBoundary: "wealth_structure_not_financial_advice", + }, + timing: { + themes: ["career"], + strictWorkflowRoute: "timing", + requiredLayers: ["Vimshottari", "Narayana", "Transit", "Varga", "negative holdout gate"], + claimBoundary: "candidate_day_month_window_only_until_holdout_passes", + prefix: "应期与阶段问题:", + }, + general: { + themes: ["career", "marriage", "wealth"], + strictWorkflowRoute: "general", + requiredLayers: ["D1", "D9", "D10", "D2", "Dasha", "Narayana", "Transit", "Functional Benefic/Malefic"], + claimBoundary: "multi_domain_summary_with_missing_layers_disclosed", + }, +}; + export function projectConsultationWorkflowRequest(question: string, theme: ConsultationTheme) { - switch (theme) { - case "career": - case "marriage": - case "wealth": - return { question, themes: [theme] } as const; - case "timing": - return { question: `应期与阶段问题:${question}`, themes: ["career"] } as const; - case "general": - return { question, themes: ["career", "marriage", "wealth"] } as const; - } + const requirement = routeRequirements[theme]; + return { + question: `${requirement.prefix ?? ""}${question}`, + themes: requirement.themes, + strictWorkflowRoute: requirement.strictWorkflowRoute, + requiredLayers: requirement.requiredLayers, + claimBoundary: requirement.claimBoundary, + } satisfies WorkflowProjection; } diff --git a/frontend/tests/claim-boundary-badge.test.ts b/frontend/tests/claim-boundary-badge.test.ts new file mode 100644 index 00000000..dd481914 --- /dev/null +++ b/frontend/tests/claim-boundary-badge.test.ts @@ -0,0 +1,16 @@ +import assert from "node:assert/strict"; +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 badgeSource = readFileSync(new URL("../src/components/claim-boundary-badge.tsx", import.meta.url), "utf8"); +const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); + +test("assistant messages render a claim boundary badge from technique truth", () => { + assert.match(rowSource, /ClaimBoundaryBadge/); + assert.match(rowSource, /status=\{message\.techniqueTruth\}/); + assert.match(badgeSource, /不把未闭环内容包装成确定预测/); + assert.match(badgeSource, /observation_only/); + assert.match(badgeSource, /reference_only/); + assert.match(globalStyles, /\.claim-boundary-badge/); +}); diff --git a/frontend/tests/consultation-workflow-contract.test.ts b/frontend/tests/consultation-workflow-contract.test.ts index fb90af0f..447d840a 100644 --- a/frontend/tests/consultation-workflow-contract.test.ts +++ b/frontend/tests/consultation-workflow-contract.test.ts @@ -34,3 +34,11 @@ test("carries commercial technique truth into the model contract", () => { assert.match(mastra, /Do not use a restricted technique/); assert.match(route, /x-jyotish-technique-truth/); }); + +test("projects consultation themes through explicit strict workflow taxonomy", () => { + const projection = readFileSync(new URL("../src/lib/consultation-workflow-request.ts", import.meta.url), "utf8"); + assert.match(projection, /strictWorkflowRoute/); + assert.match(projection, /claimBoundary/); + assert.match(projection, /requiredLayers/); + assert.match(projection, /negative holdout gate/); +}); diff --git a/frontend/tests/consultation-workflow-request.test.ts b/frontend/tests/consultation-workflow-request.test.ts index 210aa518..2961f805 100644 --- a/frontend/tests/consultation-workflow-request.test.ts +++ b/frontend/tests/consultation-workflow-request.test.ts @@ -11,11 +11,13 @@ test("timing questions use a legal report theme and preserve a timing route hint // When: its workflow request is projected for the Python service. const request = projectConsultationWorkflowRequest(question, "timing"); - // Then: the illegal public theme is converted to a legal report theme with a route hint. - assert.deepEqual(request, { - question: "应期与阶段问题:未来哪些阶段值得把握?", - themes: ["career"], - }); + // Then: the illegal public theme is converted to a legal report theme with strict route metadata. + assert.equal(request.question, "应期与阶段问题:未来哪些阶段值得把握?"); + assert.deepEqual(request.themes, ["career"]); + assert.equal(request.strictWorkflowRoute, "timing"); + assert.ok(request.requiredLayers.includes("Narayana")); + assert.ok(request.requiredLayers.includes("negative holdout gate")); + assert.equal(request.claimBoundary, "candidate_day_month_window_only_until_holdout_passes"); }); test("timing input projects only legal private workflow fields", async () => {