feat: add strict workflow taxonomy and claim badges
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user