@operato/twin-kernel 0.0.3 → 0.0.5
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/capability.d.ts +50 -0
- package/dist/capability.js +44 -0
- package/dist/contract.d.ts +79 -1
- package/dist/contract.js +11 -2
- package/dist/domain-catalog.d.ts +33 -0
- package/dist/domain-catalog.js +14 -0
- package/dist/domain-definition.d.ts +91 -0
- package/dist/domain-definition.js +76 -0
- package/dist/flow-engine.d.ts +77 -1
- package/dist/flow-engine.js +270 -27
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/mes-kernel.d.ts +48 -4
- package/dist/mes-kernel.js +185 -24
- package/dist/mes-profile.d.ts +5 -0
- package/dist/mes-profile.js +22 -5
- package/dist/state-projector.js +2 -2
- package/dist/wms-profile.d.ts +7 -0
- package/dist/wms-profile.js +14 -5
- package/dist/yms-profile.d.ts +4 -0
- package/dist/yms-profile.js +11 -8
- package/dist-cjs/index.cjs +627 -86
- package/package.json +1 -1
package/dist-cjs/index.cjs
CHANGED
|
@@ -23,14 +23,21 @@ __export(index_exports, {
|
|
|
23
23
|
BTT: () => BTT,
|
|
24
24
|
BTT_DELIVERY: () => BTT_DELIVERY,
|
|
25
25
|
BTT_PRODORDER: () => BTT_PRODORDER,
|
|
26
|
+
CAPABILITIES: () => CAPABILITIES,
|
|
27
|
+
CAPABILITY_KEYS: () => CAPABILITY_KEYS,
|
|
26
28
|
CMD: () => CMD,
|
|
27
29
|
DISP: () => DISP,
|
|
30
|
+
DOMAIN_CATALOG: () => DOMAIN_CATALOG,
|
|
31
|
+
DOMAIN_SYSTEMS: () => DOMAIN_SYSTEMS,
|
|
28
32
|
EPCIS_CONTEXT: () => EPCIS_CONTEXT,
|
|
29
33
|
EventJournal: () => EventJournal,
|
|
30
34
|
FlowEngine: () => FlowEngine,
|
|
31
35
|
MES_BIZSTEP: () => MES_BIZSTEP,
|
|
36
|
+
MES_NODE_TYPES: () => MES_NODE_TYPES,
|
|
32
37
|
MES_PART_GTINS: () => MES_PART_GTINS,
|
|
38
|
+
MES_PRODUCTS: () => MES_PRODUCTS,
|
|
33
39
|
MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
|
|
40
|
+
MES_TYPES: () => MES_TYPES,
|
|
34
41
|
MesKernel: () => MesKernel,
|
|
35
42
|
OP_EVENT: () => OP_EVENT,
|
|
36
43
|
StateProjector: () => StateProjector,
|
|
@@ -38,13 +45,20 @@ __export(index_exports, {
|
|
|
38
45
|
TwinObserver: () => TwinObserver,
|
|
39
46
|
TwinRuntime: () => TwinRuntime,
|
|
40
47
|
UTC_OFFSET: () => UTC_OFFSET,
|
|
48
|
+
WMS_NODE_TYPES: () => WMS_NODE_TYPES,
|
|
49
|
+
WMS_TYPES: () => WMS_TYPES,
|
|
41
50
|
WmsKernel: () => WmsKernel,
|
|
42
51
|
YARD_BIZSTEP: () => YARD_BIZSTEP,
|
|
52
|
+
YMS_NODE_TYPES: () => YMS_NODE_TYPES,
|
|
53
|
+
YMS_TYPES: () => YMS_TYPES,
|
|
43
54
|
YmsKernel: () => YmsKernel,
|
|
44
55
|
aggregationEvent: () => aggregationEvent,
|
|
56
|
+
capabilitiesForType: () => capabilitiesForType,
|
|
45
57
|
compareStates: () => compareStates,
|
|
58
|
+
computeOee: () => computeOee,
|
|
46
59
|
constantDuration: () => constantDuration,
|
|
47
60
|
counterfactualAt: () => counterfactualAt,
|
|
61
|
+
deriveAttentions: () => deriveAttentions,
|
|
48
62
|
fefoPolicy: () => fefoPolicy,
|
|
49
63
|
firstFitPolicy: () => firstFitPolicy,
|
|
50
64
|
gdtiUri: () => gdtiUri,
|
|
@@ -58,8 +72,10 @@ __export(index_exports, {
|
|
|
58
72
|
sgtinClass: () => sgtinClass,
|
|
59
73
|
sgtinUri: () => sgtinUri,
|
|
60
74
|
ssccUri: () => ssccUri,
|
|
75
|
+
stateFieldsOf: () => stateFieldsOf,
|
|
61
76
|
transactionEvent: () => transactionEvent,
|
|
62
77
|
transformationEvent: () => transformationEvent,
|
|
78
|
+
validateDomainDefinition: () => validateDomainDefinition,
|
|
63
79
|
validateEpcisEvent: () => validateEpcisEvent
|
|
64
80
|
});
|
|
65
81
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -68,14 +84,78 @@ module.exports = __toCommonJS(index_exports);
|
|
|
68
84
|
var OP_EVENT = {
|
|
69
85
|
task: "task.status",
|
|
70
86
|
equipment: "equipment.status",
|
|
71
|
-
order: "order.status"
|
|
87
|
+
order: "order.status",
|
|
88
|
+
quality: "quality.output"
|
|
89
|
+
// 품질 산출(양품/불량) — OEE quality 입력. live 누적기가 이걸로 good/scrap 정확 추적.
|
|
72
90
|
};
|
|
73
91
|
var CMD = {
|
|
74
92
|
orderHold: "order.hold",
|
|
75
93
|
orderResume: "order.resume",
|
|
76
|
-
orderRelease: "order.release"
|
|
94
|
+
orderRelease: "order.release",
|
|
95
|
+
attentionAck: "attention.ack",
|
|
96
|
+
// 주목 신호 확인(OPC UA A&C acknowledge) — args:{id}
|
|
97
|
+
// Operable 코어 — 모든 operable 자원(설비·이동무버) 공통. args:{resourceId}. capability-keyed(무방언, equipment.* 아님).
|
|
98
|
+
resourceHold: "resource.hold",
|
|
99
|
+
// 계획 정지(정비/오프라인) — 배정 스킵
|
|
100
|
+
resourceResume: "resource.resume",
|
|
101
|
+
// 계획 정지 해제
|
|
102
|
+
resourceDown: "resource.down",
|
|
103
|
+
// 비계획 고장 주입 — args:{resourceId, durationMs?}
|
|
104
|
+
resourceRepair: "resource.repair",
|
|
105
|
+
// 즉시 수리
|
|
106
|
+
resourceResetMetrics: "resource.reset-metrics",
|
|
107
|
+
// OEE 계측 창 리셋
|
|
108
|
+
resourceAdd: "resource.add"
|
|
109
|
+
// 라이브 자원(무버) 추가 — args:{kind, homeNode, count?}. 런타임 구조 변이(what-if 아닌 실제 act)
|
|
77
110
|
};
|
|
78
111
|
|
|
112
|
+
// src/domain-definition.ts
|
|
113
|
+
var INTENTS = ["transport", "process", "dwell"];
|
|
114
|
+
function dupes(keys) {
|
|
115
|
+
const seen = /* @__PURE__ */ new Set();
|
|
116
|
+
const dup = /* @__PURE__ */ new Set();
|
|
117
|
+
for (const k of keys) {
|
|
118
|
+
if (seen.has(k)) dup.add(k);
|
|
119
|
+
else seen.add(k);
|
|
120
|
+
}
|
|
121
|
+
return [...dup];
|
|
122
|
+
}
|
|
123
|
+
function validateDomainDefinition(def) {
|
|
124
|
+
const v = [];
|
|
125
|
+
if (!def || typeof def !== "object") return ["domain definition \uC5C6\uC74C/\uAC1D\uCCB4 \uC544\uB2D8"];
|
|
126
|
+
if (typeof def.id !== "string" || !def.id) v.push("id \uB204\uB77D");
|
|
127
|
+
if (typeof def.label !== "string" || !def.label) v.push("label \uB204\uB77D");
|
|
128
|
+
if (!Array.isArray(def.nodeTypes) || def.nodeTypes.length === 0) v.push("nodeTypes \uBE44\uC5B4\uC788\uC74C");
|
|
129
|
+
if (!Array.isArray(def.resourceTypes)) v.push("resourceTypes \uBC30\uC5F4 \uC544\uB2D8");
|
|
130
|
+
const nodeKeys2 = new Set((def.nodeTypes || []).map((n) => n.key));
|
|
131
|
+
const resKeys = new Set((def.resourceTypes || []).map((r) => r.key));
|
|
132
|
+
const matKeys = new Set((def.materials || []).map((m) => m.key));
|
|
133
|
+
const opKeys = new Set((def.operations || []).map((o) => o.key));
|
|
134
|
+
const routeKeys = new Set((def.routes || []).map((r) => r.key));
|
|
135
|
+
for (const [name, arr] of [["nodeTypes", def.nodeTypes], ["resourceTypes", def.resourceTypes], ["materials", def.materials], ["operations", def.operations], ["routes", def.routes], ["recipes", def.recipes]]) {
|
|
136
|
+
for (const d of dupes((arr || []).map((x) => x.key))) v.push(`${name} \uD0A4 \uC911\uBCF5: ${d}`);
|
|
137
|
+
}
|
|
138
|
+
for (const o of def.operations || []) {
|
|
139
|
+
if (!INTENTS.includes(o.intent)) v.push(`operation '${o.key}' intent \uBD80\uC815: ${o.intent}`);
|
|
140
|
+
if (o.nodeType && !nodeKeys2.has(o.nodeType)) v.push(`operation '${o.key}' nodeType '${o.nodeType}' \uBBF8\uC815\uC758`);
|
|
141
|
+
if (o.resourceType && !resKeys.has(o.resourceType)) v.push(`operation '${o.key}' resourceType '${o.resourceType}' \uBBF8\uC815\uC758`);
|
|
142
|
+
if (o.intent === "dwell" && o.resourceType) v.push(`operation '${o.key}' dwell \uC778\uB370 resourceType \uC9C0\uC815\uB428(\uBB34\uC790\uC6D0\uC774\uC5B4\uC57C)`);
|
|
143
|
+
}
|
|
144
|
+
for (const r of def.routes || []) {
|
|
145
|
+
if (!Array.isArray(r.steps) || r.steps.length === 0) v.push(`route '${r.key}' steps \uBE44\uC5B4\uC788\uC74C`);
|
|
146
|
+
for (const s of r.steps || []) if (!opKeys.has(s)) v.push(`route '${r.key}' step '${s}' \uBBF8\uC815\uC758 operation`);
|
|
147
|
+
}
|
|
148
|
+
for (const rc of def.recipes || []) {
|
|
149
|
+
if (rc.route && !routeKeys.has(rc.route)) v.push(`recipe '${rc.key}' route '${rc.route}' \uBBF8\uC815\uC758`);
|
|
150
|
+
if (!rc.outputs?.length) v.push(`recipe '${rc.key}' outputs \uBE44\uC5B4\uC788\uC74C`);
|
|
151
|
+
for (const p of [...rc.inputs || [], ...rc.outputs || []]) {
|
|
152
|
+
if (!matKeys.has(p.material)) v.push(`recipe '${rc.key}' material '${p.material}' \uBBF8\uC815\uC758`);
|
|
153
|
+
if (typeof p.qty !== "number" || p.qty <= 0) v.push(`recipe '${rc.key}' material '${p.material}' qty \uBD80\uC815`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return v;
|
|
157
|
+
}
|
|
158
|
+
|
|
79
159
|
// src/divergence.ts
|
|
80
160
|
function diffBy(predicted, actual, idOf, valOf) {
|
|
81
161
|
const p = new Map(predicted.map((e) => [idOf(e), valOf(e)]));
|
|
@@ -207,7 +287,7 @@ var StateProjector = class {
|
|
|
207
287
|
orders = /* @__PURE__ */ new Map();
|
|
208
288
|
revision = 0;
|
|
209
289
|
constructor(board) {
|
|
210
|
-
for (const n of board.nodes) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity });
|
|
290
|
+
for (const n of board.nodes) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, parentId: n.parentId });
|
|
211
291
|
for (const m of board.movers) this.movers.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeNode });
|
|
212
292
|
}
|
|
213
293
|
/** 마스터 동기 — 로케이션 추가/변경/제거. */
|
|
@@ -287,7 +367,7 @@ var StateProjector = class {
|
|
|
287
367
|
for (const it of this.items.values()) occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
|
|
288
368
|
return {
|
|
289
369
|
revision: this.revision,
|
|
290
|
-
nodes: [...this.master.values()].map((n) => ({ id: n.id, type: n.type, capacity: n.capacity, occupancy: occ.get(n.id) ?? 0 })),
|
|
370
|
+
nodes: [...this.master.values()].map((n) => ({ id: n.id, type: n.type, capacity: n.capacity, occupancy: occ.get(n.id) ?? 0, parentId: n.parentId })),
|
|
291
371
|
items: [...this.items.values()].map((i) => ({ epc: i.epc, gtin: i.gtin, location: i.location, disposition: i.disposition })),
|
|
292
372
|
tasks: [...this.tasks.values()].map((t) => ({ ...t })),
|
|
293
373
|
movers: [...this.movers.values()].map((m) => ({ ...m })),
|
|
@@ -450,6 +530,125 @@ var BTT = {
|
|
|
450
530
|
po: "urn:epcglobal:cbv:btt:po",
|
|
451
531
|
so: "urn:epcglobal:cbv:btt:so"
|
|
452
532
|
};
|
|
533
|
+
var WMS_NODE_TYPES = ["dock", "storage", "staging", "dock-ship"];
|
|
534
|
+
var WMS_NODE_LABELS = { dock: "\uC785\uACE0 \uB3C4\uD06C", storage: "\uBCF4\uAD00 \uC704\uCE58", staging: "\uC2A4\uD14C\uC774\uC9D5", "dock-ship": "\uCD9C\uACE0 \uB3C4\uD06C" };
|
|
535
|
+
var WMS_TYPES = [
|
|
536
|
+
...WMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label: WMS_NODE_LABELS[k] ?? k, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
537
|
+
{ key: "forklift", role: "mover", label: "\uC9C0\uAC8C\uCC28", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
// src/capability.ts
|
|
541
|
+
var CAPABILITIES = {
|
|
542
|
+
operable: {
|
|
543
|
+
key: "operable",
|
|
544
|
+
label: "\uC6B4\uC601",
|
|
545
|
+
semantics: "\uB2A5\uB3D9 \uC790\uC6D0\uC758 \uC6B4\uC601 \uC0C1\uD0DC(\uC720\uD734/\uAC00\uB3D9/\uACE0\uC7A5). status \uAD50\uCC28 \uAD00\uC2EC\uC0AC\uB97C \uC5EC\uAE30 \uD558\uB098\uB85C.",
|
|
546
|
+
stateFields: ["status"],
|
|
547
|
+
results: ["statusChanged"]
|
|
548
|
+
},
|
|
549
|
+
storable: {
|
|
550
|
+
key: "storable",
|
|
551
|
+
label: "\uC800\uC7A5",
|
|
552
|
+
semantics: "\uC544\uC774\uD15C\uC744 \uBCF4\uC720\uD558\uB294 \uC704\uCE58 \u2014 \uC810\uC720/\uC6A9\uB7C9. (\uC52C \uAE30\uC81C: Capacity)",
|
|
553
|
+
stateFields: ["occupancy", "capacity"],
|
|
554
|
+
invariants: ["0 <= occupancy <= capacity (capacity>0)"],
|
|
555
|
+
results: ["occupancyChanged"]
|
|
556
|
+
},
|
|
557
|
+
mobile: {
|
|
558
|
+
key: "mobile",
|
|
559
|
+
label: "\uC774\uB3D9",
|
|
560
|
+
semantics: "\uC790\uC6D0 \uC790\uC2E0\uC774 \uB178\uB4DC \uAC04 \uC774\uB3D9. Transferable(\uC544\uC774\uD15C \uC774\uB3D9)\uACFC \uB2E4\uB984. (\uC52C \uAE30\uC81C: CarrierLine)",
|
|
561
|
+
stateFields: ["location", "motion"],
|
|
562
|
+
models: ["Motion"],
|
|
563
|
+
results: ["moved", "motionTick"]
|
|
564
|
+
},
|
|
565
|
+
processable: {
|
|
566
|
+
key: "processable",
|
|
567
|
+
label: "\uAC00\uACF5",
|
|
568
|
+
semantics: "\uBCC0\uD658/\uAC00\uACF5 \uC218\uD589 \u2014 \uC0B0\uCD9C(\uC591\uD488/\uBD88\uB7C9). \uC6B4\uC601 status \uB294 Operable \uC870\uD569. progress \uBC29\uCD9C\uC740 \uD6C4\uC18D.",
|
|
569
|
+
stateFields: ["output"],
|
|
570
|
+
results: ["completed"]
|
|
571
|
+
},
|
|
572
|
+
trackable: {
|
|
573
|
+
key: "trackable",
|
|
574
|
+
label: "\uCD94\uC801",
|
|
575
|
+
semantics: "\uC624\uB354/\uC544\uC774\uD15C \uC0DD\uC560 \uCD94\uC801 \u2014 \uC0DD\uC560\uB2E8\uACC4(\uB3C4\uBA54\uC778 \uB77C\uBCA8, \uBB34\uBC29\uC5B8)\xB7\uC9C4\uD589\xB7\uBCF4\uB958.",
|
|
576
|
+
stateFields: ["lifecycle", "progress", "held"],
|
|
577
|
+
results: ["lifecycleChanged"]
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
var CAPABILITY_KEYS = ["operable", "storable", "mobile", "processable", "trackable"];
|
|
581
|
+
function stateFieldsOf(caps) {
|
|
582
|
+
const out = /* @__PURE__ */ new Set();
|
|
583
|
+
for (const c of caps) for (const f of CAPABILITIES[c]?.stateFields ?? []) out.add(f);
|
|
584
|
+
return [...out];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/yms-profile.ts
|
|
588
|
+
var YARD_BIZSTEP = {
|
|
589
|
+
arriving: "urn:epcglobal:cbv:bizstep:arriving",
|
|
590
|
+
// 게이트-인
|
|
591
|
+
staging: "urn:epcglobal:cbv:bizstep:staging",
|
|
592
|
+
// 야드 슬롯 주차·대기
|
|
593
|
+
loading: "urn:epcglobal:cbv:bizstep:loading",
|
|
594
|
+
// 도크 작업(상차: 트레일러←화물)
|
|
595
|
+
unloading: "urn:epcglobal:cbv:bizstep:unloading",
|
|
596
|
+
// 도크 작업(하차: 트레일러→화물)
|
|
597
|
+
departing: "urn:epcglobal:cbv:bizstep:departing"
|
|
598
|
+
// 게이트-아웃
|
|
599
|
+
};
|
|
600
|
+
var BTT_DELIVERY = "urn:epcglobal:cbv:btt:deliv";
|
|
601
|
+
function graiUri(companyPrefix, assetType, serial) {
|
|
602
|
+
return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, "0")}`;
|
|
603
|
+
}
|
|
604
|
+
var YMS_NODE_TYPES = ["gate", "yard-slot", "dock-door", "staging"];
|
|
605
|
+
var YMS_NODE_LABELS = { gate: "\uAC8C\uC774\uD2B8", "yard-slot": "\uC57C\uB4DC \uC2AC\uB86F", "dock-door": "\uB3C4\uD06C \uB3C4\uC5B4", staging: "\uC2A4\uD14C\uC774\uC9D5" };
|
|
606
|
+
var YMS_TYPES = [
|
|
607
|
+
...YMS_NODE_TYPES.map((k) => ({ key: k, role: "node", label: YMS_NODE_LABELS[k] ?? k, standardClass: { epcis: "bizLocation" }, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
608
|
+
{ key: "hostler", role: "mover", label: "\uC57C\uB4DC \uD2B8\uB799\uD130", standardClass: { epcis: "object", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["mobile", "operable"] }
|
|
609
|
+
];
|
|
610
|
+
|
|
611
|
+
// src/mes-profile.ts
|
|
612
|
+
var MES_BIZSTEP = {
|
|
613
|
+
receiving: "urn:epcglobal:cbv:bizstep:receiving",
|
|
614
|
+
// 원자재 수령
|
|
615
|
+
producing: "urn:epcglobal:cbv:bizstep:commissioning",
|
|
616
|
+
// 생산(제품 최초 생성)
|
|
617
|
+
storing: "urn:epcglobal:cbv:bizstep:storing"
|
|
618
|
+
// 완제품 저장
|
|
619
|
+
};
|
|
620
|
+
var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
|
621
|
+
function sgtinUri(companyPrefix, itemRef, serial) {
|
|
622
|
+
return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
|
|
623
|
+
}
|
|
624
|
+
var MES_NODE_TYPES = ["raw-store", "cut-station", "weld-station", "paint-booth", "assembly-line", "fg-store"];
|
|
625
|
+
var MES_NODE_META = {
|
|
626
|
+
"raw-store": { label: "\uC790\uC7AC \uCC3D\uACE0", cls: { epcis: "bizLocation" } },
|
|
627
|
+
"cut-station": { label: "\uD504\uB808\uC784 \uC808\uB2E8", cls: { isa95: "WorkCenter", epcis: "bizLocation" } },
|
|
628
|
+
"weld-station": { label: "\uC6A9\uC811", cls: { isa95: "WorkCenter", epcis: "bizLocation" } },
|
|
629
|
+
"paint-booth": { label: "\uB3C4\uC7A5", cls: { isa95: "WorkCenter", epcis: "bizLocation" } },
|
|
630
|
+
"assembly-line": { label: "\uC870\uB9BD\xB7\uC758\uC7A5", cls: { isa95: "WorkCenter", epcis: "bizLocation" } },
|
|
631
|
+
"fg-store": { label: "\uC644\uC131\uCC28 \uBCF4\uAD00", cls: { epcis: "bizLocation" } }
|
|
632
|
+
};
|
|
633
|
+
var MES_TYPES = [
|
|
634
|
+
...MES_NODE_TYPES.map((k) => ({ key: k, role: "node", label: MES_NODE_META[k]?.label ?? k, standardClass: MES_NODE_META[k]?.cls ?? {}, identity: { scheme: "gs1:SGLN" }, capabilities: ["storable"] })),
|
|
635
|
+
{ key: "cutter", role: "mover", label: "\uC808\uB2E8 \uC124\uBE44", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
636
|
+
{ key: "welder", role: "mover", label: "\uC6A9\uC811 \uB85C\uBD07", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
637
|
+
{ key: "painter", role: "mover", label: "\uB3C4\uC7A5 \uB85C\uBD07", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] },
|
|
638
|
+
{ key: "assembler", role: "mover", label: "\uC870\uB9BD \uC124\uBE44", standardClass: { isa95: "Equipment", iso55000: "Asset" }, identity: { scheme: "gs1:GIAI" }, capabilities: ["processable", "operable"] }
|
|
639
|
+
];
|
|
640
|
+
|
|
641
|
+
// src/domain-catalog.ts
|
|
642
|
+
var nodeKeys = (types) => types.filter((t) => t.role === "node").map((t) => t.key);
|
|
643
|
+
var DOMAIN_CATALOG = {
|
|
644
|
+
wms: { system: "wms", label: "WMS (\uBB3C\uB958\uCC3D\uACE0)", types: WMS_TYPES, nodeTypes: nodeKeys(WMS_TYPES) },
|
|
645
|
+
yms: { system: "yms", label: "YMS (\uC57C\uB4DC)", types: YMS_TYPES, nodeTypes: nodeKeys(YMS_TYPES) },
|
|
646
|
+
mes: { system: "mes", label: "MES (\uC81C\uC870)", types: MES_TYPES, nodeTypes: nodeKeys(MES_TYPES) }
|
|
647
|
+
};
|
|
648
|
+
var DOMAIN_SYSTEMS = ["wms", "yms", "mes"];
|
|
649
|
+
function capabilitiesForType(system, typeKey) {
|
|
650
|
+
return DOMAIN_CATALOG[system]?.types.find((t) => t.key === typeKey)?.capabilities ?? [];
|
|
651
|
+
}
|
|
453
652
|
|
|
454
653
|
// src/allocation-policy.ts
|
|
455
654
|
function freeBinsFirstFit(slots) {
|
|
@@ -583,38 +782,6 @@ var TwinRuntime = class {
|
|
|
583
782
|
}
|
|
584
783
|
};
|
|
585
784
|
|
|
586
|
-
// src/yms-profile.ts
|
|
587
|
-
var YARD_BIZSTEP = {
|
|
588
|
-
arriving: "urn:epcglobal:cbv:bizstep:arriving",
|
|
589
|
-
// 게이트-인
|
|
590
|
-
staging: "urn:epcglobal:cbv:bizstep:staging",
|
|
591
|
-
// 야드 슬롯 주차·대기
|
|
592
|
-
loading: "urn:epcglobal:cbv:bizstep:loading",
|
|
593
|
-
// 도크 작업(상차: 트레일러←화물)
|
|
594
|
-
unloading: "urn:epcglobal:cbv:bizstep:unloading",
|
|
595
|
-
// 도크 작업(하차: 트레일러→화물)
|
|
596
|
-
departing: "urn:epcglobal:cbv:bizstep:departing"
|
|
597
|
-
// 게이트-아웃
|
|
598
|
-
};
|
|
599
|
-
var BTT_DELIVERY = "urn:epcglobal:cbv:btt:deliv";
|
|
600
|
-
function graiUri(companyPrefix, assetType, serial) {
|
|
601
|
-
return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, "0")}`;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// src/mes-profile.ts
|
|
605
|
-
var MES_BIZSTEP = {
|
|
606
|
-
receiving: "urn:epcglobal:cbv:bizstep:receiving",
|
|
607
|
-
// 원자재 수령
|
|
608
|
-
producing: "urn:epcglobal:cbv:bizstep:commissioning",
|
|
609
|
-
// 생산(제품 최초 생성)
|
|
610
|
-
storing: "urn:epcglobal:cbv:bizstep:storing"
|
|
611
|
-
// 완제품 저장
|
|
612
|
-
};
|
|
613
|
-
var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
|
|
614
|
-
function sgtinUri(companyPrefix, itemRef, serial) {
|
|
615
|
-
return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
785
|
// src/flow-engine.ts
|
|
619
786
|
var BASE_EPOCH = Date.parse("2026-01-01T00:00:00Z");
|
|
620
787
|
function mulberry32(seed) {
|
|
@@ -630,6 +797,103 @@ function mulberry32(seed) {
|
|
|
630
797
|
} });
|
|
631
798
|
return fn;
|
|
632
799
|
}
|
|
800
|
+
function deriveAttentions(view, acked) {
|
|
801
|
+
const out = [];
|
|
802
|
+
for (const m of view.movers) {
|
|
803
|
+
if (m.status === "down") {
|
|
804
|
+
const where = m.location ?? "\uD574\uB2F9 \uACF5\uC815";
|
|
805
|
+
out.push({
|
|
806
|
+
id: `breakdown:${m.id}`,
|
|
807
|
+
kind: "breakdown",
|
|
808
|
+
severity: "critical",
|
|
809
|
+
title: `\uC124\uBE44 \uACE0\uC7A5 \xB7 ${m.id}`,
|
|
810
|
+
anchor: { moverId: m.id, nodeId: m.location },
|
|
811
|
+
rationale: `\uC124\uBE44 \uC815\uC9C0 \u2014 ${where}\uC758 \uC791\uC5C5\uC774 \uC911\uB2E8\uB418\uC5B4 \uD558\uB958 \uC815\uCCB4\xB7\uCC98\uB9AC\uB7C9 \uAC10\uC18C\uB85C \uC774\uC5B4\uC9D1\uB2C8\uB2E4.`,
|
|
812
|
+
recommendedActions: [
|
|
813
|
+
{ label: "\uC218\uB9AC \uC9C0\uC2DC", command: CMD.resourceRepair, args: { resourceId: m.id }, hint: "\uC124\uBE44\uB97C \uC989\uC2DC \uBCF5\uAD6C\uD574 \uAC00\uB3D9 \uC7AC\uAC1C" },
|
|
814
|
+
{ label: "\uACC4\uD68D \uC815\uC9C0 \uC720\uC9C0", command: CMD.resourceHold, args: { resourceId: m.id }, hint: "\uC218\uB9AC \uC804\uAE4C\uC9C0 \uBC30\uC815\uC5D0\uC11C \uC81C\uC678" }
|
|
815
|
+
],
|
|
816
|
+
suggestedAction: { command: CMD.resourceRepair, args: { resourceId: m.id }, label: "\uC218\uB9AC" }
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
for (const n of view.nodes) {
|
|
821
|
+
if ((n.capacity ?? 0) > 0) {
|
|
822
|
+
const r = (n.occupancy ?? 0) / n.capacity;
|
|
823
|
+
if (r >= 0.9) {
|
|
824
|
+
const saturated = r >= 1;
|
|
825
|
+
out.push({
|
|
826
|
+
id: `bottleneck:${n.id}`,
|
|
827
|
+
kind: "bottleneck",
|
|
828
|
+
severity: saturated ? "high" : "medium",
|
|
829
|
+
title: `${saturated ? "\uBCD1\uBAA9" : "\uD63C\uC7A1"} \xB7 ${n.id} \uC810\uC720 ${n.occupancy}/${n.capacity}`,
|
|
830
|
+
anchor: { nodeId: n.id },
|
|
831
|
+
rationale: `${n.id} \uC810\uC720 ${Math.round(r * 100)}% \u2014 \uC0C1\uB958 \uB300\uAE30\uAC00 \uC313\uC5EC \uB9AC\uB4DC\uD0C0\uC784\uC774 \uB298\uACE0 \uCC98\uB9AC\uB7C9\uC774 \uC81C\uD55C\uB429\uB2C8\uB2E4.`,
|
|
832
|
+
recommendedActions: [
|
|
833
|
+
{ label: "\uC790\uC6D0 \uCD94\uAC00 \uAC80\uD1A0", hint: "\uBB34\uBC84\xB7\uCC98\uB9AC \uB2A5\uB825\uC744 \uBCF4\uAC15\uD574 \uBCD1\uBAA9 \uC644\uD654" },
|
|
834
|
+
{ label: "\uD558\uB958 \uC6B0\uC120 \uCC98\uB9AC", hint: "\uC801\uCCB4 \uD574\uC18C\uB97C \uC704\uD574 \uBC30\uCD9C \uC6B0\uC120\uC21C\uC704 \uC870\uC815" }
|
|
835
|
+
]
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
for (const m of view.movers) {
|
|
841
|
+
const total = (m.goodCount ?? 0) + (m.scrapCount ?? 0);
|
|
842
|
+
if (total >= 10) {
|
|
843
|
+
const rate = (m.scrapCount ?? 0) / total;
|
|
844
|
+
if (rate >= 0.15) out.push({
|
|
845
|
+
id: `scrap:${m.id}`,
|
|
846
|
+
kind: "scrap-high",
|
|
847
|
+
severity: rate >= 0.3 ? "high" : "medium",
|
|
848
|
+
title: `\uBD88\uB7C9\uB960 ${Math.round(rate * 100)}% \xB7 ${m.id}`,
|
|
849
|
+
detail: `\uC591\uD488 ${m.goodCount} / \uBD88\uB7C9 ${m.scrapCount}`,
|
|
850
|
+
anchor: { moverId: m.id, nodeId: m.location },
|
|
851
|
+
rationale: `\uBD88\uB7C9\uB960 ${Math.round(rate * 100)}% \u2014 \uC7AC\uC791\uC5C5\xB7\uC218\uC728 \uC190\uC2E4\uC774 \uB204\uC801\uB429\uB2C8\uB2E4. \uC124\uBE44 \uC0C1\uD0DC\xB7\uC14B\uC5C5 \uD3B8\uCC28\uB97C \uC810\uAC80\uD558\uC138\uC694.`,
|
|
852
|
+
recommendedActions: [
|
|
853
|
+
{ label: "\uC124\uBE44 \uC810\uAC80 \uC815\uC9C0", command: CMD.resourceHold, args: { resourceId: m.id }, hint: "\uC810\uAC80\uC744 \uC704\uD574 \uBC30\uC815\uC5D0\uC11C \uC81C\uC678" },
|
|
854
|
+
{ label: "\uACC4\uCE21 \uB9AC\uC14B", command: CMD.resourceResetMetrics, args: { resourceId: m.id }, hint: "\uAD50\uC815 \uD6C4 \uC218\uC728 \uC7AC\uCE21\uC815" }
|
|
855
|
+
],
|
|
856
|
+
suggestedAction: { command: CMD.resourceHold, args: { resourceId: m.id }, label: "\uC810\uAC80 \uC815\uC9C0" }
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (const o of view.orders) {
|
|
861
|
+
if (o.held) out.push({
|
|
862
|
+
id: `hold:${o.id}`,
|
|
863
|
+
kind: "hold",
|
|
864
|
+
severity: "medium",
|
|
865
|
+
title: `\uC624\uB354 \uBCF4\uB958 \xB7 ${o.id}`,
|
|
866
|
+
anchor: { orderId: o.id },
|
|
867
|
+
rationale: `\uC624\uB354 \uC9C4\uD589\uC774 \uBA48\uCDA4 \u2014 \uB0A9\uAE30 \uC9C0\uC5F0 \uC704\uD5D8. \uBCF4\uB958 \uC0AC\uC720 \uD574\uC18C \uD6C4 \uC7AC\uAC1C\uD558\uC138\uC694.`,
|
|
868
|
+
recommendedActions: [{ label: "\uC7AC\uAC1C", command: CMD.orderResume, args: { orderId: o.id }, hint: "\uBCF4\uB958\uB97C \uD480\uACE0 \uD750\uB984 \uC7AC\uAC1C" }],
|
|
869
|
+
suggestedAction: { command: CMD.orderResume, args: { orderId: o.id }, label: "\uC7AC\uAC1C" }
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
if (acked) {
|
|
873
|
+
for (const a of out) if (acked.has(a.id)) a.state = "acknowledged";
|
|
874
|
+
}
|
|
875
|
+
return out;
|
|
876
|
+
}
|
|
877
|
+
function computeOee(c, nowMs) {
|
|
878
|
+
const planned = Math.max(0, nowMs - (c.metricsSinceMs ?? 0) - (c.holdMs ?? 0));
|
|
879
|
+
const uptime = Math.max(0, planned - c.setupMs - c.downMs);
|
|
880
|
+
const availability = planned > 0 ? uptime / planned : 1;
|
|
881
|
+
const performance = uptime > 0 ? Math.min(1, c.runMs / uptime) : c.runMs > 0 ? 1 : 0;
|
|
882
|
+
const totalQ = c.goodCount + c.scrapCount;
|
|
883
|
+
const quality = totalQ > 0 ? c.goodCount / totalQ : 1;
|
|
884
|
+
return {
|
|
885
|
+
availability,
|
|
886
|
+
performance,
|
|
887
|
+
quality,
|
|
888
|
+
overall: availability * performance * quality,
|
|
889
|
+
runMs: c.runMs,
|
|
890
|
+
setupMs: c.setupMs,
|
|
891
|
+
downMs: c.downMs,
|
|
892
|
+
idleMs: Math.max(0, uptime - c.runMs),
|
|
893
|
+
goodCount: c.goodCount,
|
|
894
|
+
scrapCount: c.scrapCount
|
|
895
|
+
};
|
|
896
|
+
}
|
|
633
897
|
var FlowEngine = class {
|
|
634
898
|
tenantId;
|
|
635
899
|
nodes = /* @__PURE__ */ new Map();
|
|
@@ -658,7 +922,7 @@ var FlowEngine = class {
|
|
|
658
922
|
}
|
|
659
923
|
// ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
|
|
660
924
|
loadBoard(def) {
|
|
661
|
-
for (const n of def.nodes) this.nodes.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, occupancy: 0, status: "idle" });
|
|
925
|
+
for (const n of def.nodes) this.nodes.set(n.id, { id: n.id, type: n.type, capacity: n.capacity, occupancy: 0, status: "idle", parentId: n.parentId });
|
|
662
926
|
for (const m of def.movers) {
|
|
663
927
|
const mover = { id: m.id, kind: m.kind, location: m.homeNode, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 };
|
|
664
928
|
if (m.mtbfMs !== void 0) {
|
|
@@ -669,6 +933,54 @@ var FlowEngine = class {
|
|
|
669
933
|
this.movers.set(m.id, mover);
|
|
670
934
|
}
|
|
671
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* what-if 구성 변주 — fork(또는 실행 중) 엔진에 무버 추가. loadBoard 무버 삽입과 동일 규약.
|
|
938
|
+
* 기본은 mtbf 미지정(고장 없는 신뢰 자원) → sampleExp(rng) 무소비라 baseline fork 와 깨끗이 비교 가능.
|
|
939
|
+
*/
|
|
940
|
+
addMover(m) {
|
|
941
|
+
if (this.movers.has(m.id)) return;
|
|
942
|
+
const mover = { id: m.id, kind: m.kind, location: m.homeNode, status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 };
|
|
943
|
+
if (m.mtbfMs !== void 0) {
|
|
944
|
+
mover.mtbfMs = m.mtbfMs;
|
|
945
|
+
mover.mttrMs = m.mttrMs;
|
|
946
|
+
mover.nextFailureMs = this.sampleExp(m.mtbfMs);
|
|
947
|
+
}
|
|
948
|
+
this.movers.set(m.id, mover);
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* 관측 상태 주입(라이브 예측용, kernel-unification P1) — 외부 관측 스냅샷(재고·무버·노드)과
|
|
952
|
+
* 저널 오더(원값+라인)로 이 커널의 맵을 채운다. tick 으로 만든 게 아니라 "현재 관측된 현실"을 심어
|
|
953
|
+
* 이후 fork/tick 으로 예측한다. 라이브 런타임은 여전히 projector 미러 — 이 커널은 예측용 임시본.
|
|
954
|
+
* 오더는 남은 데맨드(라인별 requested-fulfilled)를 'created' 로 복원(현재 재고에서 재계획).
|
|
955
|
+
* 진행 중 개별 task 의 내부 상태는 관측만으론 복원 불가 → 재계획에 맡김(정직한 한계).
|
|
956
|
+
*/
|
|
957
|
+
hydrateObserved(snap, orders = []) {
|
|
958
|
+
for (const n of snap.nodes) this.nodes.set(n.id, { id: n.id, type: n.type, capacity: n.capacity ?? 0, occupancy: n.occupancy ?? 0, status: "idle", parentId: n.parentId });
|
|
959
|
+
this.items.clear();
|
|
960
|
+
for (const it of snap.items) this.items.set(it.epc, { epc: it.epc, location: it.location, disposition: it.disposition ?? DISP.sellable, gtin: it.gtin, qty: it.qty ?? 1 });
|
|
961
|
+
for (const m of snap.movers) this.movers.set(m.id, { id: m.id, kind: m.kind, location: m.location ?? "", status: "idle", taskId: null, runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 });
|
|
962
|
+
for (const o of orders) {
|
|
963
|
+
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
964
|
+
const remaining = lines.reduce((s, l) => s + l.requested, 0);
|
|
965
|
+
if (remaining <= 0) continue;
|
|
966
|
+
this.orders.set(o.orderId, { id: o.orderId, kind: o.kind, status: "created", requested: remaining, fulfilled: 0, bizTransaction: "", allocated: [], picked: [], shipmentEpc: null, lines });
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/** what-if 구성 변주 — 노드 용량 변경(fork 대상). 존재하면 true. */
|
|
970
|
+
setNodeCapacity(nodeId, capacity) {
|
|
971
|
+
const n = this.nodes.get(nodeId);
|
|
972
|
+
if (!n) return false;
|
|
973
|
+
n.capacity = Math.max(0, capacity);
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* forecast 몬테카를로 — fork 의 RNG 만 재시드(시나리오·상태·gens·in-flight 는 보존).
|
|
978
|
+
* "현재 조건 지속"을 유지한 채 **미래 확률만** 변주(도착·고장 타이밍 등) → run 마다 다른 표본.
|
|
979
|
+
* (monteCarloForecast 은 scenario.load 로 gens 를 갈아끼우므로 "현재 조건"이 깨진다 — 그 대안.)
|
|
980
|
+
*/
|
|
981
|
+
reseed(seed) {
|
|
982
|
+
this.rng = mulberry32(seed >>> 0);
|
|
983
|
+
}
|
|
672
984
|
onEvent(handler) {
|
|
673
985
|
this.handlers.push(handler);
|
|
674
986
|
return () => {
|
|
@@ -681,6 +993,8 @@ var FlowEngine = class {
|
|
|
681
993
|
* 도메인 커맨드(order.release 등)는 handleCommand 로 위임. 커맨드는 sim 상태를 변이하고
|
|
682
994
|
* State 델타를 유발한다(command → 행위 → 관측 폐루프).
|
|
683
995
|
*/
|
|
996
|
+
_acked = /* @__PURE__ */ new Set();
|
|
997
|
+
// 확인(ack)된 주목 신호 id — 조건 지속돼도 acknowledged 로 표시(재발 시 재활성)
|
|
684
998
|
dispatch(cmd) {
|
|
685
999
|
const ok = () => ({ commandId: cmd.commandId, accepted: true });
|
|
686
1000
|
const fail = (error) => ({ commandId: cmd.commandId, accepted: false, error });
|
|
@@ -694,6 +1008,70 @@ var FlowEngine = class {
|
|
|
694
1008
|
this.emitOrder(order);
|
|
695
1009
|
return ok();
|
|
696
1010
|
}
|
|
1011
|
+
case CMD.attentionAck: {
|
|
1012
|
+
const id = cmd.args?.id;
|
|
1013
|
+
if (id) this._acked.add(id);
|
|
1014
|
+
return ok();
|
|
1015
|
+
}
|
|
1016
|
+
// Operable 코어 — 자원(설비·무버) 제어. capability-keyed(resourceId), 모든 operable 자원 공통.
|
|
1017
|
+
case CMD.resourceHold:
|
|
1018
|
+
case CMD.resourceResume: {
|
|
1019
|
+
const m = this.movers.get(cmd.args?.resourceId ?? "");
|
|
1020
|
+
if (!m) return fail("resource \uC5C6\uC74C");
|
|
1021
|
+
m.held = cmd.type === CMD.resourceHold;
|
|
1022
|
+
this.emitMover(m);
|
|
1023
|
+
return ok();
|
|
1024
|
+
}
|
|
1025
|
+
case CMD.resourceDown: {
|
|
1026
|
+
const a = cmd.args;
|
|
1027
|
+
const m = this.movers.get(a?.resourceId ?? "");
|
|
1028
|
+
if (!m) return fail("resource \uC5C6\uC74C");
|
|
1029
|
+
if (m.status !== "down") {
|
|
1030
|
+
m.status = "down";
|
|
1031
|
+
m.repairUntilMs = this.clockMs + (Number(a?.durationMs) || m.mttrMs || 6e4);
|
|
1032
|
+
this.emitMover(m);
|
|
1033
|
+
}
|
|
1034
|
+
return ok();
|
|
1035
|
+
}
|
|
1036
|
+
case CMD.resourceRepair: {
|
|
1037
|
+
const m = this.movers.get(cmd.args?.resourceId ?? "");
|
|
1038
|
+
if (!m) return fail("resource \uC5C6\uC74C");
|
|
1039
|
+
if (m.status === "down") {
|
|
1040
|
+
m.status = m.taskId ? "busy" : "idle";
|
|
1041
|
+
m.repairUntilMs = void 0;
|
|
1042
|
+
if (m.mtbfMs !== void 0) m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
1043
|
+
this.emitMover(m);
|
|
1044
|
+
}
|
|
1045
|
+
return ok();
|
|
1046
|
+
}
|
|
1047
|
+
case CMD.resourceResetMetrics: {
|
|
1048
|
+
const m = this.movers.get(cmd.args?.resourceId ?? "");
|
|
1049
|
+
if (!m) return fail("resource \uC5C6\uC74C");
|
|
1050
|
+
m.runMs = 0;
|
|
1051
|
+
m.setupMs = 0;
|
|
1052
|
+
m.downMs = 0;
|
|
1053
|
+
m.goodCount = 0;
|
|
1054
|
+
m.scrapCount = 0;
|
|
1055
|
+
m.holdMs = 0;
|
|
1056
|
+
m.metricsSinceMs = this.clockMs;
|
|
1057
|
+
this.emitMover(m);
|
|
1058
|
+
return ok();
|
|
1059
|
+
}
|
|
1060
|
+
case CMD.resourceAdd: {
|
|
1061
|
+
const a = cmd.args;
|
|
1062
|
+
if (!a?.kind) return fail("kind \uD544\uC694");
|
|
1063
|
+
if (!a?.homeNode || !this.nodes.has(a.homeNode)) return fail(`homeNode \uC5C6\uC74C: ${a?.homeNode}`);
|
|
1064
|
+
const count = Math.max(1, Math.min(50, Number(a.count) || 1));
|
|
1065
|
+
let seq = this.movers.size;
|
|
1066
|
+
for (let i = 0; i < count; i++) {
|
|
1067
|
+
let id = `${a.kind}-${++seq}`;
|
|
1068
|
+
while (this.movers.has(id)) id = `${a.kind}-${++seq}`;
|
|
1069
|
+
this.addMover({ id, kind: a.kind, homeNode: a.homeNode });
|
|
1070
|
+
const m = this.movers.get(id);
|
|
1071
|
+
if (m) this.emitMover(m);
|
|
1072
|
+
}
|
|
1073
|
+
return ok();
|
|
1074
|
+
}
|
|
697
1075
|
default:
|
|
698
1076
|
return this.handleCommand(cmd);
|
|
699
1077
|
}
|
|
@@ -739,15 +1117,30 @@ var FlowEngine = class {
|
|
|
739
1117
|
nodes: [...this.nodes.values()].map((n) => ({ ...n })),
|
|
740
1118
|
items: [...this.items.values()].map((i) => ({ epc: i.epc, gtin: i.gtin, qty: i.qty, location: i.location, disposition: i.disposition, expiry: i.expiry })),
|
|
741
1119
|
movers: [...this.movers.values()].map((m) => {
|
|
742
|
-
const s = { id: m.id, kind: m.kind, location: m.location, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m) };
|
|
1120
|
+
const s = { id: m.id, kind: m.kind, location: m.location, status: m.status, taskId: m.taskId ?? void 0, oee: this.oeeOf(m), held: m.held };
|
|
743
1121
|
const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
|
|
744
1122
|
if (t && t.status === "in-progress" && t.intent !== "process") s.motion = { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs - (t.durationMs - t.remainingMs), durationMs: t.durationMs, progress: this.progressOf(t), elapsedMs: t.durationMs - t.remainingMs };
|
|
745
1123
|
return s;
|
|
746
1124
|
}),
|
|
747
|
-
tasks: [...this.tasks.values()].map((t) => ({ id: t.id, kind: t.kind, status: t.status, itemRefs: [t.itemEpc], fromNode: t.fromNode, toNode: t.toNode, resourceRef: t.resource ?? void 0, progress: t.status === "in-progress" ? this.progressOf(t) : void 0 })),
|
|
748
|
-
orders: [...this.orders.values()].map((o) => ({ id: o.id, kind: o.kind, status: o.status, progress: o.requested ? o.fulfilled / o.requested : 0, held: o.held }))
|
|
1125
|
+
tasks: [...this.tasks.values()].map((t) => ({ id: t.id, kind: t.kind, status: t.status, itemRefs: [t.itemEpc], fromNode: t.fromNode, toNode: t.toNode, resourceRef: t.resource ?? void 0, orderId: t.orderId, progress: t.status === "in-progress" ? this.progressOf(t) : void 0 })),
|
|
1126
|
+
orders: [...this.orders.values()].map((o) => ({ id: o.id, kind: o.kind, status: o.status, progress: o.requested ? o.fulfilled / o.requested : 0, held: o.held })),
|
|
1127
|
+
attentions: this.computeAttentions()
|
|
749
1128
|
};
|
|
750
1129
|
}
|
|
1130
|
+
/*
|
|
1131
|
+
* 주목 신호 판단 — 상태(노드·무버·오더)에서 도메인 조건을 평가해 Attention 방출.
|
|
1132
|
+
* severity=ISA-18.2 우선순위 계열, kind=도메인 라벨. UI 는 판단 안 함(임계값 여기 소유).
|
|
1133
|
+
* 도메인별 추가 판단은 서브클래스가 override 로 확장(super.computeAttentions() 합성).
|
|
1134
|
+
*/
|
|
1135
|
+
computeAttentions() {
|
|
1136
|
+
const out = deriveAttentions(
|
|
1137
|
+
{ movers: [...this.movers.values()], nodes: [...this.nodes.values()], orders: [...this.orders.values()] },
|
|
1138
|
+
this._acked
|
|
1139
|
+
);
|
|
1140
|
+
const present = new Set(out.map((a) => a.id));
|
|
1141
|
+
for (const id of [...this._acked]) if (!present.has(id)) this._acked.delete(id);
|
|
1142
|
+
return out;
|
|
1143
|
+
}
|
|
751
1144
|
/**
|
|
752
1145
|
* fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
|
|
753
1146
|
* 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 굴려 forecast·발산(predicted vs actual) 검사에 쓴다.
|
|
@@ -867,27 +1260,11 @@ var FlowEngine = class {
|
|
|
867
1260
|
if (!m) return;
|
|
868
1261
|
if (good) m.goodCount++;
|
|
869
1262
|
else m.scrapCount++;
|
|
1263
|
+
this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount, scrapCount: m.scrapCount });
|
|
870
1264
|
}
|
|
871
1265
|
/** 무버 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
|
|
872
1266
|
oeeOf(m) {
|
|
873
|
-
|
|
874
|
-
const uptime = Math.max(0, planned - m.setupMs - m.downMs);
|
|
875
|
-
const availability = planned > 0 ? uptime / planned : 1;
|
|
876
|
-
const performance = uptime > 0 ? Math.min(1, m.runMs / uptime) : m.runMs > 0 ? 1 : 0;
|
|
877
|
-
const totalQ = m.goodCount + m.scrapCount;
|
|
878
|
-
const quality = totalQ > 0 ? m.goodCount / totalQ : 1;
|
|
879
|
-
return {
|
|
880
|
-
availability,
|
|
881
|
-
performance,
|
|
882
|
-
quality,
|
|
883
|
-
overall: availability * performance * quality,
|
|
884
|
-
runMs: m.runMs,
|
|
885
|
-
setupMs: m.setupMs,
|
|
886
|
-
downMs: m.downMs,
|
|
887
|
-
idleMs: Math.max(0, uptime - m.runMs),
|
|
888
|
-
goodCount: m.goodCount,
|
|
889
|
-
scrapCount: m.scrapCount
|
|
890
|
-
};
|
|
1267
|
+
return computeOee(m, this.clockMs);
|
|
891
1268
|
}
|
|
892
1269
|
/** 정책에 넘길 특정 타입 노드의 관측 뷰 — 예약(그 노드로 향하는 in-flight task) 포함. */
|
|
893
1270
|
slotViews(nodeType) {
|
|
@@ -908,7 +1285,7 @@ var FlowEngine = class {
|
|
|
908
1285
|
for (const h of this.handlers) h(e);
|
|
909
1286
|
}
|
|
910
1287
|
emitTask(t) {
|
|
911
|
-
this.emitOp(OP_EVENT.task, { taskId: t.id, kind: t.kind, status: t.status, fromNode: t.fromNode, toNode: t.toNode, itemRefs: [t.itemEpc], resourceRef: t.resource ?? void 0 });
|
|
1288
|
+
this.emitOp(OP_EVENT.task, { taskId: t.id, orderId: t.orderId, kind: t.kind, status: t.status, fromNode: t.fromNode, toNode: t.toNode, itemRefs: [t.itemEpc], resourceRef: t.resource ?? void 0 });
|
|
912
1289
|
}
|
|
913
1290
|
emitMover(m, motion) {
|
|
914
1291
|
this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, motion });
|
|
@@ -932,16 +1309,21 @@ var FlowEngine = class {
|
|
|
932
1309
|
*/
|
|
933
1310
|
processFailures(dt) {
|
|
934
1311
|
for (const m of this.movers.values()) {
|
|
935
|
-
if (m.mtbfMs === void 0) continue;
|
|
936
1312
|
if (m.status === "down") {
|
|
937
1313
|
m.downMs += dt;
|
|
938
|
-
if (this.clockMs >=
|
|
1314
|
+
if (m.repairUntilMs != null && this.clockMs >= m.repairUntilMs) {
|
|
939
1315
|
m.status = m.taskId ? "busy" : "idle";
|
|
940
1316
|
m.repairUntilMs = void 0;
|
|
941
|
-
m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
1317
|
+
if (m.mtbfMs !== void 0) m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
|
|
942
1318
|
this.emitMover(m);
|
|
943
1319
|
}
|
|
944
|
-
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
if (m.held) {
|
|
1323
|
+
if (m.status === "idle") m.holdMs = (m.holdMs ?? 0) + dt;
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (m.mtbfMs !== void 0 && this.clockMs >= (m.nextFailureMs ?? Infinity)) {
|
|
945
1327
|
m.status = "down";
|
|
946
1328
|
m.repairUntilMs = this.clockMs + this.sampleExp(m.mttrMs ?? m.mtbfMs);
|
|
947
1329
|
m.nextFailureMs = void 0;
|
|
@@ -955,8 +1337,9 @@ var FlowEngine = class {
|
|
|
955
1337
|
generate() {
|
|
956
1338
|
for (const g of this.gens) {
|
|
957
1339
|
while (this.clockMs >= g.nextMs) {
|
|
958
|
-
|
|
959
|
-
|
|
1340
|
+
const stimulus = g.spec.stimulus ?? (g.spec.kind === "outbound-order" ? "order" : "arrival");
|
|
1341
|
+
if (stimulus === "order") this.onOrder(g.spec);
|
|
1342
|
+
else this.onArrival(g.spec);
|
|
960
1343
|
g.nextMs += this.intervalMs(g.spec);
|
|
961
1344
|
}
|
|
962
1345
|
}
|
|
@@ -973,7 +1356,7 @@ var FlowEngine = class {
|
|
|
973
1356
|
this.emitTask(t);
|
|
974
1357
|
continue;
|
|
975
1358
|
}
|
|
976
|
-
const mover = [...this.movers.values()].find((m) => m.status === "idle" && (t.resourceType === void 0 || m.kind === t.resourceType));
|
|
1359
|
+
const mover = [...this.movers.values()].find((m) => m.status === "idle" && !m.held && (t.resourceType === void 0 || m.kind === t.resourceType));
|
|
977
1360
|
if (!mover) continue;
|
|
978
1361
|
if (t.setupMs && t.changeoverKey !== void 0 && mover.lastChangeoverKey !== void 0 && mover.lastChangeoverKey !== t.changeoverKey) {
|
|
979
1362
|
t.appliedSetupMs = t.setupMs;
|
|
@@ -1331,6 +1714,7 @@ var YmsKernel = class extends FlowEngine {
|
|
|
1331
1714
|
// src/mes-kernel.ts
|
|
1332
1715
|
var CYCLE_MS = 4e4;
|
|
1333
1716
|
var SETUP_MS = 15e3;
|
|
1717
|
+
var MES_CMD = { changeover: "mes.changeover" };
|
|
1334
1718
|
var CP2 = "0614141";
|
|
1335
1719
|
var WIP_ITEMREF = "066666";
|
|
1336
1720
|
var WIP_GTIN = sgtinClass(CP2, WIP_ITEMREF);
|
|
@@ -1343,17 +1727,49 @@ var PRODUCTS = [
|
|
|
1343
1727
|
];
|
|
1344
1728
|
var MES_PART_GTINS = { partA: PART_A.gtin, partB: PART_B.gtin };
|
|
1345
1729
|
var MES_PRODUCT_GTINS = { p1: PRODUCTS[0].gtin, p2: PRODUCTS[1].gtin };
|
|
1730
|
+
var MES_PRODUCTS = PRODUCTS.map((p) => ({ gtin: p.gtin, label: p.key }));
|
|
1731
|
+
var ROUTE = [
|
|
1732
|
+
{ kind: "cut", node: "cut-station", resource: "cutter" },
|
|
1733
|
+
{ kind: "weld", node: "weld-station", resource: "welder" },
|
|
1734
|
+
{ kind: "paint", node: "paint-booth", resource: "painter" },
|
|
1735
|
+
{ kind: "assembly", node: "assembly-line", resource: "assembler" }
|
|
1736
|
+
];
|
|
1346
1737
|
var MesKernel = class extends FlowEngine {
|
|
1347
1738
|
wipSeq = 0;
|
|
1348
1739
|
prodSeq = 0;
|
|
1349
|
-
|
|
1740
|
+
/** 정의-구동 모드(선택). 미지정 시 레거시 하드코딩 경로 — byte-identical. */
|
|
1741
|
+
mesSpec;
|
|
1742
|
+
constructor(tenantId, policy = firstFitPolicy, mesSpec) {
|
|
1350
1743
|
super(tenantId, policy);
|
|
1744
|
+
this.mesSpec = mesSpec;
|
|
1351
1745
|
}
|
|
1352
1746
|
productOf(gtin) {
|
|
1353
1747
|
return PRODUCTS.find((p) => p.gtin === gtin);
|
|
1354
1748
|
}
|
|
1749
|
+
/**
|
|
1750
|
+
* MES 도메인 커맨드(Tier 2) — mes.changeover: 설비를 제품 gtin 으로 강제 전환.
|
|
1751
|
+
* 자동 체인지오버(task.changeoverKey 상이 시 셋업)의 수동 버전 — 운영자가 사전 전환(툴링 교체) 지시.
|
|
1752
|
+
* 이미 그 제품이면 no-op, 아니면 셋업(SETUP_MS, OEE 가용성 손실) + lastChangeoverKey 각인
|
|
1753
|
+
* (이후 그 제품 task 는 자동 셋업 생략). command → 변이 → State 델타(폐루프).
|
|
1754
|
+
*/
|
|
1755
|
+
handleCommand(cmd) {
|
|
1756
|
+
if (cmd.type === MES_CMD.changeover) {
|
|
1757
|
+
const a = cmd.args;
|
|
1758
|
+
if (!a?.resourceId || !a?.gtin) return { commandId: cmd.commandId, accepted: false, error: "mes.changeover: resourceId\xB7gtin \uD544\uC694" };
|
|
1759
|
+
const m = this.movers.get(a.resourceId);
|
|
1760
|
+
if (!m) return { commandId: cmd.commandId, accepted: false, error: `resource \uC5C6\uC74C: ${a.resourceId}` };
|
|
1761
|
+
if (m.lastChangeoverKey !== a.gtin) {
|
|
1762
|
+
m.setupMs += SETUP_MS;
|
|
1763
|
+
m.lastChangeoverKey = a.gtin;
|
|
1764
|
+
this.emitMover(m);
|
|
1765
|
+
}
|
|
1766
|
+
return { commandId: cmd.commandId, accepted: true };
|
|
1767
|
+
}
|
|
1768
|
+
return super.handleCommand(cmd);
|
|
1769
|
+
}
|
|
1355
1770
|
/** 부품 수령(다품종) — skuMix 의 gtin 으로 부품 종류 결정. */
|
|
1356
1771
|
onArrival(spec) {
|
|
1772
|
+
if (this.mesSpec) return this.onArrivalDef(spec);
|
|
1357
1773
|
const rawStore = this.nodeByType("raw-store");
|
|
1358
1774
|
if (!rawStore) return;
|
|
1359
1775
|
const gtin = this.pickGtin(spec.content.skuMix);
|
|
@@ -1366,6 +1782,7 @@ var MesKernel = class extends FlowEngine {
|
|
|
1366
1782
|
}
|
|
1367
1783
|
/** 작업지시 — 제품 2종 교대(체인지오버 유발). 제품 gtin 을 오더에 기록. */
|
|
1368
1784
|
onOrder(_spec) {
|
|
1785
|
+
if (this.mesSpec) return this.onOrderDef(_spec);
|
|
1369
1786
|
const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
|
|
1370
1787
|
const id = `order-${++this.orderSeq}`;
|
|
1371
1788
|
const wo = gdtiUri(CP2, "403", ++this.soSeq);
|
|
@@ -1373,11 +1790,13 @@ var MesKernel = class extends FlowEngine {
|
|
|
1373
1790
|
this.orders.set(id, order);
|
|
1374
1791
|
this.emitOrder(order);
|
|
1375
1792
|
}
|
|
1376
|
-
/** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) +
|
|
1793
|
+
/** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) + 라우트 첫 스테이션(절단) 태스크. */
|
|
1377
1794
|
allocate(o) {
|
|
1378
|
-
|
|
1795
|
+
if (this.mesSpec) return this.allocateDef(o);
|
|
1796
|
+
const s0 = ROUTE[0];
|
|
1797
|
+
const first = this.nodeByType(s0.node);
|
|
1379
1798
|
const product = this.productOf(o.gtin);
|
|
1380
|
-
if (!
|
|
1799
|
+
if (!first || !product) return;
|
|
1381
1800
|
const picks = [];
|
|
1382
1801
|
for (const line of product.bom) {
|
|
1383
1802
|
const available = [...this.items.values()].filter((i) => i.gtin === line.part.gtin && i.disposition === DISP.sellable && this.nodes.get(i.location)?.type === "raw-store").map((i) => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
@@ -1390,40 +1809,146 @@ var MesKernel = class extends FlowEngine {
|
|
|
1390
1809
|
o.allocated.push(epc);
|
|
1391
1810
|
}
|
|
1392
1811
|
this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
|
|
1393
|
-
|
|
1812
|
+
this.emitStation(o, s0, o.allocated[0], product.gtin);
|
|
1813
|
+
o.status = "op-" + s0.kind;
|
|
1814
|
+
this.emitOrder(o);
|
|
1815
|
+
}
|
|
1816
|
+
/** 라우트 스테이션 태스크 발행(공통) — 제자리 가공(process), 이종 자원, 제품 전환 셋업. */
|
|
1817
|
+
emitStation(o, stage, itemEpc, changeoverKey) {
|
|
1818
|
+
const node = this.nodeByType(stage.node);
|
|
1819
|
+
const task = { id: `task-${++this.taskSeq}`, kind: stage.kind, status: "created", itemEpc, fromNode: node.id, toNode: node.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: stage.kind, fromNode: node.id, toNode: node.id, resourceKind: stage.resource }, CYCLE_MS), orderId: o.id, resourceType: stage.resource, changeoverKey, setupMs: SETUP_MS, intent: "process" };
|
|
1394
1820
|
this.tasks.set(task.id, task);
|
|
1395
1821
|
this.emitTask(task);
|
|
1396
|
-
o.status = "op-cut";
|
|
1397
|
-
this.emitOrder(o);
|
|
1398
1822
|
}
|
|
1399
|
-
/** op 완료 = 변환.
|
|
1823
|
+
/** op 완료 = 변환. 라우트 인덱스로 분기: 첫=BOM 소비→WIP, 중간=WIP→WIP, 마지막(조립)=WIP→완성차(수율→OEE 품질). */
|
|
1400
1824
|
onTaskComplete(t) {
|
|
1825
|
+
if (this.mesSpec) return this.onTaskCompleteDef(t);
|
|
1401
1826
|
const order = this.orders.get(t.orderId);
|
|
1402
1827
|
const product = this.productOf(order.gtin);
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1828
|
+
const i = ROUTE.findIndex((s) => s.kind === t.kind);
|
|
1829
|
+
const node = this.nodes.get(t.toNode);
|
|
1830
|
+
const isLast = i === ROUTE.length - 1;
|
|
1831
|
+
if (!isLast) {
|
|
1407
1832
|
const inputs = order.allocated.slice();
|
|
1408
1833
|
const wip2 = sgtinUri(CP2, WIP_ITEMREF, ++this.wipSeq);
|
|
1409
|
-
this.transform(inputs, [{ epc: wip2, gtin: WIP_GTIN, qty: 1, location:
|
|
1834
|
+
this.transform(inputs, [{ epc: wip2, gtin: WIP_GTIN, qty: 1, location: node.id, disposition: DISP.in_progress }], { bizStep: MES_BIZSTEP.producing, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: node.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
1410
1835
|
order.allocated = [wip2];
|
|
1411
|
-
const
|
|
1412
|
-
this.
|
|
1413
|
-
|
|
1414
|
-
order.status = "op-weld";
|
|
1836
|
+
const next = ROUTE[i + 1];
|
|
1837
|
+
this.emitStation(order, next, wip2, product.gtin);
|
|
1838
|
+
order.status = "op-" + next.kind;
|
|
1415
1839
|
this.emitOrder(order);
|
|
1416
1840
|
return;
|
|
1417
1841
|
}
|
|
1418
|
-
const weld = this.nodes.get(t.toNode);
|
|
1419
1842
|
const fgStore = this.nodeByType("fg-store");
|
|
1420
1843
|
const wip = order.allocated[0];
|
|
1421
1844
|
const good = this.rng() < YIELD;
|
|
1422
1845
|
this.recordOutput(t.resource, good);
|
|
1423
1846
|
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
1424
1847
|
const outputEpc = sgtinUri(CP2, product.ref, ++this.prodSeq);
|
|
1425
|
-
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:
|
|
1426
|
-
this.emit(objectEvent({ eventTime, action: "OBSERVE", bizStep: MES_BIZSTEP.storing, disposition: disp, epcList: [outputEpc], quantityList: [{ epcClass: product.gtin, quantity: 1 }], readPoint: fgStore.id, bizLocation: fgStore.id }));
|
|
1848
|
+
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: node.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
1849
|
+
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 }));
|
|
1850
|
+
order.allocated = [];
|
|
1851
|
+
order.fulfilled = 1;
|
|
1852
|
+
order.status = good ? "produced" : "scrapped";
|
|
1853
|
+
this.emitOrder(order);
|
|
1854
|
+
}
|
|
1855
|
+
// ── 정의-구동 모드 (도메인 정의 데이터로 실행 — 레거시와 분리, 하드코딩 대체) ──
|
|
1856
|
+
recipeDef() {
|
|
1857
|
+
const d = this.mesSpec.definition;
|
|
1858
|
+
return this.mesSpec.recipeKey ? d.recipes?.find((r) => r.key === this.mesSpec.recipeKey) : d.recipes?.[0];
|
|
1859
|
+
}
|
|
1860
|
+
/** 자재 키 → 구체 gtin 클래스(idpat). 구체 식별은 바인딩+prefix 로 인스턴스가 주입. */
|
|
1861
|
+
classOf(materialKey) {
|
|
1862
|
+
return sgtinClass(this.mesSpec.companyPrefix, this.mesSpec.binding[materialKey]);
|
|
1863
|
+
}
|
|
1864
|
+
serialOf(materialKey, serial) {
|
|
1865
|
+
return sgtinUri(this.mesSpec.companyPrefix, this.mesSpec.binding[materialKey], serial);
|
|
1866
|
+
}
|
|
1867
|
+
/** recipe.route → 오퍼레이션 시퀀스 해소. */
|
|
1868
|
+
routeOps() {
|
|
1869
|
+
const d = this.mesSpec.definition;
|
|
1870
|
+
const route = d.routes?.find((r) => r.key === this.recipeDef().route);
|
|
1871
|
+
return (route?.steps ?? []).map((sk) => d.operations?.find((o) => o.key === sk)).filter((o) => !!o);
|
|
1872
|
+
}
|
|
1873
|
+
/** 정의 모드 수령 — skuMix gtin 이 레시피 입력 자재면 raw-store 에 생성. */
|
|
1874
|
+
onArrivalDef(spec) {
|
|
1875
|
+
const rawStore = this.nodeByType("raw-store");
|
|
1876
|
+
if (!rawStore) return;
|
|
1877
|
+
const gtin = this.pickGtin(spec.content.skuMix);
|
|
1878
|
+
const inputKey = this.recipeDef().inputs.map((i) => i.material).find((k) => this.classOf(k) === gtin);
|
|
1879
|
+
if (!inputKey) return;
|
|
1880
|
+
const epc = this.serialOf(inputKey, ++this.epcSeq);
|
|
1881
|
+
this.items.set(epc, { epc, gtin, qty: 1, location: rawStore.id, disposition: DISP.sellable });
|
|
1882
|
+
rawStore.occupancy++;
|
|
1883
|
+
this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.receiving, disposition: DISP.sellable, epcList: [epc], quantityList: [{ epcClass: gtin, quantity: 1 }], readPoint: rawStore.id, bizLocation: rawStore.id }));
|
|
1884
|
+
}
|
|
1885
|
+
/** 정의 모드 작업지시 — 레시피 산출물 1개. */
|
|
1886
|
+
onOrderDef(_spec) {
|
|
1887
|
+
const rc = this.recipeDef();
|
|
1888
|
+
const id = `order-${++this.orderSeq}`;
|
|
1889
|
+
const wo = gdtiUri(this.mesSpec.companyPrefix, "403", ++this.soSeq);
|
|
1890
|
+
const order = { id, kind: "workorder", status: "created", gtin: this.classOf(rc.outputs[0].material), requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [] };
|
|
1891
|
+
this.orders.set(id, order);
|
|
1892
|
+
this.emitOrder(order);
|
|
1893
|
+
}
|
|
1894
|
+
/** 정의 모드 할당 — 레시피 입력 BOM 전량 확보 후 첫 라우트 스텝 태스크. */
|
|
1895
|
+
allocateDef(o) {
|
|
1896
|
+
const ops = this.routeOps();
|
|
1897
|
+
if (!ops.length || !ops[0].nodeType || !this.nodeByType(ops[0].nodeType)) return;
|
|
1898
|
+
const rc = this.recipeDef();
|
|
1899
|
+
const picks = [];
|
|
1900
|
+
for (const line of rc.inputs) {
|
|
1901
|
+
const g = this.classOf(line.material);
|
|
1902
|
+
const available = [...this.items.values()].filter((i) => i.gtin === g && i.disposition === DISP.sellable && this.nodes.get(i.location)?.type === "raw-store").map((i) => ({ epc: i.epc, location: i.location, qty: 1 }));
|
|
1903
|
+
const chosen = this.policy.selectStock({ gtin: g, qty: line.qty, available });
|
|
1904
|
+
if (chosen.length < line.qty) return;
|
|
1905
|
+
picks.push(...chosen);
|
|
1906
|
+
}
|
|
1907
|
+
for (const epc of picks) {
|
|
1908
|
+
this.items.get(epc).disposition = DISP.reserved;
|
|
1909
|
+
o.allocated.push(epc);
|
|
1910
|
+
}
|
|
1911
|
+
this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
|
|
1912
|
+
this.emitStationDef(o, ops[0], o.allocated[0]);
|
|
1913
|
+
o.status = "op-" + ops[0].key;
|
|
1914
|
+
this.emitOrder(o);
|
|
1915
|
+
}
|
|
1916
|
+
emitStationDef(o, op, itemEpc) {
|
|
1917
|
+
const node = this.nodeByType(op.nodeType);
|
|
1918
|
+
const task = { id: `task-${++this.taskSeq}`, kind: op.key, status: "created", itemEpc, fromNode: node.id, toNode: node.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: op.key, fromNode: node.id, toNode: node.id, resourceKind: op.resourceType }, CYCLE_MS), orderId: o.id, resourceType: op.resourceType, changeoverKey: o.gtin, setupMs: SETUP_MS, intent: op.intent };
|
|
1919
|
+
this.tasks.set(task.id, task);
|
|
1920
|
+
this.emitTask(task);
|
|
1921
|
+
}
|
|
1922
|
+
/** 정의 모드 완료 — 라우트 인덱스: 중간=WIP 변환+다음 스텝, 마지막=완제품(수율). */
|
|
1923
|
+
onTaskCompleteDef(t) {
|
|
1924
|
+
const order = this.orders.get(t.orderId);
|
|
1925
|
+
const rc = this.recipeDef();
|
|
1926
|
+
const ops = this.routeOps();
|
|
1927
|
+
const i = ops.findIndex((s) => s.key === t.kind);
|
|
1928
|
+
const node = this.nodes.get(t.toNode);
|
|
1929
|
+
const bizStep = ops[i]?.bizStep ?? MES_BIZSTEP.producing;
|
|
1930
|
+
const isLast = i === ops.length - 1;
|
|
1931
|
+
if (!isLast) {
|
|
1932
|
+
const inputs = order.allocated.slice();
|
|
1933
|
+
const wip2 = sgtinUri(this.mesSpec.companyPrefix, "WIP", ++this.wipSeq);
|
|
1934
|
+
const wipGtin = sgtinClass(this.mesSpec.companyPrefix, "WIP");
|
|
1935
|
+
this.transform(inputs, [{ epc: wip2, gtin: wipGtin, qty: 1, location: node.id, disposition: DISP.in_progress }], { bizStep, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: node.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
1936
|
+
order.allocated = [wip2];
|
|
1937
|
+
const next = ops[i + 1];
|
|
1938
|
+
this.emitStationDef(order, next, wip2);
|
|
1939
|
+
order.status = "op-" + next.key;
|
|
1940
|
+
this.emitOrder(order);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
const fgStore = this.nodeByType("fg-store");
|
|
1944
|
+
const wip = order.allocated[0];
|
|
1945
|
+
const good = this.rng() < YIELD;
|
|
1946
|
+
this.recordOutput(t.resource, good);
|
|
1947
|
+
const disp = good ? DISP.sellable : DISP.non_sellable;
|
|
1948
|
+
const outEpc = this.serialOf(rc.outputs[0].material, ++this.prodSeq);
|
|
1949
|
+
const outGtin = this.classOf(rc.outputs[0].material);
|
|
1950
|
+
this.transform([wip], [{ epc: outEpc, gtin: outGtin, qty: 1, location: fgStore.id, disposition: disp }], { bizStep, disposition: disp, transformationId: order.bizTransaction, readPoint: node.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
|
|
1951
|
+
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 }));
|
|
1427
1952
|
order.allocated = [];
|
|
1428
1953
|
order.fulfilled = 1;
|
|
1429
1954
|
order.status = good ? "produced" : "scrapped";
|
|
@@ -1436,14 +1961,21 @@ var MesKernel = class extends FlowEngine {
|
|
|
1436
1961
|
BTT,
|
|
1437
1962
|
BTT_DELIVERY,
|
|
1438
1963
|
BTT_PRODORDER,
|
|
1964
|
+
CAPABILITIES,
|
|
1965
|
+
CAPABILITY_KEYS,
|
|
1439
1966
|
CMD,
|
|
1440
1967
|
DISP,
|
|
1968
|
+
DOMAIN_CATALOG,
|
|
1969
|
+
DOMAIN_SYSTEMS,
|
|
1441
1970
|
EPCIS_CONTEXT,
|
|
1442
1971
|
EventJournal,
|
|
1443
1972
|
FlowEngine,
|
|
1444
1973
|
MES_BIZSTEP,
|
|
1974
|
+
MES_NODE_TYPES,
|
|
1445
1975
|
MES_PART_GTINS,
|
|
1976
|
+
MES_PRODUCTS,
|
|
1446
1977
|
MES_PRODUCT_GTINS,
|
|
1978
|
+
MES_TYPES,
|
|
1447
1979
|
MesKernel,
|
|
1448
1980
|
OP_EVENT,
|
|
1449
1981
|
StateProjector,
|
|
@@ -1451,13 +1983,20 @@ var MesKernel = class extends FlowEngine {
|
|
|
1451
1983
|
TwinObserver,
|
|
1452
1984
|
TwinRuntime,
|
|
1453
1985
|
UTC_OFFSET,
|
|
1986
|
+
WMS_NODE_TYPES,
|
|
1987
|
+
WMS_TYPES,
|
|
1454
1988
|
WmsKernel,
|
|
1455
1989
|
YARD_BIZSTEP,
|
|
1990
|
+
YMS_NODE_TYPES,
|
|
1991
|
+
YMS_TYPES,
|
|
1456
1992
|
YmsKernel,
|
|
1457
1993
|
aggregationEvent,
|
|
1994
|
+
capabilitiesForType,
|
|
1458
1995
|
compareStates,
|
|
1996
|
+
computeOee,
|
|
1459
1997
|
constantDuration,
|
|
1460
1998
|
counterfactualAt,
|
|
1999
|
+
deriveAttentions,
|
|
1461
2000
|
fefoPolicy,
|
|
1462
2001
|
firstFitPolicy,
|
|
1463
2002
|
gdtiUri,
|
|
@@ -1471,7 +2010,9 @@ var MesKernel = class extends FlowEngine {
|
|
|
1471
2010
|
sgtinClass,
|
|
1472
2011
|
sgtinUri,
|
|
1473
2012
|
ssccUri,
|
|
2013
|
+
stateFieldsOf,
|
|
1474
2014
|
transactionEvent,
|
|
1475
2015
|
transformationEvent,
|
|
2016
|
+
validateDomainDefinition,
|
|
1476
2017
|
validateEpcisEvent
|
|
1477
2018
|
});
|