@operato/twin-kernel 0.7.47 → 0.7.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract.d.ts +8 -0
- package/dist/domain-definition.d.ts +43 -2
- package/dist/domain-definition.js +10 -2
- package/dist/epcis.d.ts +7 -1
- package/dist/epcis.js +64 -0
- package/dist/flow-engine.d.ts +76 -2
- package/dist/flow-engine.js +217 -19
- package/dist/mes-kernel.d.ts +55 -0
- package/dist/mes-kernel.js +191 -10
- package/dist-cjs/index.cjs +349 -26
- package/package.json +1 -1
package/dist/flow-engine.js
CHANGED
|
@@ -301,6 +301,22 @@ function energyFieldsOf(m) {
|
|
|
301
301
|
export class ItemStore {
|
|
302
302
|
map = new Map();
|
|
303
303
|
byLocation = new Map();
|
|
304
|
+
/**
|
|
305
|
+
* 품목별 색인 — **보관처를 선언하지 않은 현장을 위해.**
|
|
306
|
+
*
|
|
307
|
+
* ── 왜 필요한가 (2026-08-22) ──────────────────────────────────────────────
|
|
308
|
+
* 자재를 확보할 때 예전에는 「그 자재의 보관처 타입」을 반드시 선언해야 했다(`MaterialDef.locationType`).
|
|
309
|
+
* 그런데 **재고로 위치를 말하는 시스템**에는 그 선언이 없다 — 자재에 고정된 보관처를 두지 않는 것이
|
|
310
|
+
* WMS 계열의 정상이다. 실측: 첫 실 연동에서 원자재 986건 중 보관처가 선언된 것이 **36건**이었고, 그
|
|
311
|
+
* 때문에 레시피 937/1,408 건이 아예 실리지 못했다.
|
|
312
|
+
*
|
|
313
|
+
* 선언이 없으면 **재고가 있는 곳에서 찾는다.** 그때 전 로케이션을 훑으면 규모 기준선(품목 100만)에서
|
|
314
|
+
* 감당되지 않으므로 품목 색인이 답한다.
|
|
315
|
+
*
|
|
316
|
+
* 색인을 밖에 따로 두지 않는다 — 갱신하는 자리가 흩어지면 한 자리라도 빠뜨렸을 때 **재고가 조용히
|
|
317
|
+
* 사라진다**(있는데 없다고 판정된다). 자리 색인과 같은 규율이다.
|
|
318
|
+
*/
|
|
319
|
+
byGtin = new Map();
|
|
304
320
|
get size() {
|
|
305
321
|
return this.map.size;
|
|
306
322
|
}
|
|
@@ -328,20 +344,21 @@ export class ItemStore {
|
|
|
328
344
|
set(key, item) {
|
|
329
345
|
const prev = this.map.get(key);
|
|
330
346
|
if (prev)
|
|
331
|
-
this.unindex(key, prev.location);
|
|
347
|
+
this.unindex(key, prev.location, prev.gtin);
|
|
332
348
|
this.map.set(key, item);
|
|
333
|
-
this.index(key, item.location);
|
|
349
|
+
this.index(key, item.location, item.gtin);
|
|
334
350
|
return this;
|
|
335
351
|
}
|
|
336
352
|
delete(key) {
|
|
337
353
|
const prev = this.map.get(key);
|
|
338
354
|
if (prev)
|
|
339
|
-
this.unindex(key, prev.location);
|
|
355
|
+
this.unindex(key, prev.location, prev.gtin);
|
|
340
356
|
return this.map.delete(key);
|
|
341
357
|
}
|
|
342
358
|
clear() {
|
|
343
359
|
this.map.clear();
|
|
344
360
|
this.byLocation.clear();
|
|
361
|
+
this.byGtin.clear();
|
|
345
362
|
}
|
|
346
363
|
/**
|
|
347
364
|
* 물품을 다른 자리로 옮긴다 — **색인이 함께 움직이는 유일한 통로.**
|
|
@@ -354,9 +371,10 @@ export class ItemStore {
|
|
|
354
371
|
if (!it)
|
|
355
372
|
throw new Error(`relocate: 물품 '${key}' 이 상태에 없습니다 — 없는 것을 옮길 수 없습니다`);
|
|
356
373
|
if (it.location !== to) {
|
|
357
|
-
|
|
374
|
+
/* 자리 색인만 움직인다 — 옮겨도 품목은 그대로다. */
|
|
375
|
+
this.unindexLocation(key, it.location);
|
|
358
376
|
it.location = to;
|
|
359
|
-
this.
|
|
377
|
+
this.indexLocation(key, to);
|
|
360
378
|
}
|
|
361
379
|
return it;
|
|
362
380
|
}
|
|
@@ -391,6 +409,19 @@ export class ItemStore {
|
|
|
391
409
|
}
|
|
392
410
|
return out;
|
|
393
411
|
}
|
|
412
|
+
/** 그 품목인 물품들 — 색인이 답한다(자리를 모를 때 쓴다). */
|
|
413
|
+
ofGtin(gtin) {
|
|
414
|
+
const keys = this.byGtin.get(gtin);
|
|
415
|
+
if (!keys)
|
|
416
|
+
return [];
|
|
417
|
+
const out = [];
|
|
418
|
+
for (const k of keys) {
|
|
419
|
+
const it = this.map.get(k);
|
|
420
|
+
if (it)
|
|
421
|
+
out.push(it);
|
|
422
|
+
}
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
394
425
|
/**
|
|
395
426
|
* 색인이 맵과 어긋난 자리 — **시험이 쓰는 확인 통로**(전체를 다시 세므로 비싸다).
|
|
396
427
|
*
|
|
@@ -412,14 +443,47 @@ export class ItemStore {
|
|
|
412
443
|
drift.push(`${k} 은 '${it.location}' 에 있는데 '${loc}' 색인에 있다`);
|
|
413
444
|
}
|
|
414
445
|
}
|
|
446
|
+
/* 품목 색인도 같은 규율로 본다 — 어긋나면 자재가 있는데 없다고 판정된다. */
|
|
447
|
+
for (const [key, it] of this.map) {
|
|
448
|
+
if (it.gtin && !this.byGtin.get(it.gtin)?.has(key))
|
|
449
|
+
drift.push(`${key} 이 품목 '${it.gtin}' 색인에 없다`);
|
|
450
|
+
}
|
|
451
|
+
for (const [g, keys] of this.byGtin) {
|
|
452
|
+
for (const k of keys) {
|
|
453
|
+
const it = this.map.get(k);
|
|
454
|
+
if (!it)
|
|
455
|
+
drift.push(`${k} 이 지워졌는데 품목 '${g}' 색인에 남아 있다`);
|
|
456
|
+
else if (it.gtin !== g)
|
|
457
|
+
drift.push(`${k} 은 품목 '${it.gtin}' 인데 '${g}' 색인에 있다`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
415
460
|
return drift;
|
|
416
461
|
}
|
|
417
|
-
index(key, location) {
|
|
462
|
+
index(key, location, gtin) {
|
|
463
|
+
this.indexLocation(key, location);
|
|
464
|
+
if (gtin) {
|
|
465
|
+
const set = this.byGtin.get(gtin) ?? new Set();
|
|
466
|
+
set.add(key);
|
|
467
|
+
this.byGtin.set(gtin, set);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
indexLocation(key, location) {
|
|
418
471
|
const set = this.byLocation.get(location) ?? new Set();
|
|
419
472
|
set.add(key);
|
|
420
473
|
this.byLocation.set(location, set);
|
|
421
474
|
}
|
|
422
|
-
unindex(key, location) {
|
|
475
|
+
unindex(key, location, gtin) {
|
|
476
|
+
this.unindexLocation(key, location);
|
|
477
|
+
if (gtin) {
|
|
478
|
+
const set = this.byGtin.get(gtin);
|
|
479
|
+
if (!set)
|
|
480
|
+
return;
|
|
481
|
+
set.delete(key);
|
|
482
|
+
if (!set.size)
|
|
483
|
+
this.byGtin.delete(gtin);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
unindexLocation(key, location) {
|
|
423
487
|
const set = this.byLocation.get(location);
|
|
424
488
|
if (!set)
|
|
425
489
|
return;
|
|
@@ -510,6 +574,14 @@ export class FlowEngine {
|
|
|
510
574
|
*/
|
|
511
575
|
seedDanglingRefs = 0;
|
|
512
576
|
transformInputsAbsent = 0;
|
|
577
|
+
/**
|
|
578
|
+
* 일반 요구(공정)와 구체 요구(레시피 × 공정)가 **등급 ↔ 품목으로 교차**한 횟수.
|
|
579
|
+
*
|
|
580
|
+
* 같은 키끼리는 구체가 상회한다(§`mergeMaterialNeeds`). 교차는 뜻으로는 상회일 수 있으나 판정에 등급
|
|
581
|
+
* 소속이 필요하고, 잘못 겹치면 자재가 조용히 사라지거나 두 배가 된다. 그래서 **둘 다 요구하고 센다** —
|
|
582
|
+
* 이 값이 크면 그 숫자가 다음 작업을 정한다.
|
|
583
|
+
*/
|
|
584
|
+
materialSpecCrossKeyOverlaps = 0;
|
|
513
585
|
observedDirty = false;
|
|
514
586
|
observeMode = false;
|
|
515
587
|
/** 관측 구동이 투영기를 세울 때 필요한 원본 보드(구조는 이벤트가 아니라 마스터에서 온다). */
|
|
@@ -1231,10 +1303,11 @@ export class FlowEngine {
|
|
|
1231
1303
|
nowTime: this.now(), // 트윈의 "지금" — 관측 모드면 마지막으로 들은 시각(§nowMs)
|
|
1232
1304
|
identityGrounding: this.identityGroundingView(),
|
|
1233
1305
|
/* 원본과 어긋난 사실 — 0 이면 싣지 않는다(어긋난 적 없는 트윈에 빈 칸을 만들지 않는다). */
|
|
1234
|
-
...(this.transformInputsAbsent || this.seedDanglingRefs
|
|
1306
|
+
...(this.transformInputsAbsent || this.seedDanglingRefs || this.materialSpecCrossKeyOverlaps
|
|
1235
1307
|
? { conformance: {
|
|
1236
1308
|
...(this.transformInputsAbsent ? { transformInputsAbsent: this.transformInputsAbsent } : {}),
|
|
1237
|
-
...(this.seedDanglingRefs ? { seedDanglingRefs: this.seedDanglingRefs } : {})
|
|
1309
|
+
...(this.seedDanglingRefs ? { seedDanglingRefs: this.seedDanglingRefs } : {}),
|
|
1310
|
+
...(this.materialSpecCrossKeyOverlaps ? { materialSpecCrossKeyOverlaps: this.materialSpecCrossKeyOverlaps } : {})
|
|
1238
1311
|
} }
|
|
1239
1312
|
: {}),
|
|
1240
1313
|
/* 출처 표시 — 보드(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
@@ -2496,7 +2569,14 @@ export class FlowEngine {
|
|
|
2496
2569
|
* 작업이 같은 부품을 또 잡는다). 산출(`produced`)은 여기서 다루지 않는다(완료 시점의 일이다).
|
|
2497
2570
|
*/
|
|
2498
2571
|
claimMaterials(t) {
|
|
2499
|
-
|
|
2572
|
+
/*
|
|
2573
|
+
* 두 원천을 합쳐 본다 — 뜻이 다르고 자리도 다르다(§`OperationDef.materialSpecification`).
|
|
2574
|
+
* ① 공정 명세 — 품목과 무관하게 그 자리가 늘 쓰는 것(포장 필름·세척수). 트윈 전체에 한 벌이다.
|
|
2575
|
+
* ② 그 작업이 만드는 **품목의** 그 공정 몫 — 도메인이 답한다(§`recipeInputsAt`).
|
|
2576
|
+
* 오더가 없는 작업(창고 입고 등)에는 ②가 없다 — 그때는 ①만 적용된다.
|
|
2577
|
+
*/
|
|
2578
|
+
const general = (this.operationSpecs.get(t.kind)?.materialSpecification ?? []).filter(m => m.use === 'consumed');
|
|
2579
|
+
const need = this.mergeMaterialNeeds(general, this.recipeInputsAt(t), t.kind);
|
|
2500
2580
|
if (!need.length)
|
|
2501
2581
|
return [];
|
|
2502
2582
|
const at = t.toNode;
|
|
@@ -2512,6 +2592,13 @@ export class FlowEngine {
|
|
|
2512
2592
|
break;
|
|
2513
2593
|
if (!this.materialMatches(it, req))
|
|
2514
2594
|
continue;
|
|
2595
|
+
/*
|
|
2596
|
+
* 변환 중인 것은 잡지 않는다 — 어떤 공정이 이미 먹어 그 단계의 산출로 바뀔 물품이다
|
|
2597
|
+
* (§`adoptConsumed`). **예약은 건너뛰지 않는다**: 오더가 예약한 부품을 그 공정이 먹는 흐름이
|
|
2598
|
+
* 정상이다(유통가공 키팅이 그렇게 돈다 — 예약을 막으면 세트가 만들어지지 않는다).
|
|
2599
|
+
*/
|
|
2600
|
+
if (it.disposition === DISP.in_progress)
|
|
2601
|
+
continue;
|
|
2515
2602
|
const already = takenSoFar.get(it.epc) ?? 0;
|
|
2516
2603
|
const avail = Math.max(0, (it.qty ?? 1) - already);
|
|
2517
2604
|
if (avail <= 0)
|
|
@@ -2616,7 +2703,7 @@ export class FlowEngine {
|
|
|
2616
2703
|
const uri = this.declaredObjectId(id);
|
|
2617
2704
|
if (uri)
|
|
2618
2705
|
return uri;
|
|
2619
|
-
throw new Error(this.identityMissing('object'));
|
|
2706
|
+
throw new Error(this.identityMissing('an object'));
|
|
2620
2707
|
}
|
|
2621
2708
|
/** 거래 문서 식별자 — 선언된 이름공간 또는 선언된 GDTI 문서 타입. 둘 다 없으면 오류를 낸다. */
|
|
2622
2709
|
requireBizTransactionId(id, docKind) {
|
|
@@ -2628,11 +2715,17 @@ export class FlowEngine {
|
|
|
2628
2715
|
const prefix = decl?.companyPrefix;
|
|
2629
2716
|
if (docType && prefix)
|
|
2630
2717
|
return gdtiUri(prefix, docType, Number(id) || 0);
|
|
2631
|
-
throw new Error(this.identityMissing(`business transaction '${docKind}'`));
|
|
2718
|
+
throw new Error(this.identityMissing(`the business transaction '${docKind}'`));
|
|
2632
2719
|
}
|
|
2633
|
-
/**
|
|
2720
|
+
/**
|
|
2721
|
+
* 같은 문장을 두 곳에 적지 않는다 — 고치는 자리가 하나여야 한다.
|
|
2722
|
+
*
|
|
2723
|
+
* `what` 은 **관사까지 갖춘 구**를 받는다(`'an object'`). 예전에는 여기서 `a ${what}` 로 관사를
|
|
2724
|
+
* 붙였고, 화면에 「cannot name a object」·「cannot name a business transaction 'purchase'」가 그대로
|
|
2725
|
+
* 나왔다. 사람이 읽는 문장이므로 부르는 자리가 관사를 정한다.
|
|
2726
|
+
*/
|
|
2634
2727
|
identityMissing(what) {
|
|
2635
|
-
return (`this twin has no identity declaration, so the kernel cannot name
|
|
2728
|
+
return (`this twin has no identity declaration, so the kernel cannot name ${what}. ` +
|
|
2636
2729
|
'Declare `identity.namespaces` on the model (CBV 2.0 §8.2.4 `.../obj/<id>` · §8.5.5 `.../bt/<id>` — ' +
|
|
2637
2730
|
'assigned by the owner of that internet domain), or `identity.documentTypes` with `identity.companyPrefix` ' +
|
|
2638
2731
|
'for GS1 keys. The kernel does not invent a company prefix: GS1 assigns it to a company, and a value the ' +
|
|
@@ -2813,12 +2906,117 @@ export class FlowEngine {
|
|
|
2813
2906
|
this.recordMaterialActual(t, it.definitionId ?? it.gtin, 'consumed', take, it.uom);
|
|
2814
2907
|
whole.push(epc);
|
|
2815
2908
|
}
|
|
2816
|
-
if (whole.length)
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2909
|
+
if (!whole.length)
|
|
2910
|
+
return;
|
|
2911
|
+
/*
|
|
2912
|
+
* ── 전량 소비는 **변환이 아니다** (2026-08-22) ─────────────────────────────
|
|
2913
|
+
* 예전에는 `transform(whole, [])` 였다. 그것은 출력이 빈 `TransformationEvent` 이고 **우리 검증기가
|
|
2914
|
+
* 거부한다**(「output 비어있음」). 실측: 공정 하나가 자재를 전량 먹는 경로에서 한 실행에 9건이
|
|
2915
|
+
* 무효였다. 변환은 양쪽을 요구한다(EPCIS 2.0 §7.4.5) — 만드는 것이 없으면 변환이 아니다.
|
|
2916
|
+
*
|
|
2917
|
+
* 그래서 두 갈래로 나눈다.
|
|
2918
|
+
* ① **도메인이 계보로 가져가면** 물품을 남겨 두고 예약만 한다. 그 단계가 만드는 것의 변환 입력이
|
|
2919
|
+
* 되어야 회수 범위가 온전하다 — 별도 사건으로 없애면 제품의 계보에서 그 자재가 빠진다.
|
|
2920
|
+
* ② 아무것도 만들지 않는 소비는 개체가 **사라진** 것이다 — `ObjectEvent` `DELETE`(이미 출하·출차가
|
|
2921
|
+
* 쓰는 원시다). 소비 사실은 `recordMaterialActual` 이 이미 남겼다.
|
|
2922
|
+
*/
|
|
2923
|
+
if (this.adoptConsumed(t, whole)) {
|
|
2924
|
+
/*
|
|
2925
|
+
* 다른 작업이 같은 물품을 또 잡지 못하게 한다 — 소비 시점은 시작인데 변환은 완료 시점이라 그
|
|
2926
|
+
* 사이가 열려 있다. **변환 중**으로 표시한다(상태와 사건을 한 번에 — §`observeDisposition`).
|
|
2927
|
+
*/
|
|
2928
|
+
this.observeDisposition(whole, DISP.in_progress, CBV_BIZSTEP.consuming);
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
const at = this.items.get(whole[0])?.location ?? t.toNode;
|
|
2932
|
+
/* 자리의 점유를 함께 줄인다 — 물품만 지우면 그 자리가 영원히 찬 것으로 남는다(§`transform`). */
|
|
2933
|
+
for (const epc of whole) {
|
|
2934
|
+
const it = this.items.get(epc);
|
|
2935
|
+
if (!it)
|
|
2936
|
+
continue;
|
|
2937
|
+
const n = this.locations.get(it.location);
|
|
2938
|
+
if (n)
|
|
2939
|
+
n.occupancy--;
|
|
2940
|
+
this.items.delete(epc);
|
|
2821
2941
|
}
|
|
2942
|
+
this.emit(objectEvent({
|
|
2943
|
+
eventTime: this.now(), action: 'DELETE', bizStep: CBV_BIZSTEP.consuming, disposition: DISP.in_progress,
|
|
2944
|
+
epcList: whole.slice(), readPoint: at, bizLocation: at
|
|
2945
|
+
}));
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* **소비된 자재를 도메인이 계보로 가져가는가** — 가져가면 코어는 없애지 않고 예약만 한다.
|
|
2949
|
+
*
|
|
2950
|
+
* 공정이 먹은 자재는 **그 단계가 만드는 것의 입력**이다. 그것을 별도 사건으로 없애면 제품의 계보에서
|
|
2951
|
+
* 그 자재가 빠지고, 회수 범위를 되짚을 때 조용히 좁아진다 — 식품이라면 그것이 사고다.
|
|
2952
|
+
*
|
|
2953
|
+
* 기본은 「가져가지 않는다」다: 만드는 것이 없는 소비(소모품·유통가공)는 개체가 사라진 것이 맞다.
|
|
2954
|
+
*/
|
|
2955
|
+
adoptConsumed(_t, _epcs) {
|
|
2956
|
+
return false;
|
|
2957
|
+
}
|
|
2958
|
+
/** 이 트윈이 사는 동안 한 번만 알린 상회 — 같은 말을 틱마다 반복하지 않는다. */
|
|
2959
|
+
announcedOverrides = new Set();
|
|
2960
|
+
/**
|
|
2961
|
+
* **구체가 일반을 이긴다 — 다만 상회하는 단위는 자재 한 줄이다.**
|
|
2962
|
+
*
|
|
2963
|
+
* ── 왜 합집합이 아닌가 (2026-08-22) ───────────────────────────────────────
|
|
2964
|
+
* 두 원천이 같은 자재를 말할 수 있다. 일반은 「이 공정이 늘 쓰는 것」이고(품목과 무관 — 포장 필름·
|
|
2965
|
+
* 세척수), 구체는 「이 품목을 이 공정에서 만들 때」다. 같은 자재를 둘이 말하면 **구체가 현장의 사실**
|
|
2966
|
+
* 이므로 이긴다. 합집합이면 요구가 더해져 재고가 거짓이 된다.
|
|
2967
|
+
*
|
|
2968
|
+
* ── 왜 명세 전체를 덮지 않는가 ────────────────────────────────────────────
|
|
2969
|
+
* 덮으면 반대로 틀린다. 레시피가 「무말랭이 90kg」만 말했다고 그 공정의 세척수 50L 이 사라지면, 품목과
|
|
2970
|
+
* 무관하게 늘 들어가는 것이 빠진다 — 그것이 일반 자리의 존재 이유다. 요구의 단위가 자재 한 줄이므로
|
|
2971
|
+
* 상회도 그 단위에서 일어난다.
|
|
2972
|
+
*
|
|
2973
|
+
* ── 등급 ↔ 품목이 교차하면 둘 다 요구한다 ─────────────────────────────────
|
|
2974
|
+
* 명세는 품목(`materialDefinition`)으로도 등급(`materialClass`)으로도 요구한다. 일반이 등급을, 구체가
|
|
2975
|
+
* 품목을 말하고 그 품목이 그 등급에 속하면 뜻으로는 상회지만, 그 판정에는 등급 소속이 필요하고 잘못
|
|
2976
|
+
* 겹치면 자재가 **조용히 사라지거나 두 배**가 된다. 그래서 **같은 키끼리만** 상회시키고, 교차하는
|
|
2977
|
+
* 경우는 세어 남기고 둘 다 요구한다 — 조용히 한쪽을 버리는 것이 가장 나쁘다.
|
|
2978
|
+
*/
|
|
2979
|
+
mergeMaterialNeeds(general, specific, opKey) {
|
|
2980
|
+
if (!specific.length)
|
|
2981
|
+
return general;
|
|
2982
|
+
if (!general.length)
|
|
2983
|
+
return specific;
|
|
2984
|
+
const keyOf = (m) => m.materialDefinition ? `def:${m.materialDefinition}` : m.materialClass ? `cls:${m.materialClass}` : '';
|
|
2985
|
+
const beaten = new Set(specific.map(keyOf).filter(Boolean));
|
|
2986
|
+
const out = [...specific];
|
|
2987
|
+
for (const g of general) {
|
|
2988
|
+
const k = keyOf(g);
|
|
2989
|
+
if (k && beaten.has(k)) {
|
|
2990
|
+
const note = `${opKey}|${k}`;
|
|
2991
|
+
if (!this.announcedOverrides.has(note)) {
|
|
2992
|
+
this.announcedOverrides.add(note);
|
|
2993
|
+
console.warn(`[twin] operation '${opKey}': the recipe's own requirement for ${k} overrides this operation's general ` +
|
|
2994
|
+
'requirement (the specific declaration wins). The operation\'s other lines still apply.');
|
|
2995
|
+
}
|
|
2996
|
+
continue;
|
|
2997
|
+
}
|
|
2998
|
+
/* 등급 ↔ 품목이 교차하는지 센다 — 뜻으로는 상회일 수 있으나 조용히 버리지 않는다. */
|
|
2999
|
+
const crosses = g.materialClass
|
|
3000
|
+
? specific.some(sp => !!sp.materialDefinition)
|
|
3001
|
+
: g.materialDefinition
|
|
3002
|
+
? specific.some(sp => !!sp.materialClass)
|
|
3003
|
+
: false;
|
|
3004
|
+
if (crosses)
|
|
3005
|
+
this.materialSpecCrossKeyOverlaps++;
|
|
3006
|
+
out.push(g);
|
|
3007
|
+
}
|
|
3008
|
+
return out;
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* **그 작업이 만드는 품목의, 그 공정 몫** — 품목 범위를 아는 것은 도메인이다.
|
|
3012
|
+
*
|
|
3013
|
+
* 코어는 레시피를 모른다(창고·야드에는 레시피가 없다). 그래서 시임으로 둔다 — MES 가 오더의
|
|
3014
|
+
* 레시피에서 그 공정에 태그된 투입을 돌려준다(§`RecipePart.operation`).
|
|
3015
|
+
*
|
|
3016
|
+
* 기본은 빈 목록이다: 품목 범위가 없는 트윈에서는 공정 명세만이 요구다.
|
|
3017
|
+
*/
|
|
3018
|
+
recipeInputsAt(_t) {
|
|
3019
|
+
return [];
|
|
2822
3020
|
}
|
|
2823
3021
|
/**
|
|
2824
3022
|
* 이 사람이 **속한 등급들이 요구하는 시험**을 만족하나.
|
package/dist/mes-kernel.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { GeneratorSpec, Command, CommandAck, ProductionSpec, TwinModelDef,
|
|
|
2
2
|
import type { AllocationPolicy } from './allocation-policy.ts';
|
|
3
3
|
import { FlowEngine } from './flow-engine.ts';
|
|
4
4
|
import type { FlowOrder, FlowTask } from './flow-engine.ts';
|
|
5
|
+
import { type OpMaterialSpecification } from './domain-definition.ts';
|
|
5
6
|
export declare class MesKernel extends FlowEngine {
|
|
6
7
|
private wipSeq;
|
|
7
8
|
private prodSeq;
|
|
@@ -21,6 +22,22 @@ export declare class MesKernel extends FlowEngine {
|
|
|
21
22
|
* 모든 계산이 거짓이 된다).
|
|
22
23
|
*/
|
|
23
24
|
private assertNoDoubleProduction;
|
|
25
|
+
/**
|
|
26
|
+
* **레시피 투입의 공정 태그를 기동에서 검사한다** — 어긋나면 그 자재는 영원히 확보되지 않는다.
|
|
27
|
+
*
|
|
28
|
+
* ── 두 가지를 본다 (2026-08-22) ───────────────────────────────────────────
|
|
29
|
+
* ① **태그의 공정이 그 레시피의 라우트 단계에 있어야 한다.** 없으면 그 투입은 확보되는 시점이 오지
|
|
30
|
+
* 않고, 오더는 영원히 그 단계에서 멈춘다 — 화면에는 이유가 없다. 실 마스터에서 BOM 이 말하는
|
|
31
|
+
* 공정과 품목의 경로가 어긋나는 일이 실제로 있다(BOM 은 「조림」인데 경로에 조림이 없는 경우).
|
|
32
|
+
* ② **한 레시피 안에서** 같은 자재가 태그 있는 줄과 없는 줄에 동시에 있으면 거부한다. 태그 없는 줄은
|
|
33
|
+
* 오더 착수에 확보되고 태그 붙은 줄은 그 공정에서 확보되므로, 그 자재를 **두 번 먹는다.**
|
|
34
|
+
*
|
|
35
|
+
* 레시피와 **공정 명세**가 같은 자재를 말하는 것은 거부하지 않는다 — 그것은 상회이고 정상이다
|
|
36
|
+
* (구체가 일반을 이긴다, §`mergeMaterialNeeds`).
|
|
37
|
+
*
|
|
38
|
+
* 런타임에 조용히 어긋나는 것보다 기동이 실패하는 편이 낫다.
|
|
39
|
+
*/
|
|
40
|
+
private assertRecipeOperationTags;
|
|
24
41
|
/**
|
|
25
42
|
* **레시피 모드에서는 MES 가 산출을 소유한다** — 코어는 비켜선다(§`producesOwnOutputs`).
|
|
26
43
|
*
|
|
@@ -97,8 +114,42 @@ export declare class MesKernel extends FlowEngine {
|
|
|
97
114
|
*/
|
|
98
115
|
protected canComplete(t: FlowTask): boolean;
|
|
99
116
|
protected onTaskComplete(t: FlowTask): void;
|
|
117
|
+
/**
|
|
118
|
+
* **공정이 먹은 자재는 오더의 계보에 합류한다** — 그 단계가 만드는 것의 입력이 된다.
|
|
119
|
+
*
|
|
120
|
+
* ── 무엇이 빠져 있었나 (2026-08-22) ───────────────────────────────────────
|
|
121
|
+
* 공정별 자재(`OperationDef.materialSpecification` `use:'consumed'`, ISA-95
|
|
122
|
+
* `OperationsSegment.MaterialSpecification`)는 커널이 이미 확보하고 소비했다. 그런데 그 자재가
|
|
123
|
+
* **오더가 들고 있는 것에 들어가지 않았다.** 단계의 변환 입력은 `order.allocated` 뿐이라, 뒤 공정에서
|
|
124
|
+
* 먹은 자재가 제품의 계보에서 빠졌다 — 회수 범위를 되짚으면 그 자재가 조용히 없다.
|
|
125
|
+
*
|
|
126
|
+
* 레시피가 없는 모드(유통가공)는 산출을 코어가 만들므로 가져가지 않는다(§`producesOwnOutputs`).
|
|
127
|
+
*/
|
|
128
|
+
/**
|
|
129
|
+
* **그 오더의 레시피에서, 이 공정에 태그된 투입** — 품목 범위를 아는 것은 여기다.
|
|
130
|
+
*
|
|
131
|
+
* 오더가 없는 작업에는 답하지 않는다(창고 입고 등 — 그때는 공정 명세만이 요구다).
|
|
132
|
+
*/
|
|
133
|
+
protected recipeInputsAt(t: FlowTask): OpMaterialSpecification[];
|
|
134
|
+
protected adoptConsumed(t: FlowTask, epcs: string[]): boolean;
|
|
100
135
|
/** 선언된 레시피 전부 — 오더가 자기 것을 고르고, 수령이 전부의 소요를 본다. */
|
|
101
136
|
private recipesDef;
|
|
137
|
+
/**
|
|
138
|
+
* **소비되는 자재 전부** — 선언이 그것을 말하는 자리는 둘이고, 둘 다 본다.
|
|
139
|
+
*
|
|
140
|
+
* ── 왜 둘인가 (2026-08-22) ────────────────────────────────────────────────
|
|
141
|
+
* ① `RecipeDef.inputs` — 레시피가 쓰는 자재. 오더가 시작될 때 확보한다.
|
|
142
|
+
* ② `OperationDef.materialSpecification` `use:'consumed'` — **그 공정에서만** 들어가는 자재
|
|
143
|
+
* (ISA-95 `OperationsSegment.MaterialSpecification`). 같은 부품이라도 공정마다 소요가 다르고,
|
|
144
|
+
* 표준은 「몇 개」를 공정의 사실로 둔다.
|
|
145
|
+
*
|
|
146
|
+
* 예전에는 수령이 ①만 봤다. 그래서 ②에만 선언된 자재는 **한 번도 입고되지 않았고**, 그 공정은 영원히
|
|
147
|
+
* 기다렸다 — 화면에는 이유가 없었다. 첫 실 연동의 BOM 이 (품목, 공정) 단위라 이 자리가 바로 막혔다.
|
|
148
|
+
*
|
|
149
|
+
* `binding` 을 지나지 않는 이름은 여기서 세지 않는다 — 커널이 그 자재의 정체성을 만들 수 없으므로
|
|
150
|
+
* **입고를 만들 수 없다**(밖에서 들어온 물품은 `claimMaterials` 가 클래스 문자열로 알아본다).
|
|
151
|
+
*/
|
|
152
|
+
private consumedMaterialKeys;
|
|
102
153
|
/**
|
|
103
154
|
* 이 오더의 레시피 — **오더가 들면 그것, 없으면 선언 수준의 기본**(§`FlowOrder.recipeKey`).
|
|
104
155
|
*
|
|
@@ -164,6 +215,10 @@ export declare class MesKernel extends FlowEngine {
|
|
|
164
215
|
* 레거시(선언 없는 내장 프로파일) 경로는 이 함수를 쓰지 않는다 — 그쪽에는 대조할 선언이 없으므로
|
|
165
216
|
* 커널 어휘 자체가 계약이다.
|
|
166
217
|
*/
|
|
218
|
+
/** 자리를 선언하지 않아 입고를 만들지 못한 자재 — 같은 말을 틱마다 반복하지 않는다. */
|
|
219
|
+
private arrivalsWithoutPlace;
|
|
220
|
+
/** 그 자재가 선언한 보관처 타입 — **없으면 undefined**(정책이 없다는 사실이다). */
|
|
221
|
+
private declaredLocationTypeOfMaterial;
|
|
167
222
|
private locationTypeOfMaterial;
|
|
168
223
|
/** 그 자리 타입의 자리 — 이 트윈에 없으면 말한다(선언과 모델이 어긋난 사실이다). */
|
|
169
224
|
private locationOfMaterial;
|