@operato/twin-kernel 0.11.23 → 0.11.25
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 +22 -0
- package/dist/flow-engine.js +89 -9
- package/dist/kernel.js +35 -8
- package/dist/make-to-order.js +2 -1
- package/dist/mes-kernel.js +2 -2
- package/dist/observed-reducer.d.ts +15 -0
- package/dist/observed-reducer.js +53 -0
- package/dist-cjs/index.cjs +314 -24
- package/package.json +2 -2
package/dist/flow-engine.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { VocabularyElement } from '@operato/ops-contract';
|
|
|
4
4
|
import type { ReducerCheckpoint } from './observed-reducer.ts';
|
|
5
5
|
import type { EpcisEvent, BizTransactionElement } from '@operato/ops-contract';
|
|
6
6
|
import type { AllocationPolicy, SlotView } from './allocation-policy.ts';
|
|
7
|
+
import { type FoldedOrders, type OrderFoldPolicy } from '@operato/ops-contract';
|
|
7
8
|
import type { DurationEstimator, DurationContext } from './duration-estimator.ts';
|
|
8
9
|
import type { OperationDef, IsoDuration, OpMaterialSpecification } from '@operato/ops-contract';
|
|
9
10
|
import { type CapacityAnalysis } from '@operato/ops-contract';
|
|
@@ -290,6 +291,8 @@ export interface FlowOrder {
|
|
|
290
291
|
picked: string[];
|
|
291
292
|
gtin?: string;
|
|
292
293
|
shipmentEpc?: string | null;
|
|
294
|
+
/** 언제 끝났나(ms) — 끝났다고 사건을 낸 시각. 끝난 오더를 접는 경계가 이 값으로 잰다(ADR-0092). */
|
|
295
|
+
terminalAtMs?: number;
|
|
293
296
|
lines?: OrderLine[];
|
|
294
297
|
held?: boolean;
|
|
295
298
|
dockDoor?: string;
|
|
@@ -500,6 +503,9 @@ export declare class ItemStore {
|
|
|
500
503
|
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
501
504
|
*/
|
|
502
505
|
private byGtin;
|
|
506
|
+
/** 같은 글자를 매번 뜯지 않는다 — 클래스 글자의 종류는 적고 물품은 많다. */
|
|
507
|
+
private itemClassCache;
|
|
508
|
+
private itemClass;
|
|
503
509
|
get size(): number;
|
|
504
510
|
get(key: string): FlowItem | undefined;
|
|
505
511
|
has(key: string): boolean;
|
|
@@ -565,6 +571,10 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
565
571
|
assets: Map<string, FlowAsset>;
|
|
566
572
|
tasks: Map<string, FlowTask>;
|
|
567
573
|
orders: Map<string, FlowOrder>;
|
|
574
|
+
/** 접힌 오더(ADR-0092) — 세는 수와 경계만 든다. 미러는 리듀서가 들고 이것은 옮겨 받는다(§`hydrateObserved`). */
|
|
575
|
+
protected foldedOrders: FoldedOrders;
|
|
576
|
+
/** 끝난 오더를 언제 접나 — 모델의 `orderFold` 선언. 없으면 기본(24시간 · 1,000건). */
|
|
577
|
+
protected orderFoldPolicy: OrderFoldPolicy;
|
|
568
578
|
revision: number;
|
|
569
579
|
clockMs: number;
|
|
570
580
|
/**
|
|
@@ -630,6 +640,16 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
630
640
|
*
|
|
631
641
|
* 관측 구동이 아니면 `undefined` — 시뮬은 리듀서를 갖지 않는다(저장할 것이 없다).
|
|
632
642
|
*/
|
|
643
|
+
/**
|
|
644
|
+
* **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
|
|
645
|
+
*
|
|
646
|
+
* 부르는 것은 호스트다 — 체크포인트(미러는 `observedCheckpoint`, 시뮬은 상태 스냅샷)를 쓰기 직전에 부른다. 그래야
|
|
647
|
+
* 접힌 쪽이 씨앗과 같은 때에 적힌다. 커널이 스스로 접지 않는 것은, 접힌 목록을 읽는 쪽(예측 · 화면)이 그 뜻에
|
|
648
|
+
* 맞춰진 뒤에 호스트가 부르게 하려는 것이다.
|
|
649
|
+
*
|
|
650
|
+
* 미러는 원천인 리듀서에서 접고 옮겨 받는다 — 이 목록에서만 지우면 다음 옮김이 되살린다.
|
|
651
|
+
*/
|
|
652
|
+
foldFinishedOrders(nowMs?: number): number;
|
|
633
653
|
observedCheckpoint(): ReducerCheckpoint | undefined;
|
|
634
654
|
/**
|
|
635
655
|
* **재개점에서 관측 리듀서를 되세운다** — 저널을 0부터 다시 집계하지 않게.
|
|
@@ -834,6 +854,8 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
834
854
|
assets?: AssetState[];
|
|
835
855
|
tasks?: TaskState[];
|
|
836
856
|
orders?: OrderState[];
|
|
857
|
+
/** 접힌 오더(ADR-0092) — 세는 수와 경계. 이어받지 않으면 재기동 · fork 뒤에 끝난 오더의 수를 잃는다. */
|
|
858
|
+
foldedOrders?: FoldedOrders;
|
|
837
859
|
/** 확인해 둔 주목 신호 id — 계산으로 되살릴 수 없어 스냅샷에서 이어받는다. */
|
|
838
860
|
acked?: string[];
|
|
839
861
|
/** 주목 신호가 처음 성립한 시각 — 이어받지 않으면 지속된 조건이 「방금」으로 되살아난다. */
|
package/dist/flow-engine.js
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
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';
|
|
17
17
|
import { WMS_ORDER_KIND } from '@operato/ops-contract';
|
|
18
18
|
import { transformationEvent, aggregationEvent, objectEvent, parseEpc, DISP, ILMD_ATTR, CBV_BIZSTEP, objectUri, bizTransactionUri, gdtiUri } from '@operato/ops-contract';
|
|
19
19
|
import { allocationPolicyOf } from "./allocation-policy.js";
|
|
20
|
+
import { ORDER_FOLD_DEFAULT, emptyFoldedOrders, foldOrdersInto, foldedOrderCount, orderFoldPolicyOf, planOrderFold } from '@operato/ops-contract';
|
|
20
21
|
import { OP_PARAM } from '@operato/ops-contract';
|
|
21
22
|
import { parseIsoDuration } from '@operato/ops-contract';
|
|
22
23
|
import { analyzeCapacity } from '@operato/ops-contract';
|
|
@@ -407,7 +408,21 @@ export class ItemStore {
|
|
|
407
408
|
* 색인을 밖에 따로 두지 않는다 — 갱신하는 자리가 흩어지면 한 자리라도 빠뜨렸을 때 **재고가 조용히
|
|
408
409
|
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
409
410
|
*/
|
|
411
|
+
/*
|
|
412
|
+
* 품목 색인 — 키는 **품목 클래스**다(`itemClassOf`). 재고는 로트 클래스를 들고 오더는 품목 클래스를 묻는다
|
|
413
|
+
* (ADR-0076 ⑥ · ADR-0082 결정 2). 로트 글자를 키로 쓰면 품목을 물어도 로트 재고가 안 나온다.
|
|
414
|
+
*/
|
|
410
415
|
byGtin = new Map();
|
|
416
|
+
/** 같은 글자를 매번 뜯지 않는다 — 클래스 글자의 종류는 적고 물품은 많다. */
|
|
417
|
+
itemClassCache = new Map();
|
|
418
|
+
itemClass(gtin) {
|
|
419
|
+
let c = this.itemClassCache.get(gtin);
|
|
420
|
+
if (c === undefined) {
|
|
421
|
+
c = itemClassOf(gtin) ?? gtin;
|
|
422
|
+
this.itemClassCache.set(gtin, c);
|
|
423
|
+
}
|
|
424
|
+
return c;
|
|
425
|
+
}
|
|
411
426
|
get size() {
|
|
412
427
|
return this.map.size;
|
|
413
428
|
}
|
|
@@ -515,7 +530,7 @@ export class ItemStore {
|
|
|
515
530
|
}
|
|
516
531
|
/** 그 품목인 물품들 — 색인이 답한다(자리를 모를 때 쓴다). */
|
|
517
532
|
ofGtin(gtin) {
|
|
518
|
-
const keys = this.byGtin.get(gtin);
|
|
533
|
+
const keys = this.byGtin.get(this.itemClass(gtin));
|
|
519
534
|
if (!keys)
|
|
520
535
|
return [];
|
|
521
536
|
const out = [];
|
|
@@ -549,7 +564,7 @@ export class ItemStore {
|
|
|
549
564
|
}
|
|
550
565
|
/* 품목 색인도 같은 규율로 본다 — 어긋나면 자재가 있는데 없다고 판정된다. */
|
|
551
566
|
for (const [key, it] of this.map) {
|
|
552
|
-
if (it.gtin && !this.byGtin.get(it.gtin)?.has(key))
|
|
567
|
+
if (it.gtin && !this.byGtin.get(this.itemClass(it.gtin))?.has(key))
|
|
553
568
|
drift.push(`${key} 이 품목 '${it.gtin}' 색인에 없다`);
|
|
554
569
|
}
|
|
555
570
|
for (const [g, keys] of this.byGtin) {
|
|
@@ -557,7 +572,7 @@ export class ItemStore {
|
|
|
557
572
|
const it = this.map.get(k);
|
|
558
573
|
if (!it)
|
|
559
574
|
drift.push(`${k} 이 지워졌는데 품목 '${g}' 색인에 남아 있다`);
|
|
560
|
-
else if (it.gtin !== g)
|
|
575
|
+
else if (this.itemClass(it.gtin ?? '') !== g)
|
|
561
576
|
drift.push(`${k} 은 품목 '${it.gtin}' 인데 '${g}' 색인에 있다`);
|
|
562
577
|
}
|
|
563
578
|
}
|
|
@@ -566,9 +581,10 @@ export class ItemStore {
|
|
|
566
581
|
index(key, location, gtin) {
|
|
567
582
|
this.indexLocation(key, location);
|
|
568
583
|
if (gtin) {
|
|
569
|
-
const
|
|
584
|
+
const g = this.itemClass(gtin);
|
|
585
|
+
const set = this.byGtin.get(g) ?? new Set();
|
|
570
586
|
set.add(key);
|
|
571
|
-
this.byGtin.set(
|
|
587
|
+
this.byGtin.set(g, set);
|
|
572
588
|
}
|
|
573
589
|
}
|
|
574
590
|
indexLocation(key, location) {
|
|
@@ -579,12 +595,13 @@ export class ItemStore {
|
|
|
579
595
|
unindex(key, location, gtin) {
|
|
580
596
|
this.unindexLocation(key, location);
|
|
581
597
|
if (gtin) {
|
|
582
|
-
const
|
|
598
|
+
const g = this.itemClass(gtin);
|
|
599
|
+
const set = this.byGtin.get(g);
|
|
583
600
|
if (!set)
|
|
584
601
|
return;
|
|
585
602
|
set.delete(key);
|
|
586
603
|
if (!set.size)
|
|
587
|
-
this.byGtin.delete(
|
|
604
|
+
this.byGtin.delete(g);
|
|
588
605
|
}
|
|
589
606
|
}
|
|
590
607
|
unindexLocation(key, location) {
|
|
@@ -614,6 +631,10 @@ export class FlowEngine {
|
|
|
614
631
|
assets = new Map();
|
|
615
632
|
tasks = new Map();
|
|
616
633
|
orders = new Map();
|
|
634
|
+
/** 접힌 오더(ADR-0092) — 세는 수와 경계만 든다. 미러는 리듀서가 들고 이것은 옮겨 받는다(§`hydrateObserved`). */
|
|
635
|
+
foldedOrders = emptyFoldedOrders();
|
|
636
|
+
/** 끝난 오더를 언제 접나 — 모델의 `orderFold` 선언. 없으면 기본(24시간 · 1,000건). */
|
|
637
|
+
orderFoldPolicy = ORDER_FOLD_DEFAULT;
|
|
617
638
|
revision = 0;
|
|
618
639
|
clockMs = 0;
|
|
619
640
|
/**
|
|
@@ -679,6 +700,33 @@ export class FlowEngine {
|
|
|
679
700
|
*
|
|
680
701
|
* 관측 구동이 아니면 `undefined` — 시뮬은 리듀서를 갖지 않는다(저장할 것이 없다).
|
|
681
702
|
*/
|
|
703
|
+
/**
|
|
704
|
+
* **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
|
|
705
|
+
*
|
|
706
|
+
* 부르는 것은 호스트다 — 체크포인트(미러는 `observedCheckpoint`, 시뮬은 상태 스냅샷)를 쓰기 직전에 부른다. 그래야
|
|
707
|
+
* 접힌 쪽이 씨앗과 같은 때에 적힌다. 커널이 스스로 접지 않는 것은, 접힌 목록을 읽는 쪽(예측 · 화면)이 그 뜻에
|
|
708
|
+
* 맞춰진 뒤에 호스트가 부르게 하려는 것이다.
|
|
709
|
+
*
|
|
710
|
+
* 미러는 원천인 리듀서에서 접고 옮겨 받는다 — 이 목록에서만 지우면 다음 옮김이 되살린다.
|
|
711
|
+
*/
|
|
712
|
+
foldFinishedOrders(nowMs) {
|
|
713
|
+
if (this.observeMode && this.observer) {
|
|
714
|
+
const n = this.observer.foldFinishedOrders(nowMs);
|
|
715
|
+
if (n) {
|
|
716
|
+
this.observedDirty = true;
|
|
717
|
+
this.settleObserved();
|
|
718
|
+
}
|
|
719
|
+
return n;
|
|
720
|
+
}
|
|
721
|
+
const ids = planOrderFold([...this.orders.values()], nowMs ?? this.nowMs(), this.orderFoldPolicy);
|
|
722
|
+
if (!ids.length)
|
|
723
|
+
return 0;
|
|
724
|
+
this.foldedOrders = foldOrdersInto(this.foldedOrders, ids.map(id => this.orders.get(id)).filter(Boolean));
|
|
725
|
+
for (const id of ids)
|
|
726
|
+
this.orders.delete(id);
|
|
727
|
+
this.revision++;
|
|
728
|
+
return ids.length;
|
|
729
|
+
}
|
|
682
730
|
observedCheckpoint() {
|
|
683
731
|
/*
|
|
684
732
|
* 관측 구동인데 리듀서가 아직 없으면 만든다 (2026-08-27).
|
|
@@ -809,6 +857,11 @@ export class FlowEngine {
|
|
|
809
857
|
*/
|
|
810
858
|
if (def.allocationPolicy)
|
|
811
859
|
this.policy = allocationPolicyOf(def.allocationPolicy);
|
|
860
|
+
/* 끝난 오더를 접는 경계 — 0 · 무한은 거절한다. 접지 않는 선언은 끝난 오더가 끝없이 쌓이던 때로 돌아간다(ADR-0092). */
|
|
861
|
+
const fold = orderFoldPolicyOf(def.orderFold);
|
|
862
|
+
if ('error' in fold)
|
|
863
|
+
throw new Error(fold.error);
|
|
864
|
+
this.orderFoldPolicy = fold.policy;
|
|
812
865
|
/*
|
|
813
866
|
* **생산 선언은 어느 커널이든 싣는다.** 예전에는 MES 커널만 생성자에서 이것을 실었고(그 시절
|
|
814
867
|
* 이름은 `mesSpec` 이었다), 그래서 창고 트윈은 "자재를 소비해 자재를 산출하는 공정" 을 선언할
|
|
@@ -1265,6 +1318,19 @@ export class FlowEngine {
|
|
|
1265
1318
|
*
|
|
1266
1319
|
* 지어내지 않는다 — 없는 물품을 만들어 채우면 그 뒤 계보와 재고가 전부 거짓 위에 선다.
|
|
1267
1320
|
*/
|
|
1321
|
+
/* 접힌 쪽과 종결 시각을 이어받는다(ADR-0092). 종결 시각은 델타에 없으므로 스냅샷의 오더에서 찾는다. */
|
|
1322
|
+
this.foldedOrders = snap.foldedOrders ? structuredClone(snap.foldedOrders) : emptyFoldedOrders();
|
|
1323
|
+
const terminalAtOf = new Map((snap.orders ?? []).map(o => [o.id, o.terminalAtMs]));
|
|
1324
|
+
/*
|
|
1325
|
+
* **관측이면 원천에 없는 오더를 지운다.** 이 목록은 리듀서에서 옮겨 받는 것이라, 리듀서가 접은 오더를 여기 남기면
|
|
1326
|
+
* 다시는 사라지지 않는다. 씨앗(웜스타트 · fork)은 새로 세우는 커널이라 지울 것이 없다.
|
|
1327
|
+
*/
|
|
1328
|
+
if (purpose === 'observe') {
|
|
1329
|
+
const present = new Set(observedOrders.map(o => o.orderId));
|
|
1330
|
+
for (const id of [...this.orders.keys()])
|
|
1331
|
+
if (!present.has(id))
|
|
1332
|
+
this.orders.delete(id);
|
|
1333
|
+
}
|
|
1268
1334
|
for (const o of observedOrders) {
|
|
1269
1335
|
const lines = (o.lines ?? []).map(l => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter(l => l.requested > 0);
|
|
1270
1336
|
/* **라인이 없는 오더를 조용히 빼지 않는다.** 예전에는 남은 양을 라인에서만 셌으므로, 품목 라인
|
|
@@ -1325,7 +1391,9 @@ export class FlowEngine {
|
|
|
1325
1391
|
/* 약속해 둔 자리·시각창을 이어받는다 — 없으면 도메인이 「정해 뒀다」고 믿는 것을 잃는다.
|
|
1326
1392
|
사실이 말해 주지 않으면(옛 저널) 비는 것이 사실이다 — 지어내지 않는다. */
|
|
1327
1393
|
...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
|
|
1328
|
-
...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
|
|
1394
|
+
...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {}),
|
|
1395
|
+
/* 언제 끝났나 — 관측만 이어받는다(씨앗은 끝난 오더를 심지 않는다). */
|
|
1396
|
+
...(observing && terminalAtOf.get(o.orderId) !== undefined ? { terminalAtMs: terminalAtOf.get(o.orderId) } : {})
|
|
1329
1397
|
});
|
|
1330
1398
|
}
|
|
1331
1399
|
/* 작업이 딛고 설 오더를 먼저 세운다 — 순서가 뒤바뀌면 아래 확인이 언제나 "없다" 로 답한다. */
|
|
@@ -1815,8 +1883,11 @@ export class FlowEngine {
|
|
|
1815
1883
|
...(t.startTime ? { startTime: t.startTime } : {}),
|
|
1816
1884
|
...(t.endTime ? { endTime: t.endTime } : {})
|
|
1817
1885
|
})),
|
|
1886
|
+
/* 접힌 오더 — 접은 것이 없으면 싣지 않는다(ADR-0092). */
|
|
1887
|
+
...(foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {}),
|
|
1818
1888
|
orders: [...this.orders.values()].map(o => ({
|
|
1819
1889
|
id: o.id, kind: o.kind, status: o.status,
|
|
1890
|
+
...(o.terminalAtMs !== undefined ? { terminalAtMs: o.terminalAtMs } : {}),
|
|
1820
1891
|
progress: o.requested ? o.fulfilled / o.requested : 0,
|
|
1821
1892
|
requested: o.requested, fulfilled: o.fulfilled,
|
|
1822
1893
|
/*
|
|
@@ -2991,6 +3062,15 @@ export class FlowEngine {
|
|
|
2991
3062
|
}
|
|
2992
3063
|
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
2993
3064
|
emitOrder(o) {
|
|
3065
|
+
/*
|
|
3066
|
+
* **언제 끝났나를 여기서 적는다** — 오더의 상태를 바꾸는 자리는 여럿이지만 사건을 내는 문은 이것 하나다.
|
|
3067
|
+
* 끝났던 오더는 처음 끝난 시각을 지키고, 다시 열리면 지운다. 미러는 같은 값을 그 사건의 `eventTime` 에서
|
|
3068
|
+
* 되세운다(델타에 싣지 않는다 — 두 구동이 같은 시각을 든다).
|
|
3069
|
+
*/
|
|
3070
|
+
if (isOrderTerminal(o))
|
|
3071
|
+
o.terminalAtMs ??= this.nowMs();
|
|
3072
|
+
else
|
|
3073
|
+
delete o.terminalAtMs;
|
|
2994
3074
|
this.emitOp(OP_EVENT.order, {
|
|
2995
3075
|
orderId: o.id, kind: o.kind, status: o.status, requested: o.requested, fulfilled: o.fulfilled, held: o.held,
|
|
2996
3076
|
...(o.priority !== undefined ? { priority: o.priority } : {}),
|
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
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssetState, TestResult, MaterialQuantity, TwinModelDef, CanonicalEnvelope, LocationState, ItemState, LocationObservation, EquipmentState, PersonState, TaskState, OrderState, StructureShift, DispositionFact } from '@operato/ops-contract';
|
|
2
|
+
import { type FoldedOrders } from '@operato/ops-contract';
|
|
2
3
|
import type { MaterialLotUse } from '@operato/ops-contract';
|
|
3
4
|
import { type VocabularyElement } from '@operato/ops-contract';
|
|
4
5
|
interface ProjItem {
|
|
@@ -103,6 +104,8 @@ export interface ProjectedState {
|
|
|
103
104
|
tasks: TaskState[];
|
|
104
105
|
equipment: EquipmentState[];
|
|
105
106
|
orders: OrderState[];
|
|
107
|
+
/** 접힌 오더(ADR-0092) — 접은 것이 없으면 없다. */
|
|
108
|
+
foldedOrders?: FoldedOrders;
|
|
106
109
|
/**
|
|
107
110
|
* 사람이 확인(ack)해 둔 주목 신호 id — **파생될 수 없는 유일한 축**이라 저널에서 되살린다.
|
|
108
111
|
*
|
|
@@ -159,6 +162,8 @@ export interface ReducerCheckpoint {
|
|
|
159
162
|
persons: PersonState[];
|
|
160
163
|
assets: AssetState[];
|
|
161
164
|
orders: OrderState[];
|
|
165
|
+
/** 접힌 오더(ADR-0092) — 옛 저장본에는 없다(없으면 접은 것이 없는 것이다). */
|
|
166
|
+
foldedOrders?: FoldedOrders;
|
|
162
167
|
acked: string[];
|
|
163
168
|
corrections: {
|
|
164
169
|
declaredAt: string;
|
|
@@ -267,6 +272,8 @@ export declare class ObservedReducer {
|
|
|
267
272
|
private persons;
|
|
268
273
|
private assets;
|
|
269
274
|
private orders;
|
|
275
|
+
private foldedOrders;
|
|
276
|
+
private orderFoldPolicy;
|
|
270
277
|
/** 확인해 둔 주목 신호 id — `attention.acked` 이벤트로만 들어온다(계산으로 만들지 않는다). */
|
|
271
278
|
private acked;
|
|
272
279
|
revision: number;
|
|
@@ -518,6 +525,14 @@ export declare class ObservedReducer {
|
|
|
518
525
|
* 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
|
|
519
526
|
*/
|
|
520
527
|
restore(cp: ReducerCheckpoint): void;
|
|
528
|
+
/**
|
|
529
|
+
* **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
|
|
530
|
+
*
|
|
531
|
+
* 부르는 것은 호스트다: 체크포인트를 쓰기 직전에 부른다. 그래야 접힌 쪽이 씨앗과 같은 때에 적혀 재기동 동치
|
|
532
|
+
* (`재개점 + 꼬리 == 0부터 접기`)가 선다. 「지금」은 트윈의 지금(마지막으로 들은 시각)이다 — 벽시계가 아니다.
|
|
533
|
+
* 아직 아무것도 못 들었으면 접지 않는다.
|
|
534
|
+
*/
|
|
535
|
+
foldFinishedOrders(nowMs?: number | undefined): number;
|
|
521
536
|
snapshot(): ProjectedState;
|
|
522
537
|
}
|
|
523
538
|
export {};
|
package/dist/observed-reducer.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { computeOee, isWorkShiftExecutionBasis } from '@operato/ops-contract';
|
|
23
23
|
import { OP_EVENT, capabilityOf, itemKeyOf, judgeAgainstSpec, requiredTestsFor, locationStatusOf, readBoardEquipment, readBoardLocations, readBoardAssets, effectivityAt, offCalendarAt, offCalendarReasonAt, activeShiftAt } from '@operato/ops-contract';
|
|
24
24
|
import { ILMD_ATTR, parseEpc } from '@operato/ops-contract';
|
|
25
|
+
import { emptyFoldedOrders, foldOrdersInto, foldedOrderCount, isOrderTerminal, orderFoldPolicyOf, planOrderFold, standsNewOrderRow } from '@operato/ops-contract';
|
|
25
26
|
import { expiryFromAttributes, lotFromAttributes } from '@operato/ops-contract';
|
|
26
27
|
/* 클래스 마스터 표는 상태를 들므로 커널에 있다(§`class-master-table`). */
|
|
27
28
|
import { ClassMasterTable } from "./class-master-table.js";
|
|
@@ -86,6 +87,12 @@ export class ObservedReducer {
|
|
|
86
87
|
persons = new Map();
|
|
87
88
|
assets = new Map();
|
|
88
89
|
orders = new Map();
|
|
90
|
+
/*
|
|
91
|
+
* **접힌 오더** — 끝나서 뜨거운 목록에서 걷힌 것의 세는 수와 경계(ADR-0092). 접는 때는 호스트가 정한다
|
|
92
|
+
* (체크포인트를 쓰기 직전에 `foldFinishedOrders` 를 부른다). 경계는 모델의 `orderFold` 선언이다.
|
|
93
|
+
*/
|
|
94
|
+
foldedOrders = emptyFoldedOrders();
|
|
95
|
+
orderFoldPolicy;
|
|
89
96
|
/** 확인해 둔 주목 신호 id — `attention.acked` 이벤트로만 들어온다(계산으로 만들지 않는다). */
|
|
90
97
|
acked = new Set();
|
|
91
98
|
revision = 0;
|
|
@@ -119,6 +126,11 @@ export class ObservedReducer {
|
|
|
119
126
|
/** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
120
127
|
utcOffsetMinutes;
|
|
121
128
|
constructor(model) {
|
|
129
|
+
/* 접지 않는 선언(0 · 무한)은 거절한다 — 기본값으로 조용히 바꾸면 선언한 사람은 자기 값이 쓰이는 줄 안다. */
|
|
130
|
+
const fold = orderFoldPolicyOf(model.orderFold);
|
|
131
|
+
if ('error' in fold)
|
|
132
|
+
throw new Error(fold.error);
|
|
133
|
+
this.orderFoldPolicy = fold.policy;
|
|
122
134
|
this.utcOffsetMinutes = model.utcOffsetMinutes;
|
|
123
135
|
this.classDefs = { personnel: model.personnelClasses, equipment: model.equipmentClasses, asset: model.assetClasses };
|
|
124
136
|
/* 선언된 판정 기준 — 원천이 판정하지 않은 결과를 커널이 판정할 때 쓴다(§`judgeAgainstSpec`). */
|
|
@@ -711,11 +723,26 @@ export class ObservedReducer {
|
|
|
711
723
|
const d = e.data;
|
|
712
724
|
if (this.stale(`order:${d.orderId}`, e))
|
|
713
725
|
return;
|
|
726
|
+
const prev = this.orders.get(d.orderId);
|
|
727
|
+
const at = Date.parse(String(e.eventTime ?? ''));
|
|
728
|
+
/*
|
|
729
|
+
* **접힌 오더의 옛 종결이 다시 오면 줄을 세우지 않는다**(ADR-0092 결정 2). 폴링 원본은 같은 종결을 다시
|
|
730
|
+
* 보낸다 — 줄을 세우면 다음 경계에서 또 접혀 세는 수가 두 번 는다. 레코드 자신의 시각으로 가른다(도착
|
|
731
|
+
* 시각이 아니다). 순서 판정에 적힌 이 오더의 시각도 지운다 — 남기면 그 맵이 오더 수만큼 다시 자란다.
|
|
732
|
+
*/
|
|
733
|
+
if (!prev && !standsNewOrderRow(d, Number.isFinite(at) ? at : undefined, this.foldedOrders)) {
|
|
734
|
+
this.lastAt.delete(`order:${d.orderId}`);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
/* 언제 끝났나 — 끝났던 오더는 처음 끝난 시각을 지킨다. 끝나지 않았으면(재개 포함) 없다. */
|
|
738
|
+
const terminal = isOrderTerminal({ status: d.status, requested: d.requested, fulfilled: d.fulfilled });
|
|
739
|
+
const terminalAtMs = terminal ? (prev?.terminalAtMs ?? (Number.isFinite(at) ? at : undefined)) : undefined;
|
|
714
740
|
/* **진척으로 압축하지 않는다.** 예전에는 progress 만 남겨서, 미러에서 예측을 세우려면 저널을
|
|
715
741
|
* 따로 읽어 오더 원값을 되찾아야 했다. 진척은 요청·이행에서 나오는 파생이다 — 파생을 남기고
|
|
716
742
|
* 원본을 버리면 남은 데맨드를 재계획할 수 없다. */
|
|
717
743
|
this.orders.set(d.orderId, {
|
|
718
744
|
id: d.orderId, kind: d.kind, status: d.status,
|
|
745
|
+
...(terminalAtMs !== undefined ? { terminalAtMs } : {}),
|
|
719
746
|
progress: d.requested ? d.fulfilled / d.requested : 0,
|
|
720
747
|
requested: d.requested, fulfilled: d.fulfilled,
|
|
721
748
|
...(d.lines?.length ? { lines: d.lines.map(l => ({ ...l })) } : {}),
|
|
@@ -1332,6 +1359,8 @@ export class ObservedReducer {
|
|
|
1332
1359
|
persons: [...this.persons.values()].map(x => ({ ...x })),
|
|
1333
1360
|
assets: [...this.assets.values()].map(x => ({ ...x })),
|
|
1334
1361
|
orders: [...this.orders.values()].map(o => ({ ...o })),
|
|
1362
|
+
/* 접힌 쪽도 재개점에 든다 — 없으면 재기동 뒤에 끝난 오더의 수를 잃는다(ADR-0092). 접은 것이 없으면 싣지 않는다. */
|
|
1363
|
+
...(foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {}),
|
|
1335
1364
|
acked: [...this.acked],
|
|
1336
1365
|
corrections: this.corrections.map(c => ({ ...c })),
|
|
1337
1366
|
unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v })),
|
|
@@ -1375,10 +1404,33 @@ export class ObservedReducer {
|
|
|
1375
1404
|
this.persons = new Map((cp?.persons ?? []).map(x => [x.id, { ...x }]));
|
|
1376
1405
|
this.assets = new Map((cp?.assets ?? []).map(x => [x.id, { ...x }]));
|
|
1377
1406
|
this.orders = new Map((cp?.orders ?? []).map(o => [o.id, { ...o }]));
|
|
1407
|
+
this.foldedOrders = cp?.foldedOrders ? structuredClone(cp.foldedOrders) : emptyFoldedOrders();
|
|
1378
1408
|
this.acked = new Set(cp?.acked ?? []);
|
|
1379
1409
|
this.corrections = (cp?.corrections ?? []).map(c => ({ ...c }));
|
|
1380
1410
|
this.unhandled = new Map((cp?.unhandled ?? []).map(({ eventType, ...v }) => [eventType, { ...v }]));
|
|
1381
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
|
|
1414
|
+
*
|
|
1415
|
+
* 부르는 것은 호스트다: 체크포인트를 쓰기 직전에 부른다. 그래야 접힌 쪽이 씨앗과 같은 때에 적혀 재기동 동치
|
|
1416
|
+
* (`재개점 + 꼬리 == 0부터 접기`)가 선다. 「지금」은 트윈의 지금(마지막으로 들은 시각)이다 — 벽시계가 아니다.
|
|
1417
|
+
* 아직 아무것도 못 들었으면 접지 않는다.
|
|
1418
|
+
*/
|
|
1419
|
+
foldFinishedOrders(nowMs = this.observedAtMs) {
|
|
1420
|
+
if (nowMs === undefined || !Number.isFinite(nowMs))
|
|
1421
|
+
return 0;
|
|
1422
|
+
const ids = planOrderFold([...this.orders.values()], nowMs, this.orderFoldPolicy);
|
|
1423
|
+
if (!ids.length)
|
|
1424
|
+
return 0;
|
|
1425
|
+
const rows = ids.map(id => this.orders.get(id)).filter(Boolean);
|
|
1426
|
+
this.foldedOrders = foldOrdersInto(this.foldedOrders, rows);
|
|
1427
|
+
for (const id of ids) {
|
|
1428
|
+
this.orders.delete(id);
|
|
1429
|
+
this.lastAt.delete(`order:${id}`);
|
|
1430
|
+
}
|
|
1431
|
+
this.revision++;
|
|
1432
|
+
return ids.length;
|
|
1433
|
+
}
|
|
1382
1434
|
snapshot() {
|
|
1383
1435
|
const occ = new Map();
|
|
1384
1436
|
for (const it of this.items.values())
|
|
@@ -1425,6 +1477,7 @@ export class ObservedReducer {
|
|
|
1425
1477
|
tasks: [...this.tasks.values()].map(t => structuredClone(t)),
|
|
1426
1478
|
equipment: [...this.equipment.values()].map(m => ({ ...m, ...this.oeePart(m.id), ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`), ...this.capabilityPart(m, `eq:${m.id}`, m.kind ? [m.kind] : [], this.classDefs.equipment) })),
|
|
1427
1479
|
orders: [...this.orders.values()].map(o => ({ ...o })),
|
|
1480
|
+
...(foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {}),
|
|
1428
1481
|
acked: [...this.acked]
|
|
1429
1482
|
};
|
|
1430
1483
|
}
|
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,92 @@ 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
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// ../ops-contract/dist/order-fold.js
|
|
1913
|
+
var ORDER_FOLD_DEFAULT = Object.freeze({ afterMs: 24 * 60 * 60 * 1e3, maxTerminal: 1e3 });
|
|
1914
|
+
function orderFoldPolicyOf(declared) {
|
|
1915
|
+
const policy = { ...ORDER_FOLD_DEFAULT };
|
|
1916
|
+
for (const key of ["afterMs", "maxTerminal"]) {
|
|
1917
|
+
const v = declared?.[key];
|
|
1918
|
+
if (v === void 0 || v === null)
|
|
1919
|
+
continue;
|
|
1920
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
|
|
1921
|
+
return { error: `order fold ${key} must be a positive finite number, got ${String(v)} \u2014 a fold that never folds is refused (ADR-0092)` };
|
|
1922
|
+
}
|
|
1923
|
+
policy[key] = key === "maxTerminal" ? Math.floor(v) : v;
|
|
1924
|
+
}
|
|
1925
|
+
if (policy.maxTerminal < 1)
|
|
1926
|
+
return { error: "order fold maxTerminal must be at least 1 (ADR-0092)" };
|
|
1927
|
+
return { policy };
|
|
1928
|
+
}
|
|
1929
|
+
function emptyFoldedOrders() {
|
|
1930
|
+
return { counts: {}, boundaryMs: null };
|
|
1931
|
+
}
|
|
1932
|
+
function foldedOrderCount(folded) {
|
|
1933
|
+
let n = 0;
|
|
1934
|
+
for (const byStatus of Object.values(folded?.counts ?? {}))
|
|
1935
|
+
for (const c of Object.values(byStatus))
|
|
1936
|
+
n += c;
|
|
1937
|
+
return n;
|
|
1938
|
+
}
|
|
1939
|
+
function planOrderFold(orders, nowMs, policy = ORDER_FOLD_DEFAULT) {
|
|
1940
|
+
const candidates = orders.filter((o) => isOrderTerminal(o) && !o.held && typeof o.terminalAtMs === "number" && Number.isFinite(o.terminalAtMs)).sort((a, b) => a.terminalAtMs - b.terminalAtMs || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1941
|
+
const cutoff = nowMs - policy.afterMs;
|
|
1942
|
+
const byTime = candidates.filter((o) => o.terminalAtMs <= cutoff);
|
|
1943
|
+
const stay = candidates.length - byTime.length;
|
|
1944
|
+
const byCount = stay > policy.maxTerminal ? candidates.slice(byTime.length, byTime.length + (stay - policy.maxTerminal)) : [];
|
|
1945
|
+
return [...byTime, ...byCount].map((o) => o.id);
|
|
1946
|
+
}
|
|
1947
|
+
function foldOrdersInto(folded, orders) {
|
|
1948
|
+
const out = {
|
|
1949
|
+
counts: Object.fromEntries(Object.entries(folded?.counts ?? {}).map(([k, v]) => [k, { ...v }])),
|
|
1950
|
+
boundaryMs: folded?.boundaryMs ?? null
|
|
1951
|
+
};
|
|
1952
|
+
for (const o of orders) {
|
|
1953
|
+
const kind = String(o.kind ?? "");
|
|
1954
|
+
const status = String(o.status ?? "");
|
|
1955
|
+
const byStatus = out.counts[kind] ??= {};
|
|
1956
|
+
byStatus[status] = (byStatus[status] ?? 0) + 1;
|
|
1957
|
+
if (typeof o.terminalAtMs === "number" && Number.isFinite(o.terminalAtMs)) {
|
|
1958
|
+
out.boundaryMs = out.boundaryMs === null ? o.terminalAtMs : Math.max(out.boundaryMs, o.terminalAtMs);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
return out;
|
|
1962
|
+
}
|
|
1963
|
+
function standsNewOrderRow(record, eventTimeMs, folded) {
|
|
1964
|
+
if (!isOrderTerminal(record))
|
|
1965
|
+
return true;
|
|
1966
|
+
const boundary = folded?.boundaryMs;
|
|
1967
|
+
if (boundary === null || boundary === void 0)
|
|
1968
|
+
return true;
|
|
1969
|
+
if (typeof eventTimeMs !== "number" || !Number.isFinite(eventTimeMs))
|
|
1970
|
+
return true;
|
|
1971
|
+
return eventTimeMs > boundary;
|
|
1972
|
+
}
|
|
1805
1973
|
|
|
1806
1974
|
// ../ops-contract/dist/iso-duration.js
|
|
1807
1975
|
var RE = /^(-)?P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
|
|
@@ -2727,6 +2895,12 @@ var ObservedReducer = class {
|
|
|
2727
2895
|
persons = /* @__PURE__ */ new Map();
|
|
2728
2896
|
assets = /* @__PURE__ */ new Map();
|
|
2729
2897
|
orders = /* @__PURE__ */ new Map();
|
|
2898
|
+
/*
|
|
2899
|
+
* **접힌 오더** — 끝나서 뜨거운 목록에서 걷힌 것의 세는 수와 경계(ADR-0092). 접는 때는 호스트가 정한다
|
|
2900
|
+
* (체크포인트를 쓰기 직전에 `foldFinishedOrders` 를 부른다). 경계는 모델의 `orderFold` 선언이다.
|
|
2901
|
+
*/
|
|
2902
|
+
foldedOrders = emptyFoldedOrders();
|
|
2903
|
+
orderFoldPolicy;
|
|
2730
2904
|
/** 확인해 둔 주목 신호 id — `attention.acked` 이벤트로만 들어온다(계산으로 만들지 않는다). */
|
|
2731
2905
|
acked = /* @__PURE__ */ new Set();
|
|
2732
2906
|
revision = 0;
|
|
@@ -2760,6 +2934,9 @@ var ObservedReducer = class {
|
|
|
2760
2934
|
/** 시각 해석 기준(트윈 모델 선언) — 없으면 UTC. 캘린더의 `HH:MM` 이 어느 기준인지 정한다. */
|
|
2761
2935
|
utcOffsetMinutes;
|
|
2762
2936
|
constructor(model) {
|
|
2937
|
+
const fold = orderFoldPolicyOf(model.orderFold);
|
|
2938
|
+
if ("error" in fold) throw new Error(fold.error);
|
|
2939
|
+
this.orderFoldPolicy = fold.policy;
|
|
2763
2940
|
this.utcOffsetMinutes = model.utcOffsetMinutes;
|
|
2764
2941
|
this.classDefs = { personnel: model.personnelClasses, equipment: model.equipmentClasses, asset: model.assetClasses };
|
|
2765
2942
|
for (const sp of model.testSpecifications ?? []) this.testSpecs.set(sp.id, sp);
|
|
@@ -3154,10 +3331,19 @@ var ObservedReducer = class {
|
|
|
3154
3331
|
case OP_EVENT.order: {
|
|
3155
3332
|
const d = e.data;
|
|
3156
3333
|
if (this.stale(`order:${d.orderId}`, e)) return;
|
|
3334
|
+
const prev = this.orders.get(d.orderId);
|
|
3335
|
+
const at = Date.parse(String(e.eventTime ?? ""));
|
|
3336
|
+
if (!prev && !standsNewOrderRow(d, Number.isFinite(at) ? at : void 0, this.foldedOrders)) {
|
|
3337
|
+
this.lastAt.delete(`order:${d.orderId}`);
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
const terminal = isOrderTerminal({ status: d.status, requested: d.requested, fulfilled: d.fulfilled });
|
|
3341
|
+
const terminalAtMs = terminal ? prev?.terminalAtMs ?? (Number.isFinite(at) ? at : void 0) : void 0;
|
|
3157
3342
|
this.orders.set(d.orderId, {
|
|
3158
3343
|
id: d.orderId,
|
|
3159
3344
|
kind: d.kind,
|
|
3160
3345
|
status: d.status,
|
|
3346
|
+
...terminalAtMs !== void 0 ? { terminalAtMs } : {},
|
|
3161
3347
|
progress: d.requested ? d.fulfilled / d.requested : 0,
|
|
3162
3348
|
requested: d.requested,
|
|
3163
3349
|
fulfilled: d.fulfilled,
|
|
@@ -3601,6 +3787,8 @@ var ObservedReducer = class {
|
|
|
3601
3787
|
persons: [...this.persons.values()].map((x) => ({ ...x })),
|
|
3602
3788
|
assets: [...this.assets.values()].map((x) => ({ ...x })),
|
|
3603
3789
|
orders: [...this.orders.values()].map((o) => ({ ...o })),
|
|
3790
|
+
/* 접힌 쪽도 재개점에 든다 — 없으면 재기동 뒤에 끝난 오더의 수를 잃는다(ADR-0092). 접은 것이 없으면 싣지 않는다. */
|
|
3791
|
+
...foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {},
|
|
3604
3792
|
acked: [...this.acked],
|
|
3605
3793
|
corrections: this.corrections.map((c) => ({ ...c })),
|
|
3606
3794
|
unhandled: [...this.unhandled.entries()].map(([eventType, v]) => ({ eventType, ...v })),
|
|
@@ -3647,10 +3835,31 @@ var ObservedReducer = class {
|
|
|
3647
3835
|
this.persons = new Map((cp?.persons ?? []).map((x) => [x.id, { ...x }]));
|
|
3648
3836
|
this.assets = new Map((cp?.assets ?? []).map((x) => [x.id, { ...x }]));
|
|
3649
3837
|
this.orders = new Map((cp?.orders ?? []).map((o) => [o.id, { ...o }]));
|
|
3838
|
+
this.foldedOrders = cp?.foldedOrders ? structuredClone(cp.foldedOrders) : emptyFoldedOrders();
|
|
3650
3839
|
this.acked = new Set(cp?.acked ?? []);
|
|
3651
3840
|
this.corrections = (cp?.corrections ?? []).map((c) => ({ ...c }));
|
|
3652
3841
|
this.unhandled = new Map((cp?.unhandled ?? []).map(({ eventType, ...v }) => [eventType, { ...v }]));
|
|
3653
3842
|
}
|
|
3843
|
+
/**
|
|
3844
|
+
* **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
|
|
3845
|
+
*
|
|
3846
|
+
* 부르는 것은 호스트다: 체크포인트를 쓰기 직전에 부른다. 그래야 접힌 쪽이 씨앗과 같은 때에 적혀 재기동 동치
|
|
3847
|
+
* (`재개점 + 꼬리 == 0부터 접기`)가 선다. 「지금」은 트윈의 지금(마지막으로 들은 시각)이다 — 벽시계가 아니다.
|
|
3848
|
+
* 아직 아무것도 못 들었으면 접지 않는다.
|
|
3849
|
+
*/
|
|
3850
|
+
foldFinishedOrders(nowMs = this.observedAtMs) {
|
|
3851
|
+
if (nowMs === void 0 || !Number.isFinite(nowMs)) return 0;
|
|
3852
|
+
const ids = planOrderFold([...this.orders.values()], nowMs, this.orderFoldPolicy);
|
|
3853
|
+
if (!ids.length) return 0;
|
|
3854
|
+
const rows = ids.map((id) => this.orders.get(id)).filter(Boolean);
|
|
3855
|
+
this.foldedOrders = foldOrdersInto(this.foldedOrders, rows);
|
|
3856
|
+
for (const id of ids) {
|
|
3857
|
+
this.orders.delete(id);
|
|
3858
|
+
this.lastAt.delete(`order:${id}`);
|
|
3859
|
+
}
|
|
3860
|
+
this.revision++;
|
|
3861
|
+
return ids.length;
|
|
3862
|
+
}
|
|
3654
3863
|
snapshot() {
|
|
3655
3864
|
const occ = /* @__PURE__ */ new Map();
|
|
3656
3865
|
for (const it of this.items.values()) occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
|
|
@@ -3696,6 +3905,7 @@ var ObservedReducer = class {
|
|
|
3696
3905
|
tasks: [...this.tasks.values()].map((t) => structuredClone(t)),
|
|
3697
3906
|
equipment: [...this.equipment.values()].map((m) => ({ ...m, ...this.oeePart(m.id), ...this.effectivityPart(m), ...this.offShiftPart(`eq:${m.id}`), ...this.capabilityPart(m, `eq:${m.id}`, m.kind ? [m.kind] : [], this.classDefs.equipment) })),
|
|
3698
3907
|
orders: [...this.orders.values()].map((o) => ({ ...o })),
|
|
3908
|
+
...foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {},
|
|
3699
3909
|
acked: [...this.acked]
|
|
3700
3910
|
};
|
|
3701
3911
|
}
|
|
@@ -4033,7 +4243,21 @@ var ItemStore = class _ItemStore {
|
|
|
4033
4243
|
* 색인을 밖에 따로 두지 않는다 — 갱신하는 자리가 흩어지면 한 자리라도 빠뜨렸을 때 **재고가 조용히
|
|
4034
4244
|
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
4035
4245
|
*/
|
|
4246
|
+
/*
|
|
4247
|
+
* 품목 색인 — 키는 **품목 클래스**다(`itemClassOf`). 재고는 로트 클래스를 들고 오더는 품목 클래스를 묻는다
|
|
4248
|
+
* (ADR-0076 ⑥ · ADR-0082 결정 2). 로트 글자를 키로 쓰면 품목을 물어도 로트 재고가 안 나온다.
|
|
4249
|
+
*/
|
|
4036
4250
|
byGtin = /* @__PURE__ */ new Map();
|
|
4251
|
+
/** 같은 글자를 매번 뜯지 않는다 — 클래스 글자의 종류는 적고 물품은 많다. */
|
|
4252
|
+
itemClassCache = /* @__PURE__ */ new Map();
|
|
4253
|
+
itemClass(gtin) {
|
|
4254
|
+
let c = this.itemClassCache.get(gtin);
|
|
4255
|
+
if (c === void 0) {
|
|
4256
|
+
c = itemClassOf(gtin) ?? gtin;
|
|
4257
|
+
this.itemClassCache.set(gtin, c);
|
|
4258
|
+
}
|
|
4259
|
+
return c;
|
|
4260
|
+
}
|
|
4037
4261
|
get size() {
|
|
4038
4262
|
return this.map.size;
|
|
4039
4263
|
}
|
|
@@ -4123,7 +4347,7 @@ var ItemStore = class _ItemStore {
|
|
|
4123
4347
|
}
|
|
4124
4348
|
/** 그 품목인 물품들 — 색인이 답한다(자리를 모를 때 쓴다). */
|
|
4125
4349
|
ofGtin(gtin) {
|
|
4126
|
-
const keys = this.byGtin.get(gtin);
|
|
4350
|
+
const keys = this.byGtin.get(this.itemClass(gtin));
|
|
4127
4351
|
if (!keys) return [];
|
|
4128
4352
|
const out = [];
|
|
4129
4353
|
for (const k of keys) {
|
|
@@ -4151,13 +4375,13 @@ var ItemStore = class _ItemStore {
|
|
|
4151
4375
|
}
|
|
4152
4376
|
}
|
|
4153
4377
|
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`);
|
|
4378
|
+
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
4379
|
}
|
|
4156
4380
|
for (const [g, keys] of this.byGtin) {
|
|
4157
4381
|
for (const k of keys) {
|
|
4158
4382
|
const it = this.map.get(k);
|
|
4159
4383
|
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`);
|
|
4384
|
+
else if (this.itemClass(it.gtin ?? "") !== g) drift.push(`${k} \uC740 \uD488\uBAA9 '${it.gtin}' \uC778\uB370 '${g}' \uC0C9\uC778\uC5D0 \uC788\uB2E4`);
|
|
4161
4385
|
}
|
|
4162
4386
|
}
|
|
4163
4387
|
return drift;
|
|
@@ -4165,9 +4389,10 @@ var ItemStore = class _ItemStore {
|
|
|
4165
4389
|
index(key, location, gtin) {
|
|
4166
4390
|
this.indexLocation(key, location);
|
|
4167
4391
|
if (gtin) {
|
|
4168
|
-
const
|
|
4392
|
+
const g = this.itemClass(gtin);
|
|
4393
|
+
const set = this.byGtin.get(g) ?? /* @__PURE__ */ new Set();
|
|
4169
4394
|
set.add(key);
|
|
4170
|
-
this.byGtin.set(
|
|
4395
|
+
this.byGtin.set(g, set);
|
|
4171
4396
|
}
|
|
4172
4397
|
}
|
|
4173
4398
|
indexLocation(key, location) {
|
|
@@ -4178,10 +4403,11 @@ var ItemStore = class _ItemStore {
|
|
|
4178
4403
|
unindex(key, location, gtin) {
|
|
4179
4404
|
this.unindexLocation(key, location);
|
|
4180
4405
|
if (gtin) {
|
|
4181
|
-
const
|
|
4406
|
+
const g = this.itemClass(gtin);
|
|
4407
|
+
const set = this.byGtin.get(g);
|
|
4182
4408
|
if (!set) return;
|
|
4183
4409
|
set.delete(key);
|
|
4184
|
-
if (!set.size) this.byGtin.delete(
|
|
4410
|
+
if (!set.size) this.byGtin.delete(g);
|
|
4185
4411
|
}
|
|
4186
4412
|
}
|
|
4187
4413
|
unindexLocation(key, location) {
|
|
@@ -4209,6 +4435,10 @@ var FlowEngine = class {
|
|
|
4209
4435
|
assets = /* @__PURE__ */ new Map();
|
|
4210
4436
|
tasks = /* @__PURE__ */ new Map();
|
|
4211
4437
|
orders = /* @__PURE__ */ new Map();
|
|
4438
|
+
/** 접힌 오더(ADR-0092) — 세는 수와 경계만 든다. 미러는 리듀서가 들고 이것은 옮겨 받는다(§`hydrateObserved`). */
|
|
4439
|
+
foldedOrders = emptyFoldedOrders();
|
|
4440
|
+
/** 끝난 오더를 언제 접나 — 모델의 `orderFold` 선언. 없으면 기본(24시간 · 1,000건). */
|
|
4441
|
+
orderFoldPolicy = ORDER_FOLD_DEFAULT;
|
|
4212
4442
|
revision = 0;
|
|
4213
4443
|
clockMs = 0;
|
|
4214
4444
|
/**
|
|
@@ -4274,6 +4504,31 @@ var FlowEngine = class {
|
|
|
4274
4504
|
*
|
|
4275
4505
|
* 관측 구동이 아니면 `undefined` — 시뮬은 리듀서를 갖지 않는다(저장할 것이 없다).
|
|
4276
4506
|
*/
|
|
4507
|
+
/**
|
|
4508
|
+
* **끝난 오더를 접는다**(ADR-0092) — 접은 수를 돌려준다.
|
|
4509
|
+
*
|
|
4510
|
+
* 부르는 것은 호스트다 — 체크포인트(미러는 `observedCheckpoint`, 시뮬은 상태 스냅샷)를 쓰기 직전에 부른다. 그래야
|
|
4511
|
+
* 접힌 쪽이 씨앗과 같은 때에 적힌다. 커널이 스스로 접지 않는 것은, 접힌 목록을 읽는 쪽(예측 · 화면)이 그 뜻에
|
|
4512
|
+
* 맞춰진 뒤에 호스트가 부르게 하려는 것이다.
|
|
4513
|
+
*
|
|
4514
|
+
* 미러는 원천인 리듀서에서 접고 옮겨 받는다 — 이 목록에서만 지우면 다음 옮김이 되살린다.
|
|
4515
|
+
*/
|
|
4516
|
+
foldFinishedOrders(nowMs) {
|
|
4517
|
+
if (this.observeMode && this.observer) {
|
|
4518
|
+
const n = this.observer.foldFinishedOrders(nowMs);
|
|
4519
|
+
if (n) {
|
|
4520
|
+
this.observedDirty = true;
|
|
4521
|
+
this.settleObserved();
|
|
4522
|
+
}
|
|
4523
|
+
return n;
|
|
4524
|
+
}
|
|
4525
|
+
const ids = planOrderFold([...this.orders.values()], nowMs ?? this.nowMs(), this.orderFoldPolicy);
|
|
4526
|
+
if (!ids.length) return 0;
|
|
4527
|
+
this.foldedOrders = foldOrdersInto(this.foldedOrders, ids.map((id) => this.orders.get(id)).filter(Boolean));
|
|
4528
|
+
for (const id of ids) this.orders.delete(id);
|
|
4529
|
+
this.revision++;
|
|
4530
|
+
return ids.length;
|
|
4531
|
+
}
|
|
4277
4532
|
observedCheckpoint() {
|
|
4278
4533
|
if (!this.observer && this.observeMode) {
|
|
4279
4534
|
this.observer = new ObservedReducer(this.boardDef ?? { locations: [], equipment: [] });
|
|
@@ -4382,6 +4637,9 @@ var FlowEngine = class {
|
|
|
4382
4637
|
loadTwinModel(def) {
|
|
4383
4638
|
this.boardDef = def;
|
|
4384
4639
|
if (def.allocationPolicy) this.policy = allocationPolicyOf(def.allocationPolicy);
|
|
4640
|
+
const fold = orderFoldPolicyOf(def.orderFold);
|
|
4641
|
+
if ("error" in fold) throw new Error(fold.error);
|
|
4642
|
+
this.orderFoldPolicy = fold.policy;
|
|
4385
4643
|
if (def.productionSpec?.definition?.operations?.length) this.loadOperations(def.productionSpec.definition.operations);
|
|
4386
4644
|
for (const n of readBoardLocations(def)) this.locations.set(n.id, this.buildLocation(n));
|
|
4387
4645
|
this.classDefs = { personnel: def.personnelClasses, equipment: def.equipmentClasses, asset: def.assetClasses, material: def.materialClasses };
|
|
@@ -4759,6 +5017,12 @@ var FlowEngine = class {
|
|
|
4759
5017
|
...o.dockDoor ? { dockDoor: o.dockDoor } : {},
|
|
4760
5018
|
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
|
|
4761
5019
|
}));
|
|
5020
|
+
this.foldedOrders = snap.foldedOrders ? structuredClone(snap.foldedOrders) : emptyFoldedOrders();
|
|
5021
|
+
const terminalAtOf = new Map((snap.orders ?? []).map((o) => [o.id, o.terminalAtMs]));
|
|
5022
|
+
if (purpose === "observe") {
|
|
5023
|
+
const present = new Set(observedOrders.map((o) => o.orderId));
|
|
5024
|
+
for (const id of [...this.orders.keys()]) if (!present.has(id)) this.orders.delete(id);
|
|
5025
|
+
}
|
|
4762
5026
|
for (const o of observedOrders) {
|
|
4763
5027
|
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
4764
5028
|
const remaining = lines.length ? lines.reduce((s, l) => s + l.requested, 0) : Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
@@ -4797,7 +5061,9 @@ var FlowEngine = class {
|
|
|
4797
5061
|
/* 약속해 둔 자리·시각창을 이어받는다 — 없으면 도메인이 「정해 뒀다」고 믿는 것을 잃는다.
|
|
4798
5062
|
사실이 말해 주지 않으면(옛 저널) 비는 것이 사실이다 — 지어내지 않는다. */
|
|
4799
5063
|
...o.dockDoor ? { dockDoor: o.dockDoor } : {},
|
|
4800
|
-
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
|
|
5064
|
+
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {},
|
|
5065
|
+
/* 언제 끝났나 — 관측만 이어받는다(씨앗은 끝난 오더를 심지 않는다). */
|
|
5066
|
+
...observing && terminalAtOf.get(o.orderId) !== void 0 ? { terminalAtMs: terminalAtOf.get(o.orderId) } : {}
|
|
4801
5067
|
});
|
|
4802
5068
|
}
|
|
4803
5069
|
const seededOrderIds = new Set(this.orders.keys());
|
|
@@ -5197,10 +5463,13 @@ var FlowEngine = class {
|
|
|
5197
5463
|
...t.startTime ? { startTime: t.startTime } : {},
|
|
5198
5464
|
...t.endTime ? { endTime: t.endTime } : {}
|
|
5199
5465
|
})),
|
|
5466
|
+
/* 접힌 오더 — 접은 것이 없으면 싣지 않는다(ADR-0092). */
|
|
5467
|
+
...foldedOrderCount(this.foldedOrders) ? { foldedOrders: structuredClone(this.foldedOrders) } : {},
|
|
5200
5468
|
orders: [...this.orders.values()].map((o) => ({
|
|
5201
5469
|
id: o.id,
|
|
5202
5470
|
kind: o.kind,
|
|
5203
5471
|
status: o.status,
|
|
5472
|
+
...o.terminalAtMs !== void 0 ? { terminalAtMs: o.terminalAtMs } : {},
|
|
5204
5473
|
progress: o.requested ? o.fulfilled / o.requested : 0,
|
|
5205
5474
|
requested: o.requested,
|
|
5206
5475
|
fulfilled: o.fulfilled,
|
|
@@ -6236,6 +6505,8 @@ var FlowEngine = class {
|
|
|
6236
6505
|
}
|
|
6237
6506
|
/** 오더 델타 — **라인까지 싣는다.** 라인이 빠지면 미러가 남은 데맨드를 라인별로 재계획할 수 없다. */
|
|
6238
6507
|
emitOrder(o) {
|
|
6508
|
+
if (isOrderTerminal(o)) o.terminalAtMs ??= this.nowMs();
|
|
6509
|
+
else delete o.terminalAtMs;
|
|
6239
6510
|
this.emitOp(OP_EVENT.order, {
|
|
6240
6511
|
orderId: o.id,
|
|
6241
6512
|
kind: o.kind,
|
|
@@ -7276,7 +7547,7 @@ function planMakeToOrder(gtin, shortQty, ops, stock, locations) {
|
|
|
7276
7547
|
return { steps: [{ step: "process", operation: op.key, at: station.id }] };
|
|
7277
7548
|
}
|
|
7278
7549
|
function matches(s, req) {
|
|
7279
|
-
if (req.materialDefinition) return s.gtin
|
|
7550
|
+
if (req.materialDefinition) return sameItemClass(s.gtin, req.materialDefinition);
|
|
7280
7551
|
return false;
|
|
7281
7552
|
}
|
|
7282
7553
|
|
|
@@ -7323,11 +7594,27 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7323
7594
|
const poSeq = ++this.poSeq;
|
|
7324
7595
|
const po = this.requireBizTransactionId(`PO-${poSeq}`, "purchaseorder");
|
|
7325
7596
|
const eventTime = this.now();
|
|
7326
|
-
const
|
|
7597
|
+
const lot = `L-${poSeq}`;
|
|
7598
|
+
const lotClass = lotClassOfItem(gtin, lot) ?? gtin;
|
|
7599
|
+
const qtyList = [{ epcClass: lotClass, quantity: qty }];
|
|
7327
7600
|
const poTxn = [{ type: BTT.po, bizTransaction: po }];
|
|
7601
|
+
const inbound = {
|
|
7602
|
+
id: `inbound-${poSeq}`,
|
|
7603
|
+
kind: WMS_ORDER_KIND.inbound,
|
|
7604
|
+
status: "created",
|
|
7605
|
+
requested: qty,
|
|
7606
|
+
fulfilled: 0,
|
|
7607
|
+
bizTransaction: po,
|
|
7608
|
+
allocated: [],
|
|
7609
|
+
picked: [],
|
|
7610
|
+
shipmentEpc: null,
|
|
7611
|
+
lines: [{ gtin, requested: qty }]
|
|
7612
|
+
};
|
|
7613
|
+
this.orders.set(inbound.id, inbound);
|
|
7614
|
+
this.emitOrder(inbound);
|
|
7328
7615
|
const expiry = this.clockMs + SHELF_MS - this.epcSeq % 5 * SHELF_JITTER_MS;
|
|
7329
7616
|
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() });
|
|
7617
|
+
this.items.set(epc, { epc, gtin: lotClass, qty, location: dock.id, disposition: DISP.in_progress, expiry, ilmd, receivedAtMs: this.nowMs() });
|
|
7331
7618
|
dock.occupancy++;
|
|
7332
7619
|
this.emit(transactionEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, bizTransactionList: poTxn, epcList: [epc], quantityList: qtyList, readPoint: dock.id }));
|
|
7333
7620
|
this.emit(aggregationEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, parentID: epc, childQuantityList: qtyList, readPoint: dock.id }));
|
|
@@ -7343,6 +7630,9 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7343
7630
|
bizTransactionList: poTxn,
|
|
7344
7631
|
ilmd
|
|
7345
7632
|
}));
|
|
7633
|
+
inbound.fulfilled = qty;
|
|
7634
|
+
inbound.status = "completed";
|
|
7635
|
+
this.emitOrder(inbound);
|
|
7346
7636
|
const binId = this.policy.selectPlacement({ item: { epc, gtin, qty }, slots: this.slotViews("storage") });
|
|
7347
7637
|
if (!binId) return;
|
|
7348
7638
|
const id = `task-${++this.taskSeq}`;
|
|
@@ -7421,10 +7711,10 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7421
7711
|
const staging = this.builtInLocation("staging", "picked pallets wait here before shipping");
|
|
7422
7712
|
const chosenAll = [];
|
|
7423
7713
|
for (const line of o.lines) {
|
|
7424
|
-
const already = allocatedQty(o.allocated.filter((a) => this.items.get(a.epc)?.gtin
|
|
7714
|
+
const already = allocatedQty(o.allocated.filter((a) => sameItemClass(this.items.get(a.epc)?.gtin, line.gtin)));
|
|
7425
7715
|
const need = line.requested - already;
|
|
7426
7716
|
if (need <= 0) continue;
|
|
7427
|
-
const available = [...this.items.values()].filter((i) => i.gtin
|
|
7717
|
+
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
7718
|
const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
|
|
7429
7719
|
for (const row of chosen) {
|
|
7430
7720
|
o.allocated.push(row);
|
|
@@ -7488,7 +7778,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7488
7778
|
const locations = [...this.locations.values()].map((l) => ({ id: l.id, type: l.type }));
|
|
7489
7779
|
const ops = this.declaredOperations();
|
|
7490
7780
|
for (const line of o.lines) {
|
|
7491
|
-
const have = stock.filter((s) => s.gtin
|
|
7781
|
+
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
7782
|
const short = line.requested - have;
|
|
7493
7783
|
if (short <= 0) continue;
|
|
7494
7784
|
const plan = planMakeToOrder(line.gtin, short, ops, stock, locations);
|
|
@@ -7514,7 +7804,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7514
7804
|
}
|
|
7515
7805
|
/** 부품을 작업대로 — 팔레트 이동이므로 피킹과 같은 기제다(부분 소비는 코어가 한다). */
|
|
7516
7806
|
issueFeed(o, gtin, from, to, qty) {
|
|
7517
|
-
const src = [...this.items.values()].find((i) => i.gtin
|
|
7807
|
+
const src = [...this.items.values()].find((i) => sameItemClass(i.gtin, gtin) && i.location === from && i.disposition === DISP.sellable);
|
|
7518
7808
|
if (!src) return;
|
|
7519
7809
|
src.disposition = DISP.reserved;
|
|
7520
7810
|
this.pushTask(o, "feed", src.epc, from, to);
|
|
@@ -7592,7 +7882,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7592
7882
|
order.fulfilled += allocatedQty(order.allocated);
|
|
7593
7883
|
if (order.lines) for (const epc of order.picked) {
|
|
7594
7884
|
const g = this.items.get(epc)?.gtin;
|
|
7595
|
-
const line = order.lines.find((l) => l.gtin
|
|
7885
|
+
const line = order.lines.find((l) => sameItemClass(l.gtin, g) && l.requested > 0);
|
|
7596
7886
|
if (line) line.requested--;
|
|
7597
7887
|
}
|
|
7598
7888
|
for (const epc of order.picked) {
|
|
@@ -8433,7 +8723,7 @@ The steps are the source of truth for execution order; the hierarchy only names
|
|
|
8433
8723
|
if (fromType) {
|
|
8434
8724
|
for (const n of locsOfType.get(fromType) ?? []) {
|
|
8435
8725
|
for (const i of this.items.at(n.id)) {
|
|
8436
|
-
if (i.gtin
|
|
8726
|
+
if (sameItemClass(i.gtin, g) && i.disposition === DISP.sellable) available.push({ epc: i.epc, location: i.location, qty: 1 });
|
|
8437
8727
|
}
|
|
8438
8728
|
}
|
|
8439
8729
|
} 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.25",
|
|
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.36"
|
|
32
32
|
}
|
|
33
33
|
}
|