@operato/twin-kernel 0.2.0 → 0.2.2
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 +46 -1
- package/dist/contract.js +17 -0
- package/dist/domain-definition.d.ts +13 -0
- package/dist/flow-engine.d.ts +105 -1
- package/dist/flow-engine.js +292 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/kernel.js +8 -3
- package/dist/mes-kernel.js +4 -6
- package/dist/observed-reducer.d.ts +103 -0
- package/dist/observed-reducer.js +384 -0
- package/dist/state-projector.d.ts +2 -100
- package/dist/state-projector.js +5 -345
- package/dist/yms-kernel.js +4 -2
- package/dist-cjs/index.cjs +333 -52
- package/package.json +1 -1
package/dist-cjs/index.cjs
CHANGED
|
@@ -40,9 +40,11 @@ __export(index_exports, {
|
|
|
40
40
|
MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
|
|
41
41
|
MES_TYPES: () => MES_TYPES,
|
|
42
42
|
MesKernel: () => MesKernel,
|
|
43
|
+
NODE_SATURATION_NEAR: () => NODE_SATURATION_NEAR,
|
|
43
44
|
OP_EVENT: () => OP_EVENT,
|
|
44
45
|
OP_PARAM: () => OP_PARAM,
|
|
45
|
-
|
|
46
|
+
ObservedReducer: () => ObservedReducer,
|
|
47
|
+
StateProjector: () => ObservedReducer,
|
|
46
48
|
TwinHistory: () => TwinHistory,
|
|
47
49
|
TwinObserver: () => TwinObserver,
|
|
48
50
|
TwinRuntime: () => TwinRuntime,
|
|
@@ -70,6 +72,7 @@ __export(index_exports, {
|
|
|
70
72
|
lgtinClass: () => lgtinClass,
|
|
71
73
|
mapRecord: () => mapRecord,
|
|
72
74
|
monteCarloForecast: () => monteCarloForecast,
|
|
75
|
+
nodeStatusOf: () => nodeStatusOf,
|
|
73
76
|
objectEvent: () => objectEvent,
|
|
74
77
|
parseEpc: () => parseEpc,
|
|
75
78
|
parseIsoDuration: () => parseIsoDuration,
|
|
@@ -87,6 +90,13 @@ __export(index_exports, {
|
|
|
87
90
|
module.exports = __toCommonJS(index_exports);
|
|
88
91
|
|
|
89
92
|
// src/contract.ts
|
|
93
|
+
var NODE_SATURATION_NEAR = 0.9;
|
|
94
|
+
function nodeStatusOf(n) {
|
|
95
|
+
const cap = n.capacity;
|
|
96
|
+
if (!(typeof cap === "number" && cap > 0)) return void 0;
|
|
97
|
+
const r = (n.occupancy ?? 0) / cap;
|
|
98
|
+
return r >= 1 ? "full" : r >= NODE_SATURATION_NEAR ? "near-full" : "available";
|
|
99
|
+
}
|
|
90
100
|
var OP_EVENT = {
|
|
91
101
|
task: "task.status",
|
|
92
102
|
equipment: "equipment.status",
|
|
@@ -517,14 +527,16 @@ function validateEpcisEvent(e) {
|
|
|
517
527
|
return v;
|
|
518
528
|
}
|
|
519
529
|
|
|
520
|
-
// src/
|
|
530
|
+
// src/observed-reducer.ts
|
|
521
531
|
var UNKNOWN_TYPE = "unknown";
|
|
522
|
-
var
|
|
532
|
+
var ObservedReducer = class {
|
|
523
533
|
/** 로케이션 마스터 — 출처를 함께 들고 있다(마스터가 말한 자리 vs 관측으로 알게 된 자리). */
|
|
524
534
|
master = /* @__PURE__ */ new Map();
|
|
525
535
|
items = /* @__PURE__ */ new Map();
|
|
526
536
|
aggregation = /* @__PURE__ */ new Map();
|
|
527
|
-
|
|
537
|
+
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
538
|
+
pendingParent = /* @__PURE__ */ new Map();
|
|
539
|
+
// 자식 EPC → 부모(물류단위)
|
|
528
540
|
tasks = /* @__PURE__ */ new Map();
|
|
529
541
|
movers = /* @__PURE__ */ new Map();
|
|
530
542
|
persons = /* @__PURE__ */ new Map();
|
|
@@ -534,7 +546,7 @@ var StateProjector = class {
|
|
|
534
546
|
/** 받은 정정 선언 — 상태에 반영하지 않되 **버리지도 않는다**(소비처가 볼 수 있게). */
|
|
535
547
|
corrections = [];
|
|
536
548
|
constructor(board) {
|
|
537
|
-
for (const n of board.nodes) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parentId: n.parentId, origin: "master" });
|
|
549
|
+
for (const n of board.nodes) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, parentId: n.parentId, origin: "master" });
|
|
538
550
|
for (const m of board.movers) this.movers.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeNode, origin: "master" });
|
|
539
551
|
for (const p of board.persons ?? []) this.persons.set(p.id, { id: p.id, personnelClass: p.personnelClass, status: "idle" });
|
|
540
552
|
for (const a of board.assets ?? []) this.assets.set(a.id, { id: a.id, assetClass: a.assetClass, location: a.homeNode, status: "idle" });
|
|
@@ -547,10 +559,12 @@ var StateProjector = class {
|
|
|
547
559
|
}
|
|
548
560
|
const cur = this.master.get(u.node.id);
|
|
549
561
|
const capacity = u.node.capacity ?? cur?.capacity;
|
|
562
|
+
const parallelism = u.node.parallelism ?? cur?.parallelism;
|
|
550
563
|
this.master.set(u.node.id, {
|
|
551
564
|
id: u.node.id,
|
|
552
565
|
type: u.node.type ?? cur?.type ?? UNKNOWN_TYPE,
|
|
553
566
|
...capacity === void 0 ? {} : { capacity },
|
|
567
|
+
...parallelism === void 0 ? {} : { parallelism },
|
|
554
568
|
...u.node.parentId ?? cur?.parentId ? { parentId: u.node.parentId ?? cur?.parentId } : {},
|
|
555
569
|
/* 마스터가 말한 것은 마스터 출처다 — 관측으로 알게 된 것(origin='observed')을 덮어 승격한다. */
|
|
556
570
|
origin: "master"
|
|
@@ -623,8 +637,10 @@ var StateProjector = class {
|
|
|
623
637
|
progress: d.progress,
|
|
624
638
|
remainingMs: d.remainingMs,
|
|
625
639
|
durationMs: d.durationMs,
|
|
640
|
+
startedAtSimMs: d.startedAtSimMs,
|
|
626
641
|
...d.personnel?.length ? { personnel: d.personnel.slice() } : {},
|
|
627
|
-
...d.assets?.length ? { assets: d.assets.slice() } : {}
|
|
642
|
+
...d.assets?.length ? { assets: d.assets.slice() } : {},
|
|
643
|
+
...d.resources?.length ? { resources: d.resources.slice() } : {}
|
|
628
644
|
});
|
|
629
645
|
break;
|
|
630
646
|
}
|
|
@@ -633,7 +649,7 @@ var StateProjector = class {
|
|
|
633
649
|
if (this.stale(`mover:${d.moverId}`, e)) return;
|
|
634
650
|
this.touchLocation(d.location);
|
|
635
651
|
const known = this.movers.get(d.moverId);
|
|
636
|
-
this.movers.set(d.moverId, { id: d.moverId, kind: d.kind, status: d.status, location: d.location, motion: d.motion, origin: known?.origin ?? "observed" });
|
|
652
|
+
this.movers.set(d.moverId, { id: d.moverId, kind: d.kind, status: d.status, location: d.location, taskId: d.taskId, motion: d.motion, origin: known?.origin ?? "observed" });
|
|
637
653
|
break;
|
|
638
654
|
}
|
|
639
655
|
case OP_EVENT.person: {
|
|
@@ -666,7 +682,16 @@ var StateProjector = class {
|
|
|
666
682
|
case OP_EVENT.order: {
|
|
667
683
|
const d = e.data;
|
|
668
684
|
if (this.stale(`order:${d.orderId}`, e)) return;
|
|
669
|
-
this.orders.set(d.orderId, {
|
|
685
|
+
this.orders.set(d.orderId, {
|
|
686
|
+
id: d.orderId,
|
|
687
|
+
kind: d.kind,
|
|
688
|
+
status: d.status,
|
|
689
|
+
progress: d.requested ? d.fulfilled / d.requested : 0,
|
|
690
|
+
requested: d.requested,
|
|
691
|
+
fulfilled: d.fulfilled,
|
|
692
|
+
...d.lines?.length ? { lines: d.lines.map((l) => ({ ...l })) } : {},
|
|
693
|
+
held: d.held
|
|
694
|
+
});
|
|
670
695
|
break;
|
|
671
696
|
}
|
|
672
697
|
}
|
|
@@ -686,13 +711,17 @@ var StateProjector = class {
|
|
|
686
711
|
this.aggregation.set(ev.parentID, [...ev.childEPCs]);
|
|
687
712
|
for (const child of ev.childEPCs) {
|
|
688
713
|
const cur = this.items.get(child);
|
|
689
|
-
if (cur)
|
|
690
|
-
|
|
714
|
+
if (cur) {
|
|
715
|
+
this.items.set(child, { ...cur, parent: ev.parentID });
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
this.pendingParent.set(child, ev.parentID);
|
|
691
719
|
}
|
|
692
720
|
} else if (ev.action === "DELETE") {
|
|
693
721
|
for (const child of this.aggregation.get(ev.parentID) ?? []) {
|
|
694
722
|
const cur = this.items.get(child);
|
|
695
723
|
if (cur) this.items.set(child, { ...cur, parent: void 0 });
|
|
724
|
+
this.pendingParent.delete(child);
|
|
696
725
|
}
|
|
697
726
|
this.aggregation.delete(ev.parentID);
|
|
698
727
|
}
|
|
@@ -755,7 +784,7 @@ var StateProjector = class {
|
|
|
755
784
|
gtinKey: parsedClass?.gtinKey ?? parsedSelf.gtinKey ?? cur?.gtinKey,
|
|
756
785
|
location: patch.location ?? cur?.location ?? "",
|
|
757
786
|
disposition: patch.disposition ?? cur?.disposition,
|
|
758
|
-
parent: cur?.parent,
|
|
787
|
+
parent: cur?.parent ?? this.pendingParent.get(epc),
|
|
759
788
|
qty: q?.quantity ?? cur?.qty,
|
|
760
789
|
uom: q?.uom ?? cur?.uom,
|
|
761
790
|
/* 마스터데이터는 생겨날 때 한 번 정해진다 — 뒤 이벤트가 지우지 않게 기존 값을 남긴다. */
|
|
@@ -781,15 +810,21 @@ var StateProjector = class {
|
|
|
781
810
|
return {
|
|
782
811
|
revision: this.revision,
|
|
783
812
|
...this.corrections.length ? { corrections: this.corrections.map((c) => ({ ...c })) } : {},
|
|
784
|
-
nodes: [...this.master.values()].map((n) =>
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
813
|
+
nodes: [...this.master.values()].map((n) => {
|
|
814
|
+
const occupancy = occ.get(n.id) ?? 0;
|
|
815
|
+
const status = nodeStatusOf({ occupancy, capacity: n.capacity });
|
|
816
|
+
return {
|
|
817
|
+
id: n.id,
|
|
818
|
+
type: n.type,
|
|
819
|
+
occupancy,
|
|
820
|
+
...status ? { status } : {},
|
|
821
|
+
/* 용량 미상은 **키를 만들지 않는다** — 0 으로 실으면 "자리 없음" 이라는 없는 사실이 생긴다. */
|
|
822
|
+
...n.capacity === void 0 ? {} : { capacity: n.capacity },
|
|
823
|
+
...n.parallelism === void 0 ? {} : { parallelism: n.parallelism },
|
|
824
|
+
...n.parentId ? { parentId: n.parentId } : {},
|
|
825
|
+
origin: n.origin
|
|
826
|
+
};
|
|
827
|
+
}),
|
|
793
828
|
/* 들고 있는 것을 전부 내보낸다 — 축소하면 그 자리에서 정보가 사라진다. */
|
|
794
829
|
items: [...this.items.values()].map((i) => ({ ...i })),
|
|
795
830
|
persons: [...this.persons.values()].map((p) => ({ ...p })),
|
|
@@ -828,7 +863,7 @@ var EventJournal = class {
|
|
|
828
863
|
}
|
|
829
864
|
};
|
|
830
865
|
function replay(board, events) {
|
|
831
|
-
const proj = new
|
|
866
|
+
const proj = new ObservedReducer(board);
|
|
832
867
|
for (const e of events) proj.apply(e);
|
|
833
868
|
return proj.snapshot();
|
|
834
869
|
}
|
|
@@ -1283,6 +1318,13 @@ var FlowEngine = class {
|
|
|
1283
1318
|
* 도메인 정의에서 실어 온다(`loadOperations`). 없으면 커널 기본값을 쓰고 그 사실을 `specCoverage()` 가 밝힌다.
|
|
1284
1319
|
*/
|
|
1285
1320
|
operationSpecs = /* @__PURE__ */ new Map();
|
|
1321
|
+
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
1322
|
+
observer;
|
|
1323
|
+
/** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
|
|
1324
|
+
observedDirty = false;
|
|
1325
|
+
observeMode = false;
|
|
1326
|
+
/** 관측 구동이 투영기를 세울 때 필요한 원본 보드(구조는 이벤트가 아니라 마스터에서 온다). */
|
|
1327
|
+
boardDef;
|
|
1286
1328
|
/** 명세 소비 기록 — 무엇을 선언값으로, 무엇을 기본값으로 계산했나(정직한 자기보고). */
|
|
1287
1329
|
specUse = /* @__PURE__ */ new Map();
|
|
1288
1330
|
epcSeq = 0;
|
|
@@ -1290,6 +1332,10 @@ var FlowEngine = class {
|
|
|
1290
1332
|
orderSeq = 0;
|
|
1291
1333
|
soSeq = 0;
|
|
1292
1334
|
handlers = [];
|
|
1335
|
+
/** 구독자 접근(관측 재방출) — emit 과 같은 목록을 쓴다(두 경로가 갈리지 않게). */
|
|
1336
|
+
handlersRef() {
|
|
1337
|
+
return this.handlers;
|
|
1338
|
+
}
|
|
1293
1339
|
gens = [];
|
|
1294
1340
|
generating = false;
|
|
1295
1341
|
speed = 1;
|
|
@@ -1300,6 +1346,7 @@ var FlowEngine = class {
|
|
|
1300
1346
|
}
|
|
1301
1347
|
// ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
|
|
1302
1348
|
loadBoard(def) {
|
|
1349
|
+
this.boardDef = def;
|
|
1303
1350
|
for (const n of def.nodes) this.nodes.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parallelism: n.parallelism, occupancy: 0, status: "idle", parentId: n.parentId });
|
|
1304
1351
|
for (const p of def.persons ?? []) this.persons.set(p.id, { id: p.id, personnelClass: p.personnelClass, status: "idle", taskId: null, window: p.window });
|
|
1305
1352
|
for (const a of def.assets ?? []) this.assets.set(a.id, { id: a.id, assetClass: a.assetClass, location: a.homeNode, status: "idle", taskId: null });
|
|
@@ -1341,15 +1388,15 @@ var FlowEngine = class {
|
|
|
1341
1388
|
this.items.clear();
|
|
1342
1389
|
for (const it of snap.items) {
|
|
1343
1390
|
this.items.set(it.epc, {
|
|
1391
|
+
/* 품번 키·로트는 식별자에서 파생되므로 심지 않는다(스냅샷이 다시 낸다 — 두 벌을 두면 어긋난다). */
|
|
1344
1392
|
epc: it.epc,
|
|
1345
1393
|
location: it.location,
|
|
1346
1394
|
disposition: it.disposition ?? DISP.sellable,
|
|
1347
1395
|
gtin: it.gtin,
|
|
1348
|
-
gtinKey: it.gtinKey,
|
|
1349
|
-
lot: it.lot,
|
|
1350
1396
|
qty: it.qty ?? 1,
|
|
1351
1397
|
uom: it.uom,
|
|
1352
1398
|
parent: it.parent,
|
|
1399
|
+
carriedBy: it.carriedBy,
|
|
1353
1400
|
expiry: it.expiry,
|
|
1354
1401
|
ilmd: it.ilmd
|
|
1355
1402
|
});
|
|
@@ -1387,6 +1434,7 @@ var FlowEngine = class {
|
|
|
1387
1434
|
toNode: t.toNode ?? "",
|
|
1388
1435
|
resource: known && t.status === "in-progress" ? t.resourceRef ?? null : null,
|
|
1389
1436
|
remainingMs: known ? t.remainingMs : t.durationMs ?? 0,
|
|
1437
|
+
startedAtSimMs: t.startedAtSimMs,
|
|
1390
1438
|
durationMs: t.durationMs ?? (known ? t.remainingMs : 0),
|
|
1391
1439
|
orderId: t.orderId,
|
|
1392
1440
|
intent: t.intent
|
|
@@ -1410,7 +1458,8 @@ var FlowEngine = class {
|
|
|
1410
1458
|
}
|
|
1411
1459
|
}
|
|
1412
1460
|
}
|
|
1413
|
-
|
|
1461
|
+
const observedOrders = orders.length ? orders : (snap.orders ?? []).filter((o) => o.requested !== void 0).map((o) => ({ orderId: o.id, kind: o.kind, status: o.status, requested: o.requested, fulfilled: o.fulfilled ?? 0, held: o.held, lines: o.lines }));
|
|
1462
|
+
for (const o of observedOrders) {
|
|
1414
1463
|
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
1415
1464
|
const remaining = lines.reduce((s, l) => s + l.requested, 0);
|
|
1416
1465
|
if (remaining <= 0) continue;
|
|
@@ -1562,13 +1611,20 @@ var FlowEngine = class {
|
|
|
1562
1611
|
this.processTasks(dt);
|
|
1563
1612
|
}
|
|
1564
1613
|
getSnapshot() {
|
|
1614
|
+
this.settleObserved();
|
|
1565
1615
|
return {
|
|
1566
1616
|
revision: this.revision,
|
|
1567
1617
|
simClockMs: this.clockMs,
|
|
1568
|
-
|
|
1569
|
-
|
|
1618
|
+
/* 출처 표시 — 보드(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
1619
|
+
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
1620
|
+
nodes: [...this.nodes.values()].map((n) => {
|
|
1621
|
+
const { status, ...rest } = n;
|
|
1622
|
+
const derived = nodeStatusOf(n);
|
|
1623
|
+
return { ...rest, ...derived ? { status: derived } : {}, origin: "master" };
|
|
1624
|
+
}),
|
|
1625
|
+
items: [...this.items.values()].map((i) => this.itemState(i)),
|
|
1570
1626
|
movers: [...this.movers.values()].map((m) => {
|
|
1571
|
-
const s = { id: m.id, kind: m.kind, location: m.location, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held, ...this.offShift(m) ? { offShift: true } : {} };
|
|
1627
|
+
const s = { id: m.id, kind: m.kind, location: m.location, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held, origin: "master", ...this.offShift(m) ? { offShift: true } : {} };
|
|
1572
1628
|
const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
|
|
1573
1629
|
if (t && t.status === "in-progress" && t.intent !== "process") s.motion = { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs - (t.durationMs - t.remainingMs), durationMs: t.durationMs, progress: this.progressOf(t), elapsedMs: t.durationMs - t.remainingMs };
|
|
1574
1630
|
return s;
|
|
@@ -1583,8 +1639,35 @@ var FlowEngine = class {
|
|
|
1583
1639
|
if (this.personOffShift(p)) st.offShift = true;
|
|
1584
1640
|
return st;
|
|
1585
1641
|
}),
|
|
1586
|
-
|
|
1587
|
-
|
|
1642
|
+
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
1643
|
+
* 다른 커널을 심으면(hydrateObserved) 진행 중이던 작업을 이어 굴릴 수 없었다(미러 스냅샷은
|
|
1644
|
+
* 델타에서 왔으므로 갖고 있었다 — 같은 계약을 두 구동이 다르게 채우던 자리). */
|
|
1645
|
+
tasks: [...this.tasks.values()].map((t) => ({
|
|
1646
|
+
id: t.id,
|
|
1647
|
+
kind: t.kind,
|
|
1648
|
+
status: t.status,
|
|
1649
|
+
itemRefs: [t.itemEpc],
|
|
1650
|
+
fromNode: t.fromNode,
|
|
1651
|
+
toNode: t.toNode,
|
|
1652
|
+
resourceRef: t.resource ?? void 0,
|
|
1653
|
+
orderId: t.orderId,
|
|
1654
|
+
...t.intent ? { intent: t.intent } : {},
|
|
1655
|
+
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
1656
|
+
...t.status === "in-progress" ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: this.progressOf(t) } : {},
|
|
1657
|
+
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
1658
|
+
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
1659
|
+
...t.resources?.length ? { resources: t.resources.slice() } : {}
|
|
1660
|
+
})),
|
|
1661
|
+
orders: [...this.orders.values()].map((o) => ({
|
|
1662
|
+
id: o.id,
|
|
1663
|
+
kind: o.kind,
|
|
1664
|
+
status: o.status,
|
|
1665
|
+
progress: o.requested ? o.fulfilled / o.requested : 0,
|
|
1666
|
+
requested: o.requested,
|
|
1667
|
+
fulfilled: o.fulfilled,
|
|
1668
|
+
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {},
|
|
1669
|
+
held: o.held
|
|
1670
|
+
})),
|
|
1588
1671
|
attentions: this.computeAttentions()
|
|
1589
1672
|
};
|
|
1590
1673
|
}
|
|
@@ -1609,6 +1692,7 @@ var FlowEngine = class {
|
|
|
1609
1692
|
* rng 는 fork 의 시나리오 load 시 재시드(드레인 예측은 생성 없어 rng 무관·결정적).
|
|
1610
1693
|
*/
|
|
1611
1694
|
fork(tenantId = this.tenantId) {
|
|
1695
|
+
this.settleObserved();
|
|
1612
1696
|
const Ctor = this.constructor;
|
|
1613
1697
|
const clone = new Ctor(tenantId, this.policy);
|
|
1614
1698
|
const skip = /* @__PURE__ */ new Set(["policy", "scenario", "tenantId", "handlers", "durationEstimator"]);
|
|
@@ -1631,6 +1715,46 @@ var FlowEngine = class {
|
|
|
1631
1715
|
randInt(min, max) {
|
|
1632
1716
|
return max <= min ? min : min + Math.floor(this.rng() * (max - min + 1));
|
|
1633
1717
|
}
|
|
1718
|
+
/**
|
|
1719
|
+
* 관측 구동(P0 스파이크) — **이벤트로 커널을 굴린다.**
|
|
1720
|
+
*
|
|
1721
|
+
* 상태를 만드는 구동이 둘인데(시뮬 `tick` / 미러 `apply`) 지금은 **모델도 둘**이라 한쪽만 고치면
|
|
1722
|
+
* 갈라진다(2026-08-01 하루에 아홉 곳). 근본 해법은 **한 상태 모델 두 구동**이고, 이것은 그 실현
|
|
1723
|
+
* 가능성을 재는 스파이크다(design/plans/kernel-unification-live-observe.md P0).
|
|
1724
|
+
*
|
|
1725
|
+
* 여기서는 **이미 검증된 조각을 조립**한다: 투영기가 이벤트를 접고, 그 결과를 씨앗 경로
|
|
1726
|
+
* (`hydrateObserved`)로 커널 상태에 심는다. 그래서 관측으로 굴린 커널을 그대로 `fork`·`tick` 할 수
|
|
1727
|
+
* 있다 — "미러에서 예측한다" 가 별도 배관 없이 성립하는지가 이 스파이크의 질문이다.
|
|
1728
|
+
*
|
|
1729
|
+
* **비용은 정직하게**: 이벤트마다 전체를 다시 심으므로 O(상태 크기)다. P1 에서 반영 로직을 순수
|
|
1730
|
+
* reduce 모듈로 추출해 투영기와 공유하면 사라진다. 지금은 계약이 성립하는지만 본다.
|
|
1731
|
+
*
|
|
1732
|
+
* `tick` 과 섞어 쓰지 않는다 — 섞으면 무엇이 진실인지 알 수 없다(관측이 시뮬을 덮어쓴다).
|
|
1733
|
+
*/
|
|
1734
|
+
apply(envelope) {
|
|
1735
|
+
if (!this.observer) {
|
|
1736
|
+
this.observer = new ObservedReducer(this.boardDef ?? { nodes: [], movers: [] });
|
|
1737
|
+
this.observeMode = true;
|
|
1738
|
+
}
|
|
1739
|
+
this.observer.apply(envelope);
|
|
1740
|
+
for (const h of this.observedHandlers()) h(envelope);
|
|
1741
|
+
this.observedDirty = true;
|
|
1742
|
+
this.revision++;
|
|
1743
|
+
}
|
|
1744
|
+
/** 관측분을 커널 상태로 옮긴다 — 필요할 때 한 번만(같은 규칙, 같은 씨앗 경로). */
|
|
1745
|
+
settleObserved() {
|
|
1746
|
+
if (!this.observedDirty || !this.observer) return;
|
|
1747
|
+
this.observedDirty = false;
|
|
1748
|
+
this.hydrateObserved(this.observer.snapshot());
|
|
1749
|
+
}
|
|
1750
|
+
/** 구독자 목록 — 관측 재방출용(private handlers 에 접근). */
|
|
1751
|
+
observedHandlers() {
|
|
1752
|
+
return this.handlersRef();
|
|
1753
|
+
}
|
|
1754
|
+
/** 관측 구동으로 굴러가는 중인가 — 소비처가 "이 커널의 진실이 어디서 오나" 를 물을 수 있게. */
|
|
1755
|
+
get observing() {
|
|
1756
|
+
return this.observeMode;
|
|
1757
|
+
}
|
|
1634
1758
|
/**
|
|
1635
1759
|
* 오퍼레이션 명세를 싣는다 — 도메인 정의(ISA-95 OperationsSegment)의 시뮬 명세를 커널이 소비하는 입구.
|
|
1636
1760
|
* 같은 key 를 다시 실으면 덮어쓴다(정의가 권위).
|
|
@@ -1793,6 +1917,53 @@ var FlowEngine = class {
|
|
|
1793
1917
|
bizTransactionList: opts.bizTransactionList
|
|
1794
1918
|
}));
|
|
1795
1919
|
}
|
|
1920
|
+
/**
|
|
1921
|
+
* 할당(예약) — **처분 변화를 이벤트로 낸다.**
|
|
1922
|
+
*
|
|
1923
|
+
* 예전에는 네 곳(WMS·YMS·MES 두 경로)이 각자 `disposition = reserved` 로 상태만 바꾸고 거래
|
|
1924
|
+
* 이벤트(TransactionEvent)만 냈다. 거래 이벤트는 **처분을 싣지 않으므로** 미러는 그 물건이 잡혔다는
|
|
1925
|
+
* 사실을 영영 알 수 없었다(적합성 하네스가 잡았다). 저널로 복원해도, 예측 씨앗에도 안 실린다.
|
|
1926
|
+
*
|
|
1927
|
+
* 관측 이벤트로 낸다 — 표준이 처분 변화를 표현하는 자리다(ObjectEvent OBSERVE + disposition).
|
|
1928
|
+
* 물건이 여러 자리에 흩어져 있으면 **자리별로 나눠** 낸다(한 이벤트에 한 readPoint 가 맞다).
|
|
1929
|
+
*
|
|
1930
|
+
* `bizStep` 은 **호출부가 정한다.** 할당 자체를 가리키는 CBV 단계(reserving)를 1차 출처로 확인하지
|
|
1931
|
+
* 못했으므로 어휘를 발명하지 않고, 그 할당이 속한 업무 단계를 그대로 쓴다.
|
|
1932
|
+
*/
|
|
1933
|
+
reserve(epcs, bizStep) {
|
|
1934
|
+
this.observeDisposition(epcs, DISP.reserved, bizStep);
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* 처분 변화 관측 — **상태와 이벤트를 한 번에.** 둘을 따로 쓰면 반드시 갈라진다.
|
|
1938
|
+
*
|
|
1939
|
+
* 실제로 양쪽으로 갈라져 있었다: 할당은 상태만 바꾸고 이벤트를 안 냈고(미러가 모름), 야드 도크
|
|
1940
|
+
* 도착은 이벤트만 내고 상태를 안 바꿨다(이벤트와 상태가 다른 말). 적합성 하네스가 둘 다 잡았다.
|
|
1941
|
+
*
|
|
1942
|
+
* 물건이 여러 자리에 있으면 자리별로 나눠 낸다(한 이벤트에 한 readPoint 가 맞다).
|
|
1943
|
+
* 이미 그 처분이면 아무 일도 하지 않는다(같은 사실을 두 번 말하지 않는다).
|
|
1944
|
+
*/
|
|
1945
|
+
observeDisposition(epcs, disposition, bizStep, at) {
|
|
1946
|
+
const byLocation = /* @__PURE__ */ new Map();
|
|
1947
|
+
for (const epc of epcs) {
|
|
1948
|
+
const it = this.items.get(epc);
|
|
1949
|
+
if (!it || it.disposition === disposition) continue;
|
|
1950
|
+
it.disposition = disposition;
|
|
1951
|
+
const where = at ?? it.location ?? "";
|
|
1952
|
+
const bin = byLocation.get(where);
|
|
1953
|
+
if (bin) bin.push(epc);
|
|
1954
|
+
else byLocation.set(where, [epc]);
|
|
1955
|
+
}
|
|
1956
|
+
for (const [where, list] of byLocation) {
|
|
1957
|
+
this.emit(objectEvent({
|
|
1958
|
+
eventTime: this.now(),
|
|
1959
|
+
action: "OBSERVE",
|
|
1960
|
+
bizStep,
|
|
1961
|
+
disposition,
|
|
1962
|
+
epcList: list,
|
|
1963
|
+
...where ? { readPoint: where, bizLocation: where } : {}
|
|
1964
|
+
}));
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1796
1967
|
/**
|
|
1797
1968
|
* containment 조립(EPCIS AggregationEvent ADD) — 자식들을 부모(용기)로 집約.
|
|
1798
1969
|
* consume 지정 시 자식이 컨테이너로 흡수되며 독립 아이템에서 이탈(dematerialize: ObjectEvent DELETE + 제거).
|
|
@@ -1810,6 +1981,11 @@ var FlowEngine = class {
|
|
|
1810
1981
|
this.items.delete(c);
|
|
1811
1982
|
}
|
|
1812
1983
|
}
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
for (const c of children) {
|
|
1987
|
+
const it = this.items.get(c);
|
|
1988
|
+
if (it) it.parent = parent;
|
|
1813
1989
|
}
|
|
1814
1990
|
}
|
|
1815
1991
|
/**
|
|
@@ -1819,6 +1995,10 @@ var FlowEngine = class {
|
|
|
1819
1995
|
*/
|
|
1820
1996
|
disaggregate(parent, children, opts) {
|
|
1821
1997
|
this.emit(aggregationEvent({ eventTime: this.now(), action: "DELETE", bizStep: opts.bizStep, parentID: parent, childEPCs: children.slice(), readPoint: opts.readPoint }));
|
|
1998
|
+
for (const c of children) {
|
|
1999
|
+
const it = this.items.get(c);
|
|
2000
|
+
if (it) it.parent = void 0;
|
|
2001
|
+
}
|
|
1822
2002
|
if (opts.materialize) {
|
|
1823
2003
|
const m = opts.materialize;
|
|
1824
2004
|
for (const c of children) {
|
|
@@ -1881,8 +2061,9 @@ var FlowEngine = class {
|
|
|
1881
2061
|
intent: t.intent,
|
|
1882
2062
|
...t.personnel?.length ? { personnel: t.personnel.slice() } : {},
|
|
1883
2063
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
2064
|
+
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
1884
2065
|
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
1885
|
-
...inProgress ? { remainingMs: t.remainingMs, progress: t.durationMs ? done / t.durationMs : void 0 } : {}
|
|
2066
|
+
...inProgress ? { remainingMs: t.remainingMs, startedAtSimMs: t.startedAtSimMs, progress: t.durationMs ? done / t.durationMs : void 0 } : {}
|
|
1886
2067
|
});
|
|
1887
2068
|
}
|
|
1888
2069
|
/** 사람 상태 전이 — 설비와 별개 채널(어휘가 다르다: 고장이 아니라 교대·투입). */
|
|
@@ -1892,11 +2073,22 @@ var FlowEngine = class {
|
|
|
1892
2073
|
emitPerson(p) {
|
|
1893
2074
|
this.emitOp(OP_EVENT.person, { personId: p.id, personnelClass: p.personnelClass, status: p.status, taskId: p.taskId ?? void 0, ...this.personOffShift(p) ? { offShift: true } : {} });
|
|
1894
2075
|
}
|
|
2076
|
+
/** 설비 상태 전이 — `taskId` 를 함께 싣는다(사람·자산 델타와 같은 규칙). 없으면 미러가 "이 설비가
|
|
2077
|
+
* 무슨 일을 하는 중인가" 를 알 수 없어 작업↔자원 연결이 한쪽에서만 성립한다. */
|
|
1895
2078
|
emitMover(m, motion) {
|
|
1896
|
-
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, motion });
|
|
2079
|
+
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, taskId: m.taskId ?? void 0, motion });
|
|
1897
2080
|
}
|
|
2081
|
+
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
1898
2082
|
emitOrder(o) {
|
|
1899
|
-
this.emitOp(OP_EVENT.order, {
|
|
2083
|
+
this.emitOp(OP_EVENT.order, {
|
|
2084
|
+
orderId: o.id,
|
|
2085
|
+
kind: o.kind,
|
|
2086
|
+
status: o.status,
|
|
2087
|
+
requested: o.requested,
|
|
2088
|
+
fulfilled: o.fulfilled,
|
|
2089
|
+
held: o.held,
|
|
2090
|
+
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {}
|
|
2091
|
+
});
|
|
1900
2092
|
}
|
|
1901
2093
|
// ── 내부 mechanics ─────────────────────────────────────────────────────────
|
|
1902
2094
|
/**
|
|
@@ -1980,7 +2172,11 @@ var FlowEngine = class {
|
|
|
1980
2172
|
if (!a) continue;
|
|
1981
2173
|
a.status = "in-use";
|
|
1982
2174
|
a.taskId = t.id;
|
|
1983
|
-
if (t.itemEpc)
|
|
2175
|
+
if (t.itemEpc) {
|
|
2176
|
+
a.carrying = t.itemEpc;
|
|
2177
|
+
const it = this.items.get(t.itemEpc);
|
|
2178
|
+
if (it) it.carriedBy = a.id;
|
|
2179
|
+
}
|
|
1984
2180
|
this.emitAsset(a);
|
|
1985
2181
|
}
|
|
1986
2182
|
}
|
|
@@ -1995,6 +2191,10 @@ var FlowEngine = class {
|
|
|
1995
2191
|
a.status = "idle";
|
|
1996
2192
|
a.taskId = null;
|
|
1997
2193
|
a.location = t.toNode || a.location;
|
|
2194
|
+
if (a.carrying) {
|
|
2195
|
+
const it = this.items.get(a.carrying);
|
|
2196
|
+
if (it) it.carriedBy = void 0;
|
|
2197
|
+
}
|
|
1998
2198
|
a.carrying = void 0;
|
|
1999
2199
|
this.emitAsset(a);
|
|
2000
2200
|
}
|
|
@@ -2020,6 +2220,30 @@ var FlowEngine = class {
|
|
|
2020
2220
|
}
|
|
2021
2221
|
return picked;
|
|
2022
2222
|
}
|
|
2223
|
+
/**
|
|
2224
|
+
* 필요 설비를 고른다 — **인원·자산과 같은 규칙**(등급으로 요구, 부분 확보 없이 전량 아니면 대기).
|
|
2225
|
+
* 명세(`equipmentSpecification`)가 없으면 기존 거동: `resourceType` 한 대(없으면 아무 유휴 설비).
|
|
2226
|
+
* 여기서는 고르기만 한다 — 확정은 호출부가 다른 자원까지 확보한 뒤에 한다.
|
|
2227
|
+
*/
|
|
2228
|
+
claimEquipment(t) {
|
|
2229
|
+
const free = (kind, picked2 = []) => [...this.movers.values()].filter(
|
|
2230
|
+
(m) => m.status === "idle" && !m.held && !this.offShift(m) && !picked2.includes(m.id) && (kind === void 0 || m.kind === kind)
|
|
2231
|
+
);
|
|
2232
|
+
const need = this.operationSpecs.get(t.kind)?.equipmentSpecification;
|
|
2233
|
+
if (!need?.length) {
|
|
2234
|
+
const one = free(t.resourceType)[0];
|
|
2235
|
+
return one ? [one.id] : null;
|
|
2236
|
+
}
|
|
2237
|
+
const picked = [];
|
|
2238
|
+
for (const req of need) {
|
|
2239
|
+
const want = Math.max(0, Math.floor(req.quantity ?? 0));
|
|
2240
|
+
if (!want) continue;
|
|
2241
|
+
const avail = free(req.equipmentClass, picked);
|
|
2242
|
+
if (avail.length < want) return null;
|
|
2243
|
+
for (let i = 0; i < want; i++) picked.push(avail[i].id);
|
|
2244
|
+
}
|
|
2245
|
+
return picked.length ? picked : null;
|
|
2246
|
+
}
|
|
2023
2247
|
/** 확보한 사람을 작업에 묶는다(설비까지 확정된 뒤). */
|
|
2024
2248
|
assignCrew(t, crew) {
|
|
2025
2249
|
if (!crew.length) return;
|
|
@@ -2116,6 +2340,33 @@ var FlowEngine = class {
|
|
|
2116
2340
|
}
|
|
2117
2341
|
}
|
|
2118
2342
|
}
|
|
2343
|
+
/**
|
|
2344
|
+
* 물품 상태 산출 — **보유값 + 식별자에서 나오는 파생값.**
|
|
2345
|
+
*
|
|
2346
|
+
* 품번 키(`gtinKey`)·로트(`lot`)는 식별자의 순수 함수라 저장하지 않고 여기서 낸다. 투영기와 **같은
|
|
2347
|
+
* 규칙**(`parseEpc`)을 쓴다 — 두 구동이 같은 식별자를 다르게 뜯으면 같은 사실이 다르게 보인다.
|
|
2348
|
+
* 로트는 LGTIN 이면 식별자 안에 있고, 직렬 개체는 마스터데이터(`ilmd`)에 실려 온다.
|
|
2349
|
+
*/
|
|
2350
|
+
itemState(i) {
|
|
2351
|
+
const parsedClass = i.gtin ? parseEpc(i.gtin) : void 0;
|
|
2352
|
+
const parsedSelf = parseEpc(i.epc);
|
|
2353
|
+
const lot = parsedClass?.lot ?? parsedSelf.lot ?? (typeof i.ilmd?.[ILMD_ATTR.lot] === "string" ? i.ilmd[ILMD_ATTR.lot] : void 0);
|
|
2354
|
+
const gtinKey = parsedClass?.gtinKey ?? parsedSelf.gtinKey;
|
|
2355
|
+
return {
|
|
2356
|
+
epc: i.epc,
|
|
2357
|
+
...i.gtin ? { gtin: i.gtin } : {},
|
|
2358
|
+
...gtinKey ? { gtinKey } : {},
|
|
2359
|
+
...lot ? { lot } : {},
|
|
2360
|
+
location: i.location,
|
|
2361
|
+
...i.disposition ? { disposition: i.disposition } : {},
|
|
2362
|
+
...i.parent ? { parent: i.parent } : {},
|
|
2363
|
+
...i.carriedBy ? { carriedBy: i.carriedBy } : {},
|
|
2364
|
+
...i.qty !== void 0 ? { qty: i.qty } : {},
|
|
2365
|
+
...i.uom ? { uom: i.uom } : {},
|
|
2366
|
+
...i.expiry !== void 0 ? { expiry: i.expiry } : {},
|
|
2367
|
+
...i.ilmd ? { ilmd: i.ilmd } : {}
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2119
2370
|
progressOf(t) {
|
|
2120
2371
|
return t.durationMs <= 0 ? 1 : Math.min(1, Math.max(0, (t.durationMs - t.remainingMs) / t.durationMs));
|
|
2121
2372
|
}
|
|
@@ -2141,7 +2392,18 @@ var FlowEngine = class {
|
|
|
2141
2392
|
processOrders() {
|
|
2142
2393
|
for (const o of this.orders.values()) if (o.status === "created" && !o.held) this.allocate(o);
|
|
2143
2394
|
}
|
|
2395
|
+
/**
|
|
2396
|
+
* 작업 진행 — **진행을 먼저, 배정을 나중에.**
|
|
2397
|
+
*
|
|
2398
|
+
* 예전에는 배정을 먼저 하고 같은 tick 에서 곧바로 dt 만큼 깎았다. 그래서 이제 막 시작한 작업이
|
|
2399
|
+
* 시작하자마자 한 스텝 진행된 것으로 계산됐고, **방출한 모션 앵커(startedAtSimMs)와 스냅샷이 한
|
|
2400
|
+
* tick 어긋났다**(적합성 하네스가 잡았다). 스텝이 커질수록 오차도 커진다.
|
|
2401
|
+
*/
|
|
2144
2402
|
processTasks(dt) {
|
|
2403
|
+
this.advanceTasks(dt);
|
|
2404
|
+
this.assignTasks();
|
|
2405
|
+
}
|
|
2406
|
+
assignTasks() {
|
|
2145
2407
|
for (const t of this.tasks.values()) {
|
|
2146
2408
|
if (t.status !== "created") continue;
|
|
2147
2409
|
const crew = this.claimPersonnel(t);
|
|
@@ -2151,32 +2413,40 @@ var FlowEngine = class {
|
|
|
2151
2413
|
if (t.intent === "dwell") {
|
|
2152
2414
|
t.status = "in-progress";
|
|
2153
2415
|
t.remainingMs = t.durationMs;
|
|
2416
|
+
t.startedAtSimMs = this.clockMs;
|
|
2154
2417
|
this.assignCrew(t, crew);
|
|
2155
2418
|
this.assignAssets(t, gear);
|
|
2156
2419
|
this.emitTask(t);
|
|
2157
2420
|
continue;
|
|
2158
2421
|
}
|
|
2159
2422
|
if (this.stationFull(t.toNode)) continue;
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2162
|
-
);
|
|
2163
|
-
if (!mover) continue;
|
|
2423
|
+
const rigs = this.claimEquipment(t);
|
|
2424
|
+
if (!rigs) continue;
|
|
2425
|
+
const mover = this.movers.get(rigs[0]);
|
|
2164
2426
|
if (t.setupMs && t.changeoverKey !== void 0 && mover.lastChangeoverKey !== void 0 && mover.lastChangeoverKey !== t.changeoverKey) {
|
|
2165
2427
|
t.appliedSetupMs = t.setupMs;
|
|
2166
2428
|
t.durationMs += t.setupMs;
|
|
2167
2429
|
}
|
|
2168
2430
|
if (t.changeoverKey !== void 0) mover.lastChangeoverKey = t.changeoverKey;
|
|
2169
|
-
|
|
2170
|
-
|
|
2431
|
+
for (const id of rigs) {
|
|
2432
|
+
const m = this.movers.get(id);
|
|
2433
|
+
m.status = "busy";
|
|
2434
|
+
m.taskId = t.id;
|
|
2435
|
+
}
|
|
2171
2436
|
t.status = "in-progress";
|
|
2172
2437
|
t.resource = mover.id;
|
|
2173
2438
|
t.remainingMs = t.durationMs;
|
|
2439
|
+
t.startedAtSimMs = this.clockMs;
|
|
2440
|
+
if (rigs.length > 1) t.resources = rigs.slice();
|
|
2174
2441
|
this.assignCrew(t, crew);
|
|
2175
2442
|
this.assignAssets(t, gear);
|
|
2176
2443
|
this.emitTask(t);
|
|
2177
2444
|
if (t.intent === "process") this.emitMover(mover);
|
|
2178
2445
|
else this.emitMover(mover, { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs, durationMs: t.durationMs, progress: 0, elapsedMs: 0 });
|
|
2179
2446
|
}
|
|
2447
|
+
}
|
|
2448
|
+
/** in-progress 진행/완료 — 상태 효과는 전부 도메인(onTaskComplete). base 는 자원 해제·델타만. */
|
|
2449
|
+
advanceTasks(dt) {
|
|
2180
2450
|
for (const t of this.tasks.values()) {
|
|
2181
2451
|
if (t.status !== "in-progress") continue;
|
|
2182
2452
|
if (t.resource && this.movers.get(t.resource)?.status === "down") continue;
|
|
@@ -2197,6 +2467,17 @@ var FlowEngine = class {
|
|
|
2197
2467
|
mover.status = "idle";
|
|
2198
2468
|
mover.taskId = null;
|
|
2199
2469
|
if (t.intent !== "process") mover.location = t.toNode;
|
|
2470
|
+
for (const id of t.resources ?? []) {
|
|
2471
|
+
if (id === mover.id) continue;
|
|
2472
|
+
const m = this.movers.get(id);
|
|
2473
|
+
if (!m) continue;
|
|
2474
|
+
m.setupMs += setup;
|
|
2475
|
+
m.runMs += t.durationMs - setup;
|
|
2476
|
+
m.status = "idle";
|
|
2477
|
+
m.taskId = null;
|
|
2478
|
+
if (t.intent !== "process") m.location = t.toNode;
|
|
2479
|
+
this.emitMover(m);
|
|
2480
|
+
}
|
|
2200
2481
|
this.emitTask(t);
|
|
2201
2482
|
this.emitMover(mover);
|
|
2202
2483
|
}
|
|
@@ -2225,7 +2506,8 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2225
2506
|
const qtyList = [{ epcClass: gtin, quantity: qty }];
|
|
2226
2507
|
const poTxn = [{ type: BTT.po, bizTransaction: po }];
|
|
2227
2508
|
const expiry = this.clockMs + SHELF_MS - this.epcSeq % 5 * SHELF_JITTER_MS;
|
|
2228
|
-
|
|
2509
|
+
const ilmd = { [ILMD_ATTR.expiry]: expiry };
|
|
2510
|
+
this.items.set(epc, { epc, gtin, qty, location: dock.id, disposition: DISP.in_progress, expiry, ilmd });
|
|
2229
2511
|
dock.occupancy++;
|
|
2230
2512
|
this.emit(transactionEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, bizTransactionList: poTxn, epcList: [epc], quantityList: qtyList, readPoint: dock.id }));
|
|
2231
2513
|
this.emit(aggregationEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, parentID: epc, childQuantityList: qtyList, readPoint: dock.id }));
|
|
@@ -2239,7 +2521,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2239
2521
|
readPoint: dock.id,
|
|
2240
2522
|
bizLocation: dock.id,
|
|
2241
2523
|
bizTransactionList: poTxn,
|
|
2242
|
-
ilmd
|
|
2524
|
+
ilmd
|
|
2243
2525
|
}));
|
|
2244
2526
|
const binId = this.policy.selectPlacement({ item: { epc, gtin, qty }, slots: this.slotViews("storage") });
|
|
2245
2527
|
if (!binId) return;
|
|
@@ -2314,12 +2596,12 @@ var WmsKernel = class extends FlowEngine {
|
|
|
2314
2596
|
const available = [...this.items.values()].filter((i) => i.gtin === line.gtin && i.disposition === DISP.sellable && this.nodes.get(i.location)?.type === "storage").map((i) => ({ epc: i.epc, location: i.location, qty: i.qty ?? 1, expiry: i.expiry }));
|
|
2315
2597
|
const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
|
|
2316
2598
|
for (const epc of chosen) {
|
|
2317
|
-
this.items.get(epc).disposition = DISP.reserved;
|
|
2318
2599
|
o.allocated.push(epc);
|
|
2319
2600
|
chosenAll.push(epc);
|
|
2320
2601
|
}
|
|
2321
2602
|
}
|
|
2322
2603
|
if (chosenAll.length === 0) return;
|
|
2604
|
+
this.reserve(chosenAll, BIZSTEP.storing);
|
|
2323
2605
|
this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: BIZSTEP.picking, bizTransactionList: [{ type: BTT.so, bizTransaction: o.bizTransaction }], epcList: chosenAll.slice() }));
|
|
2324
2606
|
for (const epc of chosenAll) {
|
|
2325
2607
|
const it = this.items.get(epc);
|
|
@@ -2463,7 +2745,7 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2463
2745
|
if (!staging || avail.length < CARGO_PER_TRAILER) return;
|
|
2464
2746
|
this.trailerCargo.set(trailer.epc, avail.slice(0, CARGO_PER_TRAILER).map((i) => i.epc));
|
|
2465
2747
|
}
|
|
2466
|
-
trailer.
|
|
2748
|
+
this.reserve([trailer.epc], bizStep);
|
|
2467
2749
|
this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep, bizTransactionList: [{ type: BTT_DELIVERY, bizTransaction: o.bizTransaction }], epcList: [trailer.epc], readPoint: door.id }));
|
|
2468
2750
|
const dockKind = mode === "drop" ? "pull" : "spot-live";
|
|
2469
2751
|
const task = { id: `task-${++this.taskSeq}`, kind: dockKind, status: "created", itemEpc: trailer.epc, fromNode: trailer.location, toNode: door.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: dockKind, fromNode: trailer.location, toNode: door.id }, TRAVEL_MS2), orderId: o.id };
|
|
@@ -2497,7 +2779,7 @@ var YmsKernel = class extends FlowEngine {
|
|
|
2497
2779
|
const outbound = order2?.kind === "appointment-out";
|
|
2498
2780
|
const staging = this.nodeByType("staging");
|
|
2499
2781
|
const cargo = this.trailerCargo.get(trailer.epc) ?? [];
|
|
2500
|
-
this.
|
|
2782
|
+
this.observeDisposition([trailer.epc], DISP.in_progress, outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading, to.id);
|
|
2501
2783
|
if (outbound) {
|
|
2502
2784
|
if (cargo.length && staging) {
|
|
2503
2785
|
this.aggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.loading, readPoint: to.id, consume: { readPoint: staging.id, disposition: DISP.in_transit } });
|
|
@@ -2623,10 +2905,8 @@ var MesKernel = class extends FlowEngine {
|
|
|
2623
2905
|
if (chosen.length < line.qty) return;
|
|
2624
2906
|
picks.push(...chosen);
|
|
2625
2907
|
}
|
|
2626
|
-
for (const epc of picks)
|
|
2627
|
-
|
|
2628
|
-
o.allocated.push(epc);
|
|
2629
|
-
}
|
|
2908
|
+
for (const epc of picks) o.allocated.push(epc);
|
|
2909
|
+
this.reserve(picks, MES_BIZSTEP.producing);
|
|
2630
2910
|
this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
|
|
2631
2911
|
this.emitStation(o, s0, o.allocated[0], product.gtin);
|
|
2632
2912
|
o.status = "op-" + s0.kind;
|
|
@@ -2723,10 +3003,8 @@ var MesKernel = class extends FlowEngine {
|
|
|
2723
3003
|
if (chosen.length < line.qty) return;
|
|
2724
3004
|
picks.push(...chosen);
|
|
2725
3005
|
}
|
|
2726
|
-
for (const epc of picks)
|
|
2727
|
-
|
|
2728
|
-
o.allocated.push(epc);
|
|
2729
|
-
}
|
|
3006
|
+
for (const epc of picks) o.allocated.push(epc);
|
|
3007
|
+
this.reserve(picks, MES_BIZSTEP.producing);
|
|
2730
3008
|
this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
|
|
2731
3009
|
this.emitStationDef(o, ops[0], o.allocated[0]);
|
|
2732
3010
|
o.status = "op-" + ops[0].key;
|
|
@@ -2797,8 +3075,10 @@ var MesKernel = class extends FlowEngine {
|
|
|
2797
3075
|
MES_PRODUCT_GTINS,
|
|
2798
3076
|
MES_TYPES,
|
|
2799
3077
|
MesKernel,
|
|
3078
|
+
NODE_SATURATION_NEAR,
|
|
2800
3079
|
OP_EVENT,
|
|
2801
3080
|
OP_PARAM,
|
|
3081
|
+
ObservedReducer,
|
|
2802
3082
|
StateProjector,
|
|
2803
3083
|
TwinHistory,
|
|
2804
3084
|
TwinObserver,
|
|
@@ -2827,6 +3107,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
2827
3107
|
lgtinClass,
|
|
2828
3108
|
mapRecord,
|
|
2829
3109
|
monteCarloForecast,
|
|
3110
|
+
nodeStatusOf,
|
|
2830
3111
|
objectEvent,
|
|
2831
3112
|
parseEpc,
|
|
2832
3113
|
parseIsoDuration,
|