@operato/twin-kernel 0.7.39 → 0.7.40
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 +174 -8
- package/dist/contract.js +42 -1
- package/dist/domain-definition.d.ts +17 -0
- package/dist/domain-definition.js +7 -0
- package/dist/ems-kernel.js +12 -0
- package/dist/epcis.d.ts +22 -0
- package/dist/epcis.js +91 -2
- package/dist/face2-adapter.d.ts +21 -1
- package/dist/face2-adapter.js +12 -1
- package/dist/flow-engine.d.ts +156 -8
- package/dist/flow-engine.js +324 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/kernel.d.ts +15 -0
- package/dist/kernel.js +50 -16
- package/dist/mes-kernel.d.ts +117 -27
- package/dist/mes-kernel.js +331 -168
- package/dist/observed-reducer.js +7 -4
- package/dist/yms-kernel.js +12 -5
- package/dist-cjs/index.cjs +632 -182
- package/package.json +1 -1
package/dist-cjs/index.cjs
CHANGED
|
@@ -46,13 +46,11 @@ __export(index_exports, {
|
|
|
46
46
|
FlowEngine: () => FlowEngine,
|
|
47
47
|
GUARD_PRAGMA: () => GUARD_PRAGMA,
|
|
48
48
|
ILMD_ATTR: () => ILMD_ATTR,
|
|
49
|
+
ItemStore: () => ItemStore,
|
|
49
50
|
LOCATION_SATURATION_NEAR: () => LOCATION_SATURATION_NEAR,
|
|
50
51
|
MATERIAL_PROPERTY: () => MATERIAL_PROPERTY,
|
|
51
52
|
MES_BIZSTEP: () => MES_BIZSTEP,
|
|
52
53
|
MES_LOCATION_TYPES: () => MES_LOCATION_TYPES,
|
|
53
|
-
MES_PART_GTINS: () => MES_PART_GTINS,
|
|
54
|
-
MES_PRODUCTS: () => MES_PRODUCTS,
|
|
55
|
-
MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
|
|
56
54
|
MES_TYPES: () => MES_TYPES,
|
|
57
55
|
MesKernel: () => MesKernel,
|
|
58
56
|
OPERATION_PROPERTY: () => OPERATION_PROPERTY,
|
|
@@ -86,9 +84,11 @@ __export(index_exports, {
|
|
|
86
84
|
axisAppliesTo: () => axisAppliesTo,
|
|
87
85
|
axisInfo: () => axisInfo,
|
|
88
86
|
axisSource: () => axisSource,
|
|
87
|
+
bizTransactionUri: () => bizTransactionUri,
|
|
89
88
|
capabilitiesForType: () => capabilitiesForType,
|
|
90
89
|
capabilityOf: () => capabilityOf,
|
|
91
90
|
classClosure: () => classClosure,
|
|
91
|
+
classIdentifierViolation: () => classIdentifierViolation,
|
|
92
92
|
commandsOf: () => commandsOf,
|
|
93
93
|
compareStates: () => compareStates,
|
|
94
94
|
computeOee: () => computeOee,
|
|
@@ -112,6 +112,7 @@ __export(index_exports, {
|
|
|
112
112
|
generationFractionAt: () => generationFractionAt,
|
|
113
113
|
graiUri: () => graiUri,
|
|
114
114
|
hierarchyOf: () => hierarchyOf,
|
|
115
|
+
identityGroundingOf: () => identityGroundingOf,
|
|
115
116
|
inWorkCalendar: () => inWorkCalendar,
|
|
116
117
|
inWorkCalendarAt: () => inWorkCalendarAt,
|
|
117
118
|
ingest: () => ingest,
|
|
@@ -604,6 +605,24 @@ function readBoardAssets(def) {
|
|
|
604
605
|
const list = def.assets ?? [];
|
|
605
606
|
return list.map((a) => normalizeHomeLocation(a));
|
|
606
607
|
}
|
|
608
|
+
function identityGroundingOf(spec, kernelConstantPrefix) {
|
|
609
|
+
const declared = spec?.identity?.namespaces?.filter((n) => typeof n === "string" && n.length > 0) ?? [];
|
|
610
|
+
if (spec?.identity?.issuedClaim && declared.length) {
|
|
611
|
+
return { grounding: "issued", basis: "claimed-issued", namespaces: [...declared] };
|
|
612
|
+
}
|
|
613
|
+
if (declared.length) return { grounding: "declared", basis: "declared-namespace", namespaces: [...declared] };
|
|
614
|
+
if (spec?.companyPrefix && spec.binding) {
|
|
615
|
+
return { grounding: "declared", basis: "gs1-shaped", namespaces: gs1Namespaces(spec.companyPrefix) };
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
grounding: "fabricated",
|
|
619
|
+
basis: "kernel-constant",
|
|
620
|
+
namespaces: kernelConstantPrefix ? gs1Namespaces(kernelConstantPrefix) : []
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function gs1Namespaces(prefix) {
|
|
624
|
+
return [`urn:epc:idpat:sgtin:${prefix}.`, `urn:epc:class:lgtin:${prefix}.`, `urn:epc:id:sgtin:${prefix}.`];
|
|
625
|
+
}
|
|
607
626
|
|
|
608
627
|
// src/scenario-validate.ts
|
|
609
628
|
var DISTRIBUTIONS = /* @__PURE__ */ new Set(["poisson", "uniform", "constant", "profile"]);
|
|
@@ -677,6 +696,7 @@ function validateDomainDefinition(def) {
|
|
|
677
696
|
const locationKeys2 = new Set((def.locationTypes || []).map((n) => n.key));
|
|
678
697
|
const resKeys = new Set((def.resourceTypes || []).map((r) => r.key));
|
|
679
698
|
const matKeys = new Set((def.materials || []).map((m) => m.key));
|
|
699
|
+
const matByKey = new Map((def.materials || []).map((m) => [m.key, m]));
|
|
680
700
|
const opKeys = new Set((def.operations || []).map((o) => o.key));
|
|
681
701
|
const routeKeys = new Set((def.routes || []).map((r) => r.key));
|
|
682
702
|
for (const [name, arr] of [["locationTypes", def.locationTypes], ["resourceTypes", def.resourceTypes], ["materials", def.materials], ["operations", def.operations], ["routes", def.routes], ["recipes", def.recipes]]) {
|
|
@@ -698,6 +718,9 @@ function validateDomainDefinition(def) {
|
|
|
698
718
|
for (const p of [...rc.inputs || [], ...rc.outputs || []]) {
|
|
699
719
|
if (!matKeys.has(p.material)) v.push(`recipe '${rc.key}' material '${p.material}' \uBBF8\uC815\uC758`);
|
|
700
720
|
if (typeof p.qty !== "number" || p.qty <= 0) v.push(`recipe '${rc.key}' material '${p.material}' qty \uBD80\uC815`);
|
|
721
|
+
const mat = matByKey.get(p.material);
|
|
722
|
+
if (mat && !mat.locationType) v.push(`recipe '${rc.key}' material '${p.material}' locationType \uBBF8\uC120\uC5B8`);
|
|
723
|
+
if (mat?.locationType && !locationKeys2.has(mat.locationType)) v.push(`recipe '${rc.key}' material '${p.material}' locationType '${mat.locationType}' \uBBF8\uC815\uC758`);
|
|
701
724
|
}
|
|
702
725
|
}
|
|
703
726
|
return v;
|
|
@@ -923,6 +946,16 @@ function parseEpc(uri) {
|
|
|
923
946
|
function gdtiUri(companyPrefix, docType, serial) {
|
|
924
947
|
return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
|
|
925
948
|
}
|
|
949
|
+
function bizTransactionUri(namespace, transId) {
|
|
950
|
+
const ns = namespace?.trim();
|
|
951
|
+
if (!ns) return void 0;
|
|
952
|
+
const id = String(transId);
|
|
953
|
+
if (!id || id.includes("/") || id.includes(":")) return void 0;
|
|
954
|
+
if (/^https?:\/\/[^/\s]+/.test(ns)) return `${ns.replace(/\/+$/, "")}/bt/${id}`;
|
|
955
|
+
if (/^urn:epc(global)?:/.test(ns)) return void 0;
|
|
956
|
+
if (/^urn:[^:\s]+/.test(ns)) return `${ns.replace(/:+$/, "")}:bt:${id}`;
|
|
957
|
+
return void 0;
|
|
958
|
+
}
|
|
926
959
|
function header(type, eventTime, bizStep, opts) {
|
|
927
960
|
const h = {
|
|
928
961
|
"@context": EPCIS_CONTEXT,
|
|
@@ -992,6 +1025,17 @@ function transformationEvent(p) {
|
|
|
992
1025
|
var ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
993
1026
|
var TZ_RE = /^[+-]\d{2}:\d{2}$/;
|
|
994
1027
|
var ACTIONS = ["ADD", "OBSERVE", "DELETE"];
|
|
1028
|
+
var EPC_CLASS_PREFIXES = ["urn:epc:idpat:", "urn:epc:class:"];
|
|
1029
|
+
var DL_CANONICAL = "https://id.gs1.org/";
|
|
1030
|
+
var CBV_URL_CLASS = /^https?:\/\/[^/\s]+\/(?:[^/\s]+\/)*class\/[^/\s]+$/;
|
|
1031
|
+
var CBV_URN_CLASS = /^urn:[^:\s]+:(?:[^:\s]+:)*class:[^:\s]+$/;
|
|
1032
|
+
function classIdentifierViolation(epcClass) {
|
|
1033
|
+
if (!epcClass) return `quantity epcClass \uBD80\uC815: ${epcClass}`;
|
|
1034
|
+
if (EPC_CLASS_PREFIXES.some((p) => epcClass.startsWith(p))) return void 0;
|
|
1035
|
+
if (epcClass.startsWith(DL_CANONICAL)) return void 0;
|
|
1036
|
+
if (CBV_URL_CLASS.test(epcClass) || CBV_URN_CLASS.test(epcClass)) return void 0;
|
|
1037
|
+
return `quantity epcClass \uBD80\uC815: ${epcClass} \u2014 \uD074\uB798\uC2A4 \uC2DD\uBCC4\uC790\uB294 \uC18C\uC720 \uAD8C\uD55C\uC744 \uB9D0\uD560 \uC218 \uC788\uB294 \uBAA8\uC591\uC774\uC5B4\uC57C \uD55C\uB2E4 (EPC \uD074\uB798\uC2A4 urn:epc:idpat:/urn:epc:class: \xB7 CBV \xA78.3.4 http(s)://<\uB3C4\uBA54\uC778>/**/class/<id> \xB7 CBV \xA78.3.3 urn:<\uC774\uB984\uACF5\uAC04>:**:class:<id>)`;
|
|
1038
|
+
}
|
|
995
1039
|
function validateEpcisEvent(e) {
|
|
996
1040
|
const v = [];
|
|
997
1041
|
if (e["@context"] !== EPCIS_CONTEXT) v.push("@context \uB204\uB77D/\uBD88\uC77C\uCE58");
|
|
@@ -1059,7 +1103,8 @@ function validateEpcisEvent(e) {
|
|
|
1059
1103
|
"outputQuantityList" in e ? e.outputQuantityList : void 0
|
|
1060
1104
|
];
|
|
1061
1105
|
for (const list of qtyLists) for (const q of list ?? []) {
|
|
1062
|
-
|
|
1106
|
+
const bad2 = classIdentifierViolation(q.epcClass);
|
|
1107
|
+
if (bad2) v.push(bad2);
|
|
1063
1108
|
const hasQty = q.quantity !== void 0 && q.quantity !== null;
|
|
1064
1109
|
if (!hasQty) {
|
|
1065
1110
|
if (q.uom !== void 0) v.push("quantity \uC5C6\uC73C\uBA74 uom \uB3C4 \uC5C6\uC5B4\uC57C \uD55C\uB2E4(\uC218\uB7C9 \uBBF8\uC9C0\uC815)");
|
|
@@ -1376,6 +1421,8 @@ var ObservedReducer = class {
|
|
|
1376
1421
|
/* 진행 중 확보분과 거래번호 — **미러도 같은 필드를 채운다.** 시뮬만 아는 사실을 두면
|
|
1377
1422
|
그 위에서 세운 예측이 두 구동에서 달라진다(패리티 검사가 지키는 것이 이것이다). */
|
|
1378
1423
|
...d.gtin ? { gtin: d.gtin } : {},
|
|
1424
|
+
/* 어느 레시피로 만드는가 — 품목만으로는 「무엇으로」가 남지 않는다(같은 품목에 대체 레시피). */
|
|
1425
|
+
...d.recipeKey ? { recipeKey: d.recipeKey } : {},
|
|
1379
1426
|
...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
|
|
1380
1427
|
...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
|
|
1381
1428
|
/* 약속해 둔 자리·시각창도 채운다 — 시뮬만 알면 미러 위 예측이 「어디로 들일지」를 모른다.
|
|
@@ -1449,7 +1496,7 @@ var ObservedReducer = class {
|
|
|
1449
1496
|
const cur = this.items.get(ev.parentID);
|
|
1450
1497
|
if (!cur) this.pendingQuantities.set(ev.parentID, [...ev.childQuantityList]);
|
|
1451
1498
|
else {
|
|
1452
|
-
this.items.set(cur
|
|
1499
|
+
this.items.set(itemKeyOf(cur), this.withContainedQuantity(cur, q2, ev.childQuantityList));
|
|
1453
1500
|
this.qtyAggregation.set(ev.parentID, [...ev.childQuantityList]);
|
|
1454
1501
|
}
|
|
1455
1502
|
}
|
|
@@ -1475,7 +1522,7 @@ var ObservedReducer = class {
|
|
|
1475
1522
|
const cur = this.items.get(ev.parentID);
|
|
1476
1523
|
if (cur) {
|
|
1477
1524
|
const { gtin, gtinKey, qty, uom, quantities, lot, ...rest } = cur;
|
|
1478
|
-
this.items.set(cur
|
|
1525
|
+
this.items.set(itemKeyOf(cur), { ...rest, qty: 1 });
|
|
1479
1526
|
}
|
|
1480
1527
|
}
|
|
1481
1528
|
this.pendingQuantities.delete(ev.parentID);
|
|
@@ -1533,7 +1580,7 @@ var ObservedReducer = class {
|
|
|
1533
1580
|
return void 0;
|
|
1534
1581
|
}
|
|
1535
1582
|
mergeItem(epc, patch, q, all, subLotId) {
|
|
1536
|
-
const cur = this.items.get(subLotId
|
|
1583
|
+
const cur = this.items.get(itemKeyOf({ epc, subLotId }));
|
|
1537
1584
|
const parsedClass = q?.epcClass ? parseEpc(q.epcClass) : void 0;
|
|
1538
1585
|
const parsedSelf = parseEpc(epc);
|
|
1539
1586
|
const pendingQty = this.pendingQuantities.get(epc);
|
|
@@ -2822,8 +2869,21 @@ function mapRecordChecked(record, mapping, eventTime) {
|
|
|
2822
2869
|
};
|
|
2823
2870
|
}
|
|
2824
2871
|
const epc = resolve(mapping.epc, record, "epc", errors);
|
|
2872
|
+
const quantityList = resolveQuantityList(mapping.quantityList, record, "quantityList", errors);
|
|
2873
|
+
if (!epc && !quantityList.length) {
|
|
2874
|
+
errors.push("epc \uC640 quantityList \uAC00 \uB458 \uB2E4 \uBE44\uC5C8\uB2E4 \u2014 \uAC1C\uCCB4 \uC2DD\uBCC4\uC790\uB098 \uD074\uB798\uC2A4+\uC218\uB7C9 \uC911 \uD558\uB098\uB294 \uC788\uC5B4\uC57C \uD55C\uB2E4");
|
|
2875
|
+
}
|
|
2825
2876
|
return {
|
|
2826
|
-
event: objectEvent({
|
|
2877
|
+
event: objectEvent({
|
|
2878
|
+
eventTime,
|
|
2879
|
+
action,
|
|
2880
|
+
bizStep,
|
|
2881
|
+
disposition,
|
|
2882
|
+
epcList: epc ? [epc] : [],
|
|
2883
|
+
...quantityList.length ? { quantityList } : {},
|
|
2884
|
+
readPoint,
|
|
2885
|
+
bizLocation
|
|
2886
|
+
}),
|
|
2827
2887
|
errors
|
|
2828
2888
|
};
|
|
2829
2889
|
}
|
|
@@ -3262,10 +3322,124 @@ function energyFieldsOf(m) {
|
|
|
3262
3322
|
}
|
|
3263
3323
|
return out;
|
|
3264
3324
|
}
|
|
3325
|
+
var ItemStore = class _ItemStore {
|
|
3326
|
+
map = /* @__PURE__ */ new Map();
|
|
3327
|
+
byLocation = /* @__PURE__ */ new Map();
|
|
3328
|
+
get size() {
|
|
3329
|
+
return this.map.size;
|
|
3330
|
+
}
|
|
3331
|
+
get(key) {
|
|
3332
|
+
return this.map.get(key);
|
|
3333
|
+
}
|
|
3334
|
+
has(key) {
|
|
3335
|
+
return this.map.has(key);
|
|
3336
|
+
}
|
|
3337
|
+
values() {
|
|
3338
|
+
return this.map.values();
|
|
3339
|
+
}
|
|
3340
|
+
keys() {
|
|
3341
|
+
return this.map.keys();
|
|
3342
|
+
}
|
|
3343
|
+
entries() {
|
|
3344
|
+
return this.map.entries();
|
|
3345
|
+
}
|
|
3346
|
+
[Symbol.iterator]() {
|
|
3347
|
+
return this.map[Symbol.iterator]();
|
|
3348
|
+
}
|
|
3349
|
+
forEach(fn) {
|
|
3350
|
+
this.map.forEach((v, k) => fn(v, k));
|
|
3351
|
+
}
|
|
3352
|
+
set(key, item) {
|
|
3353
|
+
const prev = this.map.get(key);
|
|
3354
|
+
if (prev) this.unindex(key, prev.location);
|
|
3355
|
+
this.map.set(key, item);
|
|
3356
|
+
this.index(key, item.location);
|
|
3357
|
+
return this;
|
|
3358
|
+
}
|
|
3359
|
+
delete(key) {
|
|
3360
|
+
const prev = this.map.get(key);
|
|
3361
|
+
if (prev) this.unindex(key, prev.location);
|
|
3362
|
+
return this.map.delete(key);
|
|
3363
|
+
}
|
|
3364
|
+
clear() {
|
|
3365
|
+
this.map.clear();
|
|
3366
|
+
this.byLocation.clear();
|
|
3367
|
+
}
|
|
3368
|
+
/**
|
|
3369
|
+
* 물품을 다른 자리로 옮긴다 — **색인이 함께 움직이는 유일한 통로.**
|
|
3370
|
+
*
|
|
3371
|
+
* 없는 물품을 옮기라는 요청은 조용히 넘기지 않는다: 그 요청을 낸 쪽의 계산이 이미 어긋난 것이고,
|
|
3372
|
+
* 지나가면 그 뒤의 이동·처분이 엉뚱한 자리에 적힌다.
|
|
3373
|
+
*/
|
|
3374
|
+
relocate(key, to) {
|
|
3375
|
+
const it = this.map.get(key);
|
|
3376
|
+
if (!it) throw new Error(`relocate: \uBB3C\uD488 '${key}' \uC774 \uC0C1\uD0DC\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 \uC5C6\uB294 \uAC83\uC744 \uC62E\uAE38 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
|
|
3377
|
+
if (it.location !== to) {
|
|
3378
|
+
this.unindex(key, it.location);
|
|
3379
|
+
it.location = to;
|
|
3380
|
+
this.index(key, to);
|
|
3381
|
+
}
|
|
3382
|
+
return it;
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3385
|
+
* 사본 — **자기 복제 방법을 스스로 안다.**
|
|
3386
|
+
*
|
|
3387
|
+
* `fork()` 는 `Map` 만 알고 나머지는 `structuredClone` 으로 복제한다. 그 함수는 클래스의 메서드를
|
|
3388
|
+
* 복제하지 못하므로(평범한 객체가 된다), 저장소가 복제를 맡지 않으면 사본의 물품 조회가 통째로
|
|
3389
|
+
* 깨진다 — 실제로 그렇게 42건이 붉어졌다. 복제 방법을 아는 객체가 스스로 복제한다.
|
|
3390
|
+
*/
|
|
3391
|
+
clone() {
|
|
3392
|
+
const out = new _ItemStore();
|
|
3393
|
+
for (const [k, it] of this.map) out.set(k, structuredClone(it));
|
|
3394
|
+
return out;
|
|
3395
|
+
}
|
|
3396
|
+
/** 그 자리에 있는 물품들 — 색인이 답한다(전체 순회가 아니다). */
|
|
3397
|
+
at(location) {
|
|
3398
|
+
const keys = this.byLocation.get(location);
|
|
3399
|
+
if (!keys) return [];
|
|
3400
|
+
const out = [];
|
|
3401
|
+
for (const k of keys) {
|
|
3402
|
+
const it = this.map.get(k);
|
|
3403
|
+
if (it) out.push(it);
|
|
3404
|
+
}
|
|
3405
|
+
return out;
|
|
3406
|
+
}
|
|
3407
|
+
/**
|
|
3408
|
+
* 색인이 맵과 어긋난 자리 — **시험이 쓰는 확인 통로**(전체를 다시 세므로 비싸다).
|
|
3409
|
+
*
|
|
3410
|
+
* 어긋남은 조용한 결함이다(자재가 있는데 없다고 판정된다). 그래서 「어긋나지 않을 것이다」를 믿지 않고
|
|
3411
|
+
* 시나리오를 돌린 뒤 이 값으로 확인한다.
|
|
3412
|
+
*/
|
|
3413
|
+
indexDrift() {
|
|
3414
|
+
const drift = [];
|
|
3415
|
+
for (const [key, it] of this.map) {
|
|
3416
|
+
if (!this.byLocation.get(it.location)?.has(key)) drift.push(`${key} \uC774 '${it.location}' \uC0C9\uC778\uC5D0 \uC5C6\uB2E4`);
|
|
3417
|
+
}
|
|
3418
|
+
for (const [loc, keys] of this.byLocation) {
|
|
3419
|
+
for (const k of keys) {
|
|
3420
|
+
const it = this.map.get(k);
|
|
3421
|
+
if (!it) drift.push(`${k} \uC774 \uC9C0\uC6CC\uC84C\uB294\uB370 '${loc}' \uC0C9\uC778\uC5D0 \uB0A8\uC544 \uC788\uB2E4`);
|
|
3422
|
+
else if (it.location !== loc) drift.push(`${k} \uC740 '${it.location}' \uC5D0 \uC788\uB294\uB370 '${loc}' \uC0C9\uC778\uC5D0 \uC788\uB2E4`);
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
return drift;
|
|
3426
|
+
}
|
|
3427
|
+
index(key, location) {
|
|
3428
|
+
const set = this.byLocation.get(location) ?? /* @__PURE__ */ new Set();
|
|
3429
|
+
set.add(key);
|
|
3430
|
+
this.byLocation.set(location, set);
|
|
3431
|
+
}
|
|
3432
|
+
unindex(key, location) {
|
|
3433
|
+
const set = this.byLocation.get(location);
|
|
3434
|
+
if (!set) return;
|
|
3435
|
+
set.delete(key);
|
|
3436
|
+
if (!set.size) this.byLocation.delete(location);
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3265
3439
|
var FlowEngine = class {
|
|
3266
3440
|
tenantId;
|
|
3267
3441
|
locations = /* @__PURE__ */ new Map();
|
|
3268
|
-
items =
|
|
3442
|
+
items = new ItemStore();
|
|
3269
3443
|
equipment = /* @__PURE__ */ new Map();
|
|
3270
3444
|
/** 등급 정의(표준 `<X>Class`) — 상속·유효기간 판정의 재료. 선언 안 하면 비어 있고, 소속 그대로 판정한다. */
|
|
3271
3445
|
classDefs = {};
|
|
@@ -3329,6 +3503,21 @@ var FlowEngine = class {
|
|
|
3329
3503
|
/** 관측 구동(P0) — 이벤트를 접는 투영기와 그 사실. tick 과 섞이지 않게 명시적으로 들고 있다. */
|
|
3330
3504
|
observer;
|
|
3331
3505
|
/** 관측분이 아직 커널 상태로 옮겨지지 않았다 — 스냅샷·fork 직전에 한 번만 옮긴다. */
|
|
3506
|
+
/**
|
|
3507
|
+
* **관측 모드에서 원본과 어긋난 횟수** — 받아들였지만 사실이 맞지 않았다.
|
|
3508
|
+
*
|
|
3509
|
+
* 미러는 원본을 비추는 쪽이라 어긋남을 만나도 멈추지 않는다(멈추면 원본의 한 건이 트윈 전체를
|
|
3510
|
+
* 세운다). 그래서 **세어 둔다** — 세지 않으면 그 결함이 아무 데도 드러나지 않고, 화면은 어긋난 적이
|
|
3511
|
+
* 없는 트윈과 구별되지 않는다.
|
|
3512
|
+
*/
|
|
3513
|
+
/**
|
|
3514
|
+
* **씨앗이 심지 못한 참조의 수** — 원본이 말했지만 그 물품이 스냅샷에 없었다.
|
|
3515
|
+
*
|
|
3516
|
+
* 0 이 아니면 이 트윈의 상태는 원본의 일부를 담지 못했다는 뜻이다. 예측·집계가 그 사실을 모르면
|
|
3517
|
+
* 부족한 씨앗 위에서 낸 답을 완전한 답으로 읽는다.
|
|
3518
|
+
*/
|
|
3519
|
+
seedDanglingRefs = 0;
|
|
3520
|
+
transformInputsAbsent = 0;
|
|
3332
3521
|
observedDirty = false;
|
|
3333
3522
|
observeMode = false;
|
|
3334
3523
|
/** 관측 구동이 투영기를 세울 때 필요한 원본 보드(구조는 이벤트가 아니라 마스터에서 온다). */
|
|
@@ -3533,6 +3722,24 @@ var FlowEngine = class {
|
|
|
3533
3722
|
for (const id of snap?.acked ?? []) this._acked.add(id);
|
|
3534
3723
|
for (const e of snap?.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
|
|
3535
3724
|
}
|
|
3725
|
+
/**
|
|
3726
|
+
* 씨앗의 확보분 중 **이 트윈에 실제로 있는 것만** 남긴다 — 없는 것은 세고 버린다.
|
|
3727
|
+
*
|
|
3728
|
+
* 원본이 확보분을 말했는데 그 물품이 스냅샷에 없으면, 그 참조는 심는 순간 이미 사실이 아니다.
|
|
3729
|
+
* 심어 두면 나중에 틱에서 드러나고(시뮬은 없는 것을 소비하지 않으므로 멈춘다) 그 판정이 예측
|
|
3730
|
+
* 경로에서 터지면 원본의 빈틈 하나가 답 전체를 서버 오류로 죽인다.
|
|
3731
|
+
*
|
|
3732
|
+
* 그래서 심을 때 맞춘다. **없는 물품을 만들어 채우지 않는다** — 채우면 계보와 재고가 거짓 위에 선다.
|
|
3733
|
+
* 대신 몇 건이 맞지 않았는지 남겨 씨앗이 불완전했다는 사실을 답에 실을 수 있게 한다.
|
|
3734
|
+
*/
|
|
3735
|
+
resolvedAllocated(allocated) {
|
|
3736
|
+
const all = allocated ?? [];
|
|
3737
|
+
if (!all.length) return { kept: [], dropped: 0 };
|
|
3738
|
+
const kept = all.filter((epc) => this.items.has(epc));
|
|
3739
|
+
const dropped = all.length - kept.length;
|
|
3740
|
+
this.seedDanglingRefs += dropped;
|
|
3741
|
+
return { kept, dropped };
|
|
3742
|
+
}
|
|
3536
3743
|
hydrateObserved(snap, orders = []) {
|
|
3537
3744
|
for (const id of snap.acked ?? []) this._acked.add(id);
|
|
3538
3745
|
for (const e of snap.attentionSince ?? []) if (e?.id && e.since) this._attentionSince.set(e.id, e.since);
|
|
@@ -3645,6 +3852,7 @@ var FlowEngine = class {
|
|
|
3645
3852
|
사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
|
|
3646
3853
|
...o.allocated?.length ? { allocated: o.allocated } : {},
|
|
3647
3854
|
...o.gtin ? { gtin: o.gtin } : {},
|
|
3855
|
+
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
3648
3856
|
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
3649
3857
|
...o.dockDoor ? { dockDoor: o.dockDoor } : {},
|
|
3650
3858
|
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
|
|
@@ -3653,6 +3861,7 @@ var FlowEngine = class {
|
|
|
3653
3861
|
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
3654
3862
|
const remaining = lines.length ? lines.reduce((s, l) => s + l.requested, 0) : Math.max(0, (o.requested ?? 0) - (o.fulfilled ?? 0));
|
|
3655
3863
|
if (remaining <= 0) continue;
|
|
3864
|
+
const resolved = this.resolvedAllocated(o.allocated);
|
|
3656
3865
|
this.orders.set(o.orderId, {
|
|
3657
3866
|
id: o.orderId,
|
|
3658
3867
|
kind: o.kind,
|
|
@@ -3664,11 +3873,15 @@ var FlowEngine = class {
|
|
|
3664
3873
|
않으면(옛 저널) 그때는 비는 것이 사실이다 — 없는 것을 지어내지 않는다. */
|
|
3665
3874
|
/* 품목도 함께 — 씨앗이 흘리면 되살아난 오더가 무엇을 만드는지 모른다. */
|
|
3666
3875
|
...o.gtin ? { gtin: o.gtin } : {},
|
|
3876
|
+
/* 레시피도 함께 — 품목만 이어받으면 되살아난 오더가 「무엇으로」를 모르고, 그 오더의
|
|
3877
|
+
남은 공정·소요가 다른 레시피 기준으로 계산된다(같은 품목에 대체 레시피가 있다). */
|
|
3878
|
+
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
3667
3879
|
bizTransaction: o.bizTransaction ?? "",
|
|
3668
|
-
allocated:
|
|
3880
|
+
allocated: resolved.kept,
|
|
3669
3881
|
picked: [],
|
|
3670
3882
|
shipmentEpc: null,
|
|
3671
3883
|
lines,
|
|
3884
|
+
...resolved.dropped ? { seedIncomplete: true } : {},
|
|
3672
3885
|
/* **보류를 이어받는다** — 잃으면 씨앗이 사람이 일부러 멈춘 오더를 다시 계획해 내보낸다.
|
|
3673
3886
|
씨앗 왕복 대조가 이것을 잡았다(그 전에는 사람이 코드를 읽어야만 알 수 있었다). */
|
|
3674
3887
|
...o.held ? { held: true } : {},
|
|
@@ -3942,6 +4155,12 @@ var FlowEngine = class {
|
|
|
3942
4155
|
simClockMs: this.clockMs,
|
|
3943
4156
|
nowTime: this.now(),
|
|
3944
4157
|
// 트윈의 "지금" — 관측 모드면 마지막으로 들은 시각(§nowMs)
|
|
4158
|
+
identityGrounding: this.identityGroundingView(),
|
|
4159
|
+
/* 원본과 어긋난 사실 — 0 이면 싣지 않는다(어긋난 적 없는 트윈에 빈 칸을 만들지 않는다). */
|
|
4160
|
+
...this.transformInputsAbsent || this.seedDanglingRefs ? { conformance: {
|
|
4161
|
+
...this.transformInputsAbsent ? { transformInputsAbsent: this.transformInputsAbsent } : {},
|
|
4162
|
+
...this.seedDanglingRefs ? { seedDanglingRefs: this.seedDanglingRefs } : {}
|
|
4163
|
+
} } : {},
|
|
3945
4164
|
/* 출처 표시 — 보드(마스터)에서 온 자리다. 미러는 관측으로 알게 된 자리를 'observed' 로 구별하는데,
|
|
3946
4165
|
* 시뮬이 아무 표시도 안 하면 소비처가 두 스냅샷을 같은 규칙으로 읽지 못한다. */
|
|
3947
4166
|
locations: [...this.locations.values()].map((n) => {
|
|
@@ -4027,6 +4246,10 @@ var FlowEngine = class {
|
|
|
4027
4246
|
* 라인이 있는 오더는 `lines[].gtin` 이 품목을 말하고, 단일 품목 작업지시는 이 자리가 말한다.
|
|
4028
4247
|
*/
|
|
4029
4248
|
...o.gtin ? { gtin: o.gtin } : {},
|
|
4249
|
+
/* **어느 레시피로 만드나** — 품목만으로는 「무엇으로」가 안 남는다(같은 품목의 대체 레시피가 있다). */
|
|
4250
|
+
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
4251
|
+
/* 씨앗이 다 심지 못했다는 사실 — 조용히 자르지 않는다(그 오더의 답은 부족한 씨앗 위에 있다). */
|
|
4252
|
+
...o.seedIncomplete ? { seedIncomplete: true } : {},
|
|
4030
4253
|
...o.lines?.length ? { lines: o.lines.map((l) => ({ gtin: l.gtin, requested: l.requested })) } : {},
|
|
4031
4254
|
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
4032
4255
|
...o.startTime ? { startTime: o.startTime } : {},
|
|
@@ -4138,7 +4361,8 @@ var FlowEngine = class {
|
|
|
4138
4361
|
if (skip.has(key)) continue;
|
|
4139
4362
|
const v = this[key];
|
|
4140
4363
|
if (typeof v === "function") continue;
|
|
4141
|
-
|
|
4364
|
+
const cloneable = v;
|
|
4365
|
+
clone[key] = v instanceof Map ? new Map([...v.entries()].map(([k, o]) => [k, structuredClone(o)])) : typeof cloneable?.clone === "function" ? cloneable.clone() : structuredClone(v);
|
|
4142
4366
|
}
|
|
4143
4367
|
clone.rng.state = this.rng.state;
|
|
4144
4368
|
clone.durationEstimator = this.durationEstimator;
|
|
@@ -4360,7 +4584,7 @@ var FlowEngine = class {
|
|
|
4360
4584
|
* 이긴다(원천 사본의 값이 낡았을 때 고칠 자리가 여기다). 무엇을 썼는지는 `specCoverage()` 가 밝힌다.
|
|
4361
4585
|
*
|
|
4362
4586
|
* ── 조용히 버리지 않는다 ────────────────────────────────────────────────────
|
|
4363
|
-
* 읽을 수 없는 값은
|
|
4587
|
+
* 읽을 수 없는 값은 오류를 낸다. 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 도는데,
|
|
4364
4588
|
* 그 어긋남을 아무도 볼 수 없다(이 시스템에서 가장 비싼 종류의 침묵이다).
|
|
4365
4589
|
*/
|
|
4366
4590
|
declareDurations(durations) {
|
|
@@ -4388,7 +4612,7 @@ var FlowEngine = class {
|
|
|
4388
4612
|
* · **현장 선언이 원천 명세를 이긴다**(ADR-0034) — 원천 사본이 낡았을 때 고칠 자리가 그것뿐이다.
|
|
4389
4613
|
* · 명세 행이 없는 종류에도 얹힌다 — 행을 지어 만들면 지어낸 `intent` 가 능력 계산을 오염시킨다.
|
|
4390
4614
|
* · 무엇을 썼는지는 `specCoverage()` 의 `parameters` 가 그대로 밝힌다.
|
|
4391
|
-
* · 빈 이름·빈 값은
|
|
4615
|
+
* · 빈 이름·빈 값은 **오류를 낸다**: 받아 두고 무시하면 화면은 「넣었습니다」라고 말하고 시뮬은 상수로 돈다.
|
|
4392
4616
|
*/
|
|
4393
4617
|
declareParameters(params) {
|
|
4394
4618
|
for (const [kind, entries] of Object.entries(params ?? {})) {
|
|
@@ -4412,6 +4636,17 @@ var FlowEngine = class {
|
|
|
4412
4636
|
routeKeys() {
|
|
4413
4637
|
return void 0;
|
|
4414
4638
|
}
|
|
4639
|
+
/**
|
|
4640
|
+
* **이 트윈이 굴리는 서로 다른 라우트들** — 용량이 답할 수 있는지를 가른다.
|
|
4641
|
+
*
|
|
4642
|
+
* 하나면 `routeKeys()` 가 그 순서를 답한다. 둘 이상이면 「하루 몇 대」의 답이 **제품 구성에 따라
|
|
4643
|
+
* 달라지므로**, 하나를 골라 답하지 않고 서로 다르다는 사실을 `capacity()` 가 함께 낸다. 조용히 고르면
|
|
4644
|
+
* 능력 숫자가 거짓이 되고, 그 위에 선 모든 계획이 거짓 위에 선다.
|
|
4645
|
+
*/
|
|
4646
|
+
distinctRouteKeys() {
|
|
4647
|
+
const one = this.routeKeys();
|
|
4648
|
+
return one ? ["(single)"] : [];
|
|
4649
|
+
}
|
|
4415
4650
|
/**
|
|
4416
4651
|
* **이 공장이 하루 몇 대를 낼 수 있는가** — 실행해 보지 않고 답한다.
|
|
4417
4652
|
*
|
|
@@ -4438,7 +4673,7 @@ var FlowEngine = class {
|
|
|
4438
4673
|
sampleWeekStartMs: opts.sampleWeekStartMs,
|
|
4439
4674
|
unitsPerDay: opts.unitsPerDay
|
|
4440
4675
|
});
|
|
4441
|
-
return { ...analysis, mixedCalendars: calendars.size > 1 };
|
|
4676
|
+
return { ...analysis, mixedCalendars: calendars.size > 1, mixedRoutes: this.distinctRouteKeys().length > 1 };
|
|
4442
4677
|
}
|
|
4443
4678
|
/**
|
|
4444
4679
|
* **생산 능력 보고서** — ISA-95 Part 4 `OperationsCapability`(§operations-capability).
|
|
@@ -4664,7 +4899,7 @@ var FlowEngine = class {
|
|
|
4664
4899
|
* `skuMix` 에서 weight 로 gtin 선택(rng) — 도착·오더 자극의 품목 결정.
|
|
4665
4900
|
*
|
|
4666
4901
|
* **빈 목록이면 고르지 않는다**(`undefined`). 예전에는 `mix[mix.length - 1].gtin` 으로 떨어져
|
|
4667
|
-
* `mix[-1]` 이 undefined 가 되고 거기서
|
|
4902
|
+
* `mix[-1]` 이 undefined 가 되고 거기서 오류가 났다 — 그 예외가 서버까지 올라가 **정확도 추세 전체를
|
|
4668
4903
|
* 죽였다**(품목 구성을 선언하지 않은 자극 하나가 예측 전체를 껐다).
|
|
4669
4904
|
*
|
|
4670
4905
|
* 없는 품목을 지어내지 않는다: 무엇을 만들지 모르면 **만들지 않는 것**이 맞고, 부르는 쪽이
|
|
@@ -4692,9 +4927,16 @@ var FlowEngine = class {
|
|
|
4692
4927
|
* 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
|
|
4693
4928
|
*/
|
|
4694
4929
|
transform(inputs, outputs, opts) {
|
|
4695
|
-
|
|
4930
|
+
const absent = inputs.filter((epc) => !this.items.get(epc));
|
|
4931
|
+
if (absent.length && !this.observeMode) {
|
|
4932
|
+
throw new Error(
|
|
4933
|
+
`transform: \uC785\uB825 ${absent.length}\uAC74\uC774 \uC0C1\uD0DC\uC5D0 \uC5C6\uC2B5\uB2C8\uB2E4 (${absent.slice(0, 3).join(", ")}${absent.length > 3 ? " \u2026" : ""}) \u2014 \uC5C6\uB294 \uAC83\uC744 \uC18C\uBE44\uD588\uB2E4\uACE0 \uAE30\uB85D\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC774 \uD2B8\uC708\uC758 \uACC4\uC0B0\uC774 \uC5C6\uB294 \uBB3C\uAC74\uC744 \uC694\uAD6C\uD588\uC2B5\uB2C8\uB2E4.`
|
|
4934
|
+
);
|
|
4935
|
+
}
|
|
4936
|
+
if (absent.length) this.transformInputsAbsent += absent.length;
|
|
4937
|
+
const consumed = absent.length ? inputs.filter((epc) => this.items.has(epc)) : inputs;
|
|
4938
|
+
for (const epc of consumed) {
|
|
4696
4939
|
const it = this.items.get(epc);
|
|
4697
|
-
if (!it) continue;
|
|
4698
4940
|
const n = this.locations.get(it.location);
|
|
4699
4941
|
if (n) n.occupancy--;
|
|
4700
4942
|
this.items.delete(epc);
|
|
@@ -4708,7 +4950,7 @@ var FlowEngine = class {
|
|
|
4708
4950
|
eventTime: this.now(),
|
|
4709
4951
|
bizStep: opts.bizStep,
|
|
4710
4952
|
disposition: opts.disposition,
|
|
4711
|
-
inputEPCList:
|
|
4953
|
+
inputEPCList: consumed.length ? consumed.slice() : void 0,
|
|
4712
4954
|
outputEPCList: outputs.length ? outputs.map((o) => o.epc) : void 0,
|
|
4713
4955
|
transformationID: opts.transformationId,
|
|
4714
4956
|
readPoint: opts.readPoint,
|
|
@@ -4911,6 +5153,9 @@ var FlowEngine = class {
|
|
|
4911
5153
|
/* 진행 중 할당과 거래번호를 함께 싣는다 — 이것이 없으면 웜스타트가 잃고, 되살아난 오더의
|
|
4912
5154
|
계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
|
|
4913
5155
|
...o.gtin ? { gtin: o.gtin } : {},
|
|
5156
|
+
/* 어느 레시피로 만드는가 — 품목만 보내면 미러는 「무엇으로」를 알 수 없다(같은 품목에 대체
|
|
5157
|
+
레시피가 있다). 시뮬만 아는 사실이 남으면 두 스냅샷을 같은 규칙으로 읽을 수 없다. */
|
|
5158
|
+
...o.recipeKey ? { recipeKey: o.recipeKey } : {},
|
|
4914
5159
|
...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
|
|
4915
5160
|
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
4916
5161
|
/* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
|
|
@@ -5056,9 +5301,8 @@ var FlowEngine = class {
|
|
|
5056
5301
|
for (const req of need) {
|
|
5057
5302
|
let remaining = Math.max(0, req.quantity ?? 0);
|
|
5058
5303
|
if (!remaining) continue;
|
|
5059
|
-
for (const it of this.items.
|
|
5304
|
+
for (const it of this.items.at(at)) {
|
|
5060
5305
|
if (remaining <= 0) break;
|
|
5061
|
-
if (it.location !== at) continue;
|
|
5062
5306
|
if (!this.materialMatches(it, req)) continue;
|
|
5063
5307
|
const already = takenSoFar.get(it.epc) ?? 0;
|
|
5064
5308
|
const avail = Math.max(0, (it.qty ?? 1) - already);
|
|
@@ -5095,7 +5339,36 @@ var FlowEngine = class {
|
|
|
5095
5339
|
* 같은 품목이 그 자리에 이미 있으면 **수량을 더한다** — 새 줄을 만들면 같은 자리의 같은 로트가
|
|
5096
5340
|
* 둘로 갈려 재고가 부푼다(§MaterialSubLot 에서 겪은 것과 반대 방향의 같은 오류).
|
|
5097
5341
|
*/
|
|
5342
|
+
/**
|
|
5343
|
+
* **이 공정의 산출을 도메인이 직접 만드는가** — 기본은 아니다(코어가 만든다).
|
|
5344
|
+
*
|
|
5345
|
+
* ── 왜 이 이음새가 있나 (2026-08-20) ──────────────────────────────────────
|
|
5346
|
+
* 코어의 산출은 **비직렬 클래스+수량**이다(일련번호를 지어내지 않으므로). 그런데 어떤 도메인은
|
|
5347
|
+
* 산출물에 **개체 정체성**이 필요하다: MES 의 레시피 생산은 개체마다 직렬번호를 갖고, 수율이
|
|
5348
|
+
* 개체별로 양품/불량을 가르고, 계보(`TransformationEvent`)가 오더의 단계들을 잇는다.
|
|
5349
|
+
*
|
|
5350
|
+
* 그 도메인이 산출을 만들 때 코어도 같은 선언을 보고 만들면 **같은 산출이 두 벌** 된다 — 재고가
|
|
5351
|
+
* 조용히 두 배가 되고, 그 위의 모든 계산이 거짓 위에 선다. 그래서 소유를 **한쪽으로 정한다.**
|
|
5352
|
+
*
|
|
5353
|
+
* 코어는 여기서 **도메인 명사를 하나도 알지 않는다**: 「누가 만드는가」만 묻는다. 무엇을 만드는지는
|
|
5354
|
+
* 여전히 선언이 정하고, 코어는 그 선언을 읽을 뿐이다.
|
|
5355
|
+
*/
|
|
5356
|
+
producesOwnOutputs(_opKey) {
|
|
5357
|
+
return false;
|
|
5358
|
+
}
|
|
5359
|
+
/**
|
|
5360
|
+
* **이 트윈의 정체성 근거** — 스냅샷에 실린다(§`StateSnapshot.identityGrounding`).
|
|
5361
|
+
*
|
|
5362
|
+
* 코어는 선언을 갖고 있지 않다(생산 선언은 도메인의 것이다). 그래서 기본은 **선언 없음**이고,
|
|
5363
|
+
* 선언을 든 커널이 override 해서 자기 것을 답한다 — `routeKeys()` 와 같은 모양이다.
|
|
5364
|
+
*
|
|
5365
|
+
* 코어는 여기서 **정체성의 값을 정하지 않는다**: 「어디서 왔나」만 묻는다.
|
|
5366
|
+
*/
|
|
5367
|
+
identityGroundingView() {
|
|
5368
|
+
return identityGroundingOf(void 0);
|
|
5369
|
+
}
|
|
5098
5370
|
produceMaterials(t) {
|
|
5371
|
+
if (this.producesOwnOutputs(t.kind)) return;
|
|
5099
5372
|
const made = (this.operationSpecs.get(t.kind)?.materialSpecification ?? []).filter((m) => m.use === "produced");
|
|
5100
5373
|
if (!made.length) return;
|
|
5101
5374
|
for (const spec of made) {
|
|
@@ -5132,7 +5405,7 @@ var FlowEngine = class {
|
|
|
5132
5405
|
* 작업은 혼자 서지 못한다: 옮길 **물품**과, (있다면) 그것을 시킨 **오더** 위에 선다. 진행 중에
|
|
5133
5406
|
* 둘 중 하나가 사라질 수 있다 — 물품은 포장·출하·소비로, 오더는 이미 이행돼 씨앗이 주입하지 않아서.
|
|
5134
5407
|
*
|
|
5135
|
-
* 그때 도메인 훅은 없는 것을 딛으려다
|
|
5408
|
+
* 그때 도메인 훅은 없는 것을 딛으려다 오류를 낸다(`order.gtin` · `item.location`). 그 예외 하나가
|
|
5136
5409
|
* **예측 전체를 죽였다** — 사용자에게는 기능이 통째로 사라진 것으로 보였다. 그래서 완료 **전에**
|
|
5137
5410
|
* 여기서 묻고, 없으면 그 작업만 접는다.
|
|
5138
5411
|
*/
|
|
@@ -5146,7 +5419,7 @@ var FlowEngine = class {
|
|
|
5146
5419
|
* 이 도메인이 이 작업을 **끝맺을 수 있나** — 코어가 모르는 조건은 도메인이 답한다.
|
|
5147
5420
|
*
|
|
5148
5421
|
* 코어는 물품과 오더까지만 안다. 그런데 도메인은 더 필요할 수 있다 — MES 는 완료 시점에 **오더와
|
|
5149
|
-
* 제품 정의**를 딛고 서고, 그 중 하나만 없어도
|
|
5422
|
+
* 제품 정의**를 딛고 서고, 그 중 하나만 없어도 오류를 낸다. 코어가 그 조건을 추측하면 도메인마다 다른
|
|
5150
5423
|
* 가정을 코어에 박게 되므로(방언), **묻는다.**
|
|
5151
5424
|
*
|
|
5152
5425
|
* 기본은 `true` — 대부분의 작업은 코어가 확인한 것으로 충분하다.
|
|
@@ -5159,7 +5432,7 @@ var FlowEngine = class {
|
|
|
5159
5432
|
*
|
|
5160
5433
|
* 물품 맵의 키는 `itemKeyOf`(직렬 물품은 `epc`, 로트의 부분은 `subLotId`)인데, 작업은 로트 식별자
|
|
5161
5434
|
* (`itemEpc`)로 가리킨다. 비직렬 로트에서는 둘이 **다르다** — 그래서 그냥 `get` 하면 못 찾는다.
|
|
5162
|
-
* 실제로 그 회귀를 냈다: 예측(씨앗) 경로에서 `item.location = …` 이 `undefined` 위에서
|
|
5435
|
+
* 실제로 그 회귀를 냈다: 예측(씨앗) 경로에서 `item.location = …` 이 `undefined` 위에서 오류를 내어
|
|
5163
5436
|
* **정확도 추세 전체가 서버 오류로 죽었다.**
|
|
5164
5437
|
*
|
|
5165
5438
|
* **부분이 여럿이면 풀지 않는다** — 어느 부분을 가리키는지 알 수 없고, 아무거나 고르면 그 뒤
|
|
@@ -5168,6 +5441,7 @@ var FlowEngine = class {
|
|
|
5168
5441
|
itemByRef(ref) {
|
|
5169
5442
|
const exact = this.items.get(ref);
|
|
5170
5443
|
if (exact) return exact;
|
|
5444
|
+
this.refScans++;
|
|
5171
5445
|
let hit;
|
|
5172
5446
|
for (const it of this.items.values()) {
|
|
5173
5447
|
if (it.epc !== ref) continue;
|
|
@@ -5176,6 +5450,16 @@ var FlowEngine = class {
|
|
|
5176
5450
|
}
|
|
5177
5451
|
return hit;
|
|
5178
5452
|
}
|
|
5453
|
+
/**
|
|
5454
|
+
* 참조가 키가 아니어서 전수 조회로 찾은 횟수 — **성능 판단의 근거**다.
|
|
5455
|
+
*
|
|
5456
|
+
* 규약대로면 0 이다. 0 이 아닌 것은 결함이 아니라 사실이다(원본이 준 참조는 우리 키를 모른다).
|
|
5457
|
+
* 다만 그 수가 규모와 함께 자라면 인덱스가 필요하다는 뜻이고, 그때 넣을 근거가 이 값이다.
|
|
5458
|
+
*/
|
|
5459
|
+
refScans = 0;
|
|
5460
|
+
refScanCount() {
|
|
5461
|
+
return this.refScans;
|
|
5462
|
+
}
|
|
5179
5463
|
/**
|
|
5180
5464
|
* 실제 자재 이동을 작업에 적어 둔다 — ISA-95 `JobResponse.MaterialActual`.
|
|
5181
5465
|
* 같은 품목·같은 쓰임은 **한 줄로 합친다**(줄을 늘리면 실적을 세는 쪽이 중복을 걷어내야 한다).
|
|
@@ -5692,7 +5976,7 @@ function matches(s, req) {
|
|
|
5692
5976
|
|
|
5693
5977
|
// src/kernel.ts
|
|
5694
5978
|
var TRAVEL_MS = 3e4;
|
|
5695
|
-
var
|
|
5979
|
+
var LEGACY_COMPANY_PREFIX = "0614141";
|
|
5696
5980
|
var SHELF_MS = 30 * 24 * 36e5;
|
|
5697
5981
|
var SHELF_JITTER_MS = 5 * 24 * 36e5;
|
|
5698
5982
|
var WmsKernel = class extends FlowEngine {
|
|
@@ -5700,15 +5984,37 @@ var WmsKernel = class extends FlowEngine {
|
|
|
5700
5984
|
constructor(tenantId, policy = firstFitPolicy) {
|
|
5701
5985
|
super(tenantId, policy);
|
|
5702
5986
|
}
|
|
5987
|
+
/**
|
|
5988
|
+
* 내장 시나리오가 요구하는 자리 — **없으면 말한다.**
|
|
5989
|
+
*
|
|
5990
|
+
* ── 왜 (2026-08-20) ────────────────────────────────────────────────────────
|
|
5991
|
+
* 이 커널은 입고·중간·보관 자리를 **자기 낱말로** 찾았고(`dock`·`staging`·`storage`), 없으면 조용히
|
|
5992
|
+
* `return` 했다. 그래서 그 낱말을 쓰지 않는 현장의 트윈은 **아무 일도 일어나지 않는 공장**이 됐다 —
|
|
5993
|
+
* 오류도 로그도 없이. 실제로 확인된 자리다: 첫 실 시스템(F&B)에는 `dock`·`gate`·`staging` 이 **한 건도
|
|
5994
|
+
* 없다**(자리 타입이 자유 문자열이고, 입출고가 도크 단계 없이 자리에 직접 일어난다).
|
|
5995
|
+
*
|
|
5996
|
+
* 여기서 이름을 여는 것이 답이 아니다 — 이 경로는 부품·라우트가 내장 상수인 레거시 시나리오라,
|
|
5997
|
+
* 이름만 열면 **지원하는 척**이 된다(MES 가 같은 이유로 같은 결론을 냈다). 답은 요구를 분명히 말하는
|
|
5998
|
+
* 것이고, 던지면 호스트가 그 트윈만 멈추고 이유를 등록부에 적는다(`tick-failed`) — 다른 트윈은 계속 돈다.
|
|
5999
|
+
* 조용한 정지는 그 사실조차 남기지 않는다.
|
|
6000
|
+
*/
|
|
6001
|
+
builtInLocation(type, what) {
|
|
6002
|
+
const loc = this.locationByType(type);
|
|
6003
|
+
if (!loc) {
|
|
6004
|
+
throw new Error(
|
|
6005
|
+
`built-in WMS scenario needs a location of type '${type}' (${what}) and this twin has none \u2014 rename it, or drive this twin from declarations instead of the built-in scenario`
|
|
6006
|
+
);
|
|
6007
|
+
}
|
|
6008
|
+
return loc;
|
|
6009
|
+
}
|
|
5703
6010
|
/** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
|
|
5704
6011
|
onArrival(spec) {
|
|
5705
|
-
const dock = this.
|
|
5706
|
-
|
|
5707
|
-
const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
|
|
6012
|
+
const dock = this.builtInLocation("dock", "inbound pallets land here");
|
|
6013
|
+
const epc = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
|
|
5708
6014
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
5709
6015
|
if (!gtin) return;
|
|
5710
6016
|
const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max);
|
|
5711
|
-
const po = gdtiUri(
|
|
6017
|
+
const po = gdtiUri(LEGACY_COMPANY_PREFIX, "401", ++this.poSeq);
|
|
5712
6018
|
const eventTime = this.now();
|
|
5713
6019
|
const qtyList = [{ epcClass: gtin, quantity: qty }];
|
|
5714
6020
|
const poTxn = [{ type: BTT.po, bizTransaction: po }];
|
|
@@ -5779,7 +6085,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
5779
6085
|
/** 오더 생성 — 약속(납기·우선순위)은 base `promiseOf` 가 계산한다(도메인마다 다르게 재지 않는다). */
|
|
5780
6086
|
createSalesOrder(lines, spec) {
|
|
5781
6087
|
const id = `order-${++this.orderSeq}`;
|
|
5782
|
-
const so = gdtiUri(
|
|
6088
|
+
const so = gdtiUri(LEGACY_COMPANY_PREFIX, "402", ++this.soSeq);
|
|
5783
6089
|
const requested = lines.reduce((s, l) => s + l.qty, 0);
|
|
5784
6090
|
const order = {
|
|
5785
6091
|
id,
|
|
@@ -5803,8 +6109,8 @@ var WmsKernel = class extends FlowEngine {
|
|
|
5803
6109
|
* 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
|
|
5804
6110
|
*/
|
|
5805
6111
|
allocate(o) {
|
|
5806
|
-
|
|
5807
|
-
|
|
6112
|
+
if (!o.lines) return;
|
|
6113
|
+
const staging = this.builtInLocation("staging", "picked pallets wait here before shipping");
|
|
5808
6114
|
const chosenAll = [];
|
|
5809
6115
|
for (const line of o.lines) {
|
|
5810
6116
|
const already = o.allocated.filter((e) => this.items.get(e)?.gtin === line.gtin).length;
|
|
@@ -5839,7 +6145,7 @@ var WmsKernel = class extends FlowEngine {
|
|
|
5839
6145
|
const to = this.locations.get(t.toNode);
|
|
5840
6146
|
from.occupancy--;
|
|
5841
6147
|
to.occupancy++;
|
|
5842
|
-
item
|
|
6148
|
+
this.items.relocate(itemKeyOf(item), to.id);
|
|
5843
6149
|
if (t.kind === "feed") {
|
|
5844
6150
|
this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: BIZSTEP.storing, disposition: item.disposition, epcList: [item.epc], quantityList: [{ epcClass: item.gtin, quantity: item.qty }], readPoint: to.id, bizLocation: to.id }));
|
|
5845
6151
|
return;
|
|
@@ -5952,21 +6258,21 @@ var WmsKernel = class extends FlowEngine {
|
|
|
5952
6258
|
const qty = row.qty ?? 0;
|
|
5953
6259
|
if (qty <= 0) continue;
|
|
5954
6260
|
this.items.delete(key);
|
|
5955
|
-
const pallet = ssccUri(
|
|
6261
|
+
const pallet = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
|
|
5956
6262
|
const qtyList = [{ epcClass: m.definitionId, quantity: qty }];
|
|
5957
6263
|
this.items.set(pallet, { epc: pallet, gtin: m.definitionId, qty, location: at, disposition: DISP.in_progress });
|
|
5958
6264
|
const eventTime = this.now();
|
|
5959
6265
|
this.emit(aggregationEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.packing, parentID: pallet, childQuantityList: qtyList, readPoint: at }));
|
|
5960
6266
|
this.emit(objectEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.packing, disposition: DISP.in_progress, epcList: [pallet], quantityList: qtyList, readPoint: at, bizLocation: at }));
|
|
5961
|
-
const storage = this.
|
|
5962
|
-
|
|
6267
|
+
const storage = this.builtInLocation("storage", "received pallets are put away here");
|
|
6268
|
+
this.pushTask(order, "putaway", pallet, at, storage.id);
|
|
5963
6269
|
}
|
|
5964
6270
|
}
|
|
5965
6271
|
/** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
|
|
5966
6272
|
finalizeOrder(order, staging) {
|
|
5967
6273
|
const shipDock = this.locationByType("dock-ship") ?? staging;
|
|
5968
6274
|
const eventTime = this.now();
|
|
5969
|
-
const shipment = ssccUri(
|
|
6275
|
+
const shipment = ssccUri(LEGACY_COMPANY_PREFIX, ++this.epcSeq);
|
|
5970
6276
|
order.shipmentEpc = shipment;
|
|
5971
6277
|
const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
|
|
5972
6278
|
this.aggregate(shipment, order.picked.slice(), { bizStep: BIZSTEP.packing, readPoint: staging.id, bizLocation: staging.id });
|
|
@@ -5999,7 +6305,7 @@ var DWELL_MS = 3e4;
|
|
|
5999
6305
|
var WINDOW_DELAY_MS = 4e4;
|
|
6000
6306
|
var WINDOW_LENGTH_MS = 20 * 6e4;
|
|
6001
6307
|
var CARGO_PER_TRAILER = 2;
|
|
6002
|
-
var
|
|
6308
|
+
var LEGACY_CP = "0614141";
|
|
6003
6309
|
var YmsKernel = class extends FlowEngine {
|
|
6004
6310
|
doorRR = 0;
|
|
6005
6311
|
cargoSeq = 0;
|
|
@@ -6029,18 +6335,18 @@ var YmsKernel = class extends FlowEngine {
|
|
|
6029
6335
|
const doors = [...this.locations.values()].filter((n) => n.type === "dock-door").sort((a, b) => a.id.localeCompare(b.id));
|
|
6030
6336
|
if (!gate || doors.length === 0) return;
|
|
6031
6337
|
const inbound = kind === "appointment";
|
|
6032
|
-
const epc = graiUri(
|
|
6338
|
+
const epc = graiUri(LEGACY_CP, "10", ++this.epcSeq);
|
|
6033
6339
|
this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
|
|
6034
6340
|
gate.occupancy++;
|
|
6035
6341
|
this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: YARD_BIZSTEP.arriving, disposition: DISP.in_progress, epcList: [epc], readPoint: gate.id, bizLocation: gate.id }));
|
|
6036
6342
|
if (inbound) {
|
|
6037
|
-
const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(
|
|
6343
|
+
const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(LEGACY_CP, ++this.cargoSeq));
|
|
6038
6344
|
this.trailerCargo.set(epc, cargo);
|
|
6039
6345
|
this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id });
|
|
6040
6346
|
}
|
|
6041
6347
|
const door = doors[this.doorRR++ % doors.length];
|
|
6042
6348
|
const id = `order-${++this.orderSeq}`;
|
|
6043
|
-
const appt = gdtiUri(
|
|
6349
|
+
const appt = gdtiUri(LEGACY_CP, "404", ++this.soSeq);
|
|
6044
6350
|
const order = {
|
|
6045
6351
|
id,
|
|
6046
6352
|
kind,
|
|
@@ -6120,7 +6426,7 @@ var YmsKernel = class extends FlowEngine {
|
|
|
6120
6426
|
const to = this.locations.get(t.toNode);
|
|
6121
6427
|
from.occupancy--;
|
|
6122
6428
|
to.occupancy++;
|
|
6123
|
-
trailer
|
|
6429
|
+
this.items.relocate(itemKeyOf(trailer), to.id);
|
|
6124
6430
|
if (t.kind === "dwell") {
|
|
6125
6431
|
const gate = this.locationByType("gate");
|
|
6126
6432
|
const dep = { id: `task-${++this.taskSeq}`, kind: "depart", status: "created", itemEpc: trailer.epc, fromNode: t.toNode, toNode: gate.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "depart", fromNode: t.toNode, toNode: gate.id }, TRAVEL_MS2), orderId: t.orderId };
|
|
@@ -6174,25 +6480,7 @@ var YmsKernel = class extends FlowEngine {
|
|
|
6174
6480
|
var DEFAULT_CYCLE_MS = 4e4;
|
|
6175
6481
|
var DEFAULT_SETUP_MS = 15e3;
|
|
6176
6482
|
var MES_CMD = { changeover: "mes.changeover" };
|
|
6177
|
-
var CP2 = "0614141";
|
|
6178
|
-
var WIP_ITEMREF = "066666";
|
|
6179
|
-
var WIP_GTIN = sgtinClass(CP2, WIP_ITEMREF);
|
|
6180
6483
|
var DEFAULT_YIELD = 0.8;
|
|
6181
|
-
var PART_A = { itemRef: "055551", gtin: sgtinClass(CP2, "055551") };
|
|
6182
|
-
var PART_B = { itemRef: "055552", gtin: sgtinClass(CP2, "055552") };
|
|
6183
|
-
var PRODUCTS = [
|
|
6184
|
-
{ key: "P1", ref: "077777", gtin: sgtinClass(CP2, "077777"), bom: [{ part: PART_A, qty: 2 }, { part: PART_B, qty: 1 }] },
|
|
6185
|
-
{ key: "P2", ref: "077778", gtin: sgtinClass(CP2, "077778"), bom: [{ part: PART_A, qty: 1 }, { part: PART_B, qty: 2 }] }
|
|
6186
|
-
];
|
|
6187
|
-
var MES_PART_GTINS = { partA: PART_A.gtin, partB: PART_B.gtin };
|
|
6188
|
-
var MES_PRODUCT_GTINS = { p1: PRODUCTS[0].gtin, p2: PRODUCTS[1].gtin };
|
|
6189
|
-
var MES_PRODUCTS = PRODUCTS.map((p) => ({ gtin: p.gtin, label: p.key }));
|
|
6190
|
-
var ROUTE = [
|
|
6191
|
-
{ kind: "cut", locationType: "cut-station", resource: "cutter" },
|
|
6192
|
-
{ kind: "weld", locationType: "weld-station", resource: "welder" },
|
|
6193
|
-
{ kind: "paint", locationType: "paint-booth", resource: "painter" },
|
|
6194
|
-
{ kind: "assembly", locationType: "assembly-line", resource: "assembler" }
|
|
6195
|
-
];
|
|
6196
6484
|
var MesKernel = class extends FlowEngine {
|
|
6197
6485
|
wipSeq = 0;
|
|
6198
6486
|
prodSeq = 0;
|
|
@@ -6203,13 +6491,15 @@ var MesKernel = class extends FlowEngine {
|
|
|
6203
6491
|
this.productionSpec = productionSpec;
|
|
6204
6492
|
if (productionSpec?.definition?.operations) {
|
|
6205
6493
|
this.loadOperations(productionSpec.definition.operations);
|
|
6206
|
-
this.assertNoDoubleProduction(productionSpec.definition.operations);
|
|
6207
6494
|
}
|
|
6208
6495
|
if (productionSpec?.definition?.recipes?.length && (!productionSpec.binding || !productionSpec.companyPrefix)) {
|
|
6209
6496
|
throw new Error(
|
|
6210
6497
|
"recipe-driven production needs `binding` and `companyPrefix` \u2014 the definition does not know GTINs, so material keys cannot be resolved to GS1 identifiers"
|
|
6211
6498
|
);
|
|
6212
6499
|
}
|
|
6500
|
+
if (productionSpec?.definition?.operations) {
|
|
6501
|
+
this.assertNoDoubleProduction(productionSpec.definition.operations);
|
|
6502
|
+
}
|
|
6213
6503
|
}
|
|
6214
6504
|
/**
|
|
6215
6505
|
* **산출을 두 곳에서 만들지 않는다** — 기동 때 막는다.
|
|
@@ -6224,14 +6514,69 @@ var MesKernel = class extends FlowEngine {
|
|
|
6224
6514
|
* 모든 계산이 거짓이 된다).
|
|
6225
6515
|
*/
|
|
6226
6516
|
assertNoDoubleProduction(ops) {
|
|
6227
|
-
const
|
|
6517
|
+
const finals = new Set((this.productionSpec?.definition?.recipes ?? []).flatMap((r) => (r.outputs ?? []).map((o) => this.classOf(o.material))));
|
|
6518
|
+
const bad2 = ops.filter((o) => (o.materialSpecification ?? []).some((m) => m.use === "produced" && m.materialDefinition && finals.has(m.materialDefinition))).map((o) => o.key);
|
|
6228
6519
|
if (!bad2.length) return;
|
|
6229
6520
|
throw new Error(
|
|
6230
|
-
`MES recipe already produces
|
|
6521
|
+
`MES recipe already produces its output \u2014 operations [${bad2.join(", ")}] must not also declare that same material as materialSpecification use:'produced' (that would create the same output twice). Declare intermediate outputs freely; the final output belongs to the recipe.`
|
|
6231
6522
|
);
|
|
6232
6523
|
}
|
|
6233
|
-
|
|
6234
|
-
|
|
6524
|
+
/**
|
|
6525
|
+
* **레시피 모드에서는 MES 가 산출을 소유한다** — 코어는 비켜선다(§`producesOwnOutputs`).
|
|
6526
|
+
*
|
|
6527
|
+
* MES 의 산출은 개체 정체성(직렬번호) + 수율 + 오더 계보를 갖는다. 코어의 일반 산출은 비직렬
|
|
6528
|
+
* 클래스+수량이라 그 셋을 표현하지 못한다. 둘이 같은 선언을 보고 각자 만들면 재고가 두 배가 된다.
|
|
6529
|
+
*
|
|
6530
|
+
* 레시피가 없으면(유통가공 등) 소유를 주장하지 않는다 — 그때는 코어의 산출이 맞다.
|
|
6531
|
+
*/
|
|
6532
|
+
producesOwnOutputs(_opKey) {
|
|
6533
|
+
return !!this.productionSpec?.definition?.recipes?.length;
|
|
6534
|
+
}
|
|
6535
|
+
/**
|
|
6536
|
+
* 이 트윈의 정체성 근거 — 선언에서 파생한다(§`identityGroundingOf`).
|
|
6537
|
+
*
|
|
6538
|
+
* 선언이 근거를 정한다: 이름공간을 선언했으면 `declared`, 발급을 주장했으면 `issued`(주장이지 사실이
|
|
6539
|
+
* 아니다). 선언이 없는 MES 트윈은 이제 만들 수 없으므로(`loadTwinModel` 이 거절한다) `fabricated` 로
|
|
6540
|
+
* 판정될 일이 없다.
|
|
6541
|
+
*/
|
|
6542
|
+
identityGroundingView() {
|
|
6543
|
+
return identityGroundingOf(this.productionSpec);
|
|
6544
|
+
}
|
|
6545
|
+
/**
|
|
6546
|
+
* 선언이 말하는 자리 타입이 **이 트윈에 있나** — 로드 시점에 한 번, 모아서 말한다 (2026-08-20).
|
|
6547
|
+
*
|
|
6548
|
+
* ── 왜 로드 시점인가 ───────────────────────────────────────────────────────
|
|
6549
|
+
* 이것은 **선언의 흠**이다: 그 트윈의 생산 선언과 그 트윈의 모델이 서로 어긋났다. 틱에서 발견하면
|
|
6550
|
+
* 그 트윈은 「도는데 아무 일도 안 일어나는」 모습이 된다 — 실제로 `allocateDef` 가 첫 공정의 자리를
|
|
6551
|
+
* 못 찾으면 조용히 `return` 했고, 오더는 영원히 배정되지 않은 채 화면에는 「running」이라 적혔다.
|
|
6552
|
+
*
|
|
6553
|
+
* ── 좁히지 않는다 ─────────────────────────────────────────────────────────
|
|
6554
|
+
* 「이 트윈이 실제로 지나는 라우트만 본다」는 예외를 한 번 검토했다가 버렸다. 근거로 삼은 「여러
|
|
6555
|
+
* 트윈이 한 선언을 공유한다」가 **이 코드에 없는 이야기**였다: 호스트는 트윈마다 자기 모델에서
|
|
6556
|
+
* 선언을 만든다(`productionSpecOf(model)`). 없는 사정을 상상해 검사에 구멍을 내지 않는다.
|
|
6557
|
+
*
|
|
6558
|
+
* 모아서 한 번에 말한다 — 하나씩 실패하면 사람이 같은 기동을 열 번 반복한다.
|
|
6559
|
+
*/
|
|
6560
|
+
loadTwinModel(def) {
|
|
6561
|
+
super.loadTwinModel(def);
|
|
6562
|
+
if (!this.productionSpec) {
|
|
6563
|
+
throw new Error(
|
|
6564
|
+
"MES twin requires a production declaration (ProductionSpec): materials, routes and recipes. The kernel no longer carries a built-in demo product \u2014 what is made is decided by the model."
|
|
6565
|
+
);
|
|
6566
|
+
}
|
|
6567
|
+
const have = new Set([...this.locations.values()].map((l) => l.type));
|
|
6568
|
+
const missing = [];
|
|
6569
|
+
for (const m of this.productionSpec.definition.materials ?? []) {
|
|
6570
|
+
if (m.locationType && !have.has(m.locationType)) missing.push(`material '${m.key}' \u2192 '${m.locationType}'`);
|
|
6571
|
+
}
|
|
6572
|
+
for (const op of this.productionSpec.definition.operations ?? []) {
|
|
6573
|
+
if (op.locationType && !have.has(op.locationType)) missing.push(`operation '${op.key}' \u2192 '${op.locationType}'`);
|
|
6574
|
+
}
|
|
6575
|
+
if (missing.length) {
|
|
6576
|
+
throw new Error(
|
|
6577
|
+
`production declaration names location types this twin does not have: ${missing.join(" \xB7 ")}. This twin has [${[...have].sort().join(", ")}]. Declare those locations in the model, or fix the declared locationType \u2014 the kernel does not invent a place for material to sit.`
|
|
6578
|
+
);
|
|
6579
|
+
}
|
|
6235
6580
|
}
|
|
6236
6581
|
/**
|
|
6237
6582
|
* MES 도메인 커맨드(Tier 2) — mes.changeover: 설비를 제품 gtin 으로 강제 전환.
|
|
@@ -6254,107 +6599,55 @@ var MesKernel = class extends FlowEngine {
|
|
|
6254
6599
|
}
|
|
6255
6600
|
return super.handleCommand(cmd);
|
|
6256
6601
|
}
|
|
6257
|
-
/** 부품 수령
|
|
6602
|
+
/** 부품 수령 — 도착한 품목이 선언된 입력 자재면 그 자재가 선언한 자리에 생성. */
|
|
6258
6603
|
onArrival(spec) {
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
const gtin = this.pickGtin(spec.content.skuMix);
|
|
6263
|
-
const part = [PART_A, PART_B].find((p) => p.gtin === gtin);
|
|
6264
|
-
if (!part) return;
|
|
6265
|
-
const epc = sgtinUri(CP2, part.itemRef, ++this.epcSeq);
|
|
6266
|
-
this.items.set(epc, { epc, gtin: part.gtin, qty: 1, location: rawStore.id, disposition: DISP.sellable });
|
|
6267
|
-
rawStore.occupancy++;
|
|
6268
|
-
this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.receiving, disposition: DISP.sellable, epcList: [epc], quantityList: [{ epcClass: part.gtin, quantity: 1 }], readPoint: rawStore.id, bizLocation: rawStore.id }));
|
|
6269
|
-
}
|
|
6270
|
-
/** 작업지시 — 제품 2종 교대(체인지오버 유발). 제품 gtin 을 오더에 기록. */
|
|
6604
|
+
return this.onArrivalDef(spec);
|
|
6605
|
+
}
|
|
6606
|
+
/** 작업지시 — 오더가 자기 레시피를 든다(선언된 레시피 중에서). */
|
|
6271
6607
|
onOrder(_spec) {
|
|
6272
|
-
|
|
6273
|
-
const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
|
|
6274
|
-
const id = `order-${++this.orderSeq}`;
|
|
6275
|
-
const wo = gdtiUri(CP2, "403", ++this.soSeq);
|
|
6276
|
-
const order = { id, kind: "workorder", status: "created", gtin: product.gtin, requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
|
|
6277
|
-
this.orders.set(id, order);
|
|
6278
|
-
this.emitOrder(order);
|
|
6608
|
+
return this.onOrderDef(_spec);
|
|
6279
6609
|
}
|
|
6280
|
-
/** 할당 —
|
|
6610
|
+
/** 할당 — 레시피 입력 전량 확보 후 첫 라우트 단계 태스크(부족하면 대기). */
|
|
6281
6611
|
allocate(o) {
|
|
6282
|
-
|
|
6283
|
-
const s0 = ROUTE[0];
|
|
6284
|
-
const first = this.locationByType(s0.locationType);
|
|
6285
|
-
const product = this.productOf(o.gtin);
|
|
6286
|
-
if (!first || !product) return;
|
|
6287
|
-
const picks = [];
|
|
6288
|
-
for (const line of product.bom) {
|
|
6289
|
-
const available = [...this.items.values()].filter((i) => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === "raw-store").map((i) => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
6290
|
-
const chosen = this.policy.selectStock({ gtin: line.part.gtin, qty: line.qty, available });
|
|
6291
|
-
if (chosen.length < line.qty) return;
|
|
6292
|
-
picks.push(...chosen);
|
|
6293
|
-
}
|
|
6294
|
-
for (const epc of picks) o.allocated.push(epc);
|
|
6295
|
-
this.reserve(picks, MES_BIZSTEP.producing);
|
|
6296
|
-
this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
|
|
6297
|
-
this.emitStation(o, s0, o.allocated[0], product.gtin);
|
|
6298
|
-
o.status = "op-" + s0.kind;
|
|
6299
|
-
this.emitOrder(o);
|
|
6300
|
-
}
|
|
6301
|
-
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
6302
|
-
emitStation(o, stage, itemEpc, changeoverKey) {
|
|
6303
|
-
const loc = this.locationByType(stage.locationType);
|
|
6304
|
-
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: "created", itemEpc, fromNode: loc.id, toNode: loc.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: stage.kind, fromNode: loc.id, toNode: loc.id, resourceKind: stage.resource }, DEFAULT_CYCLE_MS), orderId: o.id, resourceType: stage.resource, changeoverKey, setupMs: this.paramDuration(stage.kind, OP_PARAM.setupDuration) ?? DEFAULT_SETUP_MS, intent: "process" };
|
|
6305
|
-
this.tasks.set(task.id, task);
|
|
6306
|
-
this.emitTask(task);
|
|
6612
|
+
return this.allocateDef(o);
|
|
6307
6613
|
}
|
|
6308
|
-
/** op 완료 = 변환. 라우트 인덱스로 분기: 첫=BOM 소비→WIP, 중간=WIP→WIP, 마지막(조립)=WIP→완성차(수율→OEE 품질). */
|
|
6309
6614
|
/**
|
|
6310
6615
|
* MES 는 완료 시점에 **오더와 제품 정의**를 딛고 선다 — 둘 중 하나만 없어도 끝맺을 수 없다.
|
|
6311
6616
|
*
|
|
6312
6617
|
* 씨앗이 이미 이행된 오더를 주입하지 않으므로(남은 수량 0) 그 오더에 딸린 작업만 남을 수 있고,
|
|
6313
|
-
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서
|
|
6618
|
+
* 관측이 오더 연결을 담지 못한 작업도 있다. 그때 예전에는 `order.gtin` 에서 오류가 났고 **그 예외
|
|
6314
6619
|
* 하나가 정확도 추세 전체를 죽였다.** 이제 코어가 이 답을 보고 그 작업만 접는다.
|
|
6315
6620
|
*/
|
|
6316
6621
|
canComplete(t) {
|
|
6317
|
-
|
|
6318
|
-
const order = t.orderId ? this.orders.get(t.orderId) : void 0;
|
|
6319
|
-
return !!order && !!this.productOf(order.gtin);
|
|
6622
|
+
return !!(t.orderId && this.orders.get(t.orderId));
|
|
6320
6623
|
}
|
|
6321
6624
|
onTaskComplete(t) {
|
|
6322
|
-
|
|
6323
|
-
const order = this.orders.get(t.orderId);
|
|
6324
|
-
const product = order && this.productOf(order.gtin);
|
|
6325
|
-
if (!order || !product) throw new Error(`task ${t.id}: order/product vanished between the core check and the domain hook`);
|
|
6326
|
-
const i = ROUTE.findIndex((s) => s.kind === t.kind);
|
|
6327
|
-
const loc = this.locations.get(t.toNode);
|
|
6328
|
-
const isLast = i === ROUTE.length - 1;
|
|
6329
|
-
if (!isLast) {
|
|
6330
|
-
const inputs = order.allocated.slice();
|
|
6331
|
-
const wip2 = sgtinUri(CP2, WIP_ITEMREF, ++this.wipSeq);
|
|
6332
|
-
this.transform(inputs, [{ epc: wip2, gtin: WIP_GTIN, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep: MES_BIZSTEP.producing, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
6333
|
-
order.allocated = [wip2];
|
|
6334
|
-
const next = ROUTE[i + 1];
|
|
6335
|
-
this.emitStation(order, next, wip2, product.gtin);
|
|
6336
|
-
order.status = "op-" + next.kind;
|
|
6337
|
-
this.emitOrder(order);
|
|
6338
|
-
return;
|
|
6339
|
-
}
|
|
6340
|
-
const fgStore = this.locationByType("fg-store");
|
|
6341
|
-
const wip = order.allocated[0];
|
|
6342
|
-
const good = this.rng() < (this.yieldOf(t.kind) ?? DEFAULT_YIELD);
|
|
6343
|
-
t.outcome = good ? "good" : "scrap";
|
|
6344
|
-
this.recordOutput(t.resource, good);
|
|
6345
|
-
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
6346
|
-
const outputEpc = sgtinUri(CP2, product.ref, ++this.prodSeq);
|
|
6347
|
-
this.transform([wip], [{ epc: outputEpc, gtin: product.gtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep: MES_BIZSTEP.producing, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
6348
|
-
this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outputEpc], quantityList: [{ epcClass: product.gtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
|
|
6349
|
-
order.allocated = [];
|
|
6350
|
-
order.fulfilled = 1;
|
|
6351
|
-
order.status = good ? "produced" : "scrapped";
|
|
6352
|
-
this.emitOrder(order);
|
|
6625
|
+
return this.onTaskCompleteDef(t);
|
|
6353
6626
|
}
|
|
6354
6627
|
// ── 정의-구동 모드 (도메인 정의 데이터로 실행 — 레거시와 분리, 하드코딩 대체) ──
|
|
6355
|
-
|
|
6628
|
+
/** 선언된 레시피 전부 — 오더가 자기 것을 고르고, 수령이 전부의 소요를 본다. */
|
|
6629
|
+
recipesDef() {
|
|
6630
|
+
return this.productionSpec.definition.recipes ?? [];
|
|
6631
|
+
}
|
|
6632
|
+
/**
|
|
6633
|
+
* 이 오더의 레시피 — **오더가 들면 그것, 없으면 선언 수준의 기본**(§`FlowOrder.recipeKey`).
|
|
6634
|
+
*
|
|
6635
|
+
* 오더가 든 키가 선언에 없으면 **말한다**: 조용히 첫 레시피로 떨어지면 그 오더는 다른 물건을 만들고,
|
|
6636
|
+
* 그 뒤 계보·재고·수율이 전부 엉뚱한 품목에 붙는다.
|
|
6637
|
+
*/
|
|
6638
|
+
recipeDef(order) {
|
|
6356
6639
|
const d = this.productionSpec.definition;
|
|
6357
|
-
|
|
6640
|
+
const key = order?.recipeKey ?? this.productionSpec.recipeKey;
|
|
6641
|
+
if (key) {
|
|
6642
|
+
const found = d.recipes?.find((r) => r.key === key);
|
|
6643
|
+
if (!found) {
|
|
6644
|
+
throw new Error(
|
|
6645
|
+
`recipe '${key}' is not declared. Declared: [${(d.recipes ?? []).map((r) => r.key).join(", ") || "(none)"}]. The kernel does not substitute another recipe \u2014 that would make this order produce a different item.`
|
|
6646
|
+
);
|
|
6647
|
+
}
|
|
6648
|
+
return found;
|
|
6649
|
+
}
|
|
6650
|
+
return d.recipes[0];
|
|
6358
6651
|
}
|
|
6359
6652
|
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
6360
6653
|
classOf(materialKey) {
|
|
@@ -6363,54 +6656,180 @@ var MesKernel = class extends FlowEngine {
|
|
|
6363
6656
|
serialOf(materialKey, serial) {
|
|
6364
6657
|
return sgtinUri(this.productionSpec.companyPrefix, this.productionSpec.binding[materialKey], serial);
|
|
6365
6658
|
}
|
|
6659
|
+
/**
|
|
6660
|
+
* 이 공정이 **만든다고 선언한 품목** — 표준 `OpMaterialSpecification` `MaterialUse='produced'`.
|
|
6661
|
+
*
|
|
6662
|
+
* ── 왜 이 함수가 생겼나 (2026-08-20) ──────────────────────────────────────
|
|
6663
|
+
* 예전에는 라우트의 중간 단계마다 커널이 `sgtinUri(prefix, 'WIP', ++seq)` 로 **품목을 지어냈다.**
|
|
6664
|
+
* 그러면 배합물·가열완료물·소분물이 전부 같은 상품코드가 된다 — 저널에 남는 것은 「무엇인지 아무도
|
|
6665
|
+
* 답할 수 없는 물건」이고, 그것이 영구 기록이 된다. 게다가 그 조립은 `binding` 을 **건너뛴다**:
|
|
6666
|
+
* 다른 모든 품목은 `classOf`/`serialOf` 로 인스턴스가 주입한 품번을 지나는데, `'WIP'` 만 커널이
|
|
6667
|
+
* itemRef 자리에 리터럴을 넣었다.
|
|
6668
|
+
*
|
|
6669
|
+
* ── 규율 ──────────────────────────────────────────────────────────────────
|
|
6670
|
+
* 만드는 것은 **선언이 정한다**. 선언이 없으면 만들지 않는다(§`produceMaterials` 와 같은 규율 —
|
|
6671
|
+
* 「무엇을 만드는지 말하지 않으면 만들지 않는다」). 있으면 그 품목으로 만든다. 어느 쪽도 지어내지 않는다.
|
|
6672
|
+
*
|
|
6673
|
+
* 둘 이상을 선언하면 **거부한다**: 어느 것이 이 단계의 산출인지는 모델이 말할 일이고, 커널이 첫 줄을
|
|
6674
|
+
* 고르면 나머지는 조용히 사라진다.
|
|
6675
|
+
*/
|
|
6676
|
+
producedMaterialKeyOf(opKey) {
|
|
6677
|
+
const made = (this.operationSpecs.get(opKey)?.materialSpecification ?? []).filter((m) => m.use === "produced");
|
|
6678
|
+
if (!made.length) return void 0;
|
|
6679
|
+
if (made.length > 1) {
|
|
6680
|
+
throw new Error(
|
|
6681
|
+
`operation '${opKey}' declares ${made.length} produced materials \u2014 the kernel does not choose one. Declare exactly one 'produced' material for this step, or split the step.`
|
|
6682
|
+
);
|
|
6683
|
+
}
|
|
6684
|
+
const declared = made[0].materialDefinition;
|
|
6685
|
+
if (!declared) {
|
|
6686
|
+
return void 0;
|
|
6687
|
+
}
|
|
6688
|
+
return this.materialKeyOfClass(declared, opKey);
|
|
6689
|
+
}
|
|
6690
|
+
/**
|
|
6691
|
+
* 표준 GTIN 클래스 → **모델의 자재 키.**
|
|
6692
|
+
*
|
|
6693
|
+
* 공정 명세는 표준 식별자(`MaterialDefinitionID` = GTIN idpat)로 품목을 가리키고, MES 프로파일의
|
|
6694
|
+
* 나머지는 자재 키(`MaterialDef.key` + `binding`)로 가리킨다. **둘은 다른 층이고 `binding` 이 통로다.**
|
|
6695
|
+
* 그래서 선언된 식별자가 이 트윈의 `binding` 을 지나 나오는 것인지 되짚어 확인한다.
|
|
6696
|
+
*
|
|
6697
|
+
* 없으면 **모델 위반이므로 멈춘다** — 커널이 바인딩 없는 품목의 정체성을 만들어 낼 자리가 없다.
|
|
6698
|
+
*/
|
|
6699
|
+
materialKeyOfClass(gtinClass, opKey) {
|
|
6700
|
+
const binding = this.productionSpec.binding;
|
|
6701
|
+
const key = Object.keys(binding).find((k) => this.classOf(k) === gtinClass);
|
|
6702
|
+
if (!key) {
|
|
6703
|
+
throw new Error(
|
|
6704
|
+
`operation '${opKey}' declares produced material '${gtinClass}', which no declared material binds to. Known: ${Object.keys(binding).map((k) => `${k}=${this.classOf(k)}`).join(", ") || "(none)"}. Declare that material in the model with a binding \u2014 the kernel does not invent an identity for it.`
|
|
6705
|
+
);
|
|
6706
|
+
}
|
|
6707
|
+
return key;
|
|
6708
|
+
}
|
|
6709
|
+
/**
|
|
6710
|
+
* 이 자재가 놓이는 **자리 타입** — 선언이 권위다(`MaterialDef.locationType`).
|
|
6711
|
+
*
|
|
6712
|
+
* 예전에는 이 자리에 `'raw-store'`·`'fg-store'` 가 박혀 있었다. 그래서 현장은 자기 창고를 그 이름으로
|
|
6713
|
+
* **개명해야** 트윈이 굴러갔다 — 커널이 현장의 낱말을 정하는 셈이었다. 선언이 없으면 **지어내지 않고
|
|
6714
|
+
* 말한다**: 예전 거동(조용한 return)에서는 아무도 왜 자재가 들어오지 않는지 알 수 없었다.
|
|
6715
|
+
*
|
|
6716
|
+
* 레거시(선언 없는 내장 프로파일) 경로는 이 함수를 쓰지 않는다 — 그쪽에는 대조할 선언이 없으므로
|
|
6717
|
+
* 커널 어휘 자체가 계약이다.
|
|
6718
|
+
*/
|
|
6719
|
+
locationTypeOfMaterial(materialKey) {
|
|
6720
|
+
const type = this.productionSpec.definition.materials?.find((m) => m.key === materialKey)?.locationType;
|
|
6721
|
+
if (!type) throw new Error(`material '${materialKey}': MaterialDef.locationType \uBBF8\uC120\uC5B8 \u2014 \uC790\uC7AC\uAC00 \uB193\uC774\uB294 \uC790\uB9AC\uB97C \uCEE4\uB110\uC774 \uC9C0\uC5B4\uB0BC \uC218 \uC5C6\uB2E4`);
|
|
6722
|
+
return type;
|
|
6723
|
+
}
|
|
6724
|
+
/** 그 자리 타입의 자리 — 이 트윈에 없으면 말한다(선언과 모델이 어긋난 사실이다). */
|
|
6725
|
+
locationOfMaterial(materialKey) {
|
|
6726
|
+
const type = this.locationTypeOfMaterial(materialKey);
|
|
6727
|
+
const loc = this.locationByType(type);
|
|
6728
|
+
if (!loc) throw new Error(`material '${materialKey}': locationType '${type}' \uC778 \uC790\uB9AC\uAC00 \uC774 \uD2B8\uC708\uC5D0 \uC5C6\uB2E4`);
|
|
6729
|
+
return loc;
|
|
6730
|
+
}
|
|
6366
6731
|
/**
|
|
6367
6732
|
* 라우트를 용량 계산에 알려 준다 — 수율을 거슬러 올릴 때 순서가 곧 계산이다.
|
|
6368
6733
|
*
|
|
6369
6734
|
* 순서를 모르면 선언 순서대로 세는데, 그러면 하류에서 잃는 몫이 엉뚱한 공정에 얹힌다(조용히
|
|
6370
6735
|
* 틀린다). 생산 정의를 가진 커널만 이 답을 안다.
|
|
6371
6736
|
*/
|
|
6737
|
+
/*
|
|
6738
|
+
* ── 레시피가 여럿이면 「그 라우트」가 하나가 아니다 (2026-08-20) ────────────
|
|
6739
|
+
* 용량은 트윈 수준의 질문이다(「이 공장이 하루 몇 대」). 레시피마다 라우트가 다르면 그 답이 제품
|
|
6740
|
+
* 구성에 따라 달라지는데, **하나를 골라 답하면 능력 숫자가 조용히 거짓이 된다.** 그래서 갈릴 때는
|
|
6741
|
+
* 답하지 않고(`undefined`) 서로 다르다는 사실을 `capacity()` 가 함께 낸다 — `mixedCalendars` 와 같은 규율.
|
|
6742
|
+
*/
|
|
6372
6743
|
routeKeys() {
|
|
6373
6744
|
if (!this.productionSpec) return void 0;
|
|
6374
|
-
const
|
|
6375
|
-
|
|
6745
|
+
const routes = this.distinctRouteKeys();
|
|
6746
|
+
if (routes.length !== 1) return void 0;
|
|
6747
|
+
return this.productionSpec.definition.routes?.find((r) => r.key === routes[0])?.steps;
|
|
6376
6748
|
}
|
|
6377
|
-
/**
|
|
6378
|
-
|
|
6749
|
+
/** 선언된 레시피들이 쓰는 **서로 다른** 라우트 키들. 하나면 용량을 답할 수 있다. */
|
|
6750
|
+
distinctRouteKeys() {
|
|
6751
|
+
if (!this.productionSpec) return [];
|
|
6752
|
+
return [...new Set(this.recipesDef().map((r) => r.route).filter((k) => !!k))];
|
|
6753
|
+
}
|
|
6754
|
+
/** recipe.route → 오퍼레이션 시퀀스 해소. 오더가 자기 레시피를 들면 그 라우트다. */
|
|
6755
|
+
routeOps(order) {
|
|
6379
6756
|
const d = this.productionSpec.definition;
|
|
6380
|
-
const route = d.routes?.find((r) => r.key === this.recipeDef().route);
|
|
6757
|
+
const route = d.routes?.find((r) => r.key === this.recipeDef(order).route);
|
|
6381
6758
|
return (route?.steps ?? []).map((sk) => d.operations?.find((o) => o.key === sk)).filter((o) => !!o);
|
|
6382
6759
|
}
|
|
6383
|
-
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면
|
|
6760
|
+
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 **그 자재가 선언한 자리**에 생성. */
|
|
6384
6761
|
onArrivalDef(spec) {
|
|
6385
|
-
const rawStore = this.locationByType("raw-store");
|
|
6386
|
-
if (!rawStore) return;
|
|
6387
6762
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
6388
6763
|
if (!gtin) return;
|
|
6389
|
-
const inputKey = this.
|
|
6764
|
+
const inputKey = this.recipesDef().flatMap((r) => r.inputs.map((i) => i.material)).find((k) => this.classOf(k) === gtin);
|
|
6390
6765
|
if (!inputKey) return;
|
|
6766
|
+
const store = this.locationOfMaterial(inputKey);
|
|
6391
6767
|
const epc = this.serialOf(inputKey, ++this.epcSeq);
|
|
6392
|
-
this.items.set(epc, { epc, gtin, qty: 1, location:
|
|
6393
|
-
|
|
6394
|
-
this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.receiving, disposition: DISP.sellable, epcList: [epc], quantityList: [{ epcClass: gtin, quantity: 1 }], readPoint:
|
|
6768
|
+
this.items.set(epc, { epc, gtin, qty: 1, location: store.id, disposition: DISP.sellable });
|
|
6769
|
+
store.occupancy++;
|
|
6770
|
+
this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.receiving, disposition: DISP.sellable, epcList: [epc], quantityList: [{ epcClass: gtin, quantity: 1 }], readPoint: store.id, bizLocation: store.id }));
|
|
6771
|
+
}
|
|
6772
|
+
/**
|
|
6773
|
+
* 정의 모드 작업지시 — 레시피 산출물 1개. **오더가 자기 레시피를 든다**(§`FlowOrder.recipeKey`).
|
|
6774
|
+
*
|
|
6775
|
+
* 선언 수준의 `recipeKey` 가 있으면 그 하나만 만든다(현장이 한 품목만 돌린다고 말한 것이다). 없으면
|
|
6776
|
+
* 선언된 레시피를 **차례로** 돈다 — 그것이 제품 전환이고, 체인지오버 셋업은 `changeoverKey`(산출물
|
|
6777
|
+
* 클래스)가 달라지는 것으로 자동으로 따라온다. 커널 상수 제품 목록이 필요했던 이유가 이것이었다.
|
|
6778
|
+
*
|
|
6779
|
+
* 차례는 `orderSeq` 로 정한다 — 난수를 쓰면 fork 예측이 결정적이지 않게 된다.
|
|
6780
|
+
*/
|
|
6781
|
+
/**
|
|
6782
|
+
* **작업지시의 거래 문서 식별자** — 선언이 정하고, 커널은 문서 타입을 정하지 않는다.
|
|
6783
|
+
*
|
|
6784
|
+
* ── 무엇이 틀렸나 (2026-08-21) ────────────────────────────────────────────
|
|
6785
|
+
* 여기는 `gdtiUri(prefix, '403', seq)` 였다. GDTI 는 `회사 프리픽스 + 문서 타입 + 일련번호`이고
|
|
6786
|
+
* **문서 타입은 GS1 이 공표하는 목록이 아니다** — 프리픽스를 배정받은 회사가 자기 번호 용량에서
|
|
6787
|
+
* 정한다. 그런데 커널이 `'403'` 을 스스로 정해 저널에 영구히 기록했다. 회사의 배정 권한을 커널이
|
|
6788
|
+
* 대신 행사한 것이고, 프리픽스 날조와 같은 종류다.
|
|
6789
|
+
*
|
|
6790
|
+
* ── 순서 ──────────────────────────────────────────────────────────────────
|
|
6791
|
+
* ① 선언된 이름공간이 있으면 그 아래 `bt` 형태로 만든다(CBV §8.5.4·§8.5.5). **문서 타입이 필요
|
|
6792
|
+
* 없는 길**이고, 회사가 도메인만 있으면 되므로 진입장벽이 가장 낮다.
|
|
6793
|
+
* ② 없고 문서 타입을 선언했으면 GDTI 로 만든다(그 현장이 GDTI 를 쓰는 것이다).
|
|
6794
|
+
* ③ 둘 다 없으면 **말한다.** 지어내면 그 값이 영구 기록이 되고, 어느 화면도 그것이 발명된
|
|
6795
|
+
* 번호라고 말해 주지 않는다.
|
|
6796
|
+
*/
|
|
6797
|
+
workOrderId(serial) {
|
|
6798
|
+
const identity = this.productionSpec.identity;
|
|
6799
|
+
for (const ns of identity?.namespaces ?? []) {
|
|
6800
|
+
const uri = bizTransactionUri(ns, serial);
|
|
6801
|
+
if (uri) return uri;
|
|
6802
|
+
}
|
|
6803
|
+
const docType = identity?.documentTypes?.workorder;
|
|
6804
|
+
if (docType) return gdtiUri(this.productionSpec.companyPrefix, docType, serial);
|
|
6805
|
+
throw new Error(
|
|
6806
|
+
"work order needs an identifier the model declares: either `identity.namespaces` (CBV \xA78.5.4/\xA78.5.5 `.../bt/<id>`) or `identity.documentTypes.workorder` (the GDTI document type this site assigns). The kernel does not choose a document type \u2014 GS1 assigns the company prefix, the company assigns the type."
|
|
6807
|
+
);
|
|
6395
6808
|
}
|
|
6396
|
-
/** 정의 모드 작업지시 — 레시피 산출물 1개. */
|
|
6397
6809
|
onOrderDef(_spec) {
|
|
6398
|
-
const rc = this.recipeDef();
|
|
6399
6810
|
const id = `order-${++this.orderSeq}`;
|
|
6400
|
-
const
|
|
6401
|
-
const
|
|
6811
|
+
const recipes = this.recipesDef();
|
|
6812
|
+
const chosen = this.productionSpec.recipeKey ? void 0 : recipes[(this.orderSeq - 1) % recipes.length];
|
|
6813
|
+
const rc = this.recipeDef(chosen ? { recipeKey: chosen.key } : void 0);
|
|
6814
|
+
const wo = this.workOrderId(++this.soSeq);
|
|
6815
|
+
const order = { id, kind: "workorder", status: "created", ...chosen ? { recipeKey: chosen.key } : {}, gtin: this.classOf(rc.outputs[0].material), requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [], ...this.promiseOf(_spec) };
|
|
6402
6816
|
this.orders.set(id, order);
|
|
6403
6817
|
this.emitOrder(order);
|
|
6404
6818
|
}
|
|
6405
6819
|
/** 정의 모드 할당 — 레시피 입력 BOM 전량 확보 후 첫 라우트 스텝 태스크. */
|
|
6406
6820
|
allocateDef(o) {
|
|
6407
|
-
const ops = this.routeOps();
|
|
6408
|
-
if (!ops.length
|
|
6409
|
-
const
|
|
6821
|
+
const ops = this.routeOps(o);
|
|
6822
|
+
if (!ops.length) return;
|
|
6823
|
+
const firstType = ops[0].locationType;
|
|
6824
|
+
if (!firstType) {
|
|
6825
|
+
throw new Error(`route step '${ops[0].key}' declares no locationType \u2014 the kernel cannot choose where work happens`);
|
|
6826
|
+
}
|
|
6827
|
+
const rc = this.recipeDef(o);
|
|
6410
6828
|
const picks = [];
|
|
6411
6829
|
for (const line of rc.inputs) {
|
|
6412
6830
|
const g = this.classOf(line.material);
|
|
6413
|
-
const
|
|
6831
|
+
const fromType = this.locationTypeOfMaterial(line.material);
|
|
6832
|
+
const available = [...this.items.values()].filter((i) => i.gtin === g && i.disposition === DISP.sellable && this.locations.get(i.location)?.type === fromType).map((i) => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
6414
6833
|
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
6415
6834
|
if (chosen.length < line.qty) return;
|
|
6416
6835
|
picks.push(...chosen);
|
|
@@ -6432,33 +6851,51 @@ var MesKernel = class extends FlowEngine {
|
|
|
6432
6851
|
onTaskCompleteDef(t) {
|
|
6433
6852
|
const order = this.orders.get(t.orderId);
|
|
6434
6853
|
if (!order) throw new Error(`task ${t.id}: order vanished between the core check and the domain hook`);
|
|
6435
|
-
const rc = this.recipeDef();
|
|
6436
|
-
const ops = this.routeOps();
|
|
6854
|
+
const rc = this.recipeDef(order);
|
|
6855
|
+
const ops = this.routeOps(order);
|
|
6437
6856
|
const i = ops.findIndex((s) => s.key === t.kind);
|
|
6438
6857
|
const loc = this.locations.get(t.toNode);
|
|
6439
6858
|
const bizStep = ops[i]?.bizStep ?? MES_BIZSTEP.producing;
|
|
6440
6859
|
const isLast = i === ops.length - 1;
|
|
6441
6860
|
if (!isLast) {
|
|
6442
|
-
const
|
|
6443
|
-
const wip2 = sgtinUri(this.productionSpec.companyPrefix, "WIP", ++this.wipSeq);
|
|
6444
|
-
const wipGtin = sgtinClass(this.productionSpec.companyPrefix, "WIP");
|
|
6445
|
-
this.transform(inputs, [{ epc: wip2, gtin: wipGtin, qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
6446
|
-
order.allocated = [wip2];
|
|
6861
|
+
const producedKey = this.producedMaterialKeyOf(t.kind);
|
|
6447
6862
|
const next = ops[i + 1];
|
|
6448
|
-
|
|
6863
|
+
if (producedKey) {
|
|
6864
|
+
const inputs = order.allocated.slice();
|
|
6865
|
+
const made = this.serialOf(producedKey, ++this.wipSeq);
|
|
6866
|
+
this.transform(inputs, [{ epc: made, gtin: this.classOf(producedKey), qty: 1, location: loc.id, disposition: DISP.in_progress }], { bizStep, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
6867
|
+
order.allocated = [made];
|
|
6868
|
+
}
|
|
6869
|
+
const carried = order.allocated[0];
|
|
6870
|
+
if (!carried) {
|
|
6871
|
+
throw new Error(
|
|
6872
|
+
`order ${order.id} at step '${t.kind}': nothing to carry to '${next.key}' \u2014 the order holds no allocated material and this step declares no produced material. Declare the step's input or its output in the model.`
|
|
6873
|
+
);
|
|
6874
|
+
}
|
|
6875
|
+
this.emitStationDef(order, next, carried);
|
|
6449
6876
|
order.status = "op-" + next.key;
|
|
6450
6877
|
this.emitOrder(order);
|
|
6451
6878
|
return;
|
|
6452
6879
|
}
|
|
6453
|
-
const fgStore = this.
|
|
6454
|
-
const
|
|
6880
|
+
const fgStore = this.locationOfMaterial(rc.outputs[0].material);
|
|
6881
|
+
const consumed = order.allocated.slice();
|
|
6882
|
+
if (!consumed.length) {
|
|
6883
|
+
if (order.seedIncomplete) {
|
|
6884
|
+
order.status = "blocked-seed-incomplete";
|
|
6885
|
+
this.emitOrder(order);
|
|
6886
|
+
return;
|
|
6887
|
+
}
|
|
6888
|
+
throw new Error(
|
|
6889
|
+
`order ${order.id} at final step '${t.kind}': nothing allocated to consume \u2014 the kernel does not make a product out of nothing. Declare the recipe inputs, or the step's produced material.`
|
|
6890
|
+
);
|
|
6891
|
+
}
|
|
6455
6892
|
const good = this.rng() < (this.yieldOf(t.kind) ?? DEFAULT_YIELD);
|
|
6456
6893
|
t.outcome = good ? "good" : "scrap";
|
|
6457
6894
|
this.recordOutput(t.resource, good);
|
|
6458
6895
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
6459
6896
|
const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
|
|
6460
6897
|
const outGtin = this.classOf(rc.outputs[0].material);
|
|
6461
|
-
this.transform(
|
|
6898
|
+
this.transform(consumed, [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint: loc.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
6462
6899
|
this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outEpc], quantityList: [{ epcClass: outGtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
|
|
6463
6900
|
order.allocated = [];
|
|
6464
6901
|
order.fulfilled = 1;
|
|
@@ -6752,6 +7189,18 @@ var EmsKernel = class extends FlowEngine {
|
|
|
6752
7189
|
startMs: w.startMs,
|
|
6753
7190
|
endMs: w.endMs,
|
|
6754
7191
|
...w.maxKW !== void 0 ? { maxKW: w.maxKW } : {},
|
|
7192
|
+
/*
|
|
7193
|
+
* ── 선언된 것만의 합도 **사실로 남긴다** (2026-08-20) ────────────────────
|
|
7194
|
+
*
|
|
7195
|
+
* 상태에만 두면 저널을 읽는 쪽(성과·타임라인·요금 화면)은 총합만 본다. 실 서버에서 확인한 모양이
|
|
7196
|
+
* 그랬다: 유령 계량기 300kW 가 섞인 구간이 「계약 1,000 초과 1,100」으로만 남고, 그중 얼마가
|
|
7197
|
+
* 정체 모를 계량기인지 되짚을 길이 없었다. 지나간 구간의 두 수는 나중에 계산할 수 없다 —
|
|
7198
|
+
* 그 순간의 지점별 kW 를 커널이 보관하지 않기 때문이다.
|
|
7199
|
+
*
|
|
7200
|
+
* `overContract` 는 여기서도 총합으로만 판정한다(요금이 그렇게 매겨진다). 선언된 것만으로는
|
|
7201
|
+
* 넘지 않았다는 판단은 **읽는 쪽이** 이 두 수로 한다 — 판정을 두 벌 만들지 않는다.
|
|
7202
|
+
*/
|
|
7203
|
+
...w.maxDeclaredKW !== void 0 ? { maxDeclaredKW: w.maxDeclaredKW } : {},
|
|
6755
7204
|
/* 총부하도 사실로 남긴다 — 이것이 없으면 저널을 읽는 쪽은 「무엇이 깎였나」를 되짚을 수 없다
|
|
6756
7205
|
(요금은 순수요로 매겨지지만, 그 수요를 만든 것은 설비 부하다). */
|
|
6757
7206
|
...w.grossMaxKW !== void 0 ? { grossMaxKW: w.grossMaxKW } : {},
|
|
@@ -7828,13 +8277,11 @@ function retiredVocabularyIn(line) {
|
|
|
7828
8277
|
FlowEngine,
|
|
7829
8278
|
GUARD_PRAGMA,
|
|
7830
8279
|
ILMD_ATTR,
|
|
8280
|
+
ItemStore,
|
|
7831
8281
|
LOCATION_SATURATION_NEAR,
|
|
7832
8282
|
MATERIAL_PROPERTY,
|
|
7833
8283
|
MES_BIZSTEP,
|
|
7834
8284
|
MES_LOCATION_TYPES,
|
|
7835
|
-
MES_PART_GTINS,
|
|
7836
|
-
MES_PRODUCTS,
|
|
7837
|
-
MES_PRODUCT_GTINS,
|
|
7838
8285
|
MES_TYPES,
|
|
7839
8286
|
MesKernel,
|
|
7840
8287
|
OPERATION_PROPERTY,
|
|
@@ -7868,9 +8315,11 @@ function retiredVocabularyIn(line) {
|
|
|
7868
8315
|
axisAppliesTo,
|
|
7869
8316
|
axisInfo,
|
|
7870
8317
|
axisSource,
|
|
8318
|
+
bizTransactionUri,
|
|
7871
8319
|
capabilitiesForType,
|
|
7872
8320
|
capabilityOf,
|
|
7873
8321
|
classClosure,
|
|
8322
|
+
classIdentifierViolation,
|
|
7874
8323
|
commandsOf,
|
|
7875
8324
|
compareStates,
|
|
7876
8325
|
computeOee,
|
|
@@ -7894,6 +8343,7 @@ function retiredVocabularyIn(line) {
|
|
|
7894
8343
|
generationFractionAt,
|
|
7895
8344
|
graiUri,
|
|
7896
8345
|
hierarchyOf,
|
|
8346
|
+
identityGroundingOf,
|
|
7897
8347
|
inWorkCalendar,
|
|
7898
8348
|
inWorkCalendarAt,
|
|
7899
8349
|
ingest,
|