Rescored candidate sets were widening because new minutes stayed neutral. Derive support/conflict from varga signs and dasha segments, and announce range changes from facts rather than tool names. Co-authored-by: Cursor <cursoragent@cursor.com>
49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
/**
|
|
* Minute → varga-sign lookup from window-scan transitions.
|
|
* Core must not import v9; callers that need type-table keys wrap this.
|
|
*/
|
|
|
|
export type TransitionSignLookup = Readonly<{
|
|
layer: string;
|
|
at: string;
|
|
from_sign?: string;
|
|
to_sign?: string;
|
|
}>;
|
|
|
|
const CLOCK = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
|
|
|
export function clockMinutes(time: string): number | null {
|
|
const normalized = time.trim().slice(0, 5);
|
|
if (!CLOCK.test(normalized)) return null;
|
|
return Number(normalized.slice(0, 2)) * 60 + Number(normalized.slice(3, 5));
|
|
}
|
|
|
|
export function normalizeSignName(value: string | null | undefined): string | null {
|
|
const trimmed = value?.trim() ?? "";
|
|
if (!trimmed) return null;
|
|
return trimmed.endsWith("座") ? trimmed : `${trimmed}座`;
|
|
}
|
|
|
|
export function signFromTransitions(
|
|
transitions: readonly TransitionSignLookup[],
|
|
layer: string,
|
|
time: string,
|
|
): string | null {
|
|
const point = clockMinutes(time);
|
|
if (point == null) return null;
|
|
const ordered = transitions
|
|
.filter((item) => item.layer === layer)
|
|
.filter((item) => clockMinutes(item.at) != null)
|
|
.slice()
|
|
.sort((left, right) => (clockMinutes(left.at) ?? 0) - (clockMinutes(right.at) ?? 0));
|
|
if (ordered.length === 0) return null;
|
|
let sign = ordered[0]?.from_sign?.trim() || null;
|
|
for (const item of ordered) {
|
|
const at = clockMinutes(item.at);
|
|
if (at == null) continue;
|
|
if (at <= point) sign = item.to_sign?.trim() || sign;
|
|
else break;
|
|
}
|
|
return sign || null;
|
|
}
|