@operato/twin-kernel 0.7.39 → 0.7.40

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.
@@ -19,7 +19,7 @@
19
19
  * - 운영 델타(task/equipment/order.status) → tasks·equipment·orders (EPCIS 로 재구성 불가한 절반)
20
20
  * 마스터(로케이션)는 board 초기화 + applyMaster 로 갱신(마스터 동기).
21
21
  */
22
- import { OP_EVENT, capabilityOf, requiredTestsFor, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, effectivityAt, offCalendarAt, offCalendarReasonAt, activeShiftAt } from "./contract.js";
22
+ import { OP_EVENT, capabilityOf, itemKeyOf, requiredTestsFor, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, effectivityAt, offCalendarAt, offCalendarReasonAt, activeShiftAt } from "./contract.js";
23
23
  import { ILMD_ATTR, parseEpc } from "./epcis.js";
24
24
  /**
25
25
  * 투영이 들고 있는 물품 — **계약(ItemState)을 축소하지 않는다.**
@@ -364,6 +364,8 @@ export class ObservedReducer {
364
364
  /* 진행 중 확보분과 거래번호 — **미러도 같은 필드를 채운다.** 시뮬만 아는 사실을 두면
365
365
  그 위에서 세운 예측이 두 구동에서 달라진다(패리티 검사가 지키는 것이 이것이다). */
366
366
  ...(d.gtin ? { gtin: d.gtin } : {}),
367
+ /* 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(같은 품목에 대체 레시피). */
368
+ ...(d.recipeKey ? { recipeKey: d.recipeKey } : {}),
367
369
  ...(d.allocated?.length ? { allocated: d.allocated.slice() } : {}),
368
370
  ...(d.bizTransaction ? { bizTransaction: d.bizTransaction } : {}),
369
371
  /* 약속해 둔 자리·시각창도 채운다 — 시뮬만 알면 미러 위 예측이 「어디로 들일지」를 모른다.
@@ -471,7 +473,7 @@ export class ObservedReducer {
471
473
  if (!cur)
472
474
  this.pendingQuantities.set(ev.parentID, [...ev.childQuantityList]);
473
475
  else {
474
- this.items.set(cur.subLotId ?? cur.epc, this.withContainedQuantity(cur, q, ev.childQuantityList));
476
+ this.items.set(itemKeyOf(cur), this.withContainedQuantity(cur, q, ev.childQuantityList));
475
477
  this.qtyAggregation.set(ev.parentID, [...ev.childQuantityList]);
476
478
  }
477
479
  }
@@ -507,7 +509,7 @@ export class ObservedReducer {
507
509
  const cur = this.items.get(ev.parentID);
508
510
  if (cur) {
509
511
  const { gtin, gtinKey, qty, uom, quantities, lot, ...rest } = cur;
510
- this.items.set(cur.subLotId ?? cur.epc, { ...rest, qty: 1 });
512
+ this.items.set(itemKeyOf(cur), { ...rest, qty: 1 });
511
513
  }
512
514
  }
513
515
  this.pendingQuantities.delete(ev.parentID);
@@ -593,7 +595,8 @@ export class ObservedReducer {
593
595
  all,
594
596
  /** 로트의 부분 식별자(표준 `MaterialSubLot.ID`) — 비직렬 로트가 자리마다 갈릴 때. */
595
597
  subLotId) {
596
- const cur = this.items.get(subLotId ?? epc);
598
+ /* 규칙은 한 곳에서만 정한다(§itemKeyOf) 여기서 다시 적으면 두 벌이 되고 한쪽만 고쳐진다. */
599
+ const cur = this.items.get(itemKeyOf({ epc, subLotId }));
597
600
  /* 클래스는 수량 목록에서 오거나, 물품 자신이 클래스 식별자일 수 있다(비직렬 입고). */
598
601
  const parsedClass = q?.epcClass ? parseEpc(q.epcClass) : undefined;
599
602
  const parsedSelf = parseEpc(epc);
@@ -15,6 +15,7 @@
15
15
  * - drop(기본): 트레일러를 야드 슬롯에 내려놓고(주차) 창 도래 시 호슬러가 도크로 pull. 야드 버퍼 사용.
16
16
  * - live: 게이트에서 대기하다 창 도래 시 도크로 직행(야드 미경유). 게이트/도크를 더 오래 점유.
17
17
  */
18
+ import { itemKeyOf } from "./contract.js";
18
19
  import { firstFitPolicy } from "./allocation-policy.js";
19
20
  import { FlowEngine } from "./flow-engine.js";
20
21
  import { DISP, objectEvent, transactionEvent, gdtiUri, ssccUri } from "./epcis.js";
@@ -33,7 +34,12 @@ const WINDOW_DELAY_MS = 40_000; // 도착 후 어포인트먼트 창까지(early
33
34
  */
34
35
  const WINDOW_LENGTH_MS = 20 * 60_000;
35
36
  const CARGO_PER_TRAILER = 2; // 트레일러 적재/하역 화물(SSCC) 수
36
- const CP = '0614141';
37
+ /**
38
+ * 이 내장 시나리오가 쓰는 **날조된 회사 프리픽스** — 선언이 없는 트윈만 이 길로 온다.
39
+ * 근거·처분은 WMS 커널의 같은 상수 주석에 있다(`kernel.ts`). 이름의 `LEGACY_` 는 새 코드가 무심코
40
+ * 집어 들지 못하게 하는 표시다 — 이 상수를 지나는 트윈은 `fabricated` 로 판정된다.
41
+ */
42
+ const LEGACY_CP = '0614141';
37
43
  export class YmsKernel extends FlowEngine {
38
44
  doorRR = 0;
39
45
  cargoSeq = 0;
@@ -58,19 +64,19 @@ export class YmsKernel extends FlowEngine {
58
64
  if (!gate || doors.length === 0)
59
65
  return;
60
66
  const inbound = kind === 'appointment';
61
- const epc = graiUri(CP, '10', ++this.epcSeq);
67
+ const epc = graiUri(LEGACY_CP, '10', ++this.epcSeq);
62
68
  this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
63
69
  gate.occupancy++;
64
70
  this.emit(objectEvent({ eventTime: this.now(), action: 'ADD', bizStep: YARD_BIZSTEP.arriving, disposition: DISP.in_progress, epcList: [epc], readPoint: gate.id, bizLocation: gate.id }));
65
71
  // 인바운드: 화물 적재된 채 도착 → 조립(트레일러←화물) 기록. 아웃바운드: 빈 트레일러(화물은 도크에서 적재).
66
72
  if (inbound) {
67
- const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(CP, ++this.cargoSeq));
73
+ const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(LEGACY_CP, ++this.cargoSeq));
68
74
  this.trailerCargo.set(epc, cargo);
69
75
  this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id }); // 적재된 채 도착(자식 opaque, 미materialize)
70
76
  }
71
77
  const door = doors[this.doorRR++ % doors.length];
72
78
  const id = `order-${++this.orderSeq}`;
73
- const appt = gdtiUri(CP, '404', ++this.soSeq);
79
+ const appt = gdtiUri(LEGACY_CP, '404', ++this.soSeq);
74
80
  const order = { id, kind, status: 'created', requested: 1, fulfilled: 0, bizTransaction: appt, allocated: [epc], picked: [], dockDoor: door.id,
75
81
  /* 내부 스케줄 게이트(시뮬 ms) — 표준 `EarliestStartTime` 과 같은 개념의 우리 단위. */
76
82
  windowStartMs: this.clockMs + WINDOW_DELAY_MS,
@@ -163,7 +169,8 @@ export class YmsKernel extends FlowEngine {
163
169
  const to = this.locations.get(t.toNode);
164
170
  from.occupancy--;
165
171
  to.occupancy++;
166
- trailer.location = to.id;
172
+ /* 자리 이동은 저장소 통로로 — 직접 고치면 자리별 색인이 어긋난다. */
173
+ this.items.relocate(itemKeyOf(trailer), to.id);
167
174
  if (t.kind === 'dwell') {
168
175
  // 도크 체류(무자원 dwell) 완료 → 출차 이동(transport). 체류 중 호슬러 미점유(drop-and-hook 충실).
169
176
  const gate = this.locationByType('gate');