fix(consult): allow reported-time precision and persist receipts
This commit is contained in:
@@ -825,8 +825,9 @@ export async function POST(request: Request) {
|
||||
if (!prepared.serverChart) throw new Error("server_chart_truth_missing");
|
||||
const toolInput = consultationInputSchema.parse({
|
||||
...prepared.serverChart.toolInput,
|
||||
// Unverified use is still a normal chart calculation with a hard answer
|
||||
// boundary. It must never reactivate the retired rectification questionnaire.
|
||||
// A user-reported concrete minute is a normal chart calculation. Keep its
|
||||
// provenance, but let server evidence—not rectification purchase state—own
|
||||
// precise-timing permission. Never reactivate the retired questionnaire.
|
||||
entryMode: "direct_chart",
|
||||
question: resolvedQuestion.modelQuestion,
|
||||
theme: parsed.data.theme,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentExecutionReceipt, PublicActivityPhase } from "./consultation-agent-events.ts";
|
||||
import type { AgentExecutionReceipt, PublicActivityPhase, WorkflowReceipt } from "./consultation-agent-events.ts";
|
||||
|
||||
export type AgentActivityView = Readonly<{
|
||||
phase: PublicActivityPhase;
|
||||
@@ -11,12 +11,7 @@ export type ChatMessage = {
|
||||
readonly suggestions?: readonly string[];
|
||||
readonly techniqueTruth?: string;
|
||||
readonly agentExecutionReceipt?: AgentExecutionReceipt;
|
||||
readonly workflowReceipt?: {
|
||||
readonly route: string;
|
||||
readonly status: string;
|
||||
readonly preciseTiming: string;
|
||||
readonly missingLayers: readonly string[];
|
||||
};
|
||||
readonly workflowReceipt?: WorkflowReceipt;
|
||||
};
|
||||
|
||||
export type ChatMessageView = ChatMessage & {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { agentExecutionReceiptSchema, type AgentExecutionReceipt } from "./consultation-agent-events.ts";
|
||||
import {
|
||||
agentExecutionReceiptSchema,
|
||||
workflowReceiptSchema,
|
||||
type AgentExecutionReceipt,
|
||||
type WorkflowReceipt,
|
||||
} from "./consultation-agent-events.ts";
|
||||
import { consultationDomainSchema, type ConsultationDomain } from "./consultation-domain-registry.ts";
|
||||
|
||||
const chatMessageSchema = z.object({
|
||||
@@ -8,12 +13,7 @@ const chatMessageSchema = z.object({
|
||||
suggestions: z.array(z.string().max(200)).max(3).optional(),
|
||||
techniqueTruth: z.string().max(120).optional(),
|
||||
agentExecutionReceipt: agentExecutionReceiptSchema.optional(),
|
||||
workflowReceipt: z.object({
|
||||
route: z.string().max(120),
|
||||
status: z.string().max(120),
|
||||
preciseTiming: z.string().max(120),
|
||||
missingLayers: z.array(z.string().max(120)).max(30),
|
||||
}).strict().optional(),
|
||||
workflowReceipt: workflowReceiptSchema.optional(),
|
||||
}).strict();
|
||||
|
||||
export const chatSessionWriteSchema = z.object({
|
||||
@@ -44,12 +44,7 @@ export type ChatSessionWrite = Readonly<{
|
||||
suggestions?: readonly string[];
|
||||
techniqueTruth?: string;
|
||||
agentExecutionReceipt?: AgentExecutionReceipt;
|
||||
workflowReceipt?: Readonly<{
|
||||
route: string;
|
||||
status: string;
|
||||
preciseTiming: string;
|
||||
missingLayers: readonly string[];
|
||||
}>;
|
||||
workflowReceipt?: WorkflowReceipt;
|
||||
}>[];
|
||||
session_type: "consultation" | "birth_time_rectification";
|
||||
rectification_case_id: string | null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { consultationDomainSchema } from "./consultation-domain-registry.ts";
|
||||
import { consultationDomainSchema, type ConsultationDomain } from "./consultation-domain-registry.ts";
|
||||
|
||||
export const publicActivityPhaseSchema = z.enum([
|
||||
"loading-method",
|
||||
@@ -9,14 +9,21 @@ export const publicActivityPhaseSchema = z.enum([
|
||||
]);
|
||||
export type PublicActivityPhase = z.infer<typeof publicActivityPhaseSchema>;
|
||||
|
||||
export const workflowReceiptSchema = z.object({
|
||||
export type WorkflowReceipt = Readonly<{
|
||||
route: string;
|
||||
status: string;
|
||||
preciseTiming: string;
|
||||
missingLayers: readonly string[];
|
||||
domains?: readonly ConsultationDomain[];
|
||||
}>;
|
||||
|
||||
export const workflowReceiptSchema: z.ZodType<WorkflowReceipt> = z.object({
|
||||
route: z.string().max(120),
|
||||
status: z.string().max(120),
|
||||
preciseTiming: z.string().max(120),
|
||||
missingLayers: z.array(z.string().max(120)).max(30),
|
||||
domains: z.array(consultationDomainSchema).min(1).max(6).optional(),
|
||||
}).strict();
|
||||
export type WorkflowReceipt = z.infer<typeof workflowReceiptSchema>;
|
||||
|
||||
const executionStepSchema = z.object({
|
||||
sequence: z.number().int().min(1).max(32),
|
||||
|
||||
@@ -34,7 +34,6 @@ export function applyBirthTimeModeToWorkflowContext<
|
||||
...context.consumer_context,
|
||||
answer_policy: {
|
||||
...context.consumer_context.answer_policy,
|
||||
can_answer_precise_timing: false,
|
||||
birth_time_confidence: "unverified_reported_time",
|
||||
candidate_is_confirmed: false,
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@ function deepFreeze<T>(value: T): DeepReadonly<T> {
|
||||
}
|
||||
|
||||
function precisionBoundaryFor(mode: ConsultationBirthTimeMode) {
|
||||
return mode === "verified_chart" ? "server_evidence_required" as const : "precise_timing_blocked" as const;
|
||||
return mode === "general_no_birth_time" ? "precise_timing_blocked" as const : "server_evidence_required" as const;
|
||||
}
|
||||
|
||||
export function validateConsultationPlan(
|
||||
|
||||
@@ -21,13 +21,21 @@ test("chat session schema preserves the safe agent execution receipt", () => {
|
||||
runtime: "mastra-agentic" as const,
|
||||
skill: { name: "jyotish-vedic-astrology" as const, loaded: true },
|
||||
steps: [{ sequence: 1, kind: "skill" as const, name: "jyotish-vedic-astrology", status: "completed" as const }],
|
||||
workflow: { route: "career", status: "ready", preciseTiming: "blocked", missingLayers: [] },
|
||||
workflow: { route: "multi-domain", status: "ready", preciseTiming: "allowed", missingLayers: [], domains: ["general", "timing"] },
|
||||
techniqueTruth: "verified",
|
||||
};
|
||||
const workflowReceipt = {
|
||||
route: "multi-domain",
|
||||
status: "ready",
|
||||
preciseTiming: "allowed",
|
||||
missingLayers: [],
|
||||
domains: ["general", "timing"] as const,
|
||||
};
|
||||
const parsed = chatSessionWriteSchema.parse({
|
||||
...values,
|
||||
messages: [{ role: "assistant", text: "回答", agentExecutionReceipt: receipt }],
|
||||
messages: [{ role: "assistant", text: "回答", workflowReceipt, agentExecutionReceipt: receipt }],
|
||||
});
|
||||
assert.deepEqual(parsed.messages[0]?.workflowReceipt, workflowReceipt);
|
||||
assert.deepEqual(parsed.messages[0]?.agentExecutionReceipt, receipt);
|
||||
});
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ test("context-bound tool keeps legacy single theme compatibility and calculates
|
||||
assert.deepEqual(captured, { ...serverChart.toolInput, entryMode: "direct_chart", question: "事业如何", theme: "career" });
|
||||
assert.strictEqual(capturedPlan, plan);
|
||||
assert.equal(state.consultationToolCallCount, 1);
|
||||
assert.equal(state.workflowReceipt?.preciseTiming, "blocked");
|
||||
assert.equal(state.workflowReceipt?.preciseTiming, "allowed");
|
||||
assert.deepEqual(state.workflowReceipt?.domains, ["career"]);
|
||||
assert.deepEqual((first as { domains?: string[] }).domains, ["career"]);
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ test("general-no-birth-time is an explicit server mode that never runs a chart w
|
||||
assert.equal(shouldRunBirthChartWorkflow("verified_chart"), true);
|
||||
});
|
||||
|
||||
test("unverified chart context can never become confirmed or retain precise timing permission", () => {
|
||||
test("unverified chart context preserves evidence-owned precise timing permission while retaining provenance", () => {
|
||||
const original = {
|
||||
success: true,
|
||||
consumer_context: {
|
||||
@@ -35,21 +35,27 @@ test("unverified chart context can never become confirmed or retain precise timi
|
||||
assert.equal(original.consumer_context.answer_policy.can_answer_precise_timing, true);
|
||||
assert.deepEqual(guarded.consumer_context.answer_policy, {
|
||||
can_answer_direction: true,
|
||||
can_answer_precise_timing: false,
|
||||
can_answer_precise_timing: true,
|
||||
birth_time_confidence: "unverified_reported_time",
|
||||
candidate_is_confirmed: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("unverified answers keep the timing guard without a repetitive rectification warning", () => {
|
||||
const transform = createBirthTimeModeOutputGuard("unverified_birth_time", false);
|
||||
const first = transform("2026年8月适合观察方向。");
|
||||
const second = transform("你一定会升职。");
|
||||
test("unverified answers preserve exact dates when server evidence allows precise timing", () => {
|
||||
const transform = createBirthTimeModeOutputGuard("unverified_birth_time", true);
|
||||
const answer = transform("Rahu 大运为 2013年11月21日 至 2031年11月22日。");
|
||||
|
||||
assert.doesNotMatch(first, new RegExp(UNVERIFIED_BIRTH_TIME_NOTICE));
|
||||
assert.match(first, /具体时间已省略/);
|
||||
assert.doesNotMatch(second, /一定会升职/);
|
||||
assert.doesNotMatch(second, new RegExp(UNVERIFIED_BIRTH_TIME_NOTICE));
|
||||
assert.equal(answer, "Rahu 大运为 2013年11月21日 至 2031年11月22日。");
|
||||
assert.doesNotMatch(answer, new RegExp(UNVERIFIED_BIRTH_TIME_NOTICE));
|
||||
assert.doesNotMatch(answer, /具体时间已省略/);
|
||||
});
|
||||
|
||||
test("evidence-blocked unverified answers still use the deterministic timing guard", () => {
|
||||
const transform = createBirthTimeModeOutputGuard("unverified_birth_time", false);
|
||||
const answer = transform("2026年8月适合观察方向。");
|
||||
|
||||
assert.match(answer, /具体时间已省略/);
|
||||
assert.doesNotMatch(answer, new RegExp(UNVERIFIED_BIRTH_TIME_NOTICE));
|
||||
});
|
||||
|
||||
test("general mode deterministically rejects personal chart claims while preserving general knowledge", () => {
|
||||
|
||||
@@ -35,12 +35,12 @@ test("rejects a theme outside the consultation allowlist", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("server birth-time mode fixes the precision boundary", () => {
|
||||
test("reported concrete birth time uses the same evidence-owned precision boundary as a verified chart", () => {
|
||||
assert.equal(createConsultationPlan({
|
||||
userIntent: "未来什么时候适合行动",
|
||||
theme: "timing",
|
||||
consultationMode: "unverified_birth_time",
|
||||
}).precisionBoundary, "precise_timing_blocked");
|
||||
}).precisionBoundary, "server_evidence_required");
|
||||
assert.equal(createConsultationPlan({
|
||||
userIntent: "什么是上升星座",
|
||||
theme: "general",
|
||||
@@ -55,7 +55,7 @@ test("rejects mode or precision tampering", () => {
|
||||
consultationMode: "unverified_birth_time",
|
||||
});
|
||||
assert.throws(
|
||||
() => validateConsultationPlan({ ...plan, precisionBoundary: "server_evidence_required" }, {
|
||||
() => validateConsultationPlan({ ...plan, precisionBoundary: "precise_timing_blocked" }, {
|
||||
consultationMode: "unverified_birth_time",
|
||||
modelCreditCost: 1,
|
||||
}),
|
||||
|
||||
@@ -25,7 +25,7 @@ test("timing questions use a legal report theme and preserve a timing route hint
|
||||
assert.ok(request.requiredLayers.includes("negative holdout gate"));
|
||||
assert.equal(request.claimBoundary, "candidate_day_month_window_only_until_holdout_passes");
|
||||
assert.equal(request.plan_version, "consultation-plan-v2");
|
||||
assert.equal(request.precision_boundary, "precise_timing_blocked");
|
||||
assert.equal(request.precision_boundary, "server_evidence_required");
|
||||
assert.equal(request.timing_horizon, "next_12_months");
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ test("timing input projects only legal private workflow fields", async () => {
|
||||
assert.equal(body.strict_workflow_route, "timing");
|
||||
assert.ok(body.required_layers.includes("negative holdout gate"));
|
||||
assert.equal(body.claim_boundary, "candidate_day_month_window_only_until_holdout_passes");
|
||||
assert.equal(body.precision_boundary, "precise_timing_blocked");
|
||||
assert.equal(body.precision_boundary, "server_evidence_required");
|
||||
assert.deepEqual(body.requested_domains, ["timing"]);
|
||||
assert.deepEqual(body.required_evidence_categories, ["natal_foundation", "timing", "validation"]);
|
||||
assert.equal(input.question, "未来哪些阶段值得把握?");
|
||||
|
||||
Reference in New Issue
Block a user