Dated-choice exhaustion is not convergence. Refresh probes from remaining active candidates, then ask a targeted collect, then deliver. Skill 10.0.24. Co-authored-by: Cursor <cursoragent@cursor.com>
643 lines
23 KiB
TypeScript
643 lines
23 KiB
TypeScript
/**
|
|
* Range-delivery projection: up to three compare columns, ranked by posterior.
|
|
* Traits come only from D9 / D10 type tables and nakshatra_boundary options.
|
|
*/
|
|
|
|
import { rankActive } from "../core/convergence-evaluator.ts";
|
|
import { signFromTransitions as signFromTransitionLookup } from "../core/sign-from-transitions.ts";
|
|
import type {
|
|
ConflictProbe,
|
|
InferenceCandidate,
|
|
InferenceState,
|
|
} from "../core/types.ts";
|
|
import {
|
|
RANGE_DELIVERY_MORE_MINUTES,
|
|
REPRESENTATIVE_MINUTE_DISCLAIMER,
|
|
rangeDeliveryFitLine,
|
|
rangeDeliveryWindowLine,
|
|
sharedTraitLine,
|
|
} from "../user-copy.ts";
|
|
import { previousInferenceFromReceipt } from "./inference-adapter.ts";
|
|
import {
|
|
rangeNarrowHint,
|
|
remainingSplitLayers,
|
|
remainingSplitTimes,
|
|
} from "./collection-question-pool.ts";
|
|
import {
|
|
parseEventDashaLedgerByTime,
|
|
parseProspectiveWindowsByTime,
|
|
refinementFromDecisionReceipt,
|
|
type EventDashaLedgerRow,
|
|
type NakshatraBoundary,
|
|
type ProspectiveWindow,
|
|
} from "./refinement-packet.ts";
|
|
import { parseWindowScan, type WindowScan } from "./varga-observations.ts";
|
|
import {
|
|
D10_TYPE_TABLE,
|
|
D9_TYPE_TABLE,
|
|
signKey,
|
|
} from "./varga-type-tables.ts";
|
|
|
|
const CLOCK = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
|
const UUID = /^[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 COLUMN_LIMIT = 3;
|
|
const MATCH_RANK: Readonly<Record<EventDashaLedgerRow["match"], number>> = {
|
|
none: 0,
|
|
weak: 1,
|
|
medium: 2,
|
|
strong: 3,
|
|
};
|
|
|
|
export type RangeDeliveryFit = Readonly<{
|
|
strong: number;
|
|
medium: number;
|
|
weak: number;
|
|
total: number;
|
|
worst: Readonly<{ year_month: string; domain: string }> | null;
|
|
}>;
|
|
|
|
export type RangeDeliveryColumn = Readonly<{
|
|
time: string;
|
|
candidate_id: string;
|
|
probability_percent: number;
|
|
traits: Readonly<{ d9: string; d10: string; nakshatra: readonly string[] }>;
|
|
fit: RangeDeliveryFit | null;
|
|
windows: readonly ProspectiveWindow[] | null;
|
|
fit_line: string;
|
|
window_line: string;
|
|
}>;
|
|
|
|
export type RangeDeliveryProjection = Readonly<{
|
|
range: readonly [string, string] | null;
|
|
representative_time: string | null;
|
|
representative_candidate_id: string | null;
|
|
event_count: number;
|
|
fit_percent: number | null;
|
|
boundary: string;
|
|
shared_traits: readonly string[];
|
|
columns: readonly RangeDeliveryColumn[];
|
|
more_count: number;
|
|
more_label: string | null;
|
|
verification_markdown: string | null;
|
|
narrow_hint: string | null;
|
|
}>;
|
|
|
|
export type PublicCandidateClock = Readonly<{
|
|
candidateId: string;
|
|
time: string;
|
|
relativeSupport?: number;
|
|
}>;
|
|
|
|
function clock(value: unknown): string | null {
|
|
if (typeof value !== "string") return null;
|
|
const normalized = value.slice(0, 5);
|
|
return CLOCK.test(normalized) ? normalized : null;
|
|
}
|
|
|
|
function signFromProbe(
|
|
probes: readonly ConflictProbe[],
|
|
layer: "d9" | "d10",
|
|
candidateId: string,
|
|
): string | null {
|
|
const prefix = layer === "d9" ? "varga.d9" : "varga.d10";
|
|
for (const probe of probes) {
|
|
if (!probe.semantic_key.startsWith(prefix)) continue;
|
|
const options = probe.style_options;
|
|
if (!options?.length) continue;
|
|
for (const outcome of probe.expected_outcomes) {
|
|
if (!outcome.supports.includes(candidateId)) continue;
|
|
const option = options.find((item) => item.answer_class === outcome.answer_class);
|
|
const sign = option?.sign?.trim();
|
|
if (!sign) continue;
|
|
const keyed = signKey(sign);
|
|
if (layer === "d9" && D9_TYPE_TABLE[keyed]) return keyed;
|
|
if (layer === "d10" && D10_TYPE_TABLE[keyed]) return keyed;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function signFromTransitions(
|
|
transitions: readonly { layer: string; at: string; from_sign?: string; to_sign?: string }[],
|
|
layer: "d9" | "d10",
|
|
time: string,
|
|
): string | null {
|
|
const sign = signFromTransitionLookup(transitions, layer, time);
|
|
const keyed = sign ? signKey(sign) : "";
|
|
if (layer === "d9" && D9_TYPE_TABLE[keyed]) return keyed;
|
|
if (layer === "d10" && D10_TYPE_TABLE[keyed]) return keyed;
|
|
return null;
|
|
}
|
|
|
|
function signForCandidate(
|
|
probes: readonly ConflictProbe[],
|
|
windowScan: WindowScan | null,
|
|
layer: "d9" | "d10",
|
|
candidate: InferenceCandidate,
|
|
): string | null {
|
|
return signFromProbe(probes, layer, candidate.id)
|
|
?? signFromTransitions(windowScan?.transitions ?? [], layer, candidate.time);
|
|
}
|
|
|
|
export function d9DivergenceLabel(sign: string): string | null {
|
|
const keyed = signKey(sign);
|
|
const row = D9_TYPE_TABLE[keyed];
|
|
if (!row) return null;
|
|
return `关系盘落在${keyed},通常表现为${row.userChoice}`;
|
|
}
|
|
|
|
export function d10DivergenceLabel(sign: string): string | null {
|
|
const keyed = signKey(sign);
|
|
const row = D10_TYPE_TABLE[keyed];
|
|
if (!row) return null;
|
|
return `事业盘落在${keyed},通常表现为${row.userChoice}`;
|
|
}
|
|
|
|
function publicIdForTime(
|
|
publicCandidates: readonly PublicCandidateClock[],
|
|
time: string,
|
|
fallbackId: string,
|
|
): string {
|
|
const match = publicCandidates.find((item) => item.time === time);
|
|
if (match && UUID.test(match.candidateId)) return match.candidateId;
|
|
return fallbackId;
|
|
}
|
|
|
|
export function datedEventCount(inference: InferenceState | null): number {
|
|
if (!inference) return 0;
|
|
return inference.events.filter((item) => item.year != null && item.usage !== "unused").length;
|
|
}
|
|
|
|
function inRange(
|
|
time: string,
|
|
range: readonly [string, string] | null,
|
|
): boolean {
|
|
if (!range) return true;
|
|
return time >= range[0] && time <= range[1];
|
|
}
|
|
|
|
function probabilityPercent(value: number | null | undefined, fallback = 0): number {
|
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return fallback;
|
|
const raw = value <= 1 ? value * 100 : value;
|
|
return Math.min(100, Math.round(raw));
|
|
}
|
|
|
|
function fitFromLedger(rows: readonly EventDashaLedgerRow[]): RangeDeliveryFit {
|
|
let strong = 0;
|
|
let medium = 0;
|
|
let weak = 0;
|
|
let worst: RangeDeliveryFit["worst"] = null;
|
|
let worstRank = 99;
|
|
for (const row of rows) {
|
|
if (row.match === "strong") strong += 1;
|
|
else if (row.match === "medium") medium += 1;
|
|
else if (row.match === "weak") weak += 1;
|
|
const rank = MATCH_RANK[row.match] ?? 99;
|
|
if (rank < worstRank && row.year_month && row.domain) {
|
|
worstRank = rank;
|
|
worst = { year_month: row.year_month, domain: row.domain };
|
|
}
|
|
}
|
|
return { strong, medium, weak, total: rows.length, worst };
|
|
}
|
|
|
|
function lookupByTime<T>(
|
|
map: Readonly<Record<string, T>> | undefined,
|
|
time: string,
|
|
): T | undefined {
|
|
if (!map || !Object.prototype.hasOwnProperty.call(map, time)) return undefined;
|
|
return map[time];
|
|
}
|
|
|
|
function traitLines(traits: RangeDeliveryColumn["traits"]): string[] {
|
|
return [traits.d9, traits.d10, ...traits.nakshatra].filter(Boolean);
|
|
}
|
|
|
|
function splitSharedTraits(
|
|
columns: readonly Omit<RangeDeliveryColumn, "fit_line" | "window_line">[],
|
|
): { shared: string[]; columns: typeof columns } {
|
|
if (columns.length < 2) return { shared: [], columns };
|
|
const lineSets = columns.map((column) => traitLines(column.traits));
|
|
const sharedRaw = lineSets[0]?.filter((line) => lineSets.every((set) => set.includes(line))) ?? [];
|
|
const sharedSet = new Set(sharedRaw);
|
|
const shared = sharedRaw.map((line) => sharedTraitLine(line, columns.length));
|
|
return {
|
|
shared,
|
|
columns: columns.map((column) => ({
|
|
...column,
|
|
traits: {
|
|
d9: sharedSet.has(column.traits.d9) ? "" : column.traits.d9,
|
|
d10: sharedSet.has(column.traits.d10) ? "" : column.traits.d10,
|
|
nakshatra: column.traits.nakshatra.filter((line) => !sharedSet.has(line)),
|
|
},
|
|
})),
|
|
};
|
|
}
|
|
|
|
function nakshatraTraitsForTime(
|
|
boundary: NakshatraBoundary | null,
|
|
time: string,
|
|
representativeTime: string | null,
|
|
): readonly string[] {
|
|
if (!boundary?.near_boundary) return [];
|
|
const earlier = boundary.options.find((item) => item.time_bias === "earlier")?.traits ?? [];
|
|
const later = boundary.options.find((item) => item.time_bias === "later")?.traits ?? [];
|
|
if (!representativeTime || time === representativeTime) return earlier.length ? earlier : later;
|
|
return time < representativeTime ? earlier : later;
|
|
}
|
|
|
|
type ColumnClock = Readonly<{
|
|
time: string;
|
|
id: string;
|
|
candidate: InferenceCandidate | null;
|
|
probability: number;
|
|
}>;
|
|
|
|
function columnClocks(input: {
|
|
inference: InferenceState | null;
|
|
publicCandidates: readonly PublicCandidateClock[];
|
|
range: readonly [string, string] | null;
|
|
}): { clocks: ColumnClock[]; moreCount: number } {
|
|
const fromInference = input.inference
|
|
? rankActive(input.inference.candidates).filter((item) => inRange(item.time, input.range))
|
|
: [];
|
|
const seen = new Set<string>();
|
|
const ranked: ColumnClock[] = [];
|
|
for (const item of fromInference) {
|
|
if (seen.has(item.time)) continue;
|
|
seen.add(item.time);
|
|
ranked.push({
|
|
time: item.time,
|
|
id: item.id,
|
|
candidate: item,
|
|
probability: probabilityPercent(item.probability),
|
|
});
|
|
}
|
|
if (ranked.length === 0) {
|
|
const publicRanked = [...input.publicCandidates]
|
|
.filter((item) => inRange(item.time, input.range))
|
|
.sort((left, right) => (right.relativeSupport ?? 0) - (left.relativeSupport ?? 0)
|
|
|| left.time.localeCompare(right.time));
|
|
for (const item of publicRanked) {
|
|
if (seen.has(item.time)) continue;
|
|
seen.add(item.time);
|
|
ranked.push({
|
|
time: item.time,
|
|
id: item.candidateId,
|
|
candidate: null,
|
|
probability: probabilityPercent(item.relativeSupport, 0),
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
clocks: ranked.slice(0, COLUMN_LIMIT),
|
|
moreCount: Math.max(0, ranked.length - COLUMN_LIMIT),
|
|
};
|
|
}
|
|
|
|
export function verificationMarkdownFromUnknown(value: unknown): string | null {
|
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
const markdown = (value as { markdown?: unknown }).markdown;
|
|
return typeof markdown === "string" && markdown.trim() ? markdown : null;
|
|
}
|
|
|
|
function fitPercentFromUnknown(value: unknown): number | null {
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
const percent = (value as { percent?: unknown }).percent;
|
|
return typeof percent === "number" && Number.isFinite(percent) ? percent : null;
|
|
}
|
|
|
|
export function buildRangeDelivery(input: {
|
|
inference: InferenceState | null;
|
|
windowScan?: WindowScan | null;
|
|
publicCandidates?: readonly PublicCandidateClock[];
|
|
credibleRange?: readonly [string, string] | null;
|
|
representativeTime?: string | null;
|
|
fitPercent?: number | null;
|
|
verificationMarkdown?: string | null;
|
|
nakshatraBoundary?: NakshatraBoundary | null;
|
|
eventDashaLedgerByTime?: Readonly<Record<string, readonly EventDashaLedgerRow[]>>;
|
|
prospectiveWindowsByTime?: Readonly<Record<string, readonly ProspectiveWindow[]>>;
|
|
eventDashaLedger?: readonly EventDashaLedgerRow[];
|
|
evidence?: readonly Readonly<{
|
|
status: string;
|
|
domain: string;
|
|
datePrecision: string;
|
|
occurredFrom: string | null;
|
|
occurredTo: string | null;
|
|
eventKind?: string | null;
|
|
summary?: string | null;
|
|
}>[];
|
|
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
|
|
}): RangeDeliveryProjection {
|
|
const inference = input.inference;
|
|
const range = input.credibleRange
|
|
?? inference?.credible_range
|
|
?? (inference ? [inference.range_start, inference.range_end] as const : null);
|
|
const representativeTime = clock(input.representativeTime)
|
|
?? clock(inference?.representative_time)
|
|
?? null;
|
|
const publicCandidates = input.publicCandidates ?? [];
|
|
const representativeId = representativeTime
|
|
? publicIdForTime(
|
|
publicCandidates,
|
|
representativeTime,
|
|
inference?.candidates.find((item) => item.time === representativeTime)?.id ?? representativeTime,
|
|
)
|
|
: publicCandidates[0]?.candidateId ?? null;
|
|
const normalizedRange = range && clock(range[0]) && clock(range[1])
|
|
? [clock(range[0])!, clock(range[1])!] as const
|
|
: null;
|
|
const { clocks, moreCount } = columnClocks({
|
|
inference,
|
|
publicCandidates,
|
|
range: normalizedRange,
|
|
});
|
|
const probes = inference?.probes ?? [];
|
|
const windowScan = input.windowScan ?? null;
|
|
const rawColumns = clocks.map((item) => {
|
|
const d9 = item.candidate
|
|
? signForCandidate(probes, windowScan, "d9", item.candidate)
|
|
: signFromTransitions(windowScan?.transitions ?? [], "d9", item.time);
|
|
const d10 = item.candidate
|
|
? signForCandidate(probes, windowScan, "d10", item.candidate)
|
|
: signFromTransitions(windowScan?.transitions ?? [], "d10", item.time);
|
|
const timedLedger = lookupByTime(input.eventDashaLedgerByTime, item.time);
|
|
const ledger = timedLedger
|
|
?? (item.time === representativeTime ? input.eventDashaLedger : undefined);
|
|
const windows = lookupByTime(input.prospectiveWindowsByTime, item.time);
|
|
const fit = ledger ? fitFromLedger(ledger) : null;
|
|
return {
|
|
time: item.time,
|
|
candidate_id: publicIdForTime(publicCandidates, item.time, item.id),
|
|
probability_percent: item.probability,
|
|
traits: {
|
|
d9: d9 ? (d9DivergenceLabel(d9) ?? "") : "",
|
|
d10: d10 ? (d10DivergenceLabel(d10) ?? "") : "",
|
|
nakshatra: nakshatraTraitsForTime(input.nakshatraBoundary ?? null, item.time, representativeTime),
|
|
},
|
|
fit,
|
|
windows: windows === undefined ? null : windows,
|
|
};
|
|
});
|
|
const split = splitSharedTraits(rawColumns);
|
|
const columns = split.columns.map((column) => ({
|
|
...column,
|
|
fit_line: rangeDeliveryFitLine(column.fit),
|
|
window_line: rangeDeliveryWindowLine(column.windows),
|
|
}));
|
|
return {
|
|
range: normalizedRange,
|
|
representative_time: representativeTime,
|
|
representative_candidate_id: representativeId,
|
|
event_count: datedEventCount(inference),
|
|
fit_percent: input.fitPercent ?? null,
|
|
boundary: REPRESENTATIVE_MINUTE_DISCLAIMER,
|
|
shared_traits: split.shared,
|
|
columns,
|
|
more_count: moreCount,
|
|
more_label: moreCount > 0 ? RANGE_DELIVERY_MORE_MINUTES(moreCount) : null,
|
|
verification_markdown: input.verificationMarkdown ?? null,
|
|
narrow_hint: rangeNarrowHint(
|
|
remainingSplitLayers({
|
|
transitions: inference?.transitions ?? windowScan?.transitions ?? [],
|
|
scanFlags: windowScan,
|
|
activeTimes: inference?.candidates
|
|
.filter((item) => item.status !== "eliminated")
|
|
.map((item) => item.time) ?? clocks.map((item) => item.time),
|
|
}),
|
|
input.evidence ?? [],
|
|
input.declinedTopics ?? [],
|
|
remainingSplitTimes(
|
|
inference?.candidates
|
|
.filter((item) => item.status !== "eliminated")
|
|
.map((item) => item.time) ?? clocks.map((item) => item.time),
|
|
),
|
|
),
|
|
};
|
|
}
|
|
|
|
function publicCandidatesFromUnknown(value: unknown): PublicCandidateClock[] {
|
|
if (!Array.isArray(value)) return [];
|
|
const out: PublicCandidateClock[] = [];
|
|
for (const item of value) {
|
|
if (!item || typeof item !== "object") continue;
|
|
const row = item as Record<string, unknown>;
|
|
const time = clock(row.time);
|
|
const id = typeof row.candidateId === "string"
|
|
? row.candidateId
|
|
: typeof row.candidate_id === "string"
|
|
? row.candidate_id
|
|
: "";
|
|
if (!time || !id) continue;
|
|
const relativeSupport = typeof row.relativeSupport === "number"
|
|
? row.relativeSupport
|
|
: typeof row.relative_support === "number"
|
|
? row.relative_support
|
|
: undefined;
|
|
out.push({ candidateId: id, time, ...(relativeSupport != null ? { relativeSupport } : {}) });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function rangeFromUnknown(value: unknown): readonly [string, string] | null {
|
|
if (!Array.isArray(value) || value.length !== 2) return null;
|
|
const start = clock(value[0]);
|
|
const end = clock(value[1]);
|
|
return start && end ? [start, end] : null;
|
|
}
|
|
|
|
export function rangeDeliveryForSnapshot(snapshot: {
|
|
decisionReceipt?: Readonly<Record<string, unknown>> | null;
|
|
decision_receipt?: Readonly<Record<string, unknown>> | null;
|
|
candidates?: unknown;
|
|
representativeTime?: string | null;
|
|
representative_time?: string | null;
|
|
credibleRange?: unknown;
|
|
credible_range?: unknown;
|
|
skill_verification_report?: unknown;
|
|
skillVerificationReport?: unknown;
|
|
event_fit_rate?: unknown;
|
|
eventFitRate?: unknown;
|
|
evidence?: readonly Readonly<{
|
|
status: string;
|
|
domain: string;
|
|
datePrecision: string;
|
|
occurredFrom: string | null;
|
|
occurredTo: string | null;
|
|
eventKind?: string | null;
|
|
summary?: string | null;
|
|
}>[];
|
|
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
|
|
} | null | undefined): RangeDeliveryProjection {
|
|
const receipt = snapshot?.decisionReceipt ?? snapshot?.decision_receipt ?? null;
|
|
const inference = previousInferenceFromReceipt(receipt);
|
|
const windowScan = parseWindowScan(receipt?.window_scan) ?? parseWindowScan(
|
|
snapshot && "window_scan" in (snapshot as object)
|
|
? (snapshot as { window_scan?: unknown }).window_scan
|
|
: null,
|
|
);
|
|
const refinement = refinementFromDecisionReceipt(receipt);
|
|
return buildRangeDelivery({
|
|
inference,
|
|
windowScan,
|
|
publicCandidates: publicCandidatesFromUnknown(snapshot?.candidates),
|
|
credibleRange: rangeFromUnknown(snapshot?.credibleRange) ?? rangeFromUnknown(snapshot?.credible_range),
|
|
representativeTime: clock(snapshot?.representativeTime) ?? clock(snapshot?.representative_time),
|
|
fitPercent: refinement.event_fit_rate?.percent
|
|
?? fitPercentFromUnknown(snapshot?.event_fit_rate)
|
|
?? fitPercentFromUnknown(snapshot?.eventFitRate),
|
|
verificationMarkdown: verificationMarkdownFromUnknown(snapshot?.skill_verification_report)
|
|
?? verificationMarkdownFromUnknown(snapshot?.skillVerificationReport),
|
|
nakshatraBoundary: refinement.nakshatra_boundary,
|
|
eventDashaLedgerByTime: refinement.event_dasha_ledger_by_time,
|
|
prospectiveWindowsByTime: refinement.prospective_windows_by_time,
|
|
eventDashaLedger: refinement.event_dasha_ledger,
|
|
evidence: snapshot?.evidence,
|
|
declinedTopics: snapshot?.declinedTopics,
|
|
});
|
|
}
|
|
|
|
function parseFit(value: unknown): RangeDeliveryFit | null {
|
|
if (value == null) return null;
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return { strong: 0, medium: 0, weak: 0, total: 0, worst: null };
|
|
}
|
|
const row = value as Record<string, unknown>;
|
|
const count = (key: string) => (
|
|
typeof row[key] === "number" && Number.isInteger(row[key]) && (row[key] as number) >= 0
|
|
? row[key] as number
|
|
: 0
|
|
);
|
|
const worstRaw = row.worst && typeof row.worst === "object" && !Array.isArray(row.worst)
|
|
? row.worst as Record<string, unknown>
|
|
: null;
|
|
const yearMonth = typeof worstRaw?.year_month === "string" ? worstRaw.year_month : "";
|
|
const domain = typeof worstRaw?.domain === "string" ? worstRaw.domain : "";
|
|
const strong = count("strong");
|
|
const medium = count("medium");
|
|
const weak = count("weak");
|
|
const total = count("total") || strong + medium + weak;
|
|
return {
|
|
strong,
|
|
medium,
|
|
weak,
|
|
total,
|
|
worst: yearMonth && domain ? { year_month: yearMonth, domain } : null,
|
|
};
|
|
}
|
|
|
|
function parseWindows(value: unknown): ProspectiveWindow[] {
|
|
if (!Array.isArray(value)) return [];
|
|
const out: ProspectiveWindow[] = [];
|
|
for (const item of value) {
|
|
if (!item || typeof item !== "object") continue;
|
|
const row = item as Record<string, unknown>;
|
|
const domain = typeof row.domain === "string" ? row.domain : "";
|
|
const from = typeof row.from === "string" ? row.from : "";
|
|
const to = typeof row.to === "string" ? row.to : from;
|
|
if (!domain || !from) continue;
|
|
out.push({ domain, from, to });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function parseColumns(value: unknown): RangeDeliveryColumn[] {
|
|
if (!Array.isArray(value)) return [];
|
|
const out: RangeDeliveryColumn[] = [];
|
|
for (const item of value) {
|
|
if (!item || typeof item !== "object") continue;
|
|
const row = item as Record<string, unknown>;
|
|
const time = clock(row.time);
|
|
const id = typeof row.candidate_id === "string"
|
|
? row.candidate_id
|
|
: typeof row.candidateId === "string"
|
|
? row.candidateId
|
|
: "";
|
|
if (!time || !id) continue;
|
|
const traits = row.traits && typeof row.traits === "object" && !Array.isArray(row.traits)
|
|
? row.traits as Record<string, unknown>
|
|
: {};
|
|
const nakshatra = Array.isArray(traits.nakshatra)
|
|
? traits.nakshatra.filter((entry): entry is string => typeof entry === "string")
|
|
: [];
|
|
const fit = "fit" in row && row.fit === null ? null : parseFit(row.fit);
|
|
const windows = !("windows" in row) || row.windows == null ? null : parseWindows(row.windows);
|
|
out.push({
|
|
time,
|
|
candidate_id: id,
|
|
probability_percent: probabilityPercent(
|
|
typeof row.probability_percent === "number" ? row.probability_percent : 0,
|
|
0,
|
|
),
|
|
traits: {
|
|
d9: typeof traits.d9 === "string" ? traits.d9 : "",
|
|
d10: typeof traits.d10 === "string" ? traits.d10 : "",
|
|
nakshatra,
|
|
},
|
|
fit,
|
|
windows,
|
|
fit_line: typeof row.fit_line === "string" && row.fit_line.trim()
|
|
? row.fit_line.trim()
|
|
: rangeDeliveryFitLine(fit),
|
|
window_line: typeof row.window_line === "string" && row.window_line.trim()
|
|
? row.window_line.trim()
|
|
: rangeDeliveryWindowLine(windows),
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function parseRangeDelivery(value: unknown): RangeDeliveryProjection | null {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
const row = value as Record<string, unknown>;
|
|
const columns = parseColumns(row.columns);
|
|
if (columns.length === 0) return null;
|
|
const range = rangeFromUnknown(row.range);
|
|
const representativeTime = clock(row.representative_time) ?? clock(row.representativeTime);
|
|
const representativeId = typeof row.representative_candidate_id === "string"
|
|
? row.representative_candidate_id
|
|
: typeof row.representativeCandidateId === "string"
|
|
? row.representativeCandidateId
|
|
: null;
|
|
const eventCount = typeof row.event_count === "number" && Number.isInteger(row.event_count) && row.event_count >= 0
|
|
? row.event_count
|
|
: typeof row.eventCount === "number" && Number.isInteger(row.eventCount) && row.eventCount >= 0
|
|
? row.eventCount
|
|
: 0;
|
|
const moreCount = typeof row.more_count === "number" && Number.isInteger(row.more_count) && row.more_count >= 0
|
|
? row.more_count
|
|
: 0;
|
|
const boundary = typeof row.boundary === "string" && row.boundary.trim()
|
|
? row.boundary.trim()
|
|
: REPRESENTATIVE_MINUTE_DISCLAIMER;
|
|
const sharedTraits = Array.isArray(row.shared_traits)
|
|
? row.shared_traits.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
: Array.isArray(row.sharedTraits)
|
|
? row.sharedTraits.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
: [];
|
|
return {
|
|
range,
|
|
representative_time: representativeTime,
|
|
representative_candidate_id: representativeId,
|
|
event_count: eventCount,
|
|
fit_percent: fitPercentFromUnknown(row.fit_percent) ?? fitPercentFromUnknown(row.fitPercent),
|
|
boundary,
|
|
shared_traits: sharedTraits,
|
|
columns,
|
|
more_count: moreCount,
|
|
more_label: typeof row.more_label === "string" && row.more_label.trim()
|
|
? row.more_label.trim()
|
|
: moreCount > 0 ? RANGE_DELIVERY_MORE_MINUTES(moreCount) : null,
|
|
verification_markdown: verificationMarkdownFromUnknown(row.verification_markdown)
|
|
?? verificationMarkdownFromUnknown(row.verificationMarkdown),
|
|
narrow_hint: typeof row.narrow_hint === "string" && row.narrow_hint.trim()
|
|
? row.narrow_hint.trim()
|
|
: typeof row.narrowHint === "string" && row.narrowHint.trim()
|
|
? row.narrowHint.trim()
|
|
: null,
|
|
};
|
|
}
|