fix(rectification): bind evidence to current server turn
Staging Backend Quality Gate / validate (pull_request) Successful in 16m51s
Staging Backend Quality Gate / publish (pull_request) Has been skipped

This commit is contained in:
Jesse_Chen
2026-08-12 12:12:10 +08:00
parent 8beed38951
commit c7dfe1c166
6 changed files with 74 additions and 29 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ const agenticRectificationInstructions = `你是生时校正 Agent,只服务
硬性边界(必须服从):
1. 先调用 skill 工具加载 jyotish-birth-time-rectification(本 Case 固定版本),再调用 rectification-read-case 读取服务端 Case/Dossier 后才能行动。
2. 事件事实只能来自用户原话:不得虚构事件、日期、候选、分盘数据、评分或出生分钟;计算与持久化只能通过工具。
3. 工具 input 只传最小引用(caseId、sourceTurnId、evidenceId、resultId、candidateId、quote、proposedKind、日期精度等)。绝不传 userId、出生资料、候选范围、events 数组、分数或权限开关。
3. 工具 input 只传最小引用(caseId、evidenceId、resultId、candidateId、quote、proposedKind、日期精度等)。绝不传 userId、出生资料、候选范围、events 数组、分数或权限开关。
4. 日期精度如实保留:用户只说年份就按 year 处理,不得诱导编造月份。
5. 三层语义严格分开:candidate=当前候选比较结果;accepted=用户明确采用的排盘时间;confirmed=通过服务器确认门且用户明确同意。accepted 不等于 confirmed。
6. “是/对”只能确认当前 pending draft;用户更正事实用 revise(生成 revision,不覆盖历史)。
+5 -16
View File
@@ -1,8 +1,8 @@
/**
* V9 rectification tools (Case-ref only).
*
* The model may only pass minimal references: caseId, sourceTurnId, quote,
* proposedKind, evidenceId, resultId, candidateId (plus optional date fields
* The model may only pass minimal references: caseId, quote, proposedKind,
* evidenceId, resultId, candidateId (plus optional date fields
* the model grounded in the user's own words). userId, birth data, candidate
* ranges, event arrays, scores and permission booleans never appear in tool
* inputs. Every tool authenticates through the Case row, validates ownership
@@ -214,13 +214,6 @@ function assertEvidenceRef(input: { evidenceId?: unknown }): string {
return input.evidenceId;
}
function assertTurnRef(input: { sourceTurnId?: unknown }): string {
if (typeof input.sourceTurnId !== "string" || !uuidPattern.test(input.sourceTurnId)) {
throw new RectificationToolServiceError("invalid_source_turn_id");
}
return input.sourceTurnId;
}
function assertCandidateRef(input: { resultId?: unknown; candidateId?: unknown }): { resultId: string; candidateId: string } {
if (
typeof input.resultId !== "string" || !uuidPattern.test(input.resultId)
@@ -304,7 +297,6 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
"提出一条事件证据草稿。quote 必须能规范化匹配当前用户轮次原文;日期只保留用户说过的精度(只说年份则 year);subject 只能 self/family/othereventKind 使用受控枚举;summary 只复述用户原话,不得补充月份、原因、主动被动或人物关系。返回 evidenceId(服务器生成)。",
inputSchema: z.object({
caseId: z.string().uuid(),
sourceTurnId: z.string().uuid(),
quote: z.string().trim().min(2).max(400),
proposedKind: z.string().trim().min(1).max(48),
subject: z.enum(["self", "family", "other"]).default("self"),
@@ -316,13 +308,12 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
}).strict(),
execute: async (input) => {
assertCaseRef(input);
const sourceTurnId = assertTurnRef(input);
if (!isEvidenceKind(input.proposedKind)) throw new RectificationToolServiceError("invalid_event_kind");
if (!isEvidenceDomain(input.domain)) throw new RectificationToolServiceError("invalid_domain");
if (!isDatePrecision(input.datePrecision)) throw new RectificationToolServiceError("invalid_date_precision");
const inputFingerprint = canonicalToolInputFingerprint("rectification-propose-evidence", {
caseId: input.caseId,
sourceTurnId,
sourceTurnId: turnId,
quote: input.quote,
proposedKind: input.proposedKind,
});
@@ -331,7 +322,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
const occurredFrom = input.occurredFrom ? normalizeDatePart(input.occurredFrom) : null;
const occurredTo = input.occurredTo ? normalizeDatePart(input.occurredTo) : null;
const result = await proposeV9Evidence(accounting, userId, input.caseId, {
sourceTurnId,
sourceTurnId: turnId,
quote: input.quote,
subject: input.subject,
eventKind: input.proposedKind,
@@ -651,13 +642,11 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
caseId: z.string().uuid(),
resultId: z.string().uuid(),
candidateId: z.string().regex(timePattern),
sourceTurnId: z.string().uuid(),
consentQuote: z.string().trim().min(2).max(400),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
const { resultId } = assertCandidateRef({ resultId: input.resultId, candidateId: input.candidateId });
const sourceTurnId = assertTurnRef(input);
const inputFingerprint = canonicalToolInputFingerprint("rectification-confirm-birth-time", {
caseId: input.caseId,
resultId,
@@ -669,7 +658,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
resultId,
time: input.candidateId,
consentQuote: input.consentQuote,
sourceTurnId,
sourceTurnId: turnId,
});
const projection = {
saved_time: result.savedTime,