fix(rectification): ingest first-turn events without dropping day precision or claiming a unique minute
Independent Staging Quality Gate / validate (push) Successful in 10m5s
Independent Staging Quality Gate / publish (push) Successful in 7m4s

SQL kinds now match the TypeScript ledger so batch ingest can confirm dated events. Confirm no longer burns the opening focus, recap uses server date labels, and the Agent sees indistinguishable width instead of a fake unique minute.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-19 07:44:23 +08:00
parent 2bf7472645
commit f708edf365
32 changed files with 2046 additions and 100 deletions
@@ -0,0 +1,45 @@
/**
* Server-facing candidate plateau helpers. These numbers are for Agent
* projection only; they never grant exact-minute confirmation.
*/
import { RECTIFICATION_POLICY } from "../../rectification-policy.ts";
export function timeToMinutes(value: string): number | null {
const match = /^(?:[01]\d|2[0-3]):([0-5]\d)$/.exec(value);
if (!match) return null;
const hours = Number(value.slice(0, 2));
const minutes = Number(match[1]);
return hours * 60 + minutes;
}
/**
* Width of the indistinguishable top cluster, in minutes.
* Uses the engine's tied_minute_count and the inclusive span of public times.
*/
export function indistinguishableWidthMinutes(
candidates: readonly Readonly<{
time: string;
rank: number;
tiedMinuteCount: number;
}>[],
): number {
if (candidates.length === 0) return 0;
const ranked = [...candidates].sort((left, right) => left.rank - right.rank);
const top = ranked[0]!;
const minutes = ranked
.map((candidate) => timeToMinutes(candidate.time))
.filter((value): value is number => value !== null);
const span = minutes.length === 0
? 0
: Math.max(...minutes) - Math.min(...minutes) + 1;
return Math.max(top.tiedMinuteCount, span, 1);
}
export function confirmationAllowedForWidth(
storedConfirmationAllowed: boolean,
widthMinutes: number,
): boolean {
return storedConfirmationAllowed
&& widthMinutes <= RECTIFICATION_POLICY.maxConfirmationWidthMinutes;
}
@@ -257,6 +257,7 @@ const KNOWN_RPC_ERROR_CODES = new Map<string, { status: number; code: string; me
["agentic_rectification_evidence_not_found", { status: 404, code: "evidence_not_found", message: "事件记录不存在或无权访问" }],
["agentic_rectification_evidence_not_confirmable", { status: 409, code: "evidence_not_confirmable", message: "该事件当前不能确认" }],
["agentic_rectification_evidence_not_revisable", { status: 409, code: "evidence_not_revisable", message: "该事件当前不能修订" }],
["agentic_rectification_precision_downgrade", { status: 422, code: "precision_downgrade", message: "不能把已确认的更细日期精度改粗" }],
]);
export type RectificationServiceErrorView = {
@@ -89,4 +89,4 @@ export function evidenceWritesAllowed(
export const MAX_RESUMABLE_CASES_PER_USER = 1;
export const RECTIFICATION_SKILL_NAME = "jyotish-birth-time-rectification";
export const RECTIFICATION_SKILL_VERSION = "10.0.0";
export const RECTIFICATION_SKILL_VERSION = "10.0.1";
@@ -129,8 +129,45 @@ export function canTransitEvidenceStatus(
* is a substring match after whitespace/punctuation normalization of the
* source turn's user message.
*/
const QUOTE_PUNCTUATION = /[\s\u3000,。!?、;:“”‘’()《》·—…,!.;:?]/g;
export function normalizeQuote(value: string): string {
return value.replace(/[\s\u3000,。!?、;:“”‘’()《》·—…]/g, "").toLowerCase();
return value.replace(QUOTE_PUNCTUATION, "").toLowerCase();
}
const PRECISION_RANK: Readonly<Record<string, number>> = {
day: 4,
month: 3,
quarter: 2,
range: 2,
year: 1,
unknown: 0,
};
export function datePrecisionRank(precision: string): number {
return PRECISION_RANK[precision] ?? -1;
}
/**
* Agent-facing date label. Day precision must never collapse to a year.
*/
export function displayDateLabel(
precision: string,
occurredFrom: string | null,
occurredTo: string | null,
): string {
const from = occurredFrom?.slice(0, 10) ?? "";
const to = occurredTo?.slice(0, 10) ?? "";
if (precision === "day" && from) return from;
if (precision === "month" && from) return from.slice(0, 7);
if (precision === "year" && from) return `${from.slice(0, 4)}`;
if (precision === "quarter" && from) return from.slice(0, 7);
if (precision === "range") {
if (from && to) return `${from}${to}`;
return from || to || "日期范围";
}
if (precision === "unknown") return "日期不明";
return from || "日期不明";
}
export function quoteIsGroundedInMessage(
@@ -886,6 +886,14 @@ export function receiptStatusFromTurn(status: string): "completed" | "degraded"
return "failed";
}
export type ProposeEvidenceResult = Readonly<{
evidenceId: string | null;
idempotent: boolean;
outcome: "accepted" | "rejected";
errorCode: string | null;
status: string;
}>;
export async function proposeV9Evidence(
accounting: AccountingClient,
userId: string,
@@ -901,8 +909,14 @@ export async function proposeV9Evidence(
datePrecision: string;
summary: string;
},
): Promise<Readonly<{ evidenceId: string; idempotent: boolean }>> {
const row = await rpc<{ evidence_id?: unknown; idempotent?: unknown }>(
): Promise<ProposeEvidenceResult> {
const row = await rpc<{
evidence_id?: unknown;
idempotent?: unknown;
outcome?: unknown;
error_code?: unknown;
status?: unknown;
}>(
accounting,
"propose_agentic_rectification_evidence",
{
@@ -919,9 +933,24 @@ export async function proposeV9Evidence(
p_summary: input.summary,
},
);
const evidenceId = typeof row?.evidence_id === "string" ? row.evidence_id : "";
if (!evidenceId) throw new RectificationToolServiceError("invalid_evidence_id");
return { evidenceId, idempotent: row?.idempotent === true };
const errorCode = rowText(row?.error_code);
const evidenceId = typeof row?.evidence_id === "string" ? row.evidence_id : null;
if (errorCode || !evidenceId) {
return {
evidenceId: null,
idempotent: false,
outcome: "rejected",
errorCode: errorCode ?? "invalid_item",
status: String(row?.status ?? "rejected"),
};
}
return {
evidenceId,
idempotent: row?.idempotent === true,
outcome: "accepted",
errorCode: null,
status: String(row?.status ?? "draft"),
};
}
export async function confirmV9Evidence(
@@ -1044,9 +1073,9 @@ export async function confirmV10Evidence(
accounting: AccountingClient,
userId: string,
caseId: string,
focusId: string,
focusId: string | null,
evidenceId: string,
): Promise<Readonly<{ focusId: string; evidenceId: string; status: string; idempotent: boolean }>> {
): Promise<Readonly<{ focusId: string | null; evidenceId: string; status: string; idempotent: boolean }>> {
const row = await rpc<Record<string, unknown>>(
accounting,
"confirm_agentic_rectification_evidence_v10",
@@ -1060,7 +1089,7 @@ export async function confirmV10Evidence(
const confirmedId = rowText(row.evidence_id);
if (!confirmedId) throw new RectificationToolServiceError("invalid_evidence_id");
return {
focusId: rowText(row.focus_id) ?? focusId,
focusId: rowText(row.focus_id),
evidenceId: confirmedId,
status: String(row.status ?? "confirmed"),
idempotent: row.idempotent === true,
@@ -1448,6 +1477,7 @@ export function safeToolErrorCode(error: unknown): string {
"attempt_not_successful",
"idempotency_conflict",
"invalid_input",
"precision_downgrade",
];
for (const code of known) {
if (message.includes(`agentic_rectification_${code}`)) return code;