feat: add strict workflow taxonomy and claim badges

This commit is contained in:
732642856
2026-07-21 15:04:07 +08:00
parent 878ff96dc5
commit 3427b30fd4
7 changed files with 106 additions and 16 deletions
+1
View File
@@ -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; }
+2 -1
View File
@@ -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"
? <div className="thinking"><i /><i /><i /></div>
: <ChatMessageContent text={message.text} />
: <><ClaimBoundaryBadge status={message.techniqueTruth} /><ChatMessageContent text={message.text} /></>
) : <p>{message.text}</p>}
</div>
</div>
@@ -0,0 +1,13 @@
const boundaryCopy: Record<string, string> = {
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 <p className="claim-boundary-badge">{label} · </p>;
}
@@ -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<ConsultationTheme, Omit<WorkflowProjection, "question"> & { 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;
}
@@ -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/);
});
@@ -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/);
});
@@ -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 () => {