@operato/twin-kernel 0.7.39 → 0.7.41
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.
- package/dist/contract.d.ts +176 -10
- package/dist/contract.js +46 -5
- package/dist/domain-catalog.d.ts +1 -1
- package/dist/domain-catalog.js +1 -1
- package/dist/domain-definition.d.ts +17 -0
- package/dist/domain-definition.js +7 -0
- package/dist/ems-kernel.js +13 -1
- package/dist/epcis.d.ts +22 -0
- package/dist/epcis.js +91 -2
- package/dist/face2-adapter.d.ts +21 -1
- package/dist/face2-adapter.js +12 -1
- package/dist/flow-engine.d.ts +156 -8
- package/dist/flow-engine.js +324 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/kernel.d.ts +15 -0
- package/dist/kernel.js +50 -16
- package/dist/mes-kernel.d.ts +117 -27
- package/dist/mes-kernel.js +354 -170
- package/dist/observed-reducer.d.ts +1 -1
- package/dist/observed-reducer.js +7 -4
- package/dist/yms-kernel.js +12 -5
- package/dist-cjs/index.cjs +648 -187
- package/package.json +1 -1
package/dist/flow-engine.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 통합 타입은 도메인 필드를 옵셔널로 넓혀(FlowItem.gtin?, FlowOrder.shipmentEpc? 등) 두 도메인을 담는다.
|
|
10
10
|
* (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowLocation 단일 base 방향과 정합.)
|
|
11
11
|
*/
|
|
12
|
-
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf } from "./contract.js";
|
|
12
|
+
import { OP_EVENT, CMD, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf } from "./contract.js";
|
|
13
13
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
14
14
|
import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP } from "./epcis.js";
|
|
15
15
|
import { OP_PARAM } from "./domain-definition.js";
|
|
@@ -282,10 +282,151 @@ function energyFieldsOf(m) {
|
|
|
282
282
|
}
|
|
283
283
|
return out;
|
|
284
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* 물품 저장소 — **맵과 「자리별 색인」을 함께 든다.**
|
|
287
|
+
*
|
|
288
|
+
* ── 왜 클래스인가 (2026-08-21) ──────────────────────────────────────────────
|
|
289
|
+
* 자재를 확보할 때(`claimMaterials`) 「그 자리에 있는 물품」만 필요한데, 요구 한 줄마다 **물품 전체를
|
|
290
|
+
* 순회**하고 있었다. 규모 기준선이 그 값을 보인다: 물품이 15배 늘 때 주문당 비용이 3~4배가 된다
|
|
291
|
+
* (`test/order-scale-baseline.test.ts`). 목표 규모는 물품 10만이다.
|
|
292
|
+
*
|
|
293
|
+
* 색인을 따로 들면 갱신하는 자리가 흩어지고, 한 자리라도 빠뜨리면 **재고가 조용히 사라진다**(그 자리에
|
|
294
|
+
* 있는데 색인에 없으면 작업이 영원히 자재를 기다린다). 그래서 맵과 색인을 **한 객체**에 담아, 물품을
|
|
295
|
+
* 바꾸는 길이 이 클래스뿐이게 한다. 자리를 옮기는 것도 여기 통로가 있다(`relocate`) — 물품 객체의
|
|
296
|
+
* `location` 을 직접 고치면 색인이 어긋나므로, 그 규율은 시험이 지킨다.
|
|
297
|
+
*
|
|
298
|
+
* Map 과 같은 이름들을 그대로 낸다(`get`·`set`·`delete`·`has`·`values`·`clear`·순회) — 소비처를 고치지
|
|
299
|
+
* 않고 색인을 얻기 위해서다.
|
|
300
|
+
*/
|
|
301
|
+
export class ItemStore {
|
|
302
|
+
map = new Map();
|
|
303
|
+
byLocation = new Map();
|
|
304
|
+
get size() {
|
|
305
|
+
return this.map.size;
|
|
306
|
+
}
|
|
307
|
+
get(key) {
|
|
308
|
+
return this.map.get(key);
|
|
309
|
+
}
|
|
310
|
+
has(key) {
|
|
311
|
+
return this.map.has(key);
|
|
312
|
+
}
|
|
313
|
+
values() {
|
|
314
|
+
return this.map.values();
|
|
315
|
+
}
|
|
316
|
+
keys() {
|
|
317
|
+
return this.map.keys();
|
|
318
|
+
}
|
|
319
|
+
entries() {
|
|
320
|
+
return this.map.entries();
|
|
321
|
+
}
|
|
322
|
+
[Symbol.iterator]() {
|
|
323
|
+
return this.map[Symbol.iterator]();
|
|
324
|
+
}
|
|
325
|
+
forEach(fn) {
|
|
326
|
+
this.map.forEach((v, k) => fn(v, k));
|
|
327
|
+
}
|
|
328
|
+
set(key, item) {
|
|
329
|
+
const prev = this.map.get(key);
|
|
330
|
+
if (prev)
|
|
331
|
+
this.unindex(key, prev.location);
|
|
332
|
+
this.map.set(key, item);
|
|
333
|
+
this.index(key, item.location);
|
|
334
|
+
return this;
|
|
335
|
+
}
|
|
336
|
+
delete(key) {
|
|
337
|
+
const prev = this.map.get(key);
|
|
338
|
+
if (prev)
|
|
339
|
+
this.unindex(key, prev.location);
|
|
340
|
+
return this.map.delete(key);
|
|
341
|
+
}
|
|
342
|
+
clear() {
|
|
343
|
+
this.map.clear();
|
|
344
|
+
this.byLocation.clear();
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* 물품을 다른 자리로 옮긴다 — **색인이 함께 움직이는 유일한 통로.**
|
|
348
|
+
*
|
|
349
|
+
* 없는 물품을 옮기라는 요청은 조용히 넘기지 않는다: 그 요청을 낸 쪽의 계산이 이미 어긋난 것이고,
|
|
350
|
+
* 지나가면 그 뒤의 이동·처분이 엉뚱한 자리에 적힌다.
|
|
351
|
+
*/
|
|
352
|
+
relocate(key, to) {
|
|
353
|
+
const it = this.map.get(key);
|
|
354
|
+
if (!it)
|
|
355
|
+
throw new Error(`relocate: 물품 '${key}' 이 상태에 없습니다 — 없는 것을 옮길 수 없습니다`);
|
|
356
|
+
if (it.location !== to) {
|
|
357
|
+
this.unindex(key, it.location);
|
|
358
|
+
it.location = to;
|
|
359
|
+
this.index(key, to);
|
|
360
|
+
}
|
|
361
|
+
return it;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* 사본 — **자기 복제 방법을 스스로 안다.**
|
|
365
|
+
*
|
|
366
|
+
* `fork()` 는 `Map` 만 알고 나머지는 `structuredClone` 으로 복제한다. 그 함수는 클래스의 메서드를
|
|
367
|
+
* 복제하지 못하므로(평범한 객체가 된다), 저장소가 복제를 맡지 않으면 사본의 물품 조회가 통째로
|
|
368
|
+
* 깨진다 — 실제로 그렇게 42건이 붉어졌다. 복제 방법을 아는 객체가 스스로 복제한다.
|
|
369
|
+
*/
|
|
370
|
+
clone() {
|
|
371
|
+
const out = new ItemStore();
|
|
372
|
+
for (const [k, it] of this.map)
|
|
373
|
+
out.set(k, structuredClone(it));
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
/** 그 자리에 있는 물품들 — 색인이 답한다(전체 순회가 아니다). */
|
|
377
|
+
at(location) {
|
|
378
|
+
const keys = this.byLocation.get(location);
|
|
379
|
+
if (!keys)
|
|
380
|
+
return [];
|
|
381
|
+
const out = [];
|
|
382
|
+
for (const k of keys) {
|
|
383
|
+
const it = this.map.get(k);
|
|
384
|
+
if (it)
|
|
385
|
+
out.push(it);
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* 색인이 맵과 어긋난 자리 — **시험이 쓰는 확인 통로**(전체를 다시 세므로 비싸다).
|
|
391
|
+
*
|
|
392
|
+
* 어긋남은 조용한 결함이다(자재가 있는데 없다고 판정된다). 그래서 「어긋나지 않을 것이다」를 믿지 않고
|
|
393
|
+
* 시나리오를 돌린 뒤 이 값으로 확인한다.
|
|
394
|
+
*/
|
|
395
|
+
indexDrift() {
|
|
396
|
+
const drift = [];
|
|
397
|
+
for (const [key, it] of this.map) {
|
|
398
|
+
if (!this.byLocation.get(it.location)?.has(key))
|
|
399
|
+
drift.push(`${key} 이 '${it.location}' 색인에 없다`);
|
|
400
|
+
}
|
|
401
|
+
for (const [loc, keys] of this.byLocation) {
|
|
402
|
+
for (const k of keys) {
|
|
403
|
+
const it = this.map.get(k);
|
|
404
|
+
if (!it)
|
|
405
|
+
drift.push(`${k} 이 지워졌는데 '${loc}' 색인에 남아 있다`);
|
|
406
|
+
else if (it.location !== loc)
|
|
407
|
+
drift.push(`${k} 은 '${it.location}' 에 있는데 '${loc}' 색인에 있다`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return drift;
|
|
411
|
+
}
|
|
412
|
+
index(key, location) {
|
|
413
|
+
const set = this.byLocation.get(location) ?? new Set();
|
|
414
|
+
set.add(key);
|
|
415
|
+
this.byLocation.set(location, set);
|
|
416
|
+
}
|
|
417
|
+
unindex(key, location) {
|
|
418
|
+
const set = this.byLocation.get(location);
|
|
419
|
+
if (!set)
|
|
420
|
+
return;
|
|
421
|
+
set.delete(key);
|
|
422
|
+
if (!set.size)
|
|
423
|
+
this.byLocation.delete(location);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
285
426
|
export class FlowEngine {
|
|
286
427
|
tenantId;
|
|
287
428
|
locations = new Map();
|
|
288
|
-
items = new
|
|
429
|
+
items = new ItemStore();
|
|
289
430
|
equipment = new Map();
|
|
290
431
|
/** 등급 정의(표준 `<X>Class`) — 상속·유효기간 판정의 재료. 선언 안 하면 비어 있고, 소속 그대로 판정한다. */
|
|
291
432
|
classDefs = {};
|
|
@@ -349,6 +490,21 @@ export class FlowEngine {
|
|
|
349
490
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
350
491
|
observer;
|
|
351
492
|
/** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
|
|
493
|
+
/**
|
|
494
|
+
* **관측 모드에서 원본과 어긋난 횟수** — 받아들였지만 사실이 맞지 않았다.
|
|
495
|
+
*
|
|
496
|
+
* 미러는 원본을 비추는 쪽이라 어긋남을 만나도 멈추지 않는다(멈추면 원본의 한 건이 트윈 전체를
|
|
497
|
+
* 세운다). 그래서 **세어 둔다** — 세지 않으면 그 결함이 아무 데도 드러나지 않고, 화면은 어긋난 적이
|
|
498
|
+
* 없는 트윈과 구별되지 않는다.
|
|
499
|
+
*/
|
|
500
|
+
/**
|
|
501
|
+
* **씨앗이 심지 못한 참조의 수** — 원본이 말했지만 그 물품이 스냅샷에 없었다.
|
|
502
|
+
*
|
|
503
|
+
* 0 이 아니면 이 트윈의 상태는 원본의 일부를 담지 못했다는 뜻이다. 예측·집계가 그 사실을 모르면
|
|
504
|
+
* 부족한 씨앗 위에서 낸 답을 완전한 답으로 읽는다.
|
|
505
|
+
*/
|
|
506
|
+
seedDanglingRefs = 0;
|
|
507
|
+
transformInputsAbsent = 0;
|
|
352
508
|
observedDirty = false;
|
|
353
509
|
observeMode = false;
|
|
354
510
|
/** 관측 구동이 투영기를 세울 때 필요한 원본 보드(구조는 이벤트가 아니라 마스터에서 온다). */
|
|
@@ -567,6 +723,25 @@ export class FlowEngine {
|
|
|
567
723
|
if (e?.id && e.since)
|
|
568
724
|
this._attentionSince.set(e.id, e.since);
|
|
569
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* 씨앗의 확보분 중 **이 트윈에 실제로 있는 것만** 남긴다 — 없는 것은 세고 버린다.
|
|
728
|
+
*
|
|
729
|
+
* 원본이 확보분을 말했는데 그 물품이 스냅샷에 없으면, 그 참조는 심는 순간 이미 사실이 아니다.
|
|
730
|
+
* 심어 두면 나중에 틱에서 드러나고(시뮬은 없는 것을 소비하지 않으므로 멈춘다) 그 판정이 예측
|
|
731
|
+
* 경로에서 터지면 원본의 빈틈 하나가 답 전체를 서버 오류로 죽인다.
|
|
732
|
+
*
|
|
733
|
+
* 그래서 심을 때 맞춘다. **없는 물품을 만들어 채우지 않는다** — 채우면 계보와 재고가 거짓 위에 선다.
|
|
734
|
+
* 대신 몇 건이 맞지 않았는지 남겨 씨앗이 불완전했다는 사실을 답에 실을 수 있게 한다.
|
|
735
|
+
*/
|
|
736
|
+
resolvedAllocated(allocated) {
|
|
737
|
+
const all = allocated ?? [];
|
|
738
|
+
if (!all.length)
|
|
739
|
+
return { kept: [], dropped: 0 };
|
|
740
|
+
const kept = all.filter(epc => this.items.has(epc));
|
|
741
|
+
const dropped = all.length - kept.length;
|
|
742
|
+
this.seedDanglingRefs += dropped;
|
|
743
|
+
return { kept, dropped };
|
|
744
|
+
}
|
|
570
745
|
hydrateObserved(snap, orders = []) {
|
|
571
746
|
/* 확인 처리를 먼저 이어받는다 — 아래에서 상태를 주입하면 곧바로 주목 신호가 계산되므로, 늦게
|
|
572
747
|
* 이어받으면 그 한 번은 확인 안 된 것으로 계산된다(화면이 잠깐 빨개진다). */
|
|
@@ -702,10 +877,24 @@ export class FlowEngine {
|
|
|
702
877
|
사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
|
|
703
878
|
...(o.allocated?.length ? { allocated: o.allocated } : {}),
|
|
704
879
|
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
880
|
+
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
705
881
|
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
706
882
|
...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
|
|
707
883
|
...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
|
|
708
884
|
}));
|
|
885
|
+
/*
|
|
886
|
+
* ── 심을 때 참조를 맞춘다 (2026-08-21) ───────────────────────────────────
|
|
887
|
+
* 원본이 확보분을 말했는데 그 물품이 스냅샷에 없는 일이 실 연동에서 흔하다. 그 참조를 그대로
|
|
888
|
+
* 심으면 나중에 **틱에서** 드러난다 — 시뮬 규칙(없는 것을 소비하지 않는다)은 옳은데, 그 판정이
|
|
889
|
+
* 예측 경로에서 터지면 원본의 빈틈 하나가 정확도 추세 전체를 서버 오류로 죽인다(§`itemByRef` 에
|
|
890
|
+
* 같은 부류의 회귀가 적혀 있다).
|
|
891
|
+
*
|
|
892
|
+
* 그래서 판정을 **심는 시점**으로 옮긴다: 맞지 않는 참조는 심지 않고 **센다.** 그러면 시뮬의
|
|
893
|
+
* 멈추는 규칙은 그대로 남아 자기 결함을 여전히 드러내고, 원본의 빈틈은 예측을 죽이지 않으며
|
|
894
|
+
* 씨앗이 불완전했다는 사실이 답에 함께 실린다(§`StateSnapshot.conformance`).
|
|
895
|
+
*
|
|
896
|
+
* 지어내지 않는다 — 없는 물품을 만들어 채우면 그 뒤 계보와 재고가 전부 거짓 위에 선다.
|
|
897
|
+
*/
|
|
709
898
|
for (const o of observedOrders) {
|
|
710
899
|
const lines = (o.lines ?? []).map(l => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter(l => l.requested > 0);
|
|
711
900
|
/* **라인이 없는 오더를 조용히 빼지 않는다.** 예전에는 남은 양을 라인에서만 셌으므로, 품목 라인
|
|
@@ -716,6 +905,7 @@ export class FlowEngine {
|
|
|
716
905
|
: Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
717
906
|
if (remaining <= 0)
|
|
718
907
|
continue; // 이미 이행 완료 → 예측 대상 아님
|
|
908
|
+
const resolved = this.resolvedAllocated(o.allocated);
|
|
719
909
|
this.orders.set(o.orderId, {
|
|
720
910
|
id: o.orderId, kind: o.kind, status: 'created', requested: remaining, fulfilled: 0,
|
|
721
911
|
/* **확보해 둔 것과 거래번호를 이어받는다.** 비우면 되살아난 오더가 아무것도 안 잡은 것처럼
|
|
@@ -723,7 +913,11 @@ export class FlowEngine {
|
|
|
723
913
|
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
724
914
|
/* 품목도 함께 — 씨앗이 흘리면 되살아난 오더가 무엇을 만드는지 모른다. */
|
|
725
915
|
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
726
|
-
|
|
916
|
+
/* 레시피도 함께 — 품목만 이어받으면 되살아난 오더가 「무엇으로」를 모르고, 그 오더의
|
|
917
|
+
남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
|
|
918
|
+
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
919
|
+
bizTransaction: o.bizTransaction ?? '', allocated: resolved.kept, picked: [], shipmentEpc: null, lines,
|
|
920
|
+
...(resolved.dropped ? { seedIncomplete: true } : {}),
|
|
727
921
|
/* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
|
|
728
922
|
씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
|
|
729
923
|
...(o.held ? { held: true } : {}),
|
|
@@ -1030,6 +1224,14 @@ export class FlowEngine {
|
|
|
1030
1224
|
revision: this.revision,
|
|
1031
1225
|
simClockMs: this.clockMs,
|
|
1032
1226
|
nowTime: this.now(), // 트윈의 "지금" — 관측 모드면 마지막으로 들은 시각(§nowMs)
|
|
1227
|
+
identityGrounding: this.identityGroundingView(),
|
|
1228
|
+
/* 원본과 어긋난 사실 — 0 이면 싣지 않는다(어긋난 적 없는 트윈에 빈 칸을 만들지 않는다). */
|
|
1229
|
+
...(this.transformInputsAbsent || this.seedDanglingRefs
|
|
1230
|
+
? { conformance: {
|
|
1231
|
+
...(this.transformInputsAbsent ? { transformInputsAbsent: this.transformInputsAbsent } : {}),
|
|
1232
|
+
...(this.seedDanglingRefs ? { seedDanglingRefs: this.seedDanglingRefs } : {})
|
|
1233
|
+
} }
|
|
1234
|
+
: {}),
|
|
1033
1235
|
/* 출처 표시 — 보드(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
1034
1236
|
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
1035
1237
|
locations: [...this.locations.values()].map(n => {
|
|
@@ -1119,6 +1321,10 @@ export class FlowEngine {
|
|
|
1119
1321
|
* 라인이 있는 오더는 `lines[].gtin` 이 품목을 말하고, 단일 품목 작업지시는 이 자리가 말한다.
|
|
1120
1322
|
*/
|
|
1121
1323
|
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
1324
|
+
/* **어느 레시피로 만드나** — 품목만으로는 「무엇으로」가 안 남는다(같은 품목의 대체 레시피가 있다). */
|
|
1325
|
+
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1326
|
+
/* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
|
|
1327
|
+
...(o.seedIncomplete ? { seedIncomplete: true } : {}),
|
|
1122
1328
|
...(o.lines?.length ? { lines: o.lines.map(l => ({ gtin: l.gtin, requested: l.requested })) } : {}),
|
|
1123
1329
|
...(o.priority !== undefined ? { priority: o.priority } : {}),
|
|
1124
1330
|
...(o.startTime ? { startTime: o.startTime } : {}),
|
|
@@ -1241,10 +1447,16 @@ export class FlowEngine {
|
|
|
1241
1447
|
continue;
|
|
1242
1448
|
const v = this[key];
|
|
1243
1449
|
if (typeof v === 'function')
|
|
1244
|
-
continue // rng — 아래서 state 만 복제
|
|
1245
|
-
|
|
1450
|
+
continue; // rng — 아래서 state 만 복제
|
|
1451
|
+
/* 복제 방법을 아는 객체는 스스로 복제한다(`ItemStore` 처럼 색인을 든 것) — `structuredClone` 은
|
|
1452
|
+
클래스의 메서드를 복제하지 못하므로, 여기서 그것에 맡기면 사본이 조용히 반쪽이 된다. */
|
|
1453
|
+
const cloneable = v;
|
|
1246
1454
|
clone[key] =
|
|
1247
|
-
v instanceof Map
|
|
1455
|
+
v instanceof Map
|
|
1456
|
+
? new Map([...v.entries()].map(([k, o]) => [k, structuredClone(o)]))
|
|
1457
|
+
: typeof cloneable?.clone === 'function'
|
|
1458
|
+
? cloneable.clone()
|
|
1459
|
+
: structuredClone(v);
|
|
1248
1460
|
}
|
|
1249
1461
|
clone.rng.state = this.rng.state; // RNG 연속성 → 생성 포함 결정적 분기
|
|
1250
1462
|
clone.durationEstimator = this.durationEstimator; // 무상태 시임 — 참조 공유(policy 와 동형)
|
|
@@ -1485,7 +1697,7 @@ export class FlowEngine {
|
|
|
1485
1697
|
* 이긴다(원천 사본의 값이 낡았을 때 고칠 자리가 여기다). 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
|
|
1486
1698
|
*
|
|
1487
1699
|
* ── 조용히 버리지 않는다 ────────────────────────────────────────────────────
|
|
1488
|
-
* 읽을 수 없는 값은
|
|
1700
|
+
* 읽을 수 없는 값은 오류를 낸다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
|
|
1489
1701
|
* 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
|
|
1490
1702
|
*/
|
|
1491
1703
|
declareDurations(durations) {
|
|
@@ -1515,7 +1727,7 @@ export class FlowEngine {
|
|
|
1515
1727
|
* · **현장 선언이 원천 명세를 이긴다**(ADR-0034) — 원천 사본이 낡았을 때 고칠 자리가 그것뿐이다.
|
|
1516
1728
|
* · 명세 행이 없는 종류에도 얹힌다 — 행을 지어 만들면 지어낸 `intent` 가 능력 계산을 오염시킨다.
|
|
1517
1729
|
* · 무엇을 썼는지는 `specCoverage()` 의 `parameters` 가 그대로 밝힌다.
|
|
1518
|
-
* · 빈 이름·빈 값은
|
|
1730
|
+
* · 빈 이름·빈 값은 **오류를 낸다**: 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 돈다.
|
|
1519
1731
|
*/
|
|
1520
1732
|
declareParameters(params) {
|
|
1521
1733
|
for (const [kind, entries] of Object.entries(params ?? {})) {
|
|
@@ -1542,6 +1754,17 @@ export class FlowEngine {
|
|
|
1542
1754
|
routeKeys() {
|
|
1543
1755
|
return undefined;
|
|
1544
1756
|
}
|
|
1757
|
+
/**
|
|
1758
|
+
* **이 트윈이 굴리는 서로 다른 라우트들** — 용량이 답할 수 있는지를 가른다.
|
|
1759
|
+
*
|
|
1760
|
+
* 하나면 `routeKeys()` 가 그 순서를 답한다. 둘 이상이면 「하루 몇 대」의 답이 **제품 구성에 따라
|
|
1761
|
+
* 달라지므로**, 하나를 골라 답하지 않고 서로 다르다는 사실을 `capacity()` 가 함께 낸다. 조용히 고르면
|
|
1762
|
+
* 능력 숫자가 거짓이 되고, 그 위에 선 모든 계획이 거짓 위에 선다.
|
|
1763
|
+
*/
|
|
1764
|
+
distinctRouteKeys() {
|
|
1765
|
+
const one = this.routeKeys();
|
|
1766
|
+
return one ? ['(single)'] : [];
|
|
1767
|
+
}
|
|
1545
1768
|
/**
|
|
1546
1769
|
* **이 공장이 하루 몇 대를 낼 수 있는가** — 실행해 보지 않고 답한다.
|
|
1547
1770
|
*
|
|
@@ -1570,7 +1793,8 @@ export class FlowEngine {
|
|
|
1570
1793
|
sampleWeekStartMs: opts.sampleWeekStartMs,
|
|
1571
1794
|
unitsPerDay: opts.unitsPerDay
|
|
1572
1795
|
});
|
|
1573
|
-
|
|
1796
|
+
/* 라우트가 서로 다르면 그 사실을 알린다 — 「하루 몇 대」가 제품 구성에 따라 다르다는 뜻이다. */
|
|
1797
|
+
return { ...analysis, mixedCalendars: calendars.size > 1, mixedRoutes: this.distinctRouteKeys().length > 1 };
|
|
1574
1798
|
}
|
|
1575
1799
|
/**
|
|
1576
1800
|
* **생산 능력 보고서** — ISA-95 Part 4 `OperationsCapability`(§operations-capability).
|
|
@@ -1818,7 +2042,7 @@ export class FlowEngine {
|
|
|
1818
2042
|
* `skuMix` 에서 weight 로 gtin 선택(rng) — 도착·오더 자극의 품목 결정.
|
|
1819
2043
|
*
|
|
1820
2044
|
* **빈 목록이면 고르지 않는다**(`undefined`). 예전에는 `mix[mix.length - 1].gtin` 으로 떨어져
|
|
1821
|
-
* `mix[-1]` 이 undefined 가 되고 거기서
|
|
2045
|
+
* `mix[-1]` 이 undefined 가 되고 거기서 오류가 났다 — 그 예외가 서버까지 올라가 **정확도 추세 전체를
|
|
1822
2046
|
* 죽였다**(품목 구성을 선언하지 않은 자극 하나가 예측 전체를 껐다).
|
|
1823
2047
|
*
|
|
1824
2048
|
* 없는 품목을 지어내지 않는다: 무엇을 만들지 모르면 **만들지 않는 것**이 맞고, 부르는 쪽이
|
|
@@ -1850,10 +2074,34 @@ export class FlowEngine {
|
|
|
1850
2074
|
* 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
|
|
1851
2075
|
*/
|
|
1852
2076
|
transform(inputs, outputs, opts) {
|
|
1853
|
-
|
|
2077
|
+
/*
|
|
2078
|
+
* ── 없는 입력을 **조용히 지나가지 않는다** (2026-08-20) ────────────────────
|
|
2079
|
+
*
|
|
2080
|
+
* 예전에는 `if (!it) continue` 였다. 그런데 아래 이벤트는 `inputEPCList: inputs` 를 **그대로** 싣는다 —
|
|
2081
|
+
* 즉 상태에서는 소비되지 않은 것이 기록에서는 소비된 것으로 남았다. 상태와 기록이 다른 말을 하면,
|
|
2082
|
+
* 저널로 되돌린 트윈과 지금 도는 트윈이 서로 다른 사실을 말하게 된다.
|
|
2083
|
+
*
|
|
2084
|
+
* 시뮬레이션에서 이것은 **우리 계산의 결함**이다(우리가 없는 물건을 소비하라고 요구했다). 그래서
|
|
2085
|
+
* 멈춘다. 미러는 처방이 다르다 — 원본이 진실이므로 받아들이고 어긋난 횟수를 남긴다.
|
|
2086
|
+
*
|
|
2087
|
+
* ── 미러를 멈추면 안 되는 이유 (2026-08-20, 시험으로 확인) ─────────────────
|
|
2088
|
+
* 씨앗으로 세운 커널은 그대로 `tick` 한다(§`hydrateObserved`). 원본이 확보분 하나를 누락한
|
|
2089
|
+
* 스냅샷을 주면 — 실 연동에서 흔한 빈틈이다 — 그 커널은 **첫 틱에 멈춘다.** 원본의 한 건이
|
|
2090
|
+
* 라이브 트윈 전체를 세우는 것이고, 트윈은 원본을 비추는 쪽이므로 그 판정을 내릴 자리가 아니다.
|
|
2091
|
+
* 그래서 관측 모드에서는 **받아들이고 센다.** 세지 않으면 원본 결함이 아무 데도 드러나지 않는다.
|
|
2092
|
+
*/
|
|
2093
|
+
const absent = inputs.filter(epc => !this.items.get(epc));
|
|
2094
|
+
if (absent.length && !this.observeMode) {
|
|
2095
|
+
throw new Error(`transform: 입력 ${absent.length}건이 상태에 없습니다 (${absent.slice(0, 3).join(', ')}` +
|
|
2096
|
+
`${absent.length > 3 ? ' …' : ''}) — 없는 것을 소비했다고 기록하지 않습니다. ` +
|
|
2097
|
+
'이 트윈의 계산이 없는 물건을 요구했습니다.');
|
|
2098
|
+
}
|
|
2099
|
+
if (absent.length)
|
|
2100
|
+
this.transformInputsAbsent += absent.length;
|
|
2101
|
+
/* **실제로 소비한 것만** 이벤트에 싣는다 — 없던 것을 실으면 계보가 소비하지 않은 물건을 말한다. */
|
|
2102
|
+
const consumed = absent.length ? inputs.filter(epc => this.items.has(epc)) : inputs;
|
|
2103
|
+
for (const epc of consumed) {
|
|
1854
2104
|
const it = this.items.get(epc);
|
|
1855
|
-
if (!it)
|
|
1856
|
-
continue;
|
|
1857
2105
|
const n = this.locations.get(it.location);
|
|
1858
2106
|
if (n)
|
|
1859
2107
|
n.occupancy--;
|
|
@@ -1867,7 +2115,7 @@ export class FlowEngine {
|
|
|
1867
2115
|
}
|
|
1868
2116
|
this.emit(transformationEvent({
|
|
1869
2117
|
eventTime: this.now(), bizStep: opts.bizStep, disposition: opts.disposition,
|
|
1870
|
-
inputEPCList:
|
|
2118
|
+
inputEPCList: consumed.length ? consumed.slice() : undefined,
|
|
1871
2119
|
outputEPCList: outputs.length ? outputs.map(o => o.epc) : undefined,
|
|
1872
2120
|
transformationID: opts.transformationId, readPoint: opts.readPoint, bizLocation: opts.bizLocation ?? opts.readPoint,
|
|
1873
2121
|
bizTransactionList: opts.bizTransactionList
|
|
@@ -2077,6 +2325,9 @@ export class FlowEngine {
|
|
|
2077
2325
|
/* 진행 중 할당과 거래번호를 함께 싣는다 — 이것이 없으면 웜스타트가 잃고, 되살아난 오더의
|
|
2078
2326
|
계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
|
|
2079
2327
|
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
2328
|
+
/* 어느 레시피로 만드는가 — 품목만 보내면 미러는 「무엇으로」를 알 수 없다(같은 품목에 대체
|
|
2329
|
+
레시피가 있다). 시뮬만 아는 사실이 남으면 두 스냅샷을 같은 규칙으로 읽을 수 없다. */
|
|
2330
|
+
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
2080
2331
|
...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
|
|
2081
2332
|
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
2082
2333
|
/* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
|
|
@@ -2241,11 +2492,10 @@ export class FlowEngine {
|
|
|
2241
2492
|
let remaining = Math.max(0, req.quantity ?? 0);
|
|
2242
2493
|
if (!remaining)
|
|
2243
2494
|
continue;
|
|
2244
|
-
|
|
2495
|
+
/* 그 자리에 있는 것만 본다 — **색인이 답한다**(예전에는 요구 한 줄마다 물품 전체를 순회했다). */
|
|
2496
|
+
for (const it of this.items.at(at)) {
|
|
2245
2497
|
if (remaining <= 0)
|
|
2246
2498
|
break;
|
|
2247
|
-
if (it.location !== at)
|
|
2248
|
-
continue; // 그 자리에 있는 것만 — 다른 창고의 부품은 지금 쓸 수 없다
|
|
2249
2499
|
if (!this.materialMatches(it, req))
|
|
2250
2500
|
continue;
|
|
2251
2501
|
const already = takenSoFar.get(it.epc) ?? 0;
|
|
@@ -2286,7 +2536,38 @@ export class FlowEngine {
|
|
|
2286
2536
|
* 같은 품목이 그 자리에 이미 있으면 **수량을 더한다** — 새 줄을 만들면 같은 자리의 같은 로트가
|
|
2287
2537
|
* 둘로 갈려 재고가 부푼다(§MaterialSubLot 에서 겪은 것과 반대 방향의 같은 오류).
|
|
2288
2538
|
*/
|
|
2539
|
+
/**
|
|
2540
|
+
* **이 공정의 산출을 도메인이 직접 만드는가** — 기본은 아니다(코어가 만든다).
|
|
2541
|
+
*
|
|
2542
|
+
* ── 왜 이 이음새가 있나 (2026-08-20) ──────────────────────────────────────
|
|
2543
|
+
* 코어의 산출은 **비직렬 클래스+수량**이다(일련번호를 지어내지 않으므로). 그런데 어떤 도메인은
|
|
2544
|
+
* 산출물에 **개체 정체성**이 필요하다: MES 의 레시피 생산은 개체마다 직렬번호를 갖고, 수율이
|
|
2545
|
+
* 개체별로 양품/불량을 가르고, 계보(`TransformationEvent`)가 오더의 단계들을 잇는다.
|
|
2546
|
+
*
|
|
2547
|
+
* 그 도메인이 산출을 만들 때 코어도 같은 선언을 보고 만들면 **같은 산출이 두 벌** 된다 — 재고가
|
|
2548
|
+
* 조용히 두 배가 되고, 그 위의 모든 계산이 거짓 위에 선다. 그래서 소유를 **한쪽으로 정한다.**
|
|
2549
|
+
*
|
|
2550
|
+
* 코어는 여기서 **도메인 명사를 하나도 알지 않는다**: 「누가 만드는가」만 묻는다. 무엇을 만드는지는
|
|
2551
|
+
* 여전히 선언이 정하고, 코어는 그 선언을 읽을 뿐이다.
|
|
2552
|
+
*/
|
|
2553
|
+
producesOwnOutputs(_opKey) {
|
|
2554
|
+
return false;
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* **이 트윈의 정체성 근거** — 스냅샷에 실린다(§`StateSnapshot.identityGrounding`).
|
|
2558
|
+
*
|
|
2559
|
+
* 코어는 선언을 갖고 있지 않다(생산 선언은 도메인의 것이다). 그래서 기본은 **선언 없음**이고,
|
|
2560
|
+
* 선언을 든 커널이 override 해서 자기 것을 답한다 — `routeKeys()` 와 같은 모양이다.
|
|
2561
|
+
*
|
|
2562
|
+
* 코어는 여기서 **정체성의 값을 정하지 않는다**: 「어디서 왔나」만 묻는다.
|
|
2563
|
+
*/
|
|
2564
|
+
identityGroundingView() {
|
|
2565
|
+
return identityGroundingOf(undefined);
|
|
2566
|
+
}
|
|
2289
2567
|
produceMaterials(t) {
|
|
2568
|
+
/* 도메인이 소유를 주장하면 코어는 비켜선다 — 둘이 만들면 재고가 두 배가 된다. */
|
|
2569
|
+
if (this.producesOwnOutputs(t.kind))
|
|
2570
|
+
return;
|
|
2290
2571
|
const made = (this.operationSpecs.get(t.kind)?.materialSpecification ?? []).filter(m => m.use === 'produced');
|
|
2291
2572
|
if (!made.length)
|
|
2292
2573
|
return;
|
|
@@ -2336,7 +2617,7 @@ export class FlowEngine {
|
|
|
2336
2617
|
* 작업은 혼자 서지 못한다: 옮길 **물품**과, (있다면) 그것을 시킨 **오더** 위에 선다. 진행 중에
|
|
2337
2618
|
* 둘 중 하나가 사라질 수 있다 — 물품은 포장·출하·소비로, 오더는 이미 이행돼 씨앗이 주입하지 않아서.
|
|
2338
2619
|
*
|
|
2339
|
-
* 그때 도메인 훅은 없는 것을 딛으려다
|
|
2620
|
+
* 그때 도메인 훅은 없는 것을 딛으려다 오류를 낸다(`order.gtin` · `item.location`). 그 예외 하나가
|
|
2340
2621
|
* **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
|
|
2341
2622
|
* 여기서 묻고, 없으면 그 작업만 접는다.
|
|
2342
2623
|
*/
|
|
@@ -2353,7 +2634,7 @@ export class FlowEngine {
|
|
|
2353
2634
|
* 이 도메인이 이 작업을 **끝맺을 수 있나** — 코어가 모르는 조건은 도메인이 답한다.
|
|
2354
2635
|
*
|
|
2355
2636
|
* 코어는 물품과 오더까지만 안다. 그런데 도메인은 더 필요할 수 있다 — MES 는 완료 시점에 **오더와
|
|
2356
|
-
* 제품 정의**를 딛고 서고, 그 중 하나만 없어도
|
|
2637
|
+
* 제품 정의**를 딛고 서고, 그 중 하나만 없어도 오류를 낸다. 코어가 그 조건을 추측하면 도메인마다 다른
|
|
2357
2638
|
* 가정을 코어에 박게 되므로(방언), **묻는다.**
|
|
2358
2639
|
*
|
|
2359
2640
|
* 기본은 `true` — 대부분의 작업은 코어가 확인한 것으로 충분하다.
|
|
@@ -2366,7 +2647,7 @@ export class FlowEngine {
|
|
|
2366
2647
|
*
|
|
2367
2648
|
* 물품 맵의 키는 `itemKeyOf`(직렬 물품은 `epc`, 로트의 부분은 `subLotId`)인데, 작업은 로트 식별자
|
|
2368
2649
|
* (`itemEpc`)로 가리킨다. 비직렬 로트에서는 둘이 **다르다** — 그래서 그냥 `get` 하면 못 찾는다.
|
|
2369
|
-
* 실제로 그 회귀를 냈다: 예측(씨앗) 경로에서 `item.location = …` 이 `undefined` 위에서
|
|
2650
|
+
* 실제로 그 회귀를 냈다: 예측(씨앗) 경로에서 `item.location = …` 이 `undefined` 위에서 오류를 내어
|
|
2370
2651
|
* **정확도 추세 전체가 서버 오류로 죽었다.**
|
|
2371
2652
|
*
|
|
2372
2653
|
* **부분이 여럿이면 풀지 않는다** — 어느 부분을 가리키는지 알 수 없고, 아무거나 고르면 그 뒤
|
|
@@ -2376,6 +2657,18 @@ export class FlowEngine {
|
|
|
2376
2657
|
const exact = this.items.get(ref);
|
|
2377
2658
|
if (exact)
|
|
2378
2659
|
return exact;
|
|
2660
|
+
/*
|
|
2661
|
+
* ── 여기부터는 **대체 경로**다 (2026-08-21에 규약으로 못 정한 뒤 남긴 자리) ──
|
|
2662
|
+
*
|
|
2663
|
+
* 규약은 하나다: 물품 맵의 키는 `itemKeyOf` 이고, **상태에 실리는 참조도 그 키**여야 한다.
|
|
2664
|
+
* 그래야 이 함수가 `get` 한 번으로 끝난다. 이 아래가 실행되는 것은 참조가 키가 아닐 때이고,
|
|
2665
|
+
* 정당한 경우는 하나다: **원본이 준 참조**(운영 사실 유입·씨앗)는 우리 키 규칙을 모른다.
|
|
2666
|
+
*
|
|
2667
|
+
* 전수 조회이므로 규모에서 비싸다(물품 10만이 목표 규모다). 그래서 **세어 둔다** — 이 수가 0 이
|
|
2668
|
+
* 아니면 「참조가 키가 아닌 자리」가 아직 있다는 뜻이고, 인덱스를 얹을 근거는 그 수다.
|
|
2669
|
+
* 재지 않으면 「대체 경로는 드물 것이다」라는 짐작 위에서 성능을 판단하게 된다.
|
|
2670
|
+
*/
|
|
2671
|
+
this.refScans++;
|
|
2379
2672
|
let hit;
|
|
2380
2673
|
for (const it of this.items.values()) {
|
|
2381
2674
|
if (it.epc !== ref)
|
|
@@ -2386,6 +2679,16 @@ export class FlowEngine {
|
|
|
2386
2679
|
}
|
|
2387
2680
|
return hit;
|
|
2388
2681
|
}
|
|
2682
|
+
/**
|
|
2683
|
+
* 참조가 키가 아니어서 전수 조회로 찾은 횟수 — **성능 판단의 근거**다.
|
|
2684
|
+
*
|
|
2685
|
+
* 규약대로면 0 이다. 0 이 아닌 것은 결함이 아니라 사실이다(원본이 준 참조는 우리 키를 모른다).
|
|
2686
|
+
* 다만 그 수가 규모와 함께 자라면 인덱스가 필요하다는 뜻이고, 그때 넣을 근거가 이 값이다.
|
|
2687
|
+
*/
|
|
2688
|
+
refScans = 0;
|
|
2689
|
+
refScanCount() {
|
|
2690
|
+
return this.refScans;
|
|
2691
|
+
}
|
|
2389
2692
|
/**
|
|
2390
2693
|
* 실제 자재 이동을 작업에 적어 둔다 — ISA-95 `JobResponse.MaterialActual`.
|
|
2391
2694
|
* 같은 품목·같은 쓰임은 **한 줄로 합친다**(줄을 늘리면 실적을 세는 쪽이 중복을 걷어내야 한다).
|
|
@@ -2864,7 +3167,7 @@ export class FlowEngine {
|
|
|
2864
3167
|
* **주체가 사라진 작업은 완료시키지 않고 접는다.**
|
|
2865
3168
|
*
|
|
2866
3169
|
* 진행 중에 그 물품이 없어질 수 있다(포장으로 합쳐지고, 출하로 나가고, 다른 공정이 소비한다).
|
|
2867
|
-
* 그때 도메인 훅은 "없는 물품을 옮기려" 하다
|
|
3170
|
+
* 그때 도메인 훅은 "없는 물품을 옮기려" 하다 오류를 냈고, **그 예외 하나가 예측 전체를 죽였다** —
|
|
2868
3171
|
* 사용자에게는 정확도 추세가 통째로 사라진 것으로 보였다.
|
|
2869
3172
|
*
|
|
2870
3173
|
* 조용히 넘기지도 않는다: 몇 건이 왜 접혔는지 센다. 그것이 잦다면 **상류에 진짜 문제가 있는 것**
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export * from './capacity.ts';
|
|
|
27
27
|
export * from './operations-capability.ts';
|
|
28
28
|
export { WmsKernel } from './kernel.ts';
|
|
29
29
|
export { YmsKernel } from './yms-kernel.ts';
|
|
30
|
-
export { MesKernel
|
|
30
|
+
export { MesKernel } 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
33
|
export { ingestOperationalRecords, isOperationalRecord, operationalKindOf } from './operational-ingest.ts';
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ export * from "./capacity.js";
|
|
|
27
27
|
export * from "./operations-capability.js";
|
|
28
28
|
export { WmsKernel } from "./kernel.js";
|
|
29
29
|
export { YmsKernel } from "./yms-kernel.js";
|
|
30
|
-
export { MesKernel
|
|
30
|
+
export { MesKernel } 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
33
|
/* 운영 사실의 문 — 리듀서가 접는 여섯이 들어오는 자리(미러가 시뮬보다 가난하지 않게). */
|
package/dist/kernel.d.ts
CHANGED
|
@@ -5,6 +5,21 @@ import type { FlowOrder, FlowTask } from './flow-engine.ts';
|
|
|
5
5
|
export declare class WmsKernel extends FlowEngine {
|
|
6
6
|
private poSeq;
|
|
7
7
|
constructor(tenantId: string, policy?: AllocationPolicy);
|
|
8
|
+
/**
|
|
9
|
+
* 내장 시나리오가 요구하는 자리 — **없으면 말한다.**
|
|
10
|
+
*
|
|
11
|
+
* ── 왜 (2026-08-20) ────────────────────────────────────────────────────────
|
|
12
|
+
* 이 커널은 입고·중간·보관 자리를 **자기 낱말로** 찾았고(`dock`·`staging`·`storage`), 없으면 조용히
|
|
13
|
+
* `return` 했다. 그래서 그 낱말을 쓰지 않는 현장의 트윈은 **아무 일도 일어나지 않는 공장**이 됐다 —
|
|
14
|
+
* 오류도 로그도 없이. 실제로 확인된 자리다: 첫 실 시스템(F&B)에는 `dock`·`gate`·`staging` 이 **한 건도
|
|
15
|
+
* 없다**(자리 타입이 자유 문자열이고, 입출고가 도크 단계 없이 자리에 직접 일어난다).
|
|
16
|
+
*
|
|
17
|
+
* 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
|
|
18
|
+
* 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
|
|
19
|
+
* 것이고, 오류를 내면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
|
|
20
|
+
* 조용한 정지는 그 사실조차 남기지 않는다.
|
|
21
|
+
*/
|
|
22
|
+
private builtInLocation;
|
|
8
23
|
/** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
|
|
9
24
|
protected onArrival(spec: GeneratorSpec): void;
|
|
10
25
|
/**
|