@operato/twin-kernel 0.11.22 → 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 +6 -1
- package/dist/flow-engine.js +40 -11
- 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.js +14 -4
- package/dist-cjs/index.cjs +247 -32
- package/package.json +2 -2
package/dist/flow-engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MaterialActual, RecipeExecutionBasis, TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration, TestSpecificationCriterion, LocationObservation, DispositionFact, AllocatedQuantity, MaterialLotUse } from '@operato/ops-contract';
|
|
1
|
+
import type { MaterialActual, RecipeExecutionBasis, MaterialExecutionBasis, TestResult, ISOTime, MaterialQuantity, WorkCalendarEntry, EffectivePeriod, Effectivity, OffCalendarReason, ResourceProperty, ResourceClassDef, MaterialDefinition, Attention, TwinModelDef, CanonicalEnvelope, Command, CommandAck, EventHandler, EquipmentMotion, AssetState, GeneratorSpec, InterventionOutcome, OrderState, PersonState, ScenarioControl, ScenarioOverride, StateSnapshot, TwinKernel, Unsubscribe, LocationState, ItemState, EquipmentState, OrderStatusDelta, TaskState, TaskStatus, StructureShift, IdentityGroundingView, IdentityDeclaration, TestSpecificationCriterion, LocationObservation, DispositionFact, AllocatedQuantity, MaterialLotUse } from '@operato/ops-contract';
|
|
2
2
|
import { type RatedUsage, type UsedUsage } from '@operato/ops-contract';
|
|
3
3
|
import type { VocabularyElement } from '@operato/ops-contract';
|
|
4
4
|
import type { ReducerCheckpoint } from './observed-reducer.ts';
|
|
@@ -197,6 +197,7 @@ export interface FlowTask {
|
|
|
197
197
|
* 채널에 실어야 실적을 **한 곳에서** 읽는다(EPCIS 이벤트에서 다시 계산하려면 작업과 잇는 끈이 없다).
|
|
198
198
|
*/
|
|
199
199
|
materialActual?: MaterialActual[];
|
|
200
|
+
workShiftBasis?: import('@operato/ops-contract').WorkShiftExecutionBasis;
|
|
200
201
|
itemEpc: string;
|
|
201
202
|
fromNode: string;
|
|
202
203
|
toNode: string;
|
|
@@ -271,6 +272,7 @@ export declare function allocatedQty(rows: readonly AllocatedQuantity[] | undefi
|
|
|
271
272
|
/** 잡아 둔 줄들의 식별자 — EPCIS 이벤트가 싣는 것은 열쇠뿐이다. */
|
|
272
273
|
export declare function allocatedEpcs(rows: readonly AllocatedQuantity[] | undefined): string[];
|
|
273
274
|
export interface FlowOrder {
|
|
275
|
+
materialBasis?: MaterialExecutionBasis[];
|
|
274
276
|
recipeBasis?: RecipeExecutionBasis;
|
|
275
277
|
id: string;
|
|
276
278
|
kind: string;
|
|
@@ -498,6 +500,9 @@ export declare class ItemStore {
|
|
|
498
500
|
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
499
501
|
*/
|
|
500
502
|
private byGtin;
|
|
503
|
+
/** 같은 글자를 매번 뜯지 않는다 — 클래스 글자의 종류는 적고 물품은 많다. */
|
|
504
|
+
private itemClassCache;
|
|
505
|
+
private itemClass;
|
|
501
506
|
get size(): number;
|
|
502
507
|
get(key: string): FlowItem | undefined;
|
|
503
508
|
has(key: string): boolean;
|
package/dist/flow-engine.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* 통합 타입은 도메인 필드를 옵셔널로 넓혀(FlowItem.gtin?, FlowOrder.shipmentEpc? 등) 두 도메인을 담는다.
|
|
10
10
|
* (roadmap Phase5 발견 → 추출. [[project_flow_single_base_vision]] FlowLocation 단일 base 방향과 정합.)
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
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, itemClassOf, identityGroundingOf, outsideLimit, computeOee } from '@operato/ops-contract';
|
|
13
14
|
import { ObservedReducer } from "./observed-reducer.js";
|
|
14
15
|
/* 주체를 정하는 규칙은 유입 문과 한 벌이다 — 두 곳에 적으면 한쪽만 고쳐진다. */
|
|
15
16
|
import { resolveSubject } from '@operato/ops-contract';
|
|
@@ -406,7 +407,21 @@ export class ItemStore {
|
|
|
406
407
|
* 색인을 밖에 따로 두지 않는다 — 갱신하는 자리가 흩어지면 한 자리라도 빠뜨렸을 때 **재고가 조용히
|
|
407
408
|
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
408
409
|
*/
|
|
410
|
+
/*
|
|
411
|
+
* 품목 색인 — 키는 **품목 클래스**다(`itemClassOf`). 재고는 로트 클래스를 들고 오더는 품목 클래스를 묻는다
|
|
412
|
+
* (ADR-0076 ⑥ · ADR-0082 결정 2). 로트 글자를 키로 쓰면 품목을 물어도 로트 재고가 안 나온다.
|
|
413
|
+
*/
|
|
409
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
|
+
}
|
|
410
425
|
get size() {
|
|
411
426
|
return this.map.size;
|
|
412
427
|
}
|
|
@@ -514,7 +529,7 @@ export class ItemStore {
|
|
|
514
529
|
}
|
|
515
530
|
/** 그 품목인 물품들 — 색인이 답한다(자리를 모를 때 쓴다). */
|
|
516
531
|
ofGtin(gtin) {
|
|
517
|
-
const keys = this.byGtin.get(gtin);
|
|
532
|
+
const keys = this.byGtin.get(this.itemClass(gtin));
|
|
518
533
|
if (!keys)
|
|
519
534
|
return [];
|
|
520
535
|
const out = [];
|
|
@@ -548,7 +563,7 @@ export class ItemStore {
|
|
|
548
563
|
}
|
|
549
564
|
/* 품목 색인도 같은 규율로 본다 — 어긋나면 자재가 있는데 없다고 판정된다. */
|
|
550
565
|
for (const [key, it] of this.map) {
|
|
551
|
-
if (it.gtin && !this.byGtin.get(it.gtin)?.has(key))
|
|
566
|
+
if (it.gtin && !this.byGtin.get(this.itemClass(it.gtin))?.has(key))
|
|
552
567
|
drift.push(`${key} 이 품목 '${it.gtin}' 색인에 없다`);
|
|
553
568
|
}
|
|
554
569
|
for (const [g, keys] of this.byGtin) {
|
|
@@ -556,7 +571,7 @@ export class ItemStore {
|
|
|
556
571
|
const it = this.map.get(k);
|
|
557
572
|
if (!it)
|
|
558
573
|
drift.push(`${k} 이 지워졌는데 품목 '${g}' 색인에 남아 있다`);
|
|
559
|
-
else if (it.gtin !== g)
|
|
574
|
+
else if (this.itemClass(it.gtin ?? '') !== g)
|
|
560
575
|
drift.push(`${k} 은 품목 '${it.gtin}' 인데 '${g}' 색인에 있다`);
|
|
561
576
|
}
|
|
562
577
|
}
|
|
@@ -565,9 +580,10 @@ export class ItemStore {
|
|
|
565
580
|
index(key, location, gtin) {
|
|
566
581
|
this.indexLocation(key, location);
|
|
567
582
|
if (gtin) {
|
|
568
|
-
const
|
|
583
|
+
const g = this.itemClass(gtin);
|
|
584
|
+
const set = this.byGtin.get(g) ?? new Set();
|
|
569
585
|
set.add(key);
|
|
570
|
-
this.byGtin.set(
|
|
586
|
+
this.byGtin.set(g, set);
|
|
571
587
|
}
|
|
572
588
|
}
|
|
573
589
|
indexLocation(key, location) {
|
|
@@ -578,12 +594,13 @@ export class ItemStore {
|
|
|
578
594
|
unindex(key, location, gtin) {
|
|
579
595
|
this.unindexLocation(key, location);
|
|
580
596
|
if (gtin) {
|
|
581
|
-
const
|
|
597
|
+
const g = this.itemClass(gtin);
|
|
598
|
+
const set = this.byGtin.get(g);
|
|
582
599
|
if (!set)
|
|
583
600
|
return;
|
|
584
601
|
set.delete(key);
|
|
585
602
|
if (!set.size)
|
|
586
|
-
this.byGtin.delete(
|
|
603
|
+
this.byGtin.delete(g);
|
|
587
604
|
}
|
|
588
605
|
}
|
|
589
606
|
unindexLocation(key, location) {
|
|
@@ -1050,6 +1067,10 @@ export class FlowEngine {
|
|
|
1050
1067
|
* 그래서 규칙을 둘로 쪼개지 않고 **목적을 밝힌다.** 같은 함수, 같은 주입 경로, 다른 판정 하나다.
|
|
1051
1068
|
*/
|
|
1052
1069
|
purpose = 'seed') {
|
|
1070
|
+
for (const task of snap.tasks ?? []) {
|
|
1071
|
+
if (task.workShiftBasis !== undefined && !isWorkShiftExecutionBasis(task.workShiftBasis))
|
|
1072
|
+
throw new Error('Invalid pinned work shift evidence');
|
|
1073
|
+
}
|
|
1053
1074
|
/* 확인 처리를 먼저 이어받는다 — 아래에서 상태를 주입하면 곧바로 주목 신호가 계산되므로, 늦게
|
|
1054
1075
|
* 이어받으면 그 한 번은 확인 안 된 것으로 계산된다(화면이 잠깐 빨개진다). */
|
|
1055
1076
|
for (const id of snap.acked ?? [])
|
|
@@ -1242,6 +1263,7 @@ export class FlowEngine {
|
|
|
1242
1263
|
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
1243
1264
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1244
1265
|
...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
|
|
1266
|
+
...(o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {}),
|
|
1245
1267
|
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
1246
1268
|
...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
|
|
1247
1269
|
...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
|
|
@@ -1305,6 +1327,7 @@ export class FlowEngine {
|
|
|
1305
1327
|
남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
|
|
1306
1328
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1307
1329
|
...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
|
|
1330
|
+
...(o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {}),
|
|
1308
1331
|
bizTransaction: o.bizTransaction ?? '', allocated: resolved.kept, picked: [], shipmentEpc: null,
|
|
1309
1332
|
/* 라인도 같다 — 씨앗은 남은 양만 남기고, 미러는 원본이 말한 요청·이행을 그대로 든다. */
|
|
1310
1333
|
lines: observing ? (o.lines ?? []).map(l => ({ ...l })) : lines,
|
|
@@ -1368,6 +1391,7 @@ export class FlowEngine {
|
|
|
1368
1391
|
status: purpose === 'observe' ? t.status : known && t.status === 'in-progress' ? 'in-progress' : 'created',
|
|
1369
1392
|
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
1370
1393
|
...(t.materialActual?.length ? { materialActual: t.materialActual.map(r => ({ ...r })) } : {}),
|
|
1394
|
+
...(t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {}),
|
|
1371
1395
|
itemEpc: t.itemRefs?.[0] ?? '',
|
|
1372
1396
|
fromNode: t.fromNode ?? '', toNode: t.toNode ?? '',
|
|
1373
1397
|
/*
|
|
@@ -1802,6 +1826,7 @@ export class FlowEngine {
|
|
|
1802
1826
|
...(t.assets?.length ? { assets: t.assets.slice() } : {}),
|
|
1803
1827
|
...(t.resources?.length ? { resources: t.resources.slice() } : {}),
|
|
1804
1828
|
...(t.materialActual?.length ? { materialActual: t.materialActual.map(r => ({ ...r })) } : {}),
|
|
1829
|
+
...(t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {}),
|
|
1805
1830
|
...(t.priority !== undefined ? { priority: t.priority } : {}),
|
|
1806
1831
|
...(t.startTime ? { startTime: t.startTime } : {}),
|
|
1807
1832
|
...(t.endTime ? { endTime: t.endTime } : {})
|
|
@@ -1823,6 +1848,7 @@ export class FlowEngine {
|
|
|
1823
1848
|
/* **어느 레시피로 만드나** — 품목만으로는 「무엇으로」가 안 남는다(같은 품목의 대체 레시피가 있다). */
|
|
1824
1849
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
1825
1850
|
...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
|
|
1851
|
+
...(o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {}),
|
|
1826
1852
|
/* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
|
|
1827
1853
|
...(o.seedIncomplete ? { seedIncomplete: true } : {}),
|
|
1828
1854
|
/* 왜 대기하는지 — 없으면 화면은 「running」만 보여 주고 사람은 원인을 찾을 자리가 없다. */
|
|
@@ -2954,6 +2980,7 @@ export class FlowEngine {
|
|
|
2954
2980
|
...(t.resources?.length ? { resources: t.resources.slice() } : {}),
|
|
2955
2981
|
/* 실제 자재 이동 — 인원·설비와 같은 채널(실적을 한 곳에서 읽는다). */
|
|
2956
2982
|
...(t.materialActual?.length ? { materialActual: t.materialActual.map(r => ({ ...r })) } : {}),
|
|
2983
|
+
...(t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {}),
|
|
2957
2984
|
...(t.durationMs ? { durationMs: t.durationMs } : {}),
|
|
2958
2985
|
/* 품질 판정이 있었으면 **그 자리에서** 실어 보낸다 — 없으면 필드를 만들지 않는다(모름≠양품). */
|
|
2959
2986
|
...(t.outcome ? { outcome: t.outcome } : {}),
|
|
@@ -2993,6 +3020,7 @@ export class FlowEngine {
|
|
|
2993
3020
|
레시피가 있다). 시뮬만 아는 사실이 남으면 두 스냅샷을 같은 규칙으로 읽을 수 없다. */
|
|
2994
3021
|
...(o.recipeKey ? { recipeKey: o.recipeKey } : {}),
|
|
2995
3022
|
...(o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {}),
|
|
3023
|
+
...(o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {}),
|
|
2996
3024
|
...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
|
|
2997
3025
|
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
2998
3026
|
/* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
|
|
@@ -3197,7 +3225,7 @@ export class FlowEngine {
|
|
|
3197
3225
|
for (const it of this.items.at(at)) {
|
|
3198
3226
|
if (remaining <= 0)
|
|
3199
3227
|
break;
|
|
3200
|
-
if (!this.materialMatches(it, req))
|
|
3228
|
+
if (!this.materialMatches(it, req, t.orderId))
|
|
3201
3229
|
continue;
|
|
3202
3230
|
/*
|
|
3203
3231
|
* 변환 중인 것은 잡지 않는다 — 어떤 공정이 이미 먹어 그 단계의 산출로 바뀔 물품이다
|
|
@@ -3221,12 +3249,13 @@ export class FlowEngine {
|
|
|
3221
3249
|
return picked;
|
|
3222
3250
|
}
|
|
3223
3251
|
/** 이 물품이 명세를 만족하나 — 품목 지목이 우선, 없으면 등급(상속을 타고 닫는다). */
|
|
3224
|
-
materialMatches(it, req) {
|
|
3252
|
+
materialMatches(it, req, orderId) {
|
|
3225
3253
|
const defId = it.definitionId ?? it.gtin;
|
|
3226
3254
|
if (req.materialDefinition)
|
|
3227
3255
|
return defId === req.materialDefinition;
|
|
3228
3256
|
if (req.materialClass) {
|
|
3229
|
-
const
|
|
3257
|
+
const basis = orderId ? this.orders.get(orderId)?.materialBasis : undefined;
|
|
3258
|
+
const def = basis !== undefined ? basis.find(b => b.material.id === defId)?.material : defId ? this.materialDefs.get(defId) : undefined;
|
|
3230
3259
|
return classClosure(def?.materialClassIds, this.classDefs.material, this.now()).has(req.materialClass);
|
|
3231
3260
|
}
|
|
3232
3261
|
return true; // 지목도 등급도 없으면 아무 자재나(명세가 그렇게 느슨하면 그대로 따른다)
|
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/observed-reducer.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - 운영 델타(task/equipment/order.status) → tasks·equipment·orders (EPCIS 로 재구성 불가한 절반)
|
|
20
20
|
* 마스터(로케이션)는 board 초기화 + applyMaster 로 갱신(마스터 동기).
|
|
21
21
|
*/
|
|
22
|
-
import { computeOee } from '@operato/ops-contract';
|
|
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
25
|
import { expiryFromAttributes, lotFromAttributes } from '@operato/ops-contract';
|
|
@@ -345,6 +345,8 @@ export class ObservedReducer {
|
|
|
345
345
|
switch (e.eventType) {
|
|
346
346
|
case OP_EVENT.task: {
|
|
347
347
|
const d = e.data;
|
|
348
|
+
if (d.workShiftBasis !== undefined && !isWorkShiftExecutionBasis(d.workShiftBasis))
|
|
349
|
+
throw new Error('Invalid pinned work shift evidence');
|
|
348
350
|
if (this.stale(`task:${d.taskId}`, e))
|
|
349
351
|
return;
|
|
350
352
|
/* 커널이 실어 보낸 것을 그대로 담는다 — 진척·남은 시간·의도가 있어야 이 상태를 씨앗으로
|
|
@@ -387,6 +389,7 @@ export class ObservedReducer {
|
|
|
387
389
|
...(d.resources?.length ? { resources: d.resources.slice() } : {}),
|
|
388
390
|
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
389
391
|
말하지 않았으면 **앞의 것을 지킨다**(위 주석) — 빈 배열은 「지웠다」로 말한 것이다. */
|
|
392
|
+
...(d.workShiftBasis ? { workShiftBasis: structuredClone(d.workShiftBasis) } : prevTask?.workShiftBasis ? { workShiftBasis: structuredClone(prevTask.workShiftBasis) } : {}),
|
|
390
393
|
...(d.materialActual?.length
|
|
391
394
|
? { materialActual: d.materialActual.map(r => ({ ...r })) }
|
|
392
395
|
: keptActual?.length
|
|
@@ -509,6 +512,8 @@ export class ObservedReducer {
|
|
|
509
512
|
* **지금 상태를 바꾸지 않는다.** 어제의 구간이 오늘의 상태가 되면 안 된다.
|
|
510
513
|
*/
|
|
511
514
|
const d = e.data;
|
|
515
|
+
if (d.workShiftBasis !== undefined && !isWorkShiftExecutionBasis(d.workShiftBasis))
|
|
516
|
+
throw new Error('Invalid pinned work shift evidence');
|
|
512
517
|
if (!d?.moverId || !d?.from || !d?.to)
|
|
513
518
|
break;
|
|
514
519
|
const from = Date.parse(d.from);
|
|
@@ -723,6 +728,7 @@ export class ObservedReducer {
|
|
|
723
728
|
/* 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(같은 품목에 대체 레시피). */
|
|
724
729
|
...(d.recipeKey ? { recipeKey: d.recipeKey } : {}),
|
|
725
730
|
...(d.recipeBasis ? { recipeBasis: structuredClone(d.recipeBasis) } : {}),
|
|
731
|
+
...(d.materialBasis ? { materialBasis: structuredClone(d.materialBasis) } : {}),
|
|
726
732
|
...(d.allocated?.length ? { allocated: d.allocated.slice() } : {}),
|
|
727
733
|
...(d.bizTransaction ? { bizTransaction: d.bizTransaction } : {}),
|
|
728
734
|
/* 이행하는 계획 — 「이 계획이 얼마나 됐나」를 이 값으로 묶어 답한다(§`OrderState.operationsRequestId`). */
|
|
@@ -1321,7 +1327,7 @@ export class ObservedReducer {
|
|
|
1321
1327
|
/* 수량으로 담긴 것도 이어받는다 — 되풀어 주지 않는 축이다(원천은 담김을 다시 말해 주지 않는다). */
|
|
1322
1328
|
qtyAggregation: [...this.qtyAggregation.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
1323
1329
|
pendingQuantities: [...this.pendingQuantities.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
1324
|
-
tasks: [...this.tasks.values()].map(t => (
|
|
1330
|
+
tasks: [...this.tasks.values()].map(t => structuredClone(t)),
|
|
1325
1331
|
equipment: [...this.equipment.values()].map(m => ({ ...m })),
|
|
1326
1332
|
persons: [...this.persons.values()].map(x => ({ ...x })),
|
|
1327
1333
|
assets: [...this.assets.values()].map(x => ({ ...x })),
|
|
@@ -1349,6 +1355,10 @@ export class ObservedReducer {
|
|
|
1349
1355
|
* 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
|
|
1350
1356
|
*/
|
|
1351
1357
|
restore(cp) {
|
|
1358
|
+
for (const task of cp?.tasks ?? []) {
|
|
1359
|
+
if (task.workShiftBasis !== undefined && !isWorkShiftExecutionBasis(task.workShiftBasis))
|
|
1360
|
+
throw new Error('Invalid pinned work shift evidence');
|
|
1361
|
+
}
|
|
1352
1362
|
this.observations = new Map((cp?.observations ?? []).map(o => [o.id, new Map((o.values ?? []).map(v => [v.propertyId, { ...v }]))]));
|
|
1353
1363
|
this.revision = cp?.revision ?? 0;
|
|
1354
1364
|
this.master = new Map((cp?.master ?? []).map(n => [n.id, { ...n }]));
|
|
@@ -1359,7 +1369,7 @@ export class ObservedReducer {
|
|
|
1359
1369
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map(x => [x.child, x.parent]));
|
|
1360
1370
|
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map(x => [x.parent, x.quantities]));
|
|
1361
1371
|
this.pendingQuantities = new Map((cp?.pendingQuantities ?? []).map(x => [x.parent, x.quantities]));
|
|
1362
|
-
this.tasks = new Map((cp?.tasks ?? []).map(t => [t.id,
|
|
1372
|
+
this.tasks = new Map((cp?.tasks ?? []).map(t => [t.id, structuredClone(t)]));
|
|
1363
1373
|
this.equipment = new Map((cp?.equipment ?? []).map(m => [m.id, { ...m }]));
|
|
1364
1374
|
this.oeeCounters = new Map((cp?.oeeCounters ?? []).map((c) => [String(c.id), { ...emptyOeeCounters(), ...c, id: undefined }]));
|
|
1365
1375
|
this.persons = new Map((cp?.persons ?? []).map(x => [x.id, { ...x }]));
|
|
@@ -1412,7 +1422,7 @@ export class ObservedReducer {
|
|
|
1412
1422
|
만료는 이벤트 없이 시각만으로 일어나므로, 델타로 받아 두면 유휴 자원이 영원히 유효하게 남는다. */
|
|
1413
1423
|
persons: [...this.persons.values()].map(p => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`), ...this.capabilityPart(p, `person:${p.id}`, p.personnelClassIds, this.classDefs.personnel) })),
|
|
1414
1424
|
assets: [...this.assets.values()].map(a => ({ ...a, ...this.effectivityPart(a), ...this.capabilityPart(a, `asset:${a.id}`, a.assetClassIds, this.classDefs.asset) })),
|
|
1415
|
-
tasks: [...this.tasks.values()].map(t => (
|
|
1425
|
+
tasks: [...this.tasks.values()].map(t => structuredClone(t)),
|
|
1416
1426
|
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) })),
|
|
1417
1427
|
orders: [...this.orders.values()].map(o => ({ ...o })),
|
|
1418
1428
|
acked: [...this.acked]
|
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)?)?$/;
|
|
@@ -1846,6 +1952,43 @@ function readEpochMs(raw) {
|
|
|
1846
1952
|
return Number.isNaN(at) ? void 0 : at;
|
|
1847
1953
|
}
|
|
1848
1954
|
|
|
1955
|
+
// ../ops-contract/dist/work-shift-basis.js
|
|
1956
|
+
var instant = (value) => typeof value === "string" && /(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value));
|
|
1957
|
+
var nonempty = (value) => typeof value === "string" && !!value.trim();
|
|
1958
|
+
var clock = (value) => typeof value === "string" && /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(value);
|
|
1959
|
+
function isWorkShiftExecutionBasis(value) {
|
|
1960
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1961
|
+
return false;
|
|
1962
|
+
const b = value;
|
|
1963
|
+
if (![b.basisId, b.arrangementId, b.revisionId, b.planId, b.contentHash].every(nonempty) || !Number.isSafeInteger(b.revisionNumber) || b.revisionNumber < 1 || !instant(b.businessTime) || !nonempty(b.timezone))
|
|
1964
|
+
return false;
|
|
1965
|
+
try {
|
|
1966
|
+
new Intl.DateTimeFormat("en", { timeZone: b.timezone }).format(0);
|
|
1967
|
+
} catch {
|
|
1968
|
+
return false;
|
|
1969
|
+
}
|
|
1970
|
+
if (typeof b.workDate !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(b.workDate) || !Number.isFinite(Date.parse(b.workDate)) || new Date(b.workDate).toISOString().slice(0, 10) !== b.workDate)
|
|
1971
|
+
return false;
|
|
1972
|
+
if (!Array.isArray(b.shifts) || !b.shifts.length || b.shifts.length > 64)
|
|
1973
|
+
return false;
|
|
1974
|
+
const names = /* @__PURE__ */ new Set();
|
|
1975
|
+
for (const s of b.shifts) {
|
|
1976
|
+
if (!s || typeof s !== "object" || !nonempty(s.name) || names.has(s.name) || s.description !== void 0 && typeof s.description !== "string" || ![-1, 0, 1].includes(s.fromDate) || ![-1, 0, 1].includes(s.toDate) || !clock(s.fromTime) || !clock(s.toTime) || typeof s.daysOfWeek !== "string" || !/^(?:[0-6](?:,[0-6])*)?$/.test(s.daysOfWeek))
|
|
1977
|
+
return false;
|
|
1978
|
+
const minutes = (time) => Number(time.slice(0, 2)) * 60 + Number(time.slice(3));
|
|
1979
|
+
const duration = (s.toDate - s.fromDate) * 1440 + minutes(s.toTime) - minutes(s.fromTime);
|
|
1980
|
+
if (duration <= 0 || duration > 1440)
|
|
1981
|
+
return false;
|
|
1982
|
+
names.add(s.name);
|
|
1983
|
+
}
|
|
1984
|
+
if (b.shiftName === void 0)
|
|
1985
|
+
return b.occurrence === void 0;
|
|
1986
|
+
if (!nonempty(b.shiftName) || !names.has(b.shiftName) || !b.occurrence || !instant(b.occurrence.from) || !instant(b.occurrence.to))
|
|
1987
|
+
return false;
|
|
1988
|
+
const at = Date.parse(b.businessTime);
|
|
1989
|
+
return Date.parse(b.occurrence.from) <= at && at < Date.parse(b.occurrence.to);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1849
1992
|
// ../ops-contract/dist/operational-ingest.js
|
|
1850
1993
|
var TASK_STATUS = ["created", "assigned", "in-progress", "completed"];
|
|
1851
1994
|
var EQUIPMENT_STATUS = ["idle", "busy", "down", "setup", "planned-stop"];
|
|
@@ -1881,7 +2024,8 @@ var SPECS = {
|
|
|
1881
2024
|
startTime: "string",
|
|
1882
2025
|
endTime: "string",
|
|
1883
2026
|
materialActual: "object[]",
|
|
1884
|
-
recordTime: "string"
|
|
2027
|
+
recordTime: "string",
|
|
2028
|
+
workShiftBasis: "object"
|
|
1885
2029
|
},
|
|
1886
2030
|
/*
|
|
1887
2031
|
* **자재 줄의 안쪽** — 계약의 `MaterialActual` 그대로다.
|
|
@@ -1897,6 +2041,11 @@ var SPECS = {
|
|
|
1897
2041
|
* 물려받으면 단위를 모르는 현장의 사실이 통째로 거절된다.
|
|
1898
2042
|
*/
|
|
1899
2043
|
shapes: {
|
|
2044
|
+
workShiftBasis: {
|
|
2045
|
+
required: ["basisId", "arrangementId", "revisionId", "revisionNumber", "planId", "contentHash", "businessTime", "timezone", "shifts", "workDate"],
|
|
2046
|
+
fields: { basisId: "string", arrangementId: "string", revisionId: "string", revisionNumber: "number", planId: "string", contentHash: "string", businessTime: "string", timezone: "string", workDate: "string" }
|
|
2047
|
+
// Full nested content and selected occurrence are checked by isWorkShiftExecutionBasis below.
|
|
2048
|
+
},
|
|
1900
2049
|
materialActual: {
|
|
1901
2050
|
required: ["definitionId", "use"],
|
|
1902
2051
|
fields: { definitionId: "string", lotId: "string", quantity: "number", uom: "string" },
|
|
@@ -2023,13 +2172,19 @@ var SPECS = {
|
|
|
2023
2172
|
* 새 어휘가 아니다 — 계약이 `recipeKey` 를 네 자리에서 이미 쓴다. 오더 관측에만 자리가 없었다.
|
|
2024
2173
|
*/
|
|
2025
2174
|
recipeKey: "string",
|
|
2026
|
-
recipeBasis: "object"
|
|
2175
|
+
recipeBasis: "object",
|
|
2176
|
+
materialBasis: "object[]"
|
|
2027
2177
|
},
|
|
2028
2178
|
/*
|
|
2029
2179
|
* 확보분의 안쪽을 본다 — 열쇠와 수 둘이다(§`AllocatedQuantity`). 안 보면 틀린 이름이 조용히
|
|
2030
2180
|
* 지나가고(`epcs`·`quantity` 따위), 미러는 잡힌 수를 0 으로 읽는다.
|
|
2031
2181
|
*/
|
|
2032
2182
|
shapes: {
|
|
2183
|
+
materialBasis: {
|
|
2184
|
+
required: ["basisId", "materialId", "revisionId", "revisionNumber", "planId", "contentHash", "businessTime", "material"],
|
|
2185
|
+
fields: { basisId: "string", materialId: "string", revisionId: "string", revisionNumber: "number", planId: "string", contentHash: "string", businessTime: "string" }
|
|
2186
|
+
// Recursive material properties and reference lists are checked by isMaterialExecutionBasis.
|
|
2187
|
+
},
|
|
2033
2188
|
recipeBasis: {
|
|
2034
2189
|
required: ["basisId", "recipeId", "revisionId", "planId", "contentHash", "businessTime", "recipe"],
|
|
2035
2190
|
fields: { basisId: "string", recipeId: "string", revisionId: "string", planId: "string", contentHash: "string", businessTime: "string" }
|
|
@@ -2115,7 +2270,14 @@ var SPECS = {
|
|
|
2115
2270
|
/* 사람이 정한 사유 — 없을 수 있다(적지 않은 것과 사유가 없는 것은 다르므로 지어내지 않는다). */
|
|
2116
2271
|
reasonCode: "string",
|
|
2117
2272
|
decidedBy: "string",
|
|
2118
|
-
recordTime: "string"
|
|
2273
|
+
recordTime: "string",
|
|
2274
|
+
workShiftBasis: "object"
|
|
2275
|
+
},
|
|
2276
|
+
shapes: {
|
|
2277
|
+
workShiftBasis: {
|
|
2278
|
+
required: ["basisId", "arrangementId", "revisionId", "revisionNumber", "planId", "contentHash", "businessTime", "timezone", "shifts", "workDate"],
|
|
2279
|
+
fields: { basisId: "string", arrangementId: "string", revisionId: "string", revisionNumber: "number", planId: "string", contentHash: "string", businessTime: "string", timezone: "string", workDate: "string" }
|
|
2280
|
+
}
|
|
2119
2281
|
},
|
|
2120
2282
|
enums: { status: EQUIPMENT_STATUS }
|
|
2121
2283
|
},
|
|
@@ -2898,6 +3060,7 @@ var ObservedReducer = class {
|
|
|
2898
3060
|
switch (e.eventType) {
|
|
2899
3061
|
case OP_EVENT.task: {
|
|
2900
3062
|
const d = e.data;
|
|
3063
|
+
if (d.workShiftBasis !== void 0 && !isWorkShiftExecutionBasis(d.workShiftBasis)) throw new Error("Invalid pinned work shift evidence");
|
|
2901
3064
|
if (this.stale(`task:${d.taskId}`, e)) return;
|
|
2902
3065
|
this.touchLocation(d.fromNode);
|
|
2903
3066
|
this.touchLocation(d.toNode);
|
|
@@ -2922,6 +3085,7 @@ var ObservedReducer = class {
|
|
|
2922
3085
|
...d.resources?.length ? { resources: d.resources.slice() } : {},
|
|
2923
3086
|
/* 실제 자재 이동 — 인원·설비와 같은 채널로 온다(실적을 한 곳에서 읽는다).
|
|
2924
3087
|
말하지 않았으면 **앞의 것을 지킨다**(위 주석) — 빈 배열은 「지웠다」로 말한 것이다. */
|
|
3088
|
+
...d.workShiftBasis ? { workShiftBasis: structuredClone(d.workShiftBasis) } : prevTask?.workShiftBasis ? { workShiftBasis: structuredClone(prevTask.workShiftBasis) } : {},
|
|
2925
3089
|
...d.materialActual?.length ? { materialActual: d.materialActual.map((r) => ({ ...r })) } : keptActual?.length ? { materialActual: keptActual.map((r) => ({ ...r })) } : {},
|
|
2926
3090
|
...d.priority !== void 0 ? { priority: d.priority } : {},
|
|
2927
3091
|
...d.startTime ? { startTime: d.startTime } : {},
|
|
@@ -3003,6 +3167,7 @@ var ObservedReducer = class {
|
|
|
3003
3167
|
}
|
|
3004
3168
|
case OP_EVENT.equipmentPeriod: {
|
|
3005
3169
|
const d = e.data;
|
|
3170
|
+
if (d.workShiftBasis !== void 0 && !isWorkShiftExecutionBasis(d.workShiftBasis)) throw new Error("Invalid pinned work shift evidence");
|
|
3006
3171
|
if (!d?.moverId || !d?.from || !d?.to) break;
|
|
3007
3172
|
const from = Date.parse(d.from);
|
|
3008
3173
|
const to = Date.parse(d.to);
|
|
@@ -3112,6 +3277,7 @@ var ObservedReducer = class {
|
|
|
3112
3277
|
/* 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(같은 품목에 대체 레시피). */
|
|
3113
3278
|
...d.recipeKey ? { recipeKey: d.recipeKey } : {},
|
|
3114
3279
|
...d.recipeBasis ? { recipeBasis: structuredClone(d.recipeBasis) } : {},
|
|
3280
|
+
...d.materialBasis ? { materialBasis: structuredClone(d.materialBasis) } : {},
|
|
3115
3281
|
...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
|
|
3116
3282
|
...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
|
|
3117
3283
|
/* 이행하는 계획 — 「이 계획이 얼마나 됐나」를 이 값으로 묶어 답한다(§`OrderState.operationsRequestId`). */
|
|
@@ -3536,7 +3702,7 @@ var ObservedReducer = class {
|
|
|
3536
3702
|
/* 수량으로 담긴 것도 이어받는다 — 되풀어 주지 않는 축이다(원천은 담김을 다시 말해 주지 않는다). */
|
|
3537
3703
|
qtyAggregation: [...this.qtyAggregation.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
3538
3704
|
pendingQuantities: [...this.pendingQuantities.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
3539
|
-
tasks: [...this.tasks.values()].map((t) => (
|
|
3705
|
+
tasks: [...this.tasks.values()].map((t) => structuredClone(t)),
|
|
3540
3706
|
equipment: [...this.equipment.values()].map((m) => ({ ...m })),
|
|
3541
3707
|
persons: [...this.persons.values()].map((x) => ({ ...x })),
|
|
3542
3708
|
assets: [...this.assets.values()].map((x) => ({ ...x })),
|
|
@@ -3564,6 +3730,9 @@ var ObservedReducer = class {
|
|
|
3564
3730
|
* 모델에서 오는 판정 입력(교대·등급·시간대)은 생성자가 이미 세웠으므로 건드리지 않는다.
|
|
3565
3731
|
*/
|
|
3566
3732
|
restore(cp) {
|
|
3733
|
+
for (const task of cp?.tasks ?? []) {
|
|
3734
|
+
if (task.workShiftBasis !== void 0 && !isWorkShiftExecutionBasis(task.workShiftBasis)) throw new Error("Invalid pinned work shift evidence");
|
|
3735
|
+
}
|
|
3567
3736
|
this.observations = new Map(
|
|
3568
3737
|
(cp?.observations ?? []).map((o) => [o.id, new Map((o.values ?? []).map((v) => [v.propertyId, { ...v }]))])
|
|
3569
3738
|
);
|
|
@@ -3576,7 +3745,7 @@ var ObservedReducer = class {
|
|
|
3576
3745
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map((x) => [x.child, x.parent]));
|
|
3577
3746
|
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map((x) => [x.parent, x.quantities]));
|
|
3578
3747
|
this.pendingQuantities = new Map((cp?.pendingQuantities ?? []).map((x) => [x.parent, x.quantities]));
|
|
3579
|
-
this.tasks = new Map((cp?.tasks ?? []).map((t) => [t.id,
|
|
3748
|
+
this.tasks = new Map((cp?.tasks ?? []).map((t) => [t.id, structuredClone(t)]));
|
|
3580
3749
|
this.equipment = new Map((cp?.equipment ?? []).map((m) => [m.id, { ...m }]));
|
|
3581
3750
|
this.oeeCounters = new Map(
|
|
3582
3751
|
(cp?.oeeCounters ?? []).map((c) => [String(c.id), { ...emptyOeeCounters(), ...c, id: void 0 }])
|
|
@@ -3630,7 +3799,7 @@ var ObservedReducer = class {
|
|
|
3630
3799
|
만료는 이벤트 없이 시각만으로 일어나므로, 델타로 받아 두면 유휴 자원이 영원히 유효하게 남는다. */
|
|
3631
3800
|
persons: [...this.persons.values()].map((p) => ({ ...p, ...this.effectivityPart(p), ...this.offShiftPart(`person:${p.id}`), ...this.capabilityPart(p, `person:${p.id}`, p.personnelClassIds, this.classDefs.personnel) })),
|
|
3632
3801
|
assets: [...this.assets.values()].map((a) => ({ ...a, ...this.effectivityPart(a), ...this.capabilityPart(a, `asset:${a.id}`, a.assetClassIds, this.classDefs.asset) })),
|
|
3633
|
-
tasks: [...this.tasks.values()].map((t) => (
|
|
3802
|
+
tasks: [...this.tasks.values()].map((t) => structuredClone(t)),
|
|
3634
3803
|
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) })),
|
|
3635
3804
|
orders: [...this.orders.values()].map((o) => ({ ...o })),
|
|
3636
3805
|
acked: [...this.acked]
|
|
@@ -3970,7 +4139,21 @@ var ItemStore = class _ItemStore {
|
|
|
3970
4139
|
* 색인을 밖에 따로 두지 않는다 — 갱신하는 자리가 흩어지면 한 자리라도 빠뜨렸을 때 **재고가 조용히
|
|
3971
4140
|
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
3972
4141
|
*/
|
|
4142
|
+
/*
|
|
4143
|
+
* 품목 색인 — 키는 **품목 클래스**다(`itemClassOf`). 재고는 로트 클래스를 들고 오더는 품목 클래스를 묻는다
|
|
4144
|
+
* (ADR-0076 ⑥ · ADR-0082 결정 2). 로트 글자를 키로 쓰면 품목을 물어도 로트 재고가 안 나온다.
|
|
4145
|
+
*/
|
|
3973
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
|
+
}
|
|
3974
4157
|
get size() {
|
|
3975
4158
|
return this.map.size;
|
|
3976
4159
|
}
|
|
@@ -4060,7 +4243,7 @@ var ItemStore = class _ItemStore {
|
|
|
4060
4243
|
}
|
|
4061
4244
|
/** 그 품목인 물품들 — 색인이 답한다(자리를 모를 때 쓴다). */
|
|
4062
4245
|
ofGtin(gtin) {
|
|
4063
|
-
const keys = this.byGtin.get(gtin);
|
|
4246
|
+
const keys = this.byGtin.get(this.itemClass(gtin));
|
|
4064
4247
|
if (!keys) return [];
|
|
4065
4248
|
const out = [];
|
|
4066
4249
|
for (const k of keys) {
|
|
@@ -4088,13 +4271,13 @@ var ItemStore = class _ItemStore {
|
|
|
4088
4271
|
}
|
|
4089
4272
|
}
|
|
4090
4273
|
for (const [key, it] of this.map) {
|
|
4091
|
-
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`);
|
|
4092
4275
|
}
|
|
4093
4276
|
for (const [g, keys] of this.byGtin) {
|
|
4094
4277
|
for (const k of keys) {
|
|
4095
4278
|
const it = this.map.get(k);
|
|
4096
4279
|
if (!it) drift.push(`${k} \uC774 \uC9C0\uC6CC\uC84C\uB294\uB370 \uD488\uBAA9 '${g}' \uC0C9\uC778\uC5D0 \uB0A8\uC544 \uC788\uB2E4`);
|
|
4097
|
-
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`);
|
|
4098
4281
|
}
|
|
4099
4282
|
}
|
|
4100
4283
|
return drift;
|
|
@@ -4102,9 +4285,10 @@ var ItemStore = class _ItemStore {
|
|
|
4102
4285
|
index(key, location, gtin) {
|
|
4103
4286
|
this.indexLocation(key, location);
|
|
4104
4287
|
if (gtin) {
|
|
4105
|
-
const
|
|
4288
|
+
const g = this.itemClass(gtin);
|
|
4289
|
+
const set = this.byGtin.get(g) ?? /* @__PURE__ */ new Set();
|
|
4106
4290
|
set.add(key);
|
|
4107
|
-
this.byGtin.set(
|
|
4291
|
+
this.byGtin.set(g, set);
|
|
4108
4292
|
}
|
|
4109
4293
|
}
|
|
4110
4294
|
indexLocation(key, location) {
|
|
@@ -4115,10 +4299,11 @@ var ItemStore = class _ItemStore {
|
|
|
4115
4299
|
unindex(key, location, gtin) {
|
|
4116
4300
|
this.unindexLocation(key, location);
|
|
4117
4301
|
if (gtin) {
|
|
4118
|
-
const
|
|
4302
|
+
const g = this.itemClass(gtin);
|
|
4303
|
+
const set = this.byGtin.get(g);
|
|
4119
4304
|
if (!set) return;
|
|
4120
4305
|
set.delete(key);
|
|
4121
|
-
if (!set.size) this.byGtin.delete(
|
|
4306
|
+
if (!set.size) this.byGtin.delete(g);
|
|
4122
4307
|
}
|
|
4123
4308
|
}
|
|
4124
4309
|
unindexLocation(key, location) {
|
|
@@ -4527,6 +4712,9 @@ var FlowEngine = class {
|
|
|
4527
4712
|
return { kept, dropped };
|
|
4528
4713
|
}
|
|
4529
4714
|
hydrateObserved(snap, orders = [], purpose = "seed") {
|
|
4715
|
+
for (const task of snap.tasks ?? []) {
|
|
4716
|
+
if (task.workShiftBasis !== void 0 && !isWorkShiftExecutionBasis(task.workShiftBasis)) throw new Error("Invalid pinned work shift evidence");
|
|
4717
|
+
}
|
|
4530
4718
|
for (const id of snap.acked ?? []) this._acked.add(id);
|
|
4531
4719
|
for (const e of snap.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
|
|
4532
4720
|
for (const n of snap.locations) {
|
|
@@ -4688,6 +4876,7 @@ var FlowEngine = class {
|
|
|
4688
4876
|
...o.gtin ? { gtin: o.gtin } : {},
|
|
4689
4877
|
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
4690
4878
|
...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
|
|
4879
|
+
...o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {},
|
|
4691
4880
|
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
4692
4881
|
...o.dockDoor ? { dockDoor: o.dockDoor } : {},
|
|
4693
4882
|
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
|
|
@@ -4713,6 +4902,7 @@ var FlowEngine = class {
|
|
|
4713
4902
|
남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
|
|
4714
4903
|
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
4715
4904
|
...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
|
|
4905
|
+
...o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {},
|
|
4716
4906
|
bizTransaction: o.bizTransaction ?? "",
|
|
4717
4907
|
allocated: resolved.kept,
|
|
4718
4908
|
picked: [],
|
|
@@ -4766,6 +4956,7 @@ var FlowEngine = class {
|
|
|
4766
4956
|
status: purpose === "observe" ? t.status : known && t.status === "in-progress" ? "in-progress" : "created",
|
|
4767
4957
|
/* 이미 일어난 자재 이동은 **씨앗에도 남는다** — 잃으면 실적이 재기동마다 지워진다. */
|
|
4768
4958
|
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
4959
|
+
...t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {},
|
|
4769
4960
|
itemEpc: t.itemRefs?.[0] ?? "",
|
|
4770
4961
|
fromNode: t.fromNode ?? "",
|
|
4771
4962
|
toNode: t.toNode ?? "",
|
|
@@ -5123,6 +5314,7 @@ var FlowEngine = class {
|
|
|
5123
5314
|
...t.assets?.length ? { assets: t.assets.slice() } : {},
|
|
5124
5315
|
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
5125
5316
|
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
5317
|
+
...t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {},
|
|
5126
5318
|
...t.priority !== void 0 ? { priority: t.priority } : {},
|
|
5127
5319
|
...t.startTime ? { startTime: t.startTime } : {},
|
|
5128
5320
|
...t.endTime ? { endTime: t.endTime } : {}
|
|
@@ -5147,6 +5339,7 @@ var FlowEngine = class {
|
|
|
5147
5339
|
/* **어느 레시피로 만드나** — 품목만으로는 「무엇으로」가 안 남는다(같은 품목의 대체 레시피가 있다). */
|
|
5148
5340
|
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
5149
5341
|
...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
|
|
5342
|
+
...o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {},
|
|
5150
5343
|
/* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
|
|
5151
5344
|
...o.seedIncomplete ? { seedIncomplete: true } : {},
|
|
5152
5345
|
/* 왜 대기하는지 — 없으면 화면은 「running」만 보여 주고 사람은 원인을 찾을 자리가 없다. */
|
|
@@ -6141,6 +6334,7 @@ var FlowEngine = class {
|
|
|
6141
6334
|
...t.resources?.length ? { resources: t.resources.slice() } : {},
|
|
6142
6335
|
/* 실제 자재 이동 — 인원·설비와 같은 채널(실적을 한 곳에서 읽는다). */
|
|
6143
6336
|
...t.materialActual?.length ? { materialActual: t.materialActual.map((r) => ({ ...r })) } : {},
|
|
6337
|
+
...t.workShiftBasis ? { workShiftBasis: structuredClone(t.workShiftBasis) } : {},
|
|
6144
6338
|
...t.durationMs ? { durationMs: t.durationMs } : {},
|
|
6145
6339
|
/* 품질 판정이 있었으면 **그 자리에서** 실어 보낸다 — 없으면 필드를 만들지 않는다(모름≠양품). */
|
|
6146
6340
|
...t.outcome ? { outcome: t.outcome } : {},
|
|
@@ -6182,6 +6376,7 @@ var FlowEngine = class {
|
|
|
6182
6376
|
레시피가 있다). 시뮬만 아는 사실이 남으면 두 스냅샷을 같은 규칙으로 읽을 수 없다. */
|
|
6183
6377
|
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
6184
6378
|
...o.recipeBasis ? { recipeBasis: structuredClone(o.recipeBasis) } : {},
|
|
6379
|
+
...o.materialBasis ? { materialBasis: structuredClone(o.materialBasis) } : {},
|
|
6185
6380
|
...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
|
|
6186
6381
|
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
6187
6382
|
/* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
|
|
@@ -6356,7 +6551,7 @@ var FlowEngine = class {
|
|
|
6356
6551
|
if (!remaining) continue;
|
|
6357
6552
|
for (const it of this.items.at(at)) {
|
|
6358
6553
|
if (remaining <= 0) break;
|
|
6359
|
-
if (!this.materialMatches(it, req)) continue;
|
|
6554
|
+
if (!this.materialMatches(it, req, t.orderId)) continue;
|
|
6360
6555
|
if (it.disposition === DISP.in_progress) continue;
|
|
6361
6556
|
const already = takenSoFar.get(it.epc) ?? 0;
|
|
6362
6557
|
const avail = Math.max(0, (it.qty ?? 1) - already);
|
|
@@ -6371,11 +6566,12 @@ var FlowEngine = class {
|
|
|
6371
6566
|
return picked;
|
|
6372
6567
|
}
|
|
6373
6568
|
/** 이 물품이 명세를 만족하나 — 품목 지목이 우선, 없으면 등급(상속을 타고 닫는다). */
|
|
6374
|
-
materialMatches(it, req) {
|
|
6569
|
+
materialMatches(it, req, orderId) {
|
|
6375
6570
|
const defId = it.definitionId ?? it.gtin;
|
|
6376
6571
|
if (req.materialDefinition) return defId === req.materialDefinition;
|
|
6377
6572
|
if (req.materialClass) {
|
|
6378
|
-
const
|
|
6573
|
+
const basis = orderId ? this.orders.get(orderId)?.materialBasis : void 0;
|
|
6574
|
+
const def = basis !== void 0 ? basis.find((b) => b.material.id === defId)?.material : defId ? this.materialDefs.get(defId) : void 0;
|
|
6379
6575
|
return classClosure(def?.materialClassIds, this.classDefs.material, this.now()).has(req.materialClass);
|
|
6380
6576
|
}
|
|
6381
6577
|
return true;
|
|
@@ -7202,7 +7398,7 @@ function planMakeToOrder(gtin, shortQty, ops, stock, locations) {
|
|
|
7202
7398
|
return { steps: [{ step: "process", operation: op.key, at: station.id }] };
|
|
7203
7399
|
}
|
|
7204
7400
|
function matches(s, req) {
|
|
7205
|
-
if (req.materialDefinition) return s.gtin
|
|
7401
|
+
if (req.materialDefinition) return sameItemClass(s.gtin, req.materialDefinition);
|
|
7206
7402
|
return false;
|
|
7207
7403
|
}
|
|
7208
7404
|
|
|
@@ -7249,11 +7445,27 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7249
7445
|
const poSeq = ++this.poSeq;
|
|
7250
7446
|
const po = this.requireBizTransactionId(`PO-${poSeq}`, "purchaseorder");
|
|
7251
7447
|
const eventTime = this.now();
|
|
7252
|
-
const
|
|
7448
|
+
const lot = `L-${poSeq}`;
|
|
7449
|
+
const lotClass = lotClassOfItem(gtin, lot) ?? gtin;
|
|
7450
|
+
const qtyList = [{ epcClass: lotClass, quantity: qty }];
|
|
7253
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);
|
|
7254
7466
|
const expiry = this.clockMs + SHELF_MS - this.epcSeq % 5 * SHELF_JITTER_MS;
|
|
7255
7467
|
const ilmd = { [ILMD_ATTR.expiry]: expiry };
|
|
7256
|
-
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() });
|
|
7257
7469
|
dock.occupancy++;
|
|
7258
7470
|
this.emit(transactionEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, bizTransactionList: poTxn, epcList: [epc], quantityList: qtyList, readPoint: dock.id }));
|
|
7259
7471
|
this.emit(aggregationEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, parentID: epc, childQuantityList: qtyList, readPoint: dock.id }));
|
|
@@ -7269,6 +7481,9 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7269
7481
|
bizTransactionList: poTxn,
|
|
7270
7482
|
ilmd
|
|
7271
7483
|
}));
|
|
7484
|
+
inbound.fulfilled = qty;
|
|
7485
|
+
inbound.status = "completed";
|
|
7486
|
+
this.emitOrder(inbound);
|
|
7272
7487
|
const binId = this.policy.selectPlacement({ item: { epc, gtin, qty }, slots: this.slotViews("storage") });
|
|
7273
7488
|
if (!binId) return;
|
|
7274
7489
|
const id = `task-${++this.taskSeq}`;
|
|
@@ -7347,10 +7562,10 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7347
7562
|
const staging = this.builtInLocation("staging", "picked pallets wait here before shipping");
|
|
7348
7563
|
const chosenAll = [];
|
|
7349
7564
|
for (const line of o.lines) {
|
|
7350
|
-
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)));
|
|
7351
7566
|
const need = line.requested - already;
|
|
7352
7567
|
if (need <= 0) continue;
|
|
7353
|
-
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 } : {} }));
|
|
7354
7569
|
const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
|
|
7355
7570
|
for (const row of chosen) {
|
|
7356
7571
|
o.allocated.push(row);
|
|
@@ -7414,7 +7629,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7414
7629
|
const locations = [...this.locations.values()].map((l) => ({ id: l.id, type: l.type }));
|
|
7415
7630
|
const ops = this.declaredOperations();
|
|
7416
7631
|
for (const line of o.lines) {
|
|
7417
|
-
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);
|
|
7418
7633
|
const short = line.requested - have;
|
|
7419
7634
|
if (short <= 0) continue;
|
|
7420
7635
|
const plan = planMakeToOrder(line.gtin, short, ops, stock, locations);
|
|
@@ -7440,7 +7655,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7440
7655
|
}
|
|
7441
7656
|
/** 부품을 작업대로 — 팔레트 이동이므로 피킹과 같은 기제다(부분 소비는 코어가 한다). */
|
|
7442
7657
|
issueFeed(o, gtin, from, to, qty) {
|
|
7443
|
-
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);
|
|
7444
7659
|
if (!src) return;
|
|
7445
7660
|
src.disposition = DISP.reserved;
|
|
7446
7661
|
this.pushTask(o, "feed", src.epc, from, to);
|
|
@@ -7518,7 +7733,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
7518
7733
|
order.fulfilled += allocatedQty(order.allocated);
|
|
7519
7734
|
if (order.lines) for (const epc of order.picked) {
|
|
7520
7735
|
const g = this.items.get(epc)?.gtin;
|
|
7521
|
-
const line = order.lines.find((l) => l.gtin
|
|
7736
|
+
const line = order.lines.find((l) => sameItemClass(l.gtin, g) && l.requested > 0);
|
|
7522
7737
|
if (line) line.requested--;
|
|
7523
7738
|
}
|
|
7524
7739
|
for (const epc of order.picked) {
|
|
@@ -8359,7 +8574,7 @@ The steps are the source of truth for execution order; the hierarchy only names
|
|
|
8359
8574
|
if (fromType) {
|
|
8360
8575
|
for (const n of locsOfType.get(fromType) ?? []) {
|
|
8361
8576
|
for (const i of this.items.at(n.id)) {
|
|
8362
|
-
if (i.gtin
|
|
8577
|
+
if (sameItemClass(i.gtin, g) && i.disposition === DISP.sellable) available.push({ epc: i.epc, location: i.location, qty: 1 });
|
|
8363
8578
|
}
|
|
8364
8579
|
}
|
|
8365
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
|
}
|