import type { KpiResult } from './kpi-fold.js'; /** * 지표 어휘 — **이름을 한 곳에서 정한다.** * * 목표와 사실이 서로 다른 이름을 쓰면 그 사이에 매핑 표가 생기고, 그 표가 곧 방언이 된다. 그래서 * 목표가 가리킬 수 있는 지표는 여기 열거된 것뿐이고, 값을 꺼내는 방법도 여기 함께 있다. * * **`direction` 은 지표의 성질이지 사용자의 선택이 아니다.** 처리량은 높을수록 좋고 대기시간은 낮을수록 * 좋다 — 이것을 입력으로 받으면 반대로 적은 목표가 만들어지고, 그 뒤 판정이 전부 뒤집힌다. */ export declare const TWIN_METRIC: { readonly 'throughput.tasks': { readonly direction: "higher"; readonly unit: "count"; readonly of: (k: KpiResult) => number; }; readonly 'throughput.orders': { readonly direction: "higher"; readonly unit: "count"; readonly of: (k: KpiResult) => number; }; readonly 'leadTime.p50Ms': { readonly direction: "lower"; readonly unit: "ms"; readonly of: (k: KpiResult) => number; }; readonly 'leadTime.p90Ms': { readonly direction: "lower"; readonly unit: "ms"; readonly of: (k: KpiResult) => number; }; readonly 'workTime.p50Ms': { readonly direction: "lower"; readonly unit: "ms"; readonly of: (k: KpiResult) => number; }; readonly 'waitTime.p50Ms': { readonly direction: "lower"; readonly unit: "ms"; readonly of: (k: KpiResult) => number; }; readonly 'energy.kWh': { readonly direction: "lower"; readonly unit: "kWh"; readonly of: (k: KpiResult) => number; }; /** 최대수요 — 계약 상한을 목표로 적는 자리다(요금의 기본요금 부분이 여기서 정해진다). */ readonly 'energy.peakKW': { readonly direction: "lower"; readonly unit: "kW"; readonly of: (k: KpiResult) => number; }; /** 원단위 — 완료 오더당 kWh. 답할 수 없으면(`null`) 판정하지 않는다. */ readonly 'energy.intensity': { readonly direction: "lower"; readonly unit: "kWh/order"; readonly of: (k: KpiResult) => number; }; /** 자원 점유 — 개별 자원이 아니라 **창 전체의 평균 비율**(자원별은 관점 축의 일이다). */ readonly 'utilization.ratio': { readonly direction: "higher"; readonly unit: "ratio"; readonly of: (k: KpiResult) => number; }; }; export type TwinMetricId = keyof typeof TWIN_METRIC; export declare const isTwinMetric: (id: string) => id is TwinMetricId; /** * 저장된 목표 행에서 **이 범위에 걸린 목표**를 고른다 — 좁은 선언이 넓은 선언을 덮는다. * * **왜 순수 함수인가**: 이 규칙(공간 목표 위에 인스턴스 목표를 얹는다)은 지금까지 DB 접근 뒤에 숨어 * 있어서 **화면을 눌러 봐야만** 확인할 수 있었다. 그런데 이 프로젝트는 로그인 뒤 화면을 자동으로 * 검증할 방법이 없어, 실제로 여러 기능이 "실화면 미검증" 으로 쌓였다. 규칙을 밖으로 빼면 테스트가 * 값싸진다 — 조회는 행을 가져오기만 하고, **판단은 여기서** 한다. */ /** * 축 목표까지 담은 목록 — 전체 목표와 **축의 한 값**에 걸린 목표를 함께 든다. * * 축 목표는 전체 목표를 **덮지 않는다.** 다른 질문이기 때문이다: "현장 전체가 100건" 과 * "용접 구역이 30건" 은 동시에 참일 수 있다. 덮게 만들면 축 하나에 목표를 거는 순간 전체 판정이 * 사라져 사용자가 "목표를 지웠나?" 를 묻게 된다. */ export interface ScopedTargets { /** 창 전체에 걸린 목표. */ overall: TwinTargetSpec[]; /** 축 → (그 축의 값 → 목표들). 요청한 축만 채운다. */ byAxisKey: Record; } /** * 축의 각 값에 대해 목표 대비 판정 — **그룹 항목에 그대로 붙일 형태.** * * 그룹의 지표는 전체보다 좁다(주문 수·자원 점유는 축 단위로 뜻이 다르거나 없다). 없는 지표에 목표가 * 걸려 있으면 **판정하지 않는다** — 여기서 0 으로 읽으면 시간 지표가 "완벽 달성" 으로 뒤집힌다 * (전체 판정에서 이미 겪은 결함이다). */ export declare function judgeGroupTargets(group: { key: string; tasks: number; leadTime: { count: number; p50Ms: number; }; workTime: { count: number; p50Ms: number; }; waitTime: { count: number; p50Ms: number; }; }, targets: readonly TwinTargetSpec[] | undefined): TargetVerdict[]; export declare function mergeTargets(rows: readonly TargetRow[], scope: { spaceId?: string; instanceIds: readonly string[]; axis?: string; }): TwinTargetSpec[]; /** 저장된 목표 행 — 판단에 필요한 것만(엔티티 전체를 요구하지 않는다: 테스트가 값싸진다). */ export interface TargetRow { metric: string; target: number; spaceId?: string | null; instanceId?: string | null; axis?: string | null; axisKey?: string | null; } /** 전체 목표와 축 목표를 함께 고른다 — 좁은 선언(인스턴스)이 넓은 선언(공간)을 덮는 규칙은 양쪽 같다. */ export declare function scopedTargets(rows: readonly TargetRow[], scope: { spaceId?: string; instanceIds: readonly string[]; axis?: string; }): ScopedTargets; /** 목표 하나 — 어느 지표를, 얼마로. 방향은 지표가 안다(§TWIN_METRIC). */ export interface TwinTargetSpec { metric: TwinMetricId; target: number; } export interface TargetVerdict { metric: TwinMetricId; /** 이 구간의 실제 값. */ actual: number; target: number; direction: 'higher' | 'lower'; /** 목표를 만족했나. */ met: boolean; /** * 목표 대비 비율 — **항상 "1 이상이면 좋다"** 로 정규화한다(방향이 다른 지표를 한 화면에서 비교할 수 * 있게). 처리량은 `actual/target`, 시간은 `target/actual`. */ ratio: number; } /** * 목표 대비 판정 — **목표가 없는 지표는 판정하지 않는다.** * * 값이 없는 지표(그 구간에 기록이 없어 `undefined`)도 판정하지 않는다. 없는 것을 0 으로 놓으면 * "목표 크게 미달" 이라는 **없는 사실**이 만들어지고, 사용자는 멈춘 적 없는 라인을 고치러 간다. * 목표를 0 으로 준 경우도 비율을 내지 않는다(0 으로 나누지 않는다) — 만족 여부만 답한다. */ export declare function judgeTargets(kpi: KpiResult, targets: readonly TwinTargetSpec[] | undefined): TargetVerdict[];