feat: add VedAstro rectification post-validation

This commit is contained in:
Jesse_Chen
2026-07-30 11:48:02 +08:00
parent 2384faf367
commit 31e5709f33
13 changed files with 856 additions and 24 deletions
@@ -188,6 +188,38 @@ export const candidateSplitSchema = z.object({
eventIds: z.array(uuid).max(100),
}).strict();
export const vedAstroCandidateMetricSchema = z.object({
role: z.enum(["primary", "runner_up"]),
requestedEventCount: z.number().int().nonnegative().max(20),
successfulEventCount: z.number().int().nonnegative().max(20),
matchedEventCount: z.number().int().nonnegative().max(20),
eventHitCount: z.number().int().nonnegative(),
signalLift: z.number().finite(),
}).strict();
export const vedAstroPostValidationSchema = z.object({
contractVersion: z.literal("vedastro-post-validation-v1"),
provider: z.literal("vedastro_official"),
status: z.enum(["pass", "blocked", "not_validated"]),
providerStatus: nonblank(80),
blockers: z.array(nonblank(120)).max(20),
primaryCandidateTime: clockTimeSchema.nullable(),
runnerUpCandidateTime: clockTimeSchema.nullable(),
eligibleEventCount: z.number().int().nonnegative().max(100),
selectedEventCount: z.number().int().nonnegative().max(20),
unsupportedEventCount: z.number().int().nonnegative().max(100),
candidateMetrics: z.array(vedAstroCandidateMetricSchema).max(2),
minuteSensitiveValidation: z.object({
comparisonReady: z.boolean(),
discriminated: z.boolean(),
discriminatedLayers: z.array(nonblank(80)).max(10),
}).strict(),
validationHash: hash,
validatedAt: z.string().datetime({ offset: true }),
canConfirmExactMinute: z.literal(false),
}).strict();
export type VedAstroPostValidation = z.infer<typeof vedAstroPostValidationSchema>;
export const diagnosticsSummarySchema = z.object({
id: uuid,
caseId: uuid,
@@ -203,6 +235,7 @@ export const diagnosticsSummarySchema = z.object({
mostDiscriminatingLayers: z.array(nonblank(80)).max(40),
eventDateSensitivity: z.array(eventDateSensitivitySchema).max(100),
candidateSplits: z.array(candidateSplitSchema).max(20),
externalValidation: vedAstroPostValidationSchema.optional(),
calculationHash: hash,
createdAt: z.string().datetime({ offset: true }),
}).strict();
@@ -17,18 +17,51 @@ import { recordRectificationAgentTelemetry } from "./telemetry.ts";
import {
candidateFeatureSnapshotSchema,
diagnosticsSummarySchema,
vedAstroPostValidationSchema,
validateRectificationDecision,
type AgentRun,
type CandidateFeatureSnapshot,
type DiagnosticsSummary,
type StoredPublicMessage,
type ValidatedDecision,
type VedAstroPostValidation,
} from "./contracts.ts";
function hash(value: unknown): string {
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
}
function vedAstroCandidateTimes(snapshot: CandidateSnapshot): readonly [string, string] | null {
const primary = snapshot.clusters[0]?.representativeTime;
if (!primary) return null;
const runnerUp = snapshot.clusters[1]?.representativeTime
?? [...snapshot.candidates].sort((left, right) => right.score - left.score).find((candidate) => candidate.time !== primary)?.time;
return runnerUp && runnerUp !== primary ? [primary, runnerUp] : null;
}
function blockedVedAstroValidation(now: Date, blocker: string, candidateTimes: readonly [string, string] | null): VedAstroPostValidation {
const safe = {
contractVersion: "vedastro-post-validation-v1" as const,
provider: "vedastro_official" as const,
status: "blocked" as const,
providerStatus: "unavailable",
blockers: [blocker],
primaryCandidateTime: candidateTimes?.[0] ?? null,
runnerUpCandidateTime: candidateTimes?.[1] ?? null,
eligibleEventCount: 0,
selectedEventCount: 0,
unsupportedEventCount: 0,
candidateMetrics: [],
minuteSensitiveValidation: { comparisonReady: false, discriminated: false, discriminatedLayers: [] },
canConfirmExactMinute: false as const,
};
return vedAstroPostValidationSchema.parse({
...safe,
validationHash: hash(safe),
validatedAt: now.toISOString(),
});
}
const analysisPhaseLabels = {
extracting_evidence: "整理用户经历",
scoring_candidates: "扫描候选分钟",
@@ -240,6 +273,50 @@ export async function processRectificationAgentTurn(input: Readonly<{
});
}
if (snapshot?.canAcceptRange && diagnostics && claimed.case.deploymentMode === "v5_agent") {
const candidateTimes = vedAstroCandidateTimes(snapshot);
const validationStarted = Date.now();
let externalValidation: VedAstroPostValidation;
let outcome: "succeeded" | "failed" | "rejected";
if (!candidateTimes) {
externalValidation = blockedVedAstroValidation(now, "vedastro_runner_up_candidate_missing", null);
outcome = "rejected";
} else if (!input.engine.validateWithVedAstro) {
externalValidation = blockedVedAstroValidation(now, "vedastro_validator_unavailable", candidateTimes);
outcome = "failed";
} else {
try {
externalValidation = await input.engine.validateWithVedAstro({
calculationSpec: claimed.case.calculationSpec,
events: scoreable,
candidateTimes,
});
outcome = externalValidation.status === "pass" ? "succeeded" : "rejected";
} catch {
externalValidation = blockedVedAstroValidation(now, "vedastro_validation_failed", candidateTimes);
outcome = "failed";
}
}
diagnostics = diagnosticsSummarySchema.parse({ ...diagnostics, externalValidation });
analysisToolCalls.push({
category: "diagnostic",
label: "VedAstro 事后校验",
outcome,
durationMs: Date.now() - validationStarted,
});
if (externalValidation.status !== "pass") {
snapshot = {
...snapshot,
canAcceptRange: false,
gateReasons: [...new Set([
...snapshot.gateReasons,
"vedastro_validation_not_passed",
...externalValidation.blockers,
])].slice(0, 20),
};
}
}
const safeDiagnostics = diagnostics ?? diagnosticsSummarySchema.parse({
id: randomUUID(),
caseId: claimed.case.id,
@@ -1,6 +1,8 @@
import { createHash } from "node:crypto";
import { z } from "zod";
import type { CalculationSpec, CandidateMinute, LifeEventRevision } from "./contracts.ts";
import { rectificationV4AlgorithmVersion } from "./contracts.ts";
import { vedAstroPostValidationSchema, type VedAstroPostValidation } from "../rectification-agent/contracts.ts";
const uuid = z.string().uuid();
const hash = z.string().regex(/^[a-f0-9]{64}$/);
@@ -45,6 +47,34 @@ const featureSchema = z.object({
fingerprints: z.record(z.string(), z.string()),
}).passthrough()),
}).passthrough();
const vedAstroResponseSchema = z.object({
status: z.enum(["pass", "fail"]),
passed: z.boolean(),
can_confirm_exact_minute: z.literal(false),
candidate_times: z.object({ primary: z.string(), runner_up: z.string() }).strict(),
blockers: z.array(z.string()),
minute_sensitive_validation: z.object({
comparison_ready: z.boolean(),
discriminated: z.boolean(),
discriminated_layers: z.array(z.string()),
}).passthrough(),
event_validation: z.object({
eligible_event_count: z.number().int().nonnegative(),
supported_event_count: z.number().int().nonnegative(),
unsupported_events: z.array(z.unknown()),
candidates: z.array(z.object({
role: z.enum(["primary", "runner_up"]),
metric: z.object({
requested_event_count: z.number().int().nonnegative(),
successful_event_count: z.number().int().nonnegative(),
matched_event_count: z.number().int().nonnegative(),
event_hit_count: z.number().int().nonnegative(),
signal_lift: z.number().finite(),
}).strict(),
}).passthrough()).max(2),
}).passthrough(),
}).passthrough();
const responseSchema = z.object({
result_id: uuid,
algorithm_version: z.literal(rectificationV4AlgorithmVersion),
@@ -79,8 +109,71 @@ export type CandidateEngineResult = Readonly<{
missingLayers: readonly string[];
}>;
function rectificationRequestBody(calculationSpec: CalculationSpec, events: readonly LifeEventRevision[]) {
return {
birth_date: calculationSpec.birthDate,
start_time: calculationSpec.candidateRange.start,
end_time: calculationSpec.candidateRange.end,
lat: calculationSpec.latitude,
lon: calculationSpec.longitude,
tz: calculationSpec.timezoneOffsetHours,
...(Object.hasOwn(calculationSpec, "birthTimeSource") ? { birth_time_source: calculationSpec.birthTimeSource } : {}),
...(Object.hasOwn(calculationSpec, "timezoneId") ? { timezone_id: calculationSpec.timezoneId } : {}),
...(Object.hasOwn(calculationSpec, "timezoneSource") ? { timezone_source: calculationSpec.timezoneSource } : {}),
...(Object.hasOwn(calculationSpec, "localTimeStatus") ? { local_time_status: calculationSpec.localTimeStatus } : {}),
events: events.map((event) => ({
id: event.eventId,
domain: event.domain,
event_kind: event.eventKind,
date_start: event.dateRange.start,
date_end: event.dateRange.end,
precision: event.dateRange.precision,
summary: event.summary,
...(Object.hasOwn(event, "dateSource") ? { date_source: event.dateSource } : {}),
...(Object.hasOwn(event, "dateReliability") ? { date_reliability: event.dateReliability } : {}),
...(Object.hasOwn(event, "dateCorroboration") ? { date_corroboration: event.dateCorroboration } : {}),
...(Object.hasOwn(event, "dateConflictStatus") ? { date_conflict_status: event.dateConflictStatus } : {}),
})),
};
}
function projectVedAstroValidation(payload: z.infer<typeof vedAstroResponseSchema>): VedAstroPostValidation {
const safe = {
contractVersion: "vedastro-post-validation-v1" as const,
provider: "vedastro_official" as const,
status: payload.passed && payload.status === "pass" ? "pass" as const : "blocked" as const,
providerStatus: payload.status,
blockers: payload.blockers,
primaryCandidateTime: payload.candidate_times.primary,
runnerUpCandidateTime: payload.candidate_times.runner_up,
eligibleEventCount: payload.event_validation.eligible_event_count,
selectedEventCount: payload.event_validation.supported_event_count,
unsupportedEventCount: payload.event_validation.unsupported_events.length,
candidateMetrics: payload.event_validation.candidates.map((candidate) => ({
role: candidate.role,
requestedEventCount: candidate.metric.requested_event_count,
successfulEventCount: candidate.metric.successful_event_count,
matchedEventCount: candidate.metric.matched_event_count,
eventHitCount: candidate.metric.event_hit_count,
signalLift: candidate.metric.signal_lift,
})),
minuteSensitiveValidation: {
comparisonReady: payload.minute_sensitive_validation.comparison_ready,
discriminated: payload.minute_sensitive_validation.discriminated,
discriminatedLayers: payload.minute_sensitive_validation.discriminated_layers,
},
canConfirmExactMinute: false as const,
};
return vedAstroPostValidationSchema.parse({
...safe,
validationHash: createHash("sha256").update(JSON.stringify(safe)).digest("hex"),
validatedAt: new Date().toISOString(),
});
}
export interface RectificationV4CandidateEngine {
score(input: { readonly calculationSpec: CalculationSpec; readonly events: readonly LifeEventRevision[] }): Promise<CandidateEngineResult>;
validateWithVedAstro?(input: { readonly calculationSpec: CalculationSpec; readonly events: readonly LifeEventRevision[]; readonly candidateTimes: readonly [string, string] }): Promise<VedAstroPostValidation>;
}
export function createRectificationV4CandidateEngine(options: { readonly apiBase: string; readonly fetchImpl?: typeof fetch }): RectificationV4CandidateEngine {
@@ -88,22 +181,7 @@ export function createRectificationV4CandidateEngine(options: { readonly apiBase
return { async score({ calculationSpec, events }) {
const response = await fetchImpl(`${options.apiBase}/api/rectification/v5/score`, {
method: "POST", headers: { "content-type": "application/json" }, signal: AbortSignal.timeout(5 * 60_000),
body: JSON.stringify({
birth_date: calculationSpec.birthDate, start_time: calculationSpec.candidateRange.start, end_time: calculationSpec.candidateRange.end,
lat: calculationSpec.latitude, lon: calculationSpec.longitude, tz: calculationSpec.timezoneOffsetHours,
...(Object.hasOwn(calculationSpec, "birthTimeSource") ? { birth_time_source: calculationSpec.birthTimeSource } : {}),
...(Object.hasOwn(calculationSpec, "timezoneId") ? { timezone_id: calculationSpec.timezoneId } : {}),
...(Object.hasOwn(calculationSpec, "timezoneSource") ? { timezone_source: calculationSpec.timezoneSource } : {}),
...(Object.hasOwn(calculationSpec, "localTimeStatus") ? { local_time_status: calculationSpec.localTimeStatus } : {}),
events: events.map((event) => ({
id: event.eventId, domain: event.domain, event_kind: event.eventKind,
date_start: event.dateRange.start, date_end: event.dateRange.end, precision: event.dateRange.precision, summary: event.summary,
...(Object.hasOwn(event, "dateSource") ? { date_source: event.dateSource } : {}),
...(Object.hasOwn(event, "dateReliability") ? { date_reliability: event.dateReliability } : {}),
...(Object.hasOwn(event, "dateCorroboration") ? { date_corroboration: event.dateCorroboration } : {}),
...(Object.hasOwn(event, "dateConflictStatus") ? { date_conflict_status: event.dateConflictStatus } : {}),
})),
}),
body: JSON.stringify(rectificationRequestBody(calculationSpec, events)),
});
const payload: unknown = await response.json();
if (!response.ok) throw new Error(`rectification_v5_engine_${response.status}`);
@@ -123,5 +201,15 @@ export function createRectificationV4CandidateEngine(options: { readonly apiBase
contributionMatrix: parsed.event_contribution_matrix,
missingLayers: parsed.missing_layers,
};
}, async validateWithVedAstro({ calculationSpec, events, candidateTimes }) {
const response = await fetchImpl(`${options.apiBase}/api/rectification/v5/vedastro-validate`, {
method: "POST",
headers: { "content-type": "application/json" },
signal: AbortSignal.timeout(90_000),
body: JSON.stringify({ ...rectificationRequestBody(calculationSpec, events), candidate_times: candidateTimes }),
});
const payload: unknown = await response.json();
if (!response.ok) throw new Error(`rectification_v5_vedastro_${response.status}`);
return projectVedAstroValidation(vedAstroResponseSchema.parse(payload));
}};
}