@operato/twin-kernel 0.0.1

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.
Files changed (43) hide show
  1. package/README.md +80 -0
  2. package/dist/allocation-policy.d.ts +48 -0
  3. package/dist/allocation-policy.js +50 -0
  4. package/dist/contract.d.ts +205 -0
  5. package/dist/contract.js +20 -0
  6. package/dist/counterfactual.d.ts +40 -0
  7. package/dist/counterfactual.js +59 -0
  8. package/dist/divergence.d.ts +15 -0
  9. package/dist/divergence.js +28 -0
  10. package/dist/duration-estimator.d.ts +12 -0
  11. package/dist/duration-estimator.js +10 -0
  12. package/dist/epcis.d.ts +125 -0
  13. package/dist/epcis.js +172 -0
  14. package/dist/event-journal.d.ts +17 -0
  15. package/dist/event-journal.js +41 -0
  16. package/dist/face2-adapter.d.ts +42 -0
  17. package/dist/face2-adapter.js +69 -0
  18. package/dist/flow-engine.d.ts +224 -0
  19. package/dist/flow-engine.js +422 -0
  20. package/dist/forecast.d.ts +29 -0
  21. package/dist/forecast.js +34 -0
  22. package/dist/index.d.ts +19 -0
  23. package/dist/index.js +19 -0
  24. package/dist/kernel.d.ts +28 -0
  25. package/dist/kernel.js +187 -0
  26. package/dist/mes-kernel.d.ts +28 -0
  27. package/dist/mes-kernel.js +126 -0
  28. package/dist/mes-profile.d.ts +9 -0
  29. package/dist/mes-profile.js +17 -0
  30. package/dist/runtime.d.ts +42 -0
  31. package/dist/runtime.js +59 -0
  32. package/dist/state-projector.d.ts +37 -0
  33. package/dist/state-projector.js +124 -0
  34. package/dist/twin-observer.d.ts +28 -0
  35. package/dist/twin-observer.js +41 -0
  36. package/dist/wms-profile.d.ts +13 -0
  37. package/dist/wms-profile.js +20 -0
  38. package/dist/yms-kernel.d.ts +29 -0
  39. package/dist/yms-kernel.js +181 -0
  40. package/dist/yms-profile.d.ts +11 -0
  41. package/dist/yms-profile.js +22 -0
  42. package/dist-cjs/index.cjs +1477 -0
  43. package/package.json +30 -0
@@ -0,0 +1,1477 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.ts
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ BIZSTEP: () => BIZSTEP,
23
+ BTT: () => BTT,
24
+ BTT_DELIVERY: () => BTT_DELIVERY,
25
+ BTT_PRODORDER: () => BTT_PRODORDER,
26
+ CMD: () => CMD,
27
+ DISP: () => DISP,
28
+ EPCIS_CONTEXT: () => EPCIS_CONTEXT,
29
+ EventJournal: () => EventJournal,
30
+ FlowEngine: () => FlowEngine,
31
+ MES_BIZSTEP: () => MES_BIZSTEP,
32
+ MES_PART_GTINS: () => MES_PART_GTINS,
33
+ MES_PRODUCT_GTINS: () => MES_PRODUCT_GTINS,
34
+ MesKernel: () => MesKernel,
35
+ OP_EVENT: () => OP_EVENT,
36
+ StateProjector: () => StateProjector,
37
+ TwinHistory: () => TwinHistory,
38
+ TwinObserver: () => TwinObserver,
39
+ TwinRuntime: () => TwinRuntime,
40
+ UTC_OFFSET: () => UTC_OFFSET,
41
+ WmsKernel: () => WmsKernel,
42
+ YARD_BIZSTEP: () => YARD_BIZSTEP,
43
+ YmsKernel: () => YmsKernel,
44
+ aggregationEvent: () => aggregationEvent,
45
+ compareStates: () => compareStates,
46
+ constantDuration: () => constantDuration,
47
+ counterfactualAt: () => counterfactualAt,
48
+ fefoPolicy: () => fefoPolicy,
49
+ firstFitPolicy: () => firstFitPolicy,
50
+ gdtiUri: () => gdtiUri,
51
+ graiUri: () => graiUri,
52
+ ingest: () => ingest,
53
+ mapRecord: () => mapRecord,
54
+ monteCarloForecast: () => monteCarloForecast,
55
+ objectEvent: () => objectEvent,
56
+ partialFitPolicy: () => partialFitPolicy,
57
+ replay: () => replay,
58
+ sgtinClass: () => sgtinClass,
59
+ sgtinUri: () => sgtinUri,
60
+ ssccUri: () => ssccUri,
61
+ transactionEvent: () => transactionEvent,
62
+ transformationEvent: () => transformationEvent,
63
+ validateEpcisEvent: () => validateEpcisEvent
64
+ });
65
+ module.exports = __toCommonJS(index_exports);
66
+
67
+ // src/contract.ts
68
+ var OP_EVENT = {
69
+ task: "task.status",
70
+ equipment: "equipment.status",
71
+ order: "order.status"
72
+ };
73
+ var CMD = {
74
+ orderHold: "order.hold",
75
+ orderResume: "order.resume",
76
+ orderRelease: "order.release"
77
+ };
78
+
79
+ // src/divergence.ts
80
+ function diffBy(predicted, actual, idOf, valOf) {
81
+ const p = new Map(predicted.map((e) => [idOf(e), valOf(e)]));
82
+ const a = new Map(actual.map((e) => [idOf(e), valOf(e)]));
83
+ const out = [];
84
+ for (const id of /* @__PURE__ */ new Set([...p.keys(), ...a.keys()])) {
85
+ const pv = p.get(id);
86
+ const av = a.get(id);
87
+ if (pv !== av) out.push({ id, predicted: pv, actual: av });
88
+ }
89
+ return out.sort((x, y) => x.id.localeCompare(y.id));
90
+ }
91
+ function compareStates(predicted, actual) {
92
+ const itemLocation = diffBy(predicted.items, actual.items, (i) => i.epc, (i) => i.location);
93
+ const itemDisposition = diffBy(predicted.items, actual.items, (i) => i.epc, (i) => i.disposition);
94
+ const nodeOccupancy = diffBy(predicted.nodes, actual.nodes, (n) => n.id, (n) => n.occupancy);
95
+ const orderStatus = diffBy(predicted.orders, actual.orders, (o) => o.id, (o) => o.status);
96
+ const hasDrift = itemLocation.length > 0 || itemDisposition.length > 0 || nodeOccupancy.length > 0 || orderStatus.length > 0;
97
+ return { hasDrift, itemLocation, itemDisposition, nodeOccupancy, orderStatus };
98
+ }
99
+
100
+ // src/counterfactual.ts
101
+ var TwinHistory = class {
102
+ live;
103
+ tickMs;
104
+ checkpoints = [];
105
+ constructor(live, opts = {}) {
106
+ this.live = live;
107
+ this.tickMs = opts.tickMs ?? 1e3;
108
+ }
109
+ /** 현재를 체크포인트로 저장(호스트가 주기적으로 호출). */
110
+ checkpoint() {
111
+ this.checkpoints.push({ simMs: this.live.getSnapshot().simClockMs, twin: this.live.fork() });
112
+ }
113
+ get count() {
114
+ return this.checkpoints.length;
115
+ }
116
+ /** 시각 T 의 전체 커널 — T 이하 최신 체크포인트에서 fork 후 정확히 T 로 재구동(결정적). */
117
+ at(simMs) {
118
+ let best;
119
+ for (const c of this.checkpoints) if (c.simMs <= simMs) best = c;
120
+ if (!best) return void 0;
121
+ const t = best.twin.fork();
122
+ let guard = 0;
123
+ while (t.getSnapshot().simClockMs < simMs && guard++ < 1e6) t.tick(this.tickMs);
124
+ return t;
125
+ }
126
+ };
127
+ function counterfactualAt(history, atSimMs, opts) {
128
+ const base = history.at(atSimMs);
129
+ if (!base) return void 0;
130
+ const step = opts.tickMs ?? 1e3;
131
+ const target = atSimMs + opts.horizonMs;
132
+ const withAlt = base.fork();
133
+ opts.alternative(withAlt);
134
+ const baseline = base.fork();
135
+ const run = (t) => {
136
+ let g = 0;
137
+ while (t.getSnapshot().simClockMs < target && g++ < 1e6) t.tick(step);
138
+ };
139
+ run(withAlt);
140
+ run(baseline);
141
+ return { atSimMs, withAlt: withAlt.getSnapshot(), baseline: baseline.getSnapshot(), effect: compareStates(withAlt.getSnapshot(), baseline.getSnapshot()) };
142
+ }
143
+
144
+ // src/forecast.ts
145
+ function monteCarloForecast(twin, opts) {
146
+ const now = twin.getSnapshot().simClockMs;
147
+ const step = opts.tickMs ?? 1e3;
148
+ const baseSeed = opts.scenario.seed ?? 1;
149
+ const samples = [];
150
+ for (let i = 0; i < opts.runs; i++) {
151
+ const fc = twin.fork();
152
+ fc.scenario.load({ ...opts.scenario, seed: baseSeed + i });
153
+ fc.scenario.start();
154
+ const target = now + opts.horizonMs;
155
+ let guard = 0;
156
+ while (fc.getSnapshot().simClockMs < target && guard++ < 1e6) fc.tick(step);
157
+ samples.push(opts.metric(fc.getSnapshot()));
158
+ }
159
+ return summarize(opts.runs, samples);
160
+ }
161
+ function summarize(runs, samples) {
162
+ const sorted = [...samples].sort((a, b) => a - b);
163
+ const pct = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
164
+ const mean = samples.reduce((a, b) => a + b, 0) / (samples.length || 1);
165
+ return { runs, samples, min: sorted[0], max: sorted[sorted.length - 1], mean, p50: pct(0.5), p90: pct(0.9) };
166
+ }
167
+
168
+ // src/twin-observer.ts
169
+ var TwinObserver = class {
170
+ live;
171
+ opts;
172
+ pending = [];
173
+ constructor(live, opts) {
174
+ this.live = live;
175
+ this.opts = opts;
176
+ }
177
+ /** 관측 1회 — 만기 예측을 실제와 대조(발산 알림) + 새 예측 생성. 호스트가 주기적으로 호출. */
178
+ observe() {
179
+ const now = this.live.getSnapshot().simClockMs;
180
+ const due = this.pending.filter((p) => p.horizonSimMs <= now);
181
+ this.pending = this.pending.filter((p) => p.horizonSimMs > now);
182
+ for (const p of due) {
183
+ const d = compareStates(p.predicted, this.live.getSnapshot());
184
+ if (d.hasDrift) this.opts.onDivergence?.({ forecastAtSimMs: p.madeAtSimMs, horizonSimMs: p.horizonSimMs, divergence: d });
185
+ }
186
+ const fc = this.live.fork();
187
+ const step = this.opts.tickMs ?? 1e3;
188
+ const target = now + this.opts.horizonMs;
189
+ let guard = 0;
190
+ while (fc.getSnapshot().simClockMs < target && guard++ < 1e6) fc.tick(step);
191
+ this.pending.push({ madeAtSimMs: now, horizonSimMs: fc.getSnapshot().simClockMs, predicted: fc.getSnapshot() });
192
+ }
193
+ /** 대기 중(아직 만기 안 된) 예측 수 — 진단용. */
194
+ get pendingCount() {
195
+ return this.pending.length;
196
+ }
197
+ };
198
+
199
+ // src/state-projector.ts
200
+ var StateProjector = class {
201
+ master = /* @__PURE__ */ new Map();
202
+ items = /* @__PURE__ */ new Map();
203
+ aggregation = /* @__PURE__ */ new Map();
204
+ // parent SSCC → child EPCs
205
+ tasks = /* @__PURE__ */ new Map();
206
+ movers = /* @__PURE__ */ new Map();
207
+ orders = /* @__PURE__ */ new Map();
208
+ revision = 0;
209
+ constructor(board) {
210
+ for (const n of board.nodes) this.master.set(n.id, { id: n.id, type: n.type, capacity: n.capacity });
211
+ for (const m of board.movers) this.movers.set(m.id, { id: m.id, kind: m.kind, status: "idle", location: m.homeNode });
212
+ }
213
+ /** 마스터 동기 — 로케이션 추가/변경/제거. */
214
+ applyMaster(u) {
215
+ if (u.op === "remove") {
216
+ this.master.delete(u.node.id);
217
+ return;
218
+ }
219
+ const cur = this.master.get(u.node.id);
220
+ this.master.set(u.node.id, {
221
+ id: u.node.id,
222
+ type: u.node.type ?? cur?.type ?? "unknown",
223
+ capacity: u.node.capacity ?? cur?.capacity ?? 0
224
+ });
225
+ }
226
+ /** 이벤트 1건 반영 — eventType 으로 EPCIS vs 운영 델타 분기. */
227
+ apply(e) {
228
+ this.revision++;
229
+ if (e.eventType.startsWith("epcis.")) {
230
+ this.applyEpcis(e.data);
231
+ return;
232
+ }
233
+ switch (e.eventType) {
234
+ case OP_EVENT.task: {
235
+ const d = e.data;
236
+ this.tasks.set(d.taskId, { id: d.taskId, kind: d.kind, status: d.status, fromNode: d.fromNode, toNode: d.toNode, itemRefs: d.itemRefs, resourceRef: d.resourceRef });
237
+ break;
238
+ }
239
+ case OP_EVENT.equipment: {
240
+ const d = e.data;
241
+ this.movers.set(d.moverId, { id: d.moverId, kind: d.kind, status: d.status, location: d.location, motion: d.motion });
242
+ break;
243
+ }
244
+ case OP_EVENT.order: {
245
+ const d = e.data;
246
+ this.orders.set(d.orderId, { id: d.orderId, kind: d.kind, status: d.status, progress: d.requested ? d.fulfilled / d.requested : 0, held: d.held });
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ applyEpcis(ev) {
252
+ if (ev.type === "AggregationEvent") {
253
+ if (ev.action === "ADD" && ev.childEPCs?.length) this.aggregation.set(ev.parentID, [...ev.childEPCs]);
254
+ else if (ev.action === "DELETE") this.aggregation.delete(ev.parentID);
255
+ return;
256
+ }
257
+ if (ev.type === "TransactionEvent") return;
258
+ if (ev.type === "TransformationEvent") {
259
+ for (const epc of ev.inputEPCList ?? []) this.remove(epc);
260
+ const loc2 = ev.readPoint?.id ?? "";
261
+ for (const epc of ev.outputEPCList ?? []) this.items.set(epc, { epc, location: loc2, disposition: ev.disposition });
262
+ return;
263
+ }
264
+ if (ev.action === "DELETE") {
265
+ for (const epc of ev.epcList) this.remove(epc);
266
+ return;
267
+ }
268
+ const loc = ev.readPoint?.id;
269
+ const gtin = ev.quantityList?.[0]?.epcClass;
270
+ for (const epc of ev.epcList) {
271
+ const cur = this.items.get(epc);
272
+ this.items.set(epc, { epc, gtin: gtin ?? cur?.gtin, location: loc ?? cur?.location ?? "", disposition: ev.disposition ?? cur?.disposition });
273
+ }
274
+ }
275
+ /** 이탈(DELETE) — 아이템 + 조립 자식(재귀) 제거. 화물 SSCC DELETE 시 팔레트도 함께 이탈. */
276
+ remove(epc) {
277
+ this.items.delete(epc);
278
+ const children = this.aggregation.get(epc);
279
+ if (children) {
280
+ this.aggregation.delete(epc);
281
+ for (const c of children) this.remove(c);
282
+ }
283
+ }
284
+ /** 현재 투영 State — 노드 점유는 아이템 위치 집계로 유도(pure projection). */
285
+ snapshot() {
286
+ const occ = /* @__PURE__ */ new Map();
287
+ for (const it of this.items.values()) occ.set(it.location, (occ.get(it.location) ?? 0) + 1);
288
+ return {
289
+ 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 })),
291
+ items: [...this.items.values()].map((i) => ({ epc: i.epc, gtin: i.gtin, location: i.location, disposition: i.disposition })),
292
+ tasks: [...this.tasks.values()].map((t) => ({ ...t })),
293
+ movers: [...this.movers.values()].map((m) => ({ ...m })),
294
+ orders: [...this.orders.values()].map((o) => ({ ...o }))
295
+ };
296
+ }
297
+ };
298
+
299
+ // src/event-journal.ts
300
+ var EventJournal = class {
301
+ events = [];
302
+ /** 이벤트 1건 기록(추가 전용). runtime/kernel 의 onEvent 에 연결. */
303
+ append(e) {
304
+ this.events.push(e);
305
+ }
306
+ get length() {
307
+ return this.events.length;
308
+ }
309
+ all() {
310
+ return this.events;
311
+ }
312
+ /** 처음 revision 개(= 시점 T까지). revision 은 emit 당 1 증가하므로 slice(0,rev). */
313
+ until(revision) {
314
+ return this.events.slice(0, revision);
315
+ }
316
+ /** revision 이후(재동기·증분 전송용). */
317
+ since(revision) {
318
+ return this.events.slice(revision);
319
+ }
320
+ /** sim 시각(ISO eventTime) 이하 이벤트. */
321
+ untilSimTime(iso) {
322
+ return this.events.filter((e) => e.eventTime <= iso);
323
+ }
324
+ };
325
+ function replay(board, events) {
326
+ const proj = new StateProjector(board);
327
+ for (const e of events) proj.apply(e);
328
+ return proj.snapshot();
329
+ }
330
+
331
+ // src/epcis.ts
332
+ var EPCIS_CONTEXT = "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld";
333
+ var UTC_OFFSET = "+00:00";
334
+ var DISP = {
335
+ in_progress: "urn:epcglobal:cbv:disp:in_progress",
336
+ sellable: "urn:epcglobal:cbv:disp:sellable_accessible",
337
+ reserved: "urn:epcglobal:cbv:disp:reserved",
338
+ in_transit: "urn:epcglobal:cbv:disp:in_transit",
339
+ non_sellable: "urn:epcglobal:cbv:disp:non_sellable_other"
340
+ // 불량/scrap
341
+ };
342
+ function ssccUri(companyPrefix, serial) {
343
+ return `urn:epc:id:sscc:${companyPrefix}.${String(serial).padStart(10, "0")}`;
344
+ }
345
+ function sgtinClass(companyPrefix, itemRef) {
346
+ return `urn:epc:idpat:sgtin:${companyPrefix}.${itemRef}.*`;
347
+ }
348
+ function gdtiUri(companyPrefix, docType, serial) {
349
+ return `urn:epc:id:gdti:${companyPrefix}.${docType}.${serial}`;
350
+ }
351
+ function header(type, eventTime, bizStep) {
352
+ return { "@context": EPCIS_CONTEXT, type, eventTime, eventTimeZoneOffset: UTC_OFFSET, bizStep };
353
+ }
354
+ function common(type, eventTime, action, bizStep) {
355
+ return { ...header(type, eventTime, bizStep), action };
356
+ }
357
+ function objectEvent(p) {
358
+ const e = { ...common("ObjectEvent", p.eventTime, p.action, p.bizStep), epcList: p.epcList };
359
+ if (p.disposition) e.disposition = p.disposition;
360
+ if (p.quantityList) e.quantityList = p.quantityList;
361
+ if (p.readPoint) e.readPoint = { id: p.readPoint };
362
+ if (p.bizLocation) e.bizLocation = { id: p.bizLocation };
363
+ if (p.bizTransactionList) e.bizTransactionList = p.bizTransactionList;
364
+ return e;
365
+ }
366
+ function aggregationEvent(p) {
367
+ const e = { ...common("AggregationEvent", p.eventTime, p.action, p.bizStep), parentID: p.parentID };
368
+ if (p.disposition) e.disposition = p.disposition;
369
+ if (p.childEPCs) e.childEPCs = p.childEPCs;
370
+ if (p.childQuantityList) e.childQuantityList = p.childQuantityList;
371
+ if (p.readPoint) e.readPoint = { id: p.readPoint };
372
+ if (p.bizLocation) e.bizLocation = { id: p.bizLocation };
373
+ return e;
374
+ }
375
+ function transactionEvent(p) {
376
+ const e = {
377
+ ...common("TransactionEvent", p.eventTime, p.action, p.bizStep),
378
+ bizTransactionList: p.bizTransactionList
379
+ };
380
+ if (p.disposition) e.disposition = p.disposition;
381
+ if (p.parentID) e.parentID = p.parentID;
382
+ if (p.epcList) e.epcList = p.epcList;
383
+ if (p.quantityList) e.quantityList = p.quantityList;
384
+ if (p.readPoint) e.readPoint = { id: p.readPoint };
385
+ if (p.bizLocation) e.bizLocation = { id: p.bizLocation };
386
+ return e;
387
+ }
388
+ function transformationEvent(p) {
389
+ const e = header("TransformationEvent", p.eventTime, p.bizStep);
390
+ if (p.disposition) e.disposition = p.disposition;
391
+ if (p.inputEPCList) e.inputEPCList = p.inputEPCList;
392
+ if (p.inputQuantityList) e.inputQuantityList = p.inputQuantityList;
393
+ if (p.outputEPCList) e.outputEPCList = p.outputEPCList;
394
+ if (p.outputQuantityList) e.outputQuantityList = p.outputQuantityList;
395
+ if (p.transformationID) e.transformationID = p.transformationID;
396
+ if (p.readPoint) e.readPoint = { id: p.readPoint };
397
+ if (p.bizLocation) e.bizLocation = { id: p.bizLocation };
398
+ if (p.bizTransactionList) e.bizTransactionList = p.bizTransactionList;
399
+ return e;
400
+ }
401
+ var ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
402
+ var TZ_RE = /^[+-]\d{2}:\d{2}$/;
403
+ var ACTIONS = ["ADD", "OBSERVE", "DELETE"];
404
+ function validateEpcisEvent(e) {
405
+ const v = [];
406
+ if (e["@context"] !== EPCIS_CONTEXT) v.push("@context \uB204\uB77D/\uBD88\uC77C\uCE58");
407
+ if (!["ObjectEvent", "AggregationEvent", "TransactionEvent", "TransformationEvent"].includes(e.type)) v.push(`\uC54C \uC218 \uC5C6\uB294 type: ${e.type}`);
408
+ if (typeof e.eventTime !== "string" || !ISO_RE.test(e.eventTime)) v.push("eventTime ISO8601 \uC544\uB2D8");
409
+ if (typeof e.eventTimeZoneOffset !== "string" || !TZ_RE.test(e.eventTimeZoneOffset)) v.push("eventTimeZoneOffset \uD615\uC2DD \uC624\uB958");
410
+ if (typeof e.bizStep !== "string" || !e.bizStep) v.push("bizStep \uB204\uB77D");
411
+ if (e.type === "ObjectEvent") {
412
+ if (!ACTIONS.includes(e.action)) v.push(`action \uBD80\uC815: ${e.action}`);
413
+ if (!Array.isArray(e.epcList)) v.push("ObjectEvent.epcList \uB204\uB77D");
414
+ else if (e.epcList.length === 0 && !e.quantityList?.length) v.push("ObjectEvent: epcList/quantityList \uB458 \uB2E4 \uBE44\uC5B4\uC788\uC74C");
415
+ } else if (e.type === "AggregationEvent") {
416
+ if (!ACTIONS.includes(e.action)) v.push(`action \uBD80\uC815: ${e.action}`);
417
+ if (!e.parentID) v.push("AggregationEvent.parentID \uB204\uB77D");
418
+ if (!e.childEPCs?.length && !e.childQuantityList?.length) v.push("AggregationEvent: childEPCs/childQuantityList \uB458 \uB2E4 \uBE44\uC5B4\uC788\uC74C");
419
+ } else if (e.type === "TransactionEvent") {
420
+ if (!ACTIONS.includes(e.action)) v.push(`action \uBD80\uC815: ${e.action}`);
421
+ if (!Array.isArray(e.bizTransactionList) || e.bizTransactionList.length === 0) v.push("TransactionEvent.bizTransactionList \uB204\uB77D");
422
+ } else if (e.type === "TransformationEvent") {
423
+ if (!e.inputEPCList?.length && !e.inputQuantityList?.length) v.push("TransformationEvent: input \uBE44\uC5B4\uC788\uC74C");
424
+ if (!e.outputEPCList?.length && !e.outputQuantityList?.length) v.push("TransformationEvent: output \uBE44\uC5B4\uC788\uC74C");
425
+ }
426
+ const qtyLists = [
427
+ "quantityList" in e ? e.quantityList : void 0,
428
+ "childQuantityList" in e ? e.childQuantityList : void 0,
429
+ "inputQuantityList" in e ? e.inputQuantityList : void 0,
430
+ "outputQuantityList" in e ? e.outputQuantityList : void 0
431
+ ];
432
+ for (const list of qtyLists) for (const q of list ?? []) {
433
+ if (!q.epcClass?.startsWith("urn:epc:idpat:") && !q.epcClass?.startsWith("urn:epc:class:")) v.push(`quantity epcClass \uBD80\uC815: ${q.epcClass}`);
434
+ if (typeof q.quantity !== "number" || q.quantity < 0) v.push("quantity \uBD80\uC815");
435
+ }
436
+ return v;
437
+ }
438
+
439
+ // src/wms-profile.ts
440
+ var BIZSTEP = {
441
+ receiving: "urn:epcglobal:cbv:bizstep:receiving",
442
+ storing: "urn:epcglobal:cbv:bizstep:storing",
443
+ picking: "urn:epcglobal:cbv:bizstep:picking",
444
+ packing: "urn:epcglobal:cbv:bizstep:packing",
445
+ staging_outbound: "urn:epcglobal:cbv:bizstep:staging_outbound",
446
+ shipping: "urn:epcglobal:cbv:bizstep:shipping",
447
+ replenishing: "urn:epcglobal:cbv:bizstep:replenishing"
448
+ };
449
+ var BTT = {
450
+ po: "urn:epcglobal:cbv:btt:po",
451
+ so: "urn:epcglobal:cbv:btt:so"
452
+ };
453
+
454
+ // src/allocation-policy.ts
455
+ function freeBinsFirstFit(slots) {
456
+ const sorted = [...slots].sort((a, b) => a.id.localeCompare(b.id));
457
+ for (const b of sorted) if (b.occupancy + b.reserved < b.capacity) return b.id;
458
+ return null;
459
+ }
460
+ function sortByEpc(available) {
461
+ return [...available].sort((a, b) => a.epc.localeCompare(b.epc));
462
+ }
463
+ var firstFitPolicy = {
464
+ selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
465
+ selectStock: ({ qty, available }) => {
466
+ if (available.length < qty) return [];
467
+ return sortByEpc(available).slice(0, qty).map((s) => s.epc);
468
+ }
469
+ };
470
+ var partialFitPolicy = {
471
+ selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
472
+ selectStock: ({ qty, available }) => sortByEpc(available).slice(0, qty).map((s) => s.epc)
473
+ };
474
+ var fefoPolicy = {
475
+ selectPlacement: ({ slots }) => freeBinsFirstFit(slots),
476
+ selectStock: ({ qty, available }) => {
477
+ if (available.length < qty) return [];
478
+ const byExpiry = [...available].sort((a, b) => (a.expiry ?? Infinity) - (b.expiry ?? Infinity) || a.epc.localeCompare(b.epc));
479
+ return byExpiry.slice(0, qty).map((s) => s.epc);
480
+ }
481
+ };
482
+
483
+ // src/duration-estimator.ts
484
+ var constantDuration = (ms) => ({ estimate: () => ms });
485
+
486
+ // src/face2-adapter.ts
487
+ function get(obj, path) {
488
+ return path.split(".").reduce((o, k) => o == null ? o : o[k], obj);
489
+ }
490
+ function resolve(v, record) {
491
+ if (v === void 0) return void 0;
492
+ if (v.startsWith("$.")) {
493
+ const r = get(record, v.slice(2));
494
+ return r === void 0 || r === null ? void 0 : String(r);
495
+ }
496
+ return v;
497
+ }
498
+ function mapRecord(record, mapping, eventTime) {
499
+ const epc = resolve(mapping.epc, record);
500
+ return objectEvent({
501
+ eventTime,
502
+ action: resolve(mapping.action, record) ?? "",
503
+ bizStep: resolve(mapping.bizStep, record) ?? "",
504
+ disposition: resolve(mapping.disposition, record),
505
+ epcList: epc ? [epc] : [],
506
+ readPoint: resolve(mapping.readPoint, record),
507
+ bizLocation: resolve(mapping.bizLocation, record)
508
+ });
509
+ }
510
+ function ingest(records, rules, opts) {
511
+ const ruleByType = new Map(rules.map((r) => [r.sourceType, r]));
512
+ const accepted = [];
513
+ const rejected = [];
514
+ let seq = 0;
515
+ for (const record of records) {
516
+ const rule = ruleByType.get(String(record["sourceType"]));
517
+ if (!rule) {
518
+ rejected.push({ record, errors: [`\uB9E4\uCE6D \uB8F0 \uC5C6\uC74C: sourceType=${record["sourceType"]}`] });
519
+ continue;
520
+ }
521
+ const eventTime = (opts.eventTimePath ? resolve("$." + opts.eventTimePath, record) : void 0) ?? opts.defaultEventTime;
522
+ const ev = mapRecord(record, rule.mapping, eventTime);
523
+ const errors = validateEpcisEvent(ev);
524
+ if (errors.length) {
525
+ rejected.push({ record, errors });
526
+ continue;
527
+ }
528
+ accepted.push({
529
+ eventId: `${opts.tenantId}-ingest-${++seq}`,
530
+ eventType: `epcis.${ev.type}`,
531
+ eventTime: ev.eventTime,
532
+ tenantId: opts.tenantId,
533
+ data: ev
534
+ });
535
+ }
536
+ return { accepted, rejected };
537
+ }
538
+
539
+ // src/runtime.ts
540
+ var TwinRuntime = class {
541
+ kernel;
542
+ subscribers = /* @__PURE__ */ new Set();
543
+ currentRevision;
544
+ constructor(kernel) {
545
+ this.kernel = kernel;
546
+ this.currentRevision = kernel.getSnapshot().revision;
547
+ kernel.onEvent((event) => {
548
+ this.currentRevision++;
549
+ const msg = { kind: "delta", revision: this.currentRevision, event };
550
+ for (const s of this.subscribers) s(msg);
551
+ });
552
+ }
553
+ /** State 채널 구독 — 즉시 현재 snapshot, 이후 델타(revision 연속). 멀티 구독자 독립. */
554
+ subscribe(handler) {
555
+ handler({ kind: "snapshot", revision: this.currentRevision, state: this.kernel.getSnapshot() });
556
+ this.subscribers.add(handler);
557
+ return { unsubscribe: () => {
558
+ this.subscribers.delete(handler);
559
+ } };
560
+ }
561
+ /** gap(revision 불연속) 감지 시 재동기 — 현재 스냅샷. */
562
+ resync() {
563
+ return { revision: this.currentRevision, state: this.kernel.getSnapshot() };
564
+ }
565
+ /** 클록-싱크 하트비트 메시지 — host 가 자기 send-rate 로 방송(⊥ tick). 클라 보간의 nowSim. */
566
+ nowClock() {
567
+ return { kind: "clock", revision: this.currentRevision, simClockMs: this.kernel.getSnapshot().simClockMs };
568
+ }
569
+ /** Command 채널. */
570
+ dispatch(cmd) {
571
+ return this.kernel.dispatch(cmd);
572
+ }
573
+ /** Clock — host 가 서버 클럭/RAF 로 구동. 실행당 엔진 1개(ADR-0010). */
574
+ tick(dtMs) {
575
+ this.kernel.tick(dtMs);
576
+ }
577
+ /** Scenario 채널(sim 모드). */
578
+ get scenario() {
579
+ return this.kernel.scenario;
580
+ }
581
+ get revision() {
582
+ return this.currentRevision;
583
+ }
584
+ };
585
+
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
+ // src/flow-engine.ts
619
+ var BASE_EPOCH = Date.parse("2026-01-01T00:00:00Z");
620
+ function mulberry32(seed) {
621
+ let a = seed >>> 0;
622
+ const fn = () => {
623
+ a = a + 1831565813 | 0;
624
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
625
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
626
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
627
+ };
628
+ Object.defineProperty(fn, "state", { get: () => a, set: (v) => {
629
+ a = v >>> 0;
630
+ } });
631
+ return fn;
632
+ }
633
+ var FlowEngine = class {
634
+ tenantId;
635
+ nodes = /* @__PURE__ */ new Map();
636
+ items = /* @__PURE__ */ new Map();
637
+ movers = /* @__PURE__ */ new Map();
638
+ tasks = /* @__PURE__ */ new Map();
639
+ orders = /* @__PURE__ */ new Map();
640
+ revision = 0;
641
+ clockMs = 0;
642
+ rng = mulberry32(1);
643
+ policy;
644
+ /** duration 시임(선택) — 미주입 시 도메인 상수. 씬/보드 바인딩이 거리·속도 기반 estimator 주입. */
645
+ durationEstimator;
646
+ epcSeq = 0;
647
+ taskSeq = 0;
648
+ orderSeq = 0;
649
+ soSeq = 0;
650
+ handlers = [];
651
+ gens = [];
652
+ generating = false;
653
+ speed = 1;
654
+ eventSeq = 0;
655
+ constructor(tenantId, policy) {
656
+ this.tenantId = tenantId;
657
+ this.policy = policy;
658
+ }
659
+ // ── TwinKernel (mechanics, 도메인 무관) ───────────────────────────────────
660
+ 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" });
662
+ for (const m of def.movers) {
663
+ 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
+ if (m.mtbfMs !== void 0) {
665
+ mover.mtbfMs = m.mtbfMs;
666
+ mover.mttrMs = m.mttrMs;
667
+ mover.nextFailureMs = this.sampleExp(m.mtbfMs);
668
+ }
669
+ this.movers.set(m.id, mover);
670
+ }
671
+ }
672
+ onEvent(handler) {
673
+ this.handlers.push(handler);
674
+ return () => {
675
+ const i = this.handlers.indexOf(handler);
676
+ if (i >= 0) this.handlers.splice(i, 1);
677
+ };
678
+ }
679
+ /**
680
+ * Command 채널 — 트윈의 "행위(act)" 면. 코어 공통 커맨드(order.hold/resume)는 여기서,
681
+ * 도메인 커맨드(order.release 등)는 handleCommand 로 위임. 커맨드는 sim 상태를 변이하고
682
+ * State 델타를 유발한다(command → 행위 → 관측 폐루프).
683
+ */
684
+ dispatch(cmd) {
685
+ const ok = () => ({ commandId: cmd.commandId, accepted: true });
686
+ const fail = (error) => ({ commandId: cmd.commandId, accepted: false, error });
687
+ switch (cmd.type) {
688
+ case CMD.orderHold:
689
+ case CMD.orderResume: {
690
+ const orderId = cmd.args?.orderId;
691
+ const order = orderId ? this.orders.get(orderId) : void 0;
692
+ if (!order) return fail(`order \uC5C6\uC74C: ${orderId}`);
693
+ order.held = cmd.type === CMD.orderHold;
694
+ this.emitOrder(order);
695
+ return ok();
696
+ }
697
+ default:
698
+ return this.handleCommand(cmd);
699
+ }
700
+ }
701
+ /** 도메인 커맨드 처리(order.release 등). 기본은 거절 — 도메인이 override. */
702
+ handleCommand(cmd) {
703
+ return { commandId: cmd.commandId, accepted: false, error: `\uC54C \uC218 \uC5C6\uB294 \uCEE4\uB9E8\uB4DC: ${cmd.type}` };
704
+ }
705
+ scenario = {
706
+ load: (def) => {
707
+ this.rng = mulberry32((def.seed ?? 1) >>> 0);
708
+ this.speed = def.speed ?? 1;
709
+ this.gens = def.generators.map((spec) => ({ spec, nextMs: 0 }));
710
+ },
711
+ start: () => {
712
+ if (this.generating) return;
713
+ this.generating = true;
714
+ for (const g of this.gens) g.nextMs = this.clockMs + this.intervalMs(g.spec);
715
+ },
716
+ pause: () => {
717
+ this.generating = false;
718
+ },
719
+ reset: () => {
720
+ this.generating = false;
721
+ this.gens = [];
722
+ },
723
+ setSpeed: (f) => {
724
+ this.speed = f;
725
+ }
726
+ };
727
+ tick(dtMs) {
728
+ const dt = dtMs * this.speed;
729
+ this.clockMs += dt;
730
+ if (this.generating) this.generate();
731
+ this.processFailures(dt);
732
+ this.processOrders();
733
+ this.processTasks(dt);
734
+ }
735
+ getSnapshot() {
736
+ return {
737
+ revision: this.revision,
738
+ simClockMs: this.clockMs,
739
+ nodes: [...this.nodes.values()].map((n) => ({ ...n })),
740
+ 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
+ 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) };
743
+ const t = m.taskId ? this.tasks.get(m.taskId) : void 0;
744
+ 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
+ return s;
746
+ }),
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 }))
749
+ };
750
+ }
751
+ /**
752
+ * fork — 현재 상태를 정확히 복제한 새 엔진 (디지털트윈 본연: "현재로부터 예측").
753
+ * 원본(live/sim)은 계속 진행, fork 는 what-if 를 앞으로 굴려 forecast·발산(predicted vs actual) 검사에 쓴다.
754
+ * fork 는 자기 구독자·시나리오를 갖고 원본과 격리(handlers·gens 비움, generating=false).
755
+ * rng 는 fork 의 시나리오 load 시 재시드(드레인 예측은 생성 없어 rng 무관·결정적).
756
+ */
757
+ fork(tenantId = this.tenantId) {
758
+ const Ctor = this.constructor;
759
+ const clone = new Ctor(tenantId, this.policy);
760
+ const skip = /* @__PURE__ */ new Set(["policy", "scenario", "tenantId", "handlers", "durationEstimator"]);
761
+ for (const key of Object.keys(this)) {
762
+ if (skip.has(key)) continue;
763
+ const v = this[key];
764
+ if (typeof v === "function") continue;
765
+ clone[key] = v instanceof Map ? new Map([...v.entries()].map(([k, o]) => [k, structuredClone(o)])) : structuredClone(v);
766
+ }
767
+ clone.rng.state = this.rng.state;
768
+ clone.durationEstimator = this.durationEstimator;
769
+ return clone;
770
+ }
771
+ // ── 보호 헬퍼 (도메인 hook 에서 사용) ──────────────────────────────────────
772
+ now() {
773
+ return new Date(BASE_EPOCH + this.clockMs).toISOString();
774
+ }
775
+ randInt(min, max) {
776
+ return max <= min ? min : min + Math.floor(this.rng() * (max - min + 1));
777
+ }
778
+ /** task 소요 산출 — estimator 주입 시 그 값, 미주입/undefined 시 도메인 상수(fallback). "얼마"만 소비, "경로"는 씬. */
779
+ durationOf(ctx, fallbackMs) {
780
+ return this.durationEstimator?.estimate(ctx) ?? fallbackMs;
781
+ }
782
+ /** skuMix 에서 weight 로 gtin 선택(rng) — 도착/오더 자극의 품목 결정. */
783
+ pickGtin(mix) {
784
+ const total = mix.reduce((s, m) => s + m.weight, 0);
785
+ let r = this.rng() * total;
786
+ for (const m of mix) {
787
+ r -= m.weight;
788
+ if (r <= 0) return m.gtin;
789
+ }
790
+ return mix[mix.length - 1].gtin;
791
+ }
792
+ nodeByType(type) {
793
+ for (const n of this.nodes.values()) if (n.type === type) return n;
794
+ return void 0;
795
+ }
796
+ /**
797
+ * process 변화 대수(EPCIS TransformationEvent · ISA-95 Material Consumed/Produced · 씬 Processable.transform).
798
+ * inputs 소비 → outputs 생산. 입출력 arity 가 곧 대수:
799
+ * merge N→1(조립) · split 1→N(분해) · transform 1→1(타입변경) · loss N→0(소실) · gain 0→N(부산물·생성).
800
+ * 아이템 상태(소비/생산)와 노드 점유를 갱신하고 계보(TransformationEvent)를 방출한다.
801
+ * 도메인은 이 원시만 호출 — onTaskComplete 의 소비/생산/EPCIS 손코딩을 대체.
802
+ */
803
+ transform(inputs, outputs, opts) {
804
+ for (const epc of inputs) {
805
+ const it = this.items.get(epc);
806
+ if (!it) continue;
807
+ const n = this.nodes.get(it.location);
808
+ if (n) n.occupancy--;
809
+ this.items.delete(epc);
810
+ }
811
+ for (const o of outputs) {
812
+ this.items.set(o.epc, { epc: o.epc, gtin: o.gtin, qty: o.qty, location: o.location, disposition: o.disposition });
813
+ const n = this.nodes.get(o.location);
814
+ if (n) n.occupancy++;
815
+ }
816
+ this.emit(transformationEvent({
817
+ eventTime: this.now(),
818
+ bizStep: opts.bizStep,
819
+ disposition: opts.disposition,
820
+ inputEPCList: inputs.length ? inputs.slice() : void 0,
821
+ outputEPCList: outputs.length ? outputs.map((o) => o.epc) : void 0,
822
+ transformationID: opts.transformationId,
823
+ readPoint: opts.readPoint,
824
+ bizLocation: opts.bizLocation ?? opts.readPoint,
825
+ bizTransactionList: opts.bizTransactionList
826
+ }));
827
+ }
828
+ /**
829
+ * containment 조립(EPCIS AggregationEvent ADD) — 자식들을 부모(용기)로 집約.
830
+ * consume 지정 시 자식이 컨테이너로 흡수되며 독립 아이템에서 이탈(dematerialize: ObjectEvent DELETE + 제거).
831
+ * 미지정 시 이벤트만(자식은 독립 아이템 유지 — 예: WMS 패킹, 적재된 채 도착).
832
+ */
833
+ aggregate(parent, children, opts) {
834
+ this.emit(aggregationEvent({ eventTime: this.now(), action: "ADD", bizStep: opts.bizStep, disposition: opts.disposition, parentID: parent, childEPCs: children.slice(), readPoint: opts.readPoint, bizLocation: opts.bizLocation }));
835
+ if (opts.consume) {
836
+ this.emit(objectEvent({ eventTime: this.now(), action: "DELETE", bizStep: opts.bizStep, disposition: opts.consume.disposition, epcList: children.slice(), readPoint: opts.consume.readPoint }));
837
+ for (const c of children) {
838
+ const it = this.items.get(c);
839
+ if (it) {
840
+ const n = this.nodes.get(it.location);
841
+ if (n) n.occupancy--;
842
+ this.items.delete(c);
843
+ }
844
+ }
845
+ }
846
+ }
847
+ /**
848
+ * containment 분해(EPCIS AggregationEvent DELETE) — 부모(용기)에서 자식들을 풀어냄.
849
+ * materialize 지정 시 자식이 독립 아이템으로 등장(materialize: 노드 배치 + occupancy + ObjectEvent ADD).
850
+ * 미지정 시 이벤트만.
851
+ */
852
+ disaggregate(parent, children, opts) {
853
+ this.emit(aggregationEvent({ eventTime: this.now(), action: "DELETE", bizStep: opts.bizStep, parentID: parent, childEPCs: children.slice(), readPoint: opts.readPoint }));
854
+ if (opts.materialize) {
855
+ const m = opts.materialize;
856
+ for (const c of children) {
857
+ this.items.set(c, { epc: c, location: m.location, disposition: m.disposition });
858
+ const n = this.nodes.get(m.location);
859
+ if (n) n.occupancy++;
860
+ this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: opts.bizStep, disposition: m.disposition, epcList: [c], readPoint: m.location, bizLocation: m.location }));
861
+ }
862
+ }
863
+ }
864
+ /** 품질 보고(OEE Quality) — 도메인이 완료 시 자원별 양품/불량 1건 계상(예: 용접 수율). */
865
+ recordOutput(moverId, good) {
866
+ const m = moverId ? this.movers.get(moverId) : void 0;
867
+ if (!m) return;
868
+ if (good) m.goodCount++;
869
+ else m.scrapCount++;
870
+ }
871
+ /** 무버 OEE(스냅샷 파생) — Availability×Performance×Quality. planned = 설비 존재 sim 시간(clockMs). */
872
+ oeeOf(m) {
873
+ const planned = this.clockMs;
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
+ };
891
+ }
892
+ /** 정책에 넘길 특정 타입 노드의 관측 뷰 — 예약(그 노드로 향하는 in-flight task) 포함. */
893
+ slotViews(nodeType) {
894
+ const reserved = /* @__PURE__ */ new Map();
895
+ for (const t of this.tasks.values()) if (t.status !== "completed") reserved.set(t.toNode, (reserved.get(t.toNode) ?? 0) + 1);
896
+ const views = [];
897
+ for (const n of this.nodes.values()) if (n.type === nodeType) views.push({ id: n.id, capacity: n.capacity, occupancy: n.occupancy, reserved: reserved.get(n.id) ?? 0 });
898
+ return views;
899
+ }
900
+ emit(event) {
901
+ this.revision++;
902
+ const e = { eventId: `${this.tenantId}-evt-${++this.eventSeq}`, eventType: `epcis.${event.type}`, eventTime: event.eventTime, tenantId: this.tenantId, data: event };
903
+ for (const h of this.handlers) h(e);
904
+ }
905
+ emitOp(eventType, data) {
906
+ this.revision++;
907
+ const e = { eventId: `${this.tenantId}-evt-${++this.eventSeq}`, eventType, eventTime: this.now(), tenantId: this.tenantId, data };
908
+ for (const h of this.handlers) h(e);
909
+ }
910
+ 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 });
912
+ }
913
+ emitMover(m, motion) {
914
+ this.emitOp(OP_EVENT.equipment, { moverId: m.id, kind: m.kind, status: m.status, location: m.location, motion });
915
+ }
916
+ emitOrder(o) {
917
+ this.emitOp(OP_EVENT.order, { orderId: o.id, kind: o.kind, status: o.status, requested: o.requested, fulfilled: o.fulfilled, held: o.held });
918
+ }
919
+ // ── 내부 mechanics ─────────────────────────────────────────────────────────
920
+ intervalMs(spec) {
921
+ const base = 36e5 / spec.rate.meanPerHour;
922
+ return spec.rate.distribution === "poisson" ? -Math.log(1 - this.rng()) * base : base;
923
+ }
924
+ /** 지수분포 표본(고장/수리 간격) — mean 을 평균으로 하는 무기억 프로세스. */
925
+ sampleExp(meanMs) {
926
+ return -Math.log(1 - this.rng()) * meanMs;
927
+ }
928
+ /**
929
+ * 확률적 설비 고장(MTBF/MTTR) — mtbf 지정 무버만 참여(미지정=고장 없음, rng 무소비 → byte-identical).
930
+ * up: nextFailure 도래 시 down(수리까지 repairUntil). down: downMs 누적, repair 도래 시 up(다음 고장 예약).
931
+ * down 중 무버는 배정 불가 + 진행중 task 동결(processTasks 가 skip) → OEE Availability 손실.
932
+ */
933
+ processFailures(dt) {
934
+ for (const m of this.movers.values()) {
935
+ if (m.mtbfMs === void 0) continue;
936
+ if (m.status === "down") {
937
+ m.downMs += dt;
938
+ if (this.clockMs >= (m.repairUntilMs ?? 0)) {
939
+ m.status = m.taskId ? "busy" : "idle";
940
+ m.repairUntilMs = void 0;
941
+ m.nextFailureMs = this.clockMs + this.sampleExp(m.mtbfMs);
942
+ this.emitMover(m);
943
+ }
944
+ } else if (this.clockMs >= (m.nextFailureMs ?? Infinity)) {
945
+ m.status = "down";
946
+ m.repairUntilMs = this.clockMs + this.sampleExp(m.mttrMs ?? m.mtbfMs);
947
+ m.nextFailureMs = void 0;
948
+ this.emitMover(m);
949
+ }
950
+ }
951
+ }
952
+ progressOf(t) {
953
+ return t.durationMs <= 0 ? 1 : Math.min(1, Math.max(0, (t.durationMs - t.remainingMs) / t.durationMs));
954
+ }
955
+ generate() {
956
+ for (const g of this.gens) {
957
+ while (this.clockMs >= g.nextMs) {
958
+ if (g.spec.kind === "inbound-arrival") this.onArrival(g.spec);
959
+ else if (g.spec.kind === "outbound-order") this.onOrder(g.spec);
960
+ g.nextMs += this.intervalMs(g.spec);
961
+ }
962
+ }
963
+ }
964
+ processOrders() {
965
+ for (const o of this.orders.values()) if (o.status === "created" && !o.held) this.allocate(o);
966
+ }
967
+ processTasks(dt) {
968
+ for (const t of this.tasks.values()) {
969
+ if (t.status !== "created") continue;
970
+ if (t.intent === "dwell") {
971
+ t.status = "in-progress";
972
+ t.remainingMs = t.durationMs;
973
+ this.emitTask(t);
974
+ continue;
975
+ }
976
+ const mover = [...this.movers.values()].find((m) => m.status === "idle" && (t.resourceType === void 0 || m.kind === t.resourceType));
977
+ if (!mover) continue;
978
+ if (t.setupMs && t.changeoverKey !== void 0 && mover.lastChangeoverKey !== void 0 && mover.lastChangeoverKey !== t.changeoverKey) {
979
+ t.appliedSetupMs = t.setupMs;
980
+ t.durationMs += t.setupMs;
981
+ }
982
+ if (t.changeoverKey !== void 0) mover.lastChangeoverKey = t.changeoverKey;
983
+ mover.status = "busy";
984
+ mover.taskId = t.id;
985
+ t.status = "in-progress";
986
+ t.resource = mover.id;
987
+ t.remainingMs = t.durationMs;
988
+ this.emitTask(t);
989
+ if (t.intent === "process") this.emitMover(mover);
990
+ else this.emitMover(mover, { fromNode: t.fromNode, toNode: t.toNode, startedAtSimMs: this.clockMs, durationMs: t.durationMs, progress: 0, elapsedMs: 0 });
991
+ }
992
+ for (const t of this.tasks.values()) {
993
+ if (t.status !== "in-progress") continue;
994
+ if (t.resource && this.movers.get(t.resource)?.status === "down") continue;
995
+ t.remainingMs -= dt;
996
+ if (t.remainingMs > 0) continue;
997
+ this.onTaskComplete(t);
998
+ t.status = "completed";
999
+ if (!t.resource) {
1000
+ this.emitTask(t);
1001
+ continue;
1002
+ }
1003
+ const mover = this.movers.get(t.resource);
1004
+ const setup = t.appliedSetupMs ?? 0;
1005
+ mover.setupMs += setup;
1006
+ mover.runMs += t.durationMs - setup;
1007
+ mover.status = "idle";
1008
+ mover.taskId = null;
1009
+ if (t.intent !== "process") mover.location = t.toNode;
1010
+ this.emitTask(t);
1011
+ this.emitMover(mover);
1012
+ }
1013
+ }
1014
+ };
1015
+
1016
+ // src/kernel.ts
1017
+ var TRAVEL_MS = 3e4;
1018
+ var COMPANY_PREFIX = "0614141";
1019
+ var SHELF_MS = 30 * 24 * 36e5;
1020
+ var SHELF_JITTER_MS = 5 * 24 * 36e5;
1021
+ var WmsKernel = class extends FlowEngine {
1022
+ poSeq = 0;
1023
+ constructor(tenantId, policy = firstFitPolicy) {
1024
+ super(tenantId, policy);
1025
+ }
1026
+ /** 입고 도착 — §4 라이프사이클: ASN(PO) → 팔레트 조립 → 수령 → putaway task. */
1027
+ onArrival(spec) {
1028
+ const dock = this.nodeByType("dock");
1029
+ if (!dock) return;
1030
+ const epc = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
1031
+ const gtin = this.pickGtin(spec.content.skuMix);
1032
+ const qty = this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max);
1033
+ const po = gdtiUri(COMPANY_PREFIX, "401", ++this.poSeq);
1034
+ const eventTime = this.now();
1035
+ const qtyList = [{ epcClass: gtin, quantity: qty }];
1036
+ const poTxn = [{ type: BTT.po, bizTransaction: po }];
1037
+ const expiry = this.clockMs + SHELF_MS - this.epcSeq % 5 * SHELF_JITTER_MS;
1038
+ this.items.set(epc, { epc, gtin, qty, location: dock.id, disposition: DISP.in_progress, expiry });
1039
+ dock.occupancy++;
1040
+ this.emit(transactionEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, bizTransactionList: poTxn, epcList: [epc], quantityList: qtyList, readPoint: dock.id }));
1041
+ this.emit(aggregationEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, parentID: epc, childQuantityList: qtyList, readPoint: dock.id }));
1042
+ this.emit(objectEvent({ eventTime, action: "ADD", bizStep: BIZSTEP.receiving, disposition: DISP.in_progress, epcList: [epc], quantityList: qtyList, readPoint: dock.id, bizLocation: dock.id, bizTransactionList: poTxn }));
1043
+ const binId = this.policy.selectPlacement({ item: { epc, gtin, qty }, slots: this.slotViews("storage") });
1044
+ if (!binId) return;
1045
+ const id = `task-${++this.taskSeq}`;
1046
+ const task = { id, kind: "putaway", status: "created", itemEpc: epc, fromNode: dock.id, toNode: binId, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "putaway", fromNode: dock.id, toNode: binId }, TRAVEL_MS) };
1047
+ this.tasks.set(id, task);
1048
+ this.emitTask(task);
1049
+ }
1050
+ /**
1051
+ * SO 오더 자극(생성기) → 오더 생성. `linesPerOrder` 지정 시 멀티SKU(라인별 distinct gtin),
1052
+ * 미지정 시 단일 SKU(기존 경로 — rng 소비 순서 보존 = pickGtin → randInt).
1053
+ */
1054
+ onOrder(spec) {
1055
+ const lpl = spec.content.linesPerOrder;
1056
+ if (!lpl) {
1057
+ this.createSalesOrder([{ gtin: this.pickGtin(spec.content.skuMix), qty: this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max) }]);
1058
+ return;
1059
+ }
1060
+ const n = Math.min(this.randInt(lpl.min, lpl.max), spec.content.skuMix.length);
1061
+ const lines = [];
1062
+ let pool = spec.content.skuMix;
1063
+ for (let i = 0; i < n && pool.length > 0; i++) {
1064
+ const gtin = this.pickGtin(pool);
1065
+ lines.push({ gtin, qty: this.randInt(spec.content.qtyPerLine.min, spec.content.qtyPerLine.max) });
1066
+ pool = pool.filter((m) => m.gtin !== gtin);
1067
+ }
1068
+ this.createSalesOrder(lines);
1069
+ }
1070
+ /** Command 채널 — 오더 즉시 투입(트랜잭션 프론트엔드/prescriptive). */
1071
+ handleCommand(cmd) {
1072
+ if (cmd.type === CMD.orderRelease) {
1073
+ const a = cmd.args;
1074
+ const lines = a?.lines ?? (a?.gtin ? [{ gtin: a.gtin, qty: a.qty ?? 1 }] : []);
1075
+ if (lines.length === 0) return { commandId: cmd.commandId, accepted: false, error: "order.release: gtin \uB610\uB294 lines \uD544\uC694" };
1076
+ this.createSalesOrder(lines);
1077
+ return { commandId: cmd.commandId, accepted: true };
1078
+ }
1079
+ return super.handleCommand(cmd);
1080
+ }
1081
+ createSalesOrder(lines) {
1082
+ const id = `order-${++this.orderSeq}`;
1083
+ const so = gdtiUri(COMPANY_PREFIX, "402", ++this.soSeq);
1084
+ const requested = lines.reduce((s, l) => s + l.qty, 0);
1085
+ const order = {
1086
+ id,
1087
+ kind: "outbound",
1088
+ status: "created",
1089
+ requested,
1090
+ fulfilled: 0,
1091
+ bizTransaction: so,
1092
+ allocated: [],
1093
+ picked: [],
1094
+ shipmentEpc: null,
1095
+ lines: lines.map((l) => ({ gtin: l.gtin, requested: l.qty }))
1096
+ };
1097
+ this.orders.set(id, order);
1098
+ this.emitOrder(order);
1099
+ }
1100
+ /**
1101
+ * created 오더를 정책으로 할당 → SO TransactionEvent(라인 통합) + pick task(라인별).
1102
+ * 라인마다 selectStock(정책) → firstFit=라인 전량확보 대기 / partialFit=가용분 백오더.
1103
+ * 멀티SKU 는 여러 라인의 팔레트를 한 오더로 모아 단일 출하(finalizeOrder 통합 화물).
1104
+ */
1105
+ allocate(o) {
1106
+ const staging = this.nodeByType("staging");
1107
+ if (!staging || !o.lines) return;
1108
+ const chosenAll = [];
1109
+ for (const line of o.lines) {
1110
+ const already = o.allocated.filter((e) => this.items.get(e)?.gtin === line.gtin).length;
1111
+ const need = line.requested - already;
1112
+ if (need <= 0) continue;
1113
+ const available = [...this.items.values()].filter((i) => i.gtin === line.gtin && i.disposition === DISP.sellable && this.nodes.get(i.location)?.type === "storage").map((i) => ({ epc: i.epc, location: i.location, qty: i.qty ?? 1, expiry: i.expiry }));
1114
+ const chosen = this.policy.selectStock({ gtin: line.gtin, qty: need, available });
1115
+ for (const epc of chosen) {
1116
+ this.items.get(epc).disposition = DISP.reserved;
1117
+ o.allocated.push(epc);
1118
+ chosenAll.push(epc);
1119
+ }
1120
+ }
1121
+ if (chosenAll.length === 0) return;
1122
+ this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: BIZSTEP.picking, bizTransactionList: [{ type: BTT.so, bizTransaction: o.bizTransaction }], epcList: chosenAll.slice() }));
1123
+ for (const epc of chosenAll) {
1124
+ const it = this.items.get(epc);
1125
+ const id = `task-${++this.taskSeq}`;
1126
+ const task = { id, kind: "pick", status: "created", itemEpc: epc, fromNode: it.location, toNode: staging.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "pick", fromNode: it.location, toNode: staging.id }, TRAVEL_MS), orderId: o.id };
1127
+ this.tasks.set(id, task);
1128
+ this.emitTask(task);
1129
+ }
1130
+ o.status = "picking";
1131
+ this.emitOrder(o);
1132
+ }
1133
+ /** 태스크 완료 — 이동 반영 후 putaway=storing, pick=picking(+전량 시 pack→stage→ship). */
1134
+ onTaskComplete(t) {
1135
+ const item = this.items.get(t.itemEpc);
1136
+ const from = this.nodes.get(t.fromNode);
1137
+ const to = this.nodes.get(t.toNode);
1138
+ from.occupancy--;
1139
+ to.occupancy++;
1140
+ item.location = to.id;
1141
+ if (t.kind === "putaway") {
1142
+ item.disposition = DISP.sellable;
1143
+ this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: BIZSTEP.storing, disposition: DISP.sellable, epcList: [item.epc], quantityList: [{ epcClass: item.gtin, quantity: item.qty }], readPoint: to.id, bizLocation: to.id }));
1144
+ return;
1145
+ }
1146
+ item.disposition = DISP.reserved;
1147
+ this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: BIZSTEP.picking, disposition: DISP.reserved, epcList: [item.epc], quantityList: [{ epcClass: item.gtin, quantity: item.qty }], readPoint: to.id, bizLocation: to.id }));
1148
+ const order = t.orderId ? this.orders.get(t.orderId) : void 0;
1149
+ if (!order) return;
1150
+ order.picked.push(item.epc);
1151
+ if (order.picked.length === order.allocated.length) this.finalizeOrder(order, to);
1152
+ }
1153
+ /** 전량 피킹 → packing(조립)·staging·shipping 마감. 화물 사이트 이탈, 백오더 잔량 재할당. */
1154
+ finalizeOrder(order, staging) {
1155
+ const shipDock = this.nodeByType("dock-ship") ?? staging;
1156
+ const eventTime = this.now();
1157
+ const shipment = ssccUri(COMPANY_PREFIX, ++this.epcSeq);
1158
+ order.shipmentEpc = shipment;
1159
+ const soTxn = [{ type: BTT.so, bizTransaction: order.bizTransaction }];
1160
+ this.aggregate(shipment, order.picked.slice(), { bizStep: BIZSTEP.packing, readPoint: staging.id, bizLocation: staging.id });
1161
+ order.status = "packed";
1162
+ this.emit(objectEvent({ eventTime, action: "OBSERVE", bizStep: BIZSTEP.staging_outbound, disposition: DISP.reserved, epcList: [shipment], readPoint: staging.id, bizLocation: staging.id, bizTransactionList: soTxn }));
1163
+ this.emit(objectEvent({ eventTime, action: "DELETE", bizStep: BIZSTEP.shipping, disposition: DISP.in_transit, epcList: [shipment], readPoint: shipDock.id, bizTransactionList: soTxn }));
1164
+ order.fulfilled += order.allocated.length;
1165
+ if (order.lines) for (const epc of order.picked) {
1166
+ const g = this.items.get(epc)?.gtin;
1167
+ const line = order.lines.find((l) => l.gtin === g && l.requested > 0);
1168
+ if (line) line.requested--;
1169
+ }
1170
+ for (const epc of order.picked) {
1171
+ this.items.delete(epc);
1172
+ staging.occupancy--;
1173
+ }
1174
+ if (order.fulfilled >= order.requested) order.status = "shipped";
1175
+ else {
1176
+ order.allocated = [];
1177
+ order.picked = [];
1178
+ order.status = "created";
1179
+ }
1180
+ this.emitOrder(order);
1181
+ }
1182
+ };
1183
+
1184
+ // src/yms-kernel.ts
1185
+ var TRAVEL_MS2 = 2e4;
1186
+ var DWELL_MS = 3e4;
1187
+ var WINDOW_DELAY_MS = 4e4;
1188
+ var CARGO_PER_TRAILER = 2;
1189
+ var CP = "0614141";
1190
+ var YmsKernel = class extends FlowEngine {
1191
+ doorRR = 0;
1192
+ cargoSeq = 0;
1193
+ trailerCargo = /* @__PURE__ */ new Map();
1194
+ // 트레일러 epc → 화물 SSCC(하차 전 opaque / 상차 예약분)
1195
+ apptMode = /* @__PURE__ */ new Map();
1196
+ // orderId → 상하차 모드(생성 시 캡처)
1197
+ /** 야드 운영 정책(상하차 방식). 기본 drop(야드 버퍼). live 는 게이트/도크 직행. */
1198
+ mode;
1199
+ constructor(tenantId, policy = firstFitPolicy, mode = "drop") {
1200
+ super(tenantId, policy);
1201
+ this.mode = mode;
1202
+ }
1203
+ /** 인바운드(하차) 자극 — 화물 적재된 트레일러 도착. */
1204
+ onArrival(_spec) {
1205
+ this.spawnAppointment("appointment");
1206
+ }
1207
+ /** 아웃바운드(상차) 자극 — 빈 트레일러 도착(도크에서 staging 화물 적재 예정). */
1208
+ onOrder(_spec) {
1209
+ this.spawnAppointment("appointment-out");
1210
+ }
1211
+ /** 게이트-인 — 트레일러 도착 → 어포인트먼트(도크도어 배정 + 창). drop 은 야드 주차 태스크, live 는 게이트 대기. */
1212
+ spawnAppointment(kind) {
1213
+ const gate = this.nodeByType("gate");
1214
+ const doors = [...this.nodes.values()].filter((n) => n.type === "dock-door").sort((a, b) => a.id.localeCompare(b.id));
1215
+ if (!gate || doors.length === 0) return;
1216
+ const inbound = kind === "appointment";
1217
+ const epc = graiUri(CP, "10", ++this.epcSeq);
1218
+ this.items.set(epc, { epc, location: gate.id, disposition: DISP.in_progress });
1219
+ gate.occupancy++;
1220
+ this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: YARD_BIZSTEP.arriving, disposition: DISP.in_progress, epcList: [epc], readPoint: gate.id, bizLocation: gate.id }));
1221
+ if (inbound) {
1222
+ const cargo = Array.from({ length: CARGO_PER_TRAILER }, () => ssccUri(CP, ++this.cargoSeq));
1223
+ this.trailerCargo.set(epc, cargo);
1224
+ this.aggregate(epc, cargo, { bizStep: YARD_BIZSTEP.arriving, readPoint: gate.id });
1225
+ }
1226
+ const door = doors[this.doorRR++ % doors.length];
1227
+ const id = `order-${++this.orderSeq}`;
1228
+ const appt = gdtiUri(CP, "404", ++this.soSeq);
1229
+ const order = { id, kind, status: "created", requested: 1, fulfilled: 0, bizTransaction: appt, allocated: [epc], picked: [], dockDoor: door.id, windowStartMs: this.clockMs + WINDOW_DELAY_MS };
1230
+ this.orders.set(id, order);
1231
+ this.apptMode.set(id, this.mode);
1232
+ this.emitOrder(order);
1233
+ if (this.mode === "drop") {
1234
+ const slotId = this.policy.selectPlacement({ item: { epc, gtin: "trailer", qty: 1 }, slots: this.slotViews("yard-slot") });
1235
+ if (!slotId) return;
1236
+ const task = { id: `task-${++this.taskSeq}`, kind: "spot", status: "created", itemEpc: epc, fromNode: gate.id, toNode: slotId, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "spot", fromNode: gate.id, toNode: slotId }, TRAVEL_MS2) };
1237
+ this.tasks.set(task.id, task);
1238
+ this.emitTask(task);
1239
+ }
1240
+ }
1241
+ /**
1242
+ * 스케줄 게이트(base 가 created 오더마다 매 tick 호출) — 조건 충족 시 도크로 이동:
1243
+ * ① 창 도래(clock ≥ windowStart) ② 배정 도크도어 예약 가능
1244
+ * ③ drop: 트레일러가 야드 주차됨 / live: 게이트 대기 중 ④ 아웃바운드: staging 적재 화물 충분
1245
+ * 하나라도 안 되면 대기(다음 tick 재시도). = 시간창 예약 스케줄링.
1246
+ */
1247
+ allocate(o) {
1248
+ const trailer = this.items.get(o.allocated[0]);
1249
+ if (!trailer) return;
1250
+ if (this.clockMs < (o.windowStartMs ?? 0)) return;
1251
+ const door = this.nodes.get(o.dockDoor);
1252
+ const inflight = [...this.tasks.values()].some((t) => t.status !== "completed" && t.toNode === door.id);
1253
+ if (door.occupancy > 0 || inflight) return;
1254
+ const mode = this.apptMode.get(o.id) ?? "drop";
1255
+ if (mode === "drop" && this.nodes.get(trailer.location)?.type !== "yard-slot") return;
1256
+ const outbound = o.kind === "appointment-out";
1257
+ const bizStep = outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading;
1258
+ if (outbound) {
1259
+ const staging = this.nodeByType("staging");
1260
+ const reserved = new Set([...this.trailerCargo.values()].flat());
1261
+ const avail = [...this.items.values()].filter((i) => i.location === staging?.id && i.disposition === DISP.sellable && !reserved.has(i.epc)).sort((a, b) => a.epc.localeCompare(b.epc));
1262
+ if (!staging || avail.length < CARGO_PER_TRAILER) return;
1263
+ this.trailerCargo.set(trailer.epc, avail.slice(0, CARGO_PER_TRAILER).map((i) => i.epc));
1264
+ }
1265
+ trailer.disposition = DISP.reserved;
1266
+ this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep, bizTransactionList: [{ type: BTT_DELIVERY, bizTransaction: o.bizTransaction }], epcList: [trailer.epc], readPoint: door.id }));
1267
+ const dockKind = mode === "drop" ? "pull" : "spot-live";
1268
+ const task = { id: `task-${++this.taskSeq}`, kind: dockKind, status: "created", itemEpc: trailer.epc, fromNode: trailer.location, toNode: door.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: dockKind, fromNode: trailer.location, toNode: door.id }, TRAVEL_MS2), orderId: o.id };
1269
+ this.tasks.set(task.id, task);
1270
+ this.emitTask(task);
1271
+ o.status = "docking";
1272
+ this.emitOrder(o);
1273
+ }
1274
+ /** 태스크 완료 — 목적지 타입으로 분기: yard-slot=주차, dock-door=상/하차+depart, gate=출차. */
1275
+ onTaskComplete(t) {
1276
+ const trailer = this.items.get(t.itemEpc);
1277
+ const from = this.nodes.get(t.fromNode);
1278
+ const to = this.nodes.get(t.toNode);
1279
+ from.occupancy--;
1280
+ to.occupancy++;
1281
+ trailer.location = to.id;
1282
+ if (t.kind === "dwell") {
1283
+ const gate = this.nodeByType("gate");
1284
+ const dep = { id: `task-${++this.taskSeq}`, kind: "depart", status: "created", itemEpc: trailer.epc, fromNode: t.toNode, toNode: gate.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "depart", fromNode: t.toNode, toNode: gate.id }, TRAVEL_MS2), orderId: t.orderId };
1285
+ this.tasks.set(dep.id, dep);
1286
+ this.emitTask(dep);
1287
+ return;
1288
+ }
1289
+ if (to.type === "yard-slot") {
1290
+ trailer.disposition = DISP.sellable;
1291
+ this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: YARD_BIZSTEP.staging, disposition: DISP.sellable, epcList: [trailer.epc], readPoint: to.id, bizLocation: to.id }));
1292
+ return;
1293
+ }
1294
+ if (to.type === "dock-door") {
1295
+ const order2 = t.orderId ? this.orders.get(t.orderId) : void 0;
1296
+ const outbound = order2?.kind === "appointment-out";
1297
+ const staging = this.nodeByType("staging");
1298
+ const cargo = this.trailerCargo.get(trailer.epc) ?? [];
1299
+ this.emit(objectEvent({ eventTime: this.now(), action: "OBSERVE", bizStep: outbound ? YARD_BIZSTEP.loading : YARD_BIZSTEP.unloading, disposition: DISP.in_progress, epcList: [trailer.epc], readPoint: to.id, bizLocation: to.id }));
1300
+ if (outbound) {
1301
+ if (cargo.length && staging) {
1302
+ this.aggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.loading, readPoint: to.id, consume: { readPoint: staging.id, disposition: DISP.in_transit } });
1303
+ }
1304
+ this.trailerCargo.delete(trailer.epc);
1305
+ trailer.disposition = DISP.sellable;
1306
+ } else if (cargo.length && staging) {
1307
+ this.disaggregate(trailer.epc, cargo, { bizStep: YARD_BIZSTEP.unloading, readPoint: to.id, materialize: { location: staging.id, disposition: DISP.sellable } });
1308
+ this.trailerCargo.delete(trailer.epc);
1309
+ }
1310
+ const dwell = { id: `task-${++this.taskSeq}`, kind: "dwell", status: "created", itemEpc: trailer.epc, fromNode: to.id, toNode: to.id, resource: null, remainingMs: 0, durationMs: this.durationOf({ kind: "dwell", fromNode: to.id, toNode: to.id }, DWELL_MS), orderId: t.orderId, intent: "dwell" };
1311
+ this.tasks.set(dwell.id, dwell);
1312
+ this.emitTask(dwell);
1313
+ if (order2) {
1314
+ order2.status = "dwelling";
1315
+ this.emitOrder(order2);
1316
+ }
1317
+ return;
1318
+ }
1319
+ this.emit(objectEvent({ eventTime: this.now(), action: "DELETE", bizStep: YARD_BIZSTEP.departing, disposition: DISP.in_transit, epcList: [trailer.epc], readPoint: to.id }));
1320
+ this.items.delete(trailer.epc);
1321
+ to.occupancy--;
1322
+ const order = t.orderId ? this.orders.get(t.orderId) : void 0;
1323
+ if (order) {
1324
+ order.fulfilled = 1;
1325
+ order.status = "departed";
1326
+ this.emitOrder(order);
1327
+ }
1328
+ }
1329
+ };
1330
+
1331
+ // src/mes-kernel.ts
1332
+ var CYCLE_MS = 4e4;
1333
+ var SETUP_MS = 15e3;
1334
+ var CP2 = "0614141";
1335
+ var WIP_ITEMREF = "066666";
1336
+ var WIP_GTIN = sgtinClass(CP2, WIP_ITEMREF);
1337
+ var YIELD = 0.8;
1338
+ var PART_A = { itemRef: "055551", gtin: sgtinClass(CP2, "055551") };
1339
+ var PART_B = { itemRef: "055552", gtin: sgtinClass(CP2, "055552") };
1340
+ var PRODUCTS = [
1341
+ { key: "P1", ref: "077777", gtin: sgtinClass(CP2, "077777"), bom: [{ part: PART_A, qty: 2 }, { part: PART_B, qty: 1 }] },
1342
+ { key: "P2", ref: "077778", gtin: sgtinClass(CP2, "077778"), bom: [{ part: PART_A, qty: 1 }, { part: PART_B, qty: 2 }] }
1343
+ ];
1344
+ var MES_PART_GTINS = { partA: PART_A.gtin, partB: PART_B.gtin };
1345
+ var MES_PRODUCT_GTINS = { p1: PRODUCTS[0].gtin, p2: PRODUCTS[1].gtin };
1346
+ var MesKernel = class extends FlowEngine {
1347
+ wipSeq = 0;
1348
+ prodSeq = 0;
1349
+ constructor(tenantId, policy = firstFitPolicy) {
1350
+ super(tenantId, policy);
1351
+ }
1352
+ productOf(gtin) {
1353
+ return PRODUCTS.find((p) => p.gtin === gtin);
1354
+ }
1355
+ /** 부품 수령(다품종) — skuMix 의 gtin 으로 부품 종류 결정. */
1356
+ onArrival(spec) {
1357
+ const rawStore = this.nodeByType("raw-store");
1358
+ if (!rawStore) return;
1359
+ const gtin = this.pickGtin(spec.content.skuMix);
1360
+ const part = [PART_A, PART_B].find((p) => p.gtin === gtin);
1361
+ if (!part) return;
1362
+ const epc = sgtinUri(CP2, part.itemRef, ++this.epcSeq);
1363
+ this.items.set(epc, { epc, gtin: part.gtin, qty: 1, location: rawStore.id, disposition: DISP.sellable });
1364
+ rawStore.occupancy++;
1365
+ this.emit(objectEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.receiving, disposition: DISP.sellable, epcList: [epc], quantityList: [{ epcClass: part.gtin, quantity: 1 }], readPoint: rawStore.id, bizLocation: rawStore.id }));
1366
+ }
1367
+ /** 작업지시 — 제품 2종 교대(체인지오버 유발). 제품 gtin 을 오더에 기록. */
1368
+ onOrder(_spec) {
1369
+ const product = PRODUCTS[this.orderSeq % PRODUCTS.length];
1370
+ const id = `order-${++this.orderSeq}`;
1371
+ const wo = gdtiUri(CP2, "403", ++this.soSeq);
1372
+ const order = { id, kind: "workorder", status: "created", gtin: product.gtin, requested: 1, fulfilled: 0, bizTransaction: wo, allocated: [], picked: [] };
1373
+ this.orders.set(id, order);
1374
+ this.emitOrder(order);
1375
+ }
1376
+ /** 할당 — 제품 BOM 각 라인의 부품 예약(하나라도 부족하면 대기) + op1(cut, 체인지오버 셋업). */
1377
+ allocate(o) {
1378
+ const cut = this.nodeByType("cut-station");
1379
+ const product = this.productOf(o.gtin);
1380
+ if (!cut || !product) return;
1381
+ const picks = [];
1382
+ for (const line of product.bom) {
1383
+ 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 }));
1384
+ const chosen = this.policy.selectStock({ gtin: line.part.gtin, qty: line.qty, available });
1385
+ if (chosen.length < line.qty) return;
1386
+ picks.push(...chosen);
1387
+ }
1388
+ for (const epc of picks) {
1389
+ this.items.get(epc).disposition = DISP.reserved;
1390
+ o.allocated.push(epc);
1391
+ }
1392
+ this.emit(transactionEvent({ eventTime: this.now(), action: "ADD", bizStep: MES_BIZSTEP.producing, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: o.bizTransaction }], epcList: o.allocated.slice() }));
1393
+ 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" };
1394
+ this.tasks.set(task.id, task);
1395
+ this.emitTask(task);
1396
+ o.status = "op-cut";
1397
+ this.emitOrder(o);
1398
+ }
1399
+ /** op 완료 = 변환. cut: BOM 부품 소비 → WIP. weld: WIP → 완제품(수율: 양품/불량 → OEE 품질 보고). */
1400
+ onTaskComplete(t) {
1401
+ const order = this.orders.get(t.orderId);
1402
+ const product = this.productOf(order.gtin);
1403
+ const eventTime = this.now();
1404
+ if (t.kind === "cut") {
1405
+ const cut = this.nodes.get(t.toNode);
1406
+ const weld2 = this.nodeByType("weld-station");
1407
+ const inputs = order.allocated.slice();
1408
+ const wip2 = sgtinUri(CP2, WIP_ITEMREF, ++this.wipSeq);
1409
+ 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 }] });
1410
+ order.allocated = [wip2];
1411
+ 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" };
1412
+ this.tasks.set(wtask.id, wtask);
1413
+ this.emitTask(wtask);
1414
+ order.status = "op-weld";
1415
+ this.emitOrder(order);
1416
+ return;
1417
+ }
1418
+ const weld = this.nodes.get(t.toNode);
1419
+ const fgStore = this.nodeByType("fg-store");
1420
+ const wip = order.allocated[0];
1421
+ const good = this.rng() < YIELD;
1422
+ this.recordOutput(t.resource, good);
1423
+ const disp = good ? DISP.sellable : DISP.non_sellable;
1424
+ 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: weld.id, bizTransactionList: [{ type: BTT_PRODORDER, bizTransaction: order.bizTransaction }] });
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 }));
1427
+ order.allocated = [];
1428
+ order.fulfilled = 1;
1429
+ order.status = good ? "produced" : "scrapped";
1430
+ this.emitOrder(order);
1431
+ }
1432
+ };
1433
+ // Annotate the CommonJS export names for ESM import in node:
1434
+ 0 && (module.exports = {
1435
+ BIZSTEP,
1436
+ BTT,
1437
+ BTT_DELIVERY,
1438
+ BTT_PRODORDER,
1439
+ CMD,
1440
+ DISP,
1441
+ EPCIS_CONTEXT,
1442
+ EventJournal,
1443
+ FlowEngine,
1444
+ MES_BIZSTEP,
1445
+ MES_PART_GTINS,
1446
+ MES_PRODUCT_GTINS,
1447
+ MesKernel,
1448
+ OP_EVENT,
1449
+ StateProjector,
1450
+ TwinHistory,
1451
+ TwinObserver,
1452
+ TwinRuntime,
1453
+ UTC_OFFSET,
1454
+ WmsKernel,
1455
+ YARD_BIZSTEP,
1456
+ YmsKernel,
1457
+ aggregationEvent,
1458
+ compareStates,
1459
+ constantDuration,
1460
+ counterfactualAt,
1461
+ fefoPolicy,
1462
+ firstFitPolicy,
1463
+ gdtiUri,
1464
+ graiUri,
1465
+ ingest,
1466
+ mapRecord,
1467
+ monteCarloForecast,
1468
+ objectEvent,
1469
+ partialFitPolicy,
1470
+ replay,
1471
+ sgtinClass,
1472
+ sgtinUri,
1473
+ ssccUri,
1474
+ transactionEvent,
1475
+ transformationEvent,
1476
+ validateEpcisEvent
1477
+ });