feat: add VedAstro rectification post-validation
This commit is contained in:
@@ -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));
|
||||
}};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user