@operato/twin-kernel 0.0.4 → 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.
@@ -23,6 +23,8 @@ __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,
28
30
  DOMAIN_CATALOG: () => DOMAIN_CATALOG,
@@ -33,7 +35,9 @@ __export(index_exports, {
33
35
  MES_BIZSTEP: () => MES_BIZSTEP,
34
36
  MES_NODE_TYPES: () => MES_NODE_TYPES,
35
37
  MES_PART_GTINS: () => MES_PART_GTINS,
38
+ MES_PRODUCTS: () => MES_PRODUCTS,
36
39
  MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
40
+ MES_TYPES: () => MES_TYPES,
37
41
  MesKernel: () => MesKernel,
38
42
  OP_EVENT: () => OP_EVENT,
39
43
  StateProjector: () => StateProjector,
@@ -42,14 +46,19 @@ __export(index_exports, {
42
46
  TwinRuntime: () => TwinRuntime,
43
47
  UTC_OFFSET: () => UTC_OFFSET,
44
48
  WMS_NODE_TYPES: () => WMS_NODE_TYPES,
49
+ WMS_TYPES: () => WMS_TYPES,
45
50
  WmsKernel: () => WmsKernel,
46
51
  YARD_BIZSTEP: () => YARD_BIZSTEP,
47
52
  YMS_NODE_TYPES: () => YMS_NODE_TYPES,
53
+ YMS_TYPES: () => YMS_TYPES,
48
54
  YmsKernel: () => YmsKernel,
49
55
  aggregationEvent: () => aggregationEvent,
56
+ capabilitiesForType: () => capabilitiesForType,
50
57
  compareStates: () => compareStates,
58
+ computeOee: () => computeOee,
51
59
  constantDuration: () => constantDuration,
52
60
  counterfactualAt: () => counterfactualAt,
61
+ deriveAttentions: () => deriveAttentions,
53
62
  fefoPolicy: () => fefoPolicy,
54
63
  firstFitPolicy: () => firstFitPolicy,
55
64
  gdtiUri: () => gdtiUri,
@@ -63,8 +72,10 @@ __export(index_exports, {
63
72
  sgtinClass: () => sgtinClass,
64
73
  sgtinUri: () => sgtinUri,
65
74
  ssccUri: () => ssccUri,
75
+ stateFieldsOf: () => stateFieldsOf,
66
76
  transactionEvent: () => transactionEvent,
67
77
  transformationEvent: () => transformationEvent,
78
+ validateDomainDefinition: () => validateDomainDefinition,
68
79
  validateEpcisEvent: () => validateEpcisEvent
69
80
  });
70
81
  module.exports = __toCommonJS(index_exports);
@@ -73,14 +84,78 @@ module.exports = __toCommonJS(index_exports);
73
84
  var OP_EVENT = {
74
85
  task: "task.status",
75
86
  equipment: "equipment.status",
76
- order: "order.status"
87
+ order: "order.status",
88
+ quality: "quality.output"
89
+ // 품질 산출(양품/불량) — OEE quality 입력. live 누적기가 이걸로 good/scrap 정확 추적.
77
90
  };
78
91
  var CMD = {
79
92
  orderHold: "order.hold",
80
93
  orderResume: "order.resume",
81
- 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)
82
110
  };
83
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
+
84
159
  // src/divergence.ts
85
160
  function diffBy(predicted, actual, idOf, valOf) {
86
161
  const p = new Map(predicted.map((e) => [idOf(e), valOf(e)]));
@@ -212,7 +287,7 @@ var StateProjector = class {
212
287
  orders = /* @__PURE__ */ new Map();
213
288
  revision = 0;
214
289
  constructor(board) {
215
- 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 });
216
291
  for (const m of board.movers) this.movers.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeNode });
217
292
  }
218
293
  /** 마스터 동기 — 로케이션 추가/변경/제거. */
@@ -292,7 +367,7 @@ var StateProjector = class {
292
367
  for (const it of this.items.values()) occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
293
368
  return {
294
369
  revision: this.revision,
295
- 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 })),
296
371
  items: [...this.items.values()].map((i) => ({ epc: i.epc, gtin: i.gtin, location: i.location, disposition: i.disposition })),
297
372
  tasks: [...this.tasks.values()].map((t) => ({ ...t })),
298
373
  movers: [...this.movers.values()].map((m) => ({ ...m })),
@@ -456,6 +531,58 @@ var BTT = {
456
531
  so: "urn:epcglobal:cbv:btt:so"
457
532
  };
458
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
+ }
459
586
 
460
587
  // src/yms-profile.ts
461
588
  var YARD_BIZSTEP = {
@@ -475,6 +602,11 @@ function graiUri(companyPrefix, assetType, serial) {
475
602
  return `urn:epc:id:grai:${companyPrefix}.${assetType}.${String(serial).padStart(6, "0")}`;
476
603
  }
477
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
+ ];
478
610
 
479
611
  // src/mes-profile.ts
480
612
  var MES_BIZSTEP = {
@@ -489,15 +621,34 @@ var BTT_PRODORDER = "urn:epcglobal:cbv:btt:prodorder";
489
621
  function sgtinUri(companyPrefix, itemRef, serial) {
490
622
  return `urn:epc:id:sgtin:${companyPrefix}.${itemRef}.${serial}`;
491
623
  }
492
- var MES_NODE_TYPES = ["raw-store", "cut-station", "weld-station", "fg-store"];
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
+ ];
493
640
 
494
641
  // src/domain-catalog.ts
642
+ var nodeKeys = (types) => types.filter((t) => t.role === "node").map((t) => t.key);
495
643
  var DOMAIN_CATALOG = {
496
- wms: { system: "wms", label: "WMS (\uBB3C\uB958\uCC3D\uACE0)", nodeTypes: WMS_NODE_TYPES },
497
- yms: { system: "yms", label: "YMS (\uC57C\uB4DC)", nodeTypes: YMS_NODE_TYPES },
498
- mes: { system: "mes", label: "MES (\uC81C\uC870)", nodeTypes: MES_NODE_TYPES }
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) }
499
647
  };
500
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
+ }
501
652
 
502
653
  // src/allocation-policy.ts
503
654
  function freeBinsFirstFit(slots) {
@@ -646,6 +797,103 @@ function mulberry32(seed) {
646
797
  } });
647
798
  return fn;
648
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
+ }
649
897
  var FlowEngine = class {
650
898
  tenantId;
651
899
  nodes = /* @__PURE__ */ new Map();
@@ -674,7 +922,7 @@ var FlowEngine = class {
674
922
  }
675
923
  // ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
676
924
  loadBoard(def) {
677
- 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 });
678
926
  for (const m of def.movers) {
679
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 };
680
928
  if (m.mtbfMs !== void 0) {
@@ -685,6 +933,54 @@ var FlowEngine = class {
685
933
  this.movers.set(m.id, mover);
686
934
  }
687
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
+ }
688
984
  onEvent(handler) {
689
985
  this.handlers.push(handler);
690
986
  return () => {
@@ -697,6 +993,8 @@ var FlowEngine = class {
697
993
  * 도메인 커맨드(order.release 등)는 handleCommand 로 위임. 커맨드는 sim 상태를 변이하고
698
994
  * State 델타를 유발한다(command → 행위 → 관측 폐루프).
699
995
  */
996
+ _acked = /* @__PURE__ */ new Set();
997
+ // 확인(ack)된 주목 신호 id — 조건 지속돼도 acknowledged 로 표시(재발 시 재활성)
700
998
  dispatch(cmd) {
701
999
  const ok = () => ({ commandId: cmd.commandId, accepted: true });
702
1000
  const fail = (error) => ({ commandId: cmd.commandId, accepted: false, error });
@@ -710,6 +1008,70 @@ var FlowEngine = class {
710
1008
  this.emitOrder(order);
711
1009
  return ok();
712
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
+ }
713
1075
  default:
714
1076
  return this.handleCommand(cmd);
715
1077
  }
@@ -755,15 +1117,30 @@ var FlowEngine = class {
755
1117
  nodes: [...this.nodes.values()].map((n) => ({ ...n })),
756
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 })),
757
1119
  movers: [...this.movers.values()].map((m) => {
758
- 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 };
759
1121
  const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
760
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 };
761
1123
  return s;
762
1124
  }),
763
- 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 })),
764
- 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()
765
1128
  };
766
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
+ }
767
1144
  /**
768
1145
  * fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
769
1146
  * 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 굴려 forecast·발산(predicted vs actual) 검사에 쓴다.
@@ -883,27 +1260,11 @@ var FlowEngine = class {
883
1260
  if (!m) return;
884
1261
  if (good) m.goodCount++;
885
1262
  else m.scrapCount++;
1263
+ this.emitOp(OP_EVENT.quality, { moverId: m.id, good, goodCount: m.goodCount, scrapCount: m.scrapCount });
886
1264
  }
887
1265
  /** 무버 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
888
1266
  oeeOf(m) {
889
- const planned = this.clockMs;
890
- const uptime = Math.max(0, planned - m.setupMs - m.downMs);
891
- const availability = planned > 0 ? uptime / planned : 1;
892
- const performance = uptime > 0 ? Math.min(1, m.runMs / uptime) : m.runMs > 0 ? 1 : 0;
893
- const totalQ = m.goodCount + m.scrapCount;
894
- const quality = totalQ > 0 ? m.goodCount / totalQ : 1;
895
- return {
896
- availability,
897
- performance,
898
- quality,
899
- overall: availability * performance * quality,
900
- runMs: m.runMs,
901
- setupMs: m.setupMs,
902
- downMs: m.downMs,
903
- idleMs: Math.max(0, uptime - m.runMs),
904
- goodCount: m.goodCount,
905
- scrapCount: m.scrapCount
906
- };
1267
+ return computeOee(m, this.clockMs);
907
1268
  }
908
1269
  /** 정책에 넘길 특정 타입 노드의 관측 뷰 — 예약(그 노드로 향하는 in-flight task) 포함. */
909
1270
  slotViews(nodeType) {
@@ -924,7 +1285,7 @@ var FlowEngine = class {
924
1285
  for (const h of this.handlers) h(e);
925
1286
  }
926
1287
  emitTask(t) {
927
- 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 });
928
1289
  }
929
1290
  emitMover(m, motion) {
930
1291
  this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, motion });
@@ -948,16 +1309,21 @@ var FlowEngine = class {
948
1309
  */
949
1310
  processFailures(dt) {
950
1311
  for (const m of this.movers.values()) {
951
- if (m.mtbfMs === void 0) continue;
952
1312
  if (m.status === "down") {
953
1313
  m.downMs += dt;
954
- if (this.clockMs >= (m.repairUntilMs ?? 0)) {
1314
+ if (m.repairUntilMs != null && this.clockMs >= m.repairUntilMs) {
955
1315
  m.status = m.taskId ? "busy" : "idle";
956
1316
  m.repairUntilMs = void 0;
957
- m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
1317
+ if (m.mtbfMs !== void 0) m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
958
1318
  this.emitMover(m);
959
1319
  }
960
- } else if (this.clockMs >= (m.nextFailureMs ?? Infinity)) {
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)) {
961
1327
  m.status = "down";
962
1328
  m.repairUntilMs = this.clockMs + this.sampleExp(m.mttrMs ?? m.mtbfMs);
963
1329
  m.nextFailureMs = void 0;
@@ -971,8 +1337,9 @@ var FlowEngine = class {
971
1337
  generate() {
972
1338
  for (const g of this.gens) {
973
1339
  while (this.clockMs >= g.nextMs) {
974
- if (g.spec.kind === "inbound-arrival") this.onArrival(g.spec);
975
- else if (g.spec.kind === "outbound-order") this.onOrder(g.spec);
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);
976
1343
  g.nextMs += this.intervalMs(g.spec);
977
1344
  }
978
1345
  }
@@ -989,7 +1356,7 @@ var FlowEngine = class {
989
1356
  this.emitTask(t);
990
1357
  continue;
991
1358
  }
992
- 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));
993
1360
  if (!mover) continue;
994
1361
  if (t.setupMs && t.changeoverKey !== void 0 && mover.lastChangeoverKey !== void 0 && mover.lastChangeoverKey !== t.changeoverKey) {
995
1362
  t.appliedSetupMs = t.setupMs;
@@ -1347,6 +1714,7 @@ var YmsKernel = class extends FlowEngine {
1347
1714
  // src/mes-kernel.ts
1348
1715
  var CYCLE_MS = 4e4;
1349
1716
  var SETUP_MS = 15e3;
1717
+ var MES_CMD = { changeover: "mes.changeover" };
1350
1718
  var CP2 = "0614141";
1351
1719
  var WIP_ITEMREF = "066666";
1352
1720
  var WIP_GTIN = sgtinClass(CP2, WIP_ITEMREF);
@@ -1359,17 +1727,49 @@ var PRODUCTS = [
1359
1727
  ];
1360
1728
  var MES_PART_GTINS = { partA: PART_A.gtin, partB: PART_B.gtin };
1361
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
+ ];
1362
1737
  var MesKernel = class extends FlowEngine {
1363
1738
  wipSeq = 0;
1364
1739
  prodSeq = 0;
1365
- constructor(tenantId, policy = firstFitPolicy) {
1740
+ /** 정의-구동 모드(선택). 미지정 레거시 하드코딩 경로 — byte-identical. */
1741
+ mesSpec;
1742
+ constructor(tenantId, policy = firstFitPolicy, mesSpec) {
1366
1743
  super(tenantId, policy);
1744
+ this.mesSpec = mesSpec;
1367
1745
  }
1368
1746
  productOf(gtin) {
1369
1747
  return PRODUCTS.find((p) => p.gtin === gtin);
1370
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
+ }
1371
1770
  /** 부품 수령(다품종) — skuMix 의 gtin 으로 부품 종류 결정. */
1372
1771
  onArrival(spec) {
1772
+ if (this.mesSpec) return this.onArrivalDef(spec);
1373
1773
  const rawStore = this.nodeByType("raw-store");
1374
1774
  if (!rawStore) return;
1375
1775
  const gtin = this.pickGtin(spec.content.skuMix);
@@ -1382,6 +1782,7 @@ var MesKernel = class extends FlowEngine {
1382
1782
  }
1383
1783
  /** 작업지시 — 제품 2종 교대(체인지오버 유발). 제품 gtin 을 오더에 기록. */
1384
1784
  onOrder(_spec) {
1785
+ if (this.mesSpec) return this.onOrderDef(_spec);
1385
1786
  const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
1386
1787
  const id = `order-${++this.orderSeq}`;
1387
1788
  const wo = gdtiUri(CP2, "403", ++this.soSeq);
@@ -1389,11 +1790,13 @@ var MesKernel = class extends FlowEngine {
1389
1790
  this.orders.set(id, order);
1390
1791
  this.emitOrder(order);
1391
1792
  }
1392
- /** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) + op1(cut, 체인지오버 셋업). */
1793
+ /** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) + 라우트 스테이션(절단) 태스크. */
1393
1794
  allocate(o) {
1394
- const cut = this.nodeByType("cut-station");
1795
+ if (this.mesSpec) return this.allocateDef(o);
1796
+ const s0 = ROUTE[0];
1797
+ const first = this.nodeByType(s0.node);
1395
1798
  const product = this.productOf(o.gtin);
1396
- if (!cut || !product) return;
1799
+ if (!first || !product) return;
1397
1800
  const picks = [];
1398
1801
  for (const line of product.bom) {
1399
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 }));
@@ -1406,40 +1809,146 @@ var MesKernel = class extends FlowEngine {
1406
1809
  o.allocated.push(epc);
1407
1810
  }
1408
1811
  this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
1409
- const task = { id: `task-${++this.taskSeq}`, kind: "cut", status: "created", itemEpc: o.allocated[0], fromNode: cut.id, toNode: cut.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "cut", fromNode: cut.id, toNode: cut.id, resourceKind: "cutter" }, CYCLE_MS), orderId: o.id, resourceType: "cutter", changeoverKey: product.gtin, setupMs: SETUP_MS, intent: "process" };
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" };
1410
1820
  this.tasks.set(task.id, task);
1411
1821
  this.emitTask(task);
1412
- o.status = "op-cut";
1413
- this.emitOrder(o);
1414
1822
  }
1415
- /** op 완료 = 변환. cut: BOM 부품 소비 WIP. weld: WIP 완제품(수율: 양품/불량 → OEE 품질 보고). */
1823
+ /** op 완료 = 변환. 라우트 인덱스로 분기: 첫=BOM 소비→WIP, 중간=WIP→WIP, 마지막(조립)=WIP→완성차(수율→OEE 품질). */
1416
1824
  onTaskComplete(t) {
1825
+ if (this.mesSpec) return this.onTaskCompleteDef(t);
1417
1826
  const order = this.orders.get(t.orderId);
1418
1827
  const product = this.productOf(order.gtin);
1419
- const eventTime = this.now();
1420
- if (t.kind === "cut") {
1421
- const cut = this.nodes.get(t.toNode);
1422
- const weld2 = this.nodeByType("weld-station");
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) {
1423
1832
  const inputs = order.allocated.slice();
1424
1833
  const wip2 = sgtinUri(CP2, WIP_ITEMREF, ++this.wipSeq);
1425
- this.transform(inputs, [{ epc: wip2, gtin: WIP_GTIN, qty: 1, location: cut.id, disposition: DISP.in_progress }], { bizStep: MES_BIZSTEP.producing, disposition: DISP.in_progress, transformationId: order.bizTransaction, readPoint: cut.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
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 }] });
1426
1835
  order.allocated = [wip2];
1427
- const wtask = { id: `task-${++this.taskSeq}`, kind: "weld", status: "created", itemEpc: wip2, fromNode: weld2.id, toNode: weld2.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "weld", fromNode: weld2.id, toNode: weld2.id, resourceKind: "welder" }, CYCLE_MS), orderId: order.id, resourceType: "welder", changeoverKey: product.gtin, setupMs: SETUP_MS, intent: "process" };
1428
- this.tasks.set(wtask.id, wtask);
1429
- this.emitTask(wtask);
1430
- order.status = "op-weld";
1836
+ const next = ROUTE[i + 1];
1837
+ this.emitStation(order, next, wip2, product.gtin);
1838
+ order.status = "op-" + next.kind;
1431
1839
  this.emitOrder(order);
1432
1840
  return;
1433
1841
  }
1434
- const weld = this.nodes.get(t.toNode);
1435
1842
  const fgStore = this.nodeByType("fg-store");
1436
1843
  const wip = order.allocated[0];
1437
1844
  const good = this.rng() < YIELD;
1438
1845
  this.recordOutput(t.resource, good);
1439
1846
  const disp = good ? DISP.sellable : DISP.non_sellable;
1440
1847
  const outputEpc = sgtinUri(CP2, product.ref, ++this.prodSeq);
1441
- 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: weld.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
1442
- 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 }));
1443
1952
  order.allocated = [];
1444
1953
  order.fulfilled = 1;
1445
1954
  order.status = good ? "produced" : "scrapped";
@@ -1452,6 +1961,8 @@ var MesKernel = class extends FlowEngine {
1452
1961
  BTT,
1453
1962
  BTT_DELIVERY,
1454
1963
  BTT_PRODORDER,
1964
+ CAPABILITIES,
1965
+ CAPABILITY_KEYS,
1455
1966
  CMD,
1456
1967
  DISP,
1457
1968
  DOMAIN_CATALOG,
@@ -1462,7 +1973,9 @@ var MesKernel = class extends FlowEngine {
1462
1973
  MES_BIZSTEP,
1463
1974
  MES_NODE_TYPES,
1464
1975
  MES_PART_GTINS,
1976
+ MES_PRODUCTS,
1465
1977
  MES_PRODUCT_GTINS,
1978
+ MES_TYPES,
1466
1979
  MesKernel,
1467
1980
  OP_EVENT,
1468
1981
  StateProjector,
@@ -1471,14 +1984,19 @@ var MesKernel = class extends FlowEngine {
1471
1984
  TwinRuntime,
1472
1985
  UTC_OFFSET,
1473
1986
  WMS_NODE_TYPES,
1987
+ WMS_TYPES,
1474
1988
  WmsKernel,
1475
1989
  YARD_BIZSTEP,
1476
1990
  YMS_NODE_TYPES,
1991
+ YMS_TYPES,
1477
1992
  YmsKernel,
1478
1993
  aggregationEvent,
1994
+ capabilitiesForType,
1479
1995
  compareStates,
1996
+ computeOee,
1480
1997
  constantDuration,
1481
1998
  counterfactualAt,
1999
+ deriveAttentions,
1482
2000
  fefoPolicy,
1483
2001
  firstFitPolicy,
1484
2002
  gdtiUri,
@@ -1492,7 +2010,9 @@ var MesKernel = class extends FlowEngine {
1492
2010
  sgtinClass,
1493
2011
  sgtinUri,
1494
2012
  ssccUri,
2013
+ stateFieldsOf,
1495
2014
  transactionEvent,
1496
2015
  transformationEvent,
2016
+ validateDomainDefinition,
1497
2017
  validateEpcisEvent
1498
2018
  });