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 () => {