Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/engine-client.ts
T
2026-08-15 00:56:06 +08:00

463 lines
18 KiB
TypeScript

/**
* V9 deterministic engine client (Python, server-side only).
*
* The Python service owns Event Contract v2 normalization, candidate ranking,
* decision policy and the execution ledger. This client validates and projects
* that server contract; it never reconstructs rank, support, ties, confidence,
* permissions or executed methods from raw scores or event domains.
*/
import {
isBackgroundEvidenceKind,
isEvidenceDomain,
isEvidenceKind,
type EvidenceKind,
} from "./evidence-model";
import {
isPublicRectificationMethod,
type PublicRectificationMethod,
} from "./public-receipt";
export class RectificationEngineError extends Error {
readonly code: string;
constructor(code: string, message: string) {
super(message);
this.name = "RectificationEngineError";
this.code = code;
}
}
export type V9EngineEvent = Readonly<{
id: string;
domain: string;
event_kind: string;
date_start: string;
date_end: string;
precision: "day" | "month" | "quarter" | "year" | "range";
summary: string;
date_source?: string | null;
date_reliability?: string | null;
date_corroboration?: string | null;
date_conflict_status?: string | null;
source_turn_id?: string | null;
subject?: string | null;
}>;
export type V9EngineCandidate = Readonly<{
candidateId: string;
time: string;
rank: number;
relativeSupport: number;
tiedMinuteCount: number;
}>;
export type V9DecisionReceipt = Readonly<Record<string, unknown>>;
export type V9ExecutionLedger = readonly Readonly<Record<string, unknown>>[];
export type V9EngineScoreResult = Readonly<{
engineResultId: string;
algorithmVersion: string;
eventContractVersion: string;
policyVersion: string;
candidateRange: { start_time: string; end_time: string };
candidates: readonly V9EngineCandidate[];
overallConfidence: "low" | "medium" | "high";
marginPercent: number | null;
selectionAllowed: boolean;
confirmationAllowed: boolean;
representativeCandidateId: string | null;
representativeTime: string | null;
decisionReceipt: V9DecisionReceipt;
executionLedger: V9ExecutionLedger;
executedMethods: readonly PublicRectificationMethod[];
}>;
export type V9EngineDiagnostics = Readonly<{
algorithmVersion: string;
engineResultId: string;
eventContractVersion: string;
policyVersion: string;
diagnostics: Readonly<Record<string, unknown>>;
missingLayers: readonly string[];
canConfirmExactMinute: boolean;
decisionReceipt: V9DecisionReceipt;
executionLedger: V9ExecutionLedger;
executedMethods: readonly PublicRectificationMethod[];
}>;
const EVENT_CONTRACT_VERSION = "rectification-event-contract-v2";
const RECEIPT_VERSION = "candidate-decision-receipt-v2";
const EXECUTION_LEDGER_VERSION = "rectification-execution-ledger-v2";
const timePattern = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function record(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function engineNumber(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function clockMinute(value: string): number {
const [hour = 0, minute = 0] = value.split(":").map(Number);
return hour * 60 + minute;
}
function timeInRange(time: string, range: { start_time: string; end_time: string }): boolean {
const value = clockMinute(time);
const start = clockMinute(range.start_time);
const end = clockMinute(range.end_time);
return end >= start ? value >= start && value <= end : value >= start || value <= end;
}
/** Map a V9 evidence date precision to the Event Contract v2 vocabulary. */
export function enginePrecision(precision: string): V9EngineEvent["precision"] {
if (precision === "day" || precision === "month" || precision === "quarter" || precision === "range") {
return precision;
}
return "year";
}
export function toEngineEvents(
evidence: readonly Readonly<{
id: string;
eventKind: string;
domain: string;
occurredFrom: string | null;
occurredTo: string | null;
datePrecision: string;
summary: string;
dateSource?: string | null;
dateReliability?: string | null;
dateCorroboration?: string | null;
dateConflictStatus?: string | null;
sourceTurnId?: string | null;
subject?: string | null;
}>[],
): V9EngineEvent[] {
return evidence.flatMap((item): V9EngineEvent[] => {
if (!isEvidenceKind(item.eventKind) || !isEvidenceDomain(item.domain)) return [];
if (isBackgroundEvidenceKind(item.eventKind as EvidenceKind)) return [];
const start = item.occurredFrom ?? item.occurredTo;
const end = item.occurredTo ?? item.occurredFrom;
if (!start) return [];
return [{
id: item.id,
domain: item.domain,
event_kind: item.eventKind,
date_start: start.slice(0, 10),
date_end: (end ?? start).slice(0, 10),
precision: enginePrecision(item.datePrecision),
summary: item.summary,
date_source: item.dateSource ?? null,
date_reliability: item.dateReliability ?? null,
date_corroboration: item.dateCorroboration ?? null,
date_conflict_status: item.dateConflictStatus ?? null,
source_turn_id: item.sourceTurnId ?? null,
subject: item.subject ?? null,
}];
});
}
function engineBase(): string {
return process.env.JYOTISH_API_BASE?.trim() || "http://127.0.0.1:5200";
}
async function postEngine(path: string, body: unknown, timeoutMs = 60_000): Promise<Record<string, unknown>> {
const response = await fetch(`${engineBase()}${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeoutMs),
});
const data = await response.json().catch(() => null);
if (!response.ok) {
const message = data && typeof data === "object"
? String((data as Record<string, unknown>).error ?? (data as Record<string, unknown>).message ?? `Jyotish API ${response.status}`)
: `Jyotish API ${response.status}`;
throw new RectificationEngineError("engine_request_failed", message);
}
if (!data || typeof data !== "object") {
throw new RectificationEngineError("engine_invalid_response", "Jyotish API returned an invalid response");
}
return data as Record<string, unknown>;
}
function readCandidates(
value: unknown,
range: { start_time: string; end_time: string },
): V9EngineCandidate[] {
if (!Array.isArray(value) || value.length === 0) {
throw new RectificationEngineError("engine_invalid_candidate_decisions", "engine candidate decisions are missing");
}
const seenIds = new Set<string>();
const seenTimes = new Set<string>();
const candidates: V9EngineCandidate[] = [];
for (const item of value) {
const row = record(item);
const candidateId = row?.candidate_id;
const time = row?.time;
const rank = row?.rank;
const relativeSupport = row?.relative_support;
const tiedMinuteCount = row?.tied_minute_count;
if (
typeof candidateId !== "string" || !uuidPattern.test(candidateId)
|| typeof time !== "string" || !timePattern.test(time) || !timeInRange(time, range)
|| typeof rank !== "number" || !Number.isInteger(rank) || rank < 1
|| typeof relativeSupport !== "number" || !Number.isInteger(relativeSupport) || relativeSupport < 0 || relativeSupport > 100
|| typeof tiedMinuteCount !== "number" || !Number.isInteger(tiedMinuteCount) || tiedMinuteCount < 1
|| seenIds.has(candidateId) || seenTimes.has(time)
) {
throw new RectificationEngineError("engine_invalid_candidate_decisions", "engine candidate decisions are invalid");
}
seenIds.add(candidateId);
seenTimes.add(time);
candidates.push({ candidateId, time, rank, relativeSupport, tiedMinuteCount });
}
return candidates;
}
type ParsedReceipt = Readonly<{
raw: V9DecisionReceipt;
eventContractVersion: string;
policyVersion: string;
selectionAllowed: boolean;
confirmationAllowed: boolean;
representativeCandidateId: string | null;
representativeTime: string | null;
overallConfidence: "low" | "medium" | "high";
marginPercent: number | null;
}>;
function invalidReceipt(): never {
throw new RectificationEngineError("engine_invalid_v2_receipt", "engine decision receipt is missing or invalid");
}
function readDecisionReceipt(value: unknown, candidates: readonly V9EngineCandidate[]): ParsedReceipt {
const row = record(value);
if (!row) return invalidReceipt();
const representativeCandidateId = row.representative_candidate_id;
const representativeTime = row.representative_time;
const confidence = row.overall_confidence;
const marginPercent = row.margin_percent === null || row.margin_percent === undefined
? null
: engineNumber(row.margin_percent);
if (
row.receipt_version !== RECEIPT_VERSION
|| row.contract_version !== "v2"
|| row.event_contract_version !== EVENT_CONTRACT_VERSION
|| typeof row.policy_version !== "string" || !row.policy_version
|| typeof row.decision_policy_version !== "string" || row.decision_policy_version !== row.policy_version
|| typeof row.display_allowed !== "boolean"
|| typeof row.selection_allowed !== "boolean"
|| typeof row.acceptance_allowed !== "boolean"
|| typeof row.confirmation_allowed !== "boolean"
|| typeof row.accept_allowed !== "boolean"
|| typeof row.confirm_allowed !== "boolean"
|| row.selection_allowed !== row.acceptance_allowed
|| row.selection_allowed !== row.accept_allowed
|| row.confirmation_allowed !== row.confirm_allowed
|| (representativeCandidateId !== null && (typeof representativeCandidateId !== "string" || !uuidPattern.test(representativeCandidateId)))
|| (representativeTime !== null && (typeof representativeTime !== "string" || !timePattern.test(representativeTime)))
|| (confidence !== "low" && confidence !== "medium" && confidence !== "high")
|| (row.margin_percent !== null && row.margin_percent !== undefined && marginPercent === null)
) {
return invalidReceipt();
}
const representative = representativeCandidateId === null
? null
: candidates.find((candidate) => candidate.candidateId === representativeCandidateId) ?? null;
if (
(representativeCandidateId === null) !== (representativeTime === null)
|| (representativeCandidateId !== null && (!representative || representative.time !== representativeTime))
|| (row.confirmation_allowed === true && row.selection_allowed !== true)
|| (row.selection_allowed === true && row.display_allowed !== true)
) {
return invalidReceipt();
}
return {
raw: row,
eventContractVersion: EVENT_CONTRACT_VERSION,
policyVersion: row.policy_version,
selectionAllowed: row.selection_allowed,
confirmationAllowed: row.confirmation_allowed,
representativeCandidateId,
representativeTime,
overallConfidence: confidence,
marginPercent,
};
}
function readExecutionLedger(version: unknown, value: unknown): V9ExecutionLedger {
if (version !== EXECUTION_LEDGER_VERSION || !Array.isArray(value) || value.length === 0) {
throw new RectificationEngineError("engine_invalid_execution_ledger", "engine execution ledger is missing or invalid");
}
return value.map((item) => {
const row = record(item);
if (
!row
|| row.ledger_version !== EXECUTION_LEDGER_VERSION
|| typeof row.stage !== "string" || !row.stage
|| (row.status !== "executed" && row.status !== "not_executed" && row.status !== "retained_not_scored")
|| typeof row.source !== "string" || !row.source
) {
throw new RectificationEngineError("engine_invalid_execution_ledger", "engine execution ledger is missing or invalid");
}
return row;
});
}
function publicMethodFromLedger(value: unknown): PublicRectificationMethod | null {
if (isPublicRectificationMethod(value)) return value;
if (typeof value !== "string") return null;
const normalized = value.toLowerCase();
if (normalized.includes("controlled_transit") || normalized.includes("gochara")) return "gochara";
if (normalized.includes("ashtakavarga")) return "ashtakavarga";
if (normalized.includes("shadbala")) return "shadbala";
if (normalized.includes("arudha")) return "arudha-pada";
if (normalized.includes("functional_benefic") || normalized.includes("functional_malefic")) return "functional-benefic-malefic";
if (normalized.includes("vimshottari") || normalized.includes("vimsottari")) return "vimshottari-dasha";
if (normalized.includes("narayana")) return "narayana-dasha";
return null;
}
function executedMethods(ledger: V9ExecutionLedger): PublicRectificationMethod[] {
const methods = new Set<PublicRectificationMethod>();
for (const entry of ledger) {
if (entry.status !== "executed") continue;
const direct = publicMethodFromLedger(entry.method);
if (direct) methods.add(direct);
if (Array.isArray(entry.technique_layers)) {
for (const layer of entry.technique_layers) {
const method = publicMethodFromLedger(layer);
if (method) methods.add(method);
}
}
}
return [...methods];
}
function engineDiagnostics(data: Record<string, unknown>): Readonly<Record<string, unknown>> {
return record(data.diagnostics) ?? {};
}
function engineRequestBody(input: {
baselineBirthSnapshot: Readonly<Record<string, unknown>>;
candidateRange: { start_time: string; end_time: string };
events: readonly V9EngineEvent[];
}): Record<string, unknown> {
const snapshot = input.baselineBirthSnapshot;
const birthDate = String(snapshot.birth_date ?? "");
const lat = engineNumber(snapshot.latitude);
const lon = engineNumber(snapshot.longitude);
const tz = engineNumber(snapshot.timezone_offset);
if (!birthDate || lat === null || lon === null || tz === null) {
throw new RectificationEngineError("engine_profile_incomplete", "server profile snapshot is incomplete");
}
if (input.events.length === 0) {
throw new RectificationEngineError("no_scorable_evidence", "no scorable evidence for the engine");
}
return {
birth_date: birthDate,
start_time: input.candidateRange.start_time,
end_time: input.candidateRange.end_time,
lat,
lon,
tz,
events: input.events,
birth_time_source: snapshot.birth_time_source,
timezone_id: snapshot.timezone_id,
timezone_source: snapshot.timezone_source,
local_time_status: snapshot.local_time_status,
};
}
export async function runV9CandidateScore(input: {
baselineBirthSnapshot: Readonly<Record<string, unknown>>;
candidateRange: { start_time: string; end_time: string };
events: readonly V9EngineEvent[];
}): Promise<V9EngineScoreResult> {
const data = await postEngine("/api/rectification/v5/score", engineRequestBody(input));
const candidates = readCandidates(data.candidate_decisions, input.candidateRange);
const receipt = readDecisionReceipt(data.decision_receipt, candidates);
const ledger = readExecutionLedger(data.execution_ledger_version, data.execution_ledger);
if (
data.event_contract_version !== receipt.eventContractVersion
|| data.decision_policy_version !== receipt.policyVersion
) {
return invalidReceipt();
}
return {
engineResultId: String(data.result_id ?? ""),
algorithmVersion: String(data.algorithm_version ?? ""),
eventContractVersion: receipt.eventContractVersion,
policyVersion: receipt.policyVersion,
candidateRange: input.candidateRange,
candidates,
overallConfidence: receipt.overallConfidence,
marginPercent: receipt.marginPercent,
selectionAllowed: receipt.selectionAllowed,
confirmationAllowed: receipt.confirmationAllowed,
representativeCandidateId: receipt.representativeCandidateId,
representativeTime: receipt.representativeTime,
decisionReceipt: receipt.raw,
executionLedger: ledger,
executedMethods: executedMethods(ledger),
};
}
export async function runV9Diagnostics(input: {
baselineBirthSnapshot: Readonly<Record<string, unknown>>;
candidateRange: { start_time: string; end_time: string };
events: readonly V9EngineEvent[];
}): Promise<V9EngineDiagnostics> {
const data = await postEngine("/api/rectification/v5/diagnostics", engineRequestBody(input));
const candidates = readCandidates(data.candidate_decisions, input.candidateRange);
const receipt = readDecisionReceipt(data.decision_receipt, candidates);
const ledger = readExecutionLedger(data.execution_ledger_version, data.execution_ledger);
if (
data.event_contract_version !== receipt.eventContractVersion
|| data.decision_policy_version !== receipt.policyVersion
) {
return invalidReceipt();
}
const diagnostics = engineDiagnostics(data);
const missingLayers = Array.isArray(data.missing_layers)
? data.missing_layers.filter((item): item is string => typeof item === "string")
: [];
return {
algorithmVersion: String(data.algorithm_version ?? ""),
engineResultId: String(data.result_id ?? ""),
eventContractVersion: receipt.eventContractVersion,
policyVersion: receipt.policyVersion,
diagnostics: {
primary_cluster_retention_rate: diagnostics.primary_cluster_retention_rate,
leave_one_event_out_retention_rate: diagnostics.leave_one_event_out_retention_rate,
leave_one_domain_out_retention_rate: diagnostics.leave_one_domain_out_retention_rate,
date_sensitivity_retention_rate: diagnostics.date_sensitivity_retention_rate,
neighbor_support_minutes: diagnostics.neighbor_support_minutes,
primary_secondary_margin_percent: diagnostics.primary_secondary_margin_percent,
unstable_event_ids: diagnostics.unstable_event_ids,
most_discriminating_layers: diagnostics.most_discriminating_layers,
candidate_splits: diagnostics.candidate_splits,
},
missingLayers,
canConfirmExactMinute: receipt.confirmationAllowed,
decisionReceipt: receipt.raw,
executionLedger: ledger,
executedMethods: executedMethods(ledger),
};
}
export const v9EngineVersion = (): string =>
process.env.RECTIFICATION_ENGINE_VERSION?.trim() || "rectification-v5";