@operato/twin-kernel 0.11.23 → 0.11.24
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/flow-engine.d.ts +3 -0
- package/dist/flow-engine.js +24 -8
- package/dist/kernel.js +35 -8
- package/dist/make-to-order.js +2 -1
- package/dist/mes-kernel.js +2 -2
- package/dist-cjs/index.cjs +164 -23
- package/package.json +2 -2
package/dist/flow-engine.d.ts
CHANGED
|
@@ -500,6 +500,9 @@ export declare class ItemStore {
|
|
|
500
500
|
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
501
501
|
*/
|
|
502
502
|
private byGtin;
|
|
503
|
+
/** 같은 글자를 매번 뜯지 않는다 — 클래스 글자의 종류는 적고 물품은 많다. */
|
|
504
|
+
private itemClassCache;
|
|
505
|
+
private itemClass;
|
|
503
506
|
get size(): number;
|
|
504
507
|
get(key: string): FlowItem | undefined;
|
|
505
508
|
has(key: string): boolean;
|
package/dist/flow-engine.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowLocation 단일 base 방향과 정합.)
|
|
11
11
|
*/
|
|
12
12
|
import { isWorkShiftExecutionBasis } from '@operato/ops-contract';
|
|
13
|
-
import { OP_EVENT, CMD, USE_UOM, locationStatusOf, readBoardEquipment, equipmentClassMembership, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, isOrderTerminal, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, identityGroundingOf, outsideLimit, computeOee } from '@operato/ops-contract';
|
|
13
|
+
import { OP_EVENT, CMD, USE_UOM, locationStatusOf, readBoardEquipment, equipmentClassMembership, readBoardLocations, readBoardAssets, classClosure, capabilityOf, requiredTestsFor, priorityRank, dueStatusOf, isOrderTerminal, effectivityAt, offCalendarAt, offCalendarReasonAt, minuteOfDayAt, activeShiftAt, subLotIdOf, itemKeyOf, itemClassOf, identityGroundingOf, outsideLimit, computeOee } from '@operato/ops-contract';
|
|
14
14
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
15
15
|
/* 주체를 정하는 규칙은 유입 문과 한 벌이다 — 두 곳에 적으면 한쪽만 고쳐진다. */
|
|
16
16
|
import { resolveSubject } from '@operato/ops-contract';
|
|
@@ -407,7 +407,21 @@ export class ItemStore {
|
|
|
407
407
|
* 색인을 밖에 따로 두지 않는다 — 갱신하는 자리가 흩어지면 한 자리라도 빠뜨렸을 때 **재고가 조용히
|
|
408
408
|
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
409
409
|
*/
|
|
410
|
+
/*
|
|
411
|
+
* 품목 색인 — 키는 **품목 클래스**다(`itemClassOf`). 재고는 로트 클래스를 들고 오더는 품목 클래스를 묻는다
|
|
412
|
+
* (ADR-0076 ⑥ · ADR-0082 결정 2). 로트 글자를 키로 쓰면 품목을 물어도 로트 재고가 안 나온다.
|
|
413
|
+
*/
|
|
410
414
|
byGtin = new Map();
|
|
415
|
+
/** 같은 글자를 매번 뜯지 않는다 — 클래스 글자의 종류는 적고 물품은 많다. */
|
|
416
|
+
itemClassCache = new Map();
|
|
417
|
+
itemClass(gtin) {
|
|
418
|
+
let c = this.itemClassCache.get(gtin);
|
|
419
|
+
if (c === undefined) {
|
|
420
|
+
c = itemClassOf(gtin) ?? gtin;
|
|
421
|
+
this.itemClassCache.set(gtin, c);
|
|
422
|
+
}
|
|
423
|
+
return c;
|
|
424
|
+
}
|
|
411
425
|
get size() {
|
|
412
426
|
return this.map.size;
|
|
413
427
|
}
|
|
@@ -515,7 +529,7 @@ export class ItemStore {
|
|
|
515
529
|
}
|
|
516
530
|
/** 그 품목인 물품들 — 색인이 답한다(자리를 모를 때 쓴다). */
|
|
517
531
|
ofGtin(gtin) {
|
|
518
|
-
const keys = this.byGtin.get(gtin);
|
|
532
|
+
const keys = this.byGtin.get(this.itemClass(gtin));
|
|
519
533
|
if (!keys)
|
|
520
534
|
return [];
|
|
521
535
|
const out = [];
|
|
@@ -549,7 +563,7 @@ export class ItemStore {
|
|
|
549
563
|
}
|
|
550
564
|
/* 품목 색인도 같은 규율로 본다 — 어긋나면 자재가 있는데 없다고 판정된다. */
|
|
551
565
|
for (const [key, it] of this.map) {
|
|
552
|
-
if (it.gtin && !this.byGtin.get(it.gtin)?.has(key))
|
|
566
|
+
if (it.gtin && !this.byGtin.get(this.itemClass(it.gtin))?.has(key))
|
|
553
567
|
drift.push(`${key} 이 품목 '${it.gtin}' 색인에 없다`);
|
|
554
568
|
}
|
|
555
569
|
for (const [g, keys] of this.byGtin) {
|
|
@@ -557,7 +571,7 @@ export class ItemStore {
|
|
|
557
571
|
const it = this.map.get(k);
|
|
558
572
|
if (!it)
|
|
559
573
|
drift.push(`${k} 이 지워졌는데 품목 '${g}' 색인에 남아 있다`);
|
|
560
|
-
else if (it.gtin !== g)
|
|
574
|
+
else if (this.itemClass(it.gtin ?? '') !== g)
|
|
561
575
|
drift.push(`${k} 은 품목 '${it.gtin}' 인데 '${g}' 색인에 있다`);
|
|
562
576
|
}
|
|
563
577
|
}
|
|
@@ -566,9 +580,10 @@ export class ItemStore {
|
|
|
566
580
|
index(key, location, gtin) {
|
|
567
581
|
this.indexLocation(key, location);
|
|
568
582
|
if (gtin) {
|
|
569
|
-
const
|
|
583
|
+
const g = this.itemClass(gtin);
|
|
584
|
+
const set = this.byGtin.get(g) ?? new Set();
|
|
570
585
|
set.add(key);
|
|
571
|
-
this.byGtin.set(
|
|
586
|
+
this.byGtin.set(g, set);
|
|
572
587
|
}
|
|
573
588
|
}
|
|
574
589
|
indexLocation(key, location) {
|
|
@@ -579,12 +594,13 @@ export class ItemStore {
|
|
|
579
594
|
unindex(key, location, gtin) {
|
|
580
595
|
this.unindexLocation(key, location);
|
|
581
596
|
if (gtin) {
|
|
582
|
-
const
|
|
597
|
+
const g = this.itemClass(gtin);
|
|
598
|
+
const set = this.byGtin.get(g);
|
|
583
599
|
if (!set)
|
|
584
600
|
return;
|
|
585
601
|
set.delete(key);
|
|
586
602
|
if (!set.size)
|
|
587
|
-
this.byGtin.delete(
|
|
603
|
+
this.byGtin.delete(g);
|
|
588
604
|
}
|
|
589
605
|
}
|
|
590
606
|
unindexLocation(key, location) {
|
package/dist/kernel.js
CHANGED
|
@@ -11,7 +11,7 @@ import { FlowEngine, allocatedQty } from "./flow-engine.js";
|
|
|
11
11
|
import { planMakeToOrder } from "./make-to-order.js";
|
|
12
12
|
import { BIZSTEP, BTT, WMS_ORDER_KIND } from '@operato/ops-contract';
|
|
13
13
|
import { DISP, ILMD_ATTR, aggregationEvent, objectEvent, transactionEvent } from '@operato/ops-contract';
|
|
14
|
-
import { subLotIdOf, itemKeyOf } from '@operato/ops-contract';
|
|
14
|
+
import { subLotIdOf, itemKeyOf, sameItemClass, lotClassOfItem } from '@operato/ops-contract';
|
|
15
15
|
const TRAVEL_MS = 30_000;
|
|
16
16
|
const SHELF_MS = 30 * 24 * 3_600_000; // 기본 유통기한(30일)
|
|
17
17
|
const SHELF_JITTER_MS = 5 * 24 * 3_600_000; // 로트별 만료 편차(FEFO 가 FIFO 와 갈리게)
|
|
@@ -57,8 +57,30 @@ export class WmsKernel extends FlowEngine {
|
|
|
57
57
|
/* 발주 문서 식별자 — 선언된 이름공간 아래(CBV §8.5.5) 또는 선언된 GDTI 문서 타입. */
|
|
58
58
|
const po = this.requireBizTransactionId(`PO-${poSeq}`, 'purchaseorder');
|
|
59
59
|
const eventTime = this.now();
|
|
60
|
-
|
|
60
|
+
/*
|
|
61
|
+
* ── 도착은 로트를 든다 (2026-09-24, ADR-0076 ⑥ · ADR-0082 결정 2) ──────────
|
|
62
|
+
* 로트는 발주 하나에 하나다(`L-<발주 번호>`) — rng 를 더 쓰지 않으므로 같은 씨앗이면 같은 사건이다.
|
|
63
|
+
* 케이스의 클래스는 계약이 만든 로트 클래스이고(`lotClassOfItem` → `lotClassUri`), plant · warehouse 가 보내는
|
|
64
|
+
* 로트와 같은 글자다. 오더 줄은 품목을 묻고 재고는 로트를 들므로 맞춤은 `sameItemClass` 가 한다.
|
|
65
|
+
*
|
|
66
|
+
* 선언된 품목이 로트를 붙일 수 없는 모양이면(식별자가 아닌 낱말) 품목 클래스 그대로 싣는다 — 로트를
|
|
67
|
+
* 말하지 않을 뿐 거짓은 없다. 자리 수가 틀린 GTIN 은 시나리오 검사가 선언에서 거절한다.
|
|
68
|
+
*/
|
|
69
|
+
const lot = `L-${poSeq}`;
|
|
70
|
+
const lotClass = lotClassOfItem(gtin, lot) ?? gtin;
|
|
71
|
+
const qtyList = [{ epcClass: lotClass, quantity: qty }];
|
|
61
72
|
const poTxn = [{ type: BTT.po, bizTransaction: po }];
|
|
73
|
+
/*
|
|
74
|
+
* **입고 오더가 도착보다 먼저 선다**(ADR-0076 ⑥, 판정 2026-09-24 — 물음 2 의 A). 「무엇이 오기로 되어
|
|
75
|
+
* 있었나」가 저널에 먼저 있어야 미러도 같은 순서로 안다. 리드 타임은 쓰는 소비자가 생길 때 연다.
|
|
76
|
+
* 배차 · 흐름 지표는 받는 오더를 이미 뺀다(`isFlowOrder`).
|
|
77
|
+
*/
|
|
78
|
+
const inbound = {
|
|
79
|
+
id: `inbound-${poSeq}`, kind: WMS_ORDER_KIND.inbound, status: 'created', requested: qty, fulfilled: 0, bizTransaction: po,
|
|
80
|
+
allocated: [], picked: [], shipmentEpc: null, lines: [{ gtin, requested: qty }]
|
|
81
|
+
};
|
|
82
|
+
this.orders.set(inbound.id, inbound);
|
|
83
|
+
this.emitOrder(inbound);
|
|
62
84
|
// 로트 만료(FEFO 용, 결정적 — rng 무소비로 byte-identical 유지). 편차로 도착순≠만료순 → FEFO 가 유의미.
|
|
63
85
|
const expiry = this.clockMs + SHELF_MS - (this.epcSeq % 5) * SHELF_JITTER_MS;
|
|
64
86
|
/* 방출한 마스터데이터를 **상태에도 들고 있는다** — 자기가 선언한 것을 자기가 모르면, 미러는 알고
|
|
@@ -68,7 +90,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
68
90
|
* 들어온 시각을 재고가 들고 있는다 — 「먼저 들어온 것 먼저」를 할 수 있는 유일한 근거다.
|
|
69
91
|
* 값은 **receiving 이 일어난 시각**이고(수집 시각이 아니다), 여기서는 그 둘이 같다.
|
|
70
92
|
*/
|
|
71
|
-
this.items.set(epc, { epc, gtin, qty, location: dock.id, disposition: DISP.in_progress, expiry, ilmd, receivedAtMs: this.nowMs() });
|
|
93
|
+
this.items.set(epc, { epc, gtin: lotClass, qty, location: dock.id, disposition: DISP.in_progress, expiry, ilmd, receivedAtMs: this.nowMs() });
|
|
72
94
|
dock.occupancy++;
|
|
73
95
|
this.emit(transactionEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.receiving, bizTransactionList: poTxn, epcList: [epc], quantityList: qtyList, readPoint: dock.id }));
|
|
74
96
|
this.emit(aggregationEvent({ eventTime, action: 'ADD', bizStep: BIZSTEP.receiving, parentID: epc, childQuantityList: qtyList, readPoint: dock.id }));
|
|
@@ -79,6 +101,11 @@ export class WmsKernel extends FlowEngine {
|
|
|
79
101
|
epcList: [epc], quantityList: qtyList, readPoint: dock.id, bizLocation: dock.id, bizTransactionList: poTxn,
|
|
80
102
|
ilmd
|
|
81
103
|
}));
|
|
104
|
+
/* 수령했으므로 입고 오더를 이행하고 닫는다 — 시뮬에서 입고 오더를 닫는 것은 커널이다(ADR-0076 ⑥). */
|
|
105
|
+
inbound.fulfilled = qty;
|
|
106
|
+
inbound.status = 'completed';
|
|
107
|
+
this.emitOrder(inbound);
|
|
108
|
+
/* 적치는 품목으로 판단한다(같은 품목을 모으는 정책) — 로트가 아니라 품목 클래스를 넘긴다. */
|
|
82
109
|
const binId = this.policy.selectPlacement({ item: { epc, gtin, qty }, slots: this.slotViews('storage') });
|
|
83
110
|
if (!binId)
|
|
84
111
|
return; // 수용 불가 → 도크 대기
|
|
@@ -156,7 +183,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
156
183
|
const staging = this.builtInLocation('staging', 'picked pallets wait here before shipping');
|
|
157
184
|
const chosenAll = [];
|
|
158
185
|
for (const line of o.lines) {
|
|
159
|
-
const already = allocatedQty(o.allocated.filter(a => this.items.get(a.epc)?.gtin
|
|
186
|
+
const already = allocatedQty(o.allocated.filter(a => sameItemClass(this.items.get(a.epc)?.gtin, line.gtin)));
|
|
160
187
|
const need = line.requested - already;
|
|
161
188
|
if (need <= 0)
|
|
162
189
|
continue;
|
|
@@ -171,7 +198,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
171
198
|
* 모두 표현할 수 있고, **단위를 정하는 것은 부르는 쪽**이다.
|
|
172
199
|
*/
|
|
173
200
|
const available = [...this.items.values()]
|
|
174
|
-
.filter(i => i.gtin
|
|
201
|
+
.filter(i => sameItemClass(i.gtin, line.gtin) && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === 'storage')
|
|
175
202
|
.filter(i => this.usableLot(i))
|
|
176
203
|
.map(i => ({ epc: i.epc, location: i.location, qty: 1, expiry: i.expiry, ...(i.receivedAtMs !== undefined ? { receivedAtMs: i.receivedAtMs } : {}) }));
|
|
177
204
|
const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
|
|
@@ -264,7 +291,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
264
291
|
const locations = [...this.locations.values()].map(l => ({ id: l.id, type: l.type }));
|
|
265
292
|
const ops = this.declaredOperations();
|
|
266
293
|
for (const line of o.lines) {
|
|
267
|
-
const have = stock.filter(s => s.gtin
|
|
294
|
+
const have = stock.filter(s => sameItemClass(s.gtin, line.gtin) && s.sellable && this.locations.get(s.location)?.type === 'storage')
|
|
268
295
|
.reduce((n, s) => n + s.qty, 0);
|
|
269
296
|
const short = line.requested - have;
|
|
270
297
|
if (short <= 0)
|
|
@@ -301,7 +328,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
301
328
|
}
|
|
302
329
|
/** 부품을 작업대로 — 팔레트 이동이므로 피킹과 같은 기제다(부분 소비는 코어가 한다). */
|
|
303
330
|
issueFeed(o, gtin, from, to, qty) {
|
|
304
|
-
const src = [...this.items.values()].find(i => i.gtin
|
|
331
|
+
const src = [...this.items.values()].find(i => sameItemClass(i.gtin, gtin) && i.location === from && i.disposition === DISP.sellable);
|
|
305
332
|
if (!src)
|
|
306
333
|
return;
|
|
307
334
|
/* 이송 중에 다른 오더가 같은 팔레트를 집지 않게 예약으로 바꾼다 — 코어의 자재 게이트는
|
|
@@ -389,7 +416,7 @@ export class WmsKernel extends FlowEngine {
|
|
|
389
416
|
if (order.lines)
|
|
390
417
|
for (const epc of order.picked) {
|
|
391
418
|
const g = this.items.get(epc)?.gtin;
|
|
392
|
-
const line = order.lines.find(l => l.gtin
|
|
419
|
+
const line = order.lines.find(l => sameItemClass(l.gtin, g) && l.requested > 0);
|
|
393
420
|
if (line)
|
|
394
421
|
line.requested--;
|
|
395
422
|
}
|
package/dist/make-to-order.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* 그래서 한 오더가 부르는 것은 세 걸음이다: **부품 이송 → 가공 → 되돌리기.** 이 모듈은 그중
|
|
22
22
|
* "지금 무엇을 발행해야 하나" 를 답한다 — 이송이 아직 안 끝났으면 가공을 발행하지 않는다.
|
|
23
23
|
*/
|
|
24
|
+
import { sameItemClass } from '@operato/ops-contract';
|
|
24
25
|
/** 품목 → 그것을 산출하는 공정. 선언에서 파생한다(코드에 표를 두지 않는다). */
|
|
25
26
|
export function producedByIndex(ops) {
|
|
26
27
|
const idx = new Map();
|
|
@@ -113,6 +114,6 @@ export function planMakeToOrder(gtin, shortQty, ops, stock, locations) {
|
|
|
113
114
|
/** 이 재고가 명세를 만족하나 — 품목 지목만 본다(등급 상속은 엔진이 안다). */
|
|
114
115
|
function matches(s, req) {
|
|
115
116
|
if (req.materialDefinition)
|
|
116
|
-
return s.gtin
|
|
117
|
+
return sameItemClass(s.gtin, req.materialDefinition);
|
|
117
118
|
return false;
|
|
118
119
|
}
|
package/dist/mes-kernel.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ③ 셋업/체인지오버 = work-center 가 제품 전환 시 셋업(changeoverKey=제품 gtin) → OEE Availability.
|
|
9
9
|
* ④ OEE = base 가 설비(설비)별 계측(가동/셋업/기아/품질). cut=cutter·weld=welder 이종 자원.
|
|
10
10
|
*/
|
|
11
|
-
import { identityGroundingOf, procedureViolations, isRecipeExecutionBasis } from '@operato/ops-contract';
|
|
11
|
+
import { identityGroundingOf, procedureViolations, isRecipeExecutionBasis, sameItemClass } from '@operato/ops-contract';
|
|
12
12
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
13
13
|
import { FlowEngine, allocatedEpcs, allocatedQty } from "./flow-engine.js";
|
|
14
14
|
import { DISP, objectEvent, transactionEvent, gdtiUri, sgtinClass, bizTransactionUri } from '@operato/ops-contract';
|
|
@@ -820,7 +820,7 @@ export class MesKernel extends FlowEngine {
|
|
|
820
820
|
if (fromType) {
|
|
821
821
|
for (const n of locsOfType.get(fromType) ?? []) {
|
|
822
822
|
for (const i of this.items.at(n.id)) {
|
|
823
|
-
if (i.gtin
|
|
823
|
+
if (sameItemClass(i.gtin, g) && i.disposition === DISP.sellable)
|
|
824
824
|
available.push({ epc: i.epc, location: i.location, qty: 1 });
|
|
825
825
|
}
|
|
826
826
|
}
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -1629,12 +1629,19 @@ var ILMD_ATTR = {
|
|
|
1629
1629
|
/** 로트·배치 번호(직렬 개체에 로트를 붙일 때). */
|
|
1630
1630
|
lot: "cbvmda:lotNumber"
|
|
1631
1631
|
};
|
|
1632
|
+
function lgtinClass(companyPrefix, itemRefAndIndicator, lot) {
|
|
1633
|
+
return `urn:epc:class:lgtin:${companyPrefix}.${itemRefAndIndicator}.${encodeURIComponent(lot)}`;
|
|
1634
|
+
}
|
|
1632
1635
|
function parseEpc(uri) {
|
|
1633
1636
|
const raw = String(uri ?? "");
|
|
1634
1637
|
if (/(?:\/obj\/|:obj:)[^/:\s]+$/.test(raw))
|
|
1635
1638
|
return { scheme: "unknown", instance: true, uri: raw };
|
|
1636
|
-
|
|
1637
|
-
|
|
1639
|
+
const inHouseClass = raw.match(/(?:\/class\/|:class:)([^/:\s]+)$/);
|
|
1640
|
+
if (inHouseClass) {
|
|
1641
|
+
const at = inHouseClass[1].indexOf(LOT_SEPARATOR);
|
|
1642
|
+
const split = at > 0 ? decodedPair(inHouseClass[1].slice(0, at), inHouseClass[1].slice(at + LOT_SEPARATOR.length)) : void 0;
|
|
1643
|
+
return split ? { scheme: "unknown", instance: false, item: split[0], lot: split[1], uri: raw } : { scheme: "unknown", instance: false, uri: raw };
|
|
1644
|
+
}
|
|
1638
1645
|
const cls = raw.match(/^urn:epc:class:lgtin:(.+)$/);
|
|
1639
1646
|
if (cls) {
|
|
1640
1647
|
const seg = cls[1].split(".");
|
|
@@ -1666,12 +1673,83 @@ function parseEpc(uri) {
|
|
|
1666
1673
|
}
|
|
1667
1674
|
return { scheme: "unknown", instance: false, uri: raw };
|
|
1668
1675
|
}
|
|
1676
|
+
function decodedPair(item, lot) {
|
|
1677
|
+
if (!lot)
|
|
1678
|
+
return void 0;
|
|
1679
|
+
try {
|
|
1680
|
+
return [decodeURIComponent(item), decodeURIComponent(lot)];
|
|
1681
|
+
} catch {
|
|
1682
|
+
return void 0;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function itemClassOf(uri) {
|
|
1686
|
+
if (!uri)
|
|
1687
|
+
return void 0;
|
|
1688
|
+
const parsed = parseEpc(uri);
|
|
1689
|
+
if (parsed.scheme === "lgtin" && parsed.gtinKey)
|
|
1690
|
+
return `urn:epc:idpat:sgtin:${parsed.gtinKey}.*`;
|
|
1691
|
+
if (parsed.item !== void 0) {
|
|
1692
|
+
const marker = /^(.*(?:\/class\/|:class:))[^/:\s]+$/.exec(parsed.uri);
|
|
1693
|
+
if (marker)
|
|
1694
|
+
return `${marker[1]}${parsed.item.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c])}`;
|
|
1695
|
+
}
|
|
1696
|
+
return parsed.uri;
|
|
1697
|
+
}
|
|
1698
|
+
function lotClassOfItem(itemClass, lot) {
|
|
1699
|
+
if (!itemClass)
|
|
1700
|
+
return void 0;
|
|
1701
|
+
const idpat = /^urn:epc:idpat:sgtin:(\d+)\.(\d+)\.\*$/.exec(itemClass);
|
|
1702
|
+
if (idpat)
|
|
1703
|
+
return lotClassUri({ namespace: "", item: "", lot, gtin: `${idpat[1]}.${idpat[2]}` });
|
|
1704
|
+
const parsed = parseEpc(itemClass);
|
|
1705
|
+
if (parsed.instance || parsed.lot !== void 0)
|
|
1706
|
+
return void 0;
|
|
1707
|
+
const url = /^(https?:\/\/.+)\/class\/([^/:\s]+)$/.exec(itemClass);
|
|
1708
|
+
const urn = /^(urn:.+):class:([^/:\s]+)$/.exec(itemClass);
|
|
1709
|
+
const at = url ?? urn;
|
|
1710
|
+
if (!at)
|
|
1711
|
+
return void 0;
|
|
1712
|
+
let item;
|
|
1713
|
+
try {
|
|
1714
|
+
item = decodeURIComponent(at[2]);
|
|
1715
|
+
} catch {
|
|
1716
|
+
return void 0;
|
|
1717
|
+
}
|
|
1718
|
+
return lotClassUri({ namespace: at[1], item, lot });
|
|
1719
|
+
}
|
|
1720
|
+
function sameItemClass(a, b) {
|
|
1721
|
+
const x = itemClassOf(a);
|
|
1722
|
+
return x !== void 0 && x === itemClassOf(b);
|
|
1723
|
+
}
|
|
1669
1724
|
function gdtiUri(companyPrefix, docType, serial) {
|
|
1670
1725
|
return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
|
|
1671
1726
|
}
|
|
1672
1727
|
function objectUri(namespace, objId) {
|
|
1673
1728
|
return underNamespace(namespace, "obj", objId);
|
|
1674
1729
|
}
|
|
1730
|
+
function lotClassUri(input) {
|
|
1731
|
+
const item = String(input.item ?? "");
|
|
1732
|
+
const lot = String(input.lot ?? "");
|
|
1733
|
+
if (!lot)
|
|
1734
|
+
throw new Error(`lotClassUri: \uB85C\uD2B8\uAC00 \uBE44\uC5C8\uB2E4 \u2014 \uB85C\uD2B8 \uC5C6\uB294 \uD488\uBAA9\uC740 classUri \uB85C \uBD80\uB978\uB2E4 (item=${item})`);
|
|
1735
|
+
if (input.gtin !== void 0) {
|
|
1736
|
+
const [prefix, ref, ...rest] = String(input.gtin).split(".");
|
|
1737
|
+
if (!prefix || !ref || rest.length) {
|
|
1738
|
+
throw new Error(`lotClassUri: gtin \uC740 'CompanyPrefix.ItemRefAndIndicator' \uC5EC\uC57C \uD55C\uB2E4: ${input.gtin}`);
|
|
1739
|
+
}
|
|
1740
|
+
const uri = lgtinClass(prefix, ref, lot);
|
|
1741
|
+
const wrong = gs1KeyDigitViolation(uri);
|
|
1742
|
+
if (wrong)
|
|
1743
|
+
throw new Error(`lotClassUri: ${wrong}`);
|
|
1744
|
+
return uri;
|
|
1745
|
+
}
|
|
1746
|
+
if (!item)
|
|
1747
|
+
throw new Error(`lotClassUri: \uD488\uBC88\uC774 \uBE44\uC5C8\uB2E4 (lot=${lot})`);
|
|
1748
|
+
const enc = (v) => v.replace(LOT_COMPONENT_BREAKING, (c) => PERCENT_ENCODED[c]);
|
|
1749
|
+
return joinUnderNamespace(input.namespace, "class", `${enc(item)}${LOT_SEPARATOR}${enc(lot)}`);
|
|
1750
|
+
}
|
|
1751
|
+
var LOT_SEPARATOR = ";lot=";
|
|
1752
|
+
var LOT_COMPONENT_BREAKING = /[%/:?# ;]/g;
|
|
1675
1753
|
function bizTransactionUri(namespace, transId) {
|
|
1676
1754
|
return underNamespace(namespace, "bt", transId);
|
|
1677
1755
|
}
|
|
@@ -1682,16 +1760,20 @@ var PERCENT_ENCODED = {
|
|
|
1682
1760
|
":": "%3A",
|
|
1683
1761
|
"?": "%3F",
|
|
1684
1762
|
"#": "%23",
|
|
1685
|
-
" ": "%20"
|
|
1763
|
+
" ": "%20",
|
|
1764
|
+
/* 로트 클래스의 가르는 글자 — `lotClassUri` 만 쓴다(§`LOT_COMPONENT_BREAKING`). 표는 한 벌이다. */
|
|
1765
|
+
";": "%3B"
|
|
1686
1766
|
};
|
|
1687
1767
|
function underNamespace(namespace, marker, id) {
|
|
1688
|
-
const ns = namespace?.trim();
|
|
1689
|
-
if (!ns)
|
|
1690
|
-
return void 0;
|
|
1691
1768
|
const raw = String(id);
|
|
1692
1769
|
if (!raw)
|
|
1693
1770
|
return void 0;
|
|
1694
|
-
|
|
1771
|
+
return joinUnderNamespace(namespace, marker, raw.replace(STRUCTURE_BREAKING, (c) => PERCENT_ENCODED[c]));
|
|
1772
|
+
}
|
|
1773
|
+
function joinUnderNamespace(namespace, marker, v) {
|
|
1774
|
+
const ns = namespace?.trim();
|
|
1775
|
+
if (!ns)
|
|
1776
|
+
return void 0;
|
|
1695
1777
|
if (/^https?:\/\/[^/\s]+/.test(ns))
|
|
1696
1778
|
return `${ns.replace(/\/+$/, "")}/${marker}/${v}`;
|
|
1697
1779
|
if (/^urn:epc(global)?:/.test(ns))
|
|
@@ -1802,6 +1884,30 @@ function transformationEvent(p) {
|
|
|
1802
1884
|
e.bizTransactionList = p.bizTransactionList;
|
|
1803
1885
|
return e;
|
|
1804
1886
|
}
|
|
1887
|
+
var GS1_KEY_DIGITS = { sgtin: 13, lgtin: 13, sscc: 17, grai: 12, gdti: 12, sgln: 12 };
|
|
1888
|
+
function gs1KeyDigitViolation(uri) {
|
|
1889
|
+
if (!uri)
|
|
1890
|
+
return void 0;
|
|
1891
|
+
const m = /^urn:epc:(?:id|idpat|class):([a-z]+):([^:]+)$/.exec(uri);
|
|
1892
|
+
if (!m)
|
|
1893
|
+
return void 0;
|
|
1894
|
+
const want = GS1_KEY_DIGITS[m[1]];
|
|
1895
|
+
if (!want)
|
|
1896
|
+
return void 0;
|
|
1897
|
+
const parts = m[2].split(".");
|
|
1898
|
+
if (parts.length < 2)
|
|
1899
|
+
return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uAC00 '.' \uB85C \uAC08\uB824\uC57C \uD55C\uB2E4`;
|
|
1900
|
+
const [prefix, ref] = parts;
|
|
1901
|
+
if (ref === "*")
|
|
1902
|
+
return void 0;
|
|
1903
|
+
if (!/^\d+$/.test(prefix) || !/^\d+$/.test(ref)) {
|
|
1904
|
+
return `${m[1]} \uD615\uC2DD \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4\uC640 \uCC38\uC870\uB294 \uC22B\uC790\uB2E4`;
|
|
1905
|
+
}
|
|
1906
|
+
const got = prefix.length + ref.length;
|
|
1907
|
+
if (got === want)
|
|
1908
|
+
return void 0;
|
|
1909
|
+
return `${m[1]} \uC790\uB9AC \uC218 \uC624\uB958: ${uri} \u2014 \uD68C\uC0AC \uD504\uB9AC\uD53D\uC2A4(${prefix.length}) + \uCC38\uC870(${ref.length}) = ${got} \uC774\uC9C0\uB9CC ${want} \uC5EC\uC57C \uD55C\uB2E4(GS1 TDS). \uD504\uB9AC\uD53D\uC2A4\uB97C \uBC14\uAFB8\uBA74 \uCC38\uC870 \uC790\uB9AC \uC218\uB97C \uD568\uAED8 \uB9DE\uCDB0\uC57C \uD55C\uB2E4.`;
|
|
1910
|
+
}
|
|
1805
1911
|
|
|
1806
1912
|
// ../ops-contract/dist/iso-duration.js
|
|
1807
1913
|
var RE = /^(-)?P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
|
|
@@ -4033,7 +4139,21 @@ var ItemStore = class _ItemStore {
|
|
|
4033
4139
|
* 색인을 밖에 따로 두지 않는다 — 갱신하는 자리가 흩어지면 한 자리라도 빠뜨렸을 때 **재고가 조용히
|
|
4034
4140
|
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
4035
4141
|
*/
|
|
4142
|
+
/*
|
|
4143
|
+
* 품목 색인 — 키는 **품목 클래스**다(`itemClassOf`). 재고는 로트 클래스를 들고 오더는 품목 클래스를 묻는다
|
|
4144
|
+
* (ADR-0076 ⑥ · ADR-0082 결정 2). 로트 글자를 키로 쓰면 품목을 물어도 로트 재고가 안 나온다.
|
|
4145
|
+
*/
|
|
4036
4146
|
byGtin = /* @__PURE__ */ new Map();
|
|
4147
|
+
/** 같은 글자를 매번 뜯지 않는다 — 클래스 글자의 종류는 적고 물품은 많다. */
|
|
4148
|
+
itemClassCache = /* @__PURE__ */ new Map();
|
|
4149
|
+
itemClass(gtin) {
|
|
4150
|
+
let c = this.itemClassCache.get(gtin);
|
|
4151
|
+
if (c === void 0) {
|
|
4152
|
+
c = itemClassOf(gtin) ?? gtin;
|
|
4153
|
+
this.itemClassCache.set(gtin, c);
|
|
4154
|
+
}
|
|
4155
|
+
return c;
|
|
4156
|
+
}
|
|
4037
4157
|
get size() {
|
|
4038
4158
|
return this.map.size;
|
|
4039
4159
|
}
|
|
@@ -4123,7 +4243,7 @@ var ItemStore = class _ItemStore {
|
|
|
4123
4243
|
}
|
|
4124
4244
|
/** 그 품목인 물품들 — 색인이 답한다(자리를 모를 때 쓴다). */
|
|
4125
4245
|
ofGtin(gtin) {
|
|
4126
|
-
const keys = this.byGtin.get(gtin);
|
|
4246
|
+
const keys = this.byGtin.get(this.itemClass(gtin));
|
|
4127
4247
|
if (!keys) return [];
|
|
4128
4248
|
const out = [];
|
|
4129
4249
|
for (const k of keys) {
|
|
@@ -4151,13 +4271,13 @@ var ItemStore = class _ItemStore {
|
|
|
4151
4271
|
}
|
|
4152
4272
|
}
|
|
4153
4273
|
for (const [key, it] of this.map) {
|
|
4154
|
-
if (it.gtin && !this.byGtin.get(it.gtin)?.has(key)) drift.push(`${key} \uC774 \uD488\uBAA9 '${it.gtin}' \uC0C9\uC778\uC5D0 \uC5C6\uB2E4`);
|
|
4274
|
+
if (it.gtin && !this.byGtin.get(this.itemClass(it.gtin))?.has(key)) drift.push(`${key} \uC774 \uD488\uBAA9 '${it.gtin}' \uC0C9\uC778\uC5D0 \uC5C6\uB2E4`);
|
|
4155
4275
|
}
|
|
4156
4276
|
for (const [g, keys] of this.byGtin) {
|
|
4157
4277
|
for (const k of keys) {
|
|
4158
4278
|
const it = this.map.get(k);
|
|
4159
4279
|
if (!it) drift.push(`${k} \uC774 \uC9C0\uC6CC\uC84C\uB294\uB370 \uD488\uBAA9 '${g}' \uC0C9\uC778\uC5D0 \uB0A8\uC544 \uC788\uB2E4`);
|
|
4160
|
-
else if (it.gtin !== g) drift.push(`${k} \uC740 \uD488\uBAA9 '${it.gtin}' \uC778\uB370 '${g}' \uC0C9\uC778\uC5D0 \uC788\uB2E4`);
|
|
4280
|
+
else if (this.itemClass(it.gtin ?? "") !== g) drift.push(`${k} \uC740 \uD488\uBAA9 '${it.gtin}' \uC778\uB370 '${g}' \uC0C9\uC778\uC5D0 \uC788\uB2E4`);
|
|
4161
4281
|
}
|
|
4162
4282
|
}
|
|
4163
4283
|
return drift;
|
|
@@ -4165,9 +4285,10 @@ var ItemStore = class _ItemStore {
|
|
|
4165
4285
|
index(key, location, gtin) {
|
|
4166
4286
|
this.indexLocation(key, location);
|
|
4167
4287
|
if (gtin) {
|
|
4168
|
-
const
|
|
4288
|
+
const g = this.itemClass(gtin);
|
|
4289
|
+
const set = this.byGtin.get(g) ?? /* @__PURE__ */ new Set();
|
|
4169
4290
|
set.add(key);
|
|
4170
|
-
this.byGtin.set(
|
|
4291
|
+
this.byGtin.set(g, set);
|
|
4171
4292
|
}
|
|
4172
4293
|
}
|
|
4173
4294
|
indexLocation(key, location) {
|
|
@@ -4178,10 +4299,11 @@ var ItemStore = class _ItemStore {
|
|
|
4178
4299
|
unindex(key, location, gtin) {
|
|
4179
4300
|
this.unindexLocation(key, location);
|
|
4180
4301
|
if (gtin) {
|
|
4181
|
-
const
|
|
4302
|
+
const g = this.itemClass(gtin);
|
|
4303
|
+
const set = this.byGtin.get(g);
|
|
4182
4304
|
if (!set) return;
|
|
4183
4305
|
set.delete(key);
|
|
4184
|
-
if (!set.size) this.byGtin.delete(
|
|
4306
|
+
if (!set.size) this.byGtin.delete(g);
|
|
4185
4307
|
}
|
|
4186
4308
|
}
|
|
4187
4309
|
unindexLocation(key, location) {
|
|
@@ -7276,7 +7398,7 @@ function planMakeToOrder(gtin, shortQty, ops, stock, locations) {
|
|
|
7276
7398
|
return { steps: [{ step: "process", operation: op.key, at: station.id }] };
|
|
7277
7399
|
}
|
|
7278
7400
|
function matches(s, req) {
|
|
7279
|
-
if (req.materialDefinition) return s.gtin
|
|
7401
|
+
if (req.materialDefinition) return sameItemClass(s.gtin, req.materialDefinition);
|
|
7280
7402
|
return false;
|
|
7281
7403
|
}
|
|
7282
7404
|
|
|
@@ -7323,11 +7445,27 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7323
7445
|
const poSeq = ++this.poSeq;
|
|
7324
7446
|
const po = this.requireBizTransactionId(`PO-${poSeq}`, "purchaseorder");
|
|
7325
7447
|
const eventTime = this.now();
|
|
7326
|
-
const
|
|
7448
|
+
const lot = `L-${poSeq}`;
|
|
7449
|
+
const lotClass = lotClassOfItem(gtin, lot) ?? gtin;
|
|
7450
|
+
const qtyList = [{ epcClass: lotClass, quantity: qty }];
|
|
7327
7451
|
const poTxn = [{ type: BTT.po, bizTransaction: po }];
|
|
7452
|
+
const inbound = {
|
|
7453
|
+
id: `inbound-${poSeq}`,
|
|
7454
|
+
kind: WMS_ORDER_KIND.inbound,
|
|
7455
|
+
status: "created",
|
|
7456
|
+
requested: qty,
|
|
7457
|
+
fulfilled: 0,
|
|
7458
|
+
bizTransaction: po,
|
|
7459
|
+
allocated: [],
|
|
7460
|
+
picked: [],
|
|
7461
|
+
shipmentEpc: null,
|
|
7462
|
+
lines: [{ gtin, requested: qty }]
|
|
7463
|
+
};
|
|
7464
|
+
this.orders.set(inbound.id, inbound);
|
|
7465
|
+
this.emitOrder(inbound);
|
|
7328
7466
|
const expiry = this.clockMs + SHELF_MS - this.epcSeq % 5 * SHELF_JITTER_MS;
|
|
7329
7467
|
const ilmd = { [ILMD_ATTR.expiry]: expiry };
|
|
7330
|
-
this.items.set(epc, { epc, gtin, qty, location: dock.id, disposition: DISP.in_progress, expiry, ilmd, receivedAtMs: this.nowMs() });
|
|
7468
|
+
this.items.set(epc, { epc, gtin: lotClass, qty, location: dock.id, disposition: DISP.in_progress, expiry, ilmd, receivedAtMs: this.nowMs() });
|
|
7331
7469
|
dock.occupancy++;
|
|
7332
7470
|
this.emit(transactionEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, bizTransactionList: poTxn, epcList: [epc], quantityList: qtyList, readPoint: dock.id }));
|
|
7333
7471
|
this.emit(aggregationEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, parentID: epc, childQuantityList: qtyList, readPoint: dock.id }));
|
|
@@ -7343,6 +7481,9 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7343
7481
|
bizTransactionList: poTxn,
|
|
7344
7482
|
ilmd
|
|
7345
7483
|
}));
|
|
7484
|
+
inbound.fulfilled = qty;
|
|
7485
|
+
inbound.status = "completed";
|
|
7486
|
+
this.emitOrder(inbound);
|
|
7346
7487
|
const binId = this.policy.selectPlacement({ item: { epc, gtin, qty }, slots: this.slotViews("storage") });
|
|
7347
7488
|
if (!binId) return;
|
|
7348
7489
|
const id = `task-${++this.taskSeq}`;
|
|
@@ -7421,10 +7562,10 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7421
7562
|
const staging = this.builtInLocation("staging", "picked pallets wait here before shipping");
|
|
7422
7563
|
const chosenAll = [];
|
|
7423
7564
|
for (const line of o.lines) {
|
|
7424
|
-
const already = allocatedQty(o.allocated.filter((a) => this.items.get(a.epc)?.gtin
|
|
7565
|
+
const already = allocatedQty(o.allocated.filter((a) => sameItemClass(this.items.get(a.epc)?.gtin, line.gtin)));
|
|
7425
7566
|
const need = line.requested - already;
|
|
7426
7567
|
if (need <= 0) continue;
|
|
7427
|
-
const available = [...this.items.values()].filter((i) => i.gtin
|
|
7568
|
+
const available = [...this.items.values()].filter((i) => sameItemClass(i.gtin, line.gtin) && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === "storage").filter((i) => this.usableLot(i)).map((i) => ({ epc: i.epc, location: i.location, qty: 1, expiry: i.expiry, ...i.receivedAtMs !== void 0 ? { receivedAtMs: i.receivedAtMs } : {} }));
|
|
7428
7569
|
const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
|
|
7429
7570
|
for (const row of chosen) {
|
|
7430
7571
|
o.allocated.push(row);
|
|
@@ -7488,7 +7629,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7488
7629
|
const locations = [...this.locations.values()].map((l) => ({ id: l.id, type: l.type }));
|
|
7489
7630
|
const ops = this.declaredOperations();
|
|
7490
7631
|
for (const line of o.lines) {
|
|
7491
|
-
const have = stock.filter((s) => s.gtin
|
|
7632
|
+
const have = stock.filter((s) => sameItemClass(s.gtin, line.gtin) && s.sellable && this.locations.get(s.location)?.type === "storage").reduce((n, s) => n + s.qty, 0);
|
|
7492
7633
|
const short = line.requested - have;
|
|
7493
7634
|
if (short <= 0) continue;
|
|
7494
7635
|
const plan = planMakeToOrder(line.gtin, short, ops, stock, locations);
|
|
@@ -7514,7 +7655,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7514
7655
|
}
|
|
7515
7656
|
/** 부품을 작업대로 — 팔레트 이동이므로 피킹과 같은 기제다(부분 소비는 코어가 한다). */
|
|
7516
7657
|
issueFeed(o, gtin, from, to, qty) {
|
|
7517
|
-
const src = [...this.items.values()].find((i) => i.gtin
|
|
7658
|
+
const src = [...this.items.values()].find((i) => sameItemClass(i.gtin, gtin) && i.location === from && i.disposition === DISP.sellable);
|
|
7518
7659
|
if (!src) return;
|
|
7519
7660
|
src.disposition = DISP.reserved;
|
|
7520
7661
|
this.pushTask(o, "feed", src.epc, from, to);
|
|
@@ -7592,7 +7733,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7592
7733
|
order.fulfilled += allocatedQty(order.allocated);
|
|
7593
7734
|
if (order.lines) for (const epc of order.picked) {
|
|
7594
7735
|
const g = this.items.get(epc)?.gtin;
|
|
7595
|
-
const line = order.lines.find((l) => l.gtin
|
|
7736
|
+
const line = order.lines.find((l) => sameItemClass(l.gtin, g) && l.requested > 0);
|
|
7596
7737
|
if (line) line.requested--;
|
|
7597
7738
|
}
|
|
7598
7739
|
for (const epc of order.picked) {
|
|
@@ -8433,7 +8574,7 @@ The steps are the source of truth for execution order; the hierarchy only names
|
|
|
8433
8574
|
if (fromType) {
|
|
8434
8575
|
for (const n of locsOfType.get(fromType) ?? []) {
|
|
8435
8576
|
for (const i of this.items.at(n.id)) {
|
|
8436
|
-
if (i.gtin
|
|
8577
|
+
if (sameItemClass(i.gtin, g) && i.disposition === DISP.sellable) available.push({ epc: i.epc, location: i.location, qty: 1 });
|
|
8437
8578
|
}
|
|
8438
8579
|
}
|
|
8439
8580
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operato/twin-kernel",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.24",
|
|
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": {
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"test": "node --test test/*.test.ts"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@operato/ops-contract": "^0.9.
|
|
31
|
+
"@operato/ops-contract": "^0.9.35"
|
|
32
32
|
}
|
|
33
33
|
}
|