Files
Jyotisha/frontend/src/lib/birth-time-guide-agent.ts
T
732642856 4ceb3a5157 feat: enforce commercial rectification evidence contracts
* feat: enforce precise timing output contract

* fix: recognize package imports in fragment audit

* test: make workflow stream contract formatting-independent

* fix: preserve VedAstro evidence across async workflows

* feat: enforce commercial technique truth contract

* feat: add rectification technique receipt

* feat: extend rectification event evidence

* feat: score rectification arudha evidence

* feat: gate high rigor rectification confirmation

* feat: add controlled transit to rectification

* feat: include d11 in rectification finance scoring

* feat: add ashtakavarga rectification auxiliary

* feat: show rectification technique receipt

* feat: use verified shadbala components in rectification

* fix: trace transitive script references in fragment audit

* feat: run request-level rectification parity packet
2026-07-19 22:23:29 +08:00

232 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod";
import { lifeEventSchema } from "./birth-time-evidence.ts";
import type {
EvidenceDatePrecision,
EvidenceDomain,
QuestionSpec,
} from "./birth-time-question-planner.ts";
const caseIdSchema = z.string().uuid();
const actionIdSchema = z.string().uuid();
const turnVersionSchema = z.number().int().nonnegative();
export const birthTimeGuideRequestSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("render_question"),
caseId: caseIdSchema,
}).strict(),
z.object({
type: z.literal("draft_evidence"),
caseId: caseIdSchema,
actionId: actionIdSchema,
turnVersion: turnVersionSchema,
message: z.string().trim().min(1).max(500),
}).strict(),
z.object({
type: z.literal("generate_dynamic_question"),
caseId: caseIdSchema,
actionId: actionIdSchema,
turnVersion: turnVersionSchema,
}).strict(),
z.object({
type: z.literal("reframe_unmatched"),
caseId: caseIdSchema,
actionId: actionIdSchema,
turnVersion: turnVersionSchema,
questionId: z.string().uuid(),
note: z.string().trim().max(240).default(""),
}).strict(),
]).readonly();
export type BirthTimeGuideRequest = z.infer<typeof birthTimeGuideRequestSchema>;
export const guideQuestionVariants = ["direct", "gentle"] as const;
export const guideQuestionVariantSchema = z.enum(guideQuestionVariants);
const guideQuestionOutputSchema = z.object({
variant: guideQuestionVariantSchema,
}).strict().readonly();
export const evidenceDraftModelOutputSchema = z.object({
domain: z.enum(["education", "relocation", "relationship", "career", "finance", "health_pressure"]),
precision: z.enum(["year", "month", "day"]).nullable(),
date: z.string().trim().min(1).max(10).nullable(),
}).strict().readonly();
export const guideQuestionResponseSchema = z.object({
type: z.literal("question"),
caseId: caseIdSchema,
turnVersion: turnVersionSchema,
questionId: z.string().trim().min(1).max(120),
question: z.string().trim().min(4).max(120),
source: z.enum(["agent", "fallback"]),
}).strict().readonly();
export const guideDraftEnvelopeSchema = z.object({
type: z.literal("evidence_draft"),
actionId: actionIdSchema,
requestedTurnVersion: turnVersionSchema,
turn: z.unknown(),
}).strict().readonly();
export type GuideQuestionResponse = z.infer<typeof guideQuestionResponseSchema>;
export type ParsedEvidenceDraft = {
readonly domain: EvidenceDomain;
readonly precision: EvidenceDatePrecision | null;
readonly date: string | null;
readonly needsReview: boolean;
};
export interface BirthTimeGuideGenerator {
generate(prompt: string): Promise<{ readonly text: string }>;
}
export class BirthTimeGuideOutputError extends Error {
readonly name = "BirthTimeGuideOutputError";
readonly reason: "invalid_json" | "invalid_question" | "domain_tamper" | "repeated_question";
constructor(
reason: "invalid_json" | "invalid_question" | "domain_tamper" | "repeated_question",
) {
super(`Birth-time guide output ${reason}`);
this.reason = reason;
}
}
const subjectByDomain = {
education: "一次明显的升学、转学或学习方向变化",
relocation: "一次搬家、离乡或长期居住地变化",
relationship: "一次关系进入、关系结束或关系明显转变",
career: "一次明显的工作、职业方向或身份变化",
finance: "一次收入、资产、负债或资源渠道的明显变化",
health_pressure: "一次持续的健康压力或生活压力变化",
} as const satisfies Readonly<Record<EvidenceDomain, string>>;
function precisionQuestion(question: QuestionSpec): string {
const requested = new Set(question.requestedPrecision);
const labels = [
requested.has("year") ? "哪一年" : null,
requested.has("month") ? "哪一月" : null,
requested.has("day") ? "哪一天" : null,
].filter((label): label is string => label !== null);
return labels.length > 0 ? labels.join("或") : "什么时间";
}
export function renderQuestionVariant(
question: QuestionSpec,
variant: (typeof guideQuestionVariants)[number],
): string {
const subject = subjectByDomain[question.domain];
const timing = precisionQuestion(question);
switch (variant) {
case "direct":
return `请回想${subject},大约发生在${timing}`;
case "gentle":
return `如果方便回想,${subject}大约发生在${timing}`;
}
}
export function fallbackQuestionCopy(question: QuestionSpec): string {
return renderQuestionVariant(question, "direct");
}
export function parseJsonObject(text: string): unknown {
const normalized = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
const start = normalized.indexOf("{");
const end = normalized.lastIndexOf("}");
if (start < 0 || end <= start) throw new BirthTimeGuideOutputError("invalid_json");
try {
return JSON.parse(normalized.slice(start, end + 1));
} catch (error) {
if (error instanceof SyntaxError) throw new BirthTimeGuideOutputError("invalid_json");
throw error;
}
}
export function parseGuideQuestionOutput(value: unknown, question: QuestionSpec): string {
const parsed = guideQuestionOutputSchema.safeParse(value);
if (!parsed.success) throw new BirthTimeGuideOutputError("invalid_question");
return renderQuestionVariant(question, parsed.data.variant);
}
function normalizeNumerals(value: string): string {
return value.normalize("NFKC").replace(/[\u00a0\s]+/g, " ");
}
function isoDateIsGrounded(source: string, date: string): boolean {
const escaped = date.replaceAll("-", "\\-");
return new RegExp(`(?:^|[^0-9A-Za-z-])${escaped}(?=$|[^0-9A-Za-z-])`).test(source);
}
function chineseDateIsGrounded(source: string, date: string): boolean {
const parts = date.split("-").map(Number);
const year = parts[0];
if (year === undefined || !Number.isInteger(year)) return false;
let expression = `(?:^|[^0-9])${year}\\s*年`;
const month = parts[1];
if (month !== undefined) {
if (!Number.isInteger(month)) return false;
expression += `\\s*0?${month}\\s*月`;
}
const day = parts[2];
if (day !== undefined) {
if (!Number.isInteger(day)) return false;
expression += `\\s*0?${day}\\s*(?:日|号)`;
}
return new RegExp(expression).test(source);
}
function dateIsGrounded(date: string, sourceMessage: string): boolean {
const source = normalizeNumerals(sourceMessage).trim();
if (/^(19|20)\d{2}$/.test(date)) {
return source === date || chineseDateIsGrounded(source, date);
}
return chineseDateIsGrounded(source, date) || isoDateIsGrounded(source, date);
}
export function parseEvidenceDraftOutput(
value: unknown,
context: {
readonly requiredDomain: EvidenceDomain;
readonly sourceMessage?: string;
},
): ParsedEvidenceDraft {
const parsed = evidenceDraftModelOutputSchema.parse(value);
if (parsed.domain !== context.requiredDomain) {
throw new BirthTimeGuideOutputError("domain_tamper");
}
if (
parsed.date !== null
&& context.sourceMessage !== undefined
&& !dateIsGrounded(parsed.date, context.sourceMessage)
) {
return { domain: context.requiredDomain, precision: null, date: null, needsReview: true };
}
const complete = parsed.precision !== null
&& parsed.date !== null
&& lifeEventSchema.safeParse({
id: "00000000-0000-4000-8000-000000000000",
domain: parsed.domain,
precision: parsed.precision,
date: parsed.date,
}).success;
return { ...parsed, needsReview: !complete };
}
export function renderQuestionPrompt(): string {
return JSON.stringify({
task: "select_question_variant",
allowedVariants: guideQuestionVariants,
});
}
export function draftEvidencePrompt(question: QuestionSpec, message: string): string {
return JSON.stringify({
task: "draft_evidence",
serverQuestion: fallbackQuestionCopy(question),
requiredDomain: question.domain,
requestedPrecision: question.requestedPrecision,
userMessage: message,
});
}