315 lines
12 KiB
TypeScript
315 lines
12 KiB
TypeScript
import type {
|
|
DashaAgreement,
|
|
EventDashaLedgerRow,
|
|
LagnaContrast,
|
|
NakshatraBoundary,
|
|
OosBlindPrompt,
|
|
PrecisionStage,
|
|
WindowScanTransition,
|
|
} from "./rectification-agentic/v9/refinement-packet";
|
|
import {
|
|
parseWindowScanTransitions,
|
|
refinementFromDecisionReceipt,
|
|
} from "./rectification-agentic/v9/refinement-packet";
|
|
import { parseWindowScan } from "./rectification-agentic/v9/varga-observations";
|
|
import type { TechniqueAuditRow } from "./consultation-agent-events";
|
|
import { normalizeTechniqueAuditRows } from "./consultation-technique-audit";
|
|
import {
|
|
buildConfirmationGate,
|
|
type ConfirmationGate,
|
|
} from "./rectification-agentic/v9/confirmation-gate";
|
|
import { MIN_SEPARATION_LEAD } from "./rectification-agentic/core/candidate-separation";
|
|
|
|
export type RectificationCandidate = Readonly<{
|
|
candidateId: string;
|
|
rank: number;
|
|
time: string;
|
|
relativeSupport: number;
|
|
tiedMinuteCount: number;
|
|
}>;
|
|
|
|
export type RectificationHouseRow = Readonly<{
|
|
house: number;
|
|
sign: string;
|
|
occupants: readonly string[];
|
|
}>;
|
|
|
|
export type RectificationHouseTable = Readonly<{
|
|
time: string;
|
|
lagna: string;
|
|
houses: readonly RectificationHouseRow[];
|
|
}>;
|
|
|
|
export type RectificationNatalRecast = Readonly<{
|
|
time: string;
|
|
lagna: string;
|
|
user_meaning: string;
|
|
unique_minute_claim: false;
|
|
confirmation_allowed: false;
|
|
}>;
|
|
|
|
export type RectificationCandidateResult = Readonly<{
|
|
resultId: string;
|
|
candidates: readonly RectificationCandidate[];
|
|
overallConfidence: "low" | "medium" | "high";
|
|
selectionAllowed: boolean;
|
|
canAdopt: boolean;
|
|
confirmationAllowed: boolean;
|
|
representativeTime: string | null;
|
|
selectedTime: string | null;
|
|
selectionKind: string | null;
|
|
houseTable: RectificationHouseTable | null;
|
|
houseTablesByTime: Readonly<Record<string, RectificationHouseTable>>;
|
|
natalRecast: RectificationNatalRecast | null;
|
|
techniqueAudit: readonly TechniqueAuditRow[];
|
|
windowTransitions: readonly WindowScanTransition[];
|
|
eventDashaLedger: readonly EventDashaLedgerRow[];
|
|
dashaAgreement: DashaAgreement | null;
|
|
lagnaContrast: LagnaContrast | null;
|
|
nakshatraBoundary: NakshatraBoundary | null;
|
|
precisionStage: PrecisionStage | null;
|
|
oosBlindPrompts: readonly OosBlindPrompt[];
|
|
confirmationGate: ConfirmationGate;
|
|
validated: boolean;
|
|
completionStatus: "provisional_range_user_stopped" | "validated_range" | "exact_minute_confirmed" | null;
|
|
}>;
|
|
|
|
function record(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
|
}
|
|
|
|
function time(value: unknown): string | null {
|
|
if (typeof value !== "string") return null;
|
|
const normalized = value.slice(0, 5);
|
|
return /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(normalized) ? normalized : null;
|
|
}
|
|
|
|
function text(value: unknown): string | null {
|
|
return typeof value === "string" ? value : null;
|
|
}
|
|
|
|
function finiteNumber(value: unknown): number | null {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
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;
|
|
const deniedHouseTable = /lon|latitude|longitude|degree|score|fingerprint/i;
|
|
|
|
export function parseRectificationHouseTable(value: unknown): RectificationHouseTable | null {
|
|
const row = record(value);
|
|
const tableTime = time(row?.time);
|
|
const lagna = text(row?.lagna)?.trim() ?? "";
|
|
if (!row || !tableTime || !lagna || !Array.isArray(row.houses) || row.houses.length !== 12) return null;
|
|
const houses: RectificationHouseRow[] = [];
|
|
for (let index = 0; index < 12; index += 1) {
|
|
const house = record(row.houses[index]);
|
|
const houseNumber = finiteNumber(house?.house);
|
|
const sign = text(house?.sign)?.trim() ?? "";
|
|
if (
|
|
!house
|
|
|| houseNumber !== index + 1
|
|
|| !sign
|
|
|| deniedHouseTable.test(sign)
|
|
|| !Array.isArray(house.occupants)
|
|
) return null;
|
|
const occupants: string[] = [];
|
|
for (const occupant of house.occupants) {
|
|
const label = text(occupant)?.trim() ?? "";
|
|
if (!label) continue;
|
|
if (deniedHouseTable.test(label)) return null;
|
|
occupants.push(label);
|
|
}
|
|
houses.push({ house: houseNumber, sign, occupants });
|
|
}
|
|
return { time: tableTime, lagna, houses };
|
|
}
|
|
|
|
function houseTablesByTimeFromSnapshot(
|
|
snapshot: Record<string, unknown>,
|
|
receipt: Record<string, unknown> | null,
|
|
): Record<string, RectificationHouseTable> {
|
|
const raw = record(snapshot.houseTablesByTime)
|
|
?? record(snapshot.house_tables_by_time)
|
|
?? record(receipt?.house_tables_by_time);
|
|
const tables: Record<string, RectificationHouseTable> = {};
|
|
if (!raw) return tables;
|
|
for (const [key, value] of Object.entries(raw)) {
|
|
const clock = time(key);
|
|
const table = parseRectificationHouseTable(value);
|
|
if (!clock || !table) continue;
|
|
tables[clock] = table;
|
|
}
|
|
return tables;
|
|
}
|
|
|
|
function natalRecastFromSnapshot(
|
|
snapshot: Record<string, unknown>,
|
|
receipt: Record<string, unknown> | null,
|
|
table: RectificationHouseTable | null,
|
|
wantedMinute: string | null,
|
|
): RectificationNatalRecast | null {
|
|
const raw = record(snapshot.natalRecast) ?? record(snapshot.natal_recast) ?? record(receipt?.natal_recast);
|
|
const recastTime = table?.time ?? time(raw?.time);
|
|
const lagna = table?.lagna || (typeof raw?.lagna === "string" ? raw.lagna.trim() : "") || "";
|
|
if (!recastTime || !lagna || deniedHouseTable.test(lagna)) return null;
|
|
if (wantedMinute && recastTime !== wantedMinute) return null;
|
|
const meaning = natalRecastMeaning({ time: recastTime, lagna })
|
|
?? (typeof raw?.user_meaning === "string" && raw.user_meaning.trim() && !deniedHouseTable.test(raw.user_meaning)
|
|
? raw.user_meaning.trim().slice(0, 220)
|
|
: null);
|
|
if (!meaning) return null;
|
|
return {
|
|
time: recastTime,
|
|
lagna,
|
|
user_meaning: meaning,
|
|
unique_minute_claim: false,
|
|
confirmation_allowed: false,
|
|
};
|
|
}
|
|
|
|
export function natalRecastMeaning(table: Pick<RectificationHouseTable, "time" | "lagna"> | null): string | null {
|
|
if (!table) return null;
|
|
return `本命宫位已按 ${table.time} 重算(上升 ${table.lagna})。下面是本轮实际执行的技法,不能当作唯一分钟确认。`;
|
|
}
|
|
|
|
export function houseTableAlignedToMinute(
|
|
wanted: string | null,
|
|
tablesByTime: Readonly<Record<string, RectificationHouseTable>>,
|
|
fallback: RectificationHouseTable | null,
|
|
): RectificationHouseTable | null {
|
|
if (wanted && tablesByTime[wanted]?.time === wanted) return tablesByTime[wanted];
|
|
if (fallback && wanted && fallback.time === wanted) return fallback;
|
|
if (!wanted) return fallback;
|
|
return null;
|
|
}
|
|
|
|
export function publicChartForMinute(
|
|
wanted: string | null,
|
|
snapshot: Record<string, unknown>,
|
|
receipt: Record<string, unknown> | null,
|
|
): {
|
|
houseTable: RectificationHouseTable | null;
|
|
natalRecast: RectificationNatalRecast | null;
|
|
} {
|
|
const tablesByTime = houseTablesByTimeFromSnapshot(snapshot, receipt);
|
|
const fallback = parseRectificationHouseTable(snapshot.houseTable)
|
|
?? parseRectificationHouseTable(snapshot.house_table)
|
|
?? parseRectificationHouseTable(receipt?.house_table);
|
|
const houseTable = houseTableAlignedToMinute(wanted, tablesByTime, fallback);
|
|
return {
|
|
houseTable,
|
|
natalRecast: natalRecastFromSnapshot(snapshot, receipt, houseTable, wanted),
|
|
};
|
|
}
|
|
|
|
export function workingRectificationHouseTable(
|
|
result: Pick<RectificationCandidateResult, "selectedTime" | "representativeTime" | "houseTable" | "houseTablesByTime">,
|
|
): RectificationHouseTable | null {
|
|
const wanted = result.selectedTime ?? result.representativeTime;
|
|
return houseTableAlignedToMinute(wanted, result.houseTablesByTime, result.houseTable);
|
|
}
|
|
|
|
function techniqueAuditFromSnapshot(
|
|
snapshot: Record<string, unknown>,
|
|
receipt: Record<string, unknown> | null,
|
|
): readonly TechniqueAuditRow[] {
|
|
return normalizeTechniqueAuditRows(snapshot.techniqueAudit)
|
|
?? normalizeTechniqueAuditRows(snapshot.technique_audit_table)
|
|
?? normalizeTechniqueAuditRows(receipt?.technique_audit_table)
|
|
?? [];
|
|
}
|
|
|
|
function receiptFromSnapshot(snapshot: Record<string, unknown>): Record<string, unknown> | null {
|
|
return record(snapshot.decisionReceipt) ?? record(snapshot.decision_receipt);
|
|
}
|
|
|
|
export function parseRectificationCandidateResult(value: unknown): RectificationCandidateResult | null {
|
|
const snapshot = record(value);
|
|
if (!snapshot || typeof snapshot.resultId !== "string") return null;
|
|
if (!Array.isArray(snapshot.candidates) || snapshot.candidates.length === 0) return null;
|
|
|
|
const candidates: RectificationCandidate[] = [];
|
|
for (const value of snapshot.candidates) {
|
|
const candidate = record(value);
|
|
const candidateId = candidate?.candidateId;
|
|
const candidateTime = time(candidate?.time);
|
|
const rank = finiteNumber(candidate?.rank);
|
|
const relativeSupport = finiteNumber(candidate?.relativeSupport);
|
|
const tiedMinuteCount = finiteNumber(candidate?.tiedMinuteCount);
|
|
if (
|
|
!candidate || typeof candidateId !== "string" || !uuidPattern.test(candidateId)
|
|
|| !candidateTime
|
|
|| rank === null || !Number.isInteger(rank) || rank < 1
|
|
|| relativeSupport === null || !Number.isInteger(relativeSupport) || relativeSupport < 0 || relativeSupport > 100
|
|
|| tiedMinuteCount === null || !Number.isInteger(tiedMinuteCount) || tiedMinuteCount < 1
|
|
) return null;
|
|
candidates.push({ candidateId, rank, time: candidateTime, relativeSupport, tiedMinuteCount });
|
|
}
|
|
|
|
const receipt = receiptFromSnapshot(snapshot);
|
|
const refinement = refinementFromDecisionReceipt(receipt);
|
|
const windowScan = parseWindowScan(receipt?.window_scan) ?? parseWindowScan(snapshot.window_scan);
|
|
const selectedTime = time(snapshot.selectedTime);
|
|
const representativeTime = time(snapshot.representativeTime);
|
|
const houseTablesByTime = houseTablesByTimeFromSnapshot(snapshot, receipt);
|
|
const fallbackHouseTable = parseRectificationHouseTable(snapshot.houseTable)
|
|
?? parseRectificationHouseTable(snapshot.house_table)
|
|
?? parseRectificationHouseTable(receipt?.house_table);
|
|
const houseTable = houseTableAlignedToMinute(
|
|
selectedTime ?? representativeTime,
|
|
houseTablesByTime,
|
|
fallbackHouseTable,
|
|
);
|
|
|
|
return {
|
|
resultId: snapshot.resultId,
|
|
candidates,
|
|
overallConfidence: snapshot.overallConfidence === "high" || snapshot.overallConfidence === "medium"
|
|
? snapshot.overallConfidence
|
|
: "low",
|
|
selectionAllowed: snapshot.selectionAllowed === true || snapshot.selection_allowed === true,
|
|
canAdopt: snapshot.canAdopt === true || snapshot.can_adopt === true,
|
|
confirmationAllowed: snapshot.confirmationAllowed === true,
|
|
representativeTime,
|
|
selectedTime,
|
|
selectionKind: text(snapshot.selectionKind),
|
|
houseTable,
|
|
houseTablesByTime,
|
|
natalRecast: natalRecastFromSnapshot(snapshot, receipt, houseTable, selectedTime ?? representativeTime),
|
|
techniqueAudit: techniqueAuditFromSnapshot(snapshot, receipt),
|
|
windowTransitions: windowScan?.transitions ?? parseWindowScanTransitions(snapshot.window_scan),
|
|
eventDashaLedger: refinement.event_dasha_ledger,
|
|
dashaAgreement: refinement.dasha_agreement,
|
|
lagnaContrast: refinement.lagna_contrast,
|
|
nakshatraBoundary: refinement.nakshatra_boundary,
|
|
precisionStage: refinement.precision_stage,
|
|
oosBlindPrompts: refinement.oos_blind_prompts,
|
|
confirmationGate: buildConfirmationGate({
|
|
engineConfirmationAllowed: snapshot.confirmationAllowed === true,
|
|
candidates,
|
|
decisionReceipt: receipt,
|
|
}),
|
|
validated: snapshot.validated === true,
|
|
completionStatus: snapshot.completionStatus === "provisional_range_user_stopped"
|
|
|| snapshot.completionStatus === "validated_range"
|
|
|| snapshot.completionStatus === "exact_minute_confirmed"
|
|
|| snapshot.completion_status === "provisional_range_user_stopped"
|
|
|| snapshot.completion_status === "validated_range"
|
|
|| snapshot.completion_status === "exact_minute_confirmed"
|
|
? (snapshot.completionStatus ?? snapshot.completion_status) as RectificationCandidateResult["completionStatus"]
|
|
: null,
|
|
};
|
|
}
|
|
|
|
export function isRecommendedRectificationCandidate(
|
|
result: RectificationCandidateResult,
|
|
candidate: RectificationCandidate,
|
|
): boolean {
|
|
if (result.selectedTime || !result.selectionAllowed || result.representativeTime !== candidate.time) {
|
|
return false;
|
|
}
|
|
const ranked = [...result.candidates].sort((left, right) => right.relativeSupport - left.relativeSupport);
|
|
const lead = (ranked[0]?.relativeSupport ?? 0) - (ranked[1]?.relativeSupport ?? 0);
|
|
return lead >= MIN_SEPARATION_LEAD;
|
|
}
|