@operato/twin-kernel 0.7.11 → 0.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1125,6 +1125,20 @@ export interface OrderState {
1125
1125
  * 이것도 상태에 없어서 되살아난 오더는 빈 문자열을 갖고, 이후 모든 이벤트의 거래번호가 빈다.
1126
1126
  */
1127
1127
  bizTransaction?: string;
1128
+ /**
1129
+ * 도메인이 **약속해 둔 자리와 시각창** — 어디로/언제 들이기로 했는가.
1130
+ *
1131
+ * ── 왜 사실에 실어야 하나 (2026-08-17) ─────────────────────────────────────
1132
+ * 야드 트윈이 재기동 뒤 **첫 틱에서 죽었다.** 어포인트먼트를 받을 때 도크 도어를 정해 두는데
1133
+ * 그것이 어느 사실에도 실리지 않아, 웜스타트가 오더는 되살리고 배정은 잃었다. 그리고 다음 틱에
1134
+ * 「그 도어의 점유」를 읽다 예외가 났다(`Cannot read properties of undefined`). 안전망이 그 트윈만
1135
+ * 세워 다른 트윈은 살았지만, 야드 트윈 셋이 부팅마다 죽는 상태였다.
1136
+ *
1137
+ * `allocated` 가 두 자리에만 있어 계보를 잃었던 것과 **같은 부류**다: 상태가 필요로 하는 것을
1138
+ * 사실이 갖고 있지 않으면 재기동을 넘지 못한다.
1139
+ */
1140
+ dockDoor?: string;
1141
+ windowStartMs?: number;
1128
1142
  /** 우선순위 — 표준 `OperationsRequest.Priority`. 작은 값이 급하다. 오더 할당 순서를 정한다. */
1129
1143
  priority?: number;
1130
1144
  /** 예정 착수 — 표준 `OperationsRequest.StartTime`. */
@@ -1670,6 +1684,20 @@ export interface OrderStatusDelta {
1670
1684
  */
1671
1685
  allocated?: string[];
1672
1686
  bizTransaction?: string;
1687
+ /**
1688
+ * 도메인이 **약속해 둔 자리와 시각창** — 어디로/언제 들이기로 했는가.
1689
+ *
1690
+ * ── 왜 사실에 실어야 하나 (2026-08-17) ─────────────────────────────────────
1691
+ * 야드 트윈이 재기동 뒤 **첫 틱에서 죽었다.** 어포인트먼트를 받을 때 도크 도어를 정해 두는데
1692
+ * 그것이 어느 사실에도 실리지 않아, 웜스타트가 오더는 되살리고 배정은 잃었다. 그리고 다음 틱에
1693
+ * 「그 도어의 점유」를 읽다 예외가 났다(`Cannot read properties of undefined`). 안전망이 그 트윈만
1694
+ * 세워 다른 트윈은 살았지만, 야드 트윈 셋이 부팅마다 죽는 상태였다.
1695
+ *
1696
+ * `allocated` 가 두 자리에만 있어 계보를 잃었던 것과 **같은 부류**다: 상태가 필요로 하는 것을
1697
+ * 사실이 갖고 있지 않으면 재기동을 넘지 못한다.
1698
+ */
1699
+ dockDoor?: string;
1700
+ windowStartMs?: number;
1673
1701
  }
1674
1702
  export declare const CMD: {
1675
1703
  readonly orderHold: "order.hold";
@@ -16,5 +16,11 @@ export declare const EMS_PROPERTY: {
16
16
  readonly ratedKW: "power.ratedKW";
17
17
  /** 멈춰 있을 때의 소비(kW). 없으면 멈춘 동안을 **비운다** — 0 이라고 주장하지 않는다. */
18
18
  readonly standbyKW: "power.standbyKW";
19
+ /** 기본요금 단가 — 최대수요 1kW 당(청구 주기 기준). */
20
+ readonly demandChargePerKW: "tariff.demandChargePerKW";
21
+ /** 사용량 단가 — 1kWh 당. */
22
+ readonly energyChargePerKWh: "tariff.energyChargePerKWh";
23
+ /** 통화 — ISO 4217 코드(USD·KRW…). 없으면 금액에 단위를 붙이지 않는다. */
24
+ readonly currency: "tariff.currency";
19
25
  };
20
26
  export declare const EMS_TYPES: TwinTypeInfo[];
@@ -20,7 +20,26 @@ export const EMS_PROPERTY = {
20
20
  /** 가동 중 소비(kW). */
21
21
  ratedKW: 'power.ratedKW',
22
22
  /** 멈춰 있을 때의 소비(kW). 없으면 멈춘 동안을 **비운다** — 0 이라고 주장하지 않는다. */
23
- standbyKW: 'power.standbyKW'
23
+ standbyKW: 'power.standbyKW',
24
+ /*
25
+ * ── 요금 단가 — 수를 금액으로 바꾸는 선언 (2026-08-17) ─────────────────────
26
+ *
27
+ * 그동안 트윈은 「최대수요 328kW」·「전력량 1,547kWh」까지 말하고 멈췄다. 그런데 피크를 깎는 일이
28
+ * 돈이 되는 이유는 **요금이 둘로 나뉘기** 때문이다: 사용량(kWh)에 붙는 요금과 **최대수요(kW)에 붙는
29
+ * 기본요금**. 단가가 없으면 그 절반을 말할 수 없고, 그래서 「피크를 깎아 얼마를 아끼나」에 답하지 못했다.
30
+ *
31
+ * 단가는 **현장의 계약**이다(같은 나라 안에서도 사업자·요금제마다 다르다). 그래서 기본값을 두지
32
+ * 않는다 — 선언하지 않으면 금액을 계산하지 않는다. 짐작한 단가로 낸 금액은 숫자가 있다는 것만으로
33
+ * 사람을 결정으로 밀어붙인다.
34
+ *
35
+ * 계약전력(`contract.kW`)과 같은 자리(수전)에 선언한다.
36
+ */
37
+ /** 기본요금 단가 — 최대수요 1kW 당(청구 주기 기준). */
38
+ demandChargePerKW: 'tariff.demandChargePerKW',
39
+ /** 사용량 단가 — 1kWh 당. */
40
+ energyChargePerKWh: 'tariff.energyChargePerKWh',
41
+ /** 통화 — ISO 4217 코드(USD·KRW…). 없으면 금액에 단위를 붙이지 않는다. */
42
+ currency: 'tariff.currency'
24
43
  };
25
44
  export const EMS_TYPES = [
26
45
  /* ── 자리: 전기적 구간 ─────────────────────────────────────────────────── */
@@ -221,3 +221,47 @@ export declare function energyOfWindows(windows: readonly {
221
221
  startMs: number;
222
222
  endMs: number;
223
223
  }): WindowedEnergy;
224
+ /** 선언된 단가 — 없는 항목은 계산하지 않는다(0 으로 두지 않는다). */
225
+ export interface TariffDeclaration {
226
+ /** 최대수요 1kW 당 기본요금. */
227
+ demandChargePerKW?: number;
228
+ /** 1kWh 당 사용량 요금. */
229
+ energyChargePerKWh?: number;
230
+ /** ISO 4217 통화 코드. 없으면 금액에 단위를 붙일 수 없다. */
231
+ currency?: string;
232
+ }
233
+ /**
234
+ * 이 창의 전기요금 — **청구서가 아니다.**
235
+ *
236
+ * ── 무엇을 계산하나 ─────────────────────────────────────────────────────────
237
+ * · 사용량 부분: `kWh × 단가`. 이것은 창에 비례하므로 창을 잘라도 뜻이 유지된다.
238
+ * · 기본요금 부분: `최대수요 × 단가`. **이것은 창의 값이 아니다** — 기본요금은 청구 주기(보통 한 달)의
239
+ * 최고 수요로 매겨진다. 그래서 이 값의 뜻은 「이 창의 최대수요가 그 주기의 최고로 남는다면 기본요금이
240
+ * 이만큼」이고, 그 조건을 `demandBasis` 로 함께 낸다. 조건을 감추면 창을 좁힌 사람이 요금이 줄었다고
241
+ * 읽는다(실제로는 같은 달의 다른 시각에 더 높은 피크가 있을 수 있다).
242
+ *
243
+ * ── 세우지 않은 것 ──────────────────────────────────────────────────────────
244
+ * 시간대 요금(TOU)·계절 요금·래칫(직전 개월의 피크를 끌고 가는 구조)·최소 청구액·역률 요금은
245
+ * **모델에 없다.** 있는 척하지 않는다 — 그것들이 필요한 현장에서는 이 값이 실 청구서와 다르고,
246
+ * 그 사실을 아는 편이 그럴듯한 총액보다 낫다.
247
+ */
248
+ export type ElectricityCost = {
249
+ /** 사용량 요금 — 선언과 전력량이 다 있을 때만. */
250
+ energyCharge?: number;
251
+ /** 기본요금 — 선언과 최대수요가 다 있을 때만. */
252
+ demandCharge?: number;
253
+ /** 위 둘의 합. 둘 중 하나만 있으면 그 하나다(없는 쪽을 0 으로 세지 않는다). */
254
+ total?: number;
255
+ currency?: string;
256
+ /** 기본요금이 성립하는 조건 — 창의 피크가 청구 주기의 최고일 때. */
257
+ demandBasis?: 'window-peak-as-period-peak';
258
+ /** 세우지 않은 요금 요소 — 소비처가 「이 값이 청구서가 아니다」를 말할 수 있게. */
259
+ notModeled: readonly string[];
260
+ } | {
261
+ reason: 'no-tariff';
262
+ };
263
+ export declare function electricityCost(input: {
264
+ kWh?: number;
265
+ peakKW?: number;
266
+ tariff?: TariffDeclaration;
267
+ }): ElectricityCost;
@@ -239,3 +239,27 @@ export function energyOfWindows(windows, range) {
239
239
  ...(first !== undefined && last !== undefined ? { window: { startMs: first, endMs: last } } : {})
240
240
  };
241
241
  }
242
+ const NOT_MODELED = ['tou', 'seasonal', 'ratchet', 'minimum-bill', 'power-factor'];
243
+ export function electricityCost(input) {
244
+ const t = input.tariff;
245
+ const eRate = Number(t?.energyChargePerKWh);
246
+ const dRate = Number(t?.demandChargePerKW);
247
+ const hasE = Number.isFinite(eRate) && eRate > 0;
248
+ const hasD = Number.isFinite(dRate) && dRate > 0;
249
+ /* 단가가 하나도 없으면 금액을 만들지 않는다 — 이유를 낸다(빈 값과 0 원을 구별할 수 있게). */
250
+ if (!hasE && !hasD)
251
+ return { reason: 'no-tariff' };
252
+ const kWh = Number(input.kWh);
253
+ const peakKW = Number(input.peakKW);
254
+ const energyCharge = hasE && Number.isFinite(kWh) ? kWh * eRate : undefined;
255
+ const demandCharge = hasD && Number.isFinite(peakKW) ? peakKW * dRate : undefined;
256
+ /* 반올림하지 않는다 — 표현은 화면의 일이고, 여기서 깎으면 합과 부분이 어긋난다. */
257
+ const total = energyCharge === undefined && demandCharge === undefined ? undefined : (energyCharge ?? 0) + (demandCharge ?? 0);
258
+ return {
259
+ ...(energyCharge !== undefined ? { energyCharge } : {}),
260
+ ...(demandCharge !== undefined ? { demandCharge, demandBasis: 'window-peak-as-period-peak' } : {}),
261
+ ...(total !== undefined ? { total } : {}),
262
+ ...(t?.currency ? { currency: String(t.currency) } : {}),
263
+ notModeled: NOT_MODELED
264
+ };
265
+ }
@@ -589,7 +589,9 @@ export class FlowEngine {
589
589
  사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
590
590
  ...(o.allocated?.length ? { allocated: o.allocated } : {}),
591
591
  ...(o.gtin ? { gtin: o.gtin } : {}),
592
- ...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {})
592
+ ...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
593
+ ...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
594
+ ...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
593
595
  }));
594
596
  for (const o of observedOrders) {
595
597
  const lines = (o.lines ?? []).map(l => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter(l => l.requested > 0);
@@ -614,7 +616,11 @@ export class FlowEngine {
614
616
  ...(o.held ? { held: true } : {}),
615
617
  ...(o.endTime ? { endTime: o.endTime } : {}),
616
618
  ...(o.startTime ? { startTime: o.startTime } : {}),
617
- ...(o.priority !== undefined ? { priority: o.priority } : {})
619
+ ...(o.priority !== undefined ? { priority: o.priority } : {}),
620
+ /* 약속해 둔 자리·시각창을 이어받는다 — 없으면 도메인이 「정해 뒀다」고 믿는 것을 잃는다.
621
+ 사실이 말해 주지 않으면(옛 저널) 비는 것이 사실이다 — 지어내지 않는다. */
622
+ ...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
623
+ ...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
618
624
  });
619
625
  }
620
626
  /* 작업이 딛고 설 오더를 먼저 세운다 — 순서가 뒤바뀌면 아래 확인이 언제나 "없다" 로 답한다. */
@@ -955,6 +961,9 @@ export class FlowEngine {
955
961
  /* 진행 중 확보분과 거래번호 — **스냅샷에도** 실어야 한다. 델타에만 실으면 스냅샷으로
956
962
  재기동하는 경로(체크포인트)에서 그대로 잃는다(같은 사실을 두 길로 나르는 값이다). */
957
963
  ...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
964
+ /* 스냅샷도 함께 나른다 — 델타·스냅샷·복원 **세 길** 중 하나만 빠져도 재기동이 잃는다. */
965
+ ...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
966
+ ...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {}),
958
967
  ...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
959
968
  held: o.held
960
969
  })),
@@ -1660,7 +1669,10 @@ export class FlowEngine {
1660
1669
  계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
1661
1670
  ...(o.gtin ? { gtin: o.gtin } : {}),
1662
1671
  ...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
1663
- ...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {})
1672
+ ...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
1673
+ /* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
1674
+ ...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
1675
+ ...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
1664
1676
  });
1665
1677
  }
1666
1678
  // ── 내부 mechanics ─────────────────────────────────────────────────────────
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@ export { YmsKernel } from './yms-kernel.ts';
30
30
  export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS, MES_PRODUCTS } from './mes-kernel.ts';
31
31
  export { EmsKernel, DEMAND_WINDOW_MS, demandWindowStart } from './ems-kernel.ts';
32
32
  export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord } from './energy-ingest.ts';
33
- export { attributeEnergy, energyIntensity, energyOfWindows } from './energy-attribution.ts';
34
- export type { AttributionBasis, AttributionResult, EnergyConsumer, EnergyPool, EnergyShare, IntensityInput, IntensityResult, IntensityDenominator, WeightKind, WindowedEnergy } from './energy-attribution.ts';
33
+ export { attributeEnergy, electricityCost, energyIntensity, energyOfWindows } from './energy-attribution.ts';
34
+ export type { AttributionBasis, AttributionResult, ElectricityCost, EnergyConsumer, EnergyPool, EnergyShare, IntensityInput, IntensityResult, IntensityDenominator, TariffDeclaration, WeightKind, WindowedEnergy } from './energy-attribution.ts';
35
35
  export type { EnergyRecord, EnergyEquipmentRecord, EnergyIngestOptions, EnergyIngestResult } from './energy-ingest.ts';
36
36
  export * from './vocabulary.ts';
package/dist/index.js CHANGED
@@ -30,6 +30,6 @@ export { YmsKernel } from "./yms-kernel.js";
30
30
  export { MesKernel, MES_PART_GTINS, MES_PRODUCT_GTINS, MES_PRODUCTS } from "./mes-kernel.js";
31
31
  export { EmsKernel, DEMAND_WINDOW_MS, demandWindowStart } from "./ems-kernel.js";
32
32
  export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord } from "./energy-ingest.js";
33
- export { attributeEnergy, energyIntensity, energyOfWindows } from "./energy-attribution.js";
33
+ export { attributeEnergy, electricityCost, energyIntensity, energyOfWindows } from "./energy-attribution.js";
34
34
  /* 에너지 상태 타입은 **계약**에 있다(상태의 모양은 계약이다) — contract 의 `export *` 가 이미 낸다. */
35
35
  export * from "./vocabulary.js";
@@ -357,6 +357,10 @@ export class ObservedReducer {
357
357
  ...(d.gtin ? { gtin: d.gtin } : {}),
358
358
  ...(d.allocated?.length ? { allocated: d.allocated.slice() } : {}),
359
359
  ...(d.bizTransaction ? { bizTransaction: d.bizTransaction } : {}),
360
+ /* 약속해 둔 자리·시각창도 채운다 — 시뮬만 알면 미러 위 예측이 「어디로 들일지」를 모른다.
361
+ (이 필드가 어느 길에서 빠지면 무슨 일이 나는지는 §dockDoor 에 적혀 있다.) */
362
+ ...(d.dockDoor ? { dockDoor: d.dockDoor } : {}),
363
+ ...(d.windowStartMs !== undefined ? { windowStartMs: d.windowStartMs } : {}),
360
364
  held: d.held
361
365
  });
362
366
  break;
@@ -8,6 +8,8 @@ export declare class YmsKernel extends FlowEngine {
8
8
  private cargoSeq;
9
9
  private trailerCargo;
10
10
  private apptMode;
11
+ /** 도어를 못 찾아 대기시킨 오더 — 같은 말을 매 틱 반복하지 않기 위한 표시(§allocate). */
12
+ private doorWarned;
11
13
  /** 야드 운영 정책(상하차 방식). 기본 drop(야드 버퍼). live 는 게이트/도크 직행. */
12
14
  mode: YardMode;
13
15
  constructor(tenantId: string, policy?: AllocationPolicy, mode?: YardMode);
@@ -39,6 +39,8 @@ export class YmsKernel extends FlowEngine {
39
39
  cargoSeq = 0;
40
40
  trailerCargo = new Map(); // 트레일러 epc → 화물 SSCC(하차 전 opaque / 상차 예약분)
41
41
  apptMode = new Map(); // orderId → 상하차 모드(생성 시 캡처)
42
+ /** 도어를 못 찾아 대기시킨 오더 — 같은 말을 매 틱 반복하지 않기 위한 표시(§allocate). */
43
+ doorWarned = new Set();
42
44
  /** 야드 운영 정책(상하차 방식). 기본 drop(야드 버퍼). live 는 게이트/도크 직행. */
43
45
  mode;
44
46
  constructor(tenantId, policy = firstFitPolicy, mode = 'drop') {
@@ -102,7 +104,25 @@ export class YmsKernel extends FlowEngine {
102
104
  return;
103
105
  if (this.clockMs < (o.windowStartMs ?? 0))
104
106
  return; // 창 도래 전
105
- const door = this.locations.get(o.dockDoor);
107
+ /*
108
+ * ── 배정된 도어가 없으면 **기다린다** (2026-08-17) ────────────────────────
109
+ *
110
+ * 예전에는 `this.locations.get(o.dockDoor!)!` 였다. 그래서 배정을 모르는 오더가 들어오면 바로
111
+ * 아래에서 `undefined.occupancy` 를 읽고 **틱 전체가 예외로 죽었다** — 야드 트윈 셋이 재기동마다
112
+ * 그렇게 멈췄다(안전망이 세운 것이지 스스로 멈춘 것이 아니다).
113
+ *
114
+ * 이제 그 오더만 대기로 남기고 **왜인지 한 번 말한다.** 매 틱 말하면 로그가 그것만 남으므로 한 번만
115
+ * 말하고, 조용히 넘기지도 않는다 — 조용하면 「왜 이 오더는 영원히 안 움직이나」를 아무도 답할 수 없다.
116
+ */
117
+ const door = o.dockDoor ? this.locations.get(o.dockDoor) : undefined;
118
+ if (!door) {
119
+ if (!this.doorWarned.has(o.id)) {
120
+ this.doorWarned.add(o.id);
121
+ console.warn(`[yms-kernel] order "${o.id}" waits: ${o.dockDoor ? `its dock door "${o.dockDoor}" is not in this model` : 'no dock door was assigned to it'}` +
122
+ ' — the appointment cannot be scheduled until the declaration says where.');
123
+ }
124
+ return;
125
+ }
106
126
  const inflight = [...this.tasks.values()].some(t => t.status !== 'completed' && t.toNode === door.id);
107
127
  if (door.occupancy > 0 || inflight)
108
128
  return; // 도어 사용중/예약됨 → 대기(직렬화)
@@ -96,6 +96,7 @@ __export(index_exports, {
96
96
  documentPath: () => documentPath,
97
97
  dueStatusOf: () => dueStatusOf,
98
98
  effectivityAt: () => effectivityAt,
99
+ electricityCost: () => electricityCost,
99
100
  energyIntensity: () => energyIntensity,
100
101
  energyOfWindows: () => energyOfWindows,
101
102
  fefoPolicy: () => fefoPolicy,
@@ -1354,6 +1355,10 @@ var ObservedReducer = class {
1354
1355
  ...d.gtin ? { gtin: d.gtin } : {},
1355
1356
  ...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
1356
1357
  ...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
1358
+ /* 약속해 둔 자리·시각창도 채운다 — 시뮬만 알면 미러 위 예측이 「어디로 들일지」를 모른다.
1359
+ (이 필드가 어느 길에서 빠지면 무슨 일이 나는지는 §dockDoor 에 적혀 있다.) */
1360
+ ...d.dockDoor ? { dockDoor: d.dockDoor } : {},
1361
+ ...d.windowStartMs !== void 0 ? { windowStartMs: d.windowStartMs } : {},
1357
1362
  held: d.held
1358
1363
  });
1359
1364
  break;
@@ -1679,7 +1684,26 @@ var EMS_PROPERTY = {
1679
1684
  /** 가동 중 소비(kW). */
1680
1685
  ratedKW: "power.ratedKW",
1681
1686
  /** 멈춰 있을 때의 소비(kW). 없으면 멈춘 동안을 **비운다** — 0 이라고 주장하지 않는다. */
1682
- standbyKW: "power.standbyKW"
1687
+ standbyKW: "power.standbyKW",
1688
+ /*
1689
+ * ── 요금 단가 — 수를 금액으로 바꾸는 선언 (2026-08-17) ─────────────────────
1690
+ *
1691
+ * 그동안 트윈은 「최대수요 328kW」·「전력량 1,547kWh」까지 말하고 멈췄다. 그런데 피크를 깎는 일이
1692
+ * 돈이 되는 이유는 **요금이 둘로 나뉘기** 때문이다: 사용량(kWh)에 붙는 요금과 **최대수요(kW)에 붙는
1693
+ * 기본요금**. 단가가 없으면 그 절반을 말할 수 없고, 그래서 「피크를 깎아 얼마를 아끼나」에 답하지 못했다.
1694
+ *
1695
+ * 단가는 **현장의 계약**이다(같은 나라 안에서도 사업자·요금제마다 다르다). 그래서 기본값을 두지
1696
+ * 않는다 — 선언하지 않으면 금액을 계산하지 않는다. 짐작한 단가로 낸 금액은 숫자가 있다는 것만으로
1697
+ * 사람을 결정으로 밀어붙인다.
1698
+ *
1699
+ * 계약전력(`contract.kW`)과 같은 자리(수전)에 선언한다.
1700
+ */
1701
+ /** 기본요금 단가 — 최대수요 1kW 당(청구 주기 기준). */
1702
+ demandChargePerKW: "tariff.demandChargePerKW",
1703
+ /** 사용량 단가 — 1kWh 당. */
1704
+ energyChargePerKWh: "tariff.energyChargePerKWh",
1705
+ /** 통화 — ISO 4217 코드(USD·KRW…). 없으면 금액에 단위를 붙이지 않는다. */
1706
+ currency: "tariff.currency"
1683
1707
  };
1684
1708
  var EMS_TYPES = [
1685
1709
  /* ── 자리: 전기적 구간 ─────────────────────────────────────────────────── */
@@ -3219,7 +3243,9 @@ var FlowEngine = class {
3219
3243
  사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
3220
3244
  ...o.allocated?.length ? { allocated: o.allocated } : {},
3221
3245
  ...o.gtin ? { gtin: o.gtin } : {},
3222
- ...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}
3246
+ ...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
3247
+ ...o.dockDoor ? { dockDoor: o.dockDoor } : {},
3248
+ ...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
3223
3249
  }));
3224
3250
  for (const o of observedOrders) {
3225
3251
  const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
@@ -3246,7 +3272,11 @@ var FlowEngine = class {
3246
3272
  ...o.held ? { held: true } : {},
3247
3273
  ...o.endTime ? { endTime: o.endTime } : {},
3248
3274
  ...o.startTime ? { startTime: o.startTime } : {},
3249
- ...o.priority !== void 0 ? { priority: o.priority } : {}
3275
+ ...o.priority !== void 0 ? { priority: o.priority } : {},
3276
+ /* 약속해 둔 자리·시각창을 이어받는다 — 없으면 도메인이 「정해 뒀다」고 믿는 것을 잃는다.
3277
+ 사실이 말해 주지 않으면(옛 저널) 비는 것이 사실이다 — 지어내지 않는다. */
3278
+ ...o.dockDoor ? { dockDoor: o.dockDoor } : {},
3279
+ ...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
3250
3280
  });
3251
3281
  }
3252
3282
  const seededOrderIds = new Set(this.orders.keys());
@@ -3562,6 +3592,9 @@ var FlowEngine = class {
3562
3592
  /* 진행 중 확보분과 거래번호 — **스냅샷에도** 실어야 한다. 델타에만 실으면 스냅샷으로
3563
3593
  재기동하는 경로(체크포인트)에서 그대로 잃는다(같은 사실을 두 길로 나르는 값이다). */
3564
3594
  ...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
3595
+ /* 스냅샷도 함께 나른다 — 델타·스냅샷·복원 **세 길** 중 하나만 빠져도 재기동이 잃는다. */
3596
+ ...o.dockDoor ? { dockDoor: o.dockDoor } : {},
3597
+ ...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {},
3565
3598
  ...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
3566
3599
  held: o.held
3567
3600
  })),
@@ -4208,7 +4241,10 @@ var FlowEngine = class {
4208
4241
  계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
4209
4242
  ...o.gtin ? { gtin: o.gtin } : {},
4210
4243
  ...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
4211
- ...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}
4244
+ ...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
4245
+ /* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
4246
+ ...o.dockDoor ? { dockDoor: o.dockDoor } : {},
4247
+ ...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
4212
4248
  });
4213
4249
  }
4214
4250
  // ── 내부 mechanics ─────────────────────────────────────────────────────────
@@ -5300,6 +5336,8 @@ var YmsKernel = class extends FlowEngine {
5300
5336
  // 트레일러 epc → 화물 SSCC(하차 전 opaque / 상차 예약분)
5301
5337
  apptMode = /* @__PURE__ */ new Map();
5302
5338
  // orderId → 상하차 모드(생성 시 캡처)
5339
+ /** 도어를 못 찾아 대기시킨 오더 — 같은 말을 매 틱 반복하지 않기 위한 표시(§allocate). */
5340
+ doorWarned = /* @__PURE__ */ new Set();
5303
5341
  /** 야드 운영 정책(상하차 방식). 기본 drop(야드 버퍼). live 는 게이트/도크 직행. */
5304
5342
  mode;
5305
5343
  constructor(tenantId, policy = firstFitPolicy, mode = "drop") {
@@ -5371,7 +5409,16 @@ var YmsKernel = class extends FlowEngine {
5371
5409
  const trailer = this.items.get(o.allocated[0]);
5372
5410
  if (!trailer) return;
5373
5411
  if (this.clockMs < (o.windowStartMs ?? 0)) return;
5374
- const door = this.locations.get(o.dockDoor);
5412
+ const door = o.dockDoor ? this.locations.get(o.dockDoor) : void 0;
5413
+ if (!door) {
5414
+ if (!this.doorWarned.has(o.id)) {
5415
+ this.doorWarned.add(o.id);
5416
+ console.warn(
5417
+ `[yms-kernel] order "${o.id}" waits: ${o.dockDoor ? `its dock door "${o.dockDoor}" is not in this model` : "no dock door was assigned to it"} \u2014 the appointment cannot be scheduled until the declaration says where.`
5418
+ );
5419
+ }
5420
+ return;
5421
+ }
5375
5422
  const inflight = [...this.tasks.values()].some((t) => t.status !== "completed" && t.toNode === door.id);
5376
5423
  if (door.occupancy > 0 || inflight) return;
5377
5424
  const mode = this.apptMode.get(o.id) ?? "drop";
@@ -6441,6 +6488,27 @@ function energyOfWindows(windows, range) {
6441
6488
  ...first !== void 0 && last !== void 0 ? { window: { startMs: first, endMs: last } } : {}
6442
6489
  };
6443
6490
  }
6491
+ var NOT_MODELED = ["tou", "seasonal", "ratchet", "minimum-bill", "power-factor"];
6492
+ function electricityCost(input) {
6493
+ const t = input.tariff;
6494
+ const eRate = Number(t?.energyChargePerKWh);
6495
+ const dRate = Number(t?.demandChargePerKW);
6496
+ const hasE = Number.isFinite(eRate) && eRate > 0;
6497
+ const hasD = Number.isFinite(dRate) && dRate > 0;
6498
+ if (!hasE && !hasD) return { reason: "no-tariff" };
6499
+ const kWh = Number(input.kWh);
6500
+ const peakKW = Number(input.peakKW);
6501
+ const energyCharge = hasE && Number.isFinite(kWh) ? kWh * eRate : void 0;
6502
+ const demandCharge = hasD && Number.isFinite(peakKW) ? peakKW * dRate : void 0;
6503
+ const total = energyCharge === void 0 && demandCharge === void 0 ? void 0 : (energyCharge ?? 0) + (demandCharge ?? 0);
6504
+ return {
6505
+ ...energyCharge !== void 0 ? { energyCharge } : {},
6506
+ ...demandCharge !== void 0 ? { demandCharge, demandBasis: "window-peak-as-period-peak" } : {},
6507
+ ...total !== void 0 ? { total } : {},
6508
+ ...t?.currency ? { currency: String(t.currency) } : {},
6509
+ notModeled: NOT_MODELED
6510
+ };
6511
+ }
6444
6512
 
6445
6513
  // src/vocabulary.ts
6446
6514
  var RETIRED_VOCABULARY = ["mover", "Mover", "MOVER", "node", "Node", "NODE"];
@@ -6563,6 +6631,7 @@ function retiredVocabularyIn(line) {
6563
6631
  documentPath,
6564
6632
  dueStatusOf,
6565
6633
  effectivityAt,
6634
+ electricityCost,
6566
6635
  energyIntensity,
6567
6636
  energyOfWindows,
6568
6637
  fefoPolicy,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/twin-kernel",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
4
4
  "type": "module",
5
5
  "description": "Twin Domain Kernel — framework-agnostic, zero-dep (domain + sim + 3-channel contract). WMS/YMS/MES, EPCIS 2.0 · ISA-95.",
6
6
  "publishConfig": {